Unity ScriptableObject-based Pooling Framework

July 9th, 2023|Tags: , , , , , |0 Comments

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

Clockwork TD Devlog #1: Introduction

July 8th, 2023|Tags: , , , , , |0 Comments

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.

  • Computer coding on a screen

Updated Lightweight ScriptableObject Events in Unity

June 3rd, 2023|Tags: , , , , |0 Comments

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:

View this gist on GitHub

and updated sampled script:

View this gist on GitHub

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

Simple Unity Hex Grid Framework

May 30th, 2023|Tags: , , , , , |1 Comment

I’ve been using this gist in my last two projects to get an easy hex grid up and running quickly in Unity. I made some additions to it using the original source material at redblobgames.com, including an implementation of Amit’s A* Pathfinding algorithm (definitely one of my prouder comp-sci/math moments, even though I already did my own variation on A* back in Tower2Defense).

Here’s a video of the A* pathfinding in action (the enemies take damage randomly as they walk, no player towers yet in this video):

And some fun experiments with enumerating through the grid using Spiral, Ring & DistanceTo to create the game board’s pattern (the gif with enemies is actually using Unity’s AI Navigation package, which was not worth continuing to struggle with compared to the results the A* gives me now):

Finally, here’s the updated Hex.cs so you can check it out:
(Unity doesn’t include C#’s PriorityQueue class yet; you can use this direct port from the official C# lib as a drop-in replacement: https://github.com/FyiurAmron/PriorityQueue/blob/main/PriorityQueue.cs)

View this gist on GitHub

  • Programmer code on a screen

Lightweight ScriptableObject Events in Unity

October 7th, 2022|Tags: , , , |1 Comment

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;
}

Want to Work Together?

Tell me more about your project