In my game, I have a ball, walls and gaps between these walls. Ball and walls are SKSpriteNode, gap is SKNode. I use gap to count the score.
I have the following code to handle the contacts and collisions. I checked plenty of documents and tutorials but cant find why there are multiple gap contacts happen when my ball contacts the gap. It prints "gap contact" for 6 times every time.
Bodytypes definitions:
enum BodyType:UInt32 {
case ball = 1
case wall = 2
case gap = 4
}
Ball:
var ball = SKSpriteNode(texture: SKTexture(imageNamed: "img/ball.png"))
ball.position = CGPointMake(frame.width/2, 50)
ball.physicsBody = SKPhysicsBody(texture: ball.texture, size: ball.size)
ball.physicsBody?.dynamic = true
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.categoryBitMask = BodyType.ball.rawValue
ball.physicsBody?.collisionBitMask = BodyType.wall.rawValue
ball.physicsBody?.contactTestBitMask = BodyType.gap.rawValue | BodyType.wall.rawValue
Walls (walls are created through scheduledTimerWithTimeInterval every 2 seconds):
var wall1 = SKSpriteNode(texture: nil, color: UIColor.blackColor(), size: CGSizeMake(frame.size.width, 20))
var wall2 = SKSpriteNode(texture: nil, color: UIColor.blackColor(), size: CGSizeMake(frame.size.width, 20))
let gapSize = 70
let gapMargin = 50
let gapPoint = CGFloat(arc4random_uniform(UInt32(frame.size.width) - gapSize - (gapMargin*2)) + gapMargin)
wall1.position = CGPointMake(-wall1.size.width/2 + gapPoint, self.frame.size.height)
wall2.position = CGPointMake(wall1.size.width/2 + CGFloat(gapSize) + gapPoint, self.frame.size.height)
wall1.physicsBody = SKPhysicsBody(rectangleOfSize: wall1.size)
wall1.physicsBody?.dynamic = false
wall2.physicsBody = SKPhysicsBody(rectangleOfSize: wall2.size)
wall2.physicsBody?.dynamic = false
wall1.physicsBody?.categoryBitMask = BodyType.wall.rawValue
wall2.physicsBody?.categoryBitMask = BodyType.wall.rawValue
Gap Node:
var gap = SKNode()
gap.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(CGFloat(gapSize), wall1.size.height))
gap.position = CGPointMake(CGFloat(gapSize)/2 + gapPoint, self.frame.size.height)
gap.runAction(moveAndRemoveWall)
gap.physicsBody?.dynamic = false
gap.physicsBody?.collisionBitMask = 0
gap.physicsBody?.categoryBitMask = BodyType.gap.rawValue
Contact Handling:
func didBeginContact(contact: SKPhysicsContact) {
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(contactMask) {
case BodyType.ball.rawValue | BodyType.gap.rawValue:
println("gap contact")
default:
println("wall contact")
movingObjects.speed = 0
explosion(ball.position)
gameOver = 1
}
}
Wall collision works perfectly and prints "wall contact" only once when I hit the wall but I couldnt fix this multiple contact issue between the ball and gap.
UPDATE: The problem is with the ball's physicsbody. If I use circleOfRadius it works perfectly but then, this doesnt provide a great experience to the player as my ball shape is not a perfect circle so I want to use the texture to cover the physicsbody.
Related
I am building a macOS SwiftUI app. I want to show the world axis such that the user is aware of the orientation of objects. I've looked at the documentation, but the showWorldOrigin debug setting is not available on macOS. Is there an alternative way to show the world axis that I am missing?
While I've found external libraries that create a world axis and add nodes to the scene, I was hoping there was a built-in method to simplify the task and reduce any error.
You can create your own sample of procedural world axis for macOS 3D app.
SwiftUI mac version
import SwiftUI
import SceneKit
struct ContentView : View {
#State private var scene = SCNScene()
#State private var axis = SCNNode()
var options: SceneView.Options = [.allowsCameraControl]
var body: some View {
ZStack {
SceneView(scene: scene, options: options).ignoresSafeArea()
let _ = scene.background.contents = NSColor.black
let _ = createWorldAxis()
let _ = axis.opacity = 0.1 // you can hide world axis
}
}
func createWorldAxis() {
let colors: [NSColor] = [.systemRed, .systemGreen, .systemBlue]
for index in 0...2 {
let box = SCNBox(width: 0.200, height: 0.005,
length: 0.005, chamferRadius: 0.001)
let material = SCNMaterial()
material.lightingModel = .constant
material.diffuse.contents = colors[index]
box.materials[0] = material
let node = SCNNode(geometry: box)
switch index {
case 0:
node.position.x += 0.1
case 1:
node.eulerAngles = SCNVector3(0, 0, Float.pi/2)
node.position.y += 0.1
case 2:
node.eulerAngles = SCNVector3(0, -Float.pi/2, 0)
node.position.z += 0.1
default: break
}
axis.addChildNode(node)
axis.scale = SCNVector3(1.5, 1.5, 1.5)
scene.rootNode.addChildNode(axis)
}
print(axis.position)
}
}
Cocoa version
import Cocoa
import SceneKit
class ViewController : NSViewController {
var axis = SCNNode()
var sceneView = SCNView()
override func viewDidLoad() {
super.viewDidLoad()
sceneView = self.view as! SCNView
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.backgroundColor = .black
self.createWorldAxis()
axis.opacity = 0.1 // you can hide world axis
}
func createWorldAxis() {
let colors: [NSColor] = [.systemRed, .systemGreen, .systemBlue]
for index in 0...2 {
let box = SCNBox(width: 0.200, height: 0.005,
length: 0.005, chamferRadius: 0.001)
let material = SCNMaterial()
material.lightingModel = .constant
material.diffuse.contents = colors[index]
box.materials[0] = material
let node = SCNNode(geometry: box)
if index == 0 {
node.position.x += 0.1
} else if index == 1 {
node.eulerAngles = SCNVector3(0, 0, Float.pi/2)
node.position.y += 0.1
} else if index == 2 {
node.eulerAngles = SCNVector3(0, -Float.pi/2, 0)
node.position.z += 0.1
}
axis.addChildNode(node)
axis.scale = SCNVector3(1.5, 1.5, 1.5)
sceneView.scene?.rootNode.addChildNode(axis)
}
print(axis.position)
}
}
Posting this answer, as per OP comments...
Apple's docs list the following SCNDebugOptions:
.showPhysicsShapes
.showBoundingBoxes
.showLightInfluences
.showLightExtents
.showPhysicsFields
.showWireframe
.renderAsWireframe
.showSkeletons
.showCreases
.showConstraints
.showCameras
.showFeaturePoints
.showWorldOrigin
Curiously, the last two - .showFeaturePoints and .showWorldOrigin - are not defined in SceneKit. And the discussion notes refer only to ARKit, where they are defined.
The docs for SCNDebugOptions state that these are bit mask patterns ... and if we print them out, we get:
showPhysicsShapes: SCNDebugOptions(rawValue: 1)
showBoundingBoxes: SCNDebugOptions(rawValue: 2)
showLightInfluences: SCNDebugOptions(rawValue: 4)
showLightExtents: SCNDebugOptions(rawValue: 8)
showPhysicsFields: SCNDebugOptions(rawValue: 16)
showWireframe: SCNDebugOptions(rawValue: 32)
renderAsWireframe: SCNDebugOptions(rawValue: 64)
showSkeletons: SCNDebugOptions(rawValue: 128)
showCreases: SCNDebugOptions(rawValue: 256)
showConstraints: SCNDebugOptions(rawValue: 512)
showCameras: SCNDebugOptions(rawValue: 1024)
So... we try this to get "the next one in order" (expecting it to equate to .showFeaturePoints):
sceneView.debugOptions = SCNDebugOptions(rawValue: 2048)
Turns out, that gives us RGB axis indicators ... the .showWorldOrigin.
For a simple scene with an extruded bezier path (a cube), using these options:
sceneView.debugOptions = [.renderAsWireframe, SCNDebugOptions(rawValue: 2048)]
we get this output:
Trying one further, thinking maybe we get .showFeaturePoints:
sceneView.debugOptions = SCNDebugOptions(rawValue: 4096)
doesn't seem to do anything - at least, I don't see any visual change in my simple Scene.
I found what i wanted using the UI. First, ensure you enable controls for your scene:
myscene.showsStatistics = true
Then, click the configuration button on the bottom of your screen.
In the options dropdown select World Origin.
I am puzzled why all debug options can be invoked programmatically except the World Origin. None the less, that allows you to see axis.
I'm play around with the CAKeyframeAnimation in order to better understanding how this type of animation work.
I want to move my SCNnode in a square shape and at every corner rotate his eulerangle Y of 90 degrees to make it follow the orientation of the track
here my code
func animatePlaneKey(nodeToAnimate: SCNNode){
// move forward
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1) // 1
let pos4 = SCNVector3(pos.x - 1 , pos.y, pos.z + 1)
let pos5 = SCNVector3(pos.x, pos.y, pos.z)
let easeIn = CAMediaTimingFunction(controlPoints: 0.35, 0.0, 1.0, 1.0)
animation.values = [pos1,pos2, pos3,pos4,pos5]
animation.keyTimes = [0,0.25,0.5,0.75,1]
animation.timingFunctions = [easeIn]
animation.calculationMode = .cubic
animation.duration = 12
animation.repeatCount = 1
animation.isAdditive = false
animation.autoreverses = false
// Heading 1st
let firstTurnAnim = CAKeyframeAnimation(keyPath: "eulerAngles.y")
let heading = nodeToAnimate.eulerAngles.y
let rot0heading = heading
let rot2heading = heading - Float(deg2rad(90))
firstTurnAnim.values = [rot0heading,rot2heading]
firstTurnAnim.keyTimes = [0.2,0.3]
firstTurnAnim.duration = 3
firstTurnAnim.repeatCount = 1
firstTurnAnim.isAdditive = true
firstTurnAnim.autoreverses = false
// // Heading 2st
let secondTurnAnim = CAKeyframeAnimation(keyPath: "eulerAngles.y")
let heading2 = nodeToAnimate.eulerAngles.y
let rot1head0 = heading2
let rot1head1 = heading2 - Float(deg2rad(180))
secondTurnAnim.values = [rot1head0,rot1head1]
secondTurnAnim.keyTimes = [0.45,0.55]
secondTurnAnim.duration = 6
secondTurnAnim.repeatCount = 1
secondTurnAnim.isAdditive = true
secondTurnAnim.autoreverses = false
nodeToAnimate.addAnimation(animation, forKey: "movement")
nodeToAnimate.addAnimation(firstTurnAnim, forKey: "turn")
nodeToAnimate.addAnimation(secondTurnAnim, forKey: "turn2")
}
I'm struggling to combine the animation of the y axis at the correct time.
when I add the "turn2" the animation start to mess up everything's, the node appear already rotate in the wrong direction.
For my understanding turn2 animation should start at keyframe 0.45 and finish at 0.55, why it start immediately ?
any idea what should be the correct way to combine this animation?
I have couple of small paddles alligned next to each other with size of roughly 50x10 moving from left to right and right to left constantly, which would also randomly stop moving from time to time and become stationary. There is a ball that falls off towards them, and upon hitting a paddle, if its not moving and is stationary, the ball will bounce off it and go straight back up. However, if the paddles are moving and the ball hits one of the paddles during that time, instead of the ball hitting them and going right back up, it hits the paddle and starts moving either right or left (depending on which direction the paddles are moving) while going back up. I'm trying to have the ball go right back up even if it hits the paddle while its moving (pretty much a forever bouncing ball against the paddles).
I have tried couple of different ways to solve this, but I think I'm missing something important that's making it behave like this. Here is the code:
class GameScene: SKScene, SKPhysicsContactDelegate {
let greenPaddle = SKSpriteNode(imageNamed: "Green")
var ball = SKSpriteNode()
let ballCategory: UInt32 = 1 << 0
let greenPaddleCategory: UInt32 = 1 << 1
override func didMoveToView(view: SKView) {
self.physicsWorld.contactDelegate = self
greenPaddle.position = CGPoint(x: 0, y: frame.size.height / -6.5)
greenPaddle.physicsBody = SKPhysicsBody(rectangleOfSize: greenPaddle.size)
greenPaddle.physicsBody!.affectedByGravity = false
greenPaddle.physicsBody!.dynamic = false
greenPaddle.physicsBody!.categoryBitMask = greenPaddleCategory
greenPaddle.physicsBody!.contactTestBitMask = ballCategory
ball = SKSpriteNode(imageNamed: "Red")
ball.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2)
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.height / 2)
ball.physicsBody!.friction = 0
ball.physicsBody!.restitution = 1
ball.physicsBody!.linearDamping = 0
ball.physicsBody!.allowsRotation = false
ball.physicsBody!.categoryBitMask = ballCategory
ball.physicsBody!.contactTestBitMask = greenPaddleCategory
addChild(circle)
}
func didBeginContact(contact: SKPhysicsContact) {
let bodyA = (contact.bodyA.categoryBitMask <= contact.bodyB.categoryBitMask) ? contact.bodyA : contact.bodyB
let bodyB = (contact.bodyA.categoryBitMask <= contact.bodyB.categoryBitMask) ? contact.bodyB : contact.bodyA
if (bodyA.categoryBitMask) == ballCategory && (bodyB.categoryBitMask) == greenPaddleCategory {
greenPaddle.physicsBody!.friction = 0
greenPaddle.physicsBody!.restitution = 0
}
}
I do not really understand the problem; however, you have forgotten to set the physics world in the didmovetoview.
self.physicsWorld.contactDelegate = self
If the problem is that the contact do not work, this would work:)
I have a roof Sprite Node that i would like for the Player Sprite Node to bounce off of, but when they touch it runs my collision function.
Roof and player physics category:
struct PhysicsCatagory {
static let Player : UInt32 = 0x1 << 1
static let Roof : UInt32 = 0x1 << 6
}
Player and Roof code:
Player = SKSpriteNode(imageNamed: "plane")
Player.size = CGSize(width: 60, height: 70)
Player.physicsBody = SKPhysicsBody(rectangleOfSize: Player.size)
Player.position = CGPoint(x: self.frame.width / 2 - Player.frame.width, y: self.frame.width / 2)
Player.physicsBody?.categoryBitMask = PhysicsCatagory.Player
Player.physicsBody?.collisionBitMask = PhysicsCatagory.Ground | PhysicsCatagory.Mine | PhysicsCatagory.Ep | PhysicsCatagory.Bullet
Player.physicsBody?.contactTestBitMask = PhysicsCatagory.Ground | PhysicsCatagory.Mine | PhysicsCatagory.Ep | PhysicsCatagory.Bullet
Player.physicsBody?.affectedByGravity = false
Player.physicsBody?.dynamic = false
Player.zPosition = 4
Roof.size = CGSize(width: self.frame.width, height: 40)
Roof.physicsBody = SKPhysicsBody(rectangleOfSize: Roof.size)
Roof.position = CGPoint(x: self.frame.width / 2, y: self.frame.height - 20)
Roof.physicsBody?.categoryBitMask = PhysicsCatagory.Roof
Roof.physicsBody?.affectedByGravity = false
Roof.physicsBody?.dynamic = false
Roof.zPosition = 4
And here is my code for when a collision occurs:
func didBeginContact(contact: SKPhysicsContact) {
game = false
end = true
soundy = false
self.removeAllChildren()
self.removeAllActions()
endScreen()
}
I have tried changing their identities to 0 but then the player just goes through the roof. If you know anything about this please leave any recommendations or suggestions below. Thank You
remove contactTestBitmask if you dont want didBeginContact to occur
Here is problem in which I add zoombie sprite to the scene every one second. When I add another sub animated zoombie to the zoombie node, sometimes it loads animated texture, and other times appear red large X.
func addMonster() {
let zoombieSprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(40, 60))
// Determine where to spawn the monster along the Y axis
let actualY = randRange(lower: zoombieSprite.size.height, upper: size.height - zoombieSprite.size.height)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
zoombieSprite.position = CGPoint(x: size.width + zoombieSprite.size.width/2, y: actualY)
zoombieSprite.physicsBody = SKPhysicsBody(rectangleOfSize: zoombieSprite.size) // 1
zoombieSprite.physicsBody?.dynamic = true // 2
zoombieSprite.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
zoombieSprite.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
zoombieSprite.physicsBody?.collisionBitMask = PhysicsCategory.None // 5
addChild(zoombieSprite)
//zoombieSprite.addChild(createAnimatedZoombie())
let zoombieAnimation = SKAction.runBlock({
zoombieSprite.addChild(self.createAnimatedZoombie())
})
// Determine speed of the monster
let actualDuration = randRange(lower: 6.0, upper: 10.0)
//print("actualDuration = \(actualDuration)")
let actionMove = SKAction.moveTo(CGPoint(x: -zoombieSprite.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
// Create the actions
let actionMoveDone = SKAction.removeFromParent()
zoombieSprite.runAction(SKAction.sequence([zoombieAnimation ,actionMove,actionMoveDone]))
}
//MARK: - ANIMATE FRAME AND MOVE ZOOMBIE
func createAnimatedZoombie () -> SKSpriteNode {
let animatedZoobieNode = SKSpriteNode(texture: spriteArray[0])
let animationFrameAction = SKAction.animateWithTextures(spriteArray, timePerFrame: 0.2)
let durationTime = SKAction.waitForDuration(0.1)
let repeatAction = SKAction.repeatActionForever(animationFrameAction)
let quenceAction = SKAction.sequence([durationTime, repeatAction])
animatedZoobieNode.runAction(quenceAction)
return animatedZoobieNode
}
Thanks very much my respectable brother Joseph Lord and Thank God i solved my problem by just dividing sprite kit atlas array count property by 2 because in this folder i had put both #2x and #3x images so when i used to get number of images from this atlas folder it used to return the number which was addition of #2x and #3x images.