Wednesday, August 8, 2018

CloudKit Games: Using CloudKit to Load Game Assets

Note: For this tutorial, some initial setup must be done, for which we’ve provided an overview here.  CloudKit must be enabled in the Capabilities tab and recordTypes for TextureAnimationABCCharacterABC, and PhysicsConfiguration with specific properties must be defined via your CloudKit dashboard.  TextureAnimationABC basically contains a sprite along with other information for configuring character animations.  PhysicsConfiguration contains information specific to a character for configuring a character’s physics properties.  And CharacterABC contains basic information, including a default sprite image, for configuring a static character without animations.  In addition, game assets and configuration information must be uploaded by creating records for the different record types.  For future updates, I will provide more information and guidance in this regard.
In this tutorial, we will explore using CloudKit to dynamically load game assets, such as characters and collectibles, much like you would using on-demand resources and app thinning.  If you want to follow along, you can download or clone the source code from here.  The project here is the foundation for a simple shooter game that I’ve been developing, Critter Crosshair Defense, in which users shoot evasive enemies that use different tactics to avoid getting targeted by a crosshair controlled by the user.  Using CloudKit has the advantage of letting users subscribe to receive notifications each time a new enemy character is created.   For the demo project, I will use game assets from Kenney, and the data models will be relatively simple and straightforward.  Here are some screenshots showing the final result – a series of characters, including their transform properties, physics bodies, and animations, are loaded dynamically at random points on screen.



To begin with, I will define a protocol called AnimationsGenerator, which will be adopted by several helper structs, each of which contains different state information but also has the ability to generate an animation of type SKAction (you will have to import SpriteKit in order to have access to this data type).
 
protocol AnimationGenerator{
    
    var animationName: String { get set }
    
    func createAnimation() -> SKAction?
    
}

After defining this protocol, I will implement several helper structs (i.e. TextureAnimationsGeneratorScaleAnimationsGeneratorRotateAnimationsGenerator, and MoveAnimationsGenerator).  Each stores different kinds of variables and implements different kinds of helper functions in order to implement the protocol method createAnimation:
TextureAnimationsGenerator
struct TextureAnimationGenerator: AnimationGenerator{
    var animationName: String
    
    var hasSetTextureAnimation: Bool{
        
        return textures.count == 1
        
    }
    
    var hasAnimateWithTexturesAnimation: Bool{
        return textures.count > 1
    }
    
    var timePerFrame: Double
    var textures: [SKTexture]
    
    func createAnimation() -> SKAction? {
        
        if(textures.isEmpty){
            return nil
        }
        
        if(hasSetTextureAnimation){
            return SKAction.setTexture(textures.first!)
        } else {
            return SKAction.animate(with: self.textures, timePerFrame: self.timePerFrame)

        }
    }
    
    
    
}

ScaleAnimationsGenerator
struct ScaleAnimationGenerator: AnimationGenerator{
    var animationName: String
    
    var hasScaleToAnimation: Bool{
        return xScaleTo != nil || yScaleTo != nil
    }
    
    var hasScaleByAnimation: Bool{
        return xScaleBy != nil || yScaleBy != nil
    }
    
    var xScaleTo: Double?
    var yScaleTo: Double?
    
    var xScaleBy: Double?
    var yScaleBy: Double?
    
    var duration: Double
    
    
    func createAnimation() -> SKAction? {
        
        if(hasScaleToAnimation && hasScaleByAnimation){
            return createCombinedAnimation()
        } else if hasScaleByAnimation{
            return createScaleByAnimation()
        } else if hasScaleToAnimation{
            return createScaleToAnimation()
        } else {
            return nil
        }
    }
    
    private func createCombinedAnimation() -> SKAction{
        
        return SKAction.group([
            self.createScaleByAnimation(),
            self.createScaleToAnimation()
            ])
        
    }
    
    private func createScaleByAnimation() -> SKAction{
        
        switch (self.xScaleBy,self.yScaleBy) {
        case (.some,.some):
            return SKAction.scaleX(by: CGFloat(self.xScaleBy!), y: CGFloat(self.yScaleBy!), duration: self.duration)
        case (.some,.none):
            return SKAction.scaleX(by: CGFloat(self.yScaleBy!), y: 0.00, duration: self.duration)
        case (.none,.some):
            return SKAction.scaleX(by: 0.00, y: CGFloat(self.yScaleBy!), duration: self.duration)
        default:
            return SKAction()
        }
        
    }
    
    private func createScaleToAnimation() -> SKAction{
        
        switch (self.xScaleTo,self.yScaleTo) {
        case (.some,.some):
            return SKAction.scaleX(to: CGFloat(self.xScaleTo!), y: CGFloat(self.yScaleTo!), duration: self.duration)
        case (.some,.none):
            return SKAction.scaleX(to: CGFloat(self.xScaleTo!), y: 0.00, duration: self.duration)
        case (.none,.some):
            return SKAction.scaleX(to: 0.00, y: CGFloat(self.yScaleTo!), duration: self.duration)
        default:
            return SKAction()
        }
        
    }
    
    
    
    
}


RotateAnimationsGenerator
struct RotateAnimationGenerator: AnimationGenerator{
    var animationName: String
    
    var hasRotateToAnimation: Bool{
        return rotateTo != nil
    }
    
    var hasRotateByAnimation: Bool{
        return rotateBy != nil
    }
    
    var rotateTo: Double?
    var rotateBy: Double?
    var shortestUnitArc: Bool = true

    var duration: Double
    
    func createAnimation() -> SKAction? {
        
        switch (self.rotateBy,self.rotateTo) {
        case (.some,.some):
            return createCombinedAnimation()
        case (.some,.none):
            return createRotateByAnimation()
        case (.none,.some):
            return createRotateToAnimation()
        default:
            return nil
        }
    }
    
    
    private func createCombinedAnimation() -> SKAction{
        
        return SKAction.group([
            self.createRotateByAnimation(),
            self.createRotateToAnimation()
            ])
        
    }
    
    private func createRotateByAnimation() -> SKAction{
        return SKAction.rotate(byAngle: CGFloat(self.rotateBy!), duration: self.duration)
       
    }
    
    private func createRotateToAnimation() -> SKAction{
        return SKAction.rotate(toAngle: CGFloat(self.rotateTo!), duration: self.duration, shortestUnitArc: self.shortestUnitArc)
        
    }
    
    
    
}


MoveAnimationsGenerator
struct MoveAnimationGenerator: AnimationGenerator{
    
    var animationName: String
    
    var hasMoveToAnimation: Bool{
        return xMoveTo != nil || yMoveTo != nil
    }
    
    var hasMoveByAnimation: Bool{
        return xMoveBy != nil || yMoveBy != nil
    }
    
    var xMoveTo: Double?
    var yMoveTo: Double?

    var xMoveBy: Double?
    var yMoveBy: Double?
    
    var duration: Double
    
    func createAnimation() -> SKAction? {
        
        if(hasMoveToAnimation && hasMoveByAnimation){
            return createCombinedAnimation()
        } else if hasMoveByAnimation{
            return createMoveByAnimation()
        } else if hasMoveToAnimation{
            return createMoveToAnimation()
        } else {
            return nil
        }
        
        
    
    }
    
    private func createCombinedAnimation() -> SKAction{
        
       return SKAction.group([
            self.createMoveByAnimation(),
            self.createMoveToAnimation()
        ])
    }
    
    private func createMoveByAnimation() -> SKAction{
        
        switch (self.xMoveBy,self.yMoveBy) {
        case (.some,.some):
            return SKAction.moveBy(x: CGFloat(self.xMoveBy!), y: CGFloat(self.yMoveBy!), duration: self.duration)
        case (.some,.none):
            return SKAction.moveBy(x: CGFloat(self.xMoveBy!), y: 0.00, duration: self.duration)
        case (.none,.some):
            return SKAction.moveBy(x: 0.00, y: CGFloat(self.yMoveBy!), duration: self.duration)
        default:
            return SKAction()
        }
    }
    
    
    func createMoveToAnimation() -> SKAction{
        
        switch (self.xMoveTo,self.yMoveTo) {
        case (.some,.some):
            return SKAction.move(to: CGPoint(x: CGFloat(self.xMoveTo!), y: CGFloat(self.yMoveTo!)), duration: self.duration)
        case (.some,.none):
            return SKAction.moveTo(x: CGFloat(self.xMoveTo!), duration: self.duration)
        case (.none,.some):
            return SKAction.moveTo(y: CGFloat(self.yMoveTo!), duration: self.duration)
        default:
            return SKAction()
        }
        
  }
    


All of these helper structs have optional properties that are used to configure the parameters of the animation that they are intended to generate, and different kinds of actions are generated based on how values are assigned to the different optional properties. They also implement computed boolean properties to distinguish between different kinds of animations based on which parameters are assigned a value and which are left nil.
Since all of these generator classes conform to the AnimationsGeneratorprotocol, instances created by any one of these structs can be passed into a function that we will implement for a CharacterABC class, which will become the class that will eventually represent the game character whose physics properties, animations, and transform properties will be dynamically loaded from CloudKit:
class Character{
    
  
     let kMoveAnimation = "moveAnimation"
     let kRotateAnimation = "rotateAnimation"
     let kScaleAnimation = "scaleAnimation"
     let kTextureAnimation = "textureAnimation"

 var node: SKSpriteNode
    
    var name: String{
        
        return node.name!
        
    }
    
    private var animationsDict = [String: SKAction]()
 
    init(name: String, anchorPoint: CGPoint, zPosition: CGFloat, xScale: CGFloat, yScale: CGFloat){
        
        
        node = SKSpriteNode()
        
        node.name = name
        node.xScale = xScale
        node.yScale = yScale
        node.anchorPoint = anchorPoint
        node.zPosition = zPosition
        
    }

 func configureAnimations(with animationGenerator: AnimationGenerator){
        
        if animationGenerator is MoveAnimationGenerator{
            
            self.animationsDict[kMoveAnimation] = animationGenerator.createAnimation()
        }
        
        if animationGenerator is RotateAnimationGenerator{
            
            self.animationsDict[kRotateAnimation] = animationGenerator.createAnimation()
            
        }
        
        if animationGenerator is ScaleAnimationGenerator{
            
            self.animationsDict[kScaleAnimation] = animationGenerator.createAnimation()
            
        }
        
        if animationGenerator is TextureAnimationGenerator{
            
            self.animationsDict[kTextureAnimation] = animationGenerator.createAnimation()
            
            
        }
        
    }
    
}

As you will see, the CharacterABC class has constants for the dictionary keys that are used to access the different animations that it stores in a private dictionary. The instance method configureAnimations(with🙂 takes an AnimationsGenerator as an argument and uses it to generate the action that will be mapped to a specific string key in the the animationsDict.
Now we will create a CharacterManager class that will be responsible for loading all of our character data (including textures, physics properties, and animations) from CloudKit.   For this CharacterManager class, we will define a protocol CharacterManagerDelegate whose callbacks can be used to notify a delegate when different components and properties of a character have been loaded:
CharacterMangerDelegate.h
protocol CharacterManagerDelegate: class {
    
    
    func didDownloadAllCharacterImageSprites()
    
    func didConfigurePhysicsFor(_ character: Character)
    
    func didConfigureAnimationFor(_ character: Character)
    
}
Basically, each of these methods are called at different points in the overall process by which game character information is downloaded from CloudKit and used to instantiate different game characters, each of which in turn is stored in a dictionary, characterDict (a private stored property in our CharacterManager class) in which the name of each character maps to the specific character instance. The didDownloadAllCharacterImageSprites callback is called when all of the sprite images for all of the characters have been completely downloaded.  The didConfigurePhysicsFor(character:) callback is called each time physics configuration information is downloaded from CloudKit for a given character.   The didConfigureAnimationsFor(character:) method is called each time the animation for a given character is fully downloaded and configured for a character.    This order has not been optimized for performance – we decided to downloaded all of the image sprites for all of the character textures (including default and animation textures) together in one fell swoop.    This way the image data is available for configuring all of the characters even if the network connection fails prior to getting the physics properties, in which case default physics properties could be provided.   Downloading image data on a character-by-character basis is also possible, and we’ve not optimized the different orders in which character data could be loaded.
In order to make use of CloudKit, we will define a wrapper singleton for our CKContainer and CKDatabase objects:
import Foundation
import CloudKit


class CKHelper{
    
    static let sharedHelper = CKHelper()

    let container: CKContainer = CKContainer.default()
    
    var publicDB: CKDatabase
    var privateDB: CKDatabase
    var sharedDB: CKDatabase
    
    
    private init(){
        publicDB = container.publicCloudDatabase
        privateDB = container.privateCloudDatabase
        sharedDB = container.sharedCloudDatabase
    }
    
}

Now, we will go ahead and define our CharacterManager class, which will hold a stored reference to the public database available via the CKHelpersingleton.  In addition, we we will define private properties for characterDict, which will be used to store the game characters; tConfigurations, which will be used to store the textureConfiguration objects derived from the animation textures; animationTextures, which will store the CKRecord objects that store different animation and sprite textures.  In addition, we define lazy stored properties for allCharactersQuery and allTexturesQuery, which are CKQuery objects that will be used to eventually configure the CKOperations by which we will interact with the CloudKit database.  Finally, there is the yet-to-be-implemented loadGameObjects method, which should load all of our GameCharacter with a single method call.
class CharacterManager{
    
    /** Stored Properties **/
    
    private var publicDB = CKHelper.sharedHelper.publicDB
    
     private var characterDict = [String:Character]()
    
     private var tConfigurations = [TextureConfiguration]()
     private var animationTextures = [CKRecord]()
    
    weak var delegate: CharacterManagerDelegate?
    /** A computed property for the CKQuery that is used to initialize a CKQueryOperation that is responsible for loading all of the characters from the public database **/

    
    lazy private var allCharactersQuery: CKQuery = {
        
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: "CharacterABC", predicate: predicate)
        
        return query
        
    }()
    
    
    /** A computed property for the CKQuery that is used to initialize a CKQueryOperation that is responsible for loading all of the textures from database **/
    
    lazy private var allTexturesQuery: CKQuery = {
       
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: "AnimationTextureABC", predicate: predicate)
        
        return query
    }()
    
    
    func loadGameObjects(){
      
    }

}


Before we tackle the the loadGameObjects function, which will require us to define record types in CloudKit, let’s take a look at the SKScene subclass, BaseScene, in which we will call this function as well each of the CharacterManagerDelegate methods:
import Foundation
import SpriteKit

class BaseScene: SKScene, CharacterManagerDelegate{
    
    let characterManager = CharacterManager()
    
    lazy var titleLabel: SKLabelNode = {
            let label = SKLabelNode(text: "Get ready to rumble...")
        
            label.fontName = "KohinoorDevanagari-Light"
            label.fontColor = UIColor.red
            label.fontSize = 15.0
        
            return label
    }()
    
    
    var randomPoint: CGPoint{
        
        let randomX = Int(arc4random_uniform(UInt32(400))) - 200
        let randomY = Int(arc4random_uniform(UInt32(600))) - 300
        
        return CGPoint(x: randomX, y: randomY)

    }
    
    override func didMove(to view: SKView) {
    
        self.backgroundColor = UIColor.cyan
        
        characterManager.delegate = self
        
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        
        self.titleLabel.move(toParent: self)
        
        characterManager.loadGameObjects()
        
    }
    
    
    func didDownloadAllCharacterImageSprites() {
        
        print("Textures have been loaded!")
        
        self.titleLabel.removeFromParent()
    }
    
   
    
    func didConfigurePhysicsFor(_ character: Character) {
        
        print("Configured physics for: \(character.name)")
        
        }
    
    
    func didConfigureAnimationFor(_ character: Character) {
        
        print("Texture animation configure for: \(character.name)")
        
        character.addCharacter(toScene: self)
        
        character.setPosition(at: self.randomPoint)

        
        character.runTextureAnimation()
    }
    
}

In this BaseScene, we have defined a constant, characterManager, which will be responsible for loading the characters. In our didMoveTo(view:) function, we set the delegate for our characterManager to the BaseScene itself, which is why we indicate the CharacterManagerDelegate protocol conformance in our class declaration for BaseScene.  We define a computed property, randomPoint, which will return a random point on the screen.  We will use this to set the position of each character in such a way that the characters are scattered and spread out enough for us to see them individually.  We also define titleLabel, an SKLabelNode, that is removed as soon as all the character sprites are loaded.  In our implementation of the delegate callbacks, you will notice that as soon as the animation is configured for a given character, we add it to the scene, set its position, and run its animation.    Depending on the game, we could perform different tasks after the physics properties are configured.  If the character behavior is primarily driven by its physics body as opposed to its animations, then we might want to add the character after the physics properties have been configured, after which we can run the animation for that character as soon as its animations have been loaded.
Now, returning to our CharacterManager class, let’s look at how we will implement the the loadGameObjects function:
    func loadGameObjects(){
        
        let LoadTextureConfigurationsOperation = configureLoadTextureConfigurationsOperation(withCompletionHandler: {
            print("Finished downloading all the animation textures.")
            self.delegate?.didDownloadAllCharacterImageSprites()
        })
        
        
        let LoadCharacterOperation = configureLoadCharactersOperation(withCompletionHandler: {
            
            
            self.configureWingmanAnimations()
            self.configureSpikeballAnimations()
            self.configureSunAnimations()
            
            self.configureFlymanAnimations()
            self.configureSpikemanAnimations()
            
            self.showCharacterAnimationsDebugInfo()
            
            self.tConfigurations = []
            self.animationTextures = []
            
        })
        
        
        LoadCharacterOperation.addDependency(LoadTextureConfigurationsOperation)
        
        self.publicDB.add(LoadTextureConfigurationsOperation)
        self.publicDB.add(LoadCharacterOperation)
        
    }


As you will notice, there are two different CKQueryOperations that are added to the database background queue here.  These CKQueryOperationsare generated with the help of private helper functions that will see later. The first one, LoadTextureConfigurationsOperation, simply downloads all of the image sprites for all of the game characters, which is why we called the delegate method, didDownloadAllCharacterImageSprites, in its completion handler.  The second one, LoadCharacterOperation, basically configures all of the game characters, downloading character data and physics property data and then instantiating a Character instance, which is then stored in the charactersDict.  In the completion handler for this operation, we’ve called different helper methods that are used to configure the different animations for different character.  This code can absolutely be refactored so that a single method is used to accomplish this.  Alternatively, the code can be refactored so that characters and their associated physics properties and animations are downloaded on a one-by-one exercises.  This challenge is left to you.
Let’s look at the helper method used to configure and generate the CKQueryOperation objects that are used to download character data and instantiate game characters:
     func configureLoadTextureConfigurationsOperation(withCompletionHandler completionHandler: (()->(Void))?) -> CKQueryOperation{
        
        let ckQueryOperation = CKQueryOperation(query: self.allTexturesQuery)
        
        ckQueryOperation.recordFetchedBlock = {
            
            record in
            
            
            if let tConfiguration = CharacterManager.GetTextureConfiguration(fromRecord: record){
                self.tConfigurations.append(tConfiguration)
            }
           
            
        }
        
        ckQueryOperation.completionBlock = {
            print("Textures have been loaded. A total of \(self.tConfigurations.count) have been loaded")
            if let completionHandler = completionHandler{
                completionHandler()
            }
        }
        
        return ckQueryOperation
    }
    



    private func configureLoadCharactersOperation(withCompletionHandler completionHandler: (()->(Void))?) -> CKQueryOperation{
        
        let ckOperation = CKQueryOperation(query: self.allCharactersQuery)
        
        ckOperation.recordFetchedBlock = {
            
            record in
            
            if let character = CharacterManager.GetCharacter(fromRecord: record){
                
                
                self.characterDict[character.name] = character
                
                let LoadCharacterPhysicsOperation = self.configureLoadCharacterPhysicsOperation(forCharacter: character, andCKRecord: record, andCompletionHandler: {
                    
                    self.delegate?.didConfigurePhysicsFor(character)
                })
                

                LoadCharacterPhysicsOperation.addDependency(ckOperation)
                
                self.publicDB.add(LoadCharacterPhysicsOperation)
            }
            
        }
        
        ckOperation.completionBlock = {
            
            
            if let completionHandler = completionHandler{
                completionHandler()
            }
        }
        
        return ckOperation
    }
    
    

As you can see here, these helper methods themselves rely upon additional helper methods, which we will show below.  The configureLoadTextureConfigurationsOperation(withCompletionHandler:)  in its recordFetchedBlock makes use of a static helper method CharacterManager.GetTextureConfiguration(fromRecord:), which basically converts a CKRecord to a TextureConfiguration object and adds it to an array of texture configuration objects (the textures from this array are later used to configure the animations for each character via a TextureConfigurationManager struct.  These code for these classes is provided below as a reference:
TextureConfiguration.h
struct TextureConfiguration{
    
    var name: String
    var texture: SKTexture?
    var order: Int
    var orientation: Orientation
    
}

TextureConfigurationManager.h
class TextureConfigurationManager{
    
    
    let tConfigurations: [TextureConfiguration]
    
    
    init(withTextureConfigurations textureConfigurations: [TextureConfiguration]){
        self.tConfigurations = textureConfigurations
    }
    
    func getTextureAnimation(forTextureAnimationWith name: String, andOrientation orientation: Orientation, andTimePerFrame timerPerFrame: Double) -> TextureAnimationGenerator{
        
        let tConfigurations = self.tConfigurations.filter({$0.name == name && $0.orientation == orientation})
        
        let sortedTextureConfigurations = tConfigurations.sorted(by: {$0.order < $1.order})
        
        let textures = sortedTextureConfigurations.flatMap({$0.texture})
        
        return TextureAnimationGenerator(animationName: name, timePerFrame: timerPerFrame, textures: textures)
        
    }
    

  }


The configureLoadCharactersOperation(withCompletionHandler🙂 basically uses the allCharactersQuery, which is a lazy stored property we defined earlier, to initialize a CKQueryOperation, whose recordFetchedBlock makes use of the static helper method CharacterManager.GetCharacter(fromRecord:), which basically converts a CKRecord to a Character instance, as show below.  This operation also defines another operation in its resultsFetchedBlockcalled LoadCharacterPhysicsOperation, to which the current operation being defined, that is the LoadCharactersOperation, is added as a dependency so as to make sure that that the physics properties are downloaded and configured after the characters have been downloaded:
    static func GetCharacter(fromRecord ckRecord: CKRecord) -> Character?{
        
        guard let anchorPointStr = ckRecord.value(forKey: "anchorPoint") as? String,
            let zPosition = ckRecord.value(forKey: "zPosition") as? Double,
            let xScale = ckRecord.value(forKey: "xScale") as? Double,
            let yScale = ckRecord.value(forKey: "yScale") as? Double,
            let charName = ckRecord.value(forKey: "name") as? String else {
                return nil
        }
        
        guard let texture = CharacterManager.GetTexture(fromCKRecord: ckRecord) else {
            return nil
        }
        
        let anchorPoint = CGPointFromString(anchorPointStr)
        
        let character = Character(name: charName, anchorPoint: anchorPoint, zPosition: CGFloat(zPosition), xScale: CGFloat(xScale), yScale: CGFloat(yScale))
        
        character.configureTexture(with: texture)
        
        return character
    }
}

This method itself relies on another helper method CharacterManager.GetTexture(fromCKRecord:), which is used to convert the CKAsset associated with a given CKRecord into an SKTexture object:
 static func GetTexture(fromCKRecord ckRecord: CKRecord) -> SKTexture?{
        
        guard let asset = ckRecord.value(forKey: "texture") as? CKAsset,let imageData = NSData(contentsOf: asset.fileURL),let image = UIImage(data: imageData as Data) else {
            
            return nil
        }
        
        return SKTexture(image: image)
    }


Finally, we demonstrate the implementations for each of the helper methods that is responsible for configuring the animations for the different characters.  These helper methods are called in completionHandler of the configureLoadCharactersOperation in the loadGameObjects method.  In this way, we ensure that animations are downloaded and configured after they are downloaded from CloudKit:
    private func configureSpikemanAnimations(){
        
        let tConfigurationManager = TextureConfigurationManager(withTextureConfigurations: self.tConfigurations)
        
        if let spikeman = self.characterDict["spikeman"]{
            
            let spikemanTextureAnimationsGenerator = tConfigurationManager.getTextureAnimation(forTextureAnimationWith: "spikemanWalkRight", andOrientation: .Right, andTimePerFrame: 0.40)
            
            
            spikeman.configureAnimations(with: spikemanTextureAnimationsGenerator)
            self.delegate?.didConfigureAnimationFor(spikeman)
            
        }
        
    }
    
    private func configureWingmanAnimations(){
        
        let tConfigurationManager = TextureConfigurationManager(withTextureConfigurations: self.tConfigurations)
        
        if let wingman = self.characterDict["wingman"]{
            
            let wingmanTextureAnimationsGenerator = tConfigurationManager.getTextureAnimation(forTextureAnimationWith: "wingmanFly", andOrientation: .None, andTimePerFrame: 0.40)
            
            
            wingman.configureAnimations(with: wingmanTextureAnimationsGenerator)
            self.delegate?.didConfigureAnimationFor(wingman)
        
        }
        
    }
    
    
    private func configureFlymanAnimations(){
        
        let tConfigurationManager = TextureConfigurationManager(withTextureConfigurations: self.tConfigurations)
        
        if let flyman = self.characterDict["flyman"]{
            
            let flymanTextureAnimationsGenerator = tConfigurationManager.getTextureAnimation(forTextureAnimationWith: "flymanFly", andOrientation: .None, andTimePerFrame: 0.40)
            
            
            flyman.configureAnimations(with: flymanTextureAnimationsGenerator)
            self.delegate?.didConfigureAnimationFor(flyman)
            
        }
        
    }
    
    
    private func configureSunAnimations(){
        
        let tConfigurationManager = TextureConfigurationManager(withTextureConfigurations: self.tConfigurations)
        
        if let sun = self.characterDict["sun"]{
            
            let sunTextureAnimationsGenerator = tConfigurationManager.getTextureAnimation(forTextureAnimationWith: "sunRotate", andOrientation: .None, andTimePerFrame: 0.40)
            
            
            sun.configureAnimations(with: sunTextureAnimationsGenerator)
            self.delegate?.didConfigureAnimationFor(sun)
            
        }

        
    }
    
    private func configureSpikeballAnimations(){
        
        let tConfigurationManager = TextureConfigurationManager(withTextureConfigurations: self.tConfigurations)
        
        if let spikeball = self.characterDict["spikeball"]{
            
            let spikeballTextureAnimationsGenerator = tConfigurationManager.getTextureAnimation(forTextureAnimationWith: "spikeballRotate", andOrientation: .None, andTimePerFrame: 0.40)
            
            
            
            spikeball.configureAnimations(with: spikeballTextureAnimationsGenerator)
            self.delegate?.didConfigureAnimationFor(spikeball)
        }
        
    }
    
    

Now, when you run the project, assuming that you have created the appropriate record types in the CloudKit dashboard for your app, the character should appear at random points on the screen and run different animations, respectively.

No comments:

Post a Comment