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
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Apr 25, 2019 at 17:06
\$\endgroup\$
1
  • \$\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\$ Commented May 21, 2019 at 17:32

1 Answer 1

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
answered May 21, 2019 at 17:15
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.