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 RedFish, BlueFish, OrangeFish, PinkFish, 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.
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