GKObstacleGraph not finding path when obstacle introduced - ios

I'm working on a Gameplaykit pathfinding proof-of-concept and I can't get GKObstacleGraph to find paths correctly.
In the following code snippet (it should work in an Xcode 7.2 playground), path2 is always an empty array if there is an obstacle provided when the graph is created. If I create the obGraph object with an empty array of obstacles the findPathFromNode returns a correct path.
The obstacle created should be a simple U shaped polygon with the end point being inside the U.
import UIKit
import GameplayKit
let pts = [vector_float2(2,2),
vector_float2(3,2),
vector_float2(3,6),
vector_float2(7,6),
vector_float2(7,2),
vector_float2(8,3),
vector_float2(8,7),
vector_float2(2,7),
vector_float2(2,2)]
let obstacle1 = GKPolygonObstacle(points: UnsafeMutablePointer(pts) ,
count: pts.count)
let obGraph = GKObstacleGraph(obstacles: [obstacle1], bufferRadius: 0)
let startPt = GKGraphNode2D(point: vector_float2(5,9))
let endPt = GKGraphNode2D(point: vector_float2(5,5))
let pt3 = GKGraphNode2D(point: vector_float2(0,0))
let pt4 = GKGraphNode2D(point: vector_float2(0,9))
let pt5 = GKGraphNode2D(point: vector_float2(5,0))
let pt6 = GKGraphNode2D(point: vector_float2(10,0))
obGraph.connectNodeUsingObstacles(startPt)
obGraph.connectNodeUsingObstacles(endPt)
obGraph.connectNodeUsingObstacles(pt3)
obGraph.connectNodeUsingObstacles(pt4)
obGraph.connectNodeUsingObstacles(pt5)
obGraph.connectNodeUsingObstacles(pt6)
startPt.connectedNodes
endPt.connectedNodes
pt3.connectedNodes
let path2 = obGraph.findPathFromNode(startPt, toNode: endPt)
print(path2)

I was having the same problem as Jack. I started with Will's code example and translated it to Swift 5.0 in Xcode 10.3. I added it into Xcode's Game project template. I still had the same result: an empty array from findPath(from:to:).
After playing around with the code, I realized that anything physics-related is going to impact pathing. The only way to show code that will work for everyone is to include the creation of SKScene and all SKNode instances. Notice I set gravity to 0 in SKPhysicsWorld and I add no SKPhysicsBody to anything.
Run this in a Playground. You activate the animation by tapping anywhere in the scene.
import PlaygroundSupport
import SpriteKit
import GameKit
class GameScene: SKScene {
let nodeToMove:SKShapeNode = {
let n = SKShapeNode(circleOfRadius: 10)
n.lineWidth = 2
n.strokeColor = UIColor.orange
n.position = CGPoint(x: -200, y: 150)
return n
}()
override func sceneDidLoad() {
addChild(nodeToMove)
let nodeToFind = SKShapeNode(circleOfRadius: 5)
nodeToFind.lineWidth = 2
nodeToFind.strokeColor = UIColor.red
addChild(nodeToFind)
nodeToFind.position = CGPoint(x: 200, y: -150)
let nodeToAvoid = SKShapeNode(rectOf: CGSize(width: 100, height: 100))
nodeToAvoid.lineWidth = 4
nodeToAvoid.strokeColor = UIColor.blue
addChild(nodeToAvoid)
nodeToAvoid.position = CGPoint.zero
let polygonObstacles = SKNode.obstacles(fromNodeBounds: [nodeToAvoid])
let graph = GKObstacleGraph(obstacles: polygonObstacles, bufferRadius: 10.0)
let end = GKGraphNode2D(point: vector2(Float(nodeToMove.position.x), Float(nodeToMove.position.y)))
let start = GKGraphNode2D(point: vector2(Float(nodeToFind.position.x), Float(nodeToFind.position.y)))
graph.connectUsingObstacles(node: end)
graph.connectUsingObstacles(node: start)
graphNodes = graph.findPath(from: end, to: start) as! [GKGraphNode2D]
print("graphNodes = \(graphNodes)")
}
var graphNodes = [GKGraphNode2D]()
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.first.flatMap {_ in
let newActions: [SKAction] = graphNodes.map { n in
return SKAction.move(to: CGPoint(x: CGFloat(n.position.x), y: CGFloat(n.position.y)), duration: 2)
}
nodeToMove.run(SKAction.sequence(newActions))
}
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let scene = GameScene(size: CGSize(width: 640, height: 480))
scene.scaleMode = .aspectFill
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.backgroundColor = UIColor.purple
scene.physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
sceneView.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
The output in the console is:
graphNodes = [GKGraphNode2D: {-200.00, 150.00}, GKGraphNode2D: {62.85, 62.85}, GKGraphNode2D: {200.00, -150.00}]
The start state:
The end state:
Warning: I do not know why it takes an entire second between the user tap and the start of the animation. Performance tuning is a separate topic.

(sorry in advance for obj-c not swift)
It is my impression that adding each vertex point as a connection is not necessary, merely telling the GKObstacleGraph about the GKPolygonObstacle will be enough for it to avoid the generated polgyon shape. I have used the following and received 3 nodes to create a pathway around my obstacle (with a buffer of 10.0f) seen here:
- (void)findPathWithNode:(SKNode *)nodeToFindPath {
NSMutableArray *obstaclesToAvoid = [NSMutableArray array];
for (SKNode *objectInScene in chapterScene.children) {
if ([objectInScene.name isEqualToString:#"innerMapBoundary"]) {
[obstaclesToAvoid addObject:objectInScene];
}
}
/* FYI: The objectInScene is just a black SKSpriteNode with a
square physics body 100 x 100 rotated at 45°
*/
NSArray *obstacles = [SKNode obstaclesFromNodePhysicsBodies:[NSArray arrayWithArray:obstaclesToAvoid]];
GKObstacleGraph *graph = [GKObstacleGraph graphWithObstacles:obstacles bufferRadius:10.0f];
GKGraphNode2D *end = [GKGraphNode2D nodeWithPoint:vector2((float)character.position.x, (float)character.position.y)];
GKGraphNode2D *start = [GKGraphNode2D nodeWithPoint:vector2((float)nodeToFindPath.position.x, (float)nodeToFindPath.position.y)];
[graph connectNodeUsingObstacles:end];
[graph connectNodeUsingObstacles:start];
NSArray *pathPointsFound = [graph findPathFromNode:enemy toNode:target];
NSLog(#"Path: %#", pathPointsFound);
GKPath *pathFound;
// Make sure that there were at least 2 points found before creating the path
if (pathPointsFound.count > 1) {
for (GKGraphNode2D *nodeFound in pathPointsFound) {
// This is just to create a visual for the points found
vector_float2 v = (vector_float2){(float)nodeFound.position.x, (float)nodeFound.position.y};
CGPoint p = CGPointMake(v.x, v.y);
SKShapeNode *shapetoadd = [SKShapeNode shapeNodeWithCircleOfRadius:4];
shapetoadd.name = #"shapeadded";
shapetoadd.fillColor = [UIColor redColor];
shapetoadd.position = p;
[chapterScene addChild:shapetoadd];
}
pathFound = [GKPath pathWithGraphNodes:pathPointsFound radius:10.0];
}
}
Hopefully this points you in the right direction!

Related

How to add to a variable in SpriteKit

Relatively new to swift so apologies if I'm being stupid.
I'm using Xcode 13.3 beta 3
I have some code which spawns in a sprite named Monster and moves it from the right of the screen to the left at random speeds and at a random y location.
When it moves off screen it removes from parent and transitions to a loose screen.
what I want it to do when it leaves the screen is for it to add to a variable I've defined at the top of the class:
var pass = 0
here's the code for the monsters:
func addMonster() {
let monster = SKSpriteNode(imageNamed: "monster")
monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size)
monster.physicsBody?.isDynamic = true
monster.physicsBody?.categoryBitMask = PhysicsCatagory.monster
monster.physicsBody?.contactTestBitMask = PhysicsCatagory.projectile
monster.physicsBody?.collisionBitMask = PhysicsCatagory.none
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
addChild(monster)
let actualDuration = random(min: CGFloat(1.65), max: CGFloat(5.5))
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.run() { [weak self] in
guard let `self` = self else {return}
let end = SKTransition.crossFade(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: end)
}
monster.run(SKAction.sequence([actionMove,loseAction, actionMoveDone]))
}
what I want is the loose action to just add +1 to the pass variable then have an if statement for when pass is greater than 3 to run the code that goes to the loose screen.
Hope that all makes sense
any help would be much appreciated!

Swift SKPhysicsBody falling through static SKPhysicsBody with matching bitmasks

I am making a platforming game in Swift with SpriteKit involving a main character that jumps around. However, once the level loads the player immediately falls through the ground. You can see it in action here.
I am using SKTilemapNode to create the ground, and looping through the tiles when a level loads to create an SKPhysicsBody on a child node of the tile map. This is very similar to what is demoed in the "What's new in SpriteKit" video at WWDC 2016:
So, here we've got a little platform that I built. A little guy that can run around. And you can see that I got the parallax scrolling going on in the background. And you'll note that I'm colliding with the tiles here. And I achieve this by leveraging custom user data that we can put on each of our tiles. Here, I'll show you in our tile set. Select one of the variants here.
And you can see that we have some user data over here. And I just have a value called edgeTile which is a Boolean, and I set to 1.
So, in code, I'm going through the tile map in our platform demo here, and I'm looking for all of these edge tiles.
And whenever I find one, I create some physics data to allow the player to collide with it.
My function to create a physics body based off of an SKTilemapNode is as follows:
extension SKTileMapNode {
//In order for this to work, edge tile definitions must have the "edge" property in user data
func createPhysicsBody() -> SKPhysicsBody {
var physicsBodies = [SKPhysicsBody]()
for row in 0 ..< self.numberOfRows {
for column in 0 ..< self.numberOfColumns {
if self.tileDefinition(atColumn: column, row: row)?.userData?["edge"] != nil {
physicsBodies.append(SKPhysicsBody(rectangleOf: self.tileSize, center: self.centerOfTile(atColumn: column, row: row)))
}
}
}
let body = SKPhysicsBody(bodies: physicsBodies)
body.affectedByGravity = false
body.isDynamic = false
body.allowsRotation = false
body.pinned = true
body.restitution = 0
body.collisionBitMask = 0b1111
body.categoryBitMask = 0b1111
body.contactTestBitMask = 0b1000
return body
}
func initializePhysicsBody() {
let node = SKNode()
node.name = "Tilemap"
node.physicsBody = createPhysicsBody()
addChild(node)
}
}
So, in my scene setup all I have to do is call tileMap.initializePhysicsBody() to do everything that I need.
The SKPhysicsBody for my player is as follows:
let rect = CGSize(width: 16 * xScale, height: 24 * yScale)
let physics = SKPhysicsBody(rectangleOf: rect)
physics.isDynamic = true
physics.allowsRotation = false
physics.pinned = false
physics.affectedByGravity = true
physics.friction = 0
physics.restitution = 0
physics.linearDamping = 0
physics.angularDamping = 0
physics.density = 100
physics.categoryBitMask = 0b0001
physics.collisionBitMask = 0b0001
physics.contactTestBitMask = 0b0011
physics.usesPreciseCollisionDetection = true
physicsBody = physics
I'm not sure what the problem is here, but if I set the SKTilemapNode's physics body to be dynamic, it works. This is how I had the game working up until this point, however, this creates a lot of jitter in the ground because it's moving as a result of the player hitting it. So, thanks for reading this far at least, and any suggestions would be appreciated.
EDIT.
I think the ERROR Here is not using UInt 32
body.categoryBitMask: UInt32 = 2
body.collisionBitMask: UInt32 = 1
body.contactTestBitMask: UInt32 = 1
And Player
physics.categoryBitMask: UInt32 = 1
physics.collisionBitMask: UInt32 = 2
physics.contactTestBitMask: UInt32 = 2
This should definitely Work
Also try this way for the tileMapNode (given below) rather than creating the extension. This was given in a apple developer forum by dontangg
self.tileMap = self.childNode(withName: "Tile Map") as? SKTileMapNode
guard let tileMap = self.tileMap else { fatalError("Missing tile map for the level") }
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2.0 * tileSize.height
for col in 0..<tileMap.numberOfColumns {
for row in 0..<tileMap.numberOfRows {
let tileDefinition = tileMap.tileDefinition(atColumn: col, row: row)
let isEdgeTile = tileDefinition?.userData?["edgeTile"] as? Bool
if (isEdgeTile ?? false) {
let x = CGFloat(col) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)
let tileNode = SKShapeNode(rect: rect)
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = playerCollisionMask | wallCollisionMask
tileNode.physicsBody?.categoryBitMask = wallCollisionMask
tileMap.addChild(tileNode)
}
}
}

SKPhysicsBodies not colliding after addition of SKPhysicsJointLimit

I currently have two SKSpriteNodes that I have added SKPhysicsBodies to. When they have no SKJoint attached, they collide as expected. As soon as I add the SKPhysicsJoint, they just pass right through each other. Any joint I add functions properly, but the SKPhysicsJointLimit only limits the extent to which the nodes can travel apart from each other, not how close they can get. How can I fix this?
Here is code I am using for the joint:
let joint = SKPhysicsJointLimit.joint(withBodyA: object1.physicsBody!, bodyB: object2.physicsBody!, anchorA: CGPoint(x: object1.position.x + iconController.position.x, y: object1.position.y + iconController.position.y), anchorB: CGPoint(x: object2.position.x + iconController.position.x, y: object2.position.y + iconController.position.y))
joint.maxLength = screen.height * 0.4
physicsWorld.add(joint)
PhysicsBody of both nodes:
self.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width / 2)
self.physicsBody?.allowsRotation = false
self.physicsBody?.friction = 0
self.physicsBody?.mass = 0.1
I have tested it with different values for the above modifications of the SKPhysicsBody and it performs the same.
An SKPhysicsJoint object connects two physics bodies so that they are simulated together by the physics world.
You can use also SKPhysicJointPin:
A pin joint allows the two bodies to independently rotate around the
anchor point as if pinned together.
If your objects work well before the SKPhysicsJoint addition with the physic engine so they fired the didBeginContact as you wish and as you have setted, I think your problem is simply a wrong anchor. Try to add:
let skView = self.view as! SKView
skView.showsPhysics = true
to your scene initialization code: you will see an outline of the physic bodies and maybe you'll see the issue immediatly.
To help you I'll try to make an example of elements configured to collide each other:
enum CollisionTypes: UInt32 {
case Boundaries = 1
case Element = 2
}
class GameScene: SKScene,SKPhysicsContactDelegate {
private var elements = [SKNode]()
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
let boundariesFrame = CGRectMake(20, 20, 200, 400)
let boundaries = SKShapeNode.init(rect: boundariesFrame)
boundaries.position = CGPointMake(350,150)
let boundariesBody = SKPhysicsBody.init(edgeLoopFromRect: boundariesFrame)
boundariesBody.dynamic = false
boundariesBody.categoryBitMask = CollisionTypes.Boundaries.rawValue
boundariesBody.contactTestBitMask = CollisionTypes.Element.rawValue
boundaries.physicsBody = boundariesBody
addChild(boundaries)
for index in 0..<5 {
let element = SKShapeNode(circleOfRadius: 10)
let body = SKPhysicsBody(circleOfRadius: 10)
body.linearDamping = 0
// body.mass = 0
body.dynamic = true
body.categoryBitMask = CollisionTypes.Element.rawValue
body.contactTestBitMask = CollisionTypes.Boundaries.rawValue | CollisionTypes.Element.rawValue
body.collisionBitMask = CollisionTypes.Boundaries.rawValue | CollisionTypes.Element.rawValue
element.physicsBody = body
element.position = CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index))
elements.append(element)
addChild(element)
}
}
}
Hope it can help you to find your issue.

Swift: Detect when SKSpriteNode has even touched

I have a game where an object falls from the top of the screen to the bottom randomly. I want to have it to where if the object is tapped, I can tell it to do something, like add points to the score.
Can you help me try to figure this out? I'll be on to answer any questions if you happen to have any. Here is the code I'm using:
class GameSceneTest: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.clearColor()
physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self
//Change duration
runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(addObject), SKAction.waitForDuration(1)])
))
//AddMusicHere
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func add Object() {
let Object = SKSpriteNode(imageNamed: "Object\(arc4random_uniform(6) + 1).png")
Object.userInteractionEnabled = true
Object.size = CGSize(width: 50, height: 50)
Object.physicsBody = SKPhysicsBody(rectangleOfSize: Object.size)
Object.physicsBody?.dynamic = true
//Determine where to spawn gem across y axis
let actually = random(min: Object.size.width/2, max: size.width - Object.size.width/2)
//Position Object slightly off screen along right edge
//and along random y axis point
//Object.position = CGPoint(x: size.width + Object.size.width/2 , y: actually)
Object.position = CGPoint(x: actually, y: size.height + Object.size.height/2)
//Add the Object to the scene
addChild(Object)
//Determines speed of Object (edit later to where speed depends on type of Object)
let actualDuration = random(min: CGFloat(4), max: CGFloat(5))
//Create the Actions
let actionMove = SKAction.moveTo(CGPoint(x: actually, y: gem.size.height/2), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.runBlock() {
let reveal = SKTransition.flipHorizontalWithDuration(0.5) //Change to flipe vertical
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: reveal)
}
Object.runAction(SKAction.sequence([actionMove, loseAction, actionMoveDone]))
}
Take a look at "Moving The Cards Around The Board" in the following tutorial: Ray Wenderlicht SpriteKit Tutorial.
Then read a bit more around handling touch events in iOS (e.g. Handling Touches...).
Then try something in code and if you get stuck post a more specific question - because this one is a little too general.

Trying to delay spawn nodes using waitForDuration in didMoveToView with SpriteKit Scene (SWIFT) but not working

I'm trying to stop my nodes from falling just for a second or two at the start of my game. So my problem is when I push start the nodes are already halfway down the screen. I also tried changing how high the nodes start but it seems like a costly solution since I want to be careful not to let my FPS get too low. In my code I am trying to do this in the didMoveToView and I am using waitForDuration but it doesn't work.
Example Image of Nodes Falling Down
Any SpriteKit masters know what I should do? I'm using Swift.
Here is my code:
override func didMoveToView(view: SKView) {
let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
self.spawnNumbers()
}
numContainer.runAction(SKAction.sequence([wait, run]))
}
func spawnNumbers() {
let minValue = self.size.width / 8
let maxValue = self.size.width - 36
let spawnPoint = CGFloat(arc4random_uniform(UInt32(maxValue - minValue)))
let action = SKAction.moveToY(-300, duration: 2)
numContainer = SKSpriteNode(imageNamed: "Circle")
numContainer.name = "Circle"
numContainer.size = CGSize(width: 72, height: 72)
numContainer.anchorPoint = CGPointMake(0, 0)
numContainer.position = CGPoint(x: spawnPoint, y: self.size.height)
numContainer.runAction(SKAction.repeatActionForever(action))
numContainer.zPosition = 2
let numberLabel = SKLabelNode(fontNamed: "AvenirNext-Bold")
numberLabel.text = "\(numToTouch)"
numberLabel.name = "Label"
numberLabel.zPosition = -1
numberLabel.position = CGPointMake(CGRectGetMidX(numContainer.centerRect) + 36, CGRectGetMidY(numContainer.centerRect) + 36)
numberLabel.horizontalAlignmentMode = .Center
numberLabel.verticalAlignmentMode = .Center
numberLabel.fontColor = UIColor.whiteColor()
numberLabel.fontSize = 28
addChild(numContainer)
numContainer.addChild(numberLabel)
numContainerArray.append(numContainer)
numToTouch += 1
}
I think you have to cast the 2.5 seconds to an NSTimeInterval:
let wait = SKAction.waitForDuration(NSTimeInterval(2.5))
You could also try putting the actions in the init function for your scene:
override init (size: CGSize) {
super.init(size: size)
//your code from before
}
Also, not sure if it matters, but this is what I normally do and it works,
let wait = SKAction.waitForDuration(NSTimeInterval(2.5))
let action = SKAction.runBlock({() in self.spawnNumbers()})
let actionThenWait = SKAction.sequence([wait, action])
self.runAction(actionThenWait)

Resources