The Gravyzone

This is where I irregularly post about mostly game dev

Home

CS50x COMPLETED! Description of the project

Jan. 24, 2026 | Categories: GameDev

For those that prefer video format over textual, here is a demo of the game and some of its features (although I don't go into as much depth as I do below): https://youtu.be/AwCB4yvJTF4

 

My first design choice was whether to use handcrafted maps or a procedural map. I went with procedural, as I wanted to try making something resembling a roguelike game. I found an existing generator, but had to make several edits to get it working in the current version of Godot (4.5 as of writing). This included updating it to work with TileMapLayers (since TileMap is deprecated) and fixing several built-in functions whose interfaces had changed. I also made all the graphics myself using Aseprite.

 

The next major decision was movement. I chose grid-based movement simply because that is what I enjoy. To make grid-based movement work, I needed a variable that had knowledge of the entire map—specifically what tiles are passable and what are not. I implemented this using a dictionary stored as a global variable in world_state.gd, which contains important information that many parts of the code need to share. Both the player (player.gd) and enemies (enemy.gd) use this shared data to move around without passing through walls. There is also a global variable that tracks where every entity is on the map, and, like the geometry data, it is accessible to all relevant scripts.

 

After the map is generated, I added code that places extra padding around it to give the player a more “closed-in,” claustrophobic feeling. The player script contains various stats and uses signals to communicate with other scripts—for example, notifying them when an object is hit and how much damage was dealt. It also checks whether target spaces are empty before allowing movement.

 

I initially had trouble with movement because it was being updated every frame in _process(). To fix this, I removed it from _process() and locked movement behind a boolean. This ensures that the player can only move one tile at a time and cannot begin another move until the current one is completed. I also implemented inventory cycling, different actions the player can take, and other related systems.

 

One feature I added requires the player to carry an item that emits light in order to see. To prevent the player from stockpiling light sources, the lit item can be placed on the ground to mark paths. This forces the player to balance visibility against exploration.

 

The enemy script (enemy.gd) has two modes: wander and pursue. Wander is the default state, where the enemy randomly chooses directions to move in as long as the player is not nearby. Pursue is triggered when the player comes within a certain range. In this state, the enemy asks cavegenerator.gd for a path to the player and then follows that path. The path is updated after every player action, allowing the enemy to continuously pursue until it is within attacking range. When the player and enemy engage in combat, if the player’s health reaches zero, the Game Over screen is shown, the game is paused, and the player can reset the game with a button click.

 

To make enemy movement turn-based, I removed it entirely from _process() and instead tied it to a signal emitted when the player performs an action. Each time the enemy moves, it checks the tiles above, below, left, and right to see if the player is present. If the player is adjacent, the enemy attacks.

The HUD (HUD.gd) manages all inventory-related display information. It stores the graphical data to be shown and updates it based on information received from player.gd. For example, cycling left or right updates the displayed item. The HUD also shows the player’s health and textual descriptions of items, including quantities when applicable.

 

WorldState.gd also contains some inventory-related data (which I plan to refactor so it exists in a single location), along with miscellaneous information needed across many scripts. This includes geometry data (passable or not), tile occupation (what entity is at a given (x, y)), walk_speed, tile_size, and more. It also provides helper functions that allow entities to update their positions in the occupation map and to check whether entities exist at specific locations.

 

Finally, path_debug.gd exists solely for debugging purposes. It visually displays the paths generated by A* for entities that request them. Since drawing on the same TileMapLayer would erase tiles, I render the path on a separate node positioned above the map. The flow works as follows: an enemy asks CaveGenerator.gd for a path to the player; CaveGenerator.gd returns the path to the enemy, but also sends that same path to path_debug.gd with instructions to draw it. This way, the entity requesting the path has no involvement in how the path is visualized.