Please help me! Im trying to call a function I have declared in the GameScene class, within the update function. But it doesn't recognise the function, I'm wondering if this is something to do with the class or something because I want to run the function every frame (but has to update for each individual spriteCopy, depending on its own movement) to make sure the sprite copies all follow the function and continue to, infinitely.
Thank you in advance for any help.
here is the code for the function that sort of works to an extent:
func touchUp(atPoint pos : CGPoint) {
if let spriteCopy = self.sprite?.copy() as! SKShapeNode? {
spriteCopy.fillColor = UIColor.white
spriteCopy.position = initialTouch
spriteCopy.physicsBody?.restitution = 0.5
spriteCopy.physicsBody?.friction = 0
spriteCopy.physicsBody?.affectedByGravity = false
spriteCopy.physicsBody?.linearDamping = 0
spriteCopy.physicsBody?.angularDamping = 0
spriteCopy.physicsBody?.angularVelocity = 0
spriteCopy.physicsBody?.isDynamic = true
spriteCopy.physicsBody?.categoryBitMask = 1 //active
spriteCopy.isHidden = false
touchUp = pos
xAxisLength = initialTouch.x - touchUp.x
yAxisLength = initialTouch.y - touchUp.y
xUnitVector = xAxisLength / distanceBetweenTouch * power * 300
yUnitVector = yAxisLength / distanceBetweenTouch * power * 300
spriteCopy.physicsBody?.velocity = CGVector(dx: xUnitVector, dy: yUnitVector)
func directionRotation() {
if let body = spriteCopy.physicsBody {
if (body.velocity.speed() > 0.01) {
spriteCopy.zRotation = body.velocity.angle()
}
}
}
directionRotation() //When I run the function with this line, the spriteCopy
//is spawned initially with the right angle (in the direction
//of movement) but doesn't stay updating the angle
sprite?.isHidden = true
self.addChild(spriteCopy)
}
}
and here is the function not being recognised in function update:
override func update(_ currentTime: TimeInterval) {
directionRotation() //this line has error saying "use of unresolved identifier"
// Called before each frame is rendered
}
EDIT: I was thinking maybe there could be a way to spawn multiple spriteCopy's without the "copy()" method that will not restrict the access to the spriteCopy's properties after they have been spawned? Whilst remembering they still must have to be individual SpriteNodes so that the directionRotation function could be applied independently to each of them (FYI: The user can spawn upwards of 50+ sprite nodes)
You have specified local function. You need move out from touchUp function realisation directionRotation
func directionRotation() {
if let body = spriteCopy.physicsBody {
if (body.velocity.speed() > 0.01) {
spriteCopy.zRotation = body.velocity.angle()
}
}
}
func touchUp(atPoint pos : CGPoint) {
...
}
EDIT
I mean you need do some think like this:
func directionRotation(node:SKNode) {
if let body = node.physicsBody {
if (body.velocity.speed() > 0.01) {
node.zRotation = body.velocity.angle()
}
}
}
override func update(_ currentTime: TimeInterval) {
for node in self.children
{
directionRotation(node)
}
}
Related
I have an animated campfire using an texture atlas in SpriteKit, I am trying to simulate the variable lighting that a fire would produce. I was able to achieve a flicker by varying the falloff by passing in a random number form 0...1.5. It works but is a little too crazy - looking for a suggestion on smoothing it out to be more subtle and realistic - maybe pass an array of set values thru - not sure how I would do that? Or some sort of easing?
func buildCampfire() {
let campfireAtlas = SKTextureAtlas(named: "Campfire")
var fireFrames: [SKTexture] = []
let numImages = campfireAtlas.textureNames.count
for i in 1...numImages {
let fireTextureName = "campfire\(i)"
fireFrames.append(campfireAtlas.textureNamed(fireTextureName))
}
animatedCampfire = fireFrames
let firstFrameTexture = animatedCampfire[0]
campfire = SKSpriteNode(texture: firstFrameTexture)
campfire.size.height = 300
campfire.size.width = 300
campfire.position = CGPoint(x: 108, y: -188)
addChild(campfire)
}
func animateCampfire() {
campfire.run(SKAction.repeatForever(SKAction.animate(with: animatedCampfire, timePerFrame: 0.1, resize: false, restore: true)), withKey: "campfireAnimated")
}
func flickerCampfire() {
if let campfireLight = self.childNode(withName: "//campfireLight") as? SKLightNode {
campfireLight.falloff = CGFloat.random(in: 0..<1.5)
} else {
print("cannot find light node")
}
}
override func update(_ currentTime: TimeInterval) {
flickerCampfire()
}
}
My goal is to set up all my platforms in the .sks file for easier design of my levels.
this is declared at the top of game scene.swift before didMove:
private var JumpThroughPlatformObject = SKSpriteNode()
and this is in DidMove:
if let JumpThroughPlatformObjectNode = self.childNode(withName: "//jumpThroughPlatform1") as? SKSpriteNode {
JumpThroughPlatformObject = JumpThroughPlatformObjectNode}
I reference the platform to get it's height from the .sks, since all my platforms are going to be the same height I only need to get it from one.
Below is what Im trying to use in my update method to turn off collisions until my player is totally above the platform. The main issue with only checking if my players velocity is greater than zero is: if the player is at the peak of a jump (his velocity slows to zero). if this happens and the player is inside a platform, he either instantly springs up to the top of the platform or gets launched downward.
I don't want my platforms to have to be 1 pixel high lines. I also need to have the player have a full collision box since he will be interacting with other types of environments. This leads me to believe that I somehow need to only register the top of the platform as a collision box and not the entire platform.
This if statement I wrote is supposed to take the y position of a platform and add half of its height to it, since the y position is based on the center of the sprite I figured this would put the collision for the platform on its top boundary.
I did the same for the player but in reverse. Putting the players collisions on only the bottom of his border. But its not working perfectly and I'm not sure why at this point.
if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2))
The function below is giving me 3 main issues:
My players jump is always dy = 80. If I'm jumping up to a platform that position.y = 90, the players peak of the jump stops in the middle of the platform, but he teleports to the top of it instead of continuing to fall to the ground.
the left and right edges of the platforms still have full collision with the player if I'm falling
if my player is on a platform and there is another one directly above me, the player can't jump through it.
let zero:CGFloat = 0
if let body = player.physicsBody {
let dy = player.physicsBody?.velocity.dy
// when I jump dy is greater than zero else I'm falling
if (dy! >= zero) {
if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2)) {
print(" platform y: \(JumpThroughPlatformObject.position.y)")
print ("player position: \(player.position.y)")
// Prevent collisions if the hero is jumping
body.collisionBitMask = CollisionTypes.saw.rawValue | CollisionTypes.ground.rawValue
}
}
else {
// Allow collisions if the hero is falling
body.collisionBitMask = CollisionTypes.platform.rawValue | CollisionTypes.ground.rawValue | CollisionTypes.saw.rawValue
}
}
Any advice would be greatly appreciated. I've been tearing my hair out for a couple days now.
EDIT in didBegin and didEnd:
func didBegin(_ contact: SKPhysicsContact) {
if let body = player.physicsBody {
let dy = player.physicsBody?.velocity.dy
let platform = JumpThroughPlatformObject
let zero:CGFloat = 0
if contact.bodyA.node == player {
// playerCollided(with: contact.bodyB.node!)
if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue
}
} else if contact.bodyB.node == player {
// playerCollided(with: contact.bodyA.node!)
isPlayerOnGround = true
if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue}
}
}
}
func didEnd(_ contact: SKPhysicsContact) {
if let body = player.physicsBody {
// let dy = player.physicsBody?.velocity.dy
// let platform = JumpThroughPlatformObject
if contact.bodyA.node == player {
body.collisionBitMask |= CollisionTypes.platform.rawValue
}else if contact.bodyB.node == player {
body.collisionBitMask |= CollisionTypes.platform.rawValue
}
}
}
Adding what I did, the player can no longer jump through the platform.
Here is a link to the project that I made for macOS and iOS targets:
https://github.com/fluidityt/JumpUnderPlatform
Basically, this all has to do with
Detecting collision of a platform
Then determining if your player is under the platform
Allow your player to go through the platform (and subsequently land on it)
--
SK Physics makes this a little complicated:
On collision detection, your player's .position.y or .velocity.dy
may already have changed to a "false" state in reference to satisfying the #2 check from above (meaning #3 will never happen). Also, your player will bounce off the platform on first contact.
There is no "automatic" way to determine when your player has finished passing through the object (thus to allow player to land on the platform)
--
So to get everything working, a bit of creativity and ingenuity must be used!
1: Detecting collision of a platform:
So, to tackle 1 is the simplest: we just need to use the built in didBegin(contact:)
We are going to be relying heavily on the 3 big bitMasks, contact, category, and collision:
(fyi, I don't like using enums and bitmath for physics because I'm a rebel idiot):
struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4) // JUP = JumpUnderPlatform
}
override func didBegin(_ contact: SKPhysicsContact) {
// Crappy way to do "bit-math":
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask
switch contactedSum {
case BitMasks.jupCategory + BitMasks.playerCategory:
// ...
}
--
Now, you said that you wanted to use the SKSEditor, so I have accommodated you:
// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {
var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??
// NOTE: I could not properly configure any SKNode properties here..
// it's like they all get RESET if you put them in here...
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}
--
Now for the player:
class Player: SKSpriteNode {
// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }
static func makePlayer() -> Player {
let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)
newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true
newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.
return newPlayer
}
}
2. (and dealing with #4): Determining if under platform on contact:
There are many ways to do this, but I chose to use the player.pb.velocity.dy approach as mentioned by KOD to keep track of the player's position... if your dy is over 0, then you are jumping (under a platform) if not, then you are either standing still or falling (need to make contact with the platform and stick to it).
To accomplish this we have to get a bit more technical, because again, the physics system and the way SK works in its loop doesn't always mesh 100% with how we think it should work.
Basically, I had to make an initialDY property for Player that is constantly updated each frame in update
This initialDY will give us the correct data that we need for the first contact with the platform, allowing us to tell us to change the collision mask, and also to reset our player's CURRENT dy to the initial dy (so the player doesn't bounce off).
3. (and dealing with #5): Allow player to go through platform
To go through the platform, we need to play around with the collisionBitMasks. I chose to make the player's collision mask = the player's categoryMask, which is probably not the right way to do it, but it works for this demo.
You end up with magic like this in didBegin:
// Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }
// Gives us the ability to pass through the platform!
player.pb.collisionBitMask = BitMasks.playerCategory
Now, dealing with #5 is going to require us to add another piece of state to our player class.. we need to temporarily store the contacted platform so we can check if the player has successfully finished passing through the platform (so we can reset the collision mask)
Then we just check in didFinishUpdate if the player's frame is above that platform, and if so, we reset the masks.
Here are all of the files , and again a link to the github:
https://github.com/fluidityt/JumpUnderPlatform
Player.swift:
class Player: SKSpriteNode {
// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }
// This is set when we detect contact with a platform, but are underneath it (jumping up)
weak var platformToPassThrough: JumpUnderPlatform?
// For use inside of gamescene's didBeginContact (because current DY is altered by the time we need it)
var initialDY = CGFloat(0)
}
// MARK: - Funkys:
extension Player {
static func makePlayer() -> Player {
let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)
newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true
newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.
return newPlayer
}
func isAbovePlatform() -> Bool {
guard let platform = platformToPassThrough else { fatalError("wtf is the platform!") }
if frame.minY > platform.frame.maxY { return true }
else { return false }
}
func landOnPlatform() {
print("resetting stuff!")
platformToPassThrough = nil
pb.collisionBitMask = BitMasks.jupCategory
}
}
// MARK: - Player GameLoop:
extension Player {
func _update() {
// We have to keep track of this for proper detection of when to pass-through platform
initialDY = pb.velocity.dy
}
func _didFinishUpdate() {
// Check if we need to reset our collision mask (allow us to land on platform again)
if platformToPassThrough != nil {
if isAbovePlatform() { landOnPlatform() }
}
}
}
JumpUnderPlatform & BitMasks.swift (respectively:)
// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {
var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}
struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4)
}
GameScene.swift:
-
MAKE SURE YOU HAVE THE TWO NODES IN YOUR SKS EDITOR:
-
// MARK: - Props:
class GameScene: SKScene, SKPhysicsContactDelegate {
// Because I hate crashes related to spelling errors.
let names = (jup: "jup", resetLabel: "resetLabel")
let player = Player.makePlayer()
}
// MARK: - Physics handling:
extension GameScene {
private func findJup(contact: SKPhysicsContact) -> JumpUnderPlatform? {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else { fatalError("how did this happne!!??") }
if nodeA.name == names.jup { return (nodeA as! JumpUnderPlatform) }
else if nodeB.name == names.jup { return (nodeB as! JumpUnderPlatform) }
else { return nil }
}
// Player is 2, platform is 4:
private func doContactPlayer_X_Jup(platform: JumpUnderPlatform) {
// Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }
// Gives us the ability to pass through the platform!
player.physicsBody!.collisionBitMask = BitMasks.playerCategory
// Will push the player through the platform (instead of bouncing off) on first hit
if player.platformToPassThrough == nil { player.pb.velocity.dy = player.initialDY }
player.platformToPassThrough = platform
}
func _didBegin(_ contact: SKPhysicsContact) {
// Crappy way to do bit-math:
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask
switch contactedSum {
case BitMasks.jupCategory + BitMasks.playerCategory:
guard let platform = findJup(contact: contact) else { fatalError("must be platform!") }
doContactPlayer_X_Jup(platform: platform)
// Put your other contact cases here...
// case BitMasks.xx + BitMasks.yy:
default: ()
}
}
}
// MARK: - Game loop:
extension GameScene {
// Scene setup:
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
addChild(player)
}
// Touch handling: (convert to touchesBegan for iOS):
override func mouseDown(with event: NSEvent) {
// Make player jump:
player.pb.applyImpulse(CGVector(dx: 0, dy: 50))
// Reset player on label click (from sks file):
if nodes(at: event.location(in: self)).first?.name == names.resetLabel {
player.position.y = frame.minY + player.size.width/2 + CGFloat(1)
}
}
override func update(_ currentTime: TimeInterval) {
player._update()
}
func didBegin(_ contact: SKPhysicsContact) {
self._didBegin(contact)
}
override func didFinishUpdate() {
player._didFinishUpdate()
}
}
I HOPE THIS HELPS SOME!
You just need a condition that let's you know if you are in a body. I also cleaned up your code to avoid accidently putting in the wrong categories
if let body = player.physicsBody, let dy = body.velocity.dy {
// when I am jumping or I am in a platform, then do not register
if (dy > zero || body.node.intersects(platform) && (body.node.position.y - body.node.size.height/2 != platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue
}
else {
// Allow collisions if the hero is falling
body.collisionBitMask |= CollisionTypes.platform.rawValue
Well, The answers above work well but those are very completed.
Simple answer is use Platform effector 2D component. which applies various “platform” behavior such as one-way collisions, removal of side-friction/bounce etc.
Check out this Unity's official tutorial for more clearance.
I'm trying to create a game, where the objects need to chase food. Right now the objects speeds up, when the food is within the given radius. But I need the speed to always be the same.
Any suggestions how to fix this? I have tried to add an SKAction under the chase function, where I set the position.x and position.y, but I can't make it work correct.
Fish class:
class Fish:SKSpriteNode{
private let kMovingAroundKey = "movingAround"
private let kFishSpeed:CGFloat = 4.5
private var swimmingSpeed:CGFloat = 100.0
private let sensorRadius:CGFloat = 100.0
private weak var food:SKSpriteNode! = nil //the food node that this fish currently chase
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.affectedByGravity = false
physicsBody?.categoryBitMask = Collider.fish
physicsBody?.contactTestBitMask = Collider.food
physicsBody?.collisionBitMask = 0x0 //No collisions with fish, only contact detection
name = "fish"
let sensor = SKShapeNode(circleOfRadius: 100)
sensor.fillColor = .red
sensor.zPosition = -1
sensor.alpha = 0.1
addChild(sensor)
}
func getDistanceFromFood()->CGFloat? {
if let food = self.food {
return self.position.distance(point: food.position)
}
return nil
}
func lock(food:SKSpriteNode){
//We are chasing a food node at the moment
if let currentDistanceFromFood = self.getDistanceFromFood() {
if (currentDistanceFromFood > self.position.distance(point: food.position)){
//chase the closer food node
self.food = food
self.stopMovingAround()
}//else, continue chasing the last locked food node
//We are not chasing the food node at the moment
}else{
//go and chase then
if food.position.distance(point: self.position) <= self.sensorRadius {
self.food = food
self.stopMovingAround()
}
}
}
//Helper method. Not used currently. You can use this method to prevent chasing another (say closer) food while already chasing one
func isChasing(food:SKSpriteNode)->Bool{
if self.food != nil {
if self.food == food {
return true
}
}
return false
}
func stopMovingAround(){
if self.action(forKey: kMovingAroundKey) != nil{
removeAction(forKey: kMovingAroundKey)
}
}
//MARK: Chasing the food
//This method is called many times in a second
func chase(within rect:CGRect){
guard let food = self.food else {
if action(forKey: kMovingAroundKey) == nil {
self.moveAround(within: rect)
}
return
}
//Check if food is in the water
if rect.contains(food.frame.origin) {
//Take a detailed look in my Stackoverflow answer of how chasing works : https://stackoverflow.com/a/36235426
let dx = food.position.x - self.position.x
let dy = food.position.y - self.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * kFishSpeed
let vy = sin(angle) * kFishSpeed
position.x += vx
position.y += vy
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveAround(within rect:CGRect){
if scene != nil {
//Go randomly around the screen within view bounds
let point = rect.randomPoint()
//Formula: time = distance / speed
let duration = TimeInterval(point.distance(point: position) / self.swimmingSpeed)
let move = SKAction.move(to: point, duration: duration)
let block = SKAction.run {
[unowned self] in
self.moveAround(within: rect)
}
let loop = SKAction.sequence([move,block])
run(loop, withKey: kMovingAroundKey)
}
}
}
Gamescene where you can see the update function.
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "fish") {
[unowned self] node, stop in
if let fish = node as? Fish {
self.enumerateChildNodes(withName: "food") {
node, stop in
fish.lock(food: node as! SKSpriteNode)
}
fish.chase(within: self.water.frame)
}
}
}
Probably something like this (GameScene):
var prev : TimeInterval!
//MARK: Chasing the food
override func update(_ currentTime: TimeInterval) {
defer { prev = currentTime }
guard prev != nil else { return }
let dt = currentTime - prev
print("delta time \(dt)")
self.enumerateChildNodes(withName: "fish") {
[unowned self] node, stop in
if let fish = node as? Fish {
self.enumerateChildNodes(withName: "food") {
node, stop in
fish.lock(food: node as! SKSpriteNode)
}
fish.chase(within: self.water.frame, delta:CGFloat(dt))
}
}
}
The variable prev is a property of GameScene.
And change chase() method in Fish class:
//MARK: Chasing the food
func chase(within rect:CGRect, delta:CGFloat){
guard let food = self.food else {
if action(forKey: kMovingAroundKey) == nil {
self.moveAround(within: rect)
}
return
}
//Check if food is in the water
if rect.contains(food.frame.origin) {
//Take a detailed look in my Stackoverflow answer of how chasing works : https://stackoverflow.com/a/36235426
//check for collision
if self.frame.contains(food.frame.origin) {
food.removeFromParent()
}else {
let dx = food.position.x - self.position.x
let dy = food.position.y - self.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * self.swimmingSpeed * delta
let vy = sin(angle) * self.swimmingSpeed * delta
print("vx \(vx), vy (\(vy)")
position.x += vx
position.y += vy
//time = distance / speed
}
}
}
I have added the delta time parameter. You may wonder what is delta time? I will quote LearnCocos2d from that article:
Delta time is simply the time difference between the previous and the
current frame.
Why is this important to maintain the constant speed of a node? Well, we use our Fish.swimmingSpeed variable to determine the speed of fish(forget kFishSpeed, it doesn't have a purpose now).
Now in the case of SKAction, a duration parameter directly determines the speed of fish, because duration applies to time, and time = distance / speed, so we calculate the time like this currently:
let duration = TimeInterval(point.distance(point: position) / self.swimmingSpeed)
Now lets say that duration equals to 1. That means, the fish is going to move 100 pts per second. Now, the difference between update() method and actions, is that it is executed 60 times per second. And because our method chase() is ideally called 60 time per second, our speed now have to be Fish.swimmingSpeed / 60.
And this is where delta time comes in. Because it may happen that frame is not rendered in 1/60 second (0.016667), but rather rendering may take longer (eg. 0.02,0.03 sec), we calculate that difference, and use it to adjust the movement. Kind of cheating IMO in compare to normal behaviour without using delta time, because player loses the control on moments if game lags a lot (eg. its hero teleports), but that part is off topic :) It is up to you to test what works / looks better for you.
So we do (in order to calculate the distance):
let vx = cos(angle) * self.swimmingSpeed * delta
let vy = sin(angle) * self.swimmingSpeed * delta
and that will give you a constant speed.
I could go more into detail but it is late here, and you probably got an idea how things are functioning, so I will stop here. Happy coding!
I'm using the scene editor in SpriteKit to place color sprites and assign them textures using the Attributes Inspector. My problem is trying to figure out how to reference those sprites from my GameScene file. For example, I'd like to know when a sprite is a certain distance from my main character.
Edit - code added
I'm adding the code because for some reason, appzYourLife's answer worked great in a simple test project, but not in my code. I was able to use Ron Myschuk's answer which I also included in the code below for reference. (Though, as I look at it now I think the array of tuples was overkill on my part.) As you can see, I have a Satellite class with some simple animations. There's a LevelManager class that replaces the nodes from the scene editor with the correct objects. And finally, everything gets added to the world node in GameScene.swift.
Satellite Class
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.name = "satellite"
self.runAction(satAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.satellite.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.laser.rawValue
self.physicsBody?.collisionBitMask = 0
}
func createAnimations() {
let flyFrames:[SKTexture] = [textureAtlas.textureNamed("sat1.png"),
textureAtlas.textureNamed("sat2.png")]
let flyAction = SKAction.animateWithTextures(flyFrames, timePerFrame: 0.14)
satAnimation = SKAction.repeatActionForever(flyAction)
let warningFrames:[SKTexture] = [textureAtlas.textureNamed("sat8.png"),
textureAtlas.textureNamed("sat1.png")]
let warningAction = SKAction.animateWithTextures(warningFrames, timePerFrame: 0.14)
warningAnimation = SKAction.repeatActionForever(warningAction)
}
func warning() {
self.runAction(warningAnimation)
}
Level Manager Class
import SpriteKit
class LevelManager
{
let levelNames:[String] = ["Level1"]
var levels:[SKNode] = []
init()
{
for levelFileName in levelNames {
let level = SKNode()
if let levelScene = SKScene(fileNamed: levelFileName) {
for node in levelScene.children {
switch node.name! {
case "satellite":
let satellite = Satellite()
satellite.spawn(level, position: node.position)
default: print("Name error: \(node.name)")
}
}
}
levels.append(level)
}
}
func addLevelsToWorld(world: SKNode)
{
for index in 0...levels.count - 1 {
levels[index].position = CGPoint(x: -2000, y: index * 1000)
world.addChild(levels[index])
}
}
}
GameScene.swift - didMoveToView
world = SKNode()
world.name = "world"
addChild(world)
physicsWorld.contactDelegate = self
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
//This does not find the satellite nodes
let satellites = children.flatMap { $0 as? Satellite }
//This does work
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "satellite") {
self.satTuple.0 = node.position
self.satTuple.1 = (node as? SKSpriteNode)!
self.currentSatellite.append(self.satTuple)
}
}
The Obstacle class
First of all you should create an Obstacle class like this.
class Obstacle: SKSpriteNode { }
Now into the scene editor associate the Obstacle class to your obstacles images
The Player class
Do the same for Player, create a class
class Player: SKSpriteNode { }
and associate it to your player sprite.
Checking for collisions
Now into GameScene.swift change the updated method like this
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
let obstacleNearSprite = obstacles.contains { (obstacle) -> Bool in
let distance = hypotf(Float(player.position.x) - Float(obstacle.position.x), Float(player.position.y) - Float(obstacle.position.y))
return distance < 100
}
if obstacleNearSprite {
print("Oh boy!")
}
}
What does it do?
The first line retrieves all your obstacles into the scene.
the second line retrieves the player (and does crash if it's not present).
Next it put into the obstacleNearSprite constant the true value if there is at least one Obstacle at no more then 100 points from Player.
And finally use the obstacleNearSprite to print something.
Optimizations
The updated method gets called 60 times per second. We put these 2 lines into it
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
in order to retrieves the sprites we need. With the modern hardware it is not a problem but you should save references to Obstacle and Player instead then searching for them in every frame.
Build a nice game ;)
you will have to loop through the children of the scene and assign them to local objects to use in your code
assuming your objects in your SKS file were named Obstacle1, Obstacle2, Obstacle3
Once in local objects you can check and do whatever you want with them
let obstacle1 = SKSpriteNode()
let obstacle2 = SKSpriteNode()
let obstacle3 = SKSpriteNode()
let obstacle3Location = CGPointZero
func setUpScene() {
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "Obstacle1") {
self.obstacle1 = node
}
else if (node.name == "Obstacle2") {
self.obstacle2 = node
}
else if (node.name == "Obstacle3") {
self.obstacle3Location = node.position
}
}
}
I have a GKEntity that has a GKAgent2D component. Its behaviors are GKGoal, toWander: and toStayOnPath:maxPredictionTime:. The entity wanders continuously in the scene; however, I would like it to stop wandering for a while. For example, if the entity is a sheep that wanders about, I would like it to stop periodically to eat and, after a delay, start wandering again.
UPDATE:
In the Entity:
addComponent(MoveIdleComponent(maxSpeed: 60, maxAcceleration: 6, radius: Float(node.texture!.size().width * 0.3), entityManager: entityManager))
MoveIdleComponent
class MoveIdleComponent : GKAgent2D, GKAgentDelegate {
let entityManager: EntityManager
init(maxSpeed: Float, maxAcceleration: Float, radius: Float, entityManager: EntityManager) {
self.entityManager = entityManager
super.init()
delegate = self
self.maxSpeed = maxSpeed
self.maxAcceleration = maxAcceleration
self.radius = radius
print(self.mass)
self.mass = 0.01
}
func agentWillUpdate(agent: GKAgent) {
guard let spriteComponent = entity?.componentForClass(SpriteComponent.self) else {
return
}
self.position = float2(spriteComponent.node.position)
}
func agentDidUpdate(agent: GKAgent) {
guard let spriteComponent = entity?.componentForClass(SpriteComponent.self) else {
return
}
spriteComponent.node.position = CGPoint(position)
}
override func updateWithDeltaTime(seconds: NSTimeInterval) {
super.updateWithDeltaTime(seconds)
behavior = WanderBehavoir(targetSpeed: maxSpeed)
}
}
WanderBehavoir:
class WanderBehavoir: GKBehavior {
init(targetSpeed: Float) {
super.init()
if targetSpeed > 0 {
setWeight(0.5, forGoal: GKGoal(toWander: targetSpeed))
}
}
}
How can I do this?
Thanks in advance
There doesn't seem to be a GKGoal(toEatFood:withFrequency:) API, so you'll have to step back a bit toandthink about how to set the agent's goals to achieve your goals.
If you want the agent to stop wandering, or stop following a path, for some period of time, what you want is for those to no longer be its goals. (And furthermore, you want it to stop, not continue with whatever direction and speed when you took away its goals, so you'll want to introduce a toReachTargetSpeed: goal for a speed of zero.)
There are two general ways to do this:
Have your behavior include wander, follow-path, and speed (of zero) goals, with weights set such that wander and follow-path outweigh speed. When you want to switch between wander+path behavior and stopping behavior, use setWeight(_:forGoal:) to make the speed goal outweigh the others.
Have one behavior that includes wander and follow-path goals, and another with a speed (of zero) goal, and set the agent's behavior property when you want to switch between them.
Ok, i found a solution with #rickster suggestion. I post it if can help someone.
I've added pause value.
if pause is true, the weight of speed change to 1, with GKGoal(toReachTargetSpeed: 0)
in WanderBehavoir class:
class WanderBehavoir: GKBehavior {
init(targetSpeed: Float, entityManager: EntityManager, pause: Bool) {
super.init()
var weightWander : Float = 1.0
var weightSpeed : Float = 0.0
if pause {
weightWander = 0
weightSpeed = 1
}
// | |
// --A--B--
// | |
if targetSpeed > 0 {
let lato = Float(500.0)
let pts = [vector_float2(-lato,0),vector_float2(+lato,0)]
let path = GKPath(points: UnsafeMutablePointer(pts), count: pts.count, radius: 980, cyclical: true)
let obstacleNode = entityManager.nodesWithUnitType(Tree)
let obstacles = SKNode.obstaclesFromNodePhysicsBodies(obstacleNode)
setWeight(0.5, forGoal: GKGoal(toAvoidObstacles: obstacles, maxPredictionTime: 0.5))
setWeight(0.2, forGoal: GKGoal(toStayOnPath: path, maxPredictionTime: 0.5))
setWeight(weightWander, forGoal: GKGoal(toWander: targetSpeed))
setWeight(weightSpeed, forGoal: GKGoal(toReachTargetSpeed: 0))
}
}
}
In class class MoveIdleComponent : GKAgent2D, GKAgentDelegate pause switch true and false after delta time randomly
private var lastUpdateInterval: NSTimeInterval = 0
var setPause = true
var randomTimeStop = NSTimeInterval(Int(5...8))
var randomTimeMove = NSTimeInterval(Int(10...20))
override func updateWithDeltaTime(seconds: NSTimeInterval) {
super.updateWithDeltaTime(seconds)
lastUpdateInterval += seconds
if setPause {
if lastUpdateInterval > randomTimeStop {
setPause = !setPause
lastUpdateInterval = 0
randomTimeMove = NSTimeInterval(Int(10...20))
}
}
else {
if lastUpdateInterval > randomTimeMove {
setPause = !setPause
lastUpdateInterval = 0
randomTimeStop = NSTimeInterval(Int(5...8))
}
}
print("randomTimeMove \(randomTimeMove)")
behavior = WanderBehavoir(targetSpeed: maxSpeed, entityManager: entityManager, pause: setPause)
}