Hellsword Technical Implementation Plan
Backend Architecture
Platform & Responsibilities: The game’s backend will run on PHP (on Linux) behind an Nginx server. PHP will handle user authentication, core game logic (crafting, battles, etc.), and voxel world management. The server will be authoritative over game state (simpler for a turn-based or slower-paced game
), ensuring consistency and preventing client-side cheats.
User Authentication: Implement a secure login system using PHP. You can use PHP sessions or JWT tokens to maintain logged-in state. Store user credentials and profiles in a database (e.g. MySQL) and use PHP’s password hashing APIs for security. Ensure to check session/JWT on each request that modifies game state.
World Storage as Flat Files: Represent each “tile” of the sandbox world as a separate flat file on the server. For example, each file could be a JSON or serialized PHP array containing the tile’s voxel data (block types and positions), any placed items, and metadata (like last modified time). Use a clear naming scheme (e.g. world_tile_<x>_<y>.json
) so the server can load the correct file for a given tile coordinate.
-
Loading and Saving Worlds: Use PHP file I/O functions to load and save these files. For example, when a player enters a tile, use
file_get_contents()
to read the JSON, then decode it into a data structure. After a player modifies the world (places/breaks a block), update the in-memory structure and write it back withfile_put_contents()
(using theLOCK_EX
flag to prevent simultaneous writes). Implement file locking or transactions – e.g. useflock()
or atomic rename operations – so that two actions on the same tile don’t corrupt the file. -
Efficient I/O: Keep file reads/writes efficient by only accessing tiles as needed. Because each tile is a separate file, you avoid loading the entire world at once. If the game frequently accesses the same tile, consider caching the tile data in memory (APCu or Redis) and periodically flushing to disk, to reduce repetitive file reads. Watch out for having too many small files – accessing thousands of individual files can become slow. (Minecraft originally stored each chunk in its own file, but moved to a region file system grouping 32×32 chunks for efficiency
.) For Hellsword’s scale, tile-per-file is fine, but if performance suffers, you might later group adjacent tiles into one file or adopt a lightweight database.
Game Logic in PHP: Implement game mechanics (crafting outcomes, combat resolution, card effects) in PHP on the server side. Create PHP classes or modules for major entities: e.g. Player
, TileWorld
, Card
, Inventory
, etc. The PHP code will validate actions (ensuring players have required cards/resources, enforcing cooldowns or turn order) before modifying game state. Because PHP typically runs per-request, long-running game loops (like turn timers) might be handled by the WebSocket server or a background process (see below).
Real-Time Communication with WebSockets: To support real-time features (like live world updates and card battles), integrate a WebSocket server into the backend. This allows the server to push updates to clients instantly rather than relying on frequent AJAX polling. You have two primary options:
-
Ratchet (PHP WebSocket): Ratchet is a PHP library for WebSockets that enables a persistent event-loop server
. You can include Ratchet via Composer (
cboden/ratchet
) and write a PHP script that implementsMessageComponentInterface
for your game events. For example, create aGameSocket
class with methodsonOpen
,onMessage
, andonClose
to handle new connections and incoming messages. InonMessage
, parse the action (e.g. a player played a card or moved in the world) and then broadcast updates to relevant players. Run the Ratchet server script as a separate process (e.g. via CLI or supervise it with systemd/Docker) listening on a port (e.g. 8080). Ratchet will continuously run and handle multiple clients concurrently (non-blocking I/O) – for instance, you can start it with:IoServer::factory(new HttpServer(new WsServer(new GameSocket())), 8080)->run();
. This keeps a persistent PHP process for realtime events alongside your normal PHP/Nginx web API.
-
Node.js WebSocket Server (alternative): If you prefer, you can offload realtime handling to Node.js (which excels at WebSockets). For example, use Socket.IO or the Node
ws
library to create a WebSocket server. The Node server can manage battle messaging and world change broadcasts. To integrate with the PHP backend, you’d expose APIs or use a shared datastore: e.g. the Node server could call PHP’s REST endpoints or read/write to the same flat files or a database. This adds complexity (two server environments), so for initial development Ratchet in PHP might be simpler. The Node approach could be useful if you eventually hit performance limits in PHP WebSockets, but many real-time PHP setups work fine for moderate loads
.
Using WebSockets for Gameplay: Whichever approach, use WebSockets for events like: notifying nearby players when a block is placed or removed, synchronizing turns and moves in a 1v1 battle, and broadcasting guild war status on a tile. WebSockets provide a low-latency, bidirectional channel (server can push data without client polling)
. For example, when a player uses a card to place a trap block in the world, the server will update the tile file and immediately send a message over WebSocket to all other players in that tile so their clients can render the new block. Similarly, during a battle, each card played can be sent to the opponent’s client in real-time.
Voxel Rendering and Frontend Technologies
Choosing a Voxel Engine: On the frontend, you need to render a 3D voxel world (essentially a Minecraft-like view). This is best done with WebGL. You can either use a general 3D engine and build voxel features on top, or use a specialized voxel engine:
-
Three.js: A popular general-purpose WebGL library. Three.js can certainly render cubes and scenes efficiently. You would manually create box geometries or use
THREE.BoxGeometry
for blocks, and manage chunking and culling yourself. Three.js gives flexibility with materials, lighting, and controls. However, rendering each block as an independent mesh is too slow at scale; you’ll want to combine many blocks into larger meshes (chunks)and only render the visible faces
. Three.js can handle this via
THREE.BufferGeometry
(to build a mesh of many cube faces) or usingTHREE.InstancedMesh
for repeated geometries. -
Babylon.js: Another robust engine similar to Three.js. It has a rich feature set and might have easier out-of-the-box support for certain tasks (like a physics engine or GUI overlay). Like Three.js, you’d implement custom logic for voxels (chunk management, mesh optimization). Either Three.js or Babylon.js is a solid choice with plenty of community support.
-
Voxel.js or Noa Engine: Voxel.js is a collection of open-source JS modules specifically for voxel games. It uses Three.js under the hood but provides higher-level constructs for blocks, chunks, and even plugins for things like physics. The voxel-engine (on NPM) can generate a basic block world quickly, and handle chunk loading/unloading. However, Voxel.js had its heyday around 2013-2015; ensure it’s compatible with modern browsers if you choose it. Noa is another open-source voxel engine (also built on Three.js) that provides an out-of-the-box voxel world with chunking, collisions, and a simple API. These specialized engines can jump-start development (because they solve chunk rendering and world management), but they might be less flexible if your mechanics diverge from their assumptions. Evaluate if their performance and maintenance status meet your needs – otherwise, using Three.js/Babylon and coding voxel logic might be preferable for full control.
-
Custom Lightweight Renderer: If existing libraries prove too heavy, you could write a minimal WebGL renderer for cubes. This involves writing shader programs to draw cubes and handling perspective, input, etc. This is a lot of work and not necessary unless you have extremely custom needs. In most cases, Three.js with custom chunk optimization hits a good balance between performance and effort.
Implementing the Voxel World: Once you pick a rendering library, implement the voxel world step by step:
-
Basic World Display: Start by rendering a single chunk or tile. For example, use Three.js to create a
THREE.Scene
, add aTHREE.PerspectiveCamera
, and aTHREE.WebGLRenderer
attached to a<canvas>
on your webpage. Then, parse a world tile file (e.g. the JSON from the backend for the current tile) and for each block, add a cube mesh at the correct coordinates. Initially, you can add individualMesh
objects for each block to verify it works. Show some basic lighting (a directional light or ambient light) so blocks have shading. This will confirm that the 3D engine is set up and your coordinate system (e.g. using tile’s grid coordinates) is correct. -
Chunk-Based Rendering: After the basic view works, refactor to improve rendering performance. Instead of many individual cube objects, divide the world into chunks (could be the same as the “tile” size if each tile is small, or you might subdivide each tile into smaller chunks if tiles are large). For each chunk, combine all its block geometries into one mesh. For example, with Three.js, create an empty
BufferGeometry
, then loop through each block in that chunk and if the block is exposed (no neighbor on a given face), add the vertices for that face into the geometry. At the end, you’ll have one mesh per chunk representing all visible faces. This dramatically reduces draw calls and offloads a lot of work to the GPU in one go. Only render visible faces: (Just as Minecraft does) skip faces between adjacent blocks
so you’re not wasting resources on unseen sides. Implement frustum culling and possibly occlusion culling – Three.js does basic frustum culling automatically per object, so by having one object per chunk, entire chunks will cull if outside camera view.
-
Dynamic Loading of Chunks: Implement lazy loading so that as the player moves, new chunks/tiles come into view and far ones are removed. If your world is composed of many tiles, you might only load the tile the player is in and its neighbors. For example, define a view distance (in tiles or chunks). On player movement, check which tile they’re on; if they move to a new tile, load that tile’s data from the server (via an AJAX call or an API endpoint that returns the tile JSON). Also load adjacent tiles around it. At the same time, unload or garbage-collect tiles that are now outside the view distance (remove their meshes from the scene to free memory). This ensures the browser isn’t rendering the entire universe at once, just the local vicinity. Each time you load a tile, use the chunk mesh generation from step 2 to add it to the scene. This streaming world approach will keep performance steady regardless of total world size.
-
Frontend Data Handling: Decide how the client knows what blocks exist. One approach is when a player enters a tile, the server PHP could serve the initial HTML plus the tile’s data (embedded as JSON) or the client can fetch it via REST after loading. A neat approach is to use the WebSocket: upon connection or upon moving, the client could send a “load tile X,Y” request and the server responds with the tile data. However, binary voxel data might be heavy for large areas, so a REST endpoint (which can gzip compress the response) is also fine. Use whatever is simpler initially (e.g. an authenticated GET request to
/api/world?tile=X_Y
returning JSON). -
Player Interaction and World Updates: Allow the player to modify the world through the UI and reflect it in the 3D view. For example, if a player places a block (by selecting a block type from their inventory and clicking a location in the world), you’d capture that event on the client, send a request to the server (perhaps over WebSocket: e.g.
{action: "place_block", type: "stone", position: [x,y,z]}
), wait for server confirmation, then update the local scene. On confirmation, the server will have updated the flat file and broadcast to all clients in that tile, including the initiator. The client receives the update (if it’s the initiator, it’s effectively an acknowledgment; for others, it’s a new event) and then updates the Three.js scene: e.g. add the new block’s mesh to the appropriate chunk (or regenerate the chunk mesh). Ensure the client can handle updates for areas not currently loaded (it can ignore them or cache them) – but if your update protocol is scoped to the current tile or nearby, that’s manageable. -
Inventory and UI: Build the user interface for inventory management, crafting, and card gameplay. This can be done in React for a modular approach or with plain HTML/CSS/JavaScript if you prefer minimal overhead. If using React, create components for the inventory grid, card hand, crafting menu, etc., and manage their state with React’s state or a global store. The 3D canvas can be a React component or just a fullscreen canvas with the React UI overlaid (e.g. absolutely positioned HUD). Keep the 3D rendering in sync with React state by using callbacks or state hooks when actions occur. For example, if the player selects a “block card” in the UI, highlight that selection in the UI and prepare the 3D code to place that block on next click.
Tip: Even with React, you’ll likely have global game state (player position, current tile data, etc.) that the Three.js part uses. You can use a state management library or simply attach game state to the
window
or a singleton game manager module for easy access from non-React code. The React components can call functions on this game manager to perform actions (which send messages to server etc.). -
Controls and UX: Implement controls for moving around the voxel world (if applicable) and manipulating blocks. This might include keyboard controls (WASD to move, space to jump if verticality) and mouse controls (click to break or place blocks, drag to rotate camera unless you use pointer lock and first-person style). Three.js has examples of first-person controls or orbit controls that you can adapt. Ensure the UI and controls are intuitive: e.g. allow switching between a “build mode” (place blocks) and a “combat mode” (play cards for battle) if those are distinct.
-
Rendering Optimization: As the world and features grow, continuously profile and optimize. Use techniques like mipmapping and texture atlases if you have many different block types (reduces material switches), and consider LOD (Level of Detail) if you show far-off structures (distant voxels could be a simplified mesh or even skipped). Since Hellsword is also a card game, the world might not be as densely built as a pure voxel game, but be prepared. Also, ensure garbage collection of Three.js objects when unloading chunks to avoid memory leaks.
Game Mechanics Implementation
Card-Based System Design: In Hellsword, every game element can be represented as a card – blocks, items, crafted tools, etc. The game should treat cards as a unifying concept for inventory and abilities. Start by defining a data structure for cards. For example, a Card could have properties like id
, name
, type
(block, weapon, spell, etc.), description
, and stats or effects. Maintain a catalog of all card types (perhaps as a JSON or database table) so both server and client know the full list. Some cards correspond to placeable blocks (e.g. a “Stone Block” card), others might be consumables or equipment (e.g. a “Health Potion” or “Sword of Night”). Implement functions to convert an in-world object to a card and vice versa:
-
Block/Item to Card: When a player harvests a resource or picks up an item in the world, give them the corresponding card. For instance, breaking a tree voxel might yield a “Wood” card (or several) in their inventory. The backend will remove the block from the world file and add an entry to the player’s inventory list. You might maintain player inventories in a database or a server memory structure (persisting on save/logout). Ensure the drop rates or quantities are balanced (you could introduce randomness or fixed yields).
-
Card to World Object: Conversely, when a player wants to use a card to place a block or create an item in the world, implement that action. E.g. if a player uses a “Stone Block” card while pointing at an empty space in the voxel grid, the backend will check they have that card, deduct it from their inventory, and then update the tile file to add a stone block at that location. It then notifies clients to render the new block. Not all cards will be placeable in the world – some might be spells or one-time effects – but a large subset (especially resource and structure cards) will translate to world changes. Create clear rules for this conversion to prevent exploits (for example, if you place a block card, maybe you don’t get the card back by simply breaking that block; otherwise players could spam place/break to duplicate resources unless there’s a cost or durability loss).
Crafting System: The game should allow crafting, where players combine resources (blocks or cards) to create new cards or objects. Implement crafting recipes on the backend (perhaps store them in a JSON file or DB table). A simple approach is to define recipes mapping input items to an output card. For example: 3 “Iron Ore” cards + 1 “Wood” card => 1 “Iron Sword” card. The crafting UI on the frontend will list available recipes and highlight which ones the player has ingredients for. When a craft action is taken, the PHP backend should verify the player has the required input cards, remove them, and grant the new card. Balance the crafting so that obtaining powerful cards requires appropriate effort or rare ingredients.
-
Card Conversion Balancing: Because players can convert between world blocks and cards (through harvesting and placing), balance is critical. Define conversion rules carefully: perhaps some losses on conversion to prevent infinite looping. For instance, smelting iron ore block to an “Iron Ingot” card could consume some fuel card, or breaking certain placed blocks might not return a full card (maybe a chance to drop nothing or a lesser card) to discourage farming the same block. This ensures players actually explore and gather rather than create an infinite resource loop. Test these rules and iterate to avoid any one strategy dominating
(every card or resource should have trade-offs so no single type is overwhelmingly powerful or abundant).
Battle System (1v1 Combat): Design a turn-based card battle system for duels. When two players decide to battle (perhaps one challenges another and the other accepts), transition to a battle mode. This could be presented as a separate screen or interface where each player’s deck or hand is displayed, along with perhaps a representation of the battlefield (which might be a simplified grid or just abstract zones since the real “battlefield” is conceptual). Key steps to implement:
-
Initiating a Duel: Provide a way for players to challenge each other. This could be a UI action when clicking on another player’s character or a chat command. The server can handle a challenge request by sending a WebSocket message to the challenged player. If they accept, the server creates a battle session (an object in memory representing the ongoing duel).
-
Battle Mechanics: Decide the rules of the card combat. For example, each player might have a deck of cards (built from their collection) and draw a hand of 5. They take turns playing cards which could summon creatures (maybe represented by entities on a grid) or cast spells (directly affecting the opponent). Given Hellsword’s voxel theme, perhaps the cards correspond to actions or creatures that still manifest in the voxel world of that tile temporarily. However, to keep scope manageable, you might implement battles as more of a traditional card game (like Hearthstone or Magic: The Gathering style) with health points, attack values, etc., and abstract the spatial aspect. Determine win conditions (e.g. reduce opponent’s health to 0, or capture some objectives).
-
Implementing Turns: Use the WebSocket channel to synchronize turns. For instance, the server sends a “start_turn” message to the current player’s client. That client then can choose a card to play. When they play a card (e.g. click a card and target something), send a WebSocket message to the server like
{action: "play_card", cardId: 123, target: ...}
. The server validates (is it that player’s turn? Do they have that card in hand? Does the move follow game rules?) then updates the battle state. It might update health values, remove the card from the hand, etc. Then the server broadcasts the results of that card play to both players (and any spectators, if allowed): e.g. “Player A played Fireball, Player B takes 5 damage”. This continues, alternating turns, until a win condition is reached. -
Card Effects and Abilities: Implement the effects of cards in battle. Some cards might deal direct damage, others might spawn a creature or defensive structure. If the battle integrates with the voxel world, you could have effects like creating temporary blocks or traps on the ground. To start, you might keep effects abstract (just numbers and status changes) and later visualize them in the world if desired. All card effects should be resolved on the server to prevent any tampering. The clients simply receive updates about what happened (they can show animations or graphics corresponding to the effect, but the source of truth is the server’s calculation).
-
Balancing Battles: Balancing a card game is an ongoing challenge, but follow general best practices: ensure no single card or combo is overwhelmingly powerful without counterplay
. Assign resource costs or cooldowns to powerful cards. For example, you might have a notion of “mana” or another energy that limits how many high-level cards can be played early. If Hellsword uses resources from the world, perhaps require certain cards to consume those resources when played (e.g. to play a “Dragon” card, you must sacrifice 10 ore cards). This ties the card game back to the sandbox economy and prevents pure card-play from outpacing the world interaction. Playtest the 1v1 battles internally and tweak card stats for fairness.
Tile-Based Guild Wars: Extend the battle system to group conflicts. The idea is to allow guilds to fight over territory (the voxel tiles). Implementation could work as follows: if a guild initiates war on a tile controlled by another guild, it could trigger either a large team battle or a series of 1v1 battles contributing to a score. Design this to suit your team sizes. One approach is a “raid” style battle – one guild’s members versus another’s, possibly in a sequence of duels or a simultaneous card battle with multiple players. For simplicity, you might start with something like a chain of 1v1 fights: e.g. each guild selects champions and the majority of wins takes the tile. A more interactive approach is a turn-based strategy on the tile’s grid: guild members play cards that summon units onto the voxel tile, and the fight is essentially a tactical battle for control of the physical map. This is complex, so starting with sequential battles or a point system might be easier.
-
Implementing Guild War Outcomes: The result of a guild war should impact the world. For example, the winning guild now “owns” the tile. You can reflect this by marking the tile’s file with an owner guild ID and perhaps changing some features in the world (like a guild banner block appears). Guild-owned tiles might grant resources or strategic advantage. Ensure the backend handles updating ownership after a war and notifies all players in that tile or region. You might also set a cooldown so that once a tile is conquered, it can’t be immediately attacked again (to give the guild some reward time).
-
Collaboration and Team Mechanics: If doing multi-player battles, extend the WebSocket handling to support multiple players in one “room” (e.g. all guild members involved). This means broadcasting battle updates to all participants. You may need to implement a turn order that includes all players or allow simultaneous turns with some synchronization. This can get complex, so as a first iteration, clarify if guild wars can be simplified to a series of duels or a single duel between representatives.
Persistent Game State and Progression: Ensure that changes in game state are saved. Player inventories (their cards collected) should be saved server-side (in a database or in persistent files) whenever updated, so progress isn’t lost if the server restarts. World changes are already in flat files which is good for persistence (just ensure you have backups or versioning for recovery or rollback in case of corruption). Battle outcomes should also update player stats (e.g. win/loss record, any rewards like new cards earned from victory, etc.). If you plan to have an experience or leveling system, implement that in the backend as well (gaining XP from placing blocks or winning battles, for example).
Rules and Balancing World-Building vs Card Play: Because Hellsword blends world building with card gaming, establish rules that balance the two modes:
-
Limit world editing during battles: If a battle is taking place for a tile, you may want to “lock” that tile’s world from arbitrary editing. Otherwise, someone could run around placing walls during a card battle, which might break the game. Perhaps when a guild war is declared on a tile, that tile becomes a battleground where only battle-related actions are allowed until the conflict is resolved.
-
Resource generation vs Card power: Make sure the effort to gather or craft translates appropriately into card strength. For example, if it takes an hour of mining to get a “Fireball” card, that card should be impactful in battle, but not so overpowered that it guarantees victory. Likewise, common blocks that are plentiful (like dirt) might correspond to weak cards or be used as basic building material, whereas rare ores yield strong cards or gear.
-
Economy and Trading: If players can trade cards, build a secure trading system (maybe a simple exchange where both sides have to confirm). This isn’t core mechanics, but part of balancing – an open economy can break balance if not monitored. Early on, you might disable trading or limit it to prevent creating black markets that bypass gameplay progression.
Throughout development, test mechanics thoroughly. Use a small group of play-testers to try to abuse the systems (duplication glitches, unbeatable card combos, etc.) and adjust rules accordingly. Balancing is iterative; focus on making the game fun and fair even in this early stage, and you can fine-tune numeric values as you gather more feedback.
Networking and Real-Time Features
WebSocket Real-Time Updates: As noted, WebSockets will drive real-time communication. Set up channels or identifiers for different contexts: e.g. one channel for each world tile (all players in tile X listen for world_X_updates
messages) and one channel per active battle. If using Ratchet (PHP), you’ll manage this in your onOpen
by subscribing connections to certain groups. You might maintain a dictionary in the WebSocket server mapping tile IDs to a list of connections in that tile. When a player moves to a new tile, update their subscription (add to new tile group, remove from old). Ratchet doesn’t have built-in rooms like Socket.IO, but you can code the logic to loop over connections and send to those matching a criterion. If using Socket.IO in Node, it has join/leave room methods which simplify grouping by tile or battle.
-
World State Sync: Ensure that when a client connects or joins a tile, it gets the latest state. For example, when a player initially loads a tile, you might send them the full tile data via the REST API or an immediate WebSocket message. After that, incremental updates (like “block at (10,5,2) changed to type GOLD”) can be sent over WebSocket. All such messages should include enough info for the client to apply them (position, new block type or removal). Keep a protocol version or message version in case you need to update the message format later – this can be as simple as an
"action": "update_block"
versus"action": "start_battle"
field to distinguish message types. -
Turn-Based Sync: For battles, use WebSockets to sync turns and moves reliably. Because battles are turn-based, you don’t have to worry about simultaneous actions from both players, but you do need to enforce turn order. The server should track whose turn it is and ignore or queue messages from the non-active player if they try to perform an action out of turn. Send explicit messages like
{event: "your_turn"}
to one client and{event: "opponent_turn"}
to the other, so the UI can indicate whose turn it is. Each card play or action in battle is broadcast to both players with the result. Given the turn structure, the real-time demands are manageable – it’s more important that messages are reliable and in correct sequence. WebSocket (over TCP) ensures order, but if a client disconnects mid-battle, have a strategy (maybe auto-forfeit after a timeout, or allow reconnection to resume if possible).
Action Queue & Concurrency: Even with an authoritative server, you must handle cases where multiple events happen near-simultaneously on the server – especially for world interactions. For instance, two players might try to pick up the same item card from the ground, or two players place a block in the same spot at the same time. To prevent race conditions, implement a locking or queue system for critical sections:
-
Tile Locking: When updating a tile file, use a lock so only one update happens at a time per tile. PHP’s file locking or even an advisory lock in a small database table can work. For example, before writing to
world_tile_5_7.json
, acquire a lock (flock) – if another action tries to write at the same time, it will wait briefly. This serializes modifications per tile. -
Queue of Actions: In the WebSocket server, if a burst of events come in, you can enqueue them and process sequentially in the order received. Ratchet will essentially do this as it’s single-threaded – events are handled one by one in the event loop. Similarly, Node’s event loop processes socket events serially. However, if you offload long tasks (not likely here) or use multithreading, then you’d need an explicit queue. For simplicity, design the game loop such that each event is processed to completion before the next. This may already be naturally enforced by the single-threaded nature of Node or Ratchet’s loop, but be mindful if you ever integrate external async calls (like a slow database write) – in that case, protect critical sections.
-
Preventing Conflicts: Code defensively on the server. If two players somehow try to place a block at the exact same coordinate, the second action should detect that the spot is no longer free (because the first action filled it) and either fail or adjust. The server can send a failure message back to the second player (“action failed, spot occupied”) prompting their client to update (e.g. show that a block appeared there by someone else). These edge cases will improve robustness.
State Consistency: The server is the source of truth, so the main goal is to keep all clients consistent with that truth. Some strategies:
-
Acknowledge and Correct: When a client performs an action, you can optimistically show it (for better user experience) but mark it pending. After the server processes it, either confirm it or correct it. For example, a player breaks a block on their screen – you could remove it client-side immediately for responsiveness, but then if the server says “actually that block break was invalid (maybe it was protected or already broken)”, the client must re-add it. Alternatively, you can require strict server confirmation: the client plays a “break block” animation but doesn’t actually remove it until the server says success. This is simpler to keep in sync but can feel a bit laggy. A hybrid approach is often used: quick client feedback with the possibility of rollback if the server disagrees.
-
Periodic Sync: Implement periodic full-state sync for sanity. For instance, every few minutes or on certain events (like end of a battle or entering a tile), the client could request the full state of that context to verify nothing was missed. This can correct any drift. Because we store worlds in files, a client could request the entire tile file if it suspects something off (like if a lot of updates happened while it was disconnected). Similarly, at the end of a battle, the client might fetch the official result and players’ updated stats from the server to make sure it aligns with what was shown.
-
Versioning: Keep a version or timestamp for world data and important entities. For example, each tile file could have a field “version”: increment it each time the tile changes. Clients track the version they have; if they ever receive an update referencing an older version than they have (out-of-order message, unlikely with WebSocket TCP, but just in case) they can discard it, and if they detect they missed some versions (gap in sequence), they know to request a full refresh. This is similar to how some games send a state ID to ensure clients stay in sync with the authoritative state.
Use of Server Authoritative Model: As mentioned, Hellsword’s server will validate everything. This prevents cheating – e.g. a hacked client cannot grant itself cards or break blocks without the server checking permissions. All moves in battle are checked for validity (correct turn, card in hand, etc.). This authoritative stance is important for consistency and fairness
. The trade-off is that the server needs to handle all logic which might slightly increase server load, but given the scope (a collectible card game with moderate players, not a FPS with thousands of ticks), PHP or Node can handle it.
Latency Mitigation: In a turn-based setting, latency is not as critical as in twitch action games. A small delay for actions is acceptable. However, for world interactions like moving around or placing blocks, try to keep latency low so the game feels responsive. Hosting the server in a region close to your player base and using efficient protocols (WebSocket as we have) covers this. If you allow free movement in the voxel world, consider client-side interpolation for other players’ movements: e.g. if you broadcast player positions over WebSocket, update positions smoothly on clients. But for initial implementation, you might even restrict movement (maybe point-and-click tile movement or no continuous movement) to simplify.
Handling Disconnections: Because WebSocket connections can drop, handle it gracefully. If a player disconnects unexpectedly, the server (Ratchet or Node) will trigger onClose
. Use that to update game state: e.g. mark the player as offline, remove their avatar from the world after a timeout (maybe give a few seconds grace for reconnection), and if they were in a battle, decide how to resolve it (possibly forfeit if they don’t return). Also, free up any resources (unsubscribe from tile channels, etc.). When the player reconnects, authenticate them (you might use a token or session ID to auto-login) and restore state: put them back in the same world position and give them any state they missed. Having the persistent world on disk and inventory in DB helps here – they can jump back in where they left off.
Testing Real-Time Features: Use multiple clients in a dev environment to test synchronization. For example, open two browser windows, log in as two different players, and perform actions to ensure both see the updates. Test edge cases like one player moving out of a tile and another placing a block just at that moment – does the first player miss the update or not? If they do, perhaps when they return to that tile later, the full sync will correct it. Also test with simulated latency (there are browser dev tools or proxies to add delay) to see how it affects the experience.
Deployment and Scaling
Nginx Web Server Setup: Use Nginx as the front-end web server serving both the PHP application (via FastCGI) and static assets (JavaScript, images, CSS, world files, etc.). Configure Nginx with a server block for your game domain. Typical configuration in /etc/nginx/sites-available/hellsword.conf
:
server {
listen 80;
server_name hellsword.example.com;
root /var/www/hellsword; # your project directory
location / {
# Try to serve static files, fallback to index.php for routing
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php-fpm:9000; # if PHP-FPM is on a Docker service or use unix socket if local
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
fastcgi_index index.php;
}
# (Optional) WebSocket proxy settings would go here if we route WS through Nginx
}
This example shows an Nginx config directing PHP requests to a PHP-FPM process listening on port 9000
. In a non-Docker (native) setup, fastcgi_pass
might be unix:/run/php/php7.4-fpm.sock;
depending on PHP version
. Adjust the root
and PHP socket as appropriate. Test the config with nginx -t
and reload Nginx. Once configured, Nginx will serve as a reverse proxy for PHP – handling many concurrent connections efficiently and only using PHP processes for the dynamic requests.
PHP Configuration: Ensure PHP is configured with sufficient resources. For example, adjust php.ini
to a reasonable memory_limit
(to handle big world files or concurrent users) and max_execution_time
if using long-poll or heavy scripts (though most requests should be quick, except the WebSocket server which runs continuously by design). Use PHP-FPM process manager so that PHP can handle multiple requests. The default settings (dynamic process spawning) are usually fine; you might increase the process pool if expecting high load (e.g. pm.max_children). Also enable PHP extensions you need (JSON, sockets, etc. – Ratchet will require the sockets or event extension).
Docker Containerization (Optional): Using Docker can simplify deployment and help with scaling later. You can create a multi-container setup:
-
PHP-FPM Container: Use the official PHP-FPM image (e.g.
php:8.1-fpm-alpine
) which comes with PHP and necessary extensions. Copy your PHP code into it (or mount volumes in dev). This container will run the PHP application and the Ratchet WebSocket server (you can run the WebSocket script via an entrypoint or supervisor process here). -
Nginx Container: Use the official Nginx image (e.g.
nginx:1.23-alpine
). Copy in your Nginx config (you can use a Dockerfile to ADD the config, or mount it). This container will link to the PHP container. In Docker Compose, you’d setlinks
or better, use a common network and refer to the PHP service by name (fastcgi_pass php-fpm:9000
as seen above). Map port 80 (and 443 if SSL) from the host to the Nginx container. -
Database Container: If using MySQL or another DB for user accounts, you can have a container for it as well. In Compose, declare a mysql service, set environment variables for root password, etc., and use a volume for persistence.
-
Node Container (if applicable): If you chose a Node.js WebSocket server, containerize that too. Use an official Node image, add your Node server code, and expose the port it listens on (e.g. 8080). Nginx can reverse-proxy to it for WSS, or clients can connect directly to that port (not ideal for SSL, so you’d likely proxy for WSS).
Using docker-compose is convenient to define these services. For example, a docker-compose snippet might look like:
yaml CopyEdit
version: '3'
services:
php:
image: php:8.1-fpm-alpine
container_name: hellsword-php
volumes:
- ./src:/var/www/hellsword # your source code
- ./world_data:/var/www/hellsword/world_data # volume for world files
working_dir: /var/www/hellsword
command: php artisan serve # or run php-fpm by default
websocket:
build: ./websocket # if you have a custom Dockerfile for Ratchet or Node
ports:
- "8080:8080" # expose if needed (or you use nginx to proxy)
depends_on:
- php
nginx:
image: nginx:alpine
container_name: hellsword-nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./src:/var/www/hellsword # to serve static files
ports:
- "80:80"
- "443:443"
depends_on:
- php
This is an example; in practice, adjust volumes and commands. The idea is Nginx and PHP share the code volume (so Nginx can serve static files and PHP can execute PHP files). Also share the world data directory so both PHP and Nginx (and potentially Node) see the same files (for instance, if world files are under /world_data
, Nginx might serve screenshots or static dumps if you ever generate any, but mostly PHP will use it). By using Docker, you encapsulate the environment, making it easy to deploy the same setup on another server or scale components independently.
WebSocket Deployment Considerations: If using Ratchet in the PHP container, you’ll need to start the WebSocket server process. In Docker, you might run it via a separate service as shown (like a websocket
service that could simply run php /var/www/hellsword/ws-server.php
). Alternatively, you can use a process manager like Supervisor in the PHP container to run both php-fpm and the Ratchet loop. For clarity, splitting into two containers (one for PHP-FPM handling web requests, one for the WebSocket server) might be easier to manage. They can communicate via shared volumes or network (though the Ratchet server might directly modify the same world files or use database calls just like the PHP-FPM code).
If using Node for WebSockets, the websocket
service would be a Node image. In that case, ensure it’s on the same Docker network so it can communicate with PHP (maybe via HTTP calls or sharing the database).
For Nginx and WebSockets: If you want to serve the WebSocket through the same domain (for WSS on standard ports), configure Nginx to proxy the connection. In your Nginx config, inside the server
block, add something like:
location /ws/ {
proxy_pass http://hellsword-websocket:8080/; # proxy to the websocket service (by name)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
This assumes your websocket server is reachable as hellsword-websocket:8080
(Docker Compose service name). The critical part is the Upgrade
and Connection
headers
which tell Nginx to switch to WebSocket protocol. Without those, WebSocket connections won’t establish through the proxy
. If you prefer, you can let WebSocket traffic connect directly to a port (like have clients use ws://game.example.com:8080
) but that requires opening that port and, for TLS, either handling TLS in the WS server or exposing an unencrypted port which is not ideal. The recommended way is proxying through Nginx so you can use wss on standard port 443.
HTTPS and Security: Obtain an SSL certificate (use Let’s Encrypt in development/production) and configure Nginx for HTTPS (443) with that certificate. Serve the game site over HTTPS always, and use wss://
for WebSocket connections. Modern browsers require WSS if the main page is HTTPS. In Docker, you can either manage certs on the host and mount them, or use another container (like certbot) to manage them.
Scaling Considerations: In the early stage, you might run everything on a single server (one Docker host or one VM). As the game grows, consider scaling the following:
-
PHP Application: If many users are online, PHP-FPM might need more processes or you might run multiple instances. You can scale horizontally by running multiple PHP containers behind Nginx (in Docker, you’d typically put them behind a load balancer or use Docker Swarm/Kubernetes). However, with the flat-file world storage, multiple PHP instances need access to the same files. This can be handled by using a shared volume (like NFS or a cloud storage) or ensuring all containers mount the same host directory. Be careful with concurrent writes from multiple containers (file locking should still work if they share an NFS, but performance might degrade). An alternative for scaling world state would be to migrate tile data into a database or key-value store which all app servers can access – but that’s a big change, so for now, a single shared filesystem is simplest if scaling.
-
WebSocket Server: A single Ratchet or Node instance can handle a good number of connections (hundreds or a few thousand) depending on server resources and traffic frequency. If you need to scale beyond one WebSocket process, you’ll have to partition players between them (for example, run one WS server per region or use a load balancer that does sticky sessions to multiple WS backends). Scaling WebSockets horizontally is non-trivial because clients in different WS servers won’t see each others’ messages unless you bridge the servers. Bridging could be done by having each WS server also publish events to a common message broker (like Redis pub/sub or RabbitMQ) so that an event in server A can be forwarded to server B’s clients if needed. This is probably overkill unless you have a large player base; initially aim for one WebSocket server and vertical scale (give it more CPU/RAM).
-
Database: If using a single DB instance, monitor its load. For mostly reads/writes of user data, a single MySQL or similar can handle a lot. Use indexing and avoid heavy complex queries in gameplay loops. If needed, one can scale MySQL with replicas or move to a cluster, but that’s a future concern.
-
Static Content: World files and assets could be served via a CDN if needed, but since they are generated and frequently updated, it might be easier to keep them on the origin server. Perhaps static art assets (images, js, css) can go to CDN to offload those.
Security Measures:
-
Rate Limiting: Protect the HTTP endpoints from abuse. For example, limit login attempts to prevent brute force (use something like fail2ban or Nginx’s
limit_req_zone
). Throttle any expensive API calls. Nginx can rate limit by IP or key; you might implement, for instance,limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
for certain paths. Similarly, in the WebSocket protocol, if a client is spamming actions (sending a flood of messages beyond normal play), detect that in the server and possibly drop or penalize that client. This prevents denial-of-service attacks via spamming game actions. -
Input Validation: Since the server is authoritative, ensure every client input is validated. Never trust coordinates or card IDs from the client without checking. For example, if the client says “place block at (x,y,z)”, verify that (x,y,z) is adjacent to an existing block or within allowed build area, not far away or inside protected zones. Likewise, verify players can’t place blocks in tiles they don’t own or during battles if that’s disallowed. Sanitize any identifiers to prevent path traversal (if a client somehow sends a tile name, ensure it maps to allowed filenames).
-
Cheat Prevention: Aside from validating actions, consider game-specific anti-cheat. If the game has a concept of physics or movement, ensure on the server that players don’t move faster than possible or clip through walls (if movement is handled client-side at all). Since movement can be controlled via the physics of the voxel engine on the client, you still want the server to enforce limits (e.g. don’t allow moving 10 tiles in one second if that’s impossible normally). For card play, prevent “replaying” the same card: once a card is used, mark it as spent. A client might try to send the same action twice due to a bug or malicious intent – handle idempotency or duplication by tracking action IDs or simply ignoring actions that don’t make sense in the current state.
-
Secure WebSockets: If you proxy through Nginx with SSL, your WebSocket traffic is encrypted. If not, ensure the WS server itself runs with SSL (which is more complicated to set up). Always require logins and possibly use tokens for the WebSocket auth (you could have the client send a JWT or session cookie upon connecting to the WS, and the WS server checks it with the main server or against a cache of logged-in tokens). This prevents unauthorized connections or hijacking.
-
File Permissions: The world flat files should be stored in a directory not accessible via the web except through the game logic. In the Nginx config above,
root
is the web root, which might not include the directory where flat files reside (you might keep them one level up to avoid direct download). If you do want to allow downloading a world state (for backup or debugging), secure it. But generally, serve world data only via the PHP or WS mechanisms to authenticated users. On the server file system, ensure the files are owned by the web user and not readable by others. Regularly back up these files, as they represent the world state! -
Docker Security: If using Docker, keep your images updated (security patches). Do not run processes as root in containers if not needed (the official images often use non-root users for services like MySQL, but PHP-FPM sometimes runs as www-data which is fine). Limit container resources if necessary to avoid one service starving others (you can use Docker Compose resource limits). Also, if exposing to the internet, consider using a firewall (like ufw) to only allow necessary ports (80,443, maybe 22 for SSH). Docker networks are internal for container-to-container, which is good to isolate the DB and internal comms from outside.
Logging and Monitoring: Set up logging for debugging and stability. Nginx will log access and errors – keep an eye on those for 404s, 500s, or unusual traffic. PHP can log errors (configure error_log
). For the WebSocket server, you might need to add your own logging (e.g. print to console or a file when important events happen). In production, consider aggregating logs or using monitoring tools. Also monitor performance: use top/htop on the server, or better, tools like Prometheus/Grafana (if you want to get fancy) to track CPU, memory, and network. This will help identify bottlenecks as you scale.
Example: Running the Stack Locally: In development, you might run without Docker (just PHP’s built-in server or Apache, etc.). But if using Docker, after writing your docker-compose.yml
, you’d run docker-compose up -d
. Then ensure the WebSocket server is running (check its logs). Navigate to your game’s URL (you might map localhost:80
to it). You should see the landing page or login. From there, test end-to-end: create account, log in, world loads, etc. Debug any container networking issues (common ones: forgetting to apply the Nginx config inside container, or PHP not having the correct hostnames).
Future Scaling: The plan above gets the core game running on a single server or a couple of containers. If the game becomes popular, consider using a cloud service for scaling. For example, container orchestration (Kubernetes) could manage multiple instances of each service and handle load balancing. Using a service like AWS, you might use an ALB (Application Load Balancer) in front of Nginx containers, and an EFS (Elastic File Storage) for the world files so all instances share them. Or transition world data to a database like DynamoDB or MongoDB for easier horizontal scaling. Those changes are beyond the initial implementation, but the key is to keep the architecture modular (separating concerns into services and using Docker networks) so that moving pieces to separate hosts later is straightforward.
By following this plan – setting up a robust PHP/Nginx backend, choosing a suitable voxel rendering approach, implementing the card and battle mechanics, ensuring real-time sync with WebSockets, and deploying carefully – you will achieve a functional and scalable Hellsword prototype. Focus on getting the core features working end-to-end (world building, card collection, basic combat) and then iteratively refine performance and balance. Good luck with your dark fantasy voxel adventure!