Tag: Scriptable Objects

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.

2023-07-14T19:43:46-04:00July 11th, 2023|Categories: Devlog, General|Tags: , , , , , , , , , |1 Comment

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: , , , , , , , |2 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

Updated Lightweight ScriptableObject Events in Unity

Since my original post on Lightweight ScriptableObject Events, I’ve made some minor variations to the version of code I’ve been using in production. I replaced the Lists with standard Event subscriptions (I like the way the += and -= syntax looks better than RegisterListener() and UnregisterListener(), plus it sticks out better so you know it’s an event subscription:

and updated sampled script:

Try it out and see which solutions works best for your project!

2023-06-03T19:22:34-04:00June 3rd, 2023|Categories: General|Tags: , , , , |0 Comments

Lightweight ScriptableObject Events in Unity

There’s loads of sources on this topic, even on Unity’s own Learn page! My goal with this code was to create a lightweight version that I can drop into any project if I need a quick event system.  Most of the code examples I’ve found online have been based around wiring up events using the Inspector, or having a one-to-one relationship between each Event and its Listener(s).  The system below replaces the GameListenerEvent middleman class from the Unity Learn example with the standard Action type, letting you assign your delegate for each Event Listener in your code. 

The code below creates base classes for Action, Action<T1>, and Action<T1,T2> delegates, similar to the UnityAction versions (which you could probably replace Action with, if you want to remove your dependence on the System library).  You can go ahead and add T3 and beyond if you find the need for them.

Use the standard OnEnable() => eventObject.RegisterListener(delegate); and OnDisable() => eventObject.UnregisterListener(delegate); syntax to register your Listeners.

This code was adapted from the code in the discussion located here.

using System;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseGameEvent : ScriptableObject
{
#if UNITY_EDITOR
[TextArea] public string developerDescription = "";
#endif
private readonly List<Action> listeners = new ();
public void Raise()
{
for (var i = listeners.Count 1; i >= 0; i)
{
listeners[i].Invoke();
}
}
public void RegisterListener(Action listener)
{
if (!listeners.Contains(listener)) { listeners.Add(listener); }
}
public void UnregisterListener(Action listener)
{
if (listeners.Contains(listener)) { listeners.Remove(listener); }
}
}
public abstract class BaseGameEvent<T1> : BaseGameEvent
{
private readonly List<Action<T1>> listeners = new ();
public void Raise(T1 t1)
{
for (var i = listeners.Count 1; i >= 0; i)
{
listeners[i].Invoke(t1);
}
}
public void RegisterListener(Action<T1> listener)
{
if (!listeners.Contains(listener)) { listeners.Add(listener); }
}
public void UnregisterListener(Action<T1> listener)
{
if (listeners.Contains(listener)) { listeners.Remove(listener); }
}
}
public abstract class BaseGameEvent<T1,T2> : BaseGameEvent
{
private readonly List<Action<T1,T2>> listeners = new ();
public void Raise(T1 t1, T2 t2)
{
for (var i = listeners.Count 1; i >= 0; i)
{
listeners[i].Invoke(t1, t2);
}
}
public void RegisterListener(Action<T1,T2> listener)
{
if (!listeners.Contains(listener)) { listeners.Add(listener); }
}
public void UnregisterListener(Action<T1,T2> listener)
{
if (listeners.Contains(listener)) { listeners.Remove(listener); }
}
}
/* USE:
*************
[CreateAssetMenu()]
public class GameEvent : BaseGameEvent {}
public class IntGameEvent : BaseGameEvent<int> {}
public class IntStringGameEvent : BaseGameEvent<int,string> {}
**************
*/
///
/// examples of the three components required to utilize the system:
///
// the Scriptable Object Event definition
[CreateAssetMenu]
public class CoinGameEvent : BaseGameEvent<int>
{}
// the MonoBehaviour on each Coin to collect
public class Coin : MonoBehaviour
{
// the Scriptable Object Event we created via the Create Asset menu
[SerializeField] private CoinGameEvent coinEvent;
// raise our coin event whenever the user picks up a coin
void OnTriggerEnter2D(Collider2D other)
{
coinEvent.Raise(1);
Destroy(gameobject);
}
}
// this can also be a ScriptableObject since they still have OnEnable() and OnDisable() methods
public class CoinManager : MonoBehaviour
{
int coins;
// register to Listen for the Coin Event
void OnEnable() => coinEvent.RegisterListener(AddCoins);
void OnDisable() => coinEvent.UnregisterListener(AddCoins);
// add the number of coins in the Event to our total
void AddCoins(int value) => coins += value;
}
2022-10-08T04:26:24-04:00October 7th, 2022|Categories: General|Tags: , , , |1 Comment

Projectile Arcing in Unity with Animation Curves

I recently had to figure out how to move a projectile in an Arc for a project I’m working on and found that a Google search was harder than expected, so I’m writing this up in that hopes that it might help someone else! For starters, take a look at this post on how to do it in 2D, as well as some explanation on the math behind it. For our purposes, we’re going to let Unity handle calculating the arc by simply using an Animation Curve. (Kind of hilariously, I spent part of the past weekend learning Animation Curves in Unity, only to have it be the subject of my next Maya class come Monday morning. Hindsight!)

This is the code I’m using, which is hyper-specific to my project, but we can break it down in principle. (I’ll cover ScriptableObject TypeObjects in a later post, but that’s what’s going on here; Projectile is a MonoBehaviour and the class below is a ScriptableObject)

  • Line 7: CurrentTime += Time.deltaTime in Update(). Duration is how long the projectile should take from firing to reach its target. linearT calculates the ratio from 0 to 1 of how close to done we are, given a desired Duration.
  • Line 9: PositionXZ() is an extension function I wrote that returns a Vector3 with its Y value set to 0. This function handles moving the projectile towards the target on the X and Z axes, using Lerp and linearT to interpolate smoothly.
  • Line 11: does the same thing but only for the Y axis. This ensures that we end up at the correct height for the target, in case the start and end Y positions are different.
  • Line 13: uses the same 0 to 1 value in linearT to evaluate the animation curve and return a height offset value. This is multiplied by curveMaxHeight to determine the effective height at the top of the parabola.
  • Line 15: adds the Lerped Y to the Offset Y to get the new effective Y. (If we’re Lerping from 0y to 0y, baseY will be 0y throughout, so the arc offset will determine the Y value alone.)

And here’s the animation curve stored in the curveHeight field:

You’ll want to manually clamp the points on the X axis at 0 and 1 to a value of 0. This ensures that at the start and end, no height is added to the projectile. Add a key point at 0.55 and set its value to .95. Set the Start and End tangents to linear; you want the projectile to shoot up towards the peak and come down towards the target quickly. In the middle, you can keep the point at Auto; grab the tangent handle and turn it clockwise so that the right hand side of the parabola is as close to a 45 degree angle as you can get it, which should push the peak to touching 1 on the Y axis.

This system works beautifully, because the offset for the mortar in the Animation Curve matches what you expect it to look like in the game, which I find makes it easier to wrap your head around how it works. You can play around with the tangents if you want to extend that hang at the top, but this works pretty well as is.

Looks pretty good, and no tricky math required… it should also be adaptable for other projectiles like rockets or arrows if you flatten out the Animation Curve. Try it out!

Edit 2022/08/20: I randomly stumbled across https://blog.terresquall.com/2019/11/coding-projectiles-for-your-tower-defense-game-part-2/ and thought it was a great explanation of the same concept; so for further reading, check it out.

2022-10-08T04:26:59-04:00July 21st, 2022|Categories: General|Tags: , , , , , |0 Comments

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