The game

Agent Hackman

Stealth

The theme of the Github Game Off was “secrets” which is just about as vague a theme as you can get without flipping to a random page in the dictionary and blindly poking at a word. We were also given a month to complete it.

For me, this was paralysing.

Analysis paralysis

I couldn’t focus on this jam for ages, as it felt like an inordinate amount of time with such a vague concept. I spent a lot of time flip-flopping between ideas, even making one prototype which was a spy management game set in a fictional Eastern European state with a green and black colour palette.

I found though I just do not like making UI-based games, especially in Unity where the UI creation feels very much tacked on and poorly designed.

I opted for something simple and went for a kind of Metal Gear-style top down stealth game. Now, I’m a fan of certain kinds of stealth games - I loved Dishonoured and some of the later Thief games (including the remake … yeah, yeah, boooo I know), but in general the genre has never really spoken to me on a deeper level.

However, making it was kinda fun! A topic for a separate blog, is that I’ve found I enjoy making certain kinds of games that I don’t actually really play myself.

Development

The thing about stealth games, is that the player has to feel like the system is repeatable, deterministic and easy to make predictions about how it works. The player has to be able to fit the system in their head so they can make decisions about how to avoid the enemy.

As a result - unsurprisingly - the enemy AI was the most complex part of this game. It had to work in such a way that the player wouldn’t get frustrated that they had done everything right, yet the enemy still detected them. Here’s (some of) the code handling the player detection:

void LookForPlayer()
{
    var ray = controller.FacingDirection() * viewDistance;
    Debug.DrawRay(transform.position, ray, Color.red);
    
    var noFilter = new ContactFilter2D().NoFilter();
    List<RaycastHit2D> results = new();
    
    Physics2D.Raycast(transform.position, ray, noFilter, results);
    
    if (results.Count == 0) return;
    
    RaycastHit2D closestObject = results.OrderBy(r => r.distance).First();
    
    // We found the player object
    if (closestObject.collider.CompareTag("Player"))
    {
        // Now are they hidden?
        if (!closestObject.collider.GetComponent<PlayerHiding>().IsHidden())
        {
            SoundTheAlarm();
        }
    }
    
    if (PlayerMovingNearby())
    {
        SoundTheAlarm();
    }
}

bool PlayerMovingNearby()
{
    if (!playerController.IsMoving()) return false;

    var playerPosition = playerController.transform.position;

    bool playerMovingNearby = Vector2.Distance(transform.position, playerPosition) < soundRadius;

    if (!playerMovingNearby) return false;

    // If the player _is_ moving nearby, draw a line from the enemy to the player
    // and see if there's a wall blocking at least so they can't "see" the player move
    var ray = (playerPosition - transform.position).normalized;
    Debug.DrawRay(transform.position, ray, Color.blue);

    var noFilter = new ContactFilter2D().NoFilter();
    List<RaycastHit2D> results = new();

    Physics2D.Raycast(transform.position, ray, noFilter, results);

    RaycastHit2D closestObject = results.OrderBy(r => r.distance).First();
    if (closestObject.collider.CompareTag("Player") && playerMovingNearby)
    {
        return true;
    }

    return false;
}

It feels very revealing to lay this mess out in the open, however nothing beats an inordinate amount of raycasts for detection. You’ll notice also that, despite the enemy conceptually having no idea about the player, it knows exactly where the player is and how far away they are, in order to decide whether or not to make a fuss or not. The latter function represents “hearing” the player.

Of course, the better and more robust way would be for the enemy AI to actually react to sound and movement within the game world (you could make noises happen elsewhere and have the enemy go check it out), but they say “fake it til you make it” for a reason.

Side note, after a decade in web development, it’s fun to write function names like SoundTheAlarm.

How did it fare?

Well, the game itself was fine. It was functional with some mild key/door puzzles. The hiding mechanic was kinda goofy but fit in with the kind of absurd aesthetic and tone of the whole game. Maybe it would have been more fun if there more levels, but since I didn’t vibe already with the theme I was happy to do the bare minimum here.

Agent hackman screenshot

I reused the simple pixel art-style of the previous game I had just done and added some super basic dithering fullscreen shader to add a bit of flavour. All the sounds I generated from jsfxr and wrote the background music myself in Logic Pro.

Learnings

Just always include a web build for itch.io hackathons (if you can), otherwise nobody will play it! I had literally two people rate the game, so it rated slightly higher than the games with no files uploaded, or that just failed to load. There were some games that looked amazing from the screenshot / video so I was bit more motivated to download them and play them. I suppose that’s another key lesson - make something that looks appealing (duh).

My heart wasn’t really in this one (putting aside the cynicism that this was so obviously a push by Github to get people to create repositories on Github and adopt their product(s)).

The community surrounding it was … exhausting to say the least. I tend to join the Discord servers for these jams and so much of this one was people joining and just immediately giving up and saying they don’t know how to make a game and they will fail. On the other side there were already established teams creating huge 3D games, with detailed narrative, 3D art and even one submission which had dynamic footstep sounds for different surfaces (it wasn’t a stealth game and didn’t seem to benefit much from such a feature).

However, the silver lining is I really enjoyed putting together the detection system and writing the enemy AI in such a way that I could just call the underlying controller code like “move in this direction” or “stop moving”. The movement and animation was taken care of by a separate movement component.

But I think next year I’ll skip this one.