3
\$\begingroup\$

My questions are:

  1. How can I improve my implementation of the FSM model?
  2. Should Finite State Machines have functionality for Adding and Removing states from table? (Since I can only be in one state at a time I feel like there's no need for me to store the states when I can just call SetState(state)).
  3. Am I doing the transitions right with SetState(state)? (I feel like I can just handle the transitions inside each States update method. Maybe there's a better way but I'm not sure.)

Below is the code that I have these questions about.


Diagram

AI FSM Diagram

State Module:

local State = {}
State.__index = State

function State:New()
    local newState = {
            Init = function() print("Init ran") end,
            Update = function() print("Updating") end,
            Enter = function() print("Entering") end,
            Exit = function() print("Exiting") end,
    }
    setmetatable(newState, self)

    print("Created new state")
    return newState
end

return State

StateMachine Module:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local State = require(ReplicatedStorage:WaitForChild("State"))

local StateMachine = {}
StateMachine.__index = StateMachine

function StateMachine:Create()
    local machine = {}
    machine.initState = State:New()
    machine.currentState = machine.initState
    machine.currentState.Init()

    setmetatable(machine, self)
    return machine
end

function StateMachine:Update()
    if self.currentState ~= nil then
        self.currentState:Update()
    end
end

function StateMachine:SetState(state)
    assert(state ~= nil, "Cannot set a nil state.")
    self.currentState:Exit()
    self.currentState = state
    self.currentState.Init()
    self.currentState.Enter()
end

return StateMachine

Here's the way I'm using my version of a FSM.

Example:

newZombie.stateMachine = StateMachine:Create()

newZombie.idleState = State:New()
newZombie.idleState.Init = function()
    print("idle state init")
end
newZombie.idleState.Enter = function()
    print("idle state enter!")
end
newZombie.idleState.Update = function()
    print("idle state updating!")
    if not newZombie.target then
        print("Getting target")
    newZombie.target = newZombie:GetNearestTarget()
    end

    if newZombie.zombieTarget then
        print("Found target")             
        newZombie.stateMachine:SetState(newZombie.chaseState)
    end
end

newZombie.chaseState = State:New()
newZombie.chaseState.Init = function()
    print("chaseState init")
end
newZombie.chaseState.Enter = function()
    print("chaseState enter")
end
newZombie.chaseState.Update = function()
    print("chaseState updating!")
    if newZombie.target then
        local direction = (newZombie.target.Position - newZombie.rootPart.Position).Unit * 0.5
        local distanceToTarget = (newZombie.target.Position - newZombie.rootPart.Position).magnitude
        local MAX_ATTACK_RADIUS = 4
        local ray = Ray.new(newZombie.rootPart.Position, (newZombie.target.Position - newZombie.rootPart.Position).Unit * 500)
        local ignoreList = {}
        for i, v in pairs(ZombiesServerFolder:GetChildren()) do
            table.insert(ignoreList, v)
        end
        local hit, position, normal = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)

        if not hit.Parent:FindFirstChild("Humanoid") then
            print("Walk Path")
        end

        if distanceToTarget >= MAX_ATTACK_RADIUS then
            newZombie.rootPart.CFrame = newZombie.rootPart.CFrame + direction
        else
            newZombie.stateMachine:SetState(newZombie.attackState)
        end
    else
            newZombie.stateMachine:SetState(newZombie.idleState)
        end
end

newZombie.attackState = State:New()
newZombie.attackState.Init = function()
    print("attackState init")
end
newZombie.attackState.Enter = function()
    print("attackState enter")
end
newZombie.attackState.Update = function()
    print("attackState updating!")
    if newZombie.target then
        local distanceToTarget = (newZombie.target.Position - newZombie.rootPart.Position).magnitude
        local MAX_ATTACK_RADIUS = 4

        if distanceToTarget >= MAX_ATTACK_RADIUS then     
            newZombie.stateMachine:SetState(newZombie.chaseState)
        end
    end
end
----------------------------------------------------
----               STARTING STATE               ----
----------------------------------------------------
newZombie.stateMachine:SetState(newZombie.idleState)
----------------------------------------------------

Also in the NPC update function I'm updating the state machines current state update function.

if self.stateMachine then
    self.stateMachine:Update()
end
\$\endgroup\$
  • \$\begingroup\$ Shouldn't there be another state between Idle and Chase? Or is 'Find target' guaranteed to deliver the target? In what state should your machine be while it's trying to find the target, you think? It's hardly idle when it's actually on the hunt. \$\endgroup\$ – Mast May 21 at 17:32
1
\$\begingroup\$

Am I doing the transitions right with SetState(state)? (I feel like I can just handle the transitions inside each States update method. Maybe there's a better way but I'm not sure.)

You allow a state to change the state of its parent state machine. For instance:

newZombie.attackState.Update = function()
    ..
        if distanceToTarget >= MAX_ATTACK_RADIUS then     
            newZombie.stateMachine:SetState(newZombie.chaseState)
        end
    ..
end

Because of this, there is a potential problem with your state transition flow. Nothing prevents a state to change the state of the machine while in Init/Enter/Exit.

function StateMachine:SetState(state)
    assert(state ~= nil, "Cannot set a nil state.")
    self.currentState:Exit()
    self.currentState = state
    self.currentState.Init()
    self.currentState.Enter()
end

For example, if stateB starts a state transition to stateC while in stateA in Enter, the following could happen:

  • stateA.Exit (ok)
  • stateB.Init (ok)
  • stateB.Enter (ok)
  • stateB.Exit (fishy because in transition, but consistent)
  • stateC.Init (fishy because in transition, but consistent)
  • stateC.Enter (fishy because in transition, but consistent)
  • stateB.Exit (wrong, the previous active state gets exited after the current state is activacted)

You can fix this by either:

  1. blocking new state transitions while transitioning
  2. allowing states to immediately transition to other states while in transition, but then you need to make sure the order of Exit/Init/Enter remains consistent
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.