iOS Sprites Contact Detection - swift

I'm working on a platform game where the character can climb ladders. Since the total length of each ladder varies, I have several ladders stacked on top of each other in SceneEditor. Upon entering a ladder, the contact delegate works fine and my code allows the character to move up the ladder. The problem I'm having is that as soon as the character moves off the first ladder segment, the didEnd method fires even though the character has entered the next ladder segment. I've gotten around it by given the ladder segments different category masks. Is this the only way to do it?

try overlapping the ladders by 1 rung, and then set a count whenever didBegin contact is fired increase the value by 1 whenever didEnd is called decrease the value by 1. at the end of the didEnd func check if onladderCount == 0. if it is, trigger whatever code is supposed to fire when the player is not on a ladder.
this assumes that you have a ladder class, and will have to put a property in the ladder class for onLadder to ensure that the += 1 isn't called multiple times.
var onladderCount: Int = 0
func didBegin(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
if (contactAName == "ladder") || (contactBName == "ladder") {
let ladder: Ladder? = (contact.bodyA.categoryBitMask == PhysicsCategory. ladder ? (contact.bodyA.node as? Ladder) : (contact.bodyB.node as? Ladder))
if !ladder.onLadder {
ladder.onLadder = true
onladderCount += 1
}
}
}
func didEnd(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
if (contactAName == "ladder") || (contactBName == "ladder") {
let ladder: Ladder? = (contact.bodyA.categoryBitMask == PhysicsCategory. ladder ? (contact.bodyA.node as? Ladder) : (contact.bodyB.node as? Ladder))
if ladder.onLadder {
ladder.onLadder = false
onladderCount -= 1
}
}
if onladderCount == 0 {
//trigger whatever code happens when they leave a ladder
}
}

Related

How to check if a node is within a certain distance of node and when it left that area?

Here is what I'm trying to do: In games, when approaching an NPC, players will be given an indicator to interact with the NPC. The indicator shows up when the player is within a certain distance of the npc. It also goes away when the player moves away from the NPC.
Here is what I tried: I had thought that it would be as easy as using the physics world methods of didBegin/didEnd contact and a transparent cylinder around the NPC as a contact trigger. This unfortunately didn't work because didBegin/didEnd methods are called every frame and not when contact is made (this is how I thought it worked).
I also tried to use PHYKit from GitHub but It didn't seem compatible to what I was trying to do.
I've thought about giving the NPC a Magnetic field and checking if player is within the scope of that field but it doesn't look like there is way to check for that (maybe I missed something).
I thought I could also use hitTestWithSegment but didn't understand how I can apply it to what I'm trying to do.
There also doesn't seem to be anything online to help with this (I've checked for the last three days so if there is anything I'm willing to see what it's about).
The Question: How can I check if a node is within a certain distance of another node and when it left that area?
I still think your physics answer works. Yeah it worked differently than I thought it did too, but you have to play around with it a bit and check it both ways:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact)
{
if(data.gameState == .run)
{
guard let nodeNameA = contact.nodeA.name else { return }
guard let nodeNameB = contact.nodeB.name else { return }
if(nodeNameA.prefix(5) == "Explo" && nodeNameB.prefix(5) == "Missi")
{
gameControl.enemyMissileHit(vIndex: Int(nodeNameB.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Missi" && nodeNameB.prefix(5) == "Explo")
{
gameControl.enemyMissileHit(vIndex: Int(nodeNameA.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Explo" && nodeNameB.prefix(5) == "Hive ")
{
gameControl.enemyHiveHit(vIndex: Int(nodeNameB.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Hive " && nodeNameB.prefix(5) == "Explo")
{
gameControl.enemyHiveHit(vIndex: Int(nodeNameA.suffix(4))!)
}
}
}
Or - on a timer, check the node distance periodically:
func distance3D(vector1: SCNVector3, vector2: SCNVector3) -> Float
{
let x: Float = (vector1.x - vector2.x) * (vector1.x - vector2.x)
let y: Float = (vector1.y - vector2.y) * (vector1.y - vector2.y)
let z: Float = (vector1.z - vector2.z) * (vector1.z - vector2.z)
let temp = x + y + z
return Float(sqrtf(Float(temp)))
}
Updated Answer:
Here is a better answer and overall more effective than my old answer. It takes advantage of SCNActions with physicsWorld methods and when they are called.
When the first contact between nodes happens, the physicsWorld methods are called in this order:
beginsContact
updateContact
endContact
And as the node proceeds to pass through the other node it goes in this order every other moment:
beginsContact
endContact
updateContact
Once the node is fully inside the other node and moves about inside the node, only this method is called.
updateContact
Note: In my example below, when referring to nodes, I'm talking about the player node and the invisible chatRadius as the other node for an NPC.
Since the beginsContact method is only called when both nodes have their edges touching, I have it set a boolean value equal true.
Note: The bool value is what I use to show interaction indicator for the player and is interchangeable for whatever you what to use.
var interaction: Bool = false
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
let node = (contact.nodeA.name == "player") ? contact.nodeB : contact.nodeA
if node.physicsBody!.categoryBitMask == control.CategoryInteraction && interaction {
interaction = true
}
}
The endContact method is almost always called after the didBegin contact method, so I have it wait for a while before setting the boolean value back to false.
Note: I make the action wait for the updateContact method to be called.
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
let node = (contact.nodeA.name == "player") ? contact.nodeB : contact.nodeA
if node.physicsBody!.categoryBitMask == control.CategoryInteraction && ViewManager.shared.interaction {
let wait = SCNAction.wait(duration: 0.1)
let leave = SCNAction.run { _ in
self.interaction = false
}
let sequence = SCNAction.sequence([wait, leave])
player.runAction(sequence, forKey: "endInteraction")
}
}
The update method is always called when the node moves around the edges or inside the other node. So I have it counter the endContact method by removing the action that would have set the bool value to false.
func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) {
if player.action(forKey: "endInteraction") != nil {
player.removeAction(forKey: "endInteraction")
}
}
The endContact method is also called when the two nodes stop touching (who would have guessed) and, in my testing, is always the last method to be called.
Old Answer
It's not at all a perfect answer and it still has room for improvement, but for now, this is how I did it.
For the first code section, I am checking to see if the player has collided with the chat radius (or is within a certain distance of the NPC). If the player is inside the radius, then show the indicator to interact with the NPC and add 1 to the count.
var count = 0
var previousCount = 0
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
let node = (contact.nodeA.name == "Player") ? contact.nodeB : contact.nodeA
if node.physicsBody!.categoryBitMask == CategoryInteraction {
// Show chat indicator here.
count += 1
if count > 100 {
count = 0
}
}
}
For the second code section, I'm checking to see if the player has left the chat radius by making the previousCount equal to count. If the count is equal to previousCount, then the player has left the chat radius so hide the interaction indicator.
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
if contact.nodeA.name != "chatRadius" && contact.nodeB.name != "chatRadius" { return }
if previousCount != count {
previousCount = count
} else {
// Hide chat indicator here.
count = 0
previousCount = 0
}
}

Is there a way to apply sound effects to collisions?

It's my first project with Swift and I'm trying to learn, but I'm stuck and can't find a solution online. Basically, it's a clone of pong, and everything is going good, until now.
My problem it's that I can't understand how to make a sound effect play, once my ball hit the paddles.
Everything else with the collisions is ok, and other sound like the goal one is playing fine. Just can't figure out how i can make a sound play when the two sprites collide.
Here's what I've written:
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.node?.name == "main" && contact.bodyB.node?.name == "ball" || contact.bodyB.node?.name == "main" && contact.bodyA.node?.name == "ball" || contact.bodyA.node?.name == "enemy" && contact.bodyB.node?.name == "ball" || contact.bodyB.node?.name == "enemy" && contact.bodyA.node?.name == "ball" {
ball.run(blipPaddleSound)
}
}
It's all wrong?
I’ve already tried to break the if statement, and already tried with make one of my node run the sound effect (ball.run(‘nameofthesound’). I’ve thought maybe it could be a problem with the file, but other effect don’t play as well in that part of the code. I don’t think there is a problem with the collision, because the ball bounce on the paddle and on the wall without problem
This is the part of the code where I have put all the collision and contact mask:
ball.physicsBody?.contactTestBitMask = 1
ball.physicsBody?.collisionBitMask = 3
ball.physicsBody?.categoryBitMask = 1
main.physicsBody?.contactTestBitMask = 3
main.physicsBody?.collisionBitMask = 1
main.physicsBody?.categoryBitMask = 2
enemy.physicsBody?.contactTestBitMask = 3
enemy.physicsBody?.collisionBitMask = 1
enemy.physicsBody?.categoryBitMask = 2
Most likely you are not setting the SKPhysicsContactDelegate to the Scene
You'll also find that a single collision can fire multiple triggers within the game cycle so you may want to put a variable controlling the sound play so that it doesn't play multiple times per collision
class GameScene: SKScene, SKPhysicsContactDelegate {
private var didCollide = false
func didBegin(_ contact: SKPhysicsContact) {
let object1 = contact.bodyA.node?.name
let object2 = contact.bodyB.node?.name
if object1 == "ball" || object2 == "ball" {
if object1 == "main" || object2 == "main" || object1 == "enemy"|| object2 == "enemy" {
//check if we have just collided within the last 0.3 seconds
if !didCollide {
//set the marker to true so that the sound doesn't repeatedly fire
didCollide = true
//play the sound
ball.run(blipPaddleSound)
//wait 0.3 seconds (give the ball a chance to move away from the object)
self.run(.wait(forDuration: 0.3) {
self.didCollide = false
}
}
}
}
}
}

I am trying to only allow the SKSpritenode to jump one time but having issue with velocity

I am currently using the WizardCharacter.physicsbody.velocity.dy, and checking if it is equal to 0. I do not want the character to be able to jump in air. The issue I am facing is, my character is resting and he is not moving and the velocity of y is going from 0 to 1 while not doing any actions to the character. I am not sure why this is happening, I am using SpriteKit and SceneKit. I have provided my code, as well a picture for what the console is printing out.
override func update(_ currentTime: TimeInterval) {
print("\(String(describing: wizardCharacter.physicsBody?.velocity.dy))")
cameraNode.position.x = wizardCharacter.position.x
if wizardCharacter.action(forKey: "wizardRun") == nil {
wizardCharacter.texture = SKTexture(imageNamed: "wizard_1_attack-b_001")
}
if upArrow.contains(pointTouched)
{
if wizardCharacter.physicsBody?.velocity.dy == 0 && isJumping == true {
upArrow.alpha = 0.5
jumpWizard(forTheKey: "jumped")
jumpWizardAnimation(fortheKey: "jumpedAnimation")
}
}

Creating sound effects for bouncing ball in SpriteKit

My Swift game uses SpriteKit and SKPhysics to animate a small red ball that frequently collides with other physicsBodys over the course of the game. I want to create an natural sounding bounce sound effect whenever the ball collides. I currently have a rather disorderly method for doing this, and there's a significant lag noticeable while employing it that disappears when I disable sounds:
var bounceSoundPlayers = [AVAudioPlayer!]()
func buildBounceSound() {
let path = Bundle.main.path(forResource: "Bounce.mp3", ofType:nil)!
let url = URL(fileURLWithPath: path)
for _ in 0...2 {
do {
let sound = try AVAudioPlayer(contentsOf: url)
sound.prepareToPlay()
bounceSoundPlayers.append(sound)
} catch {
fatalError("couldn't load music file")
}
}
}
This creates three AVAudioPlayers with sound "Bounce.mp3" and runs the prepareToPlay() method on them. Then it stores them in an array called bounceSoundPlayers so I can use them later. Then I have a didBeginContact method that is called whenever anything collides with anything:
func didBegin(_ contact: SKPhysicsContact) {
if contact.collisionImpulse > 0.45 && defaults.bool(forKey: "SFX") {
if ((contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "permeable platform") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "permeable platform") ||
(contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "ring") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "ring"))
&&
ball.collidingWithPermeable {
playBounceSound(contact.collisionImpulse/8<=1 ? contact.collisionImpulse/8 : 1)
}
if (contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "impermeable platform") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "impermeable platform") ||
(contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "wall") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "wall") {
playBounceSound(contact.collisionImpulse/5<=1 ? contact.collisionImpulse/5 : 1)
}
}
}
Basically what this method does is it checks each collision, and if the ball collides with the right kind of platform or wall, it calls the method playBounceSound(volume: CGFloat) with a volume proportional to the force of the collision. So if the ball collides with a platform lightly, the volume is quieter than if it's a really strong collision. Here's the playBounceSound method:
var bouncePlayerIndex = Int(0)
func playBounceSound(_ volume: CGFloat) {
let previousIndex = bouncePlayerIndex - 1 >= 0 ? bouncePlayerIndex - 1 : 2
if bounceSoundPlayer[previousIndex].isPlaying {
if bounceSoundPlayer[previousIndex].currentTime > 0.1 {
bounceSoundPlayer[bouncePlayerIndex].volume = Float(volume)
bounceSoundPlayer[bouncePlayerIndex].play()
}
}
else {
bounceSoundPlayer[bouncePlayerIndex].volume = Float(volume)
bounceSoundPlayer[bouncePlayerIndex].play()
}
bouncePlayerIndex += 1
if bouncePlayerIndex > 2 {bouncePlayerIndex = 0}
}
Basically what this method does is it cycles through the three AVAudioPlayers we created earlier by using an index integer. Every time it's called, it tells the bounceSoundPlayer at bouncePlayerIndex to play() at the given volume. Then it increments bouncePlayerIndex so that next time it's called, it uses a different AVAudioPlayer. If bouncePlayerIndex > 2, it goes back to 0. Using three AVAudioPlayers allows me to play multiple bounces simultaneous — i.e. a bounce sound can begin before the last one ended, such as when the ball lands on a platform and bounces high, then lower and lower and more frequently, until it stops bouncing.
I'm sure there's a better way of doing this than with an array of AVAudioPlayers. Please help.
Swift 3
This is the best way I have found to play sounds. They can overlap and you can play as many as you want at the same time. Fast and simple.
1.) Make sure you import the framework
import AVFoundation
2.) Create the function
// my sound file is named shatter.mp3
func shatterSound() {
if let soundURL = Bundle.main.url(forResource: "shatter", withExtension: "mp3") {
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &mySound)
AudioServicesPlaySystemSound(mySound);
}
}
3.) Call the function where you need it with
shatterSound()

Unexpected repeated collisions in SpriteKit

I'm making my first game with Swift and SpriteKit and while I've had collisions working properly for some time now, I recently noticed a huge bug. My collisions can either be the user with and enemy (spaceship against alien) user shooting an enemy (laser against alien). The latter works fine, but recently the collision detection when the ship touches an alien hasn't been working-- normally the alien should be removed from the scene when it touches the so that only a single life (1 of 3 total) is removed. However, the ship looses 3-6 lives upon touch now. Here's the code and where I'd assume the problem is:
This is one of a few contact functions that is then called in the general contact method.
func alien_ship_contact(contact:SKPhysicsContact){
var alien:SKNode? = nil
if contact.bodyA.categoryBitMask == PhysicsCategory.Alien && contact.bodyB.categoryBitMask == PhysicsCategory.Ship{
alien = contact.bodyA.node
}
else if contact.bodyB.categoryBitMask == PhysicsCategory.Alien && contact.bodyA.categoryBitMask == PhysicsCategory.Ship{
alien = contact.bodyB.node
}
else{
return
}
killOffAlien((alien)!)
aliensKilled = aliensKilled + 1
shipLives = shipLives-1
aShip.lives = aShip.lives - 1
print("ship/alien contact")
}
Here is the killOffAlien function:
func killOffAlien(alien:SKNode){
print("Kill off")
//alien.removeFromParent()
func stopMotion(){
alien.physicsBody?.categoryBitMask = 0
alien.physicsBody?.collisionBitMask = 0
alien.physicsBody?.contactTestBitMask = 0
alien.physicsBody?.dynamic = false
alien.physicsBody?.velocity = CGVector(dx:0, dy:0)
alien.removeActionForKey("facialMotion")
}
func removeAlien(){
alien.removeFromParent()
}
let stopMoving = SKAction.runBlock(stopMotion)
let fadeOut = SKAction.fadeOutWithDuration(1)
let removeFromParent = SKAction.runBlock(removeAlien)
let die = SKAction.sequence([stopMoving, fadeOut, removeFromParent])
alien.runAction(die)
}
And here is the general contact method:
func didBeginContact(contact:SKPhysicsContact){
alien_laser_contact(contact)
alien_ship_contact(contact)
....
Any help would be awesome, I would think that upon initial contact the alien has it's bitmask set off from Alien so that in itself would prevent future collisions as every alien should only be able to remove at max one life from the ship.