Skspritenode texture animation doesn't work - swift

I just tried to animate some coins in my scene. In my other projects the animation works. But recently not here.
func addCoins() {
for coins in map.coinSpawns {
var coin = SKSpriteNode(imageNamed: "coin1")
coin.position = coins
coin.size = CGSize(width:map.tileSize - 10,height: map.tileSize - 10)
let action = SKAction.repeatForever(SKAction.animate(withNormalTextures: [SKTexture(imageNamed: "coin1.png"),SKTexture(imageNamed: "coin2.png"),SKTexture(imageNamed: "coin3.png"),SKTexture(imageNamed: "coin4.png")], timePerFrame: 0.5, resize: false, restore: true))
self.addChild(coin)
coin.run(action)
self.coins.append(coin)
}
}

Try to refactor a bit your code, removing .png from the file name (should be the fix) and extracting the textures array outside the coin loop (optimization), so your code might be:
func addCoins() {
let textures = ["coin1", "coin2", "coin3", "coin4"].flatMap { SKTexture(imageNamed: $0) }
for coins in map.coinSpawns {
var coin = SKSpriteNode(imageNamed: "coin1")
coin.position = coins
coin.size = CGSize(width:map.tileSize - 10,height: map.tileSize - 10)
self.addChild(coin)
self.coins.append(coin)
let action = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.5, resize: true, restore: false))
coin.run(action)
}
}

Related

How can I animate a SKPhysicsBody texture to match an SKTextureAtlas

I implemented #mfessenden suggestion regarding using a custom SKAction to match the SKPhysicsBody to follow the animation using alphaThreshold. ( https://stackoverflow.com/a/54096147 ) But it does not appear to be functioning for me. Where have I gone wrong? Can you help #mfessenden?
var rockFrames: [SKTexture] = []
let rockAnimatedAtlas = SKTextureAtlas(named: "funny")
let numImages = rockAnimatedAtlas.textureNames.count
for i in 1...numImages {
let rockTextureName = "\(i)"
rockFrames.append(rockAnimatedAtlas.textureNamed(rockTextureName))
}
let firstFrameTexture = rockFrames[0]
rock = SKSpriteNode(texture: firstFrameTexture)
rock.position = CGPoint(x: self.frame.midX, y: self.frame.midY + 100)
rock.size = CGSize(width: 480 * wR, height: 270 * hR)
self.addChild(rock)
let rockaction = SKAction.animate(with: rockFrames,
timePerFrame: (1/24),
resize: false,
restore: true)
rock.run(SKAction.repeatForever(rockaction))
typealias Frame = (texture: SKTexture, duration: TimeInterval)
let timePerFrame: TimeInterval = 1/24
let dogFrames: [Frame] = rockFrames.map {
return ($0, timePerFrame)
}
rock = SKSpriteNode(texture: rockFrames.first)
let dogAnimationAction = animateTexturesWithPhysics(dogFrames)
rock.run(dogAnimationAction)
public func animateTexturesWithPhysics(_ frames: [(texture: SKTexture, duration: TimeInterval)], repeatForever: Bool=true) -> SKAction {
var actions: [SKAction] = []
for frame in frames {
// define a custom action for each frame
let customAction = SKAction.customAction(withDuration: frame.duration) { node, _ in
// if the action target is a sprite node, apply the texture & physics
if node is SKSpriteNode {
let setTextureGroup = SKAction.group([
SKAction.setTexture(frame.texture, resize: false),
SKAction.wait(forDuration: frame.duration),
SKAction.run {
node.physicsBody = SKPhysicsBody(texture: frame.texture, alphaThreshold: 0.5, size: frame.texture.size())
node.physicsBody?.isDynamic = false
node.physicsBody?.categoryBitMask = self.arrowCategory
// add physics attributes here
}
])
node.run(setTextureGroup)
}
}
actions.append(customAction)
}
// add the repeating action
if (repeatForever == true) {
return SKAction.repeatForever(SKAction.sequence(actions))
}
return SKAction.sequence(actions)
}

Sprite-Kit animations — using SKTextureFilteringMode.nearest

I'm trying to apply SKTextureFilteringMode.nearest to all the frames in my animation.
Previously, when I was using a non-animated sprite, the following worked:
super.init(texture: texture, color: .clear, size: playerSize)
self.texture?.filteringMode = SKTextureFilteringMode.nearest;
Now I've added animation frames (see full code below), this doesn't work — the sprite is blurry. I can't work out how to add this filtering mode to all frames.
class Player: SKSpriteNode {
private var playerAtlas: SKTextureAtlas {
return SKTextureAtlas(named: "Player")
}
private var playerRunTextures: [SKTexture] {
return [
playerAtlas.textureNamed("run1"),
playerAtlas.textureNamed("run2"),
playerAtlas.textureNamed("run3"),
playerAtlas.textureNamed("run4")
]
}
func startRunAnimation() {
let runAnimation = SKAction.animate(with: playerRunTextures, timePerFrame: 0.1)
self.run(SKAction.repeatForever(runAnimation), withKey: "playerRunAnimation")
}
init() {
let texture = SKTexture(imageNamed: "player")
let playerSize = CGSize(width: 30, height: 50)
super.init(texture: texture, color: .clear, size: playerSize)
self.texture?.filteringMode = SKTextureFilteringMode.nearest;
self.position = CGPoint(x: 100, y: 400)
self.startRunAnimation()
}
...
set SKTextureFilteringMode for each texture when you create the sprite animation array:
private var playerRunTextures: [SKTexture] {
let spritesheet = [
"run1",
"run2",
"run3",
"run4"
]
return spritesheet.map { name in
let t1 = playerAtlas.textureNamed(name)
t1.filteringMode = .nearest //<--- for each texture in the atlas
return t1
}
}

Swift / SpriteKit Collision producing varying results

I have a small test project (my first using Swift / XCode) which is designed to move me away from HTML5 and Canvas for game production.
The code compiles and runs fine. I use my iPhone as the test device rather than the built in simulator.
The symptoms of the problem are
that the lasers being repeatedly fired from the player's ship appear to occasionally bend around the aliens
the names being pulled out from the nodes are being shown as their default names not the names I assigned to them at creation
In some cases the collision works fine and the alien explosion is generated and the alien sprite node is removed from the scene.
I have named the alien nodes "alien" and the laser nodes "laser".
Both have their contactTestBitMask set to the same value.
Here is my GameScene.swift code:
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var lastUpdateTime: TimeInterval = 0
var delta: TimeInterval = 0
var sp_player: SKSpriteNode!
var stars: SKSpriteNode!
var deeperstars: SKSpriteNode!
var laser: SKSpriteNode!
var alien: SKSpriteNode!
var explosionSplat1: SKSpriteNode!
var playerScore: UInt32!
struct PhysicsCategory {
static let base:UInt32 = 0x1 << 0
static let alien:UInt32 = 0x1 << 1
static let laser:UInt32 = 0x1 << 2
static let player:UInt32 = 0x1 << 3
}
override func didMove(to view: SKView) { // called when the scene is presented into view (happens only once)
playerScore = 0
physicsWorld.contactDelegate = self
physicsWorld.gravity = .zero
// BACKGROUND
backgroundColor = UIColor(red: 0/255, green: 0/255, blue: 48/255, alpha: 1.0)
print("Background color is set")
// WRAP THE STARFIELDS
// Front most layer of stars
let starsTexture = SKTexture(imageNamed: "stars.png")
let bgAnimation = SKAction.move(by: CGVector(dx: 0, dy: -starsTexture.size().height), duration: 5)
let bgReset = SKAction.move(by: CGVector(dx: 0, dy: starsTexture.size().height), duration: 0)
let bgConstantMotion = SKAction.repeatForever(SKAction.sequence([bgAnimation,bgReset]))
// Back layer of slower stars
let deeperStarsTexture = SKTexture(imageNamed: "stars-deeper.png")
let deeperStarsbgAnimation = SKAction.move(by: CGVector(dx: 0, dy: -deeperStarsTexture.size().height), duration: 8)
let deeperStarsbgReset = SKAction.move(by: CGVector(dx: 0, dy: deeperStarsTexture.size().height), duration: 0)
let deeperStarsbgConstantMotion = SKAction.repeatForever(SKAction.sequence([deeperStarsbgAnimation,deeperStarsbgReset]))
var i: CGFloat = 0
while i < 3
{
stars = SKSpriteNode(texture: starsTexture)
stars.position = CGPoint(x: frame.midX, y: starsTexture.size().height * i)
stars.size.height = frame.height
stars.run(bgConstantMotion)
stars.zPosition = -1
addChild(stars)
deeperstars = SKSpriteNode(texture: deeperStarsTexture)
deeperstars.position = CGPoint(x: frame.midX, y: deeperStarsTexture.size().height * i)
deeperstars.size.height = frame.height
deeperstars.run(deeperStarsbgConstantMotion)
deeperstars.zPosition = -1
addChild(deeperstars)
i += 1
}
// PLAYER
let playerTexture1 = SKTexture(imageNamed: "player-1.png")
let playerTexture2 = SKTexture(imageNamed: "player-2.png")
let playerAnimation = SKAction.animate(with: [playerTexture1, playerTexture2], timePerFrame: 0.2)
let constantAnimation = SKAction.repeatForever(playerAnimation)
sp_player = SKSpriteNode(texture: playerTexture1)
sp_player.position = CGPoint(x: frame.midX, y: (sp_player.size.height * 2))
sp_player.physicsBody = SKPhysicsBody(rectangleOf: sp_player.size)
sp_player.physicsBody!.isDynamic = false
sp_player.name = "player"
sp_player.run(constantAnimation)
addChild(sp_player)
// PLACE ALIENS
let alienTexture1 = SKTexture(imageNamed: "alien-1a.png")
let alienTexture2 = SKTexture(imageNamed: "alien-1b.png")
let alienAnimation = SKAction.animate(with: [alienTexture1, alienTexture2], timePerFrame: 0.4)
let constantAlienAnimation = SKAction.repeatForever(alienAnimation)
var x: CGFloat = 0, y: CGFloat = 0
while y < 6
{
while x < 6
{
alien = SKSpriteNode(texture: alienTexture1)
alien.position = CGPoint(x: 32 + (x * alien.size.width), y: (frame.size.height - (alien.size.height * 1.5) - (alien.size.height * y)))
print("Setting y to \(frame.size.height - (alien.size.height * y))")
alien.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: alien.size.width, height: alien.size.height))
alien.physicsBody!.isDynamic = false
alien.name = "alien"
alien.physicsBody!.contactTestBitMask = PhysicsCategory.laser
alien.run(constantAlienAnimation)
addChild(alien)
x += 1
}
y += 1
x = 0
}
print("Sprites added to scene")
spawnLasers()
}
func spawnLasers()
{
let delay1 = SKAction.wait(forDuration: 0.5)
let spawn = SKAction.run {
let laserTexture = SKTexture(imageNamed: "laser-1.png")
self.laser = SKSpriteNode(texture: laserTexture)
self.laser.position = CGPoint(x: self.sp_player.position.x, y: self.sp_player.position.y + self.sp_player.size.height)
self.laser.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.laser.size.width, height: self.laser.size.height))
self.laser.physicsBody!.isDynamic = true
self.laser.physicsBody!.linearDamping = 0
self.laser.physicsBody!.allowsRotation = false
self.laser.physicsBody!.contactTestBitMask = PhysicsCategory.laser
self.laser.name = "laser"
self.addChild(self.laser)
let shoot = SKAction.moveTo(y: self.frame.size.height, duration: 1)
let killLaser = SKAction.removeFromParent()
let handleLaser = SKAction.sequence([shoot,killLaser])
self.laser.run(handleLaser)
}
let action = SKAction.sequence([delay1,spawn])
let constantLasers = SKAction.repeatForever(action)
self.run(constantLasers)
}
func didBegin(_ contact: SKPhysicsContact) {
var check: UInt32 = 0
if contact.bodyA.node != nil
{
check += 1
}
if contact.bodyB.node != nil
{
check += 1
}
if check == 2
{
if contact.bodyA.node!.name == "alien" && contact.bodyB.node!.name == "laser"
{
// EXPLOSION
let explosionSplatTexture1 = SKTexture(imageNamed: "explosion-1a.png")
let explosionSplatTexture2 = SKTexture(imageNamed: "explosion-1b.png")
let explosionSplatTexture3 = SKTexture(imageNamed: "explosion-1c.png")
let explosionSplatTexture4 = SKTexture(imageNamed: "explosion-1d.png")
let explosionSplatAnimation = SKAction.animate(with: [explosionSplatTexture1, explosionSplatTexture2, explosionSplatTexture3, explosionSplatTexture4], timePerFrame: 0.1)
let killExplosion = SKAction.removeFromParent()
let explosionSequence = SKAction.sequence([explosionSplatAnimation,killExplosion])
explosionSplat1 = SKSpriteNode(texture: explosionSplatTexture1)
explosionSplat1.name = "explosion"
explosionSplat1.position = CGPoint(x: contact.bodyA.node!.position.x, y: contact.bodyA.node!.position.y)
addChild(explosionSplat1)
explosionSplat1.run(explosionSequence)
self.playerScore += 1
print("Score: \(self.playerScore!)")
contact.bodyA.node?.removeFromParent()
print("Alien named \(contact.bodyA.node?.name ?? "defaultAlienName") from scene")
contact.bodyB.node?.removeFromParent()
print("Laser named \(contact.bodyB.node?.name ?? "defaultLaserName") from scene")
}
}
}
func didEnd(_ contact: SKPhysicsContact) {
//print("Contact ended between \(contact.bodyA) and \(contact.bodyB)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
// if let touch = touches.first {
// let position = touch.location(in: view)
// storedTouch = position
// }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
if let touch = touches.first {
let position = touch.location(in: view)
var playerpos: CGPoint!
playerpos = sp_player.position
let pl_move = SKAction.move(to: CGPoint(x: position.x, y: playerpos.y), duration: 0.1)
sp_player.run(pl_move)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
/*
Need to figure out how to use storedTouch properly
to move player relative to the screen touch co-ordinates
*/
// if let touch = touches.first {
// let position = touch.location(in: view)
// }
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if (lastUpdateTime > 0)
{
delta = currentTime - lastUpdateTime
} else {
delta = 0
}
lastUpdateTime = currentTime
}
}
When the game runs it produces this screen:
You can see the unexpected laser behavior here:
In my diags I get the following output from the collision function:
Score: 1
Alien named defaultAlienName removed from scene
Laser named laser removed from scene
Score: 2
Alien named defaultAlienName removed from scene
Laser named laser removed from scene
Score: 3
Alien named defaultAlienName removed from scene
Laser named laser removed from scene
Score: 4
Alien named defaultAlienName removed from scene
Laser named defaultLaserName removed from scene
Score: 5
Alien named defaultAlienName removed from scene
Laser named defaultLaserName removed from scene
Score: 6
Alien named defaultAlienName removed from scene
Laser named defaultLaserName removed from scene
This is most likely my complete lack of understanding for optionals and how collision actually works. I'd be super grateful for any insights.
In your alien loop, as well as spawnLasers(), you are not giving the sprite nodes an actual PhysicsBody category. For sprites to be able to detect contact between one another, they need a category name and a contact name.
So in your while loop (building the aliens), you need to have this:
alien.physicsBody!.categoryBitMask = PhysicsCategory.alien
alien.physicsBody!.contactTestBitMask = PhysicsCategory.laser
And in spawnLasers(), you want this added:
self.laser.physicsBody!.categoryBitMask = PhysicsCategory.laser
and change the contactTestBitMask to alien, not laser:
self.laser.physicsBody!.contactTestBitMask = PhysicsCategory.alien
Hopefully you can see that the alien category wants to know when lasers touch, and the laser category wants to know when the aliens touch.
To visually help you, turn on the show physics option, this way you can see the actual physics bodies you are dealing with.
To do this, in your GameViewController (or similar), find:
showsFPS = true
showsNodeCount = true
You want to add the following:
showsPhysics = true
This will help with seeing the actual physics bodies on screen.
In:
func didBegin(_ contact: SKPhysicsContact)
you are only testing for BodyA being alien and BodyB being laser.
This is the contact test:
if contact.bodyA.node!.name == "alien" && contact.bodyB.node!.name == "laser"
I believe BodyA could be laser and BodyB be alien. Basically the physics engine contact events could be "A hitting B", or "B hitting A".
Therefore, a quick and dirty solution is to create another if statement below the current one, but changing the body names, so:
if contact.bodyA.node!.name == "laser" && contact.bodyB.node!.name == "alien" {
and duplicate the code from your existing if statement, and changing the two print statements.
This isn't the ideal way to do it, but hopefully when you tidy it up you'll get an understanding of what the physics contact is doing.
I am hoping once you have implmented the above, you will be in a much better shape.

Animation atlas and dynamic physicsBody in Swift 3

I did a small test project using the "Hello World" Sprite-kit template where there is an atlas animation composed by these frames:
-
I want to show this knight and it's animation.
I want to set a DYNAMIC physics body.
So I've used a tool to separated single frames and I did an atlasc folder
so the code should be:
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
self.setPhysics()
let animation = SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false)
knight.run(animation, withKey:"knight")
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
}
The output is:
As you can see, the physicsBody is STATIC, don't respect the animation: this is normal because during the animation the texture change dimension / size and we don't change the physicsBody that remain the same during the action.
Following the sources there aren't methods that , during SKAction.animate, allow to change the physicsBody.
Although we use :
/**
Creates an compound body that is the union of the bodies used to create it.
*/
public /*not inherited*/ init(bodies: [SKPhysicsBody])
to create bodies for each frame of our animation, these bodies remain all together in the scene creating an ugly bizarre situation like this pic:
So, the correct way to do it should be to intercept frames during animation and change physicsBody on the fly.
We can use also the update() method from SKScene, but I was thinking about an extension.
My idea is to combine the animation action with a SKAction.group, making another custom action that check the execution of the first action, intercept frames that match the current knight.texture with the textures array and change the physicsBody launching an external method, in this case setPhysicsBody.
Then, I've write this one:
extension SKAction {
class func animateWithDynamicPhysicsBody(animate:SKAction, key:String, textures:[SKTexture], duration: TimeInterval, launchMethod: #escaping ()->()) ->SKAction {
let interceptor = SKAction.customAction(withDuration: duration) { node, _ in
if node is SKSpriteNode {
let n = node as! SKSpriteNode
guard n.action(forKey: key) != nil else { return }
if textures.contains(n.texture!) {
let frameNum = textures.index(of: n.texture!)
print("frame number: \(frameNum)")
// Launch a method to change physicBody or do other things with frameNum
launchMethod()
}
}
}
return SKAction.group([animate,interceptor])
}
}
Adding this extension, we change the animation part of the code with:
//
self.setPhysics()
let animation = SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false)
let interceptor = SKAction.animateWithDynamicPhysicsBody(animate: animation, key: "knight", textures: textures, duration: 60.0, launchMethod: self.setPhysics)
knight.run(interceptor,withKey:"knight")
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
This finally works, the output is:
Do you know a better way, or a more elegant method to obtain this result?
Like I mentioned in the comments, since you are doing boxed physics, add a child SKSpriteNode to your knight that will handle the contacts part of the physics, and just scale based on the knight's frame:
(Note: this is for demo purposes only, I am sure you can come up with a more elegant way to handle this across multiple sprites)
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
private var child = SKSpriteNode(color:.clear,size:CGSize(width:1,height:1))
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
self.setPhysics()
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
child.xScale = knight.frame.size.width
child.yScale = knight.frame.size.height
}
func setPhysics() {
child.physicsBody = SKPhysicsBody.init(rectangleOf: child.size)
child.physicsBody?.isDynamic = false
knight.addChild(child)
}
}
To handle texture based bodies. I would write a custom animation to handle it:
extension SKAction
{
static func animate(withPhysicsTextures textures:[(texture:SKTexture,body:SKPhysicsBody)], timePerFrame:TimeInterval ,resize:Bool, restore:Bool) ->SKAction {
var originalTexture : SKTexture!;
let duration = timePerFrame * Double(textures.count);
return SKAction.customAction(withDuration: duration)
{
node,elapsedTime in
guard let sprNode = node as? SKSpriteNode
else
{
assert(false,"animatePhysicsWithTextures only works on members of SKSpriteNode");
return;
}
let index = Int((elapsedTime / CGFloat(duration)) * CGFloat(textures.count))
//If we havent assigned this yet, lets assign it now
if originalTexture == nil
{
originalTexture = sprNode.texture;
}
if(index < textures.count)
{
sprNode.texture = textures[index].texture
sprNode.physicsBody = textures[index].body
}
else if(restore)
{
sprNode.texture = originalTexture;
}
if(resize)
{
sprNode.size = sprNode.texture!.size();
}
}
}
}
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures = [texture:SKTexture,body:SKPhysicsBody]()
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
let texture = genericAtlas.textureNamed(textureName)
let body = SKPhysicsBody(texture:texture)
body.isDynamic = false
textures.append((texture:texture,body:body))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first.texture)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
let animation = SKAction.animate(withPhysicsTextures: textures, timePerFrame: 0.15, resize: true, restore: false)
knight.run(animation, withKey:"knight")
}
}
Solution for isDynamic = false.
*Update to 2017 below
After days of test maded with my answer, Knight0fDragon answer's and some other ideas came from other SO answers (Confused and Whirlwind suggestions..) I've seen that there is a new problem : physicsBody can't propagate their properties to other bodies
adequately and correctly. In other words copy all properties from a body to another body it's not enough. That's because Apple restrict the access to some methods and properties of the physicsBody original class.
It may happen that when you launch a physicsBody.applyImpulse propagating adequately the velocity, the gravity isn't yet respected correctly. That's really orrible to see..and obviusly that's wrong.
So the main goal is: do not change the physicBody recreating it.In other words DON'T RECREATE IT!
I thought that, instead of creating sprite children, you could create a ghost sprites that do the work instead of the main sprite, and the main sprite takes advantage of the ghost changes but ONLY the main sprite have a physicsBody.
This seems to work!
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
private var ghostKnight:SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightTexture : SKTexture!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare the ghost
ghostKnight = SKSpriteNode(texture:textures.first)
addChild(ghostKnight)
ghostKnight.alpha = 0.2
ghostKnight.position = CGPoint(x:self.frame.midX,y:100)
lastKnightTexture = ghostKnight.texture
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
let ghostAnimation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
ghostKnight.run(ghostAnimation,withKey:"ghostAnimation")
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
if ghostKnight.action(forKey: "ghostAnimation") != nil {
if ghostKnight.texture != lastKnightTexture {
setPhysics()
lastKnightTexture = ghostKnight.texture
}
}
}
func setPhysics() {
if let _ = knight.physicsBody{
knight.xScale = ghostKnight.frame.size.width
knight.yScale = ghostKnight.frame.size.height
} else {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
}
Output:
Obviusly you can hide with alpha set to 0.0 and re-positioning the ghost as you wish to make it disappear.
Update 2017:
After hours of testing I've try to improve the code, finally I managed to remove the ghost sprite but, to work well, one condition is very important: you should not use SKAction.animate with resize in true. This because this method resize the sprites and don't respect the scale (I really don't understand why, hope to some future Apple improvements..). This is the best I've obtain for now:
NO CHILDS
NO OTHER GHOST SPRITES
NO EXTENSION
NO ANIMATE METHOD RECREATED
Code:
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightSize: CGSize!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
lastKnightSize = knight.texture?.size()
setPhysics()
}
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
lastKnightSize = knight.texture?.size()
knight.xScale = lastKnightSize.width
knight.yScale = lastKnightSize.height
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
Important detail:
About isDynamic = true that's not possible simply because , during the frequently changes of size, Apple reset also frequently the knight physicsBody but don't apply the inherit of the latest physicsBody properties to the new resetted physicsBody, this is a real shame, you can test it in update printing the knight.physicsBody?.velocity (is always zero but should change due to gravity...). This is probably the reason why Apple recommended to don't scale sprites during physics. To my point of view is a Sprite-kit limitation.
Another idea could be the suggestion about didEvaluateActions() to search a more general method to have a "variable" physicsBody that follow the real current knight texture OR physics settings as a rectangle body like this case:
Update: (thanks to Knight0fDragon and 0x141E interventions)
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightTexture : SKTexture!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
lastKnightTexture = knight.texture
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
if knight.action(forKey: "knight") != nil {
if knight.texture != lastKnightTexture {
setPhysics()
lastKnightTexture = knight.texture
}
}
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
}
I am getting ready to dive into some texture physics bodies and ran into this problem a while back.
After reading this I am thinking of an approach like so:
When animating, why not have a base node (center node), and then children that are are each the frames of animation. Then patterns of hiding and unhiding the child nodes, where each child node (which I will be doing) is using the alpha of the texture (all loaded) for their PBs. But just HIDE / UNHIDE (my answer).
I mean how much of a penalty would we incure having 12 nodes per se, of a base node that are the animation frames, versus loading and unloading images for animation.
Any penalty in my mind would be well worth being able to have the variety of various physics bodies.

SpriteNode animation is failing

I have been trying to have a sprite with more than one texture so that it looks as if it was moving. I was looking at other questions and nothing they have done has fixed my issue, this is my code:
var textures = [SKTexture]()
for x in 0...2 {
let texture = SKTexture(imageNamed: "peng_" + String(x))
textures.append(texture)
print("pen_" + String(x))
}
let pen = SKSpriteNode(imageNamed: "peng_0")
self.addChild(pen)
let action = SKAction.animate(withNormalTextures: textures, timePerFrame: 0.1, resize: false, restore: true)
pen.run(SKAction.repeatForever(action))
When I run the simulation, the sprite stays still with the "peng_0" texture, but I want it to iterate the other textures every 0.1s
Does anyone know what am I doing wrong, thanks.
Here you go set your code like this
var array = ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8"]
var textures:[SKTexture] = []
for i in 0 ..< array.count{
let texture: SKTexture = SKTexture(imageNamed: array[i])
textures.insert(texture, at:i)
}
let pen = SKSpriteNode(imageNamed: "R1")
self.addChild(pen)
let animation = SKAction.animate(with: textures, timePerFrame: 8/60, resize: true , restore:false )
pen.run = SKAction.repeatForever(animation)