I think a finite state machine would work really well for a turn-based battle. You could create whichever states you need to define the battle and then run logic whenever you enter/exit a state. I drew up a quick example of how I’d probably design it.
When the battle starts, it goes into Start Battle
. That’s where I’d probably set up and initialize the battle. From there, it’ll go into the Select Next Attackers
state to see who should be attacking. Afterwards, it’ll go around to choosing attacks, performing attacks, and determining a winner. If no winner has been decided yet, it loops back around and starts at Select Next Attackers
again. If a winner has been decided, it’ll end the battle.
I also wrote up a simple implementation. I haven’t tested it, but I wanted to show an example of how I’d code it.
local StartBattleState = {}
function StartBattleState.OnEnter(Battle)
-- Setup the camera and players in their correct positions, like how you already do in the handler
-- for BindableEvents.StartTheBattle
-- Maybe select who will go first
-- Battle.CurrentAttacker = Participants[1]
Battle:GotoState("SelectNextAttackers")
end
local SelectNextAttackersState = {}
function SelectNextAttackersState.OnEnter(Battle)
-- Basically just cycling the list here but you can decide how you want to choose the next player
local CurrentIndex = table.find(Battle.Participants, Battle.CurrentAttacker)
if CurrentIndex == -1 then
Battle.CurrentAttacker = Participants[1]
else
local NextIndex = (CurrentIndex % #Battle.Participants) + 1
Battle.CurrentAttacker = Battle.Participants[NextIndex]
end
Battle:GotoState("ChooseAttacks")
end
local ChooseAttacksState = {}
function ChooseAttacksState.OnEnter(Battle)
local IsNpc = false -- Somehow determine if the current attacker is an NPC or a player
if isNPC then
-- If the current attacker is an NPC, immediately choose which attacks the NPC should use
local NpcAttack = NPCSystem:DetermineAttacks(Battle.CurrentAttacker)
Battle:GotoState("PerformAttacks", NpcAttack)
else
-- If the current attacker is a player, maybe bring up UI here or something that lets the player choose
-- the attack they want, and listen for events that fire when they choose
UISystem:ShowAttackOptions(Battle.CurrentAttacker)
UISystem.AttackSelected:Once(function(Attack)
Battle:GotoState("PerformAttacks", Attack)
end)
end
end
local PerformAttacksState = {}
function PerformAttacksState.OnEnter(Battle, Attack)
-- Maybe show animations? Show UI health changing? Anything that actually does the attack
end
local DetermineWinnerState = {}
function DetermineWinnerState.OnEnter(Battle)
local Winner = nil -- Determine if someone has lost/won after the attack
if Winner ~= nil then
Battle:GotoState("EndBattle", Winner)
else
Battle:GotoState("SelectNextAttackers")
end
end
local EndBattleState = {}
function EndBattleState.OnEnter(Battle, Winner)
-- Move the camera and players/npcs back to original position
-- Give rewards to any of the winners, etc.
end
local TurnBasedBattle = {}
TurnBasedBattle.__index = TurnBasedBattle
function TurnBasedBattle:GotoState(StateName, ...)
if self.States[StateName] == nil then
warn(`Unable to find state '{StateName}'`)
return
end
if self.CurrentState ~= nil then
-- Make sure the function exists
if typeof(self.CurrentState.OnExit) == "function" then
self.CurrentState.OnExit(self)
end
end
self.CurrentState = self.States[StateName]
-- Make sure the function exists
if typeof(self.CurrentState.OnEnter) == "function" then
-- Deferring this call so we don't get caught in a potential endless cycle
-- of GotoState -> State.OnEnter -> GotoState -> State.OnEnter
task.defer(function()
self.CurrentState.OnEnter(self, ...)
end)
end
end
local function NewBattle(Players, Enemies)
-- Moving the players and enemies into a single participants array
local Participants = table.clone(Players)
table.move(Enemies, 1, #Enemies, #Participants + 1, Participants)
return setmetatable({
Participants = Participants,
CurrentAttacker = Participants[1] -- Could even decide this in the StartBattle state
CurrentState = nil
States = {
StartBattle = StartBattleState,
SelectNextAttackers = SelectNextAttackersState,
ChooseAttacks = ChooseAttacksState,
PerformAttacks = PerformAttacksState,
DetermineWinner = DetermineWinnerState,
EndBattle = EndBattleState
}
}, TurnBasedBattle)
end
local Battle = NewBattle(Players, Enemies)
Battle:GotoState("StartBattle")
Obviously, you’d need to consider whether this works well within your current game’s architecture/framework, but hopefully it helps and gives you some ideas on how to implement it. I think finite state machines work really well in these types of situations.