Making of Castle Tintagel, Part 2


Finite State Machines

Hi, Pixelated Opus here and today I would like to continue the discussion of the development process of my playdate game titled "Castle Tintagel".  

Finite state machines are commonly used in game development to control and organize logic flow. It is commonly used in implementation for AI(its worth noting that there are other ways to implement AI, i.e behavior trees, but for the purposes of my game FSMs worked perfectly fine). You can model AI behavior of an enemy using states( "Attack", "Run", "Evade" , etc )

Visually, each state can be represented by a node and the edges between them the transitions.  The transitions are the conditions required to move into another state or change states. A well implemented FSM guarantees that an entity will only be in 1 state at a given time.

Example States

There are many ways to implement an FSM. I am going to show you my method that I was originally introduced to me at my Game Development internship . I have since modified the original pattern and adapted it, but at its core it remains the same. 

Its quite simple, we define a set of states via an enumeration of a table in LUA. 

local PlayerState <const> = {    
    IDLE = 1,     
    RUN = 2,     
    JUMP = 3,       
    ATTACK = 4,     
    BLOCK = 5,     
    ... 
}

We then implement two functions that will set and finalize the state change.

function Player:SetState(state)     
    if self.requestedState ~= state then         
        self.requestedState = state;     
    end 
end
function Player:FinalizeStateChange()     
    if self.state ~= self.requestedState then         
        self:OnExitState(self.state);         
        self:OnEnterState(self.requestedState);         
        self.state = self.requestedState;     
    end 
end

Finalizing a state change is done at the end of the update tick. This ensures we only actually attempt to exit and enter a new state once per update. 

function Player:OnEnterState(state)     
    if state == PlayerState.RUN then         
        self:OnEnterRun();     
    elseif state == PlayerState.IDLE then         
        self:OnEnterIdle();     
    elseif state == PlayerState.JUMP then         
        self:OnEnterJump();     
    elseif ...
end

This is a nice way to keep things tidy. You can ensure logic only runs when you are in a given state and not muddy your update loop. I use this pattern often to control logic flow of various entities and systems within my game. You can use this to control the logic flow for interacting with doors, driving enemy logic, player logic, UI transitions, and so on.

You can also extend the enter and exit state functions to take in the previous and next states. That way if you special behavior when transitioning from Run->Idle vs Attack->Idle or Jump->Idle, you can account for that. 

Here is sample attack logic from the Frog enemy found in the Underground Cave. 

function Frog:OnUpdateAttack()     
    local elapsed = pd.getCurrentTimeMilliseconds() - self.frogAttackStartTimeInMs;     
    local t = elapsed / self.frogAttackDurationInMs;      
    t = math.abs((2.0 * t) - 1.0);     
    t = 1.0 - t;     
   
    local offsetX = self.facingDir * t * ATTACK_DISTANCE;     
    self.tongue:SetPositionOffset(offsetX, 0);      
    local xPos, yPos, width, height = Game:GetPlayerBoundsRect();     
    if self.tongue:CheckIntersectsRect( xPos, yPos, width, height ) then         
        Game:DamagePlayer(self.attackDamage, self.tongue:GetEndXPos());     
    end      
    
    if elapsed >= self.frogAttackDuration then         
        self:SetState(FrogState.IDLE);     
    end 
end

This pattern is mostly for organization. The logic for updating the frogs attack is guaranteed to only run during the attack state. The update function is not bloated with a bunch of branching logic.  For example:

DONT DO THIS! :)
function Frog:update()     
    Frog.super.update(self);      
       
    if self.state == FrogState.Attack then
        if self.justEnteredAttack then
            self.justEnteredAttack = false;
            -- initialize attack state vars
        end
    end
end
DO THIS INSTEAD :)
function Frog:update()          
    Frog.super.update(self);    
    self:OnUpdateState();     
    self:FinalizeStateChange();
end

Enemies

Enemies in Castle Tintagel are all implement in their own lua files, but extend from a common base Enemy class. The base enemy class handles movement, collision, taking damage and sets up some common vars like health and damage. 

Each derived enemy object needs only implement their own specific behaviors. I have left the movement fairly flexible. Each derived enemy can overwrite values that make it easy to implement unique behaviors. For example,  here is the movement code in the base enemy class.

-- compute velocity     
self.velocity.y += self.gravity * self.gravityScalar;     
self.velocity.x = Clamp(self.velocity.x, -self.maxSpeed, self.maxSpeed);     
self.velocity.y = Clamp(self.velocity.y, -self.maxVerticalSpeed, self.maxVerticalSpeed);  
   
 -- apply movement    
local posX, posY = self:getPosition();     
local targetXPosition = posX + self.velocity.x;     
local targetYPosition = posY + self.velocity.y;   
  
if self.targetPositionOverrideY then         
    targetYPosition = self.targetPositionOverrideY;     
end     
if self.targetPositionOverrideX then        
    targetXPosition = self.targetPositionOverrideX;     
end

You want a flying enemy? Set self.gravityScalar to 0.0. in the derived enemies init function. You want to control the position yourself completely? Use the position override vars (this  is extremely flexible for some custom movement) You can even set an overlapAll flag to make enemies overlap everything in their collision response, useful for enemies that don't collide with walls.  

As with everything, there are many different ways to solve problems, always choose the solution that provides flexibility and works for your project.

There is plenty more to cover in the upcoming blog segments, so I hope you enjoy reading along and feel free to drop any questions in the chat.

Until next time! 😊

-PixelatedOpus

Get Castle Tintagel

Buy Now$8.99 USD or more

Leave a comment

Log in with itch.io to leave a comment.