Dark Energy
Try the demo!

Heroic moment. The teal ship charges through the choke point to revive their teammate under fire.
Dark Energy is a 2D Soulslike with a focus on multiplayer teamwork. I emulated many of the classic features of the genre. These include earning “souls” from killing enemies that you use to level up certain statistics. You lose all your “souls” when you die and have one chance to pick them back up again. There are items hidden in alternative paths throughout the world that can give you alternative attack patterns. You have a certain number of healing items that you get back when “resting,” which also respawns the enemies. Combat is slow, deliberate, and impactful.
This game is still in development, though it is on pause. We still have plans to eventually release it. The primary barrier to that is finding a dimension where we can differentiate ourselves from classic souls likes. Ideas we’ve had include: roguelike upgrades that reset when you rest, a deck building element that replaces items, or a heavy commitment to skill trees. None of these ideas have made it past ideation.
Genre: Coop Soulslike
Timeline: Worked on throughout 2024
Engine: Unity
Platform: Windows
Team size: 2
Responsibilities: Concept, Programming, Design, Art
Itch Page: https://buildsgames.itch.io/dark-energy (Try the demo!)
Github: https://github.com/WillyBMiles/Dark-Energy-Scripts
Highlights
- Synchronization was simplified by making every attack and ability in the game into an animation that can be synced by a built-in animator syncer.
- Combat is slow and deliberate, forcing the player to learn moments of weakness and attack patterns to efficiently deal with combat situations.
- Level design effectively tutorializes each enemy by introducing more limited versions of that enemy before allowing the player to fight a full powered version.
Challenges
- The animation based system is prone to bugs. Due to the way that hot-swapped animations work, the synchronization code required more and more manual intervention. As the number of abilities grew, more needed to be added to the player base prefab. Switching to an item-animation system rather than a character-animation based solves both problems.
- Large connected areas meant a lot of performance loss when pathfinding and pushing updates to a large number of enemies at once, not to mention the network cost of synchronization. Robust interest management prevent too many objects from running at the same time. A memory based pathfinding solution turns a computationally difficult task into a simple table look up.
System Spotlight: Animation-based Combat

Animator for player ship. Most animation states represent a trigger types for one of the item slots, for example, light, heavy, and sustain.
As is tradition in the genre, attacks and ability are uncancellable but interruptible by damage. This extends to literally every action in the game other than moving. Each action exists as a state on the player’s animator, and the animation triggers everything.
The animation itself contains most of the info for the action, including timings and hitboxes, but also any dashing or self targeted effects like invulnerability from dodging or damage reduction from shielding. This is all triggered through animation events or just directly changing values or bools on scripts attached to the ship.
The main aspects that aren’t stored in the animation are any values that are affected by items and player stats. Any item effects are stored in a ScriptableObject for the item, the same one that contains the animations, as well as it’s name, icon, description and other information like that.
Speaking of items, each weapon and “tool” has it’s own animations associated with, for example, light and heavy attacks, but the Animator only has states for two different weapons and one tool. When you swap weapons or tools the game swaps the animations associated with each state for the ones from the newly equipped item.
This system makes a lot of sense because the player can only be in a single state at a time which matches the design, though it is somewhat inflexible if that design changes. Additionally, it meant I could use Mirror’s built-in Animator syncer to sync all of the abilities for me.
However, there are two major problems with this approach. First, animations belong to the player GameObject rather than the items and are used in its animator. This means every hitbox, animation, effect and so on from each item in the entire game needed to stored on the player prefab. This makes the player prefab huge and unwieldy and, because animations rely on the specific hierarchy of the GameObject, it means the hierarchy can’t be changed for better organization.
The second problem is functional. Because every object is stored on the player, the idle animation for the player needs to reset every item to default. This adds more possible points of failure and more busy work when a new item is created. More importantly, it means that whenever a player switches items it must always visit the Idle state firs, otherwise multiple effects may play at the same time. Unfortunately this creates a major tradeoff. the game is networked so it’s hard to guarantee that sequence of events when looking at another player, especially using the built-in solution.
Luckily, both of these problems can be solved by making each item its own GameObject with its own Animator. It does mean I’ll have to sync item switching myself instead of using the out-of-the-box solution, but it’s worth it for the extra stability.

The animator for enemies. All enemies use the same animator controller. It’s very straightforward, allowing up to 10 separate attacks. All enemies share a single death animation so that doesn’t need to be its own state.
System Spotlight: Enemy AI and Level Design
Each enemy has multiple states that represent different behaviors and attacks. The enemy runs on a finite state machine with somewhat random transitions. When encountering an enemy for the first time it would be quite hard for a player to know exactly what an enemy can do which could be frustrating. I designed the system so as a designer I can have more control over how enemies act to introduce their behavior slowly. In the inspector, I can remove or add states and change various behaviors like making the enemy slower or stay closer to its spawn location.

The states for the lighting enemy.
As a concrete example, this lightning enemy has a fairly complex standard pattern. It has a shield to block attacks, a short range lightning attack, and a long range plasma attack. This is a lot to understand and devise a strategy against all at once.
The first time the players encounter this enemy, it’s in an extremely controlled environment. It’s protecting a collectible item and won’t chase them very far. Additionally it only has its short ranged lightning attack and it’s the only enemy encountered at that time. This allows players to experiment and gives them incentive to learn how to beat this enemy to earn the item. The second encounter is a challenge to test their learning. They must fight two of this enemy at the same time. They have the same attack but are a bit more aggressive. However, they’re easily exploitable due to the choke point that is right next to them. This empowers the player to show that if they think strategically, this enemy is quite beatable.


The state for this enemy. The nearby enemy will likely be defeated before the players before this enemy is approached.
The second encounter is two of them in a choke point.

These values can be changed to affect the enemy as well.

The next time they are encountered, the first thing players will notice is plasma shots coming from off screen while they fight smaller enemies. As they arrive they’ll see that the source of these shots is the lightning enemy. This enemy acts as a turret, not moving and just firing at the enemies, showing that it has a powerful ranged attack.

A good fight.
Directly next to that is the enemy again, but this time it’s only ability is to hold up it’s shield and block the path. This shows the players to wait until it’s shield is down to beat it.


The shielding enemy has no attacking states.
Finally they face the enemy at full power, all of its abilities are unlocked and now it can control its own movement ducking in and out of battle. Players may have a challenge but they have seen all of its abilities in isolation and learned how to deal with them, so it nothing seems unfair.

The final test. Players are rewarded with a station right after this fight if they succeed.
This system also allows enemies to act more as puzzles or obstacles. Similar to the shield enemy blocking the path, these laser enemies slowly spin making this pass more dangerous but also giving the player multiple ways to pass it. It’s easy enough to dodge or shield past the lasers but the players are incentivized to take extra risk to shutdown the obstacle by killing the enemy itself.

Laser enemies block the path like obstacles.
System Spotlight: Pathfinding and Interest Management
Unity does not natively have a 2D pathfinding system, so I decided to create my own. My main goal with the system was to make it as efficient as I could. I knew that pathfinding is a performance hog and I didn’t want to have to worry about that.
My first order of business on that front was to define my graph for pathfinding. I manually placed nodes throughout the level, such that anywhere that the player could go would be seen by at least one point. Additionally, I ensured that all topographical loops or paths that could be traversed by travelling from one node to another.

Each green node is a node on the pathfinding graph.

Every node holds a precalculated path to every other node.
After placing these nodes I can press the “Precalculate” button. The code for that is reproduced here. Essentially, each node keeps track of their immediate neighbors and the distance between them. Then it recursively pathfinds to every other node in the graph and records the adjacent node that an agent should travel when travelling from the origin node to any final node.
This entire process is done in the editor and stored on the nodes. This means that runtime pathfinding is just a matter of figuring out which nodes to travel between and then looking up the proper path. The system presents a major tradeoff of space for efficiency, needing to store the path between all nodes but massively speeding up in game pathfinding.
public void Precalculate(bool chain)
{
#if UNITY_EDITOR
allNodes.Clear();
allNodes.AddRange(
FindObjectsByType<Node>(FindObjectsSortMode.InstanceID));
foreach (Node n in allNodes)
{
n.FindAdjacent();
}
foreach (Node n in allNodes)
{
n.FindAllPaths();
UnityEditor.EditorUtility.SetDirty(n.gameObject);
UnityEditor.EditorUtility.SetDirty(n);
}
if (allNodes.Count > cameFrom.Count)
{
Debug.LogWarning("Node graph is unconnected!");
int minimum = int.MaxValue;
Node minNode = null;
foreach (Node n in allNodes)
{
if (n.cameFrom.Count < minimum)
{
minimum = n.cameFrom.Count;
minNode = n;
}
}
Debug.LogWarning($"Least connected node: {minNode.name}");
}
foreach (Ship s in FindObjectsByType<Ship>(FindObjectsSortMode.None))
{
s.Precalculate();
UnityEditor.EditorUtility.SetDirty(s.gameObject);
UnityEditor.EditorUtility.SetDirty(s);
}
foreach (ManagedInterest mi in
FindObjectsByType<ManagedInterest>(FindObjectsSortMode.None))
{
mi.Precalculate();
UnityEditor.EditorUtility.SetDirty(mi.gameObject);
UnityEditor.EditorUtility.SetDirty(mi);
}
if (chain)
{
NodeGroup nodeGroup = FindObjectOfType<NodeGroup>();
nodeGroup.Precalculate(false);
}
#endif
}

Node groups hold all nearby nodes and ships.
Three Player gameplay of the first area of Dark Energy.