FROSTFIRE SERPENT

GitHub repository itch.io page

Sections

Overview

FROSTFIRE SERPENT puts you in control of a flaming serpent that is summoned by a witch, seeking vengeance against a nearby town covered in snow. This game was made in 48 hours for GameDev Knight's Spooky Jam.

The goal of the game is to destroy every building in the town. Completing a full loop with the serpent causes anything inside the loop to be destroyed. Additional vertebrae spawn around the map, allowing the player to grow in size.

Enemies will spawn to attack the serpent with icy projectiles. Attacks to the serpent's body will cause its tail to break off from the point hit. The serpent may recollect its fallen pieces for a short duration - otherwise, it's gone forever!

The serpent has two other abilities at its disposal. Each ability consumes vertebrae on use and have a cooldown. The first ability is Flame Parry. The serpent sheathes itself in fire, causing ice projectiles to deflect right off it and back at the attacking villager. Its second ability is a devastating charge attack, where the serpent rushes forward and destroys almost anything it runs into.

BFS Loop Detection

I was responsible for implementing the entirety of the player character. This includes movement, abilities, picking up and dropping segments, and much more. Each one had their unique challenges, but the one that required the most consideration to implement was the serpent loop logic.

The serpent should destroy everything that is in the center of it after it makes a full loop on itself (i.e. when the head of the serpent touches one of its body segments). To do this, I considered the snake to be a graph, where each bone vertebra is a node connected to adjacent nodes. Additionally, segments of the snake that cross over each other are also considered to be adjacent.

After reframing the problem as such, the segments that make up a snake loop can be found simply by performing breadth-first search on the graph, starting from the closing-loop segment and going to the head.

For example, here is one configuration of the snake making a loop and a corresponding graph representation. The shaded nodes represent the segments of the most recently formed loop.

Snake Loop Filling

Once the segments that form the loop have been found, the next step is to find all the objects that are within that loop. My first thought was to approximate the shape formed by the loop segments using raycasts. First, the positions of all the segments are averaged to find the "center" of the loop, and then each segment casts a ray to that center.

This works pretty well. However, it has one glaring issue: it only works for convex shapes. In odd, concave shapes where the calculated center falls outside of the loop, the solution starts to show its cracks.

While exploring other options, I came across Unity's Polygon Collider 2D. This is a type of collider that takes a shape of a polygon given a set of points, which is exactly what I needed. Creating this polygon and casting it out into the world ensured that anything in the loop would be caught, regardless of the shape of the loop. By using some built-in Unity collider functions and raycasts, I am also able to approximate a "visual" center of mass that's within the shape to spawn effects, like particles.

Here is one example of a concave shape being captured perfectly by a polygon collider, with a red square indicating the calculated "visual" center.

Performance & Optimization

One of the most fun parts of this game is growing your serpent to extraordinary lengths. However, the game initially ran into some performance issues when the player reached greater lengths, such as a few hundred vertebrae. The one area in particular that had the biggest frame drops were when the player dropped or picked up large amounts of segments at a time.

The first step into investigating any performance issue is to use the profiler. I first started with investigating why dropping segments was so expensive. Taking a look at the profiler, I saw that a lot of it was being used for Physics Composite Collider calculations. Here's what happens when dropping 1000 vertebrae at once.

At the time, I added each segment that was dropped into a composite collider so that a collision with any segment would cause all connected segments to be picked up. However, what wasn't clear at first was how terribly inefficient this was. After becoming aware of this, I switched the dropping system to have each segment simply call a function on an assigned "pickup object". Assigning a reference is much faster than recalculating a composite colliders hundreds of times. Here's the same 1000 vertebrae with the fix.

Similarly, picking up a large number of segments at a time also called heavy frame drops. Profiling quickly revealed this issue was due to a large number of instantiations. An obvious solution to this is to use an object pool. By preloading a lot of vertebrae at the start of the game, we can save time when adding a lot of segments by just adding pre-loaded segments instead of instantiating new ones.

Here's the profiler for dropping 1000 vertebrae at once. The top is without object pooling and the bottom is with object pooling.