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:
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!