Midterm

Sketch link

Try the experience out for yourself here.

It’s highly recommended to play it through once or twice before reading onwards.

Concept

I wanted to create a short retro game inspired by the original The Legend of Zelda game. Simultaneously I wanted to make the player question what it means to attack and kill digital characters in a game. In the majority of games, the player has to or can choose to kill other characters or enemies in the game, and these deaths are very simple, in that the killed character disappears or stops moving, after which the player can completely forget about them forever. My game is a subversion of that.

The game is intended to be played through in two main ways: without killing anyone, or by killing everyone. In order to not kill anyone, the player can try to lure the enemies and run around them. Also, the dash mechanic (activated by pressing E) should make this easier. In order to kill everyone, the player has to attack them with their sword. After the death, a few things happen which can be described as unsettling: the screen flashes in black and white for a second, the music tones down in pitch, a pool of blood appears where the enemy died, and a ghost of the enemy starts following the player. These things are meant to encourage the player to continue playing in one of two ways: either avoiding to kill any future enemies, or killing everything, pushed by their curiosity of what would happen next.

Also, the ending screen changes depending on how many kills the player has, and subtly encourages the player to do another play-through.

High-level project overview

The project uses the Entity-Component system to structure things in a way that allows easily adding new features and content. My GameEngine class manages several “scenes”, with one currently active scene at any moment. At game start, the Menu scene is active, which just shows the title screen and waits for user input to start the game. Afterwards there is a Play scene which loads the level and plays it. Finally, once the level is completed, an End scene shows the end screen and allows going back to the menu.

At each frame, the GameEngines’s update function is called, which calls the currently active scene’s update function, and then draws things to the screen.

// GameEngine update function
update() {
  if (!this.running || this.sceneMap.size == 0) return;
  const currentScene = this.sceneMap.get(this.currentSceneName);
  currentScene.update();
  currentScene.sRender();
}

// ...
// bunch of code
// ...

// Scene_Play update function
update() {
  this.entityManager.update();

  if (this.paused) {
  } else {
    this.sAI();
    this.sDash();
    this.sMovement();
    this.sStatus();
    this.sAttack();
    this.sCollision();
    this.sOwnership();
    this.sScriptedEvents();
    this.sAnimation();
    this.sCamera();
  }

  this.currentFrame++;
}

Here we can see the main concepts of the Entity-Component system. The logic of the game is divided into several “systems”, which are just functions that do all the work.

What does Entity mean? I do not create separate classes for each different “thing” in the game. In other words, there is no Player class, Enemy class, Tile class, etc. Instead, I have a generic Entity class:

class Entity {
  constructor(id, tag) {
    this.id = id;
    this.tag = tag;
    this.active = true;
    this.c = new ComponentList();
  }

  destroy() {
    this.active = false;
  }
}

Here is the definition of ComponentList:

class ComponentList {
  // A list of components that every entity can have, although they can be null.
  constructor() {
    this.animation = null;
    this.transform = null;
    this.bbox = null;
    this.input = null;
    this.state = null;
    this.lifespan = null;
    this.followPlayer = null;
    this.patrol = null;
    this.health = null;
    this.damage = null;
    this.invincibility = null;
    this.keys = null;
    this.owner = null;
    this.message = null;
    this.trigger = null;
  }
}

To each entity, I add components as I see fit, and the systems above handle things based on which components an entity has. For example: everything that I plan to draw to the screen (the player, enemies, tiles) has an Animation component, which is explained in my Midterm progress blog post. Everything that will have a position, velocity, etc., will have a transform component. Everything that will interact with other things (i.e. collision) will have a bounding box component. Everything that is supposed to have health has a Health component. Everything that is supposed to do damage has a Damage component. And so on.

I’m really happy with my decision to structure things in this way, because after I set things up, it was very straightforward to make changes and add new content. However, this was one of the biggest challenges of the project, and it took a long time to implement.

Another big challenging part for me included figuring out how to create, save, and load the level, which led to me to creating a python script which probably saved me several hours of menial work.

Another thing I’m happy about are the effects that happen when an enemy is killed. I experimented with various ways to manipulate the pixels on the screen. I turn the screen to black and white immediately after an enemy’s death. I also wanted to add a greyscale/desaturation effect which increases in intensity as more enemies are killed, and it looked really good, but unfortunately Javascript is very slow and the game became unplayable, even by just scanning the pixels array from p5 once a frame. In terms of sound, after a kill, the pitch goes up for a second, and then goes down to lower than before.

Bonus screenshots

Room 2, normal view.
Room 2, debug view. Collision boxes and enemy line of sight are visible.

To enable debug view, uncomment the line:

else if (key == "c") this.drawCollision = !this.drawCollision;

And press “c” while in-game.

Reflection

Areas of improvement include the content of the level and the unsettling/horror effects. Details of the level can be improved, such as adding more rooms, more enemy types, more decorations, and so on. The more this looks like a real game, the better the delivery of the second, hidden part of it. Also, many more horror effects can be added. Although outside of the scope of this project, one could add a hidden final boss or obstacle that only appears if the player has chosen to kill all enemies they encounter. This could further distinguish and solidify the two routes the game is intended to be played through.

Leave a Reply