I need to make my objects move randomised around on the screen. While they are moving, the object is looking for another object within a certain amount of radius.
I found the below link, and I implemented the getDuration function. But I’m having the same glitch as the topic owner. I can see it should be possible to fix, by removing the running action.
Moving an object across the screen at a certain speed.(Sprite Kit)
Video: https://www.youtube.com/watch?v=jHE5RC-mvwU
But I have now tried several solutions, but I can’t make it work. When I’m killing the action, then my objects stop to move.
Can someone please tell me, where to kill my action, create in moveToWaypoint? I need my objects to move to a random waypoint, but if a specific object get within the radius, then it should set a new waypoint to the closet object and start the action again.
Code:
///Called in update method
func move(scene: GameplayScene) {
checkForWaypoint()
checkForFood(scene: scene)
moveToWaypoint()
}
///Creates a new waypoint
func createWaypoint() {
waypoint = CGPoint(x: randomBetweenNumbers(firstNum: minX, secondNum: maxX), y: randomBetweenNumbers(firstNum: minX, secondNum: maxX))
}
override func checkForFood(scene: GameplayScene) {
var closesObject: SKNode? = nil
scene.enumerateChildNodes(withName: "Food") {
node, _ in
let distance = node.position.distanceFromCGPoint(point: self.position)
if distance < self.FOOD_RANGE {
closesObject = node
}
}
if hungryState == HungryState.hungry {
if closesObject != nil {
waypoint = closesObject?.position
moveState = MoveState.movingToFood
} else {
if moveState == MoveState.movingToFood {
createWaypoint()
}
}
}
}
///Moves the object to the waypoint
func moveToWaypoint () {
let action = SKAction.move(to: waypoint!, duration: getDuration(pointA: position, pointB: waypoint!, speed: 25))
self.run(action)
}
///Calcuate a speed between to cordinates
private func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)-> TimeInterval {
let xDist = (pointB.x - pointA.x)
let yDist = (pointB.y - pointA.y)
let distance = sqrt((xDist * xDist) + (yDist * yDist));
let duration : TimeInterval = TimeInterval(distance/speed)
return duration
}
EDIT:
Update function from Gamescene class
override func update(_ currentTime: TimeInterval) {
moveFish()
}
private func moveFish() {
for node in self.children {
if node.name != nil {
switch node.name {
case "Fish"?:
let fishToMove = node as! Fish
fishToMove.move(scene: self)
default:
break
}
}
}
}
What you are trying to solve here could be done in two ways. First is using physics, and second is without it of course. I have decided to go without physics because that is obviously how you are doing it.
In short, fish in these example move using SKAction while there is no food around. When food is in range, fish are moved to their targets using update: method. When fish eat its found it continues to move around using SKAction.
Also before everything, here are some useful extensions that I have borrowed here from Stackoverflow that you might find useful in the future:
import SpriteKit
import GameplayKit
//Extension borrowed from here : https://stackoverflow.com/a/40810305
extension ClosedRange where Bound : FloatingPoint {
public func random() -> Bound {
let range = self.upperBound - self.lowerBound
let randomValue = (Bound(arc4random_uniform(UINT32_MAX)) / Bound(UINT32_MAX)) * range + self.lowerBound
return randomValue
}
}
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
func randomPoint() -> CGPoint {
let origin = self.origin
return CGPoint(x:CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x,
y:CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y)
}
}
//Extension borrowed from here: https://stackoverflow.com/a/33292919
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
}
Now there is a Fish class like yours, which has few methods and a physics body which is used just to detect contact between food and a fish, but that's all from the physics. Here is the Collider struct just in case that you want to know how I have defined it:
struct Collider{
static let food : UInt32 = 0x1 << 0
static let fish : UInt32 = 0x1 << 1
static let wall : UInt32 = 0x1 << 2
}
Now back to the Fish class...I have put comments in the code, so I guess it is not needed to explain what those methods do. Here is the code:
class Fish:SKSpriteNode{
private let kMovingAroundKey = "movingAround"
private let kFishSpeed:CGFloat = 4.5
private var swimmingSpeed:CGFloat = 100.0
private let sensorRadius:CGFloat = 100.0
private weak var food:SKSpriteNode! = nil //the food node that this fish currently chase
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.affectedByGravity = false
physicsBody?.categoryBitMask = Collider.fish
physicsBody?.contactTestBitMask = Collider.food
physicsBody?.collisionBitMask = 0x0 //No collisions with fish, only contact detection
name = "fish"
let sensor = SKShapeNode(circleOfRadius: 100)
sensor.fillColor = .red
sensor.zPosition = -1
sensor.alpha = 0.1
addChild(sensor)
}
func getDistanceFromFood()->CGFloat? {
if let food = self.food {
return self.position.distance(point: food.position)
}
return nil
}
func lock(food:SKSpriteNode){
//We are chasing a food node at the moment
if let currentDistanceFromFood = self.getDistanceFromFood() {
if (currentDistanceFromFood > self.position.distance(point: food.position)){
//chase the closer food node
self.food = food
self.stopMovingAround()
}//else, continue chasing the last locked food node
//We are not chasing the food node at the moment
}else{
//go and chase then
if food.position.distance(point: self.position) <= self.sensorRadius {
self.food = food
self.stopMovingAround()
}
}
}
//Helper method. Not used currently. You can use this method to prevent chasing another (say closer) food while already chasing one
func isChasing(food:SKSpriteNode)->Bool{
if self.food != nil {
if self.food == food {
return true
}
}
return false
}
func stopMovingAround(){
if self.action(forKey: kMovingAroundKey) != nil{
removeAction(forKey: kMovingAroundKey)
}
}
//MARK: Chasing the food
//This method is called many times in a second
func chase(within rect:CGRect){
guard let food = self.food else {
if action(forKey: kMovingAroundKey) == nil {
self.moveAround(within: rect)
}
return
}
//Check if food is in the water
if rect.contains(food.frame.origin) {
//Take a detailed look in my Stackoverflow answer of how chasing works : https://stackoverflow.com/a/36235426
let dx = food.position.x - self.position.x
let dy = food.position.y - self.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * kFishSpeed
let vy = sin(angle) * kFishSpeed
position.x += vx
position.y += vy
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveAround(within rect:CGRect){
if scene != nil {
//Go randomly around the screen within view bounds
let point = rect.randomPoint()
//Formula: time = distance / speed
let duration = TimeInterval(point.distance(point: position) / self.swimmingSpeed)
let move = SKAction.move(to: point, duration: duration)
let block = SKAction.run {
[unowned self] in
self.moveAround(within: rect)
}
let loop = SKAction.sequence([move,block])
run(loop, withKey: kMovingAroundKey)
}
}
}
So basically, there are methods to move the fish around while it is not in a chase for a food. Also there is method which stop this (infinite) action (SKAction). The most important method is the chase(within rect:) method. That method is called in scene's update() method and defines how and when fish will (try to) chase the food.
Now the GameScene:
//MARK: GameScene
class GameScene: SKScene, SKPhysicsContactDelegate {
private var nodesForRemoval:[SKNode] = []
private var water = SKSpriteNode()
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: -0.5)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.categoryBitMask = Collider.wall
physicsBody?.contactTestBitMask = 0x0
physicsBody?.collisionBitMask = Collider.fish | Collider.food
self.backgroundColor = .white
//Water setup
water = SKSpriteNode(color: .blue, size: CGSize(width: frame.width, height: frame.height - 150))
water.position = CGPoint(x: 0, y: -75)
water.alpha = 0.3
addChild(water)
water.zPosition = 4
//Fish one
let fish = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
addChild(fish)
fish.position = CGPoint(x: frame.midX-50, y: frame.minY + 100)
fish.zPosition = 5
fish.moveAround(within: water.frame)
//Fish two
let fish2 = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
addChild(fish2)
fish2.position = CGPoint(x: frame.midX+50, y: frame.minY + 100)
fish2.zPosition = 5
fish2.moveAround(within: water.frame)
}
func feed(at position:CGPoint, with food:SKSpriteNode){
food.position = CGPoint(x: position.x, y: frame.size.height/2 - food.frame.size.height)
addChild(food)
}
//MARK: Food factory :)
func getFood()->SKSpriteNode{
let food = SKSpriteNode(color: .purple, size: CGSize(width: 10, height: 10))
food.physicsBody = SKPhysicsBody(rectangleOf: food.frame.size)
food.physicsBody?.affectedByGravity = true
food.physicsBody?.categoryBitMask = Collider.food
food.physicsBody?.contactTestBitMask = Collider.fish
food.physicsBody?.collisionBitMask = Collider.wall
food.physicsBody?.linearDamping = (0.1 ... 0.95).random()
food.name = "food"
return food
}
//MARK: Feeding
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let food = getFood()
feed(at: location, with: food)
}
}
//MARK: Eating
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
//Silliness like removing a node from a node tree before physics simulation is done will trigger this error
fatalError("Physics body without its node detected!")
}
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch mask {
//Contact between fish and a food
case Collider.fish | Collider.food:
if let food = (contact.bodyA.categoryBitMask == Collider.food ? nodeA : nodeB) as? SKSpriteNode
{
self.nodesForRemoval.append(food)
}
default:
//some unknown contact occurred
break
}
}
//MARK: Removing unneeded nodes
override func didSimulatePhysics() {
for node in self.nodesForRemoval {
node.removeFromParent()
}
self.nodesForRemoval.removeAll()
}
//MARK: Chasing the food
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "fish") {
[unowned self] node, stop in
if let fish = node as? Fish {
self.enumerateChildNodes(withName: "food") {
node, stop in
fish.lock(food: node as! SKSpriteNode)
}
fish.chase(within: self.water.frame)
}
}
}
}
And that's it. Here we setup our nodes, resolve contact detections and tell which fish should chase which food node. I left the comments, so everything is there. I hope that methods are pretty much self explanatory, but of course you can ask me for details about how things works.
And here is a short video of how this works:
and the longer version because I can only upload two 2 megabytes: screen recording
Basically, fish don't chase the food node if it is not its defined range. Still, fish will chase the locked node until it eats it. But if there is some other food which is more close, the fish will chase for that food node. Of course this is not a must, and you can tweak it as you like (check isChasing:) method.
Related
I describe as StackOverflow standards the following issue.
Summarize the problem
I have issue about colliding two nodes. One is composed by a crowd and each people is a single item of my crowd defined in the same way (included in a while just to be clear and using index "i" and "j" to create the row of the crowd). I wanted to make disappear once arrive a node (as civilian) to the bottom and going to the top and the crowd remove the civilian spawned along the path. Actually I have this thing and the func tells me that the colliding happens but it didn't 'cause nothing happens and the civilian node captured by crowd it didn't disappear or removed with the removefromparent(). I've got no error messages with my compiler, it works for him. My scope is : detecting node civilian during the path by the crowd and remove this one from the path.
What I've tried
I tried many things to fix this. The first thing is following a lot of tutorials about how Collision Masks etc.. work. I know what they do. But what I've tried it was to make a invisible line for the last line of crowd of people just to see if the problem is the crowd itself and making that if the civilian node arrives to collide the invisible line is like he was in contact with the crowd but it didin't this effect. I followed a lot of tutorial such as HackingWithswift, Youtube tutorials but the procedure for me it's clear but nothing happens (sorry for being repetitive).
Show code
My problem is about this GameScene.sks because it it just one file with all the functions.
import SpriteKit
import GameplayKit
enum CategoryMask: UInt32 {
case civilian_value = 1
case crowd_value = 2
case background_value = 0
}
enum GameState {
case ready
case playing
case dead
}
var gameState = GameState.ready {
didSet {
print(gameState)
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(imageNamed: "player1")
let textureA = SKTexture(imageNamed: "player1")
let textureB = SKTexture(imageNamed: "player2")
let pause = SKSpriteNode(imageNamed: "pause-button")
let resume = SKSpriteNode(imageNamed: "pause-button")
var civilian = SKSpriteNode()
let pauseLayer = SKNode()
let gameLayer = SKNode()
weak var sceneDelegate: GameSceneDelegate?
//main
override func didMove(to view: SKView) {
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
//func for dynamic background
moveBackground(image: ["background1", "background2", "background3", "background1"], x: 0, z: -3, duration: 5, size: CGSize(width: 0.5, height: 1.0))
character(player: player)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(civilians),
SKAction.wait(forDuration: 3.0)])))
run(SKAction.run(crowdSpawn))
pause.name="pause"
pause.position = CGPoint(x: frame.minX/1.3, y: frame.minY/1.15)
pause.size=CGSize(width: 0.1, height: 0.1)
pause.zPosition = 4
addChild(pause)
if self.scene?.isPaused == true {
resume.name="resume"
resume.position = CGPoint(x: frame.minX/1.5, y: frame.minY/1.15)
resume.size=CGSize(width: 0.1, height: 0.1)
resume.zPosition = 12
addChild(resume)
}
}
func pauseGame() {
sceneDelegate?.gameWasPaused()
let barr = SKSpriteNode()
let barrbehind = SKSpriteNode()
let buttonresume = SKSpriteNode(imageNamed: "back")
barrbehind.name = "barrbehind"
barrbehind.zPosition = 9
barrbehind.color = SKColor.black
barrbehind.size = CGSize(width: frame.width, height: frame.height)
barrbehind.alpha = 0.5
self.addChild(barradietro)
barr.name = "bar"
barr.size = CGSize(width: 0.4, height: 0.5)
barr.color = SKColor.white
barr.zPosition = 10
self.addChild(barr)
buttonresume.name = "resume"
buttonresume.zPosition = 11
buttonresume.color = SKColor.black
buttonresume.size = CGSize(width: 0.1, height: 0.1)
buttonresume.alpha = 0.5
self.addChild(buttonresume)
self.scene?.isPaused = true
}
//random func (it helps for generate randomly civilians along the path
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
//func to define civilians
func civilians() {
let civilian = SKSpriteNode(imageNamed: "PV")
civilian.name = "civilian"
//posiziono il civile
civilian.position = CGPoint(x: frame.size.width/8.0 * random(min: -1.5, max: 1.5), y: -frame.size.height * 0.45)
civilian.physicsBody = SKPhysicsBody(rectangleOf: civilian.size)
civilian.zPosition = 3
civilian.physicsBody?.categoryBitMask = CategoryMask.civilian_value.rawValue
civilian.physicsBody?.collisionBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.contactTestBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.isDynamic = true
//civilian size
civilian.size=CGSize(width: 0.2, height: 0.2)
//civilian movement
civilian.run(
SKAction.moveBy(x: 0.0, y: frame.size.height + civilian.size.height,duration: TimeInterval(1.77)))
addChild(civilian)
}
//func for the main character
func character(player: SKSpriteNode){
player.position = CGPoint(x: 0, y: 0)
player.size = CGSize(width: 0.2, height: 0.2)
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.2)
player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(player)
player.run(SKAction.repeatForever(animation))
}
//func for generate the crowd
func crowdSpawn(){
var i = 0.0
var j = 0.25
var crowdRaw : Bool = true
while crowdRaw {
if i <= 1 {
let crowd = SKSpriteNode(imageNamed: "player1")
crowd.name = "crowd"
//posiziono il civile
crowd.size=CGSize(width: 0.15, height: 0.15)
crowd.position = CGPoint(x: -frame.size.width / 3.6 + CGFloat(i)/2 * crowd.size.width , y: frame.size.height / 2 + (CGFloat(j)*2) * -crowd.size.height)
crowd.zPosition = 3
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.25)
crowd.run(SKAction.repeatForever(animation))
crowd.run(SKAction.moveBy(x: frame.size.width / 16.0 + CGFloat(i) * crowd.size.width, y: 0, duration: 0))
let infectedCollision = SKSpriteNode(color: UIColor.red,
size: CGSize(width: 1, height: 0.1))
infectedCollision.physicsBody = SKPhysicsBody(rectangleOf: infectedCollision.size)
infectedCollision.physicsBody?.categoryBitMask = CategoryMask.crowd_value.rawValue
//collisionBitMask : qui la linea della folla non può collidere con il civilian
infectedCollision.physicsBody?.collisionBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.contactTestBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.isDynamic = true
infectedCollision.name = "infectedCollision"
infectedCollision.position = crowd.position
addChild(crowd)
addChild(infectedCollision)
i += 0.25
} else {
j += 0.25
i = 0.0
}
if j == 1 {
crowdRaw = false
}
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.node?.position == contact.bodyB.node?.position {
let actionMoveDone = SKAction.removeFromParent()
civilian.run(SKAction.sequence([actionMoveDone]))
}
}
//func about the touches
func touchDown(atPoint pos : CGPoint) {
let action = SKAction.move(to: pos, duration: 1.0)
// playerSprite is a SpriteKit sprite node.
player.run(action)
}
//func about the touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
switch gameState {
case .ready:
gameState = .playing
case .playing:
for t in touches {
let location = t.location(in: self)
player.position.x = location.x/2
for node in self.nodes(at: location){
if node.name == "civilian" {
let explode = SKAction.colorize(with: UIColor.systemBlue,colorBlendFactor: 5.0, duration: 2)
let vanish = SKAction.fadeOut(withDuration: 2.0)
node.run(explode , completion: {
node.run(vanish) {
node.removeFromParent()
}
})
}else if node.name == "pause" {
pauseGame()
}else if node.name == "resume" {
self.scene?.isPaused = false
}
}
}
case .dead:
print("dead")
}
}
//function to have different backgrounds in scrolling (3 backgrounds in a loop)
func moveBackground(image: [String], x: CGFloat, z:CGFloat, duration: Double, size: CGSize) {
for i in 0...3 {
let background = SKSpriteNode(imageNamed: image[i])
background.position = CGPoint(x: x, y: size.height * CGFloat(i))
background.size = size
background.zPosition = z
let move = SKAction.moveBy(x: 0, y: -background.size.height*3, duration: 0)
let back = SKAction.moveBy(x: 0, y: background.size.height*3, duration: duration)
let sequence = SKAction.sequence([move,back])
let repeatAction = SKAction.repeatForever(sequence)
addChild(background)
background.run(repeatAction)
}
}
}
Ok, it was fun to recall how all this SpriteKit stuff works :D
First problem you have is node/sprite creation. The solution could be some kind of Factory pattern with more or less abstraction. GameScene doesn't have to know how nodes are initialized/configured. Scene could know only which type of nodes exist, and thats enough to get them ready for use.
//MARK: - Factory
protocol AbstractFactory {
func getNode()-> SKNode
func getNodeConfig()->SpriteConfig
}
class CivilianFactory : AbstractFactory {
// Local Constants
private struct K {
static let size = CGSize(width: 32, height: 32)
static let name = "civilian"
static let color = UIColor.yellow
}
// Here we get Civilian sprite config
func getNodeConfig() -> SpriteConfig {
let physics = SpritePhysicsConfig(categoryMask: Collider.civilian, contactMask: Collider.player | Collider.wall, collisionMask: Collider.none)
return SpriteConfig(name: K.name, size: K.size, color: K.color, physics: physics)
}
func getNode() -> SKNode {
let config = getNodeConfig()
let sprite = SKSpriteNode(color: config.color, size: config.size)
sprite.color = config.color
sprite.name = config.name
sprite.zPosition = 1
if let physics = config.physics {
sprite.physicsBody = SKPhysicsBody(rectangleOf: config.size)
sprite.physicsBody?.isDynamic = physics.isDynamic
sprite.physicsBody?.affectedByGravity = physics.isAffectedByGravity
sprite.physicsBody?.categoryBitMask = physics.categoryMask
sprite.physicsBody?.contactTestBitMask = physics.contactMask
sprite.physicsBody?.collisionBitMask = physics.collisionMask
}
}
return sprite
}
}
Same as this, You will make other "factories" as needed (just copy the factory and change visual/physics data setup). For this example I will make PlayerFactory.
and with next method I will create my nodes:
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
and then just use it like this:
let node = getNode(factory: self.civiliansFactory) // or self.whateverFactory
Here you just provide a factory you want (can be anything that conforms to AbstractFactory), and in return, You get a desired node (You can return here anything that is SKNode). This way, we have hid initialization process, dependencies etc. from outside world (GameScene), and put everything in one place.
So, quite flexible, plus removes a bunch of repeating code from your scene.
And here are config structs for sprites creation:
//MARK: - Sprite Config
struct SpriteConfig {
let name:String
let size:CGSize
let color:UIColor
let physics:SpritePhysicsConfig? // lets make this optional
}
struct SpritePhysicsConfig {
let categoryMask: UInt32
let contactMask: UInt32
let collisionMask:UInt32
let isDynamic:Bool
let isAffectedByGravity:Bool
init(categoryMask:UInt32, contactMask:UInt32, collisionMask:UInt32, isDynamic:Bool = true, isAffectedByGravity:Bool = false){
self.categoryMask = categoryMask
self.contactMask = contactMask
self.collisionMask = collisionMask
self.isDynamic = isDynamic
self.isAffectedByGravity = isAffectedByGravity
}
}
Now some useful extensions that I needed:
//MARK: - Extensions
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
func randomPoint(x:CGFloat? = nil, y:CGFloat? = nil) -> CGPoint {
let origin = self.origin
return CGPoint(x: x == nil ? CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x : x!,
y: y == nil ? CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y : y!)
}
}
//Extension borrowed from here: https://stackoverflow.com/a/33292919
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
}
And the GameScene:
//MARK: - Game Scene
class GameScene: SKScene {
//MARK: - Local Constants
// It's always good to have some kind of local constants per file, so that you have all variables in one place when it comes to changing/tuning
private struct K {
struct Actions {
static let civilianSpawningKey = "civilian.spawning"
static let playerMovingKey = "player.moving"
static let spawningDuration:TimeInterval = 0.7
static let spawningRange = 0.2
static let fadeOutDuration:TimeInterval = 0.35
}
struct General {
static let playerSpeed:CGFloat = 350
}
}
//MARK: - Private Properties
private var player:SKSpriteNode?
// Just in case, nodes are removed after physics simulation is done (in didSimulatePhysics which is called in each frame)
// Frame-Cycle Events : https://developer.apple.com/documentation/spritekit/skscene/responding_to_frame-cycle_events
private var trash:[SKNode] = []
private let civilianFactory = CivilianFactory()
private let playerFactory = PlayerFactory()
//MARK: - Scene lifecycle
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
spawnCivilians()
}
//MARK: - Creating & Spawning sprites
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
private func spawnCivilian(at position: CGPoint){
let node = getNode(factory: civilianFactory)
node.position = position
addChild(node)
}
private func spawnPlayer(at position: CGPoint){
// If its a first time, create player and leave it there
guard let `player` = player else {
let node = getNode(factory: playerFactory)
node.position = position
self.player = (node as? SKSpriteNode)
addChild(node)
return
}
// If player exists, move it around
let distance = player.position.distance(point: position)
let speed = K.General.playerSpeed
// To maintain same moving speed, cause if we use constant here, sprite would move faster or slower based on a given distance
let duration = distance / speed
let move = SKAction.move(to: position, duration:duration)
// This is a good way to check if some action is running
if player.action(forKey: K.Actions.playerMovingKey) != nil {
player.removeAction(forKey: K.Actions.playerMovingKey)
}
player.run(move, withKey: K.Actions.playerMovingKey)
}
private func spawnCivilians(){
let wait = SKAction .wait(forDuration: K.Actions.spawningDuration, withRange: K.Actions.spawningRange)
let spawn = SKAction.run({[weak self] in
guard let `self` = self else {return}
self.spawnCivilian(at: self.frame.randomPoint())
})
let spawning = SKAction.sequence([wait,spawn])
self.run(SKAction.repeatForever(spawning), withKey:K.Actions.civilianSpawningKey)
}
//MARK: - Touches Handling
func touchDown(atPoint pos : CGPoint) {
spawnPlayer(at: pos)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
}
So I pretty much commented everything. Here, you :
Start spawning civilians infinitely, immediately after the scene is loaded
On touch you add player to the scene
On every next touch player travels to the touch location (by the same speed)
And contacts:
//MARK: - Physics
struct Collider{
static let player : UInt32 = 0x1 << 0
static let civilian : UInt32 = 0x1 << 1
static let wall : UInt32 = 0x1 << 2
static let none : UInt32 = 0x0
}
extension GameScene: SKPhysicsContactDelegate{
//MARK: - Removing Sprites
override func didSimulatePhysics() {
for node in trash {
// first remove node from parent (with fadeOut)
node.run(SKAction.sequence([SKAction.fadeOut(withDuration: K.Actions.fadeOutDuration), SKAction.removeFromParent()]))
}
trash.removeAll() // then empty the trash
}
//MARK: Removing
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
//Silliness like removing a node from a node tree before physics simulation is done will trigger this error
fatalError("Physics body without its node detected!")
}
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch mask {
// Contact between player and civilian detected
case Collider.player | Collider.civilian:
if let civilian = (contact.bodyA.categoryBitMask == Collider.civilian ? nodeA : nodeB) as? SKSpriteNode
{
trash.append(civilian)
}
default:
break
}
}
}
I guess those contacts and node removal were your problem. The point is that nodes that have physics body, are safer to remove from a node tree when didSimulatePhysics method is finished. There is a link in comments that explains what happens each frame, but the bottom point is, that physics engine retains physics body cause simulation is not finished, but the node is removed and that often end up in some unexpected results.
So to try how this work, you just copy / paste it in your GameScene. Here is how it looks:
You can see how nodes are really removed by observing nodes count label. (to enable these labels, you just go (in your view controller class) with (self.view as? SKView)?.showsNodeCount = true, showsFPS, showsPhysics etc).
I'm making my own Mario Bros. replica for the first level to learn how to make games with iOS, with my own assets. So far I've managed to place three SKSpriteNodes for the controls (left, right, up), and my player node can move in those three directions, but if I make my player jump while running in either direction, as soon as I remove my finger from the "left control", the player loses all its momentum and falls right there (as if it hit a wall) instead of following the parabola.
I don't know what might be needed in this case to be an MRE, so this is basically the whole thing that can reproduce the issue, along with some attempts I've made to make it work.
Basically I tried to apply an impulse / set the velocity / change the position directly and this last one was the one with better results (yet it still makes the player node to fall as soon as I remove the finger from the direction controls).
Here's a video demonstrating the issue.
This is the GameScene
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var player = SKSpriteNode()
private var bg = SKSpriteNode()
private var leftArrow = SKSpriteNode()
private var rightArrow = SKSpriteNode()
private var upArrow = SKSpriteNode()
private var floor = [SKSpriteNode]()
private var isLeftTouched = false
private var isRightTouched = false
private var selectedNodes: [UITouch:SKSpriteNode] = [:]
override func didMove(to view: SKView) {
addBackground()
addFloor()
addPlayer(xOffset: 0, yOffset: 0)
addControls()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
let touch = touches.first! as UITouch
let positionInScene = touch.location(in: self)
let touchedNode = self.atPoint(positionInScene)
for touch in touches {
let location = touch.location(in:self)
if let node = self.atPoint(location) as? SKSpriteNode {
if let name = touchedNode.name {
selectedNodes[touch] = node
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
}
if let name = touchedNode.name {
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//let direction = ((touches.first?.location(in: self).x)! < (touches.first?.previousLocation(in: self).x)!) ? Direction.LEFT : Direction.RIGHT
//runIn(direction: direction)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if selectedNodes[touch] != nil {
if selectedNodes[touch]?.name == "left" {
isLeftTouched = false
} else if selectedNodes[touch]?.name == "right" {
isRightTouched = false
}
selectedNodes[touch] = nil
}
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if isLeftTouched {
runIn(direction: Direction.LEFT)
}
if isRightTouched {
runIn(direction: Direction.RIGHT)
}
}
// MARK: INTERACTION METHODS
func runIn(direction: Direction) {
let x = player.position.x + (direction == Direction.RIGHT ? 5 : -5)
let position = CGPoint(x: x, y: player.position.y)
if position.x >= self.frame.maxX || position.x <= self.frame.minX {
return
}
player.position = position
//player.physicsBody?.velocity = CGVector(dx: direction == Direction.RIGHT ? 50 : -50, dy: 0)
//player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 5 : -5 , dy: 0))
}
// MARK: UI METHODS
func addBackground() {
let bgTexture = SKTexture(imageNamed: "bg")
bg = SKSpriteNode(texture: bgTexture)
bg.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bg.size.height = self.frame.height
bg.zPosition = -10
self.addChild(bg)
}
func addPlayer(xOffset: CGFloat, yOffset: CGFloat) {
let playerTexture = SKTexture(imageNamed: "player")
player = SKSpriteNode(texture: playerTexture)
//let xPos = calculateXOffset(for: player, from: self.frame.midX, offset: xOffset)
//let yPos = calculateXOffset(for: player, from: self.frame.midY, offset: yOffset)
player.position = CGPoint(x: self.frame.midX,
y: self.frame.midY)
player.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: player.frame.width, height: player.frame.height))
player.physicsBody?.isDynamic = true
self.addChild(player)
}
func addFloor() {
let blockTexture = SKTexture(imageNamed: "block")
for i in 0 ... (Int) (self.frame.width / blockTexture.size().width) {
let blockNode = SKSpriteNode(texture: blockTexture)
blockNode.position = CGPoint(x: self.frame.minX + (blockNode.frame.width * CGFloat(i)),
y: self.frame.minY + blockNode.frame.height / 2)
blockNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: blockNode.frame.width, height: blockNode.frame.height))
blockNode.physicsBody?.isDynamic = false
floor.append(blockNode)
self.addChild(blockNode)
}
}
func addControls() {
addLeftArrow()
addRightArrow()
addUpArrow()
}
func addLeftArrow() {
let leftTexture = SKTexture(imageNamed: "left")
leftArrow = SKSpriteNode(texture: leftTexture)
leftArrow.name = "left"
leftArrow.position = CGPoint(x: calculateXOffset(for: leftArrow, from: self.frame.minX, offset: 50),
y: calculateXOffset(for: leftArrow, from: self.frame.minY, offset: 50))
self.addChild(leftArrow)
}
func addRightArrow() {
let rightTexture = SKTexture(imageNamed: "right")
rightArrow = SKSpriteNode(texture: rightTexture)
rightArrow.name = "right"
rightArrow.position = CGPoint(x: calculateXOffset(for: rightArrow, from: self.frame.minX, offset: 150),
y: calculateXOffset(for: rightArrow, from: self.frame.minY, offset: 50))
self.addChild(rightArrow)
}
func addUpArrow() {
let upTexture = SKTexture(imageNamed: "up")
upArrow = SKSpriteNode(texture: upTexture)
upArrow.name = "up"
upArrow.position = CGPoint(x: calculateXOffset(for: upArrow, from: self.frame.maxX, offset: -(125 + upTexture.size().width)),
y: calculateXOffset(for: upArrow, from: self.frame.minY, offset: 50))
self.addChild(upArrow)
}
// MARK: UTILITY FUNCTIONS
func calculateXOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let width = asset.frame.width
return coord + offset + width;
}
func calculateYOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let height = asset.frame.height
return coord + offset + height;
}
}
My Direction enum:
enum Direction {
case LEFT
case RIGHT
case UP
case DOWN
}
And the only change I made in GameViewController was this:
scene.scaleMode = .resizeFill
My GameScene.sks is 926 x 428, only supporting landscape. I also set the LaunchScreen to Main due to a bug in Xcode 12: Background is not filling the whole view SpriteKit
And these are all my assets:
Edit
I tried applying an impulse in my runIn method like this:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
This makes the player node move in the parabola but now from time to time it gets stuck and the only way to make it move is to make it jump until it happens again.
Here's a video demonstrating the issue again.
If I try to set the velocity instead, then I'm not able to jump while moving and it seems to glide when jumping and moving after.
I ended up following #JohnL suggestion in the comments above, to use an impulse as well to move my player node:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
The issue where the player node was stuck while moving was removed when changing the floor for a single asset rather than multiple blocks one next to each other.
I created an SKSpriteNode object, but multiple objects are created, appearing as strings of them. The sound coming from the SKAudioNode object is also duplicated many times to give a reverberating effect.
Here is the code below. I noticed it doing this when I added the code to set the pinned properties of the physicsBody objects to true in the didBegin callback method.
Here is a screenshot. Notice there are multiple explosion SKSpriteNode objects. I only created one SKSpriteNode object for the explosion.
I suspect there is a setting somewhere that I can change to disable this effect.
import UIKit
import SpriteKit
class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: "player")
var moveRate: CGFloat!
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
if UIDevice.current.userInterfaceIdiom == .phone {
moveRate = 5
} else {
moveRate = 15
}
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody?.isDynamic = true
player.physicsBody?.affectedByGravity = false
player.physicsBody?.categoryBitMask = 0b00001
// player.physicsBody?.collisionBitMask = 0b00001
player.physicsBody?.contactTestBitMask = 0b00001
player.position = CGPoint(x: 20 + player.size.width/2, y: view.frame.height / 2)
addChild(player)
let carEngineStart = SKAudioNode(fileNamed: "car_engine_running")
addChild(carEngineStart)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(addCompetitor),
SKAction.wait(forDuration: 4)
])
))
}
override func update(_ currentTime: TimeInterval) {
let internalRollSign = TrialSpriteKit.sign(internalRoll)
switch internalRollSign {
case .zero:
break
case .positive:
if player.position.y < self.size.height {
player.position.y += moveRate
}
case .negative:
if player.position.y > 0 {
player.position.y -= moveRate
}
}
}
enum Car: String, CaseIterable {
case blue = "blue"
case green = "green"
case orange = "orange"
case purple = "purple"
case utili = "utili"
case white = "white"
case yellow = "yellow"
static func random<G: RandomNumberGenerator>(using generator: inout G) -> Car {
return Car.allCases.randomElement(using: &generator)!
}
static func random() -> Car {
var g = SystemRandomNumberGenerator()
return Car.random(using: &g)
}
}
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / /* 0xFFFFFFFF */ 4294967296)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func addCompetitor() {
// Create sprite
let carString = Car.random().rawValue
let car = SKSpriteNode(imageNamed: carString)
car.physicsBody = SKPhysicsBody(rectangleOf: car.size) // 1
car.physicsBody?.isDynamic = true
car.physicsBody?.affectedByGravity = false
// car.physicsBody?.categoryBitMask = 0b00001
car.physicsBody?.collisionBitMask = 0b00001
car.physicsBody?.contactTestBitMask = 0b00001
// Determine where to spawn the car along the Y axis
let actualY = random(min: car.size.height/2, max: size.height - car.size.height/2)
// Position the car slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
car.position = CGPoint(x: size.width + car.size.width/2, y: actualY)
// Add the car to the scene
addChild(car)
// Determine speed of the car
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
// Create the actions
let actionMove = SKAction.move(to: CGPoint(x: -car.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
car.run(SKAction.sequence([actionMove, actionMoveDone]))
}
}
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
contact.bodyA.pinned = true
player.physicsBody?.pinned = true
let explosion = SKSpriteNode(imageNamed: "explosion")
explosion.position = contact.contactPoint
addChild(explosion)
run(
SKAction.sequence(
[
SKAction.playSoundFileNamed("car_explosion", waitForCompletion: true),
SKAction.run({
explosion.removeFromParent()
contact.bodyB.node?.removeFromParent()
}),
SKAction.wait(forDuration: 1),
SKAction.run({
self.player.zRotation = 0
self.player.position = CGPoint(x: 20 + self.player.size.width/2, y: self.view!.frame.height / 2)
self.player.physicsBody?.pinned = false
})
]
)
)
}
}
You have the reason for the multiple nodes in your question.
I noticed it doing this when I added the code to set the pinned
properties of the physicsBody objects to true in the didBegin callback
method.
According to the SpriteKit documentation, when you set the pinned property to true and the parent node has a physics body, the two bodies are treated as if they are connected with a pin joint.
Because the two bodies are connected, contact is being made repeatedly, even if there isn't a collision, resulting in repeated calls to the didBegin function. Each call to didBegin creates a new sprite node. Your game is calling didBegin many more times than you expect so you end up with more sprite nodes than you expect.
The general solution is to create the explosion sprite node only when there is a collision and only create the sprite node once per collision.
I'm an ubernoob developing a game using SpriteKit from scratch and im trying to make a character that will slide back and forth off the sides of the screen (in landscape mode) until collision is detected with another node (that I will add later). Think of pong and how the paddle can move side to side except I want that movement to be completely automatic/infinite.
Side Note: I plan on having this character jump when the screen is touched but continue with the back and forth movement. idk if that makes a difference in your approach.
Ok, so this answer isn't perfect but it's working for me right now so:
basically you touch the screen to spawn a boxd, and when the paddle touches the box some stuff happens (it stops moving):
import SpriteKit
// constants!
class GameScene: SKScene, SKPhysicsContactDelegate {
// A little complicated, but basically we want to have a constant speed across all screen sizes
var sliderSpeed: CGFloat { return self.size.width / 3 }
var slider = SKSpriteNode()
var sliderVelocity = CGFloat(0)
var sliderIsContacted = false
let boxMask = UInt32(2)
let sliderMask = UInt32(4)
// For use to contact slider
func spawnBox(at pos: CGPoint) {
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 45))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .green
shape.position = pos
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = boxMask
pb.contactTestBitMask = sliderMask
shape.physicsBody = pb
addChild(shape)
}
func setupSlider() {
sliderVelocity = sliderSpeed
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 10))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .blue
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = sliderMask
pb.contactTestBitMask = boxMask
pb.velocity.dx = sliderVelocity // moves our slider to the right!
// A little complicated, but basically we want to have a spritenode, not a shapenode:
slider = SKSpriteNode(texture: view!.texture(from: shape))
slider.physicsBody = pb
addChild(slider)
}
func setupWorld() {
let pb = SKPhysicsBody(edgeLoopFrom: frame)
pb.categoryBitMask = UInt32(0)
self.physicsBody = pb
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector.zero
}
override func didMove(to view: SKView) {
setupSlider()
setupWorld()
}
}
// Game loop:
extension GameScene {
// touchesBegan in iOS:
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
spawnBox(at: location)
}
override func update(_ currentTime: TimeInterval) {
let sliderPB = slider.physicsBody!
let halfWidth = slider.size.width/2
// move slider left when it reaches far right border:
if sliderPB.velocity.dx > 0 {
if slider.position.x >= (frame.maxX - halfWidth) {
sliderVelocity = -sliderSpeed
}
}
// move slider right when it reaches far left border:
else {
if slider.position.x <= (frame.minX + halfWidth) {
sliderVelocity = sliderSpeed
}
}
// Keep slider at constant rate:
if sliderIsContacted == false {
sliderPB.velocity.dx = sliderVelocity
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask == sliderMask + boxMask {
sliderIsContacted = true
slider.physicsBody!.velocity.dx = 0 // stop slider
}
}
}
more complicated than it shoudl be, but I"m sdrunk so this is what I got :)
}hope it helps.
I'm making a match-3 game using SpriteKit. Explanation: http://www.raywenderlich.com/75273/make-game-like-candy-crush-with-swift-tutorial-part-2. Please refer to swm93's comment on page 4
This is a tutorial but it seems to have a memory leak in the code. Could anyone possibly download this swift project file and find what causes the memory leak and give possible solutions?
The maker of this tutorial said that there is a memory leak in "handleSwipe(swap)" method and that we can fix it by adding "weak" to field declaration. I tried to write "weak var scene: GameScene?" but if I do, it says "scene is nil" even if I initialized it like this: "scene = GameScene(size: skView.bounds.size)" in "viewDidLoad()" function.
The rest of the classes can be downloaded through my link here.
Even the view controllers have been dismissed, the memory use percentage wouldn't decrease... If I call a GameViewController, dismiss it, then call it again, the memory use is twice. In other words,
PreViewController(18MB) segue-> GameViewController(75MB) dismiss-> PreViewController(75MB) segue-> GameViewController(104MB)
import UIKit
import SpriteKit
import AVFoundation
class GameViewController: UIViewController {
// The scene draws the tiles and cookie sprites, and handles swipes.
var scene: GameScene!
// The level contains the tiles, the cookies, and most of the gameplay logic.
// Needs to be ! because it's not set in init() but in viewDidLoad().
var level: Level!
var movesLeft = 0
var score = 0
#IBOutlet weak var targetLabel: UILabel!
#IBOutlet weak var movesLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet weak var gameOverPanel: UIImageView!
#IBOutlet weak var shuffleButton: UIButton!
var tapGestureRecognizer: UITapGestureRecognizer!
#IBAction func dismiss(sender: UIButton) {
self.dismissViewControllerAnimated(true, completion: {})
}
lazy var backgroundMusic: AVAudioPlayer = {
let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3")
let player = AVAudioPlayer(contentsOfURL: url, error: nil)
player.numberOfLoops = -1
return player
}()
override func prefersStatusBarHidden() -> Bool {
return true
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
}
override func viewDidLoad() {
super.viewDidLoad()
// Configure the view.
let skView = view as! SKView
skView.multipleTouchEnabled = false
// Create and configure the scene.
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
// Load the level.
level = Level(filename: "Level_1")
scene.level = level
scene.addTiles()
scene.swipeHandler = handleSwipe
// Hide the game over panel from the screen.
gameOverPanel.hidden = true
shuffleButton.hidden = true
// Present the scene.
skView.presentScene(scene)
// Load and start background music.
backgroundMusic.play()
// Let's start the game!
beginGame()
}
func beginGame() {
movesLeft = level.maximumMoves
score = 0
updateLabels()
level.resetComboMultiplier()
scene.animateBeginGame() {
self.shuffleButton.hidden = false
}
shuffle()
}
func shuffle() {
// Delete the old cookie sprites, but not the tiles.
scene.removeAllCookieSprites()
// Fill up the level with new cookies, and create sprites for them.
let newCookies = level.shuffle()
scene.addSpritesForCookies(newCookies)
}
// This is the swipe handler. MyScene invokes this function whenever it
// detects that the player performs a swipe.
func handleSwipe(swap: Swap) {
// While cookies are being matched and new cookies fall down to fill up
// the holes, we don't want the player to tap on anything.
view.userInteractionEnabled = false
if level.isPossibleSwap(swap) {
level.performSwap(swap)
scene.animateSwap(swap, completion: handleMatches)
} else {
scene.animateInvalidSwap(swap) {
self.view.userInteractionEnabled = true
}
}
}
// This is the main loop that removes any matching cookies and fills up the
// holes with new cookies.
func handleMatches() {
// Detect if there are any matches left.
let chains = level.removeMatches()
// If there are no more matches, then the player gets to move again.
if chains.count == 0 {
beginNextTurn()
return
}
// First, remove any matches...
scene.animateMatchedCookies(chains) {
// Add the new scores to the total.
for chain in chains {
self.score += chain.score
}
self.updateLabels()
// ...then shift down any cookies that have a hole below them...
let columns = self.level.fillHoles()
self.scene.animateFallingCookies(columns) {
// ...and finally, add new cookies at the top.
let columns = self.level.topUpCookies()
self.scene.animateNewCookies(columns) {
// Keep repeating this cycle until there are no more matches.
self.handleMatches()
}
}
}
}
func beginNextTurn() {
level.resetComboMultiplier()
level.detectPossibleSwaps()
view.userInteractionEnabled = true
decrementMoves()
}
func updateLabels() {
targetLabel.text = String(format: "%ld", level.targetScore)
movesLabel.text = String(format: "%ld", movesLeft)
scoreLabel.text = String(format: "%ld", score)
}
func decrementMoves() {
--movesLeft
updateLabels()
if score >= level.targetScore {
gameOverPanel.image = UIImage(named: "LevelComplete")
showGameOver()
}
else if movesLeft == 0 {
gameOverPanel.image = UIImage(named: "GameOver")
showGameOver()
}
}
func showGameOver() {
gameOverPanel.hidden = false
scene.userInteractionEnabled = false
shuffleButton.hidden = true
scene.animateGameOver() {
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver")
self.view.addGestureRecognizer(self.tapGestureRecognizer)
}
}
func hideGameOver() {
view.removeGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer = nil
gameOverPanel.hidden = true
scene.userInteractionEnabled = true
beginGame()
}
#IBAction func shuffleButtonPressed(AnyObject) {
shuffle()
// Pressing the shuffle button costs a move.
decrementMoves()
}
}
,
import SpriteKit
class GameScene: SKScene {
// This is marked as ! because it will not initially have a value, but pretty
// soon after the GameScene is created it will be given a Level object, and
// from then on it will always have one (it will never be nil again).
var level: Level!
var swipeHandler: ((Swap) -> ())?
let TileWidth: CGFloat = 32.0
let TileHeight: CGFloat = 36.0
let gameLayer = SKNode()
let cookiesLayer = SKNode()
let tilesLayer = SKNode()
let cropLayer = SKCropNode()
let maskLayer = SKNode()
var swipeFromColumn: Int?
var swipeFromRow: Int?
var selectionSprite = SKSpriteNode()
// Pre-load the resources
let swapSound = SKAction.playSoundFileNamed("Chomp.wav", waitForCompletion: false)
let invalidSwapSound = SKAction.playSoundFileNamed("Error.wav", waitForCompletion: false)
let matchSound = SKAction.playSoundFileNamed("Ka-Ching.wav", waitForCompletion: false)
let fallingCookieSound = SKAction.playSoundFileNamed("Scrape.wav", waitForCompletion: false)
let addCookieSound = SKAction.playSoundFileNamed("Drip.wav", waitForCompletion: false)
// MARK: Game Setup
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder) is not used in this app")
}
override init(size: CGSize) {
super.init(size: size)
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let background = SKSpriteNode(imageNamed: "Background")
addChild(background)
gameLayer.hidden = true
addChild(gameLayer)
let layerPosition = CGPoint(
x: -TileWidth * CGFloat(NumColumns) / 2,
y: -TileHeight * CGFloat(NumRows) / 2)
tilesLayer.position = layerPosition
gameLayer.addChild(tilesLayer)
// We use a crop layer to prevent cookies from being drawn across gaps
// in the level design.
gameLayer.addChild(cropLayer)
// The mask layer determines which part of the cookiesLayer is visible.
maskLayer.position = layerPosition
cropLayer.maskNode = maskLayer
// This layer holds the Cookie sprites. The positions of these sprites
// are relative to the cookiesLayer's bottom-left corner.
cookiesLayer.position = layerPosition
cropLayer.addChild(cookiesLayer)
// nil means that these properties have invalid values.
swipeFromColumn = nil
swipeFromRow = nil
// Pre-load the label font so prevent delays during game play.
SKLabelNode(fontNamed: "GillSans-BoldItalic")
}
func addSpritesForCookies(cookies: Set<Cookie>) {
for cookie in cookies {
// Create a new sprite for the cookie and add it to the cookiesLayer.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row:cookie.row)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// Give each cookie sprite a small, random delay.
sprite.alpha = 0
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(0.25, withRange: 0.5),
SKAction.group([
SKAction.fadeInWithDuration(0.25),
SKAction.scaleTo(1.0, duration: 0.25)
])
]))
}
}
func removeAllCookieSprites() {
cookiesLayer.removeAllChildren()
}
func addTiles() {
for row in 0..<NumRows {
for column in 0..<NumColumns {
// If there is a tile at this position, then create a new tile
// sprite and add it to the mask layer.
if let tile = level.tileAtColumn(column, row: row) {
let tileNode = SKSpriteNode(imageNamed: "MaskTile")
tileNode.position = pointForColumn(column, row: row)
maskLayer.addChild(tileNode)
}
}
}
// The tile pattern is drawn *in between* the level tiles. That's why
// there is an extra column and row of them.
for row in 0...NumRows {
for column in 0...NumColumns {
let topLeft = (column > 0) && (row < NumRows)
&& level.tileAtColumn(column - 1, row: row) != nil
let bottomLeft = (column > 0) && (row > 0)
&& level.tileAtColumn(column - 1, row: row - 1) != nil
let topRight = (column < NumColumns) && (row < NumRows)
&& level.tileAtColumn(column, row: row) != nil
let bottomRight = (column < NumColumns) && (row > 0)
&& level.tileAtColumn(column, row: row - 1) != nil
// The tiles are named from 0 to 15, according to the bitmask that is
// made by combining these four values.
let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3
// Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
if value != 0 && value != 6 && value != 9 {
let name = String(format: "Tile_%ld", value)
let tileNode = SKSpriteNode(imageNamed: name)
var point = pointForColumn(column, row: row)
point.x -= TileWidth/2
point.y -= TileHeight/2
tileNode.position = point
tilesLayer.addChild(tileNode)
}
}
}
}
// MARK: Conversion Routines
// Converts a column,row pair into a CGPoint that is relative to the cookieLayer.
func pointForColumn(column: Int, row: Int) -> CGPoint {
return CGPoint(
x: CGFloat(column)*TileWidth + TileWidth/2,
y: CGFloat(row)*TileHeight + TileHeight/2)
}
// Converts a point relative to the cookieLayer into column and row numbers.
func convertPoint(point: CGPoint) -> (success: Bool, column: Int, row: Int) {
// Is this a valid location within the cookies layer? If yes,
// calculate the corresponding row and column numbers.
if point.x >= 0 && point.x < CGFloat(NumColumns)*TileWidth &&
point.y >= 0 && point.y < CGFloat(NumRows)*TileHeight {
return (true, Int(point.x / TileWidth), Int(point.y / TileHeight))
} else {
return (false, 0, 0) // invalid location
}
}
// MARK: Detecting Swipes
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// Convert the touch location to a point relative to the cookiesLayer.
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
// If the touch is inside a square, then this might be the start of a
// swipe motion.
let (success, column, row) = convertPoint(location)
if success {
// The touch must be on a cookie, not on an empty tile.
if let cookie = level.cookieAtColumn(column, row: row) {
// Remember in which column and row the swipe started, so we can compare
// them later to find the direction of the swipe. This is also the first
// cookie that will be swapped.
swipeFromColumn = column
swipeFromRow = row
showSelectionIndicatorForCookie(cookie)
}
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
// If swipeFromColumn is nil then either the swipe began outside
// the valid area or the game has already swapped the cookies and we need
// to ignore the rest of the motion.
if swipeFromColumn == nil { return }
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
let (success, column, row) = convertPoint(location)
if success {
// Figure out in which direction the player swiped. Diagonal swipes
// are not allowed.
var horzDelta = 0, vertDelta = 0
if column < swipeFromColumn! { // swipe left
horzDelta = -1
} else if column > swipeFromColumn! { // swipe right
horzDelta = 1
} else if row < swipeFromRow! { // swipe down
vertDelta = -1
} else if row > swipeFromRow! { // swipe up
vertDelta = 1
}
// Only try swapping when the user swiped into a new square.
if horzDelta != 0 || vertDelta != 0 {
trySwapHorizontal(horzDelta, vertical: vertDelta)
hideSelectionIndicator()
// Ignore the rest of this swipe motion from now on.
swipeFromColumn = nil
}
}
}
// We get here after the user performs a swipe. This sets in motion a whole
// chain of events: 1) swap the cookies, 2) remove the matching lines, 3)
// drop new cookies into the screen, 4) check if they create new matches,
// and so on.
func trySwapHorizontal(horzDelta: Int, vertical vertDelta: Int) {
let toColumn = swipeFromColumn! + horzDelta
let toRow = swipeFromRow! + vertDelta
if toColumn < 0 || toColumn >= NumColumns { return }
if toRow < 0 || toRow >= NumRows { return }
// Can't swap if there is no cookie to swap with. This happens when the user
// swipes into a gap where there is no tile.
if let toCookie = level.cookieAtColumn(toColumn, row: toRow),
let fromCookie = level.cookieAtColumn(swipeFromColumn!, row: swipeFromRow!),
let handler = swipeHandler {
// Communicate this swap request back to the ViewController.
let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
handler(swap)
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
// Remove the selection indicator with a fade-out. We only need to do this
// when the player didn't actually swipe.
if selectionSprite.parent != nil && swipeFromColumn != nil {
hideSelectionIndicator()
}
// If the gesture ended, regardless of whether if was a valid swipe or not,
// reset the starting column and row numbers.
swipeFromColumn = nil
swipeFromRow = nil
}
override func touchesCancelled(touches: Set<NSObject>, withEvent event: UIEvent) {
touchesEnded(touches, withEvent: event)
}
// MARK: Animations
func animateSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
// Put the cookie you started with on top.
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.3
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
spriteA.runAction(moveA, completion: completion)
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteB.runAction(moveB)
runAction(swapSound)
}
func animateInvalidSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.2
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteA.runAction(SKAction.sequence([moveA, moveB]), completion: completion)
spriteB.runAction(SKAction.sequence([moveB, moveA]))
runAction(invalidSwapSound)
}
func animateMatchedCookies(chains: Set<Chain>, completion: () -> ()) {
for chain in chains {
animateScoreForChain(chain)
for cookie in chain.cookies {
// It may happen that the same Cookie object is part of two chains
// (L-shape or T-shape match). In that case, its sprite should only be
// removed once.
if let sprite = cookie.sprite {
if sprite.actionForKey("removing") == nil {
let scaleAction = SKAction.scaleTo(0.1, duration: 0.3)
scaleAction.timingMode = .EaseOut
sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
withKey:"removing")
}
}
}
}
runAction(matchSound)
runAction(SKAction.waitForDuration(0.3), completion: completion)
}
func animateScoreForChain(chain: Chain) {
// Figure out what the midpoint of the chain is.
let firstSprite = chain.firstCookie().sprite!
let lastSprite = chain.lastCookie().sprite!
let centerPosition = CGPoint(
x: (firstSprite.position.x + lastSprite.position.x)/2,
y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)
let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
scoreLabel.fontSize = 16
scoreLabel.text = String(format: "%ld", chain.score)
scoreLabel.position = centerPosition
scoreLabel.zPosition = 300
cookiesLayer.addChild(scoreLabel)
let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7)
moveAction.timingMode = .EaseOut
scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}
func animateFallingCookies(columns: [[Cookie]], completion: () -> ()) {
var longestDuration: NSTimeInterval = 0
for array in columns {
for (idx, cookie) in enumerate(array) {
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let delay = 0.05 + 0.15*NSTimeInterval(idx)
let sprite = cookie.sprite!
let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1)
longestDuration = max(longestDuration, duration + delay)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([moveAction, fallingCookieSound])]))
}
}
// Wait until all the cookies have fallen down before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateNewCookies(columns: [[Cookie]], completion: () -> ()) {
// wait that amount before we trigger the completion block.
var longestDuration: NSTimeInterval = 0
for array in columns {
let startRow = array[0].row + 1
for (idx, cookie) in enumerate(array) {
// Create a new sprite for the cookie.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row: startRow)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// fall after one another.
let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1)
// Calculate duration based on far the cookie has to fall.
let duration = NSTimeInterval(startRow - cookie.row) * 0.1
longestDuration = max(longestDuration, duration + delay)
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.alpha = 0
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([
SKAction.fadeInWithDuration(0.05),
moveAction,
addCookieSound])
]))
}
}
// Wait until the animations are done before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateGameOver(completion: () -> ()) {
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseIn
gameLayer.runAction(action, completion: completion)
}
func animateBeginGame(completion: () -> ()) {
gameLayer.hidden = false
gameLayer.position = CGPoint(x: 0, y: size.height)
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseOut
gameLayer.runAction(action, completion: completion)
}
// MARK: Selection Indicator
func showSelectionIndicatorForCookie(cookie: Cookie) {
if selectionSprite.parent != nil {
selectionSprite.removeFromParent()
}
if let sprite = cookie.sprite {
let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
selectionSprite.size = texture.size()
selectionSprite.runAction(SKAction.setTexture(texture))
sprite.addChild(selectionSprite)
selectionSprite.alpha = 1.0
}
}
func hideSelectionIndicator() {
selectionSprite.runAction(SKAction.sequence([
SKAction.fadeOutWithDuration(0.3),
SKAction.removeFromParent()]))
}
}