My questions are:
- How can I improve my implementation of the FSM model?
- 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)).
- 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
-
\$\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– Mast ♦2019年05月21日 17:32:30 +00:00Commented May 21, 2019 at 17:32
1 Answer 1
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:
- blocking new state transitions while transitioning
- 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