Swift - How to add scene boundaries for specific nodes? - swift

I'm trying to develop an IOS game using SpriteKit, and I want to add a Physics body to the scene so that the player won't be able to go through the edges of the screen. At the same time, I want some nodes (for example - bombs that fall from the sky) to be able to go through the edges of the screen.
I know that I can use the following line to add a physics body to the scene:
self.physicsBody = SKPhysicsBody (edgeLoopFrom: self.frame)
My question is how can I allow a "bomb" object to go through such body while having a "player" object obligated to those boundaries.

The answer is relative to categoryBitMask and collisionBitMask of the involved physic bodies.
For example, for the scene:
if let scenePB = scene.physicsBody {
scenePB.categoryBitMask = 1
scenePB.collisionBitMask = 2 // collides with player
}
For the player:
if let playerPB = player.physicsBody {
playerPB.categoryBitMask = 2
playerPB.collisionBitMask = 1+4 // collides with scene and bombs
}
For any bomb:
if let bombPB = bomb.physicsBody {
bombPB.categoryBitMask = 4
bombPB.collisionBitMask = 2 // collides with player
}

Related

SKSpriteNode physics body created from texture results in nil value

I'm currently working on a small iOS game. In its current iteration, 20 targets spawn and move across the screen space-invaders style, and you control a little ship to shoot and destroy them. The code for my targets, the player ship's bullets, and a simple collision detection function I've written in the interim are as follows:
class Red_Target: SKSpriteNode{
var game_scene: GameScene!
private var ship_texture: SKTexture!
convenience init(scale: CGFloat, game_world: GameScene){
self.init(texture: SKTexture(imageNamed: "Proto Target"))
self.ship_texture = SKTexture(imageNamed: "Proto Target")
self.setScale(scale)
game_scene = game_world
game_scene.addChild(self)
self.position = CGPoint(x: game_scene.view!.bounds.width/10, y: 9 * game_scene.view!.bounds.height/10)
//self.physicsBody = SKPhysicsBody(texture: ship_texture, size: self.size)
self.physicsBody = SKPhysicsBody(circleOfRadius: 13)
self.physicsBody!.affectedByGravity = false
self.physicsBody!.collisionBitMask = 0x0
self.physicsBody!.categoryBitMask = CollisionType.Enemy.rawValue
self.physicsBody!.contactTestBitMask = CollisionType.Player_Bullet.rawValue
}
func move() {
self.run(space_invaders(scene: game_scene))
}
}
class PC_Bullet: SKSpriteNode{
convenience init(scale: CGFloat){
self.init(imageNamed: "Goodbullet")
self.setScale(scale)
self.physicsBody = SKPhysicsBody(circleOfRadius: 3)
self.physicsBody!.affectedByGravity = false
self.physicsBody!.categoryBitMask = CollisionType.Player_Bullet.rawValue
self.physicsBody!.collisionBitMask = 0x0
self.physicsBody!.contactTestBitMask = CollisionType.Enemy.rawValue
}
}
func didBegin(_ contact: SKPhysicsContact) {
contact.bodyA.node!.removeFromParent()
contact.bodyB.node!.removeFromParent()
}
}
This code, in its current iteration, works just fine. However, if the line defining the target's physicsbody as its texture is uncommented and the line defining physicsbody as circleOfRadius is removed, the game will consistently crash after the 5th target is destroyed, claiming that didBegin unwraps a nil value. Why does this only happen from physics bodies with textures? Is there any way I could change the code for this to work? I would love to be able to use the physics body from texture function later on, when working with more irregular shapes.
You are pulling a classic nooby mistake. You are removing your bodies too early. If you browse Stackoverflow you will find a plethera of ways to solve it.
The basic idea is do not remove your sprites until the end of the physics phase because your 1 sprite could have multiple contact points to handle. So come up with a way to flag sprites that need to be deleted, and remove them during the didSimulatePhysics function.

Stop sprite from leaving screen using physics bodies

I have a sprite that the user is able to move side to side by pressing on the left or right side of the screen. If you hold down on either side, the player sprite will leave the screen. I want to stop that from happening using physic bodies, but I can't seem to make it work.
To start, here are my categories.
//Categories for physics bodies
let sceneCategory:UInt32 = 0x1 << 0 // Equal to 1
let playerCategory:UInt32 = 0x1 << 1 // Equal to 2
Here is where I set the physics body of the scene itself.
self.physicsWorld.contactDelegate = self
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
self.physicsBody?.categoryBitMask = sceneCategory
self.physicsBody?.contactTestBitMask = playerCategory
self.physicsBody?.collisionBitMask = 0
self.physicsBody?.isDynamic = false
This is how I have the physics set for the player itself. I'm trying to set the physics body around the car itself. Is the way I have it setup the same as setting an alpha mask physics body?
let texture = SKTexture(imageNamed: "PorscheBlue")
player.physicsBody = SKPhysicsBody(texture: texture, size: player.size)
player.physicsBody?.isDynamic = false
player.physicsBody?.categoryBitMask = playerCategory
player.physicsBody?.contactTestBitMask = sceneCategory
player.physicsBody?.collisionBitMask = 0
Then, in the didBegin method, I just wanted it to print showing that the method was called. This isn't happening though for some reason.
func didBegin(_ contact: SKPhysicsContact)
{
print("called")
}
Why isn't the didBegin method being called? Do I have the physics set up properly? How can I make it so the player isn't allowed to leave the screen when moving?
Thank you
EDIT: So when having the physics boundaries visible, it looks like the boundaries for the scene are only being drawn on the top and bottom. I can't see any lines being drawn on the sides. That may be the issue, but I can't get it to show on the sides.
I resolved my issue by changing the dynamic value. I had both set to false, which causes the didBegin function to not be called. At least one of the nodes needs to be dynamic.

Swift Spritekit physicsBodies do collision bit masks in the SKScene editor

I have two balls. I don't want them to collide with each other. I want them to collide with everything else. I made the two balls have different category bit masks of 2 and 3. All the other objects have the same category bit mask of 1. I have tried setting the collision bit masks of both balls to 1. I also read something that said that an operation is done to the collision bit mask and the category bit mask and if it is nonzero then there is a collision, so I made the balls have the collision bit mask as the category bit mask of the other. So they would be 0 and everything else would be nonzero and collide. None of this works, so how do I actually make them so it works the way I want them to.
These are some of the masks I tried
Do it in code, it's so much easier to read.
class GameScene: SKScene, SKPhysicsContactDelegate {
weak var ball1: SKSpriteNode?
weak var ball2: SKSpriteNode?
let noCategory:UInt32 = 0
let ball1Category:UInt32 = 0b1
let ball2Category:UInt32 = 0b1 << 1
let wall1Category:UInt32 = 0b1 << 2
override func didMove(to view: SKView) {
self.physicsWorld.contactDelegate = self
ball1 = self.childNode(withName: "ball1") as? SKSpriteNode
ball2 = self.childNode(withName: "ball2") as? SKSpriteNode
wall1 = self.childNode(withName: "wall1") as? SKSpriteNode
ball1?.physicsBody?.categoryBitMask = ball1Category
ball1?.physicsBody?.collisionBitMask = noCategory
ball2?.physicsBody?.categoryBitMask = ball2Category
ball2?.physicsBody?.collisionBitMask = noCategory
wall1?.physicsBody?.categoryBitMask = wall1Category
wall1?.physicsBody?.collisionBitMask = ball1Category | ball2Category
}
}
Now you can easily see that wall1 collides with the ball1 and ball2 but the balls themselves don't collide with anything. With collisions only one of the physicsbodies needs to have the collisionbitmask set. Therefore you could add the ball1 and ball2Category to all physicsbodies collisionbitmasks except the opposite ball and the balls with collide with those physicsbodies but not each other.

How do I programmatically move an ARAnchor?

I'm trying out the new ARKit to replace another similar solution I have. It's pretty great! But I can't seem to figure out how to move an ARAnchor programmatically. I want to slowly move the anchor to the left of the user.
Creating the anchor to be 2 meters in front of the user:
var translation = matrix_identity_float4x4
translation.columns.3.z = -2.0
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
later, moving the object to the left/right of the user (x-axis)...
anchor.transform.columns.3.x = anchor.transform.columns.3.x + 0.1
repeated every 50 milliseconds (or whatever).
The above does not work because transform is a get-only property.
I need a way to change the position of an AR object in space relative to the user in a way that keeps the AR experience intact - meaning, if you move your device, the AR object will be moving but also won't be "stuck" to the camera like it's simply painted on, but moves like you would see a person move while you were walking by - they are moving and you are moving and it looks natural.
Please note the scope of this question relates only to how to move an object in space in relation to the user (ARAnchor), not in relation to a plane (ARPlaneAnchor) or to another detected surface (ARHitTestResult).
Thanks!
You don't need to move anchors. (hand wave) That's not the API you're looking for...
Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) β€” you can't move it someplace else because then it's not the point (1,1,1) anymore.
To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.
What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content β€” there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).
Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.
ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.
You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).
var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Start bouncing on touch after placing 2 anchors (don't allow more)
if anchors.count > 1 {
startBouncing(time: 1)
return
}
// Create anchor using the camera's current position
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
// Create a transform with a translation of 30 cm in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.3
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
anchors.append(anchor)
}
}
Then, some SpriteKit fun for making that animation happen:
var ballNode: SKLabelNode = {
let labelNode = SKLabelNode(text: "πŸ€")
labelNode.horizontalAlignmentMode = .center
labelNode.verticalAlignmentMode = .center
return labelNode
}()
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSKView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
addChild(ballNode)
}
ballNode.setScale(start.xScale)
ballNode.zRotation = start.zRotation
ballNode.position = start.position
let scale = SKAction.scale(to: end.xScale, duration: time)
let rotate = SKAction.rotate(toAngle: end.zRotation, duration: time)
let move = SKAction.move(to: end.position, duration: time)
let scaleBack = SKAction.scale(to: start.xScale, duration: time)
let rotateBack = SKAction.rotate(toAngle: start.zRotation, duration: time)
let moveBack = SKAction.move(to: start.position, duration: time)
let action = SKAction.repeatForever(.sequence([
.group([scale, rotate, move]),
.group([scaleBack, rotateBack, moveBack])
]))
ballNode.removeAllActions()
ballNode.run(action)
}
Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera β€” not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.
You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):
Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.
Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.
Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)
That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit β€” hence my comments about SpriteKit not being a great first step into ARKit.
If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)
Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:
// in global scope
func makeBillboardNode(image: UIImage) -> SCNNode {
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.firstMaterial!.diffuse.contents = image
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
return node
}
// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// emoji to image based on https://stackoverflow.com/a/41021662/957768
let billboard = makeBillboardNode(image: "⛹️".image())
node.addChildNode(billboard)
}
Finally, adding the animation for the bouncing ball:
let ballNode = makeBillboardNode(image: "πŸ€".image())
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSCNView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
sceneView.scene.rootNode.addChildNode(ballNode)
}
let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
animation.fromValue = start.transform
animation.toValue = end.transform
animation.duration = time
animation.autoreverses = true
animation.repeatCount = .infinity
ballNode.removeAllAnimations()
ballNode.addAnimation(animation, forKey: nil)
}
This time the animation code is a lot shorter than the SpriteKit version.
Here's how it looks in action.
Because we're working in 3D to start with, we're actually animating between two 3D positions β€” unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)

Centering the camera on a node in swift spritekit

I am creating a Terraria-style game in Swift. I want to have it so the player node is always in the center of the screen, and when you move right the blocks go left like in Terraria.
I am currently trying to figure out how to keep the view centered on the character. Does anyone know of a good way of accomplishing this?
Since iOS 9 / OS X 10.11 / tvOS, SpriteKit includes SKCameraNode, which makes a lot of this easier:
positioning the camera node automatically adjusts the viewport
you can easily rotate/zoom the camera by transform in the camera node
you can fix HUD elements relative to the screen by making them children of the camera node
the scene's position stays fixed, so things like physics joints don't break the way they do when you emulate a camera by moving the world
It gets even better when you combine camera nodes with another new feature, SKConstraint. You can use a constraint to specify that the camera's position is always centered on a character... or add extra constraints to say, for example, that the camera's position must stay within some margin of the edge of the world.
The below will center the camera on a specific node. It can also smoothly transition to the new position over a set time frame.
class CameraScene : SKScene {
// Flag indicating whether we've setup the camera system yet.
var isCreated: Bool = false
// The root node of your game world. Attach game entities
// (player, enemies, &c.) to here.
var world: SKNode?
// The root node of our UI. Attach control buttons & state
// indicators here.
var overlay: SKNode?
// The camera. Move this node to change what parts of the world are visible.
var camera: SKNode?
override func didMoveToView(view: SKView) {
if !isCreated {
isCreated = true
// Camera setup
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.world = SKNode()
self.world?.name = "world"
addChild(self.world)
self.camera = SKNode()
self.camera?.name = "camera"
self.world?.addChild(self.camera)
// UI setup
self.overlay = SKNode()
self.overlay?.zPosition = 10
self.overlay?.name = "overlay"
addChild(self.overlay)
}
}
override func didSimulatePhysics() {
if self.camera != nil {
self.centerOnNode(self.camera!)
}
}
func centerOnNode(node: SKNode) {
let cameraPositionInScene: CGPoint = node.scene.convertPoint(node.position, fromNode: node.parent)
node.parent.position = CGPoint(x:node.parent.position.x - cameraPositionInScene.x, y:node.parent.position.y - cameraPositionInScene.y)
}
}
Change what’s visible in the world by moving the camera:
// Lerp the camera to 100, 50 over the next half-second.
self.camera?.runAction(SKAction.moveTo(CGPointMake(100, 50), duration: 0.5))
Source: swiftalicio - 2D Camera in SpriteKit
For additional information, look at Apple's SpriteKit Programming Guide (Example: Centering the Scene on a Node).
You have to create World node that contains nodes. And you should put anchorPoint for example (0.5,0.5). Center on your player. And then you should move your player.
func centerOnNode(node:SKNode){
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: world!)
world!.position = CGPoint(x:world!.position.x - cameraPositionInScene.x, y: world!.position.y - cameraPositionInScene.y)
}
override func didSimulatePhysics() {
self.centerOnNode(player!)
}