Transitioning a game from single-player to multiplayer is one of those things that sounds straightforward on paper but is surprisingly difficult in practice. Many assumptions that are perfectly valid in a single-player context completely fall apart when multiple players and a network are involved. Here are some of the key challenges I ran into while adding multiplayer to the Obsidian Tower.
1. Enemy State Management
In single-player, you can make a lot of implicit assumptions about game state. One of the biggest is that an enemy will never move after it's dead. When you kill an enemy, it plays its death animation and that's the end of its story. No more movement updates, no more AI decisions, no more state changes. The code can safely assume that once HP reaches zero, the enemy is done.
Multiplayer breaks this assumption entirely. Because the game state is distributed across multiple clients and a server, events don't happen in a neat sequential order. An enemy might be dead on one client's screen while still alive on the server for a brief moment, or vice versa. Messages about enemy actions can arrive out of order, and the timing of state updates is never guaranteed.
2. The Server Authority Problem
In the Obsidian Tower's multiplayer architecture, the server has authority over enemy movement — it decides where enemies go and broadcasts those decisions to all players. Meanwhile, players track enemy HP locally based on the damage they deal and the damage they see others deal.
This split creates a race condition. Consider this scenario: a player deals the killing blow to an enemy. On the player's client, the enemy's HP drops to zero, and the death animation begins. But at almost the same instant, the server has already calculated the enemy's next movement and sent that update over the network. The movement message arrives at the player's client a fraction of a second after the death animation starts.
What happens? The enemy, which is in the middle of dying, suddenly snaps to a new position and starts its movement animation. The death animation is replaced by a walking animation. From the player's perspective, the enemy they just killed appears to come back to life briefly before dying again — or worse, it might get stuck in an incorrect visual state entirely.
The Solution
The fix was to add HP validation checks when applying enemy movement updates. Before processing any movement data from the server, the client now checks: "Does this enemy still have HP greater than zero?" If the enemy's HP has already reached zero locally, the movement update is discarded. This ensures that once an enemy begins its death sequence on a client, no incoming network messages can interrupt or override that state.
3. The Reward System Problem
A related issue appeared in the reward system. When an enemy dies, it can drop items — gold, equipment, potions, and so on. In single-player, this is simple: the enemy dies, calculate the drop, award it to the player. One enemy, one death, one reward.
In multiplayer, multiple players can be attacking the same enemy simultaneously. Due to network delays, it's possible for two players to both believe they dealt the killing blow. Player A's attack brings the enemy to zero HP on their client. A split second later, Player B's damage message arrives — but Player B's client calculated that damage before knowing about Player A's attack. Both clients think they killed the enemy, and both try to award drops.
The result is duplicate rewards, which throws off the game's economy and progression balance.
The Fix
The solution was to add a check before applying new damage and awarding drops: if the enemy's HP is already at zero, the incoming damage is acknowledged but no rewards are given. Only the hit that actually brings HP from a positive value to zero triggers the drop calculation. This prevents duplicate rewards regardless of network timing or message ordering.
Lessons Learned
The common thread through all of these issues is that multiplayer forces you to abandon assumptions about event ordering and state consistency. In single-player, events happen in sequence and state is always coherent. In multiplayer, every piece of state can be slightly different on each client at any given moment, and messages from the server can arrive in any order relative to local events.
The key defense is validation. Before applying any state change — whether it's movement, damage, rewards, or anything else — always verify that the change makes sense given the current local state. Don't assume that just because the server said an enemy should move, that enemy is still alive on your client. Don't assume that because you dealt damage, the enemy wasn't already dead.