4

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.

peacetype
  • 1,928
  • 3
  • 29
  • 49
  • 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 May 18 '17 at 12:52
  • 1
    `Sensor` and `Handle` have separate `SKPhysicsBody`s. Different masses,... Not sure what to expect in the case you apply an impulse to the `Handle`. – andih May 18 '17 at 13:35
  • I added details about this approach in a post on the [Game Development Stack Exchange](https://gamedev.stackexchange.com/questions/141270/2d-infinite-scrolling-terrain-on-two-axes) which may help clarify this question. – peacetype May 18 '17 at 18:09

1 Answers1

3

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

Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • Thanks, it works! I had tried setting `physicsBody?.pinned = true` on the sensor but that also requires the parent to have a physics body which I want to avoid. However I overlooked using a joint. One line your code is missing is a call for `self.physicsWorld.add(pin)` after creating the pin. Maybe that seems obvious but newcomers to SpriteKit like myself may have to hunt for it. – peacetype May 18 '17 at 14:19
  • @peacetype Yeah, you are right, newcomers might get confused...I have added that line in my answer. – Whirlwind May 18 '17 at 14:29
  • I'll leave this unanswered for now; still hoping someone might figure out why the velocity is causing the child to separate. It's great to have a solution but it really bothers me when some behavior runs counter to a concept I thought I understood (i.e. child nodes move with their parent nodes). – peacetype May 18 '17 at 14:43
  • @peacetype Child node in this case has nothing with a parent node when it comes to physics simulation. When you move handle with pan by changing its position, because startTile is a child of handle, it moves along with it. But when you stop moving handle, and apply impulse to a handle, there is nothing to move the startTile. Thus your error. You might wait of course for others to comment on this :), but that is a whole story really. I just didn't know you are unaware of why this is happening. – Whirlwind May 18 '17 at 14:48
  • But in this case the tile does continue moving after you release your gesture. If I comment out your joint pin code and run the project, when I do a pan gesture the tile (green box) keeps moving after I let go while the sensor (red box) stays still. The handle is an invisible node with a physics body and the green tile is attached to it. My only thought is that the red sensor doesn't stick with the handle's velocity because it has its own physics body while the green tile does not. That's the only differentiating factor I can see here. – peacetype May 18 '17 at 14:58
  • @peacetype I will check out your node structure later, but in general, node's visual rendering has nothing with a physics world. So if you put one node into another one, and apply different forces to their physics bodies, they will move in a different ways. Also if you apply impulse to one node, the other with a physics body will stay in place (assuming that both physics bodies have collisionBitMask = 0). – Whirlwind May 18 '17 at 15:07
  • OK, that sounds like it's probably the answer to my mystery. In this case the invisible handle object has a physics body and is receiving the velocity. The green tile has no physics body so it is simply following its parent handle. And the red sensor has a physics body but it's not getting a velocity boost, so it is staying behind while the handle's physics body is moving due to its velocity. – peacetype May 18 '17 at 15:18
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/144581/discussion-between-whirlwind-and-peacetype). – Whirlwind May 18 '17 at 15:22