4
votes

I'm trying to make infinite scrolling terrain by programmatically adding/deleting textured tiles as the player pans the view. New tiles should only be added next to existing tiles that have an open edge. To detect if a tile has an open edge, I plan to attach a small physics body that sticks out from all 4 sides of the tiles to act as a sensor. If the sensor contacts any other sensors, we know that edge of the tile is not open.

The problem I'm having is that the sensors do not always stay aligned with the tiles. To show this problem, I created a SpriteKit project with the code below.

Touch behavior includes a gesture recognizer in the GameScene class which causes the invisible Handle object to move. When the gesture ends, I use the handle's physics body to give it a little velocity on this line:

handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))

I'm also creating a Tile object (big green square below) and adding it as a child of the invisible handle. That's great, now all child tiles I add will move along with their parent handle.

Whenever a tile is instantiated, a Sensor object (small red square below) is created and added as a child of the tile. That's also great, now all sensors will move along with their parent tile which in turn moves with its parent, the invisible handle. There's just one problem...

When I pan the screen, both the green tile and its red sensor (shown below) move together in unison, as expected. When I release my pan gesture, the extra kick of velocity I give to the handle also carries over to its child tile, also as expected. But that velocity does not affect the child sensor of the tile. As soon as I release the gesture, the sensor stops dead on the screen while the tile continues moving along with the handle until they both slow to a halt. The desired behavior is for the sensor to keep moving along with its parent tile.

Here's a link to a video that might show what's happening better than I can describe it: https://youtu.be/ccJKdZv-NsM

I can't understand why the tile is staying in sync with its parents motion but the sensor is not doing the same. Thanks for any insight into this problem.

Screenshot of result in scene

GameScene.swift:

import SpriteKit

class GameScene: SKScene {
    let handle = Handle()
    let startTile = Tile()

    override func didMove(to view: SKView) {
        self.backgroundColor = .white
        self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)

        self.addChild(handle)
        startTile.position.x = handle.anchorPoint.x
        startTile.position.y = handle.anchorPoint.y
        handle.addChild(startTile)

        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanFrom))
        panGestureRecognizer.cancelsTouchesInView = false
        panGestureRecognizer.delaysTouchesEnded = false
        self.view!.addGestureRecognizer(panGestureRecognizer)
    }

    func handlePanFrom(_ recognizer: UIPanGestureRecognizer) {
        if recognizer.state == .changed {
            var translation = recognizer.translation(in: recognizer.view)
            translation = CGPoint(x: translation.x, y: -translation.y)
            self.panForTranslation(translation)
            recognizer.setTranslation(.zero, in: recognizer.view)
        } else if recognizer.state == .ended {
            let velocity = recognizer.velocity(in: self.view)
            let multiplier = CGFloat(0.5)
            handle.physicsBody?.applyImpulse(CGVector(dx: velocity.x * multiplier, dy: -velocity.y * multiplier))
        }
    }

    func panForTranslation(_ translation: CGPoint) {
        let position = handle.position
        let newPosition = CGPoint(x: position.x + translation.x, y: position.y + translation.y)
        handle.position = newPosition
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        handle.physicsBody?.isResting = true
    }
}

Handle class:

import SpriteKit

class Handle : SKSpriteNode {
    init() {
        super.init(texture: nil, color: .clear, size: CGSize(width: 1, height: 1))
        self.physicsBody = SKPhysicsBody(rectangleOf: self.size)
        self.physicsBody?.mass = 1
        self.physicsBody?.linearDamping = 2
        self.physicsBody?.categoryBitMask = 0
        self.physicsBody?.contactTestBitMask = 0
        self.physicsBody?.collisionBitMask = 0
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

Tile class:

import SpriteKit

class Tile : SKSpriteNode {

    init() {
        super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))
        let sensorA = Sensor()
        self.addChild(sensorA)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

Sensor class:

import SpriteKit

class Sensor : SKSpriteNode {

    init() {
        super.init(texture: nil, color: .red, size: CGSize(width: 50, height: 50))
        self.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
        self.physicsBody?.categoryBitMask = 0b1
        self.physicsBody?.contactTestBitMask = 0b1
        self.physicsBody?.collisionBitMask = 0
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

UPDATE: The accepted answer provided by Whirlwind solved the problem I had with the child separating from the parent. I believe that the cause of the problem became clear in the comments of that answer.

My understanding of it is that the red square did not move because it has its own physics body which is not receiving any velocity after the handle stops moving. While the handle object (and its child tile) keeps moving because it does have velocity. So it sounds like the red box's own physics body was holding it back.

1
By the way, if anyone knows a better design approach for making endlessly scrolling terrain of this sort, I'm all ears. This is my first project in SpriteKit and I feel like there's a better solution here but I'm just not seeing it. - peacetype
Sensor and Handle have separate SKPhysicsBodys. Different masses,... Not sure what to expect in the case you apply an impulse to the Handle. - andih
I added details about this approach in a post on the Game Development Stack Exchange which may help clarify this question. - peacetype

1 Answers

3
votes

I don't really have a time to get into why your code does some things, but if you want to move another physics body along with handle's physics body, then you could pin it to it.

I will just modify your code to make it work, but you should worry by yourself about encapsulation. First make a sensor variable inside of Tile class visible to the outside world, so you can use it later in your scene:

class Tile : SKSpriteNode {
    let sensorA = Sensor()
    init() {
        super.init(texture: nil, color: .green, size: CGSize(width: 300, height: 300))

        self.addChild(sensorA)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

Then in you scene pin sensor to a handle:

   let pin = SKPhysicsJointPin.joint(withBodyA: self.startTile.sensorA.physicsBody!, bodyB: self.handle.physicsBody!, anchor: CGPoint.zero)
   startTile.sensorA.physicsBody?.allowsRotation = false
   self.physicsWorld.add(pin)

I guess this is what you wanted:

sensor