For a couple of weeks I've been working on a number of new projects, but the one I want to talk about today is a little web game. I don't have anything but a code-name for it yet, but the overall goal is a web-based game in the vein of roguelikes and more specifically NetHack.
For the uninitiated, Rogue was a text-based game released in 1980 that involved controlling a character that is exploring a dungeon, finding items and killing monsters on the way. What made Rogue truly unique for the time was that the dungeons were randomly generated - no two playthroughs of the game would be the same. Additionally, if your character died then that was it - you'd have to start all over.
This spawned a number of clones over the years and the genre of "roguelikes". One of the most notable clones is NetHack - a very similar game released in 1987. NetHack is one of the more popular Rogue clones and has been updated numerous times over the years - the last update was in 2015. The game involves attempting to traverse a large dungeon to grab the Amulet of Yendor at the bottom, then head back to the top where you can offer the amulet to your god in exchange for immortality. The game, much like Rogue, has randomly generated levels but also a larger variety of items, monsters, and even puzzles.
I'm not an experienced player of NetHack. I played it for a couple of months at most, and never got very far. That said, what really gets me going about it is the NetHack community's idea of "The Dev Team Thinks of Everything" or TDTTOE. This is the idea that every single way you think you can break the game has been thought of. Every weird trick is handled. Every interaction between items and the world makes sense, and works the way you would expect it to.
Here are a few examples:
- Touching a cockatrice causes petrification. If you touch a cocatrice corpse, you will be petrified. Therefore, wear gloves when picking up a cockatrice corpse and you can use it as a weapon.
- In the same vein - cockatrices lay eggs. If you play as a female character and polymorph into a cockatrice, you can lay eggs. Reverting to human will then allow you to pick up these eggs (with gloves!) and throw them at enemies, which petrifies them instantly. The dev team thought of that, however, so you also take a penalty to your luck stat out of guilt.
- A number of interactions in the game are there just for jokes, but also show the game's apparent foresight into what players will try:
- You can dip potions into things. What happens if you dip a potion into itself? This action doesn't actually work, but you get a message about the potion bottle not being a klein bottle.
- If you are on the bottom-most line of the screen and you have a stethoscope, using it towards the bottom will result in a message that you hear a faint typing noise.
- Trying to put a saddle on a succubus will result in the message "Shame on you!". It also causes a penalty to your wisdom stat.
- The game comments on you literally beating a dead horse if you use a whip on a horse corpse.
- Dying results in a death message specific to how you died. Some interactions result in very specific messages...
- "Went to heaven prematurely" is the message if you try to teleport to dungeon level -10 or higher (negative dungeon levels are above the dungeon/aboveground)
- "Killed by wedging into a narrow crevice" is what you get if you are carrying too much, are levitating, and throw an object such that newton's third law results in you being forced through a diagonal space between two walls.
- "Crunched in the head by an iron ball" is what happens if you have an iron ball and chain attached to you (from being punished for certain actions) and you fall into a trapdoor or pit.
Some of these interactions are just fun, and some have actual gameplay value (either in helping or harming the player). Part of the fun is discovering all of these weird interactions, especially since pretty much all of them make perfect logical sense.
NetHack has a large list of "verbs" or "actions" you can take, and I believe this stems largely from the classic text adventure games like Zork where you would type in what's effectively a very simple sentence in order to do anything. The result is that the player can be incredibly expressive in what they want to do - much more-so than modern games. That isn't to say having a huge list of verbs is inherently better than what modern games do - it's just different. If anything, it makes these games less accessible as there's more you have to keep in mind at any one time.
So that's where I have my inspiration - I want to create a NetHack/Rogue clone that has a similar feeling of TDTTOE. That's not enough, though - I want something that's a bit more accessible than NetHack. As such, I've started working on a web game based on this idea.
Now, a disclaimer: I'm well aware that this idea isn't unique at all. There are a number of games that have cloned NetHack and Rogue but with more modern touches (see Dungeons of Dredmor, the Mystery Dungeon series, etc), this project is mostly for my own satisfaction.
Building an Engine
This section is a bit more technical.
To start the game I want to create, I wanted to first build a solid foundation that I could easily expand upon. I'm not talking about building a full-blown game engine, but a highly-specific engine that is designed for games like NetHack.
Now, even knowing that I wanted this to be a game that runs in your browser, I had a choice to make about client-server architecture. The options I considered are as follows:
- The entire game runs on the client. All logic, level generation, etc run on the client (effectively an offline game once the browser downloads the game's webpage)
- The client understands how to play the game, but everything is checked by the server (client prediction)
- The client simply sends input to the server and displays whatever the server tells it to ("dumb terminal" client)
Immediately I cut out #2. The primary benefit of client-side prediction is for realtime games where the client can pretend it knows what happens when it receives input from the player while the server is still processing.
That leaves #1 and #3. I decided on #3 for a few reasons:
I have some ideas (which will likely be expanded upon in a future post) involving some degree of multiplayer interactions which requires the server to at least have some semblance of how the game works
I like the idea of keeping things mysterious. If everything was implemented client-side, it'd be much easier to see what enemies exist, what items there are, spawn rates, etc.
Now, this is probably a bad way to choose an architecture. That said, my goal is primarily my own enjoyment and I'd much rather write a dumb-terminal client in Typescript + the game in C# than the entire game in TypeScript. That isn't to speak badly of TypeScript, but I just enjoy C# more.
The first two orders of business were choosing what technologies to use beyond just the languages (I'm skipping over ASP.Net Core because it's pretty much the defacto choice for C# running on .Net Core). On the client, I needed a way to draw 2d sprites and tilesets on screen. I ended up settling in with PixiJS. Pixi is incredibly easy to use, and had all the features I needed.
For client-server communication, I needed something that could handle bi-directional communication. I've used Socket.io in the past and loved it, but there's no server library for C#. I looked towards raw websockets, but I also wanted a way to fall back if a browser didn't have full websocket support. I ended up stumbling into SignalR (which was confusing at first - turns out there is a newer .Net Core version and an old .Net Framework version) which has many of the same features as Socket.io but provides a C# server. It also makes RPC incredibly easy.
I'm going to avoid the client architecture for this post - it isn't particularly interesting and mostly involves just figuring out how to draw what the server sends onto the screen.
The most important aspect of the server is the entity system. It's modeled on an entity/component architecture, much like Unity. Outside of the level "geometry" (which is a separate system based on tiles), everything is an entity. Input is handled by a
PlayerInput component. If an enemy carries items that drop on death, it has an
DropItemsOnDeath component. Components can be specific behaviors, systems in and of themselves, they can contain data, or any combination of the above.
Here's a full example of what the zombie enemy is made up of right now:
Transform- Contains the zombie's position in the world
Sprite- Contains graphics data and also handles sending this entity's data to the client when necessary
GroundMovement- handles movement along the ground
Collider- emits collision events and also prevents anything from occupying the same tile
Inventory- holds items to be dropped on death
FollowAndAttackAI- defines how this entity acts every turn
StatsContainer- holds stats such as speed and attack
Damageable- emits events when this entity is damaged, and signals that this entity can be damaged in the first place
AgentTimer- emits an event whenever this enemy should take a turn. This is required as enemies don't take a turn every time the player does - the rate at which enemies take turns depends on the player's and the enemy's speed stats.
DropItemsOnDeath- drops the contents of the enemy's inventory when it dies
If in the future I wanted to make a flying zombie, all I would have to do is swap out the
GroundMovement component for a theoretical
FlyingMovement component, and if I wanted more complex AI then I'd just have to swap in that component to replace
Using components specifically as sets of behaviors that depend on very little other than themselves results in making entities very expressive. With a relatively small library of components, a huge variety of enemies and complex interactions can result, which makes the goal of many complex interactions all the easier to achieve.
That's it for this post. I'll be making more posts as development on this project continues and more details become concrete. My next post will likely be on how dungeons are generated. To close this out, I'll leave you with an image of how the game currently looks (with temporary assets from Kenney!):