Hice un pequeño proyecto de prueba usando la plantilla del kit de Sprite "Hello World" donde hay una animación de atlas compuesta por estos cuadros: ingrese la descripción de la imagen aquí

-

Quiero mostrar a este caballero y su animación .

Quiero establecer un cuerpo de física DINÁMICA.

Así que usé una herramienta para separar cuadros individuales e hice una carpeta atlasc entonces el código debería ser:

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
    }
}

El resultado es:

enter image description here

Como puede ver, el physicsBody es ESTÁTICO, no respete la animación : esto es normal porque durante la animación la textura cambia la dimensión / tamaño y no cambiamos el {{ X1}} que permanecen igual durante la acción.

Siguiendo las fuentes no hay métodos que, durante SKAction.animate, permitan cambiar el physicsBody.

Aunque usamos:

/**
Creates an compound body that is the union of the bodies used to create it.
*/
public /*not inherited*/ init(bodies: [SKPhysicsBody])

Para crear cuerpos para cada cuadro de nuestra animación, estos cuerpos permanecen todos juntos en la escena creando una situación extraña y fea como esta foto:

enter image description here


Entonces, la forma correcta de hacerlo debería ser interceptar fotogramas durante la animación y cambiar physicsBody sobre la marcha. También podemos usar el método update() de SKScene, pero estaba pensando en una extensión.

Mi idea es combinar la acción animation con un SKAction.group, haciendo otra acción personalizada que verifique la ejecución de la primera acción, interceptar marcos que coincidan con el knight.texture actual con el {{X3} } matriz y cambie physicsBody iniciando un método externo, en este caso setPhysicsBody.

Entonces, he escrito este:

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])
    }
}

Al agregar esta extensión, cambiamos la parte de animación del código con:

 //
        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
    }

Esto finalmente funciona, el resultado es:

enter image description here

¿Conoces una forma mejor o un método más elegante para obtener este resultado?

9
Alessandro Ornano 23 dic. 2016 a las 00:51

3 respuestas

La mejor respuesta

Solución para isDynamic = false.

* Actualización a 2017 a continuación

Después de días de prueba combinados con mi respuesta, las respuestas de Knight0fDragon y algunas otras ideas vinieron de otras respuestas SO (sugerencias confusas y torbellinas ...) He visto que hay un nuevo problema : {{X0} } no puede propagar sus propiedades a otros cuerpos adecuada y correctamente En otras palabras, copie todas las propiedades de un cuerpo a otro cuerpo no es suficiente . Esto se debe a que Apple restringe el acceso a algunos métodos y propiedades de la clase original physicsBody. Puede suceder que cuando ejecutas un physicsBody.applyImpulse propagando adecuadamente el velocity, la gravedad aún no se respeta correctamente. Eso es realmente horrible de ver ... y obviamente eso está mal.

Entonces, el objetivo principal es: no cambiar el physicBody recreándolo. En otras palabras, ¡NO LO RECRETE !

Pensé que, en lugar de crear hijos de sprites, podría crear un sprites fantasma que haga el trabajo en lugar del sprite principal, y el sprite principal aprovecha los cambios de fantasmas, pero SOLO el sprite principal tiene un cuerpo físico.

Esto parece funcionar!

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
        }
    }
}

Salida :

enter image description here

Obviamente, puede esconderse con alpha establecido en 0.0 y volver a colocar el fantasma como desee para que desaparezca.


Actualización 2017 :

Después de horas de pruebas, intenté mejorar el código, finalmente logré eliminar el sprite fantasma pero, para que funcione bien, una condición es muy importante: no debe usar SKAction.animate con resize en verdadero . Esto se debe a que este método redimensiona los sprites y no respeta la escala (realmente no entiendo por qué, espero algunas mejoras futuras de Apple ...). Esto es lo mejor que he obtenido por ahora:

  • Sin hijos
  • NO OTROS SPRITES FANTASMA
  • SIN EXTENSIÓN
  • NINGÚN MÉTODO ANIMADO RECREADO

Código:

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
    }
}

Detalles importantes :

Acerca de isDynamic = true eso no es posible simplemente porque, durante los cambios frecuentes de tamaño, Apple restablece también frecuentemente el cuerpo físico del caballero, pero no aplica la herencia de las últimas propiedades physicsBody a las nuevas propiedades restablecidas {{X2} }, es una verdadera lástima, puedes probarlo en la actualización imprimiendo el knight.physicsBody?.velocity (siempre es cero pero debería cambiar debido a la gravedad ...). Esta es probablemente la razón por la cual Apple recomendó no escalar sprites durante la física. En mi opinión, hay una limitación de Sprite-kit .

3
Alessandro Ornano 22 ene. 2017 a las 10:00

Como mencioné en los comentarios, ya que está haciendo física en recuadro, agregue un niño SKSpriteNode a su caballero que manejará la parte de los contactos de la física, y solo escale según el marco del caballero:

(Nota: esto es solo para fines de demostración, estoy seguro de que puede encontrar una forma más elegante de manejar esto en varios 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)
    }
}

Para manejar cuerpos basados en texturas. Escribiría una animación personalizada para manejarlo:

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")
    }
}
3
Knight0fDragon 25 dic. 2016 a las 14:51

Otra idea podría ser la sugerencia sobre didEvaluateActions() para buscar un método más general para tener una "variable" physicsBody que siga la textura real actual de caballero O configuración de física como un cuerpo rectangular como este caso:

Actualización : (gracias a las intervenciones Knight0fDragon y 0x141E)

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
    }
}
2
Alessandro Ornano 10 ene. 2017 a las 15:27