The project

Beerserker: The Stolen Brew is our final-year project at ESAT, made by about 20 people across programming, art, design, music and production. It came out on Steam in Early Access on 7 July 2026 and is free.

What I did

I worked mostly on the AI. I implemented the four enemy archetypes with StateTree, in C++ and Blueprints, with custom tasks and conditions, and used EQS to query the environment. I also built the enemy spawners and an object pool, as a plugin, and I was in charge of configuring and uploading the builds to Steam. We used Perforce for version control.

Other Unreal tools I used: Actor Components, Animation Montages, Animation Notifies, Two Bone IK and GameInstance Subsystems.

The four archetypes

Melee

It moves towards the player until the player is in range, then hits them.

Ranged

It gets in range and shoots. When the player is too close, it looks for a safe spot to retreat to and shoot from.

Explosive

It blinks red, faster and faster as its timer runs down, so the player can tell at a glance how much time is left. If it never reaches the player, it explodes when the timer runs out; if it does, it explodes after a short wind-up animation; and if it is killed, it explodes straight away. The way to deal with it is to kill it from a distance before it gets close.

Tank mini-boss

It has four attacks. At range, it throws honey or throws a dwarf; up close, it does a sweep that leaves honey on the ground, or an area attack. Each ranged attack has its own cooldown, and it only throws honey when the player is within a set distance range. The two melee attacks share a cooldown and are picked by weight (see below). To throw a dwarf, it calls one over, waits for it to arrive, checks it's still alive, grabs it and throws it. If there's no dwarf to throw, it uses a different attack.

Why StateTree

We chose StateTree because Epic is adopting it as the standard, and because we were forcing Behavior Trees to work like state machines, when StateTree already is one.

StateTree graph of the tank mini-boss
The tank's StateTree graph.

Design decisions

Why an object pool

The game keeps many enemies on screen at all times, plus particles, decals and projectiles, so I wrote an object pool, as a plugin that can be reused in other projects. When an enemy returns to the pool, its StateTree stops on the next frame, and when it is reused it starts again from the beginning.

Where the ranged enemy retreats to

The enemy retreats to a safe spot, picked with an EQS query based on distance to the player and line of sight.

Which attack the tank picks

Each of the tank's melee attacks starts with a weight of 1, and every time it uses one, that attack's weight goes up. The less an attack has been used, the more likely it is to come up, so the tank doesn't keep repeating the same one.

Which dwarf the tank throws

When the tank calls a dwarf, it looks for the dwarves within a configurable radius, checks with a line trace that it can see them, and picks the closest one. The selection is triggered by an AnimNotifyState at the point in the animation where the tank calls the dwarf.

Stun through StateTree events

When the player hits an enemy with an attack that stuns, the enemy receives the StateTree.Stun event, which takes it into the stun state. When the animation ends, an Animation Notify sends StateTree.StunFinish to leave it. The tank can't be stunned.

Exposed attributes and varied melee attacks

The enemies' attributes are exposed in the editor for tuning: attack range, cooldowns, detection radius and honey distances. Each attack's cooldown is set by GameplayTag and checked by a custom StateTree condition. In the melee enemies, attacks are a list of animations: adding a new one is enough for the enemy to use it, so combat feels less repetitive.