This is the second post in an ongoing series about the development of my thesis project, Clockwork TD.

In the last entry, we covered A* Pathfinding for the enemies to find their way to the grid center (check out my Hex Grid Framework here). A major part of my thesis was a Tower Defense game where the enemy spawn point changed throughout the course of the game, requiring the player to counter that somehow. Using the Hex grid math I learned at redblobgames.com, I came up with a system where the spawner will move around each ring of the game map–similar to a clock–working its way from inside to out, giving the player the longer stretch of enemy path they’ll need as the game goes on and the number of enemies multiplies.

In this early version, the spawner simply lasts for one turn at each location, then the tile it’s standing on changes from a path tile to a buildable tile, giving the player an extra location to place a tower. When the spawn tile moves to a new ring, it spawns at the location closest to the last ring’s opening for the first round, to get the player acquainted to the change in the game.

I also built upon the camera system I created for Accumulus, which builds on Unity’s Cinemachine follow camera to move an invisible GameObject around the map with the player’s keyboard input. Here I added a mouse scrollwheel function for Zoom, which simultaneously moves the camera down and forward. Camera control is a huge sticking point for me in top-down games like this, so making sure I nailed my camera was very important to me. The camera control hooks into the GameState and CursorState systems to ensure it’s only active during the correct game phases.

The GameState and CursorState systems are both ScriptableObject-based FSMs; each State has Entered() and Exited() events that game managers and other objects can subscribe to, such as deleting all objects OnNewGameStart, or changing the bottom buttons (not shown yet in these gifs) when the player clicks the OnPlayerPrepared button to start the enemy attack phase.

public abstract class GameState : ScriptableObject
{
    public event Action Entered, Exited;
    [SerializeField] private bool debug;

    public void Enter(GameData gameData)
    {
        if(gameData.CurrentState) gameData.CurrentState.Exit(gameData);
        
        gameData.CurrentState = this;

        if (debug) Debug.Log(this + "::Enter() (" + gameData.CurrentState + ")");
        OnEnter(gameData);
        Entered?.Invoke();
    }

    public virtual void Execute(GameData gameData)
    { }

    protected virtual void OnEnter(GameData gameData)
    { }

    private void Exit(GameData gameData)
    {
        if (debug) Debug.Log(this + "::Exit() (" + gameData.CurrentState + ")");
        OnExit(gameData);
        Exited?.Invoke();
    }

    protected virtual void OnExit(GameData gameData)
    { }

    public void Return(GameData gameData)
    {
        gameData.PreviousState.Enter(gameData);
    }
}

By inheriting from the base state, strongly typed states can be defined that allow each state to have unique behavior and hold any necessary data. For example, the MainMenu state can automatically pause and resume the game when exited and entered:

[CreateAssetMenu (fileName = "State_MainMenu", menuName = "Game States/Main Menu", order = 51)]
public class MainMenuState : GameState
{
    private float oldTimeScale;
    
    protected override void OnEnter(GameData gameData)
    {
        base.OnEnter(gameData);
        
        oldTimeScale = Time.timeScale;
        Time.timeScale = 0;
    }

    protected override void OnExit(GameData gameData)
    {
        base.OnExit(gameData);
        Time.timeScale = oldTimeScale;
    }
}

In the next entry, I’ll cover how this same technique is used to define each of the states the game cursor can be in, which allows for straightforward, durable code flexible enough to handle Building, Moving, Selling, Upgrading, Hovering Over, and Clicking On the game’s Buildables.

Unity ScriptableObject-based Pooling Framework
Clockwork TD Devlog #3: CursorState FSM

2 Comments

  1. […] in the GameStates, CursorStates have Entered and Exited events that can be subscribed to, as well as OnEnter() and […]

  2. […] of the game, and not a bolted on afterthought. This sort of thinking is what led me use FSMs for GameState and CursorState, to ensure that I have the utmost control over the player’s game experience […]

Leave a Reply