Hi there! It’s been a while since we last saw each other! Since my last blog post, I’ve upgraded the project to Godot 4, taken a long vacation, and implemented three new helper systems. Let’s take a quick look at these new systems!
Coroutines
Coroutines are a type of function that can be suspended during its execution and continued later on. I find them extremely useful in all kinds of game logic, mostly – game AI. It’s one of the features in Unity that I use almost in every project so it’s no wonder that I wanted something like that Lurking Behind as well. I’ve found some implementations, but I felt like writing my own, since it’s not too complicated and adds some extra things.
The base of this is the IEnumerator, which allows the suspension of the execution. And the core principle is to gather all of the coroutines in a single array and iterate over them every game tick. If a coroutine needs a time-based pause it can return WaitSeconds class. Initially, I was accounting for ints, doubles and floats, but decided against it for simplicity reasons – now instead of having to account for every numeric type, I expect a simple wrapper around a double (which I chose because the delta time coming into the tick function is also a double).
The main array that holds all of the coroutines is initialized with a fixed size and has a separate index to show where the new element should be written into. I did this to avoid unnecessary resizing of the array when adding or removing the elements. And when I want to terminate the coroutine, I just switched it with the last active coroutine.
I’ve also added a small test to check if everything works fine, though it’s usually compiled out.
This is the code for my coroutine at the time of writing:
//#define CoroutineTest
using Godot;
using System.Collections;
using System;
public class WaitSeconds
{
public WaitSeconds(double secondsToWait)
{
Seconds = secondsToWait;
}
public double Seconds;
}
public partial class CoroutineManager : Godot.Node
{
private class CoroutineData
{
public object Owner { get; set; }
public IEnumerator Coroutine { get; set; }
public WaitSeconds SecondsToWait { get; set; }
};
// Array size
private const int ArraySize = 1024;
private static CoroutineData[] _coroutines;
private static int _nextCoroutineIndex;
public override void _Ready()
{
_coroutines = new CoroutineData[ArraySize];
_nextCoroutineIndex = 0;
#if CoroutineTest
TestCoroutineHandle = DoMyCoroutine();
CoroutineManager.StartCoroutine(this, TestCoroutineHandle);
#endif
}
public override void _Process(double delta)
{
for (int i = 0; i < _nextCoroutineIndex; i++)
{
var coroutineData = _coroutines[i];
if (coroutineData.SecondsToWait.Seconds > 0f)
{
coroutineData.SecondsToWait.Seconds -= delta;
if (coroutineData.SecondsToWait.Seconds > 0f)
{
continue;
}
}
if (coroutineData.Owner == null || !coroutineData.Coroutine.MoveNext())
{
// The owner is no longer valid or the coroutine has ended
// therefore scrap the
// coroutine by no longer iterating over it
// and overwriting it on the next add and just continue
// as per usual.
ScrapCoroutineAt(i);
coroutineData = _coroutines[i];
}
HandleCoroutineReturnResult(ref coroutineData);
}
}
public static void StartCoroutine(object owner, IEnumerator coroutine)
{
if (_nextCoroutineIndex > _coroutines.Length)
{
Array.Resize(ref _coroutines, _coroutines.Length + ArraySize);
}
var newCoroutineData = new CoroutineData()
{
Owner = owner,
Coroutine = coroutine,
SecondsToWait = new WaitSeconds(0)
};
HandleCoroutineReturnResult(ref newCoroutineData);
_coroutines[_nextCoroutineIndex++] = newCoroutineData;
}
public static void StopAllCoroutines()
{
_nextCoroutineIndex = 0;
}
public static void TerminateCoroutine(IEnumerator coroutine)
{
for (int i = 0; i < _nextCoroutineIndex; i++)
{
if (_coroutines[i].Coroutine.Equals(coroutine))
{
ScrapCoroutineAt(i);
break;
}
}
}
private static void ScrapCoroutineAt(int index)
{
--_nextCoroutineIndex;
(_coroutines[index], _coroutines[_nextCoroutineIndex]) =
(_coroutines[_nextCoroutineIndex], _coroutines[index]);
}
private static void HandleCoroutineReturnResult(ref CoroutineData coroutineData)
{
switch (coroutineData.Coroutine.Current)
{
case WaitSeconds secondsToWait:
coroutineData.SecondsToWait = secondsToWait;
break;
default:
break;
}
}
#if CoroutineTest
private IEnumerator TestCoroutineHandle;
private IEnumerator DoMyCoroutine()
{
GD.Print("First print");
yield return null;
GD.Print("Wait 4 seconds");
yield return new WaitSeconds(4.0);
GD.Print("4 seconds passed!");
CoroutineManager.StartCoroutine(this, SecondCoroutine());
while (true)
{
yield return new WaitSeconds(1.0);
GD.Print("ping");
}
}
private IEnumerator SecondCoroutine()
{
GD.Print("Will kill the first coroutine after 5 seconds");
yield return new WaitSeconds(5.0);
CoroutineManager.TerminateCoroutine(TestCoroutineHandle);
GD.Print("Goodbye!");
}
#endif
}
If you want to check the latest version of this code – check out my GitHub little helpers!
Publishers and subscribers
Many years ago (4 years ago from the time of writing) I encountered ROS – Robot Operating System. I liked how it allowed multiple systems to communicate with each other without actually knowing about each other. The system consists of messages, publishers and subscribers. Messages are the data that is being sent, publishers are the ones that send out the data and the subscribers are the ones that receive the data. When initializing both the publishers and the subscribers declare what topic/channel they are subscribing to, this helps prevent subscribers from getting messages they are not meant to handle. There are, obviously, more things to the system, like buffering, but in the case of my implementation, I was only concerned with the basics for now. I tried using the terminology from ROS mostly because I think it makes sense, though I did name my system Postbox, mostly because calling it Messaging System or something like that felt boring to both think about and type out. Unlike the coroutines, this stands on its legs.
The core principle is that there’s a key-value collection. The key consists of two parts: the channel and the message type. Ensuring that only the same type of message is received as the one that was sent eliminates the need to have to account for the mismatch in the subscribed functions. When subscribed a Subscriber variable is returned, which can be used to unsubscribe from the Postbox. I was initially thinking of returning just a handle, which can be passed into an Unsubscribe function, but decided against it when I added the callback variable type into the key which would make the lookup a bit more intensive. When a publisher broadcasts the message the topic and the message type are used for a quick look-up in the map after which every subscriber is triggered and their delegate functions are called.
The function of this system is the observer pattern just without the need to know what you are subscribing to, which is very helpful in situations like tracking enemy kills. Without this system, every time an NPC dies it would have to notify every system about its death like the player experience points tracker, quest tracker and achievement tracker and if a new system is added this notification has to be added manually in all relevant behaviours.
This is the initial code that I wrote:
using System;
using System.Collections.Generic;
using System.Linq;
// If there was a need for load balancing a coroutine could be used, otherwise,
// no need for inheriting from Godot.Node;
public static class Postbox
{
public partial class Key_Internal
{
public MessageTopics Topic { get; set; }
public Type PayloadType { get; set; }
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
return obj is Key_Internal otherKey
? otherKey.Topic.Equals(Topic) && otherKey.PayloadType.Equals(PayloadType)
: false;
}
public override int GetHashCode() => HashCode.Combine(Topic, PayloadType);
}
public partial class Value_Internal
{
public int Handle { get; set; }
public Action<object> Callback { get; set; }
}
// Main reason for this class is to avoid having to go through
// every entry inside the dictionary since the keys are made out of
// topics and the types
public partial class Subscriber
{
private Key_Internal _storedKey;
private int _handle;
static Subscriber()
{
// Yoinked from
// https://stackoverflow.com/questions/1664793/how-to-restrict-access-to-nested-class-member-to-enclosing-class
HiddenSubscriberConstructor = (x, y) => new Subscriber(x, y);
}
private Subscriber(Key_Internal key, int handle)
{
_storedKey = key;
_handle = handle;
}
public void Unsubscribe()
{
_subscribers[_storedKey].RemoveAll(x => x.Handle == _handle);
}
}
private static Dictionary<Key_Internal, List<Value_Internal>> _subscribers =
new Dictionary<Key_Internal, List<Value_Internal>>();
private static int _handleCounter = -1;
private static Func<Key_Internal, int, Subscriber> HiddenSubscriberConstructor;
public static void Publish<T>(MessageTopics topic, T payload)
{
// This should allow for different payloads to be reached on the same topic.
var key = new Key_Internal() { Topic = topic, PayloadType = typeof(T) };
if (_subscribers.ContainsKey(key))
{
_subscribers[key].ForEach(x => x.Callback(payload));
}
}
public static Subscriber Subscribe<T>(MessageTopics topic, Action<T> callback)
{
var key = new Key_Internal() { Topic = topic, PayloadType = typeof(T) };
// Wraps the callback into a lambda and stores that instead.
// Yoinked from https://stackoverflow.com/questions/3444246/convert-actiont-to-actionobject
var value = new Value_Internal()
{
Handle = ++_handleCounter,
Callback = (Action<object>)(o => callback((T)o))
};
if (!_subscribers.ContainsKey(key))
{
_subscribers.Add(key, new List<Value_Internal>() { value });
}
else
{
_subscribers[key].Add(value);
}
if (HiddenSubscriberConstructor is null)
{
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Subscriber).TypeHandle);
}
return HiddenSubscriberConstructor(key, value.Handle);
}
}
public enum MessageTopics
{
DamageDealt,
}
And similarly to the Coroutines, you can find the latest public version here!
Game saves
Some time ago, during one of my scrolls through Twitter, I read a thread about how important it is to write down the game save/load system early on in the dev cycle. The primary reason was that it is a good idea to track the data you want to save early on, as that could become quite troublesome later on. So I did. To be fair, there isn’t much to talk about. I used Godot’s File system (you can read about it here) and stored variables such as the current level, the RNG Seed, the RNG state, player class, current dungeon size and power level. Additionally, the save state loading was also added. I did encounter a small bump there, apparently, Godot’s special dictionary wasn’t playing very nice with long type (which the RNG seed and state are) so I had to convert it into a string first and then store it, which seems to work fine! Currently, I am just storing a JSON string, but later on will try out making a binary file, since that seems like a fun little thing to do.
And I think that is all for now!