2

I need to make my objects move randomised around on the screen. While they are moving, the object is looking for another object within a certain amount of radius.

I found the below link, and I implemented the getDuration function. But I’m having the same glitch as the topic owner. I can see it should be possible to fix, by removing the running action.

Moving an object across the screen at a certain speed.(Sprite Kit)

Video: https://www.youtube.com/watch?v=jHE5RC-mvwU

But I have now tried several solutions, but I can’t make it work. When I’m killing the action, then my objects stop to move.

Can someone please tell me, where to kill my action, create in moveToWaypoint? I need my objects to move to a random waypoint, but if a specific object get within the radius, then it should set a new waypoint to the closet object and start the action again.

Code:

///Called in update method
func move(scene: GameplayScene) {
    checkForWaypoint()
    checkForFood(scene: scene)
    moveToWaypoint()
}

///Creates a new waypoint
func createWaypoint() {
     waypoint = CGPoint(x: randomBetweenNumbers(firstNum: minX, secondNum: maxX), y: randomBetweenNumbers(firstNum: minX, secondNum: maxX))
}

override func checkForFood(scene: GameplayScene) {
    var closesObject: SKNode? = nil

    scene.enumerateChildNodes(withName: "Food") {
        node, _ in
        let distance = node.position.distanceFromCGPoint(point: self.position)

        if distance < self.FOOD_RANGE  {
            closesObject = node
        }
    }

    if hungryState == HungryState.hungry {
        if closesObject != nil {
            waypoint = closesObject?.position
            moveState = MoveState.movingToFood
        } else {
            if moveState == MoveState.movingToFood {
                createWaypoint()
            }
        }
    }
}

///Moves the object to the waypoint
func moveToWaypoint () {
    let action = SKAction.move(to: waypoint!, duration: getDuration(pointA: position, pointB: waypoint!, speed: 25))
    self.run(action)
}

///Calcuate a speed between to cordinates
private func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)-> TimeInterval {
    let xDist = (pointB.x - pointA.x)
    let yDist = (pointB.y - pointA.y)
    let distance = sqrt((xDist * xDist) + (yDist * yDist));
    let duration : TimeInterval = TimeInterval(distance/speed)
    return duration
}

EDIT:

Update function from Gamescene class

override func update(_ currentTime: TimeInterval) {
    moveFish()
}

private func moveFish() {
    for node in self.children {
        if node.name != nil {
            switch node.name {
            case "Fish"?:
                let fishToMove = node as! Fish

                fishToMove.move(scene: self)

            default:
                break
            }
        }
    }
}
Grumme
  • 794
  • 2
  • 9
  • 31
  • Can you show how do you call move() method from within scene's update() method? I am just interested is it called every frame strictly or there is some custom logic? If the answer is "Yes, I call the move method every frame... thus I am creating SKAction every frame", that would be your first mistake. What you are about to achieve should be easy. Also it might be done using physics contact detection. But that's just me :) – Whirlwind May 25 '17 at 13:45
  • It's called in the update() method from the scene, yes. Can you provide me some details, how to fix this @Whirlwind? It's my first SpriteKit app, so I'm still new :) – Grumme May 25 '17 at 13:51
  • I just asked few question and you didn't answer :) Try to read what I asked carefully so I can continue with debugging. – Whirlwind May 25 '17 at 13:55
  • I just updated the question @Whirlwind. I have added the update function and moveFish() - Which calls the move() function. – Grumme May 25 '17 at 14:00
  • Well personally I would run an action only if needed, not every time. And about the error itself...What is the actual issue? (sorry I don't have time to read that other question at the moment). – Whirlwind May 25 '17 at 15:22
  • Also you may want to use action with key to move your objects. That way, you will be sure that you are overwriting the right action (previously will be removed). – Whirlwind May 25 '17 at 15:30
  • If you just watch the YouTube video, you can see how it suddenly moves random around the screen. That's happening to my fishes too. It happening when it is targeting a new object. I don't know why, but if I'm using action with key in my moveToWaypoint function, nothing happens. It only works without the key. – Grumme May 25 '17 at 15:34
  • So, if I understand correctly, there is a fish node (or more of them), and there are food nodes, right? So you want that fish move towards the food if it is near to it? – Whirlwind May 25 '17 at 15:42
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/145141/discussion-between-whirlwind-and-grumme). – Whirlwind May 25 '17 at 15:42
  • Also, does food moves or floats in place and only the fish is the one who moves? – Whirlwind May 25 '17 at 15:45
  • I wasn't at my computer yesterday evening. But I have answered your questions in the chat @Whirlwind :) – Grumme May 26 '17 at 09:54
  • Check out my answer. Just copy and paste the code to see how it works. It is fully workable example. – Whirlwind May 26 '17 at 18:11

1 Answers1

3

What you are trying to solve here could be done in two ways. First is using physics, and second is without it of course. I have decided to go without physics because that is obviously how you are doing it.

In short, fish in these example move using SKAction while there is no food around. When food is in range, fish are moved to their targets using update: method. When fish eat its found it continues to move around using SKAction.

Also before everything, here are some useful extensions that I have borrowed here from Stackoverflow that you might find useful in the future:

import SpriteKit
import GameplayKit

//Extension borrowed from here : https://stackoverflow.com/a/40810305
extension ClosedRange where Bound : FloatingPoint {
    public func random() -> Bound {
        let range = self.upperBound - self.lowerBound
        let randomValue = (Bound(arc4random_uniform(UINT32_MAX)) / Bound(UINT32_MAX)) * range + self.lowerBound
        return randomValue
    }
}
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
    func randomPoint() -> CGPoint {
        let origin = self.origin
        return CGPoint(x:CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x,
                       y:CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y)
    }
}

//Extension borrowed from here:  https://stackoverflow.com/a/33292919

extension CGPoint {
    func distance(point: CGPoint) -> CGFloat {
        return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
    }
}

Now there is a Fish class like yours, which has few methods and a physics body which is used just to detect contact between food and a fish, but that's all from the physics. Here is the Collider struct just in case that you want to know how I have defined it:

struct Collider{
            static let food : UInt32 = 0x1 << 0
            static let fish : UInt32 = 0x1 << 1
            static let wall : UInt32 = 0x1 << 2
        }

Now back to the Fish class...I have put comments in the code, so I guess it is not needed to explain what those methods do. Here is the code:

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)
        }
    }
}

So basically, there are methods to move the fish around while it is not in a chase for a food. Also there is method which stop this (infinite) action (SKAction). The most important method is the chase(within rect:) method. That method is called in scene's update() method and defines how and when fish will (try to) chase the food.

Now the GameScene:

//MARK: GameScene
class GameScene: SKScene, SKPhysicsContactDelegate {

    private var nodesForRemoval:[SKNode] = []
    private var water = SKSpriteNode()

    override func didMove(to view: SKView) {

        physicsWorld.contactDelegate = self
        physicsWorld.gravity = CGVector(dx: 0.0, dy: -0.5)
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        physicsBody?.categoryBitMask = Collider.wall
        physicsBody?.contactTestBitMask = 0x0
        physicsBody?.collisionBitMask = Collider.fish | Collider.food
        self.backgroundColor = .white

        //Water setup
        water = SKSpriteNode(color: .blue, size: CGSize(width: frame.width, height: frame.height - 150))
        water.position = CGPoint(x: 0, y: -75)
        water.alpha = 0.3
        addChild(water)
        water.zPosition = 4

        //Fish one
        let fish = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
        addChild(fish)
        fish.position = CGPoint(x: frame.midX-50, y: frame.minY + 100)
        fish.zPosition = 5

        fish.moveAround(within: water.frame)

        //Fish two
        let fish2 = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
        addChild(fish2)
        fish2.position = CGPoint(x: frame.midX+50, y: frame.minY + 100)
        fish2.zPosition = 5

        fish2.moveAround(within: water.frame)

    }


    func feed(at position:CGPoint, with food:SKSpriteNode){

        food.position = CGPoint(x: position.x, y: frame.size.height/2 - food.frame.size.height)
        addChild(food)
    }

    //MARK: Food factory :)
    func getFood()->SKSpriteNode{

        let food = SKSpriteNode(color: .purple, size: CGSize(width: 10, height: 10))

        food.physicsBody = SKPhysicsBody(rectangleOf: food.frame.size)
        food.physicsBody?.affectedByGravity = true
        food.physicsBody?.categoryBitMask = Collider.food
        food.physicsBody?.contactTestBitMask =  Collider.fish
        food.physicsBody?.collisionBitMask = Collider.wall
        food.physicsBody?.linearDamping = (0.1 ... 0.95).random()
        food.name = "food"
        return food
    }

    //MARK: Feeding
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        if let touch = touches.first {

            let location = touch.location(in: self)

            let food = getFood()
            feed(at: location, with: food)
        }
    }

    //MARK: Eating
    func didBegin(_ contact: SKPhysicsContact) {


        guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {

            //Silliness like removing a node from a node tree before physics simulation is done will trigger this error
            fatalError("Physics body without its node detected!")
        }

        let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

        switch mask {

           //Contact between fish and a food
          case Collider.fish | Collider.food:

            if let food = (contact.bodyA.categoryBitMask == Collider.food ? nodeA : nodeB) as? SKSpriteNode
            {
                self.nodesForRemoval.append(food)
            }

        default:
            //some unknown contact occurred
            break
        }
    }

    //MARK: Removing unneeded nodes
    override func didSimulatePhysics() {

        for node in self.nodesForRemoval {
            node.removeFromParent()
        }
        self.nodesForRemoval.removeAll()
    }

    //MARK: Chasing the food
    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)
            }
        }
    }
}

And that's it. Here we setup our nodes, resolve contact detections and tell which fish should chase which food node. I left the comments, so everything is there. I hope that methods are pretty much self explanatory, but of course you can ask me for details about how things works.

And here is a short video of how this works:

enter image description here

and the longer version because I can only upload two 2 megabytes: screen recording

Basically, fish don't chase the food node if it is not its defined range. Still, fish will chase the locked node until it eats it. But if there is some other food which is more close, the fish will chase for that food node. Of course this is not a must, and you can tweak it as you like (check isChasing:) method.

Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • Thank you! Great answer. – Grumme May 26 '17 at 19:24
  • @Grumme You are welcome! Note that things like `swimmingSpeed` or `sensorRadius` might be passed through custom initializer of a Fish class. That way you can make every fish different. – Whirlwind May 26 '17 at 19:29
  • Also in `viewDidLoad()` of `GameViewController` when scene is loaded from the sks file, you should set `scene.size = view.bounds.size` and set the scale mode of aspectFill, in order to this code work as in the video. – Whirlwind May 26 '17 at 19:40
  • I just implemented and started testing your solution @Whirlwind, but it seems like my fishes is speeded up? When there is food within the radius, the fish gets 'boosted'. Please have a look at this video: https://giphy.com/gifs/3o7btUkxKM96u9iVk4 The fishes should keep a constant speed :). – Grumme May 27 '17 at 09:01
  • @Grumme Of course it is up to you to set the correct speed. :) You will likely have to set other things. I just showed you in which way you may go, to implement randomized moving + chasing target. – Whirlwind May 27 '17 at 11:16
  • But your video shows a lot more constant moving. In the video I posted, it seems like it speeds extremely up, no food bypass the fishes. I have tried changing the swimmingSpeed, and yes - The fish swims slower, until it find food - Then it accelerates as shown in the video @Whirlwind. – Grumme May 27 '17 at 11:18
  • @Grumme That is one thing I leave to you to try to solve :) I am not at the computer right now, so if you are still stuck later today, ask another question and I will answer it. – Whirlwind May 27 '17 at 11:21
  • Fair enough @Whirlwind. Can you give me a clue ;)? – Grumme May 27 '17 at 11:24
  • @Grumme Well it is about speed formula : speed = distance / time. you can calculate time from that formula easily and use it if needed (if you use duration parameter in SKAction). – Whirlwind May 27 '17 at 12:22
  • Here is the answer to your question : https://stackoverflow.com/a/44233003/3402095 I have just added the link for the possible future readers. – Whirlwind May 28 '17 at 23:40