← Work
Case study · 2021 · Game Developer

Zombie Dinosaurs 3D

Zombie Dinosaurs 3D was my high school graduation project. I did the programming, game design, 3D modeling and animation, with a friend helping on some of the programming and another writing the music. In the game you fend off hordes of zombie dinosaurs and try to survive as long as possible.

The project

Zombie Dinosaurs 3D was my high school graduation project, which I developed over roughly six months in 2021. The goal was to make a complete first-person survival game rather than just a technical prototype, so I ended up working across almost every part of production.

I was responsible for the programming, game design, 3D modeling, animation, sound effects, and general implementation. The game was built in Unity using C#. My friend Leo Ekstam helped with some of the programming, while Karl Sandegren created the music. I also used three 3D models and the game's textures from external sources. Most of the rest of the content was made by me.

The core gameplay is built around surviving increasingly difficult waves of zombie dinosaurs while managing limited resources. The player can earn points by killing enemies, search the environment for supplies such as ammunition, food, and medkits, and use different weapon types depending on the situation.

Killing enemies also earns ZD Coins. The shop is reached from the main menu, and that is where those coins buy and equip weapons for the next run.

Weapons

The game has several weapon types with very different behaviour, including a pistol, knife, SMG, shotgun, sniper rifle, and grenades. The weapon system was one of the areas where I put particular effort into making the code reusable rather than implementing every weapon independently.

I used a BaseWeapon class to hold shared properties and behaviour such as damage, range, and fire mode, while individual weapons could add or ignore functionality depending on what they needed. This made it possible to build new weapons without duplicating the common logic.

Grenades were implemented separately from the regular weapons because they needed their own interaction model. The player can hold the input to prime a grenade, release it to throw, and use an additional input while holding it to cook the grenade before throwing it.

One thing I identified during development was that the weapon manager itself was becoming too responsible for individual weapon behaviour. A more scalable version of the system would have the manager act primarily as a coordinator, while each item handles its own behaviour through generic methods and callbacks. That would also make the architecture easier to extend to non-weapon interactables such as consumable items.

Dynamic enemy spawning

I also built a spawning system to control how the difficulty changes as the player progresses. Instead of using a fixed sequence of enemies, each enemy type has spawn probabilities that can change at different score thresholds.

The system represents these changes as events containing the score at which a probability should change. When an enemy needs to be spawned, the current probabilities are combined into a cumulative range and a random value is generated within that range. The value is then used to select the corresponding enemy type.

After selecting an enemy, the system also finds a suitable spawn position. It randomly selects a spawn point and checks its distance from the player, retrying when the point is too close. This prevents enemies from appearing directly next to the player while still keeping the spawn system relatively simple. The spawning setup and probability logic are shown in more detail in the spawning image above and in the code below.

The spawning system uses weighted random selection to determine which enemy type should appear. Each enemy has a set of probability values tied to score thresholds, allowing the composition of the enemy waves to change as the player progresses.

FindValue() finds the probability currently applicable to an enemy by checking its thresholds from highest to lowest. FindSpawnObj() then adds these probabilities together to create a total range and selects a random point within it. By subtracting each enemy's probability from that value in sequence, the system effectively gives each enemy a weighted chance of being selected.

Spawning
private float FindValue(SpawnThing spawnThing)
{
    for (int i = spawnThing.values.Length - 1; i >= 0; i--)
    {
        if (ScoreManager.points >= spawnThing.values[i].pointThreshold)
            return spawnThing.values[i].value;
    }

    return 0;
}

public GameObject FindSpawnObj()
{
    float fullValue = 0;

    foreach (SpawnThing spawnThing in spawnThings)
        fullValue += FindValue(spawnThing);

    float randNum = Random.value * fullValue;

    foreach (SpawnThing spawnThing in spawnThings)
    {
        float value = FindValue(spawnThing);

        if (randNum < value)
            return spawnThing.obj;

        randNum -= value;
    }

    return null;
}

This let me increase the difficulty over time without having to define a fixed sequence of enemy spawns.

A complete game built from scratch

What makes the project particularly valuable to me in hindsight is that it was my first experience taking a game from an idea all the way to a playable finished project. I had to work across gameplay programming, systems design, content creation, and the practical problems that come with putting all of those pieces together.

Looking back at the code in 2026, there are definitely parts of the architecture I would redesign, particularly around the weapon and interaction systems. At the same time, the project gave me a solid foundation in Unity and C#, and taught me a lot about structuring gameplay systems so that they can be extended without rewriting large parts of the game.