Wednesday, August 8, 2018

Fish Game: Part 2

If you are interested in more iOS tutorials, please visit my portfolio site at https://suzhoupanda.github.io/. Some of my games and apps haven’t been updated for a while and may be a little buggy, but they are free, so feel free to download and enjoy.
In the previous tutorial, we found our game assets and configured some minimal background scenery in the SpriteKit Scene Editor.  I personally enjoy having some colorful and vivid feedback while developing a game, so I thought it worthwhile to begin this way.
In this part of the tutorial, we will start to develop our BaseScene class and other game objects.  To begin with, we will define a protocol called FishAgentProtocol, whose methods will allow us to decouple a GKAgent2D object from the SKSpriteNode that will be responsible for displaying our Fish sprite. The SKSpriteNode object is also responsible for holding the fish’s transform properties (i.e. position and rotation) as well as its physics body. GameplayKit provides a GKAgent2D class that allows game objects to participate in a simulation driven by Agent-Goal behavior. We wish to use that simulation here, so we will have to associate a GKAgent2D object with an SKSpriteNode object for our fish.
protocol FishAgentDelegate{
    
    func fishAgentWillUpdatePosition(_ agent: GKAgent2D,to agentPosition: CGPoint)
    
    func fishAgentWillUpdateOrientation(_ agent: GKAgent2D, to agentOrientation: FishOrientation)
    
    func fishAgentDidUpdatePosition(_ agent: GKAgent2D, to agentPosition: CGPoint)
    
    func fishAgentDidUpdateOrientation(_ agent: GKAgent2D, to agentOrientation: FishOrientation)
    
}

Having defined this protocol, we will next define a class called FishAgent, which will inherit from GKAgent2D and conform to the GKAgentDelegateprotocol:
class FishAgent: GKAgent2D, GKAgentDelegate{


}

In order for the class to conform to the GKAgentDelegate protocol, it must implement two methods, agentWillUpdate(agent:) and agentDidUpdate(agent:), which are basically callbacks that allows to sync the position and rotation of our SKSpriteNode with that of the agent. We define stubs for these two protocol methods, as shown below:
class FishAgent: GKAgent2D, GKAgentDelegate{

func agentWillUpdate(_ agent: GKAgent) {
    

    }
    
    func agentDidUpdate(_ agent: GKAgent) {
        
     
        
    }

}

In addition, we will define a stored optional property conforming to our FishAgenetDelegate protocol, some initializers, and a helper function func configureAgent(withMaxSpeed:andWithMaxAccelerationOf:withRadius:) that can be used to configure the properties of the GKAgent2D object.
  
    var fishAgentDelegate: FishAgentDelegate?


    init(radius: Float, position: CGPoint, zRotation: CGFloat, maxSpeed: Float = 100, maxAcceleration: Float = 50) {
        
        super.init()

        self.radius = radius
        self.rotation = Float(zRotation)
        self.position = vector_float2(x: Float(position.x), y: Float(position.y))
        
        self.delegate = self
        self.maxSpeed = maxSpeed
        self.maxAcceleration = maxAcceleration
        

    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    

    func configureAgent(withMaxSpeed speed: Float, andWithMaxAccelerationOf acceleration: Float, withRadius radius: Float){
        
        self.maxSpeed = speed
        self.maxAcceleration = acceleration
        self.radius = radius
        
    }
For this game, fish orientation will be confined to two directions, either left or right. That is, fish can only face left or right. To that end, we’ll create a new Swift file and define an enum type, FishOrientation, that can represent the orientation of the fish:
enum FishOrientation: String{
    case Left
    case Right
}

It is the agent object that is ultimately responsible for driving the movement of SKSpriteNode by which the Fish is displayed. The SKSpriteNode by which we render the fish will update its position and orientation based on the position and orientation of the agent. When the speed of the agent is less than zero, therefore, the fish will have a left-facing orientation. Conversely, when the speed of the agent is greater than zero, the fish will have a right-facing orientation. Now we can implement the GKAgentDelegate methods for which we defined stubs previously. The delegate methods will be used as wrappers for custom FishAgent delegate protocol methods:
    //MARK:     **** GKAgentDelegate Methods
    
    func agentWillUpdate(_ agent: GKAgent) {
    
        let agent = agent as! GKAgent2D

        let agentPosition = agent.position.getCGPoint()

        fishAgentDelegate?.fishAgentWillUpdatePosition(self, to: agentPosition)
        
        
        let xVelocity = agent.velocity.x
        
        let newOrientation: FishOrientation = xVelocity < 0.00 ? .Left : .Right
        
        fishAgentDelegate?.fishAgentWillUpdateOrientation(self, to: newOrientation)
        
    }
    
    func agentDidUpdate(_ agent: GKAgent) {
        
        let agent = agent as! GKAgent2D
        
        let agentPosition = agent.position.getCGPoint()
        
        fishAgentDelegate?.fishAgentDidUpdatePosition(self, to: agentPosition)
        
        
        let xVelocity = agent.velocity.x
        
        let newOrientation: FishOrientation = (xVelocity < 0.00) ? .Left : .Right
        
        fishAgentDelegate?.fishAgentDidUpdateOrientation(self, to: newOrientation)
        
        
        
    }

As you can see, in our delegate methods, we cast the agent to a GKAgent2D object and use its position and xVelocity to derive the arguments that we will ultimately pass into our FishAgent delegate methods.
So for so good. Now, we are ready to define another class Fish, which will conform to the FishAgentProtocol and will act as wrapper for the FishAgentresponsible for driving the movement behavior of our Fish and the SKSpriteNode object that will be responsible for rendering and displaying our fish.
First, though, we will define another enum type, FishType, which will allow us to access information unique to a specific fish type, such as its default or basic texture, its texture in the dead state, its texture in different orientations, as well as its contact/collision properties. The FishType enum has cases for RedFishBlueFishOrangeFishPinkFish, Eel, and BlowFish, which basically correspond to the different kinds of game assets we have available.
enum FishType: Int{
    
    case RedFish, BlueFish, OrangeFish, PinkFish, Eel, BlowFish

    static let allPlayerTypes: [FishType] = [.OrangeFish,.RedFish,.BlueFish]
    
    static let allFishTypes: [FishType] = [.OrangeFish,.RedFish,.BlueFish, .PinkFish,.Eel]
    
    func getBasicTexture(forFishType fishType: FishType) -> SKTexture{
        switch self {
            case .RedFish:
                return SKTexture(imageNamed: "RedFish_Right")
            case .BlowFish:
                return SKTexture(imageNamed: "BlowFish_Right")
            case .BlueFish:
                return SKTexture(imageNamed: "BlueFish_Right")
            case .Eel:
                return SKTexture(imageNamed: "Eel_Right")
            case .PinkFish:
                return SKTexture(imageNamed: "PinkFish_Right")
            case .OrangeFish:
                return SKTexture(imageNamed: "OrangeFish_Right")
            }
    }
   
    func getTexture(forOrientation orientation: FishOrientation, andForOutlineState outlineState: FishOutlineState, isDead: Bool) -> SKTexture{
       
        switch (self, orientation,outlineState) {
        case (.RedFish, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "RedFish_Outline_Dead_Left") :  SKTexture(imageNamed: "RedFish_Outline_Left")
        case (.RedFish, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "RedFish_Dead_Left") :  SKTexture(imageNamed: "RedFish_Left")
        case (.RedFish, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "RedFish_Outline_Dead_Right") :  SKTexture(imageNamed: "RedFish_Outline_Right")
        case (.RedFish, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "RedFish_Dead_Right") :  SKTexture(imageNamed: "RedFish_Right")
        case (.BlueFish, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "BlueFish_Outline_Dead_Left") :  SKTexture(imageNamed: "BlueFish_Outline_Left")
        case (.BlueFish, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "BlueFish_Dead_Left") :  SKTexture(imageNamed: "BlueFish_Left")
        case (.BlueFish, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "BlueFish_Outline_Dead_Right") :  SKTexture(imageNamed: "BlueFish_Outline_Right")
        case (.BlueFish, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "BlueFish_Dead_Right") :  SKTexture(imageNamed: "BlueFish_Right")
        case (.OrangeFish, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "OrangeFish_Outline_Dead_Left") :  SKTexture(imageNamed: "OrangeFish_Outline_Left")
        case (.OrangeFish, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "OrangeFish_Dead_Left") :  SKTexture(imageNamed: "OrangeFish_Left")
        case (.OrangeFish, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "OrangeFish_Outline_Dead_Right") :  SKTexture(imageNamed: "OrangeFish_Outline_Right")
        case (.OrangeFish, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "OrangeFish_Dead_Right") :  SKTexture(imageNamed: "OrangeFish_Right")
        case (.Eel, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "Eel_Outline_Dead_Left") :  SKTexture(imageNamed: "Eel_Outline_Left")
        case (.Eel, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "Eel_Dead_Left") :  SKTexture(imageNamed: "Eel_Left")
        case (.Eel, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "Eel_Outline_Dead_Right") :  SKTexture(imageNamed: "Eel_Outline_Right")
        case (.Eel, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "Eel_Dead_Right") :  SKTexture(imageNamed: "Eel_Right")
        case (.BlowFish, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "BlowFish_Outline_Dead_Left") :  SKTexture(imageNamed: "BlowFish_Outline_Left")
        case (.BlowFish, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "BlowFish_Dead_Left") :  SKTexture(imageNamed: "BlowFish_Left")
        case (.BlowFish, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "BlowFish_Outline_Dead_Right") :  SKTexture(imageNamed: "BlowFish_Outline_Right")
        case (.BlowFish, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "BlowFish_Dead_Right") :  SKTexture(imageNamed: "BlowFish_Right")
            
        case (.PinkFish, .Left, .Outlined):
            return isDead ?  SKTexture(imageNamed: "PinkFish_Outline_Dead_Left") :  SKTexture(imageNamed: "PinkFish_Outline_Left")
        case (.PinkFish, .Left, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "PinkFish_Dead_Left") :  SKTexture(imageNamed: "PinkFish_Left")
        case (.PinkFish, .Right, .Outlined):
            return isDead ?  SKTexture(imageNamed: "PinkFish_Outline_Dead_Right") :  SKTexture(imageNamed: "PinkFish_Outline_Right")
        case (.PinkFish, .Right, .Unoutlined):
            return isDead ?  SKTexture(imageNamed: "PinkFish_Dead_Right") :  SKTexture(imageNamed: "PinkFish_Right")
        }
    }
    
    func getPredator() -> FishType{
        switch self {
        case .OrangeFish:
            return .RedFish
        case .RedFish:
            return .BlueFish
        case .BlueFish:
            return .OrangeFish
        default:
            break
            
        }
        
        return .Eel
    }
    
    func getPrey() -> FishType{
        switch self {
        case .OrangeFish:
            return .BlueFish
        case .RedFish:
            return .OrangeFish
        case .BlueFish:
            return .RedFish
        default:
            break
            
        }
        
        return .PinkFish
    }
    
    func getColliderType() -> ColliderType{
        switch self {
        case .BlowFish:
            return ColliderType.BlowFish
        case .BlueFish:
            return ColliderType.BlueFish
        case .RedFish:
            return ColliderType.RedFish
        case .OrangeFish:
            return ColliderType.OrangeFish
        case .PinkFish:
            return ColliderType.PinkFish
        case .Eel:
            return ColliderType.Eel
        }
    }
}

As you will notice, the enum also defines helper methods that allow us to determine the predator, prey, texture, and collider type that correspond to a specific type. ColliderType is a wrapper for the contact and collision properties that we will define shortly. Note, for our game here, we have included separate png files for each fish orientation, as well as png files for the each fish in the outlined, unoutlined, and dead states, which accounts for the complexity of the switch statement in the getTexture(forOrientation: andForOutlineState:isDead:) method. Since there are 5 different kinds of fish, and since each fish can have two outline states(i.e. outlined and outlined), two living states (i.e. a dead state and alive state), and two different orientations (i.e. left and right), we have to provide 40 different assets.
Now back to the ColliderType mentioned above. This is basically a wrapper class for the contact and collisions properties adapted from the Demobotssample code provided by Apple. You can click the link to see how it’s implemented in the context of the DemoBots game developed by Apple.
import Foundation
import SpriteKit

struct ColliderType: OptionSet, Hashable{
    
    
    //MARK: Static properties
    
    //A dictionary of which ColliderType's should collide with other ColliderType's
    static var definedCollisions: [ColliderType:[ColliderType]] = [
        
        ColliderType.RedFish : [ColliderType.BlowFish,ColliderType.Barrier],
        ColliderType.OrangeFish: [ColliderType.BlowFish,ColliderType.Barrier],
        ColliderType.BlueFish: [ColliderType.BlowFish,ColliderType.Barrier],
        ColliderType.BlowFish: [ColliderType.BlowFish, ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.Barrier],
        ColliderType.PinkFish: [ColliderType.BlowFish,ColliderType.Barrier],
        ColliderType.Eel: [ColliderType.Eel,ColliderType.BlowFish],
        ColliderType.Barrier: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.Player],
        ColliderType.Player: [ColliderType.Player,ColliderType.Barrier]
      
        
    ]
    
    //A dictionary to specify which ColliderType's should be notified of contact with other ColliderType's
    static var requestedContactNotifications: [ColliderType:[ColliderType]] =  [
        
        ColliderType.RedFish : [ColliderType.BlueFish,ColliderType.OrangeFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.BlowFish,ColliderType.Barrier],
        ColliderType.OrangeFish: [ColliderType.RedFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.Barrier],
        ColliderType.BlueFish: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.Barrier],
        ColliderType.BlowFish: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Barrier],
        ColliderType.PinkFish: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel,ColliderType.Barrier],
        ColliderType.Eel: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel],
        ColliderType.Barrier: [ColliderType.RedFish,ColliderType.OrangeFish,ColliderType.BlueFish,ColliderType.BlowFish,ColliderType.PinkFish,ColliderType.Eel],
        ColliderType.Player: [ColliderType.Collectible,ColliderType.Barrier]
    ]
    
    
    //MARK: Properties
    
    let rawValue: UInt32
    
    static var Player: ColliderType { return self.init(rawValue: 0 << 0)}
    static var Barrier: ColliderType { return self.init(rawValue: 1 << 0)}
    static var Obstacle: ColliderType { return self.init(rawValue: 1 << 1)}
    static var OrangeFish: ColliderType { return self.init(rawValue: 1 << 2)}
    static var PinkFish: ColliderType { return self.init(rawValue: 1 << 3)}
    static var BlowFish: ColliderType { return self.init(rawValue: 1 << 4)}
    static var BlueFish: ColliderType { return self.init(rawValue: 1 << 5)}
    static var Eel: ColliderType { return self.init(rawValue: 1 << 6)}
    static var RedFish: ColliderType { return self.init(rawValue: 1 << 7)}
    static var Collectible: ColliderType { return self.init(rawValue: 1 << 8)}

    //MARK: Hashable
    
    var hashValue: Int{
        return Int(self.rawValue)
    }
    
    //MARK: Convenience Methods for SpriteKit Physics Properties
    
    //A value that can be assigned to an SKPhysicsBody's category mask property
    
    var categoryMask: UInt32{
        return rawValue
    }
    
    //A value that can be assigned to an SKPhysicsBody's collision mask property
    
    var collisionMask: UInt32{
        let mask = ColliderType.definedCollisions[self]?.reduce(ColliderType()){
            
            initial, colliderType in
            
            return initial.union(colliderType)
        }
        
        return mask?.rawValue ?? 0
    }
    
    //A value that can be assigned to an SKPhysicsBody's contact mask property
    
    var contactMask: UInt32{
        let mask = ColliderType.requestedContactNotifications[self]?.reduce(ColliderType()){
            
            initial, colliderType in
            
            return initial.union(colliderType)
            
        }
        
        return mask?.rawValue ?? 0
    }
    
}
Finally, we are ready to define our long-awaited Fish class, which will act as a wrapper for the SKSpriteNode used to render our Fish sprite and the Fish agent used to drive its behavior.
class Fish: FishAgentDelegate{
  
    
  
    
    var isDead: Bool = false
    
    var fishType: FishType = .BlueFish

    var colliderType: ColliderType{
        return self.fishType.getColliderType()
    }
    
    
    var node: SKSpriteNode!

    var agent: FishAgent!
    
   
    var previousOrientation: FishOrientation?
    var currentOrientation: FishOrientation?{
        
        didSet{
            
            if let previousOrientation = oldValue, let currentOrientation = self.currentOrientation, previousOrientation != currentOrientation{
                
                let newTexture = self.fishType.getTexture(forOrientation: currentOrientation, andForOutlineState: .Unoutlined, isDead: false)
                
                self.node.run(SKAction.setTexture(newTexture))
                
            }
            
        }
    }
    
    init(baseScene: BaseScene, fishType: FishType, position: CGPoint, zRotation: CGFloat, radius: Float) {
        
        
        let defaultTexture = fishType.getTexture(forOrientation: .Right, andForOutlineState: .Unoutlined, isDead: false)
        
        self.node = SKSpriteNode(texture: defaultTexture, color: .clear, size: defaultTexture.size())
        self.node.position = position

        baseScene.worldNode.addChild(self.node)
        
        self.fishType = fishType
        
        self.agent = FishAgent(radius: radius, position: position, zRotation: zRotation)
        self.agent.fishAgentDelegate = self
        
        configurePhysicsProperties(withTexture: defaultTexture)

        
    }
    
    
    
   
    
    func configureAgent(withMaxSpeed maxSpeed: Float, andWithMaxAccelerationOf maxAcceleration: Float){
        self.agent.configureAgent(withMaxSpeed: maxSpeed, andWithMaxAccelerationOf: maxAcceleration, withRadius: 100.0)
        
    }


    
    func configurePhysicsProperties(withTexture texture: SKTexture){
        
        self.node.physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
        
        self.node.physicsBody?.affectedByGravity = false
        self.node.physicsBody?.allowsRotation = false
        self.node.physicsBody?.categoryBitMask = self.colliderType.categoryMask
        self.node.physicsBody?.collisionBitMask = self.colliderType.collisionMask
        self.node.physicsBody?.contactTestBitMask = self.colliderType.contactMask
    }
    
    /** Fish Agent Delegate Methods **/
    
    func fishAgentWillUpdatePosition(_ agent: GKAgent2D, to agentPosition: CGPoint) {
        print("Agent will upate position to x: \(self.node.position.x), y: \(self.node.position.y)")

    }
    
    func fishAgentWillUpdateOrientation(_ agent: GKAgent2D, to agentOrientation: FishOrientation) {
        print("Agent will upate orientation to: \(agentOrientation.rawValue)")

    }
    
    func fishAgentDidUpdatePosition(_ agent: GKAgent2D, to agentPosition: CGPoint) {
        print("Agent did upate position to x: \(self.node.position.x), y: \(self.node.position.y)")
        
        self.node.position = agentPosition
    }
    
    func fishAgentDidUpdateOrientation(_ agent: GKAgent2D, to agentOrientation: FishOrientation) {
        print("Agent did update orientation to: \(agentOrientation.rawValue)")
        
        self.currentOrientation = agentOrientation
        
    }
   
    
    
    

 
}

With our Fish class fully define, we next define a Player class, which inherits from the Fish class but whose physics properties
must be configured slightly differently from a regular fish, which requires us to override the the configurePhysicsProperties(withTexture:) with method defined in the Fish class.
import Foundation
import SpriteKit
import GameplayKit


class Player: Fish{
    
    override init(baseScene: BaseScene, fishType: FishType, position: CGPoint, zRotation: CGFloat, radius: Float) {
        
        super.init(baseScene: baseScene, fishType: fishType, position: position, zRotation: zRotation, radius: radius)
        
    }

    override func configurePhysicsProperties(withTexture texture: SKTexture) {
        
        self.node.physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
        
        self.node.physicsBody?.affectedByGravity = false
        self.node.physicsBody?.allowsRotation = false
        self.node.physicsBody?.categoryBitMask = self.colliderType.categoryMask | ColliderType.Player.categoryMask
        self.node.physicsBody?.collisionBitMask = self.colliderType.collisionMask | ColliderType.Player.collisionMask
        self.node.physicsBody?.contactTestBitMask = self.colliderType.contactMask | ColliderType.Player.contactMask
    }
    
    
}
Our player is ready to rock! Good job. We will be able to appreciate the fruits of our labors shortly.
To continue to the next section, click here. Otherwise, to go back to the previous section, click here.

No comments:

Post a Comment