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.
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.