tjcioffe

About onewinter

My name is TJ Cioffe and I’m from Brooklyn NY, where I currently live with my wife and our 3(!) cats. I spent the first 15 years of my career in the IT space as a sysadmin, db admin, web developer, and general jack-of-all-trades “IT guy”. As much as I loved that world, I realized that I needed to pivot to a new career that would allow me to use some of those skills while also doing something that I loved. As such, I enrolled in the MA program for Game Development at AAU. I don’t yet know what I want to focus on in my degree, but am excited to learn more and nail down an area of specialization as I continue! Avid Arsenal, Giants, and Knicks fan. Cat owner.

Clockwork TD Devlog #9: Towers

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

The final focus of this semester was on actually adding a whole host of Towers for the player to build! After starting with a very simple first three towers (Gun, Mortar, and Glue) simply to test and demonstrate the towers’ framework, I spent some time implementing my planned towers (again, tracked in Notion):

The first step was revamping the info screen the player sees when building or inspecting towers. The old screen was functional, but didn’t fully communicate the effects of Move, Upgrade, or the bonuses unlocked by new cards.

Reworking the screen in Figma (where else?), I was able to implement a similar version that also communicated the deltas for each stat (you’ll also see that each tower can have its default Targeting Priority overriden):

I spent some time on the whole base management system, adding visual and audio effects to each action to increase the feedback for the player. Once I was satisfied with all of the systems around Towers, I started implementing the list. As in before, I created a test scene in Unity to set each tower up; this also included setting up their projectile, muzzle flash, and impact effects to test them for scale and legibility:

Towers themselves use an FSM for each of their states (Idle, Reloading, Charging, Firing, Shoot Projectile)

public abstract class BaseTowerState
{
    protected const float ExitMultiplier = 2f;

    protected readonly Buildable Buildable;

    public TowerStates State;

    protected BaseTowerState(Buildable newBuildable)
    {
        Buildable = newBuildable;
    }

    protected abstract void OnEnter();
    protected abstract void OnExit();
    protected abstract void OnUpdate();
    protected abstract void OnFixedUpdate();
    protected abstract void CheckTransitions();
    protected abstract void CheckInterruptTransitions();

    public void FixedUpdate()
    {
        OnFixedUpdate();
    }

    public void Update()
    {
        // in every state, choose a new target if our current is invalid/out of range and we know we have targets avail
        //if (!Buildable.IsCurrentTargetInRange() && Buildable.AnyEnemyCollidersInRange()) Buildable.ChooseNewTarget();
        if (!Buildable.IsCurrentTargetInRange()) Buildable.ChooseNewTarget();

        // transitions that should keep update from running
        CheckInterruptTransitions();

        // what we do every frame
        OnUpdate();

        // should we transition to a new state
        CheckTransitions();
    }

    public void Enter()
    {
        //Debug.Log(TowerState);
        Buildable.CurrentState?.Exit();
        Buildable.CurrentState = this;

        OnEnter();
    }

    private void Exit()
    {
        OnExit();

        Buildable.CurrentState = null;
    }
}
public class TowerStateReloading : BaseTowerState
{
    private float reloadTimer;
    
    protected override void OnEnter()
    {
        reloadTimer = 0;
    }

    protected override void OnExit() { }

    protected override void OnUpdate()
    {
        reloadTimer += Time.deltaTime;
        
        // track the target
        Buildable.Turret.RotateForTarget(Buildable);
    }

    protected override void OnFixedUpdate() { }

    protected override void CheckTransitions()
    {
        // if we have no targets after reload, back to idle
        if(reloadTimer > Buildable.ReloadTime && !Buildable.AnyEnemyCollidersInRange())
            Buildable.States[TowerStates.Idle].Enter();
        
        // if we have been reloading for awhile and have no target, go back to idle
        if (!Buildable.IsCurrentTargetValid() && reloadTimer > Buildable.ReloadTime * ExitMultiplier)
            Buildable.States[TowerStates.Idle].Enter();
    }

    protected override void CheckInterruptTransitions()
    {
        // if we have a valid target and we finished reloading, move on to charging
        if (!Buildable.IsCurrentTargetValid()) return;
        if (Buildable.IsCurrentTargetLocked() && reloadTimer > Buildable.ReloadTime)
            Buildable.States[TowerStates.Charging].Enter();
    }

    public TowerStateReloading(Buildable newBuildable) : base(newBuildable)
    {
        State = TowerStates.Reloading;
    }
}
public class TowerStateShoot : BaseTowerState
{
    protected override void OnEnter() { }

    protected override void OnExit() { }

    protected override void OnUpdate()
    {
        // track target
        Buildable.Turret.RotateForTarget(Buildable);

        // don't shoot if not target locked
        if (!Buildable.IsCurrentTargetLocked()) return;
        
        // fire at the target
        var projectileSpawn = Buildable.Turret.FireWeapon();
        Buildable.TypeObject.ProjectileTypeObject.SpawnNewProjectile(projectileSpawn, Buildable, Buildable.CurrentTarget,
            Buildable.AttackPower.AsInt, Buildable.TypeObject.DamageTypeObject, Buildable.GetProjectileSpeed());
    }

    protected override void OnFixedUpdate() { }

    protected override void CheckTransitions()
    {
        // back to firing state (shoot once then exit)
        if (Buildable.FiringLengthPlusDelay == 0)
            Buildable.States[TowerStates.Reloading].Enter();
        // if we have a firing length, go back to Firing instead of Reloading
        else
            Buildable.States[TowerStates.Firing].Enter();
    }

    protected override void CheckInterruptTransitions() { }

    public TowerStateShoot(Buildable newBuildable) : base(newBuildable)
    {
        State = TowerStates.Shoot;
    }
}

Some example towers:

Lightning Tower
Shatter Tower
Barrage Tower

The goal is for each branch of towers to have a very different feel to them: Kinetic towers are slower, but deal good damage; Energy towers rely on firing quickly and occasionally bouncing lasers; explosive fire very slowly but do great AoE; Elemental towers focus on inflicting status effects to large groups of enemies; and Support towers focus on helping other towers shine, while doing low damage themselves. Check the game out and try out the 15 towers available!

2023-12-16T10:48:00-05:00December 15th, 2023|Categories: Devlog|Tags: , , , |0 Comments

Clockwork TD Devlog #8: Enemies

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

Creating simple enemies that can combine to create complex problems for the player to solve is the aim of most Tower Defense games. For Clockwork TD, I looked at a number of games, including Rogue Tower, Tower Tactics: Liberation, and even the recent Axon TD: Uprising to come up with inspiration for the different skills and conditions that enemies can utilize. To standardize the different conditions that could trigger an Enemy Action, I started with an enum:

public enum EnemyActionTypes
{
    Spawn = 0,
    Tick = 1,
    Damage = 2,
    FirstDamage = 3,
    NearTower = 4,
    NearDeath = 5,
    Death = 6
}

Each master Enemy type contains a UDictionary (code here) keyed to EnemyActionType; and containing an EnemyAction:

public abstract class EnemyAction : ScriptableObject
{
    public string UIDescription;
    
    [SerializeField] protected float tickLength;
    
    public virtual void RunAction(Enemy enemy){ }
}

This can be used to create all sorts of behaviour via inheritance, such as running an action on the caster:

public abstract class EnemyActionSelf : EnemyAction
{
    [SerializeField] protected GameFxSetup casterFx;

    public override void RunAction(Enemy enemy)
    {
        if (enemy.ActionTimer < tickLength) return;
        
        enemy.ActionTimer = 0;

        TickSelf(enemy);

        if(casterFx) casterFx.SpawnNewFx(enemy.ColliderCenter, enemy.transform);
    }
    
    protected abstract void TickSelf(Enemy enemy);
}

or running an action on an entire group around the caster:

public abstract class EnemyActionGroup : EnemyAction
{
    [SerializeField] protected float effectRadius;
    [SerializeField] protected GameFxSetup casterFx;
    
    [SerializeField] protected LayerMask enemyLayerMask;

    public override void RunAction(Enemy enemy)
    {
        if (enemy.ActionTimer < tickLength) return;
        
        enemy.ActionTimer = 0;

        var enemies = Physics.OverlapSphere(enemy.transform.position, effectRadius, enemyLayerMask);
        foreach (var collider in enemies)
        {
            if (!collider.TryGetComponent(out Enemy nearbyEnemy)) continue;

            TickAffectedEnemy(nearbyEnemy);
        }

        if(casterFx) casterFx.SpawnNewFx(enemy.ColliderCenter, enemy.transform);
    }

    protected abstract void TickAffectedEnemy(Enemy enemy);
}

or even simply dropping currency as a reward:

public class EnemyActionDropCurrency : EnemyAction
{
    [SerializeField] private GameData gameData;
    [SerializeField] private UDictionary<Currencies, int> bonuses;
    [SerializeField] private bool multiplyByRound;
    [SerializeField] private GameFxSetup gameFxSetup;

    public override void RunAction(Enemy enemy)
    {
        foreach (var (key, value) in bonuses)
        {
            var newValue = multiplyByRound ? gameData.GameCounters.Round.value * value : value;
            gameData.GameCounters.CountersDict[(GlobalCounters)key].value += newValue;
        }

        if (gameFxSetup) gameFxSetup.SpawnNewFx(enemy.ColliderCenter, enemy.transform);
    }
}

In the Enemy class, we refer to the master enemy setup (the TypeObject) to look up if we have an EnemyAction to run when certain events occur:

    private void OnDamage()
    {
        if (TypeObject.ActionsDict.TryGetValue(EnemyActionTypes.Damage, out var action)) action.RunAction(this);
        if (damaged) return;

        damaged = true;
        if (TypeObject.ActionsDict.TryGetValue(EnemyActionTypes.FirstDamage, out var faction)) faction.RunAction(this);
    }

    private void OnSpawn()
    {
        if (TypeObject.ActionsDict.TryGetValue(EnemyActionTypes.Spawn, out var action)) action.RunAction(this);
    }

    private void OnNearDeath()
    {
        nearDeath = true;
        if (TypeObject.ActionsDict.TryGetValue(EnemyActionTypes.NearDeath, out var action)) action.RunAction(this);
    }

All of the Enemies are laid out in Notion, with their stats, special abilities, and the models I’ve chosen for them (from the awesome Quaternius’ Ultimate Monsters Pack):

At some point, as continually balancing becomes more important, I may write a utility to import these values into the game from an Excel version of the file, but there are only 20 enemies still, so it’s not a huge lift to make the changes manually. Portraits are rendered using a simple scene set up specially for it in Blender with a tuned Camera and Overhead Lighting for depth:

Enemies are laid out in a separate test scene in Unity to ensure their model is sized and positioned appropriately when attached to the base enemy prefab:

Finally, the ScriptableObject representing the Enemy is filled out using the Notion stats, Blender portrait, and Model tweaked in the test scene:

Here’s the final result in action, with the red Rushnub applying Haste as it gets Near the Tower and the blue Wallfish applying Fortify.

2023-12-14T10:14:37-05:00December 13th, 2023|Categories: Devlog|Tags: , , , , |0 Comments

Clockwork TD Devlog #7: Map Progression Pt. 2

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

In previous devlog entries, I covered the initial development of the map. This fall, I got some good feedback from playtesters on gameplay progression, including a very important question: “why would you want to close spawn tiles?” To my mind, closing spawn tiles was sort of self-evident: you want more space between where the enemies spawn and your home tower, so of course you’d want to keep closing spawn tiles! However, I needed to add a mechanic to force the player to engage with this system–and so, the Pressure gauge was born. The Pressure gauge increasing makes the game harder, but increases player rewards; closing a spawn tile releases Pressure, letting the player have some breathing room again.

Initially, the Pressure gauge increased when the player did damage, and decreased slightly with each enemy spawned; the problem with that the player’s damage is wildly unforecastable (and increasingly so as the game goes on). Instead, the Pressure gauge simply increases with enemies spawned, which makes the max pressure amount much easier to grow consistently as the game goes on.

But what to do with Pressure? At the time, the player received choices from two drawn card hands at the end of each round; the higher the Pressure, the higher the minimum reward rarity of the cards drawn (more pressure, better rewards). This could tend to lead to mindless clicking at the end of each round, though, as players eagerly attempted to get to the next enemy attack phase.

So, let’s slow the player down: give them a mini-store at the end of each round, and let them choose which areas they want to strengthen with their available currency (this is a roguelike, after all!)

For the players who’d rather just build more towers, let’s give them a currency exchange too:

The effects of Pressure itself went through multiple iterations: at first, it simply changed the number of enemies spawned. However, this generally wouldn’t be enough to change the math on a player’s defenses, and would simply lead to the player racking up a huge amount of gold as they mowed enemies down. I briefly flirted with having it also affect the spacing between spawns, which would disrupt the timing of the player defenses; but this just led to the lanes looking like a muddled mess once Pressure was high enough, and also negatively impacted performance (too many colliders too close together, is my guess). Finally, I settled on using the same stat mechanics as the enemies themselves do: boosting HP & speed. This, coupled with a toned down version of the spacing changes and the increased spawn count changes, led to a system where the difference Pressure makes slowly creeps up until all of a sudden you bite off more than you can chew and are punished for it. At that point, if you manage to survive, you will definitely be closing spawn tiles and relieving Pressure! If not, then you’ll have a better feel for how Pressure affects the game on your next run.

The final changes with Pressure involve the spawners themselves; original Pressure iterations had Pressure above 67% and 95% leading to the player dealing with 2 and 3 enemy spawners, respectively. This was both an attempt to keep the game from getting too predictable with a single spawner, and another way to try and make the player feel the effects of too much Pressure. Eventually, however, I realized that this locked multi-spawners away behind a Pressure level that some players may never experience. Instead, multi-spawn is now a counterbalance to sealing spawn tiles and growing the map; each time the player pushes the map out a level, the number of spawners is permanently increased by one. So, constantly avoiding dealing with Pressure means that you’ll instead have to deal with more attack vectors–so pick your poison!

2023-12-13T09:54:49-05:00December 11th, 2023|Categories: Devlog|Tags: , , , |0 Comments

Clockwork TD Devlog #6: Enemy Info

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

An important part of Tower Defense games is clearly communicating to the player the sort of threat they’re facing.

This would look better grouped, right?

It’d be great if the player could see even more info on the enemy, like Health, Speed, and any special abilities:

This was a short update, but I fell out of the habit over the last two months, so I needed to get these gifs out into the world! I may try to make this devblog more contemporaneous each week as part of my workflow, so let’s see how it goes 🫠

2023-09-11T18:24:52-04:00September 11th, 2023|Categories: Devlog, General|Tags: , , , |0 Comments

Clockwork TD Devlog #5: Terrain Bonuses

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

Terrain bonuses, ala Rogue Tower, were something I decided I wanted early on in order to add to the randomness of each playthrough. Tiles are simply assigned a random TileBonus from 0-3 at creation, then moved up and color lerped in order to match, while the CursorState uses this Bonus value to generate the floating text on hover:

Bonus = Random.Range(0, 4);
transform.localScale = Vector3.one + Vector3.up * (Bonus * .5f + 1f);
var color = Color.Lerp(renderer.material.color, Color.white, Bonus / 5f);
renderer.material.color = color;

Stats in the game use the SeawispHunter.Roleplay.Attributes package, which provides an amazing framework for working with all sorts of RPG stats. The Tower Bonus is assigned at time of tower placement; this pattern is used for all Upgradeable stats throughout the game, allowing trickle-down from the Tower Setup to the individual tower level.

// add 1% to each stat for each level of tile bonus
TileBonus = new ModifiableValue<int>(0);
var rangeMod = TileBonus.Select(x => 1f + x / 100f);
AttackRange.modifiers.Add(Modifier.Times(rangeMod));
AttackSpeed.modifiers.Add(Modifier.Times(rangeMod));
AttackPower.modifiers.Add(Modifier.Times(rangeMod));

I had used Kryzarel’s CharacterStat framework in previous RPG stat projects, but found the SeawispHunter package at the start of this project. It’s really powerful and flexible, and highly recommended! Next time around, we’ll take a look at generating / showing the next enemy wave.

2023-07-18T16:57:05-04:00July 18th, 2023|Categories: Devlog, General|Tags: , , , , |0 Comments

Clockwork TD Devlog #4: Unlocks & Dynamic Build Menus

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

One thing that I knew would be of huge importance before I even started designing my game was the UI. Many games in the Tower Defense genre focus on depth and breadth of systems in lieu of a coherent UI, which makes no sense — tower defense games are meant to suck the player in and keep them hitting the Next Turn button, so why would you not concentrate on creating as frictionless an experience for your player as possible? As such, I knew that designing the UI had to be a major part 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 at any given time. My UI and my GDD were created in a back-and-forth iterative process, as I worked out gameplay ideas on paper and then tried to imagine how they’d translate to a UI in Figma.

By taking the time to create a full style & color guide from the beginning, it allowed me to very easily combine building blocks in Figma to create a coherent UI language.

Like the Command Manager, the Unlock Manager is another ScriptableObject, as it has no per-frame logic to execute. Unlockables are another abstract ScriptableObject class; by providing virtual methods for OnUnlock and OnReset, we can override UnlockableBase to Unlock all sorts of objects in the game, whether a Damage Bonus or New Buildable Tower:

public abstract class UnlockableBase : ScriptableObject
{
    public UnlockableBase LockedBy;
    public bool Unlocked;
    [SerializeField] private bool unlockedToStart;

    public bool Unlockable => !Unlocked && (!LockedBy || (LockedBy && LockedBy.Unlocked));
    
    protected abstract void OnUnlock();
    protected abstract void OnReset();
    
    public void UnlockItem(bool force = false)
    {
        if (!force && !Unlockable) return;
        
        Unlocked = true;
        OnUnlock();
    }

    public void ResetItem()
    {
        Unlocked = unlockedToStart;
        OnReset();
    }

    private void OnEnable()
    {
        ResetItem();
    }
}
public class UnlockableBuildable : UnlockableBase
{
    [SerializeField] private BuildableSetup buildableSetup;

    protected override void OnUnlock()
    {
        buildableSetup.Unlock(true);
    }

    protected override void OnReset()
    {
        buildableSetup.Unlock(Unlocked);
        buildableSetup.Reset();
    }
}
public class UnlockableTowerBonus : UnlockableBase
{
    [SerializeField] private BuildableSetup buildableSetup;
    [SerializeField] private BonusTypes bonusType;
    
    protected override void OnUnlock()
    {
        switch (bonusType)
        {
            case BonusTypes.Power:
                buildableSetup.PowerBonus.initial.value++;
                break;
            case BonusTypes.Range:
                buildableSetup.RangeBonus.initial.value++;
                break;
            case BonusTypes.Speed:
                buildableSetup.SpeedBonus.initial.value++;
                break;
            case BonusTypes.Health:
                buildableSetup.HealthBonus.initial.value++;
                break;
            case BonusTypes.Armor:
                buildableSetup.ArmorBonus.initial.value++;
                break;
            case BonusTypes.Shield:
                buildableSetup.ShieldBonus.initial.value++;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    protected override void OnReset()
    {
        buildableSetup?.Reset();
    }
}

This lets us query our list of Unlockables to find out what’s Unlocked, what’s Unlockable, and what’s still Locked; which can then be used to create the build menu using UXML classes that inherit from Button to pass data:

void ToggleBuildMenu(ClickEvent evt, BuildableType buildableType)
{
	currentBuildableType = buildableType;
	secondContainer.Clear();
	buildableButtons = new List<BottomButton>();
	foreach (var buildableSetup in buildableType.GetBuildables(gameData.BuildableSetups))
	{
		var newButton = new BottomButton();
		newButton.Init(buildableSetup);
		
		newButton.AddClickEvent(BuildableButtonClicked, buildableSetup);
		if(!buildableSetup.Unlocked) newButton.SetEnabled(false);
		
		buildableButtons.Add(newButton);
		secondContainer.Add(newButton);
	}
	buildableTypeButtons.ForEach(b => b.EnableInClassList("active", false));
	buildableTypeButtons.FirstOrDefault(b => b.ButtonType == buildableType)?.EnableInClassList("active", true);
}

This same data can also be used to unlock new cards at the end of every round (more on this in a later post, covering card pack minimum rarity levels):

The damage numbers here showing off the game’s first splash damage projectile are created with a TMPro Billboard shader I found on github, combined with the rising number effect from Accumulus. The UnlockManager calls Reset on all Unlockables at the start of each new game, ensuring that all Unlockable items start each new game fresh.

By focusing on the end result I wanted the gameplay & UI experiences to have early on in the process, I was able to use my GDD and Figma to create a coherent, functional UI in my game even this early in the development process. Come back next time for info on terrain bonuses and the enemy wave info screen!

2023-07-14T19:48:40-04:00July 14th, 2023|Categories: Devlog, General|Tags: , , , , , , |0 Comments

Clockwork TD Devlog #3: CursorState FSM

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

Rather than define separate game states Building, Moving, Selling, etc., I chose to create a separate FSM for the cursor state. Like the game states, each of the cursor states is strongly defined as a ScriptableObject:

[Header("Game States")]
public MainMenuState StateMainMenu;
public GameStartState StateGameStart;
public PlayerPreparationState StatePlayerPreparation;
public EnemyAttackState StateEnemyAttack;
public GameOverState StateGameOver;

[Header("Cursor States")] 
public CursorDefault CursorStateDefault;
public CursorBuilding CursorStateBuilding;
public CursorSelling CursorStateSelling;
public CursorMoving CursorStateMoving;
public CursorUpgrading CursorStateUpgrading;

This makes it easy to add new game & cursor states and also to access them from anywhere in the project.

The base CursorState includes abstract & virtual methods to override, depending on the desired behavior of the current state:

public abstract void TileClicked(Tile tile);
public abstract void TileEntered(Tile tile);
public abstract void TileExited(Tile tile);
public abstract void BuildableClicked(Buildable buildable);
public abstract void BuildableEntered(Buildable buildable);
public abstract void BuildableExited(Buildable buildable);

protected void Enter()
{
	if(gameData.CurrentCursor) gameData.CurrentCursor.Exit();
	
	gameData.CurrentCursor =  this;

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

protected virtual void OnEnter(){ }

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

protected virtual void OnExit(){ }

protected virtual void Cancel()
{
	gameData.CursorStateDefault.Enter();
}

As in the GameStates, CursorStates have Entered and Exited events that can be subscribed to, as well as OnEnter() and OnExit() methods for inheriting CursorStates to override. Buildable and Tile both implement the “IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler” interfaces, calling back to the current CursorState during those mouse events. This allows the code for say, Building to be fairly straightforward (incomplete code but you can get the gist — oh I just got why github named it that):

public void SetNewBuildable(BuildableSetup newTower)
{
	currentBuildable = newTower;
	ShowInfoPanel(newTower);
	
	if (gameData.CurrentCursor != this) Enter();
}

protected override void OnEnter()
{
	base.OnEnter();
	
	InputManager.EscapePressed += Cancel;
	InputManager.RightClickPressed += Cancel;
}

protected override void OnExit()
{
	base.OnExit();
	
	HideGhostTower();
	HideBonusText();
	HideRangefinder();
	HideInfoPanel();
	currentBuildable = null;
	
	InputManager.EscapePressed -= Cancel;
	InputManager.RightClickPressed -= Cancel;
}

public override void TileClicked(Tile tile)
{
	if (currentBuildable.Cost > currentBuildable.Currency.Value) return;
	
	var buildCommand = new BuildCommand(currentBuildable, tile);
	gameData.CommandManage.AddCommand(buildCommand);

	HideGhostTower();
	HideBonusText();
	HideRangefinder();
	
	if(!InputManager.ShiftHeld) Cancel();
}

public override void TileEntered(Tile tile)
{
	
	ShowGhostTower(currentBuildable, tile);
	ShowBonusText(tile);
	ShowRangefinder(tile, currentBuildable.AttackRange.value * (1 + (tile.Bonus / 100f)));
}

public override void TileExited(Tile tile)
{
	HideGhostTower();
	HideBonusText();
	HideRangefinder();
}

public override void BuildableClicked(Buildable buildable) { }
public override void BuildableEntered(Buildable buildable) { }
public override void BuildableExited(Buildable buildable) { }

Here’s the initial results for the various states:

The Undo queue is a fairly simple Command Stack, with everything routing through a ScriptableObject CommandManager (for Managers that don’t require pertick functionality, I tend to take the extra step to make them ScriptableObjects). Here’s that class, along with the MoveCommand:

public class CommandManager : ScriptableObject
{
    public event Action QueueChanged; 
    public int Count => commandsBuffer.Count;
    private Stack<ICommand> commandsBuffer = new();

    public void AddCommand(ICommand command)
    {
        command.Execute();
        commandsBuffer.Push(command);
        QueueChanged?.Invoke();
    }
    
    // undoes the last command
    public void UndoCommand()
    {
        if(commandsBuffer.Count == 0) return;
        commandsBuffer.Pop().Undo();
        QueueChanged?.Invoke();
    }

    // undoes all the commands in reverse order
    public void UndoAllCommands()
    {
        if(commandsBuffer.Count == 0) return;
        while (commandsBuffer.Count > 0)
        {
            commandsBuffer.Pop().Undo();
        }
        QueueChanged?.Invoke();
    }

    // executes each tower's cleanup command and clears the stack to finalize the commands
    // (only used to cleanup after sell commands right now)
    public void FinalizeCommands()
    {
        while (commandsBuffer.Count > 0)
        {
            commandsBuffer.Pop().Commit();
        }
        ClearCommands();
    }

    public void ClearCommands()
    {
        commandsBuffer.Clear();
        QueueChanged?.Invoke();
    }

}
public interface ICommand
{
    void Execute();
    void Undo();
    void Commit();
}
public class MoveCommand : ICommand
{
    private readonly Buildable commandObject;
    private readonly Tile originalTile;
    private readonly Tile newTile;
    
    public MoveCommand(Buildable buildable, Tile original, Tile target)
    {
        commandObject = buildable;
        originalTile = original;
        newTile = target;
    }

    // move the Buildable, cleanup the Tile statuses
    public void Execute()
    {
        originalTile.MarkBuilt(false);
        commandObject.transform.position = newTile.GetTopOfTile();
        commandObject.HomeTile = newTile;
        commandObject.TileBonus.initial.value = newTile.Bonus;
        newTile.MarkBuilt(true);
    }

    // move the Buildable back, revert the Tile statuses
    public void Undo()
    {
        originalTile.MarkBuilt(true);
        commandObject.transform.position = originalTile.GetTopOfTile();
        commandObject.HomeTile = originalTile;
        commandObject.TileBonus.initial.value = originalTile.Bonus;
        newTile.MarkBuilt(false);
    }

    // no cleanup necessary
    public void Commit() {}
}

Stay tuned for the next installment, which will probably cover some UI elements.

Clockwork TD Devlog #2: Gameplay Progression & FSMs

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.

2023-07-14T19:44:08-04:00July 10th, 2023|Categories: Devlog, General|Tags: , , , , , , , |3 Comments

Unity ScriptableObject-based Pooling Framework

Brief break from Clockwork TD devlog updates today, as I published my first Unity toolset on github! Over the past year, I’ve been using Unity’s Object Pooling feature, which is flexible and powerful, but requires a lot of boilerplate code to set up.

Around the time of Ron Swansong, I took the time to formally standardize my pooling code into a framework, which cut down my code repetition and codebase size nicely. With Accumulus, I kept tweaking and improving it, and finally after two months of development during Clockwork TD, the framework is ready to be shared with the world today.

It’s meant to be flexible but also lightweight, requiring just a few lines of code and two inheriting classes to get going. However, it allows you to override methods at every stage in the object pooling lifecycle, letting you use the framework in whatever way your project requires. Check it out and see what you think!

https://github.com/onewinter/ScriptObjPoolingFramework

2023-07-10T08:36:07-04:00July 9th, 2023|Categories: General|Tags: , , , , , |0 Comments

Clockwork TD Devlog #1: Introduction

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

For this new series, I’ve decided to start a dev blog for my MFA thesis project, Clockwork TD (working title!). I’ll be developing it over the next year as I work on finishing my MFA, and hope to polish it enough to release it (in Early Access, at least) next summer on Steam!

This project started off as a thought experiment — what makes a Tower Defense game? For awhile, I tried to go down the road of Citadelic and do away with enemy paths altogether, using Citadelic’s Move mechanic to let the player readjust their defenses each round, but 1. that felt tedious and 2. it opened up questions around enemy pathfinding. I tried to persist with the idea that enemies would “bash down” towers in their way, making adjusting the enemy path via placing towers part of the game’s strategy (ala Defense Grid: The Awakening), but a tower defense game where the enemies can damage your towers feels like it’s getting too close to an RTS. Similarly, all of the solutions I could think of to help a player cope semi-comfortably with a lack of defined enemy paths also started slanting the game towards an RTS. While Diplomacy is Not an Option was a big influence in my thinking (especially before discovering Citadelic), that game is ultimately a base-building RTS, not a Tower Defense game.

So how then, to achieve a Tower Defense game with a dynamic map like Rogue Tower or Isle of Arrows? I tried to answer this question last year when making Voxel Defense, but eventually found that asking the player to build their own enemy path quickly got tedious, as fun as it could be to create the ultimate winding killbox. When thinking about this game, I had just finished working on Accumulus, which gave me experience with hex grid math and made me realize that hexagons as tiles are far more interesting than cubes. Originally, I thought of using Perlin or other noise to generate the map like I did in Ron Swansong, but I didn’t feel like spending ages trying to massage procedural generation algorithms enough to give me safe, reliable randomness; and even so, it felt like I was falling backwards into “this feels like an RTS” territory again.

Eventually, after spending an inordinate amount of time on redblobgames’ hex math page, I realized the answer was somewhere in the hex math itself — that I should be using diagonals, rings, etc. to “carve” my enemy path out of the playing space (the final effect in the current build is even cooler than this):

A big part of my thesis proposal was using Unity’s AI Navigation system (as seen in use by the red cylinder above) for simple enemy pathfinding, but its tendency to get jumpy, combined with the finicky nature of AI NavMesh (even in the 2022 LTS) meant that it was back to redblobgames and learning A* Pathfinding for the first time (finally!). Once I got that down, the march of the monsters could begin!

Finally, the last element of my dynamic path: moving the enemy spawners around the map! Once this mechanic was in, I dropped the first working title (the very dry Hex Vector Defense) and went with Clockwork TD to reflect the way the spawners move around the (hex) circle (shout out to 75th Hunger Games for a little inspiration, heh)

More to come in the next entry — I’m about 30 gifs behind still, even after the handful here!

You can check out the latest Clockwork TD build at itch.io.

2023-07-14T19:44:28-04:00July 8th, 2023|Categories: Devlog, General|Tags: , , , , , |1 Comment

About My Work

Phasellus non ante ac dui sagittis volutpat. Curabitur a quam nisl. Nam est elit, congue et quam id, laoreet consequat erat. Aenean porta placerat efficitur. Vestibulum et dictum massa, ac finibus turpis.

Recent Works

Recent Posts