Building a Speed-Based Turn Order System for a Tactics RPG
When building my tactics RPG in Godot, one of the first major systems I needed to tackle was turn order. I wanted combat where speed actually matters—faster units should act more frequently, not just go earlier in a fixed rotation. This post walks through how I implemented a speed-based turn order system using an accumulator pattern.

This is an early stage implementation that's working well for basic combat, but there's definitely room for improvement—particularly in areas like performance optimization, tiebreaker logic, and handling edge cases like speed buffs/debuffs.
The Core Concept
Instead of a simple round-robin system where everyone gets one turn per round, I use an accumulator-based approach:
- Each unit has a "turn value" that increases by their Speed stat every tick
- When a unit's turn value reaches 100 (the TURN_COST), they get to act
- After acting, 100 is subtracted from their turn value
- This continues in a simulation to predict the next 10 turns
This means a unit with Speed 12 will act roughly twice as often as a unit with Speed 6, creating meaningful strategic differences.
System Architecture
The turn order system involves several key components working together:
Key Components
TurnDetails (Battle/TurnDetails.cs) - The brain of the system:
- Calculates turn order queue
- Manages the current turn index
- Displays turn order UI (showing next 10 turns)
- Emits events when turns change
BattleUnit (Battle/BattleUnit.cs) - Individual unit state:
TurnValue: Persistent turn value (survives turn calculations)TempTurnValue: Working value during simulation_turnValueOnNextTurn: Snapshot for accurate UI predictionAttributes.Speed: The stat that determines turn frequency
BattleManager (Battle/BattleManager.cs) - Battle state machine:
- Receives
NextTurnReadyevents from TurnDetails - Sets
CurrentTurnUnitwhen a new turn begins - Manages overall battle flow through state transitions
BattleEventBus (Battle/BattleEventBus.cs) - Communication layer:
NextTurnReady- Fired when turn order determines next unitUnitTurnFinished- Triggers turn advancementUnitDied- Removes unit from turn orderBattleStateChanged- Notifies of state transitions
Turn Value Management
Each unit needs methods to manage their turn values:
// Increase turn value based on unit's speed public void AddSpeed() { workingTurnValue += speed; } // Subtract turn cost when unit takes a turn public void SpendTurnCost(int cost) { workingTurnValue -= cost; } // Save the current working value as the real turn value public void CommitTurnValue() { actualTurnValue = savedTurnValue; } // Reset working value to start fresh simulation public void ResetForSimulation() { workingTurnValue = actualTurnValue; }
These methods separate the "real" turn value from the "working" turn value used during simulation. This is helpful because I need to simulate future turns for the UI without affecting the actual battle state.
The Turn Order Algorithm

The heart of the system is calculating which units act when. Here's the simplified logic:
private void CalculateTurnOrder() { // Keep turns that already happened PreserveCompletedTurns(); // Reset all units to their actual turn values foreach (var unit in allUnits) { unit.ResetForSimulation(); } // Simulate future turns until we have 10 turns to display while (turnQueue.Count < 10) { // Check if anyone is ready to act (turn value >= 100) var readyUnits = allUnits .Where(unit => unit.workingTurnValue >= 100) .ToList(); if (readyUnits.Count > 0) { // Pick the unit with highest turn value (handles ties) readyUnits.Sort((a, b) => b.workingTurnValue.CompareTo(a.workingTurnValue)); var nextUnit = readyUnits[0]; // Add to queue and subtract the turn cost turnQueue.Add(nextUnit); nextUnit.SpendTurnCost(100); } else { // Nobody ready yet, increase everyone's turn value foreach (var unit in allUnits) { unit.AddSpeed(); } } } UpdateTurnOrderDisplay(); }
How It Works
- Preserve completed turns: Keep turns that have already happened
- Reset working values: All units start with their actual turn values
- Simulation loop:
- Check if any unit has turn value ≥ 100
- If yes: Add the unit with the highest value to the queue, subtract 100
- If no: Add each unit's Speed to their turn value
- Repeat until we've calculated 10 future turns
This simulation doesn't affect the actual battle state—it's just for predicting and displaying upcoming turns.
Turn Progression Flow
When a unit finishes their turn, here's what happens:
private void AdvanceToNextTurn() { // Recalculate turn order with new state CalculateTurnOrder(); // Move to next turn in queue currentTurnIndex++; // Save all units' turn values foreach (var unit in allUnits) { unit.CommitTurnValue(); } // Notify battle system that new turn is ready NotifyNextTurnReady(currentUnit); }
The battle manager listens for this notification:
OnNextTurnReady(unit => { currentTurnUnit = unit; battleState = BattleState.AwaitingAction; NotifyTurnStarted(); });
This sets the active unit and transitions the battle to await the player's action or trigger the AI.
Example: Speed Math in Action
Let's see how this plays out with three units:
- Mage (Speed: 8)
- Knight (Speed: 5)
- Thief (Speed: 12)
| Iteration | Mage | Knight | Thief | Who Acts? |
|---|---|---|---|---|
| Start | 0 | 0 | 0 | - |
| +Speed #1-8 | 64 | 40 | 96 | - |
| +Speed #9 | 72 | 45 | 108 | Thief acts! |
| After turn | 72 | 45 | 8 | - |
| +Speed #4-5 | 104 | 65 | 32 | Mage acts! |
| After turn | 4 | 65 | 32 | - |
| +Speed #7 more | 60 | 100 | 102 | Knight & Thief ready |
| Tiebreaker | - | - | - | Thief (102 > 100) |
In this scenario, the Thief acts first, then the Mage, then the Thief again—all before the Knight gets a single turn. This creates interesting dynamics where speed differences really matter.
Design Decisions
Accumulator Pattern vs. Initiative Rolls
I chose an accumulator over rolling initiative for each round because:
- Turn order is deterministic and predictable - players can plan ahead
- It creates meaningful speed differences - a few points of speed compound over time
- It's easier to visualize - the turn order UI shows exactly who goes when
- It supports future features like speed buffs affecting turn timing
Multiple Turn Value Variables
Using three different turn value variables might seem excessive, but each serves a purpose:
actualTurnValue: The "source of truth" - the real turn progressworkingTurnValue: The "workspace" - used for simulating future turnssavedTurnValue: The "snapshot" - locked in when predictions are confirmed
This separation has been helpful for keeping the actual battle state separate from UI predictions.
Tiebreaker Logic
When multiple units reach 100 simultaneously, the unit with the higher accumulated value goes first:
// Sort by turn value, highest first readyUnits.Sort((a, b) => b.workingTurnValue.CompareTo(a.workingTurnValue)); var nextUnit = readyUnits[0];
So if Unit A has 105 and Unit B has 102 (both over 100), Unit A goes first. This feels natural—they "earned" their turn slightly earlier.
Dynamic Recalculation
The turn order is recalculated whenever the battle state changes:
- Battle start
- Every turn completion
- Unit death
- Battle units updated
This ensures the turn order always reflects the current state, which will be especially important when I add speed buffs/debuffs.
Integration with Battle State Machine
The turn system integrates with the battle manager's state machine through events:
private void SetupBattleEvents() { // When turn order determines next unit OnNextTurnReady(unit => { currentTurnUnit = unit; battleState = State.AwaitingAction; NotifyTurnStarted(); }); // State transitions during a turn OnUnitBeginMove(() => battleState = State.SelectingMovement); OnUnitFinishMove(() => battleState = State.SelectingAction); OnUnitAttack(() => battleState = State.ExecutingAttack); // More state transitions... }
Each turn progresses through states: AwaitingAction → SelectingMovement → SelectingAction → ExecutingAttack → TurnComplete, then loops back for the next unit.
Visual Feedback
The turn order UI shows up to 10 future turns with visual indicators:
- Current unit highlighted with a margin offset
- Past turns shown semi-transparent (0.5 opacity)
- Color-coded by team (allies vs enemies)
- Icons representing each unit
This gives players strategic information—they can see exactly when they'll act again and plan accordingly.
Areas for Improvement
While this implementation has been working for me, I'm sure there are improvements to be made:
Performance Optimization
- The current system recalculates the entire turn queue every turn, which could be expensive with many units
- Could potentially cache calculations or use incremental updates
- The 50-iteration safety limit is arbitrary—could calculate exact number of iterations needed
Tiebreaker Refinement
- Current tiebreaker (highest turn value) works, but might want more sophisticated logic
- Could consider unit positioning, stat totals, or deterministic ordering by unit ID
- Edge case: what if two units have exactly the same turn value and speed?
Speed Buff/Debuff Support
- The foundation is there, but haven't fully implemented speed modification during battle
- Need to decide: do speed changes affect current turn value or only future accumulation?
- May need to recalculate turn order more intelligently when speed changes
Memory Efficiency
- Currently storing entire turn order list in memory
- For battles with many units, this could get large
- Could potentially only calculate next N turns on-demand
Testing Edge Cases
- What happens with very high speed disparities (Speed 1 vs Speed 20)?
- How does it handle units joining mid-battle?
- Does it behave correctly when all enemies are defeated partway through the turn queue?
UI Responsiveness
- Turn order recalculation happens synchronously on the main thread
- Could potentially move calculations to background thread for smoother performance
- Animation timing could be improved for better visual feedback
Related Systems
This turn order system interacts with several other battle systems:
- Event Bus (
Battle/BattleEventBus.cs): All turn changes flow through events for loose coupling - Battle Manager (
Battle/BattleManager.cs): Consumes turn events and manages overall battle flow - Character Attributes (
Character/Resources/AttributesData.cs): Defines the Speed stat that drives turn frequency - Battle Units (
Battle/BattleUnit.cs): Each unit tracks its own turn state and values
Conclusion
Building this speed-based turn order system has been a useful learning experience. The accumulator pattern creates meaningful speed differences in combat while remaining predictable for strategic planning. The separation between actual state and simulated state allows for accurate turn order previews without corrupting the battle state.
The system is functional for basic combat, but as I learn more about game development and optimization techniques, I'll continue refining it—especially around performance, edge cases, and handling dynamic speed changes.
If you're building something similar, I'd recommend starting simple with the core accumulator loop, then layering on features like UI prediction and tiebreakers once the fundamentals are solid.
Key Takeaways
- Accumulator patterns create dynamic, speed-based turn order
- Separate working state from actual state to enable safe simulation
- Event-driven architecture helps decouple turn system from battle management
- Visual feedback (showing 10 future turns) helps players plan strategically
- Start simple, add complexity incrementally as you learn what works
This post reflects my current implementation as of November 2025. I'm sure there are better ways to handle some of these systems, and I'm looking forward to learning more and improving them over time.
