Wednesday, August 8, 2018

Fish Game: Part 4

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 our FishManager class, we will define a nested enum type FishMandate, which will be used to configure the different kinds of GKGoal objects necessary to configure the behavior of a GKAgent. This will allow us to configure different kinds of agent-goal driven behavior for various kinds fish, whether they be predators, prey, or the player itself. A closer look at this enum type will reveal that each enum case has associated values. That is, each enum case roughly corresponds to a specific type of GKGoal, and the associated values for each enum case represent the parameters whose values are required to instantiate a GKGoal instance. The nested enum type also includes a computed property FishGoal, which uses the values passed in for the associated types to configure a GKGoal object corresponding to the specific enum case for which the GKGoal object is instantiated.
import Foundation
import SpriteKit
import GameplayKit

class FishManager{

enum FishMandate{

    case HuntPlayer(Player)
    case FleePlayer(Player)
    case Wander(Float)
    case AvoidObstacles([GKObstacle],TimeInterval)
    case AvoidAgentObstacles([GKAgent2D],TimeInterval)
    case ReachVelocity(Float)
    case FollowPath(GKPath,TimeInterval,Bool)
    case Stop

    var FishGoal: GKGoal{

    switch self {
        case .HuntPlayer(let player):
        return GKGoal(toSeekAgent: player.agent)
    case .FleePlayer(let player):
        return GKGoal(toFleeAgent: player.agent)
    case .ReachVelocity(let targetVelocity):
        return GKGoal(toReachTargetSpeed: targetVelocity)
    case .Stop:
        return GKGoal(toReachTargetSpeed: 0.00)
    case .Wander(let float):
        return GKGoal(toWander: float)
    case .AvoidObstacles(let obstacles,let predictionTime):
        return GKGoal(toAvoid: obstacles, maxPredictionTime: predictionTime)
    case .AvoidAgentObstacles(let agents, let predictionTime):
        return GKGoal(toAvoid: agents, maxPredictionTime: predictionTime)
    case .FollowPath(let path,let predictionTime, let isCyclical):
        return GKGoal(toFollow: path, maxPredictionTime: predictionTime, 
        forward:isCyclical)

    }
}

}

}

Our FishManager class will also have an unowned reference to the BaseSceneclass, allowing the FishManager initializer to take as an argument any kind of specialized scene inheriting from the BaseScene. Furthermore, a computed property is used to obtain a reference to the player property in the BaseScene class. This will be necessary to configure certain kinds of behaviors that involve avoiding, fleeing from, or hunting the player.
We also define optional fish arrays to store different groups of fish which will be configured with common behaviors using the FishMandate enum shown above. It is also worthwhile to define a constant maxFleeingDistance, which will be used in the FishManager’s update() function to establish a distance criterion that can be used to determine when certain behaviors, such as a fleeing behavior, come into effect for different kinds of fish (e.g. the fleeingFishGroup will only flee the player fish when the player comes within a minimum proximity, otherwise these fish will follow their default behavior patterns).
class FishManager{

...
...
...

    unowned var baseScene: BaseScene

    init(baseScene: BaseScene){
        self.baseScene = baseScene
    }

    var fleeingFishGroup: [Fish]?
    var wanderingFishGroup: [Fish]?
    var playerHuntingFishGroup: [Fish]?
    var pathFollowingFishGroup: [Fish]?
    var avoidingFish: [Fish]?
    var flockingPreyFish: [Fish]?
    var flockingPredatorFish: [Fish]?
 
    var player: Player{
        return baseScene.player
    }

    let maxFleeingDistance: Float = 200.0

}

Now that we have defined our stored properties along with an initializer, we proceed to define the helper functions that will actually add different kinds of fish to the scene. For example, if we want to configure a group of fish that will hunt the player fish, we call the addPlayerHuntingFishGroup(fishGroup:avoidsObstacles) method on the fish manager. This method takes an array of fish, which can include instances of various kinds of fish (i.e. BlueFish, OrangeFish, Eel, etc) which we can instantiate randomly or via placeholders for spawning points defined in the SpriteKit Scene file. The method also takes an additional parameter, avoidsObstacles, which will determine whether or not the fish, in addition to hunting the player, actively try to avoid obstacles. Once the fish array of fish are configured, they are added to the BaseScene.
func addPlayerHuntingFishGroup(fishGroup: [Fish], avoidsObstacles: Bool = false){

    self.playerHuntingFishGroup = fishGroup

    self.playerHuntingFishGroup?.forEach({

    fish in

    fish.agent.behavior = GKBehavior(goal: 
    FishMandate.HuntPlayer(self.player).FishGoal, weight: 1.00)

    if let obstacleAgents = self.baseScene.obstaclesAgents, avoidsObstacles{

        let weight1 = NSNumber(floatLiteral: 1.00)
        let weight2 = NSNumber(floatLiteral: 100.00)

    fish.agent.behavior = GKBehavior(goals: [
        FishMandate.HuntPlayer(self.player).FishGoal,
        FishMandate.AvoidAgentObstacles(obstacleAgents, 3.00).FishGoal
        ], andWeights:

        [
        weight1,
        weight2
        ])

    }

    baseScene.agentSystem.addComponent(fish.agent)

    })
    }
}

We define an additional helper method, addWanderingFishGroup(fishGroup:avoidsObstacles), which adds to the BaseScene a group of fish whose primary behavior is to wander the ocean. This method also takes a parameter avoidsObstacles which is a Boolean flag for determining whether or not these fish will avoid obstacles in addition to wandering the ocean. Note that I’ve used the arbitrary float value of 500 to configure the GKGoal for wandering fish. This need not be a hard-coded value and can be defined as a constant in the FishManager class if you want.
 

func addWanderingFishGroup(fishGroup: [Fish], avoidsObstacles: Bool = false){

    self.wanderingFishGroup = fishGroup

    self.wanderingFishGroup?.forEach({

        fish in

        fish.agent.behavior = GKBehavior(goal: 
        FishMandate.Wander(500.00).FishGoal, weight: 1.00)

        if let obstacles = self.baseScene.obstacles, avoidsObstacles{
            fish.agent.behavior = GKBehavior(goals: [

            FishMandate.Wander(500.00).FishGoal,
            FishMandate.AvoidObstacles(obstacles, 3.00).FishGoal

        ], andWeights: [

            NSNumber(floatLiteral: 10.00),
            NSNumber(floatLiteral: 100.00)
        ])
    }

    baseScene.agentSystem.addComponent(fish.agent)

    })
}


Let’s define another helper method, addFleeingFishGroup(fishGroup:avoidsObstacles), which will be used to configure a group of fish whose main behavioral characteristic is to avoid the player fish:
func addFleeingFishGroup(fishGroup: [Fish], avoidsObstacles: Bool = false){

    self.fleeingFishGroup = fishGroup

    self.fleeingFishGroup!.forEach({

        fish in

        fish.agent.behavior = GKBehavior(goals: 
       [FishMandate.FleePlayer(self.player).FishGoal,FishMandate.Stop.FishGoal])

        if let obstacleAgents = self.baseScene.obstaclesAgents, avoidsObstacles{ 
            fish.agent.behavior = GKBehavior(goals: [

            FishMandate.AvoidAgentObstacles(obstacleAgents, 3.00).FishGoal,
            FishMandate.FleePlayer(self.player).FishGoal,
            FishMandate.Stop.FishGoal],

            andWeights: [
            NSNumber(value:100.0),
            NSNumber(value:100.0),
            NSNumber(value:0.00)
        ])
    }

    baseScene.agentSystem.addComponent(fish.agent)
    })
}

Finally, we will also define a helper function, addFlockingPredatorFish(fishGroup:avoidsObstacles), whose main behavioral characteristic is hunt the player fish, but not only hunt the player fish but also flock together in a group. This will give rise to an effect where a large school of fish seems to chase after the player fish in unison.
func addFlockingPredatorFish(fishGroup: [Fish], avoidsObstacles: Bool = false){


/** Get reference for each fish's agent component **/
let fishAgents: [GKAgent2D] = fishGroup.map({$0.agent})


let separationRadius: Float = 0.553*50.00
let separationAngle: Float = 3*Float.pi/4.0
let separationWeight: Double = 10.0


let alignmentRadius: Float = 0.83333*50.0
let alignmentAngle: Float = Float.pi/4.0
let alignmentWeight: Double = 12.66

let cohesionRadius: Float = 1.0*100.00
let cohesionAngle: Float = Float.pi/2.0
let cohesionWeight: Double = 8.66


let behavior = GKBehavior(weightedGoals: [
    GKGoal(toAlignWith: fishAgents, maxDistance: alignmentRadius, maxAngle: 
    alignmentAngle): NSNumber(floatLiteral: alignmentWeight),
    GKGoal(toSeparateFrom: fishAgents, maxDistance: separationRadius, maxAngle: 
    separationAngle): NSNumber(floatLiteral: separationWeight),
    GKGoal(toCohereWith: fishAgents, maxDistance: cohesionRadius, maxAngle: 
    cohesionAngle): NSNumber(floatLiteral: cohesionWeight),
    GKGoal(toWander: 500.0): NSNumber(floatLiteral: 100.0)
    // GKGoal(toSeekAgent: self.player.agent): NSNumber(floatLiteral: 10.00)
    ])



    fishAgents.forEach({


    agent in


    agent.behavior = behavior


    self.baseScene.agentSystem.addComponent(agent)
    })
}

Finally, our FishManager will also need an update function that can be called in the BaseScene’s game loop functions, such as the BaseScene’s own updatefunction. This function will regularly evaluate the distance between individual fish in the fleeingFishGroup and the player. If the distance drops below the maxFleeingDistance, then the fish will be configured such that their fleeing behavior is configured with a weight of 1.0, while their stopping behavior is configured with a weight of 0.0. If, on the other hand, the a fish from this group is well-away from the player fish (i.e beyond the maximum fleeing distance), then the fish will be configured such that their fleeing behavior is configured with a weight of 0.0 while their stopping behavior is configured with a weight of 0.0.
func update(currentTime: TimeInterval){

    if let fleeingFishGroup = self.fleeingFishGroup{
        fleeingFishGroup.forEach({

        let distanceFromPlayer = $0.agent.position.getDistanceTo(point: 
        self.player.agent.position)

        if(distanceFromPlayer < maxFleeingDistance){

         $0.agent.behavior?.setWeight(1.0, for: 
        FishMandate.FleePlayer(self.player).FishGoal)
        $0.agent.behavior?.setWeight(0.0, for: FishMandate.Stop.FishGoal)

        } else {
            $0.agent.behavior?.setWeight(0.0, for: 
            FishMandate.FleePlayer(self.player).FishGoal)
            $0.agent.behavior?.setWeight(1.0, for: FishMandate.Stop.FishGoal)
        }

})
}
}

}

Let’s see our FishManager in action. We will define a new scene, PHSScene for “Player Hunting Scene,” that will inherit from the BaseScene and
also define a force-unwrapped optional for the fish manager. In the viewDidLoad() function, we instantiate several fish which we pass into the the method addPlayerHuntingFishGroup(fishGroup:avoidsObstacles), which we call on our fish manager class. Don’t forget to also call the update method on the fish manager class in the scene’s update function.
import Foundation
import SpriteKit
import GameplayKit

class PHSScene: BaseScene{

    var fishManager: FishManager!

    override func didMove(to view: SKView) {
        super.didMove(to: view)

        let fish1 = Fish(baseScene: self, fishType: .OrangeFish, position: 
        CGPoint(x: 100, y: 100), zRotation: 0.00, radius: 40.0)
        fish1.configureAgent(withMaxSpeed: 20, andWithMaxAccelerationOf: 40)

        let fish2 = Fish(baseScene: self, fishType: .OrangeFish, position: 
        CGPoint(x: 300, y: 40), zRotation: 0.00, radius: 40.0)
        fish2.configureAgent(withMaxSpeed: 20, andWithMaxAccelerationOf: 40)

        let fish3 = Fish(baseScene: self, fishType: .OrangeFish, position: 
        CGPoint(x: -50, y: -100), zRotation: 0.00, radius: 40.0)
        fish3.configureAgent(withMaxSpeed: 20, andWithMaxAccelerationOf: 40)

        self.fishManager = FishManager(baseScene: self)
 
        self.fishManager.addPlayerHuntingFishGroup(fishGroup: 
        [fish1,fish2,fish3], avoidsObstacles: true)

     }

    override func update(_ currentTime: TimeInterval) {
        super.update(currentTime)

        self.fishManager.update(currentTime: currentTime)

     }

   override func didSimulatePhysics() {
        super.didSimulatePhysics()
    }

    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
         super.touchesBegan(touches, with: event)
    }

}

Excellent job, you have now laid the foundation for developing an awesome fish game. For more review, go back to the previous tutorial, or you want to go back to the beginning, click here.

No comments:

Post a Comment