Refactoring, UI, and Character Prefabs - Rooty Tooty's Next Steps

By Chad Nippard

Since the last blog, Rooty Tooty has gone through a major refactor and a wave of new features. This post is all about splitting up the TurnManager, introducing new scripts, and building a more robust gameplay UI. I've also been learning a lot about Unity best practices, especially around code architecture and variable visibility.

Splitting Up the TurnManager

The original TurnManager was doing far too much - handling player turns, actions, UI, and even some player management. As the project grew, it became clear that this approach was making things harder to debug and extend. To make the codebase more maintainable and modular, I split it into three new scripts:

  • ActionManager - Handles the queue and execution of player actions (move, attack, card, etc.), as well as tracking how many actions a player has left each turn.
  • GameplayUIManager - Responsible for updating the UI: showing player info, round number, action history (notifications), and managing the state of action buttons (attack, card, move, end turn).
  • PlayerManager - Keeps track of all player objects, their states, and handles spawning and switching between them.

This separation has made it much easier to reason about each part of the game, and scripts now communicate with each other through clear method calls and events. For example, when a player finishes their move, ActionManager notifies GameplayUIManager to update the UI, and PlayerManager to check if the turn should end.

ActionManager Example

The ActionManager is now the central hub for all player actions. It keeps track of how many actions a player has left, and ensures that once all actions are used, the turn ends automatically. Here's a simplified snippet:

// ActionManager.cs
private int MaxActionsPerTurn = 3;
private int actionsRemaining;
private bool[] actionsSpent = new bool[3];

public int ActionsRemaining => actionsRemaining;
public bool[] ActionsSpent => actionsSpent;

public event Action<int> OnActionsChanged;

public void StartTurn()
{
    actionsRemaining = MaxActionsPerTurn;
    for (int i = 0; i < actionsSpent.Length; i++)
        actionsSpent[i] = false;
    OnActionsChanged?.Invoke(actionsRemaining);
}

public void SpendAction(ActionType type)
{
    int idx = (int)type;
    if (actionsRemaining > 0 && !actionsSpent[idx])
    {
        actionsSpent[idx] = true;
        actionsRemaining--;
        OnActionsChanged?.Invoke(actionsRemaining);
    }
}

This approach has made it much easier to add new action types in the future, and keeps the logic for action limits in one place.

GameplayUIManager Example

The GameplayUIManager is responsible for keeping the player informed. It updates the UI whenever the game state changes, such as when a player uses an action or when the round advances. It also manages the game log, which shows up to five of the most recent actions:

// Adds an entry to the game log (shows up to 5 most recent entries)
public void AddLogEntry(string entry)
{
    gameLog.Add(entry);
    if (gameLog.Count > 5)
        gameLog.RemoveAt(0);

    if (logText)
        logText.text = string.Join("\n", gameLog);
}

By keeping UI logic out of the gameplay scripts and using a dedicated log, it's much easier to tweak the look and feel of the game without risking gameplay bugs. This approach also makes it simple to keep players up to date with the latest actions and events in the match.

PlayerManager Example

PlayerManager now handles all player objects, including spawning, switching, and tracking their state. This has made it much easier to support multiple players and to add new character prefabs:

private List<PlayerCharacter> players = new();
public IReadOnlyList<PlayerCharacter> Players => players;

public void SpawnPlayers(Vector2Int[] spawnCells, Transform gridTransform)
{
    // ...spawning logic
}

This modular approach means that adding new player types or changing how turns work is much less risky.

PlayerPanel and GameLoader updates

Another improvement is the introduction of a dedicated EventListener scene. By ensuring only one event listener is present in the game at any time, I've avoided the duplicate event handler errors that sometimes cropped up in earlier builds. This has made event management much more reliable and debugging a lot easier.

PlayerPanel Example

public void SetPlayerInfo(Sprite characterSprite, string playerName, int currentHealth, int maxHealth)
{
    if (characterImage) characterImage.sprite = characterSprite;
    if (playerNameText) playerNameText.text = playerName;
    if (healthText) healthText.text = $"HP: {currentHealth}/{maxHealth}";
}

These scripts have helped keep responsibilities clear and the codebase tidy, especially as the game's complexity grows. It's also made it much easier to debug UI issues, since each panel is responsible for its own updates.

Gameplay UI Overhaul

One of the biggest changes is the new gameplay UI. Players can now see their character info, the current round, a history of actions (notifications), and have dedicated buttons for attacking, playing a card, and moving. The move action now works by clicking a space and confirming the movement. Once all actions are used (3/3), the turn ends automatically, but players can also end their turn early with the end turn button.

Block out Town

This UI overhaul has made the game much more user-friendly and has helped me learn a lot about Unity's UI system, event-driven programming, and keeping UI logic separate from game logic. I've also started using events and delegates to keep the UI responsive to changes in the game state, which has been a great learning experience.

// Example: Notifying UI of action changes
public event Action<int> OnActionsChanged;

public void SpendAction(ActionType type)
{
    int idx = (int)type;
    if (actionsRemaining > 0 && !actionsSpent[idx])
    {
        actionsSpent[idx] = true;
        actionsRemaining--;
        OnActionsChanged?.Invoke(actionsRemaining);
    }
}

Character Prefabs and Visuals

Another big step forward: each character now has their own prefab. When spawning in the world, they appear as a placeholder capsule with their card art texture, making it easy to distinguish between characters. Andy has modelled Bobby Bullets, and I've added a skeletal mesh to that prefab, removing the capsule mesh renderer but keeping the component for collision - as is standard in Unity character setup.

Block out Town

This approach means we can quickly swap out placeholder art for final models as they're completed, and ensures that each character is visually distinct from the start.

Script Changes and Best Practices

Refactoring meant updating a lot of existing scripts to work with the new managers. I've started using [SerializeField] private instead of public variables, and where variables need to be accessed by other scripts, I've made them private with public read-only properties. This has improved encapsulation and made the codebase safer and easier to maintain.

private int actionsRemaining;
public int ActionsRemaining => actionsRemaining;

I've also been learning about the importance of clear responsibilities and communication between scripts. By using events and method calls, rather than direct variable access, the code is more robust and less prone to bugs when things change.

Learning and Next Steps

This iteration has been a real learning process - not just in Unity, but in software architecture. Splitting up responsibilities, using proper variable visibility, and building a modular UI has made the project feel much more professional. There's still plenty to do, but the foundation is much stronger now.

Next up: I'll be refining the turn phase system and introducing dedicated cards for each player, so actions will be based on the value and suit of the card drawn rather than just the basic attack, card, and move buttons. This will add a lot more variety and strategy, with different effects for low, high, face, and ace cards. I'm also planning to setup proper animations for Bobby Bullets created by Andy, and we're hoping to experiment with cloth physics for his neckerchief. As always, there's plenty to polish, but I'm excited to see how these new features will bring the game to life!