Walking (Not Talking)

Part One.

The movement animation, for the instances of Denizen which roam the beach, should start and stop seamlessly, in response both to changes in the instance's internal instructions and also to the player's interactions.

When trying to devise a best practices approach, I tried to cull as much as possible, or as was appropriate, from Apple's Adventure, which handles its characters' movements with a well-organized, if not somewhat complex1, collection of methods. When a sprite is told a new animation is requested, an array filled with the corresponding SKTextures is passed to the fireAnimationForState function.

While the complexity of Adventure requires safety and interchangeability far beyond the scope of this game, I think that the basic implementation of the animation states could be adapted for Denizen's and/or ScenerySprite's.


init()

SKSpriteNode's designated initializer asks for texture, color, and size 2. Consequently, one cannot call any of SKSpriteNode's convenience initializers, of which there are several, from within a subclass' own convenience initializer; simply calling super.init(color: myColor, size: mySize) will throw an error.

That call to the superclass' initializer is actually calling:

/**
  Initialize a sprite with a color and the specified bounds.
  @param color the color to use for tinting the sprite.
  @param size the size of the sprite in points
*/
  convenience init(color: UIColor!, size: CGSize)

One could override SKSpriteNode's designated initializer in the subclass3, but in Adventure, Apple uses a convenience initializer in their main subclass4 of SKSpriteNode which includes a call to self.init. Adapting that for the Denizen's means stripping away a lot of the pattern matching and struct loading that Adventure uses. Doing so yields a pretty straight-forward class setup.

class BeachSprite: SKSpriteNode {  
  convenience init () {
    self.init(texture: nil, color: SKColor.whiteColor(), size: CGSize(width: 0, height: 0))
  }
}

All further subclasses may begin to implement varied convenience initializers without overriding the superclass' designated init().

Subclasses

The subclasses of BeachSprite will be of two varieties, those which can move (either in some randomized pattern or along a pre-determined path), and those which cannot. The simplest scheme to adopt would seem to be separating StationarySprite's and MovingSprite's.

While the implementation of the animation handling won't be overly complex, this differentiation at least keeps it only in the classes that need it.

class MovingSprite: BeachSprite {  
  convenience init(texture: SKTexture?, atPosition position: CGPoint) {
    let size = texture != nil ? texture!.size() : CGSize(width: 0, height: 0)
    self.init(texture: texture, color: SKColor.whiteColor(), size: size)
  }
}

MovingSprite's will have array's of SKTexture's, these will be composed of the individual frames of the animation. when a DenizenSprite, which will subclass MovingSprite, is asked to animate for its current state, be it moving or idle, the corresponding array will be passed to our version of the method fireAnimationForState.

final class DenizenSprite: MobileSprite {  
  convenience init(atPosition position: CGPoint) {
    let atlas = SKTextureAtlas(named: "Denizen_Idle")
    let texture  = atlas.textureNamed("Idle_Frame_001")
    self.init(texture: texture, atPosition: position)
  }
}

And here we adopt the same ideas that are used in Apple's Warrior and Archer classes, which is to say we load the first frame of idleAnimationFrames and initialize with that texture.

Part Two: Animations, Atlases, Loading.


  1. I had quite a few symbol look-up's to follow the thread from one class to another. There is a Code Explained: Adventure, but it has not been updated since the pre-Swift days. Still, it is extremely useful and enlightening.

  2. An SKTexture, UIColor, and CGSize respectively.

  3. As in this answer from Stack Overflow.

  4. class ParallaxSprite: SKSpriteNode

  5. Nil Coalescing Operator: a != nil ? a! : b. If a does not equal nil, then return a unwrapped, otherwise, return b which, because it has to be the same type as a, should always work.