iOS animation using spritekit - ios

I'm trying to create an animation on my iOS application using spritekit.
so far the code that i have done for this is
import UIKit
import SpriteKit
import GameplayKit
class TygaScene: SKScene {
var tygaFrames: [SKTexture]? // array of images
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(size: CGSize) { // init function then set size
super.init(size: size)
self.backgroundColor = UIColor.white // setting colour to background
var frames:[SKTexture] = [] // create place holder for texture to initialise
let tygaAtlas = SKTextureAtlas(named: "tyga")
for index in 1...5 { // adding all images from folder
let textureName = "tyga_\(index)" // naming the first bit of the file
let texture = tygaAtlas.textureNamed(textureName)
frames.append(texture)
}
self.tygaFrames = frames
}
func moveTyga() {
let texture = self.tygaFrames![0]
let tyga = SKSpriteNode(texture: texture)
tyga.size = CGSize(width: 280, height: 280)
let randometygaYPositionGenerator = GKRandomDistribution(lowestValue: 50, highestValue: Int(self.frame.size.height))
let yPosition = CGFloat(randometygaYPositionGenerator.nextInt())
let rightToLeft = arc4random() % 2 == 0
let xPosition = rightToLeft ? self.frame.size.width + tyga.size.width / 2 : -tyga.size.width / 2
tyga.position = CGPoint(x: xPosition, y: yPosition)
if rightToLeft {
tyga.xScale = -1
}
self.addChild(tyga)
tyga.run(SKAction.repeatForever(SKAction.animate(with: self.tygaFrames!, timePerFrame: 0.05, resize: false, restore: true)))
var distanceToCover = self.frame.size.width + tyga.size.width
if rightToLeft {
distanceToCover += -1
}
let time = TimeInterval(abs(distanceToCover/140))
let moveAction = SKAction.run {
tyga.removeAllActions ()
tyga.removeFromParent()
}
let allActions = SKAction.sequence([moveAction, removeAction])
tyga.run(allActions)
}
}
The part that is giving an error is the line:
let allActions = SKAction.sequence([moveAction, removeAction])
tyga.run(allActions)
}
could you please advise me on how i could overcome this problem?
Thanks.

Related

Collision in Spritekit not collide with another node

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).

How to create a SKSpriteNode with Custom GKComponent?

So im currently working on a game with spriteKit and gameplayKit. I've created a custom GKComponent class that I'm giving to certain SKSpriteNode via the scene inspector in Xcode (image). Now, when I want to re-create random SKSpritenodes with the same GKComponent, it doesn't seems to work because the overridden update method of my custom GKComponent is not working. Any clues?
I've tried to give the node a new entity object (since it was returning nil) and I gave that entity my custom GKComponent but nothing is working
// In scene
let node = SKSpriteNode(texture: "coin", color: UIColor.clear, size: CGSize(width: 60, height: 60))
let entities: GKEntity! = GKEntity()
let component: CollectableComponent = CollectableComponent()
component.itemType = 1
node.anchorPoint = CGPoint(x: 0.5, y: 0.5)
node.zPosition = 6
node.setScale(0.5)
node.size = CGSize(width: 60, height: 60)
node.position = CGPoint(x: 90, y: 90)
entities.addComponent(component)
node.entity = entities
self.scene?.addChild(node)
// GKComponent
class CustomComponent: GKComponent {
var node: SKSpriteNode?
#GKInspectable var itemType: Int = 0
#GKInspectable var isStatic: Bool = false
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func update(deltaTime seconds: TimeInterval) {
if node == nil {
if let nodeComponent = self.entity?.component(ofType: GKSKNodeComponent.self){
node = nodeComponent.node as? SKSpriteNode
}
}
}
}
Desired behaviour: if the nodeComponent variable in CustomComponent is not nil, I would like to make it run an animation like this:
node = nodeComponent.node as? SKSpriteNode; node.run(SKAction.moveBy(x: 0, y: 15, duration: 0.3));
but with the code, the action would not be running
This is the way you want to be doing this.
Take note that you need to retain your entities, so a global variable is needed.
Also, when you add a node to GKSKNodeComponent, the node.entity variable is set automatically.
class GameScene : SKScene{
var entities: [GKEntity] = [GKEntity]()
func doSomething(){
let node = SKSpriteNode(texture: "coin", color: UIColor.clear, size: CGSize(width: 60, height: 60))
node.anchorPoint = CGPoint(x: 0.5, y: 0.5)
node.zPosition = 6
node.setScale(0.5) //<--- YUCK!!!!!
node.size = CGSize(width: 60, height: 60)
node.position = CGPoint(x: 90, y: 90)
self.scene?.addChild(node)
let entity = GKEntity()
let nodeComponent : GKSKNodeComponent = GKSKNodeComponent(node:node)
let component: CollectableComponent = CollectableComponent()
component.itemType = 1
entity.addComponent(nodeComponent)
entity.addComponent(component)
entities.append(entity)
}
}
// GKComponent
class CustomComponent: GKComponent {
//We want this line to crash if node is empty, so do not use ?
lazy var node: SKSpriteNode! = {return self.entity!.component(ofType: GKSKNodeComponent.self).node as? SKSpriteNode! }
#GKInspectable var itemType: Int = 0
#GKInspectable var isStatic: Bool = false
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func update(deltaTime seconds: TimeInterval) {
}
}

SpriteKit - Adding enemy causes frame drop

My game is a side scroller using SpriteKit. The player stays at the same x-position and the obstacles are moving towards him. But whenever I add an enemy the frame rate drops for a short moment. I tried to change many things, like removing the enemy's dynamics or animation. But nothing works.
Here's some code:
func addZombie() {
let zombie = Zombie()
zombie.position = CGPoint(x: screenWidth + zombie.size.width * 0.5, y: screenHeight * 0.5)
zombies.append(zombie)
addChild(zombie)
}
The above method creates the enemies. It's called by using:
func generateZombies() {
let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
self.addZombie()
}
runAction(SKAction.repeatActionForever(SKAction.sequence([wait, run])))
}
I also tried NSTimer, but nothing works. Please help me.
Zombie():
class Zombie: SKSpriteNode {
var textureArrayWalking = [SKTexture]()
init() {
let mySize = CGSize(width: Player().size.width, height: Player().size.height * (4/3))
super.init(texture: nil, color: UIColor.greenColor(), size: mySize)
orderTextureAtlases()
texture = textureArrayWalking[0]
zPosition = 1
physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: (2/3) * self.size.width, height: self.size.height))
physicsBody?.restitution = 0.0
physicsBody?.categoryBitMask = enemyCategory
physicsBody?.collisionBitMask = groundCategory
physicsBody?.contactTestBitMask = playerCategory | bulletCategory | groundCategory
physicsBody!.allowsRotation = false;
walk()
}
func walk() {
runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(textureArrayWalking, timePerFrame: 0.07)), withKey: walkingActionKeyZombie)
}
func orderTextureAtlases() {
let textureAtlasWalking = SKTextureAtlas(named: "ZombieWalking")
for index in 0..<textureAtlasWalking.textureNames.count {
let name = "ZombieWalking_JeromeWalk_\(index)"
let tex = SKTexture(imageNamed: name)
tex.filteringMode = .Nearest
textureArrayWalking.append(tex)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Force custom view to redraw (or invalidate) using a timer

I've created a custom widget, which is animated. Now my problem is that I can't redraw the view, when the corresponding data gets updated.
Just don't hold anything against me in the code. This is my first piece of code in swift and I haven't worked with neither swift nor with Objective-C :-D
And also I've read the following questions, but they didn't help me:
How to force a view to render itself?
what-is-the-most-robust-way-to-force-a-uiview-to-redraw
p.s. : I can see the output of print(digit.phase) in the console.
p.s.s: I've also used performSelectorOnMainThread for calling the setNeedsDisplay function
The code:
import UIKit
struct Digit {
var targetDigit: Int
var currentDigit: Int
var phase: Float
}
#IBDesignable class RollerCounter: UIView {
var view: UIView!
var viewRect: CGRect!
var intNumber: Int
var digits = [Digit]()
let baseY = 20
var timer: NSTimer?
#IBInspectable var number: Int {
get {
return intNumber
}
set(number) {
intNumber = number
digits = []
var tempNumber:Int = intNumber
while tempNumber > 0 {
digits.append(Digit(targetDigit: tempNumber % 10, currentDigit: Int(rand()) % 10, phase: 0.0))
tempNumber /= 10
}
}
}
//init
override init(frame: CGRect) {
// set properties:
intNumber = 1111
super.init(frame: frame)
// setup the thing!
setup()
}
required init?(coder aDecoder: NSCoder) {
intNumber = 1111
super.init(coder: aDecoder)
// setup the thing
setup()
}
// Inital setup
func setup() {
let viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
view = UIView(frame: viewRect)
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
self.setNeedsDisplay()
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.0)
}
func animate() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.016, target: self, selector: Selector("tick"), userInfo: nil, repeats: true)
}
func tick() {
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
setNeedsDisplay()
//TEST: Also tested this
// if let rect = viewRect {
// drawRect(rect)
// } else {
// viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
// drawRect(viewRect
// }
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
var tempNumber: Int = number
let strTempNumber = String(tempNumber)
var index: Int = 1
let width = Float(rect.width)
let charWidth: Float = Float(rect.width) / Float(strTempNumber.characters.count)
let charHeight: CGFloat = 36
let color = UIColor.blackColor()
let font: UIFont = UIFont(name: "Helvetica Neue", size: charHeight)!
let paraStyle = NSMutableParagraphStyle()
paraStyle.lineSpacing = 6.0
let skew = 0.1
let textAttribs = [
NSForegroundColorAttributeName: color,
NSParagraphStyleAttributeName: paraStyle,
NSObliquenessAttributeName: skew,
NSFontAttributeName: font
]
for digit in digits {
let strCurrentDigit: NSString = String(digit.currentDigit) as NSString
let strNextDigit: NSString = String(digit.currentDigit - 1) as NSString
let xPos = width - Float(index) * charWidth
let yPos = Float(baseY) + Float(charHeight) * digit.phase
let point: CGPoint = CGPoint(x: Int(xPos), y: Int(yPos))
strCurrentDigit.drawAtPoint(point, withAttributes: textAttribs)
let nextDigitYPos = yPos - Float(charHeight) * 1.2
let nextDigitPoint: CGPoint = CGPoint(x: Int(xPos), y: Int(nextDigitYPos))
strNextDigit.drawAtPoint(nextDigitPoint, withAttributes: textAttribs)
index++
tempNumber /= 10
}
}
}
Sorry folks. My bad :-(
There's nothing wrong with the invalidation system. Here's what's wrong:
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
As it turns out, the changes to phase only get reflected in the local digit instance inside the for loop
But just to be clear, the setNeedsDisplay() call inside the tick method is crucial for the view to be updated.

How to create progress bar in sprite kit?

I want to create my own progress bar in Sprite Kit.
I figured I will need to images - one fully empty progress bar and filled progress bar.
I have those images, I put filled one on top of empty one, they are regular SKSPriteNodes now I can't figure out how do I cut my filled image where I need?
How do I cut SKSpriteNode image at certain point? Maybe texture?
I would recommend looking into SKCropNode. For a visual aid how SKCropNode works, look it up in the Apple Programming Guide. I have read through the entire document multiple times and it is a particularly good read.
SKCropNode is basically an SKNode which you add to your scene, but its children can be cropped by a mask. This mask is set in the maskNode property of the SKCropNode. In this way, you only need one texture image. I would subclass SKCropNode to implement functionality to move or resize the mask, so you can easily update its appearance.
#interface CustomProgressBar : SKCropNode
/// Set to a value between 0.0 and 1.0.
- (void) setProgress:(CGFloat) progress;
#end
#implementation CustomProgressBar
- (id)init {
if (self = [super init]) {
self.maskNode = [SKSpriteNode spriteNodeWithColor:[SKColor whiteColor] size:CGSizeMake(300,20)];
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"progressBarImage"];
[self addChild:sprite];
}
return self;
}
- (void) setProgress:(CGFloat) progress {
self.maskNode.xScale = progress;
}
#end
In your scene:
#import "CustomProgressBar.h"
// ...
CustomProgressBar * progressBar = [CustomProgressBar new];
[self addChild:progressBar];
// ...
[progressBar setProgress:0.3];
// ...
[progressBar setProgress:0.7];
Note: this code doesn't move the mask (so the sprite will be cropped on either side) but I'm sure you get the idea.
Quite simply: you need a frame image (optional) and a "bar" image. The bar image out to be a single, solid color and as high as you need it and 1 or 2 pixels wide. A SKShapeNode as bar will do as well.
Just making the bar and animating is simply a matter of changing the SKSpriteNode's size property. For example to make the bar represent progress between 0 and 100 just do:
sprite.size = CGSizeMake(progressValue, sprite.size.height);
Update the size whenever progressValue changes.
You'll notice the image will increase in width to both left and right, to make it stretch only to the right change the anchorPoint to left-align the image:
sprite.anchorPoint = CGPointMake(0.0, 0.5);
That is all. Draw a frame sprite around it to make it look nicer.
that is my ProgressBar in swift :
import Foundation
import SpriteKit
class IMProgressBar : SKNode{
var emptySprite : SKSpriteNode? = nil
var progressBar : SKCropNode
init(emptyImageName: String!,filledImageName : String)
{
progressBar = SKCropNode()
super.init()
let filledImage = SKSpriteNode(imageNamed: filledImageName)
progressBar.addChild(filledImage)
progressBar.maskNode = SKSpriteNode(color: UIColor.whiteColor(),
size: CGSize(width: filledImage.size.width * 2, height: filledImage.size.height * 2))
progressBar.maskNode?.position = CGPoint(x: -filledImage.size.width / 2,y: -filledImage.size.height / 2)
progressBar.zPosition = 0.1
self.addChild(progressBar)
if emptyImageName != nil{
emptySprite = SKSpriteNode.init(imageNamed: emptyImageName)
self.addChild(emptySprite!)
}
}
func setXProgress(xProgress : CGFloat){
var value = xProgress
if xProgress < 0{
value = 0
}
if xProgress > 1 {
value = 1
}
progressBar.maskNode?.xScale = value
}
func setYProgress(yProgress : CGFloat){
var value = yProgress
if yProgress < 0{
value = 0
}
if yProgress > 1 {
value = 1
}
progressBar.maskNode?.yScale = value
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//How to use :
let progressBar = IMProgressBar(emptyImageName: "emptyImage",filledImageName: "filledImage")
or
let progressBar = IMProgressBar(emptyImageName: nil,filledImageName: "filledImage")
and add this progressBar to any SKNode :
self.addChild(progressBar)
//That's all.
Assuming HealthBarNode is a subclass of SKSpriteNode with a public property health that varies between 0.0 and 1.0 and whose parental property texture is generated from the entire color bar image of width _textureWidth (a private property), you could do something like this:
- (void)setHealth:(CGFloat)fraction
{
self.health = MIN(MAX(0.0, fraction), 1.0); // clamp health between 0.0 and 1.0
SKTexture *textureFrac = [SKTexture textureWithRect:CGRectMake(0, 0, fraction, 1.0) inTexture:self.texture];
// check docs to understand why you can pass in self.texture as the last parameter every time
self.size = CGSizeMake(fraction * _textureWidth, self.size.height);
self.texture = textureFrac;
}
Setting the health to a new value will cause the health bar (added as a child to the main scene, say) to get cropped properly.
I built a small library to deal with this exact scenario! Here is SpriteBar: https://github.com/henryeverett/SpriteBar
There is no "cutting" an image/texture.
An alternative to what Cocos offered is to make a couple of textures and interchange them into your node depending on health. I did a game where the health bar changed texture every 10 points (range was 0-100). After some trial and error though, I just ended up doing what Cocos already suggested.
I did it like this, and it works perfectly.
So first, I declared a SKSpriteNode:
baseBar = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:CGSizeMake(CGRectGetMidX(self.frame)-40, self.frame.size.height/10)];
//The following will make the health bar to reduce from right to left
//Change it to (1,0.5) if you want to have it the other way
//But you'd have to play with the positioning as well
[baseBar setAnchorPoint:CGPointMake(0, 0.5)];
CGFloat goodWidth, goodHeight;
goodHeight =self.frame.size.height-(baseBar.frame.size.height*2/3);
goodWidth =self.frame.size.width-(10 +baseBar.frame.size.width);
[baseBar setPosition:CGPointMake(goodWidth, goodHeight)];
[self addChild:baseBar];
I then added a 'Frame' for the bar, with an SKShapeNode, without fill colour (clearcolour), and a stroke colour:
//The following was so useful
SKShapeNode *edges = [SKShapeNode shapeNodeWithRect:baseBar.frame];
edges.fillColor = [UIColor clearColor];
edges.strokeColor = [UIColor blackColor];
[self addChild:edges];
When I wanted to reduce the health, I did the following:
if (playerHealthRatio>0) {
playerHealthRatio -= 1;
CGFloat ratio = playerHealthRatio / OriginalPlayerHealth;
CGFloat newWidth =baseBar.frame.size.width*ratio;
NSLog(#"Ratio: %f newwidth: %f",ratio,newWidth);
[baseBar runAction:[SKAction resizeToWidth:newWidth duration:0.5]];
}else{
// NSLog(#"Game over");
}
Simple, clean and not complicated at all.
Swift 4:
( my answer 1 -> make a rapid and simple progress bar)
To make a simple progress bar based to colors you can subclass a simple SKNode without using SKCropNode:
class SKProgressBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = SKSpriteNode(color: baseColor, size: size)
self.coverSprite = SKSpriteNode(color: coverColor, size: size)
self.addChild(baseSprite)
self.addChild(coverSprite)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 0.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width - (originalSize.width * value)
}
self.coverSprite.size = CGSize(width: originalSize.width-calculateFraction, height: originalSize.height)
if value>0.0 && value<1.0 {
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFraction)/2,y:self.coverSprite.position.y)
}
}
}
Usage:
self.energyProgressBar = SKProgressBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
addChild(self.energyProgressBar)
// other code to see progress changing..
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
Swift 4
( my answer 3 -> old SpriteBar project fully translated to swift)
To make a progress bar based to SKTextureAtlas you can use the Objective C project called SpriteBar maded by Henry Everett.
I've forked and fully translated this project, this is the source:
class SpriteBar: SKSpriteNode {
var textureReference = ""
var atlas: SKTextureAtlas!
var availableTextureAddresses = Array<Int>()
var timer = Timer()
var timerInterval = TimeInterval()
var currentTime = TimeInterval()
var timerTarget: AnyObject!
var timerSelector: Selector!
init() {
let defaultAtlas = SKTextureAtlas(named: "sb_default")
let firstTxt = defaultAtlas.textureNames[0].replacingOccurrences(of: "#2x", with: "")
let texture = defaultAtlas.textureNamed(firstTxt)
super.init(texture: texture, color: .clear, size: texture.size())
self.atlas = defaultAtlas
commonInit()
}
convenience init(textureAtlas: SKTextureAtlas?) {
self.init()
self.atlas = textureAtlas
commonInit()
}
func commonInit() {
self.textureReference = "progress"
resetProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func closestAvailableToPercent(_ percent:Int)->Int {
var closest = 0
for thisPerc in self.availableTextureAddresses {
if labs(Int(thisPerc) - percent) < labs(closest - percent) {
closest = Int(thisPerc)
}
}
return closest
}
func percentFromTextureName(_ string:String) -> Int? {
let clippedString = string.replacingOccurrences(of: "#2x", with: "")
let pattern = "(?<=\(textureReference)_)([0-9]+)(?=.png)"
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex?.matches(in: clippedString, options: [], range: NSRange(location: 0, length: clippedString.count))
// If the matches don't equal 1, you have done something wrong.
if matches?.count != 1 {
NSException(name: NSExceptionName(rawValue: String("SpriteBar: Incorrect texture naming.")), reason: "Textures should follow naming convention: \(textureReference)_#.png. Failed texture name: \(string)", userInfo: nil).raise()
}
for match: NSTextCheckingResult? in matches ?? [NSTextCheckingResult?]() {
let matchRange = match?.range(at: 1)
let range = Range(matchRange!, in: clippedString)!
return Int(clippedString[range.lowerBound..<range.upperBound])
}
return nil
}
func resetProgress() {
self.texture = self.atlas.textureNamed("\(self.textureReference)_\(closestAvailableToPercent(0)).png")
self.availableTextureAddresses = []
for name in self.atlas.textureNames {
self.availableTextureAddresses.append(self.percentFromTextureName(name)!)
}
self.invalidateTimer()
self.currentTime = 0
}
func setProgress(_ progress:CGFloat) {
// Set texure
let percent: CGFloat = CGFloat(lrint(Double(progress * 100)))
let name = "\(textureReference)_\(self.closestAvailableToPercent(Int(percent))).png"
self.texture = self.atlas.textureNamed(name)
// If we have reached 100%, invalidate the timer and perform selector on passed in object.
if fabsf(Float(progress)) >= fabsf(1.0) {
if timerTarget != nil && timerTarget.responds(to: timerSelector) {
typealias MyTimerFunc = #convention(c) (AnyObject, Selector) -> Void
let imp: IMP = timerTarget.method(for: timerSelector)
let newImplementation = unsafeBitCast(imp, to: MyTimerFunc.self)
newImplementation(self.timerTarget, self.timerSelector)
}
timer.invalidate()
}
}
func setProgressWithValue(_ progress:CGFloat, ofTotal maxValue:CGFloat) {
self.setProgress(progress/maxValue)
}
func numberOfFrames(inAnimation animationName: String) -> Int {
// Get the number of frames in the animation.
let allAnimationNames = atlas.textureNames
let nameFilter = NSPredicate(format: "SELF CONTAINS[cd] %#", animationName)
return ((allAnimationNames as NSArray).filtered(using: nameFilter)).count
}
func startBarProgress(withTimer seconds: TimeInterval, target: Any?, selector: Selector) {
resetProgress()
timerTarget = target as AnyObject
timerSelector = selector
// Split the progress time between animation frames
timerInterval = seconds / TimeInterval((numberOfFrames(inAnimation: textureReference) - 1))
timer = Timer.scheduledTimer(timeInterval: timerInterval, target: self, selector: #selector(self.timerTick(_:)), userInfo: seconds, repeats: true)
}
#objc func timerTick(_ timer: Timer) {
// Increment timer interval counter
currentTime += timerInterval
// Make sure we don't exceed the total time
if currentTime <= timer.userInfo as! Double {
setProgressWithValue(CGFloat(currentTime), ofTotal: timer.userInfo as! CGFloat)
}
}
func invalidateTimer() {
timer.invalidate()
}
}
Usage:
let progressBarAtlas = SKTextureAtlas.init(named: "sb_default")
self.energyProgressBar = SpriteBar(textureAtlas: progressBarAtlas)
self.addChild(self.energyProgressBar)
self.energyProgressBar.size = CGSize(width:350, height:150)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let action6 = SKAction.run {
self.energyProgressBar.startBarProgress(withTimer: 10, target: self, selector: #selector(self.timeOver))
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5,wait,action6])
self.run(sequence)
To have more details you can find my GitHUB repo here
Swift 4:
( my answer 2 -> make a complex progress bar using textures)
To make a complex progress bar based to texture and colors you can subclass a simple SKNode. About this case, SpriteKit for now (swift v4.1.2) doesn't have a method to directly cutting a SKTexture. We need to use another method called texture(from:crop:)
class SKProgressImageBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
var originalCoverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseImageName:String="", coverImageName:String="", baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = baseImageName.isEmpty ? SKSpriteNode(color: baseColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:baseImageName), size: size)
self.coverSprite = coverImageName.isEmpty ? SKSpriteNode(color: coverColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:coverImageName), size: size)
self.originalCoverSprite = self.coverSprite.copy() as! SKSpriteNode
self.addChild(baseSprite)
self.addChild(coverSprite)
self.coverSprite.zPosition = 2.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
self.coverSprite.texture = self.originalCoverSprite.texture
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 1.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width * value
}
let coverRect = CGRect(origin: self.baseSprite.frame.origin, size: CGSize(width:calculateFraction,height:self.baseSprite.size.height))
if let parent = self.parent, parent is SKScene, let parentView = (parent as! SKScene).view {
if let texture = parentView.texture(from: self.originalCoverSprite, crop: coverRect) {
let sprite = SKSpriteNode(texture:texture)
self.coverSprite.texture = sprite.texture
self.coverSprite.size = sprite.size
}
if value == 0.0 {
self.coverSprite.texture = SKTexture()
self.coverSprite.size = CGSize.zero
}
if value>0.0 && value<1.0 {
let calculateFractionForPosition = originalSize.width - (originalSize.width * value)
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFractionForPosition)/2,y:self.coverSprite.position.y)
}
}
}
}
Usage:
some texture just to make an example:
baseTxt.jpeg:
coverTxt.png:
Code:
self.energyProgressBar = SKProgressImageBar.init(baseImageName:"baseTxt.jpeg", coverImageName: "coverTxt.png", baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
//self.energyProgressBar = SKProgressImageBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
self.addChild(self.energyProgressBar)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
with colors:
with textures:
A simple class using two sprite nodes
class PBProgressBar: SKNode {
private var baseNode : SKSpriteNode!
private var progressNode : SKSpriteNode!
private var basePosition: CGPoint!
var progress: CGFloat!
init(progress: CGFloat = 0.45, position: CGPoint = CGPoint.zero) {
super.init()
self.progress = progress
self.basePosition = position
configureProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureProgress() {
baseNode = SKSpriteNode(color: .white, size: CGSize(width: 10, height: 100))
baseNode.anchorPoint = CGPoint.zero
let heightFraction = baseNode.size.height * progress
baseNode.position = basePosition
progressNode = SKSpriteNode(color: .blue, size: CGSize(width: 10, height: heightFraction))
progressNode.anchorPoint = CGPoint.zero
baseNode.addChild(progressNode)
self.addChild(baseNode)
}
}

Resources