A long CGPath does not render - ios

I work with a spritekit engine and faced with the following problem. I need to draw a route on a map, for this I make an array of points, and create a path for the array with CGPathAddLines method. Everything worked fine until I tried to build a route between two distant points on the map. The path of it route not rendered.
I began to deal with the problem. I get the bounding box of my path with CGPathGetBoundingBox and I noticed that the route is not drawn each time width of it rect more than 2005 px. I know it sounds strange but in my case it really is. Below that part of the code which is used to create and display the path:
var pathToDraw = CGPathCreateMutable()
let pathPoints = generatePathPoints()
CGPathAddLines(pathToDraw, nil, pathPoints, pathPoints.count)
var shapeNode = SKShapeNode()
shapeNode.path = pathToDraw
shapeNode.lineWidth = 10
shapeNode.strokeColor = UIColor.blueColor()
var effectNode = SKEffectNode()
effectNode.addChild(shapeNode)
mapNode.addChild(effectNode)
generatePathPoints - function return [CGPoint]
mapNode - object of SKSpriteNode type
Maybe I'm doing something wrong or is this a restriction of CGPath which I do not know?

If it works well for short paths and then stops working for long ones, I'll assume (as #matt did) that it's because a limitation of SKShapeNode.
Have you tried creating more than one SKShapeNode with shorter paths and putting all of them inside another SKNode? With this solution you could work with the parent SKNode (full of smaller SKShapeNodes) as if it were your bigger SKShapeNode but (I hope) all the shorter paths will render correctly...

Related

Box2D: How to use b2ChainShape for a tile based map with squares

Im fighting here with the so called ghost collisions on a simple tile based map with a circle as player character.
When applying an impulse to the circle it first starts bouncing correctly, then sooner or later it bounces wrong (wrong angle).
Looking up on the internet i read about an issue in Box2D (i use iOS Swift with Box2d port for Swift).
Using b2ChainShape does not help, but it looks i misunderstood it. I also need to use the "prevVertex" and "nextVertex" properties to set up the ghost vertices.
But im confused. I have a simple map made up of boxes (simple square), all placed next to each other forming a closed room. Inside of it my circle i apply an impulse seeing the issue.
Now WHERE to place those ghost vertices for each square/box i placed on the view in order to solve this issue? Do i need to place ANY vertex close to the last and first vertice of chainShape or does it need to be one of the vertices of the next box to the current one? I dont understand. Box2D's manual does not explain where these ghost vertices coordinates are coming from.
Below you can see an image describing the problem.
Some code showing the physics parts for the walls and the circle:
First the wall part:
let bodyDef = b2BodyDef()
bodyDef.position = self.ptm_vec(node.position+self.offset)
let w = self.ptm(Constants.Config.wallsize)
let square = b2ChainShape()
var chains = [b2Vec2]()
chains.append(b2Vec2(-w/2,-w/2))
chains.append(b2Vec2(-w/2,w/2))
chains.append(b2Vec2(w/2,w/2))
chains.append(b2Vec2(w/2,-w/2))
square.createLoop(vertices: chains)
let fixtureDef = b2FixtureDef()
fixtureDef.shape = square
fixtureDef.filter.categoryBits = Constants.Config.PhysicsCategory.Wall
fixtureDef.filter.maskBits = Constants.Config.PhysicsCategory.Player
let wallBody = self.world.createBody(bodyDef)
wallBody.createFixture(fixtureDef)
The circle part:
let bodyDef = b2BodyDef()
bodyDef.type = b2BodyType.dynamicBody
bodyDef.position = self.ptm_vec(node.position+self.offset)
let circle = b2CircleShape()
circle.radius = self.ptm(Constants.Config.playersize)
let fixtureDef = b2FixtureDef()
fixtureDef.shape = circle
fixtureDef.density = 0.3
fixtureDef.friction = 0
fixtureDef.restitution = 1.0
fixtureDef.filter.categoryBits = Constants.Config.PhysicsCategory.Player
fixtureDef.filter.maskBits = Constants.Config.PhysicsCategory.Wall
let ballBody = self.world.createBody(bodyDef)
ballBody.linearDamping = 0
ballBody.angularDamping = 0
ballBody.createFixture(fixtureDef)
Not sure that I know of a simple solution in the case that each tile can potentially have different physics.
If your walls are all horizontal and/or vertical, you could write a class to take a row of boxes, create a single edge or rectangle body, and then on collision calculate which box (a simple a < x < b test) should interact with the colliding object, and apply the physics appropriately, manually calling the OnCollision method that you would otherwise specify as the callback for each individual box.
Alternatively, to avoid the trouble of manually testing intersection with different boxes, you could still merge all common straight edge boxes into one edge body for accurate reflections. However, you would still retain the bodies for the individual boxes. Extend the boxes so that they overlap the edge.
Now here's the trick: all box collision handlers return false, but they toggle flags on the colliding object (turning flags on OnCollision, and off OnSeparation). The OnCollision method for the edge body then processes the collision based on which flags are set.
Just make sure that the colliding body always passes through a box before it can touch an edge. That should be straightforward.

Need serious help on creating a comet with animations in SpriteKit

I'm currently working on a SpriteKit project and need to create a comet with a fading tail that animates across the screen. I am having serious issues with SpriteKit in this regards.
Attempt 1. It:
Draws a CGPath and creates an SKShapeNode from the path
Creates a square SKShapeNode with gradient
Creates an SKCropNode and assigns its maskNode as line, and adds square as a child
Animates the square across the screen, while being clipped by the line/SKCropNode
func makeCometInPosition(from: CGPoint, to: CGPoint, color: UIColor, timeInterval: NSTimeInterval) {
... (...s are (definitely) irrelevant lines of code)
let path = CGPathCreateMutable()
...
let line = SKShapeNode(path:path)
line.lineWidth = 1.0
line.glowWidth = 1.0
var squareFrame = line.frame
...
let square = SKShapeNode(rect: squareFrame)
//Custom SKTexture Extension. I've tried adding a normal image and the leak happens either way. The extension is not the problem
square.fillTexture = SKTexture(color1: UIColor.clearColor(), color2: color, from: from, to: to, frame: line.frame)
square.fillColor = color
square.strokeColor = UIColor.clearColor()
square.zPosition = 1.0
let maskNode = SKCropNode()
maskNode.zPosition = 1.0
maskNode.maskNode = line
maskNode.addChild(square)
//self is an SKScene, background is an SKSpriteNode
self.background?.addChild(maskNode)
let lineSequence = SKAction.sequence([SKAction.waitForDuration(timeInterval), SKAction.removeFromParent()])
let squareSequence = SKAction.sequence([SKAction.waitForDuration(1), SKAction.moveBy(CoreGraphics.CGVectorMake(deltaX * 2, deltaY * 2), duration: timeInterval), SKAction.removeFromParent()])
square.runAction(SKAction.repeatActionForever(squareSequence))
maskNode.runAction(lineSequence)
line.runAction(lineSequence)
}
This works, as shown below.
The problem is that after 20-40 other nodes come on the screen, weird things happen. Some of the nodes on the screen disappear, some stay. Also, the fps and node count (toggled in the SKView and never changed)
self.showsFPS = true
self.showsNodeCount = true
disappear from the screen. This makes me assume it's a bug with SpriteKit. SKShapeNode has been known to cause issues.
Attempt 2. I tried changing square from an SKShapeNode to an SKSpriteNode (Adding and removing lines related to the two as necessary)
let tex = SKTexture(color1: UIColor.clearColor(), color2: color, from: from, to: to, frame: line.frame)
let square = SKSpriteNode(texture: tex)
the rest of the code is basically identical. This produces a similar effect with no bugs performance/memory wise. However, something odd happens with SKCropNode and it looks like this
It has no antialiasing, and the line is thicker. I have tried changing anti-aliasing, glow width, and line width. There is a minimum width that can not change for some reason, and setting the glow width larger does this
. According to other stackoverflow questions maskNodes are either 1 or 0 in alpha. This is confusing since the SKShapeNode can have different line/glow widths.
Attempt 3. After some research, I discovered I might be able to use the clipping effect and preserve line width/glow using an SKEffectNode instead of SKCropNode.
//Not the exact code to what I tried, but very similar
let maskNode = SKEffectNode()
maskNode.filter = customLinearImageFilter
maskNode.addChild(line)
This produced the (literally) exact same effect as attempt 1. It created the same lines and animation, but the same bugs with other nodes/fps/nodeCount occured. So it seems to be a bug with SKEffectNode, and not SKShapeNode.
I do not know how to bypass the bugs with attempt 1/3 or 2.
Does anybody know if there is something I am doing wrong, if there is a bypass around this, or a different solution altogether for my problem?
Edit: I considered emitters, but there could potentially be hundreds of comets/other nodes coming in within a few seconds and didn't think they would be feasible performance-wise. I have not used SpriteKit before this project so correct me if I am wrong.
This looks like a problem for a custom shader attached to the comet path. If you are not familiar with OpenGL Shading Language (GLSL) in SpriteKit it lets you jump right into the GPU fragment shader specifically to control the drawing behavior of the nodes it is attached to via SKShader.
Conveniently the SKShapeNode has a strokeShader property for hooking up an SKShader to draw the path. When connected to this property the shader gets passed the length of the path and the point on the path currently being drawn in addition to the color value at that point.*
controlFadePath.fsh
void main() {
//uniforms and varyings
vec4 inColor = v_color_mix;
float length = u_path_length;
float distance = v_path_distance;
float start = u_start;
float end = u_end;
float mult;
mult = smoothstep(end,start,distance/length);
if(distance/length > start) {discard;}
gl_FragColor = vec4(inColor.r, inColor.g, inColor.b, inColor.a) * mult;
}
To control the fade along the path pass a start and end point into the custom shader using two SKUniform objects named u_start and u_end These get added to the custom shader during initialization of a custom SKShapeNode class CometPathShape and animated via a custom Action.
class CometPathShape:SKShapeNode
class CometPathShape:SKShapeNode {
//custom shader for fading
let pathShader:SKShader
let fadeStartU = SKUniform(name: "u_start",float:0.0)
let fadeEndU = SKUniform(name: "u_end",float: 0.0)
let fadeAction:SKAction
override init() {
pathShader = SKShader(fileNamed: "controlFadePath.fsh")
let fadeDuration:NSTimeInterval = 1.52
fadeAction = SKAction.customActionWithDuration(fadeDuration, actionBlock:
{ (node:SKNode, time:CGFloat)->Void in
let D = CGFloat(fadeDuration)
let t = time/D
var Ps:CGFloat = 0.0
var Pe:CGFloat = 0.0
Ps = 0.25 + (t*1.55)
Pe = (t*1.5)-0.25
let comet:CometPathShape = node as! CometPathShape
comet.fadeRange(Ps,to: Pe) })
super.init()
path = makeComet...(...) //custom method that creates path for comet shape
strokeShader = pathShader
pathShader.addUniform(fadeStartU)
pathShader.addUniform(fadeEndU)
hidden = true
//set up for path shape, eg. strokeColor, strokeWidth...
...
}
func fadeRange(from:CGFloat, to:CGFloat) {
fadeStartU.floatValue = Float(from)
fadeEndU.floatValue = Float(to)
}
func launch() {
hidden = false
runAction(fadeAction, completion: { ()->Void in self.hidden = true;})
}
...
The SKScene initializes the CometPathShape objects, caches and adds them to the scene. During update: the scene simply calls .launch() on the chosen CometPathShapes.
class GameScene:SKScene
...
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.name = "theScene"
...
//create a big bunch of paths with custom shaders
print("making cache of path shape nodes")
for i in 0...shapeCount {
let shape = CometPathShape()
let ext = String(i)
shape.name = "comet_".stringByAppendingString(ext)
comets.append(shape)
shape.position.y = CGFloat(i * 3)
print(shape.name)
self.addChild(shape)
}
override func update(currentTime: CFTimeInterval) {
//pull from cache and launch comets, skip busy ones
for _ in 1...launchCount {
let shape = self.comets[Int(arc4random_uniform(UInt32(shapeCount)))]
if shape.hasActions() { continue }
shape.launch()
}
}
This cuts the number of SKNodes per comet from 3 to 1 simplifying your code and the runtime environment and it opens the door for much more complex effects via the shader. The only drawback I can see is having to learn some GLSL.**
*not always correctly in the device simulator. Simulator not passing distance and length values to custom shader.
**that and some idiosyncrasies in CGPath glsl behavior. Path construction is affecting the way the fade performs. Looks like v_path_distance is not blending smoothly across curve segments. Still, with care constructing the curve this should work.

Creating an SKShapeNode from CGPoint array that starts and ends on the edges of the screen rect

Okay, so I am working on my first iOS game using SpriteKit and Swift, and I am having a hard time finding a good way to accomplish this one task. I am trying to figure out how to take a CGPoint array that basically contains a user-made path going from one edge of the screen to another. I want to figure out the best way to fill in this space, perhaps with an SKShapeNode but maybe theres a better way. One important condition is to determine which side of path will get filled, and this should be determined based on whether the ball in the game is in that area; I want to make it so that the area where there is no ball gets filled in. I was thinking of making an SKShapeNode so I could check if the current position of the ball is inside that node, but I realized I'm not sure how to figure out how to set up the node so that it is put against the edges of the screen. The only thing I can think of is adding the screen corners but it would probably get really messed up if I added all of them, so I want to know what the best way to do this would be. I started writing this function but just got stuck and have no idea what to do now:
func fillInPolgyon(contactPoint: CGPoint) {
var frameContactPoint = contactPoint
if contactPoint.x < 3 && contactPoint.x > -3 {
frameContactPoint.x = 0
}
else if contactPoint.y < 3 && contactPoint.y > -3 {
frameContactPoint.y = 0
}
//println("ship hit bounds: \(frameContactPoint)")
var polygonPath = UIBezierPath(CGPath: createTrailPath()!)
polygonPath.addLineToPoint(frameContactPoint)
polygonPath.closePath()
var polygonPathCG = polygonPath.CGPath
var ballLocation = ball.position
if !polygonPath.containsPoint(ballLocation) {
let filledPolygonNode = SKShapeNode(path: polygonPathCG)
filledPolygonNode.name = FilledPolygonName
filledPolygonNode.fillColor = UIColor(red: 0, green: 1, blue: 0, alpha: 1)
addChild(filledPolygonNode)
filledPolygonNode.physicsBody = SKPhysicsBody(edgeChainFromPath: polygonPathCG)
filledPolygonNode.physicsBody!.categoryBitMask = FilledPolygon
filledPolygonNode.physicsBody!.collisionBitMask = ShipCategory | BallCategory
filledPolygonNode.physicsBody!.dynamic = false
var trailPoints: [CGPoint] = [contactPoint]
}
}
Clearly what I have here sucks... it doesn't fill out the corners of the screen and also, even though I have it checking to make sure it doesn't have the same contact point passed through it (this function is called whenever the path hits the walls of the screen through the didBeginContact function), but the points will be slightly different anyways, resulting in the creation of way too many unwanted nodes, causing major performance issues. So, does anyone have any ideas??? Thank you in advance.

Creating a boundary around a tiled map

I've created a tiled map composed of multiple sprite nodes that are 367x367. I create this map like so:
for var i = 0; i < Int(multiplier); i++ {
for var j = 0; j < Int(multiplier); j++ {
// Positive
var map = SKSpriteNode(imageNamed: "tiledBackground.png")
var x = CGFloat(i) * map.size.height
var y = CGFloat(j) * map.size.height
map.position = CGPointMake(x, y)
self.addChild(map)
}
}
In the above example, the multiplier is 27 and the map size is 10,000x10,000.
This creates the map as expected, however I want this map to have boundaries that the player can't leave. I know how to create the boundaries, but I'm not sure what values to initialize the physics body with.
I've tried this: SKPhysicsBody(rectangleOfSize: map.mapSize) however that produced very erroneous results.
I also tried this: SKPhysicsBody(edgeLoopFromRect: CGRectMake(0, 0, map.mapSize.width, map.mapSize.height)) which built a physics body like it should (I have showPhysics = TRUE), however the physics body seemed to move with the player (I have the player moving and am centering the map on the player). You can see what I mean here: http://gyazo.com/675477d5dd86984b393b10024341188a (It's a bit hard to see, but that green line is the boundary for the physics body. When the tiled map ends (And where it turns grey), the physics body should stop as that's where the player shouldn't be allowed to move any more).
Just leave a comment if you need any more code (I believe I included anything that is relevant).
After messing around with a bit of my code I found a fix was to just add the physicsBody to my map instead of the scene. A rather easy fix, so I'm surprised I didn't think of it sooner.
With this in mind, I've answered my own question and no longer need help.

iOS CALayer and TapGestureRecognizer

I'm developing an app on iOS 6.1 for iPad.
I've a problem with the CALayer and a TapGestureRecognizer.
I've 7 CALayers forming a rainbow (every layer is a colour).
Every layer is built using a CAShapeLayer generate from a CGMutablePathRef. Everything works fine. All the layers are drawn on screen and I can see a beautiful rainbow.
Now I want to detect the tap above a single color/layer. I try this way:
- (void)tap:(UITapGestureRecognizer*)recognizer
{
//I've had the tapGestureRecognizer to rainbowView (that is an UIView) in viewDidLoad
CGLayer* tappedLayer = [rainbowView.layer.presentationlayer hitTest:[recognizer locationInView:rainbowView];
if (tappedLayer == purpleLayer) //for example
NSLog(#"PURPLE!");
}
I don't understand why this code won't work! I've already red other topics in here: all suggest the hitTest: method for solving problems like this. But in my case I can't obtain the desired result.
Can anyone help me? Thanks!!
EDIT:
Here's the code for the creation of paths and layers:
- (void)viewDidLoad
{
//Other layers
...
...
//Purple Arc
purplePath = CGPathCreateMutable();
CGPathMoveToPoint(purplePath, NULL, 150, 400);
CGPathAddCurveToPoint(purplePath, NULL, 150, 162, 550, 162, 550, 400);
purpletrack = [CAShapeLayer layer];
purpletrack.path = purplePath;
purpletrack.strokeColor = [UIColor colorWithRed:134.0/255.0f green:50.0/255.0f blue:140.0/255.0f alpha:1.0].CGColor;
purpletrack.fillColor = nil;
purpletrack.lineWidth = 25.0;
[rainbowView.layer insertSublayer:purpletrack above:bluetrack];
}
This was my first approach to the problem. And the touch didn't work.
I also tried to create a RainbowView class where the rainbow was drawing in drawRect method using UIBezierPaths.
Then I follow the "Doing Hit-Detection on a Path" section in http://developer.apple.com/library/ios/#documentation/2ddrawing/conceptual/drawingprintingios/BezierPaths/BezierPaths.html
In this case the problem was the path variable passed to the method. I try to compare the UIBezierPath passed with the paths in RainbowView but with no results.
I could try to create curves instead of paths. In this case maybe there isn't a fill part of figure and the touching area is limited to the stroke. But then how can I recognize the touch on a curve?
I'm so confused about all of these stuff!!! :D
The problem you are facing is that you are checking agains the frame/bounds of the layer when hit testing and not agains the path of the shape layer.
If your paths are filled you should instead use CGPathContainsPoint() to determine if the tap was inside the path. If your paths aren't filled but instead stroked I refer you to Ole Begemann's article about CGPath Hit Testing.
To make your code cleaner you could do the hit testing in your own subclass. Also, unless the layer is animating when hit testing it makes no sense using the presentationLayer.

Resources