Doomenstein is a 2.5D remake version of Wolfenstein, supporting simple multiplayer mode (tested with 6 people connected to the server in total), developed for ~70 hours.
The game is developed in a data-driven manner, all entities and maps definitions are run-time generated from XML files. There are generally three kinds of entities: actors, portals and projectiles.
Players could walk, shoot monsters or each other, and teleport through portals to other maps. And each player just has 100 health, once dead, you could only join the game with a new session.
The game has different world coordinates compared to other games: +X is forward, +Y is left, and +Z is up.
XML defined maps
Sprite based entity render
3D space map
Simplified ray cast
Base networking
Multi-axis convention support
TestRoom map xml definition
Considering the structure of this game: only one single layer of map and no second floor, it is very natural to consider simplify ray cast calculations by seeing the world only in the flat XY-plane when appropriate. Entities are represented by cylinders, so raycasting entity could be split into raycasting circles in XY planes and line segments in the z-axis, and find the intersection of these two. Map raycasting could be split into two problems: when would ray on z-axis hit ceiling or floor, and when would ray in XY plane hit a solid block. Since this is a tilemap world, the exact hitting point for each x-aligned and y-aligned line could be calculated easily, so the ray hit block problem could be translated into stepping forward per x unit and per y unit in turn, and judging the new block is solid or not.
What's more, the world render could be simplified because of no actual z-axis world: for solid blocks, the ceiling and floor could be neglected; for non-solid blocks, the surrounding four faces could be neglected.
Simplified ray cast code
The game supports simple networking, and has been tested with 6 players connected to the server. Two types of communication are supported: reliable TCP and unreliable UDP.
TCP is for server and client establishing and ending the connection and UDP connection. Each client is assigned with a random UDP port and a unique identifier by TCP server at the start. UDP is for unreliable events like entity transform update and global sound playing. I set up a NetworkObserver class to gather UDP messages, so that UDP messages could be packed tightly and sent at a configurable rate.
Since the TCP and UDP are inside the engine and specific server and client code is inside the game, the engine uses the event system to fire networking events and the game is responsible to handle those events, like receiving a new TCP client connection, or connection failed for one TCP client.
The code below shows how an entity's position change being encoded and sent out.
Entity position update encoding and sending
The code below shows how a TCP socket failing being notified and handled across the engine and game.
TCP socket failing notifying and handling
Multi-direction sprite animation
Data-driven development is powerful: XML-based entity and map definitions make value tweaking and texture switching much easier
Draw and test, especially for ray casts: This simplified ray cast has many calculations, I wouldn't be able to find minor calculation bugs without debug drawing of ray cast result.
Networking has countless edge cases: So many edge cases could happen for a networking game, like package losing, connection failed from either or both server and client side, client requested for initializing multiple times, etc. I was only able to fix some most critical scenarios in the end, so that both the server and clients could safely quit instead of hang and not responding.