I want to reproduce Super-Mario for iOS using Swift and SpriteKit. I use SKPhysics-bodies to simulate collisions between the player and the environment. The player and all objects have their own SKPhysicsBody (rectangle of their size). But when the player jumps against a brick (from top left or top right) like this, the player stucks in the air.
My assumption is that when the two SKSpriteNodes collide, they overlap a tiny bit. And the SKPhysics-Engine thinks that the player is on top of the middle brick because the player is a tiny bit inside the brick above and falls down on the middle brick.
So my question is: How can i prevent SKSPriteNodes from overlapping? Or how can i fix this?
P.S. If you need more information to answer, please tell me!
EDIT: Some Code of the Brick Class:
import SpriteKit
class Brick: SKSpriteNode {
let imgBrick = SKTexture(imageNamed: "Brick")
init(let xCoor: Float, let yCoor: Float) {
// position is a global variable wich is 1334/3840
super.init(texture: imgBrick, color: UIColor.clearColor(), size: CGSize(width: 80*proportion, height: 80*proportion))
position = CGPoint(x:Int(xCoor*667)+Int(40*proportion), y:Int(375-(yCoor*375))-Int(40*proportion))
name = "Brick"
physicsBody = SKPhysicsBody(rectangleOfSize: self.size)
physicsBody?.affectedByGravity = false
physicsBody?.dynamic = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and my Player Class
import SpriteKit
class Mario: SKSpriteNode {
let atlas = SKTextureAtlas(named: "Mario")
var imgMario = [SKTexture]()
var action = SKAction()
init(let xCoor: Float, let yCoor: Float) {
for(var i = 1; i < 24; i++) {
imgMario.append(atlas.textureNamed("X\(i)"))
}
super.init(texture: imgMario[0], color: UIColor.clearColor(), size: CGSize(width: 120*proportion, height: 160*proportion))
position = CGPoint(x:Int(xCoor)+Int(60*proportion), y:Int(yCoor)-Int(90*proportion))
name = "Mario"
physicsBody = SKPhysicsBody(rectangleOfSize: self.size)
physicsBody?.allowsRotation = false
action = SKAction.repeatActionForever(SKAction.animateWithTextures(imgMario, timePerFrame: 0.03, resize: false, restore: true))
runAction(action, withKey: "walking")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and SOME PART of my Level Class
class Level: SKScene, SKPhysicsContactDelegate {
var motionManager = CMMotionManager()
var mario = Mario(xCoor: 100, yCoor: 90)
var world = SKSpriteNode()
let MarioCategory: UInt32 = 0x1 << 0
let BrickCategory: UInt32 = 0x1 << 1
/*
Some more code...
*/
func setUpPhysics() {
physicsWorld.contactDelegate = self
mario.physicsBody!.categoryBitMask = marioCategory
mario.physicsBody?.collisionBitMask = brickCategory
mario.physicsBody?.contactTestBitMask = brickCategory
for(var i = 0; i < world.children.count; i++) {
if(world.children[i].name == "Brick") {
world.children[i].physicsBody?.categoryBitMask = brickCategory
world.children[i].physicsBody?.collisionBitMask = marioCategory
world.children[i].physicsBody?.contactTestBitMask = marioCategory
world.children[i].physicsBody?.friction = 0
}
}
}
}
Try setting UsePreciseCollisionDetection to true on your physicsbody.
mario.physicsBody?.usesPreciseCollisionDetection = true
https://developer.apple.com/reference/spritekit/skphysicsbody has some examples.
By setting this property to true could cause it to detect collisions more accurately so only when you mean to. I'd also rethink giving each block its own physics body. You only really care about the edge of a group of blocks rather than the edges of each individual block.
Another suggestion is that you can create a physics body based on a texture. So if your SKSpriteNode has a complex texture with transparent bits you can create another texture that is just a simple black block or similar this will help the engine detect more accurate collisions.
Related
I created a class that declare a ball of kind 'SKShapeNode'. in its init I set the 'SKPhysicsBody' properties. before I set these properties (leave it as default) when one ball touch another one the both affected by the touch (like: move to different position depends on the collision position). after I set the 'physicsBody' properties it won't affected any more - one ball appear to be above the other one (like frame on frame - hiding it). how can I set this property? I look at apple doc and did find nothing...
here is my code:
lass BallNode: SKShapeNode {
var radius:CGFloat = 0
var color:UIColor?
var strokeWidth:CGFloat = 0
private var _name:String?
override init() {
super.init()
self.fillColor = ranColor()
self.lineWidth = strokeWidth
}
convenience init(radius:CGFloat){
self.init(circleOfRadius: radius)
self.radius = radius
self.physicsBody = SKPhysicsBody.init(circleOfRadius: self.radius)
self.physicsBody?.affectedByGravity = true
print("ball physicsbody is init")
self.physicsBody?.restitution = 0.2
self.physicsBody?.linearDamping = 0.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The purpose of this "hitbox" is so the player will only be able to jump while walking on top of ground/platforms.The hitbox is a little less wide then the player and is on the players feet. Here's my player class:
class Player: SKSpriteNode {
let maxPlayerSpeed:CGFloat = 300
static var isPlayerOnGround = false
init() {
//players texture
let texture = SKTexture(imageNamed: "playerMove1")
super.init(texture: texture, color: SKColor.clear, size: texture.size())
//hitbox that sits underneath the player and follows him
let jumpHitBox = SKSpriteNode(color: .red, size: CGSize(width: texture.size().width - (texture.size().width / 8), height: texture.size().height / 5))
jumpHitBox.position.y = (-texture.size().height) + (texture.size().height / 2)
jumpHitBox.alpha = 0.5
jumpHitBox.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: jumpHitBox.size.width,
height: jumpHitBox.size.height))
jumpHitBox.zPosition = 3
jumpHitBox.physicsBody?.pinned = true
jumpHitBox.physicsBody?.allowsRotation = false
jumpHitBox.physicsBody?.categoryBitMask = CollisionTypes.playerJump.rawValue
jumpHitBox.physicsBody?.collisionBitMask = 0
jumpHitBox.physicsBody?.contactTestBitMask = CollisionTypes.ground.rawValue
addChild(jumpHitBox)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.categoryBitMask = CollisionTypes.player.rawValue
physicsBody?.contactTestBitMask = CollisionTypes.star.rawValue | CollisionTypes.saw.rawValue | CollisionTypes.finish.rawValue
physicsBody?.collisionBitMask = CollisionTypes.ground.rawValue
physicsBody?.affectedByGravity = true
physicsBody?.restitution = 0.2
physicsBody?.isDynamic = true
physicsBody?.allowsRotation = false
setScale(0.6)
zPosition = 5
physicsBody?.linearDamping = 0.0 }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) //error here
}
I want to use the jumpHitBox in this method as an additional contact.bodyA/B node in GameScene.swift:
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.node == player {
playerCollided(with: contact.bodyB.node!)
} else if contact.bodyB.node == player {
playerCollided(with: contact.bodyA.node!)
}
}
I don't know how to reference the the jumpHitBox child node from my player class in the didBegin in GameScene.swift.
Any advice is greatly appreciated.
EDIT: I'm getting an error at the
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) //error here
}
I dont put anything in here in my player class but since moving the jumpHitBox sprite to a global declaration I get an error at the super.init(coder: aDecoder) line in the required init saying: Property 'self.jumpHitBox' not initialized at super.init call
Although I would try the much simpler velocity.dy approach as mentioned in comments, your problem is that your hitbox is declared inside the scope of an initializer, so you can only access it there. If you give the hitbox a higher scope, such as a class property, then you can access it most anywhere:
class Player: SKSpriteNode {
let maxPlayerSpeed:CGFloat = 300
// This is probably going to cause you bugs later btw.. it should probably be
// just a regular property:
static var isPlayerOnGround = false
// Now you can just call playerInstance.jumpHitBox :
private(set) var jumpHitBox = SKSpriteNode()
init() {
//players texture
let texture = SKTexture(imageNamed: "playerMove1")
super.init(texture: texture, color: SKColor.clear, size: texture.size())
jumpHitBox = SKSpriteNode(color: .red, size: CGSize(width: texture.size().width - (texture.size().width / 8), height: texture.size().height / 5))
//hitbox that sits underneath the player and follows him
}
}
UPDATE:
class Player: SKSpriteNode {
let maxPlayerSpeed:CGFloat = 300
// This is probably going to cause you bugs later btw.. it should probably be
// just a regular property:
static var isPlayerOnGround = false
// Now you can just call playerInstance.jumpHitBox :
var jumpHitBox = SKSpriteNode()
private func makeHitBox() -> SKSpriteNode {
let texture = SKTexture(imageNamed: "playerMove1")
//hitbox that sits underneath the player and follows him
let localJumpHitBox = SKSpriteNode(color: .red, size: CGSize(width: texture.size().width - (texture.size().width / 8), height: texture.size().height / 5))
localJumpHitBox.position.y = (-texture.size().height) + (texture.size().height / 2)
localJumpHitBox.alpha = 0.5
localJumpHitBox.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: localJumpHitBox.size.width,
height: localJumpHitBox.size.height))
localJumpHitBox.zPosition = 3
localJumpHitBox.physicsBody?.pinned = true
localJumpHitBox.physicsBody?.allowsRotation = false
localJumpHitBox.physicsBody?.categoryBitMask = CollisionTypes.playerJump.rawValue
localJumpHitBox.physicsBody?.collisionBitMask = 0
localJumpHitBox.physicsBody?.contactTestBitMask = CollisionTypes.ground.rawValue
return localJumpHitBox
}
init() {
//players texture
let texture = SKTexture(imageNamed: "playerMove1")
super.init(texture: texture, color: SKColor.clear, size: texture.size())
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.categoryBitMask = CollisionTypes.player.rawValue
physicsBody?.contactTestBitMask = CollisionTypes.star.rawValue | CollisionTypes.saw.rawValue | CollisionTypes.finish.rawValue
physicsBody?.collisionBitMask = CollisionTypes.ground.rawValue
physicsBody?.affectedByGravity = true
physicsBody?.restitution = 0.2
physicsBody?.isDynamic = true
physicsBody?.allowsRotation = false
setScale(0.6)
zPosition = 5
physicsBody?.linearDamping = 0.0
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) //error here
jumpHitBox = makeHitBox()
addChild(jumpHitBox)
}
}
One way to reference jumpHitBox from outside its class is by making it a property of the Player class as in let jumpHitBox: SKSpriteNode, in the same way as you have declared maxPlayerSpeed to be a property of the Player class.
If you make this change, then remember to remove the let in this line let jumpHitBox = SKSpriteNode(color: .red .... since you now just need to assign a value to it, instead of declaring it. You should also now move the call to super.init to be after this line. Otherwise the compiler will complain, i.e. all properties of a class must be assigned a value before calling super.init.
The jumpHitBox should be now be accessible in didBegin by using player.hitBox
Hope this helps!
Here is a secondary answer showing a different approach to your problem. Note, the player only jumps when on the ground. This uses a "delayed frame" or "frame skip" or however you want to put it, because the jump command (pb.applyImpulse) is only called inside of didSimulatePhysics which means the character won't actually go any higher until the next frame.
// NOTE: This is a very simple example. The player bounces a bit on landing,
// which is essentially "landing lag" before you can jump again. In other words,
// the less the player bounces when contact the ground, the faster the player
// can jump again. There are ways to make this bouncing effect minimal / 0, but would
// require more work to implement, and I don't have any brilliantly simple ideas at the moment.
class GameScene : SKScene {
var player = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
var initialY = CGFloat(0)
var flag_wantsToJump = false
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
let pb = SKPhysicsBody(rectangleOf: player.size)
pb.restitution = 0
player.physicsBody = pb
addChild(player)
}
override func mouseDown(with event: NSEvent) {
// Tells game that we want to jump next frame:
flag_wantsToJump = true
}
override func update(_ currentTime: TimeInterval) {
// Give us new, initial frame data to compare against whatever our position.y will be
// later in the frame:
let curY = player.position.y
initialY = curY
}
override func didSimulatePhysics() {
// Determine whether or not we want to jump next frame:
guard flag_wantsToJump else { return }
// Determine whether or not we are allowed to jump:
let curY = player.position.y
print(curY, initialY)
if curY == initialY {
// Even though we are calling .applyImpulse this frame, it won't be processed
// until next frame--because we are calling this from inside `didSimulatePhysics`!
player.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 75))
}
}
override func didFinishUpdate() {
// Clear our flags for new mouse / touch input for next frame:
flag_wantsToJump = false
}
}
I'm making a simple tile-based game in SpriteKit and I'm having some difficulty with high CPU usage in my game. I have a map made of 60 tiles, and each tile is a subclass of SKSpriteNode. Just displaying these 60 sprites in the scene is using up to 80% of CPU in the iPhone 6s simulator. There is no motion, user interaction, or physics going on. When I made the same game in UIKit and not SpriteKit my CPU usage was 0. What could be using so much CPU?
my tile class:
import SpriteKit
import UIKit
class Tile: SKSpriteNode {
var tileType = "grass", tileX = 0, tileY = 0
init (tileType: String, tileX: Int, tileY: Int) {
self.tileType = tileType
self.tileX = tileX
self.tileY = tileY
let texture = SKTexture(imageNamed: tileType)
super.init(texture: texture, color: UIColor(), size: texture.size())
self.userInteractionEnabled = true
self.position = CGPoint(x: CGFloat(45+64*(tileX-1)), y: CGFloat(47+56*(tileY-1)))
self.zPosition = -1
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and my Sprite Kit scene code:
import SpriteKit
var map: [[String]] = [["grass","water","grass","rocky","rocky","grass","grass","grass","grass","water"],["grass","water","grass","grass","rocky","rocky","grass","grass","water","water"],["grass","water","water","grass","rocky","grass","grass","water","water","water"],["grass","grass","water","rocky","rocky","grass","grass","water","water","water"],["grass","grass","water","rocky","rocky","grass","water","water","water","water"],["grass","grass","water","rocky","rocky","grass","water","water","water","water"] ]
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
for (rowNumber, row) in map.enumerate() {
for (columnNumber, type) in row.enumerate() {
let theTile = Tile(tileType: type, tileX: columnNumber+1, tileY: rowNumber+1)
self.addChild(theTile)
}
}
self.backgroundColor = UIColor(colorLiteralRed: 0, green: 0, blue: 0, alpha: 0)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
Iphone Simulator on Xcode have a much greater CPU Usage than in a real device.
Test in a real device to have a real metric about CPU Usage.
Tyshka, you need to reuse resources. Your code creates a new SKTexture for the exact same input image data. Just create one SKTexture and then set that as the texture for the new node. Add a map to your code so that the SKTexture can be checked and reused given the texture string name.
In my application, I have items randomly popping up from the bottom of the screen. The way I set my code forced me to use hard coded values for x-position from where the items are popping up. Instead of those x-positions being hard coded, I want to change them to for instance GameScene.size.width / 4, so that the application's game play stays the same for all devices. Here is how I set up my code:
class Items{
var node = SKNode()
var item1 = SKSpriteNode(imageNamed: "CY")
var item2 = SKSpriteNode(imageNamed: "SY")
var item3 = SKSpriteNode(imageNamed: "PY")
var velocity = CGPoint.zero
var positionOffset = CGFloat(0)
var minVelocity = CGFloat(200)
init(pOffset: CGFloat) {
positionOffset = pOffset
node.zPosition = 1
node.addChild(item1)
node.addChild(item2)
node.addChild(item3)
node.hidden = true
}
....
class GameWorld{
var size = CGSize()
var node = SKNode()
var can1 = Items(pOffset: 208) //this is what is deciding the position
var can2 = Items(pOffset: 620)
init() {
node.addChild(can1.node)
node.addChild(can2.node)
}
....
The offset line in GameWorld class is what is deciding the position. What can I change in order to be able to say "GameScene.size.width / 4" for can 1 and similarly the same for can2 and not have an issue? I did try different ways to get it to work, but nothing seems to go my way. When I'm able to say GameScene.size.width / 4, for some reason, the app launches, but none of the gameplay loads up. Any help will be greatly appreciated.
Here is my GameScene Class:
class GameScene: SKScene {
... // added sprites but erased the code for now for space purposes
var touchLocation = CGPoint(x: 0, y: 0)
var nrTouches = 0
var rightTap: Bool = false
var leftTap: Bool = false
var delta: NSTimeInterval = 1/60
static var world = GameWorld()
override init(size: CGSize) {
super.init(size: size)
GameScene.world.size = size
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func didMoveToView(view: SKView) {
backgroundColor = UIColor.whiteColor()
let pos1 = CGPoint(x: size.width / 1.333, y: size.height / 1.1)
addChild(GameScene.world.node)
delta = NSTimeInterval(view.frameInterval) / 60
sprite1.position = pos1
sprite2.position = pos1
sprite3.position = pos1
sprite1.zPosition = 2
sprite2.zPosition = 2
sprite3.zPosition = 2
sprite2.hidden = true
sprite3.hidden = true
addChild(sprite1)
addChild(sprite2)
addChild(sprite3)
}
.....
Edit: added GameScene Class
For this scenario, you cannot declare:
var can1 = Items(pOffset: 208)
var can2 = Items(pOffset: 620)
The way you are doing it now. This is because at compile time, the game has no idea what the screen size is. Instead do not initialize the variable, just declare it like this:
var can1 : Items!
var can2 : Items!
Then this next part is tricky, if you are doing auto layouts, you need to make sure that the view is already created and sized before you present your scene. This does't happen till somewhere around viewDidAppear in the view controller
In your scenes didMoveToView, initialize your variables:
override func didMoveToView(view : SKView)
{
... //super and other code
can1 = Items(pOffset: view.frame.size.width/4)
can2 = Items(pOffset: view.frame.size.width/2)
}
I've been designing a game where I want the game to stop as soon as the ball makes contact with the ground. My function below is intended to set up the ground.
class GameScene: SKScene, SKPhysicsContactDelegate {
var ground = SKNode()
let groundCategory: UInt32 = 0x1 << 0
let ballCategory: UInt32 = 0x1 << 1
//Generic Anchor coordinate points
let anchorX: CGFloat = 0.5
let anchorY: CGFloat = 0.5
/*Sets the background and ball to be in the correct dimensions*/
override init(size: CGSize) {
super.init(size: size)
//Create the Phyiscs of the game
setUpPhysics()
setUpGround()
setUpBall()
}
func setUpPhysics() -> Void {
self.physicsWorld.gravity = CGVectorMake( 0.0, -5.0 )
self.physicsWorld.contactDelegate = self
}
func setUpGround() -> Void {
self.ground.position = CGPointMake(0, self.frame.size.height)
self.ground.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(self.frame.size.width, self.frame.size.height)) //Experiment with this
self.ground.physicsBody?.dynamic = false
self.ground.physicsBody?.categoryBitMask = groundCategory //Assigns the bit mask category for ground
self.ball.physicsBody?.contactTestBitMask = ballCategory //Assigns the contacts that we care about for the ground
/*Added in*/
self.ground.physicsBody?.dynamic = true
self.ground.physicsBody?.affectedByGravity = false
self.ground.physicsBody?.allowsRotation = false
/**/
self.addChild(self.ground)
}
func setUpBall() -> Void {
ball.anchorPoint = CGPointMake(anchorX, anchorY)
self.ball.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2)
self.ball.name = "ball"
self.ball.userInteractionEnabled = false
ball.physicsBody?.usesPreciseCollisionDetection = true
self.ball.physicsBody?.categoryBitMask = ballCategory //Assigns the bit mask category for ball
self.ball.physicsBody?.collisionBitMask = wallCategory | ceilingCategory //Assigns the collisions that the ball can have
self.ball.physicsBody?.contactTestBitMask = groundCategory //Assigns the contacts that we care about for the ball
addChild(self.ball) //Add ball to the display list
}
The problem I am noticing is that when I run the iOS Simulator, the Ground node is not being detected. What am I doing wrong in this case?
Is your ground node not showing up at all? I believe your ground should be an SKSpriteNode. Add that SKSpriteNode as a child of the SKNode object you created.
The SKNode is an empty node and the SKSpriteNode has an actual visual representation. And instead of doing it in a separate function, it would be much simpler if you did it in the DidMoveToView itself.
var objects = SKNode()
addChild(objects)
let ground = SKNode()
ground.position = .......
let colorsprite1 = SKSpriteNode(imageNamed: "yourimageoftheground")
ground.addChild(colorsprite1)
ground.physicsBody?.dynamic = false
Yes you were right to set ground's physics definition as not dynamic. I don't know why people are telling you otherwise. You don't want it to move upon contact with your ball thingies or whatever it is you have moving around in your scene.
Add all other physics definitions you need. Consider using a ContactTestBitMask as you want SpriteKit to let you know when a collision with the ground occurs, instead of SpriteKit handling it by itself. Details about that mask is easily available in StackOverFlow with examples.
objects.addChild(ground)
Hope this helps
Your ballCategory should be assigned to self.ground.contactBitMask not the ball.
You need to set the dynamic property to true in order for contact detection to work.
At the same time, you can set the following properties.
ground.affectedByGravity = false
ground.allowsRotation = false