I did a small test project using the "Hello World" Sprite-kit template where there is an atlas animation composed by these frames:
-
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")
}
}
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With