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")
}
}
Related
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've made a full width (constrained to edge 0 on both sides) but there are a couple of pixels not used, see image below:
I've heard this is due to leaving space for the thumb slider?
Are there any ways to fill this small hole in?
I thought about placing a small view where the gap is to plug it, but this seems pretty rough and hacky :(
Thanks!
Slider set up:
class TrackProgressSlider : BiggerThumbTouchUISlider {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.minimumValue = 0
self.maximumValue = 100
self.value = 0
self.backgroundColor = UIColor.clearColor()
self.transform = CGAffineTransformMakeScale(1.0, 2.0)
self.setThumbImage(UIImage(named: "progress-marker-half")!, forState: UIControlState.Normal)
self.minimumTrackTintColor = UIColor(hexString: "#f2f2f2")
self.maximumTrackTintColor = UIColor.clearColor()
}
}
Make use of trackRectForBounds(_ :)
class CustomSlider: UISlider {
override func trackRectForBounds(bounds: CGRect) -> CGRect {
var trackRect = super.trackRectForBounds(bounds)
trackRect.size.width = bounds.width
trackRect.origin.x = 0
return trackRect
}
}
This simple project inserts a square sprite in the center of the field when you press the button, which calls the add() function below. In the simulator, when you add multiple sprites, it pushes the others out of the way, so when you have pressed it a bunch of times you get...
screen shot from simulator, iphone 6, iOS 9.2, and this is the behavior I want.
But the same code running on my iphone, after adding the same number of sprites yields this... screen show from physical iphone 6, iOS 9.2
Here is the code from GameScene.swift:
import SpriteKit
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size:size)
self.physicsWorld.gravity = CGVectorMake(0, -1.0)
let worldBorder = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = worldBorder
self.physicsBody?.friction = 0.5
}
func add()
{
let sprite = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 10, height: 10))
sprite.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 8)
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.restitution = 0.5
sprite.physicsBody?.linearDamping = 0.5
addChild(sprite)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
}
Where am I going wrong? And how to I get the behavior I want on the real iPhone?
I'm not sure if which one is the desired behavior, but it does make sense that they wouldn't slide if you are stacking them literally right on top of each other. That being said, there is a way to get around this.
Keep in mind that nodes use floating point positioning, but of course they can only actually visibly be positioned by the pixel.
1 pixel would be 0.5 points on a 2x screen, and 0.33 points on 3x screen. With that in mind, you can use an offset of < 0.33 to get the physics bodies offset without it being visible to the user. Here's a little example:
class GameScene: SKScene {
var xOffset: CGFloat = 0.05
var yOffset: CGFloat = 0.3
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override init(size: CGSize) {
super.init(size:size)
self.physicsWorld.gravity = CGVectorMake(0, -1.0)
let worldBorder = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = worldBorder
self.physicsBody?.friction = 0.5
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
add()
updateOffsets()
}
func add()
{
let sprite = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 10, height: 10))
sprite.position = CGPointMake((self.frame.size.width / 2) + xOffset, (self.frame.size.height / 2) + yOffset)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 8)
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.restitution = 0.5
sprite.physicsBody?.linearDamping = 0.5
addChild(sprite)
}
private func updateOffsets() {
xOffset = -xOffset
yOffset = -yOffset
}
}
Switching the offsets is the important part. If you don't do that, you'll have the same problem of stacking exactly on top of each other. The offsets I used get pretty close to the simulator behavior, but a few taps in you'll notice it's a little different. Hopefully you don't care about matching the exact same pattern you're getting in simulator. It's a very similar one with this code. If you do care, you'll notice changing the offsets will change the pattern.
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 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.