1
\$\begingroup\$

Background

I'm using the lockstep model for a multiplayer Node.js/Socket.IO game in a client-server architecture. User input (mouse or keypress) is parsed into commands like 'attack' and 'move' on the client, which are sent to the server and scheduled to be executed on a certain tick. This is in contrast to sending state data to clients, which I don't wish to use due to bandwidth issues.

Each tick, the server will send the list of commands on that tick (possibly empty) to each client. The server and all clients will then process the commands and simulate that tick in exactly the same way. With Node.js this is actually quite simple due to possibility of code sharing between server and client. I'll just put the deterministic simulator in the /shared folder which can be run by both server and client. The server simulation is required so that there is an authoritative version of the simulation which clients cannot alter.

Problem

Now, the game has many entity classes, like Unit, Item, Tree etc. Entities are created in the simulator. However, for each class, it has some methods that are shared and some that are client-specific. For instance, the Unit class has addHp method which is shared. It also has methods like getSprite (gets the image of the entity), isVisible (checks if unit can be seen by the client), onDeathInClient (does a bunch of stuff when it dies only on the client like adding announcements) and isMyUnit (quick function to check if the client owns the unit). Up till now, I have been piling all the client functions into the shared Unit class, and adding a this.game.isServer() check when necessary. For instance, when the unit dies, it will call if (!this.game.isServer()) { this.onDeathInClient(); }.

This approach has worked pretty fine so far, in terms of functionality. But as the codebase grew bigger, this style of coding seems a little strange. Firstly, the client code is clearly not shared, and yet is placed under the /shared folder. Secondly, client-specific variables for each entity are also instantiated on the server entity (like unit.sprite) and can run into problems when the server cannot instantiate the variable (it doesn't have Image class like on browsers).

So my question is, is there a better way to organize the client code, or is this a common way of doing things for lockstep multiplayer games? I can think of a possible workaround, but it does have its own problems.

Possible workaround (with problems)

I could use Javascript mixins that are only added when in a browser. Thus, in the /shared/unit.js file in the /shared folder, I would have this code at the end:

if (typeof exports !== 'undefined') module.exports = Unit;
else mixin(Unit, LocalUnit);

Then I would have /client/localunit.js store an object LocalUnit of client-side methods for Unit.

Now, I already have a publish-subscribe system in place for events in the simulator. To remove the this.game.isServer() checks, I could publish entity-specific events whenever I want the client to do something. For instance, I would do this.publish('Death') in /shared/unit.js and do this.subscribe('Death', this.onDeathInClient) in /client/localunit.js. But this would make the simulator's event listeners list on the server and the client different. Now if I want to clear all subscribed events only from the shared simulator, I can't. Of course, it is possible to create two event subscription systems - one client-specific and one shared - but now the publish() method would have to do if (!this.game.isServer()) { this.publishOnClient(event); }.

All in all, the workaround off the top of my head seems pretty complicated for something as simple as separating the client and shared code. Thus, I wonder if there is an established and simpler method for better code organization, hopefully specific to Node.js games.

\$\endgroup\$
2
  • \$\begingroup\$ This is off topic. Socket.io is convenient to use, but it inflates packets like crazy to achieve that, better to use a more low level solution like github.com/Worlize/WebSocket-Node Read up buildnewgames.com/optimizing-websockets-bandwidth and blog.lightstreamer.com/2013/10/… if you want to learn more \$\endgroup\$
    – dreta
    Commented Oct 26, 2013 at 14:23
  • \$\begingroup\$ @dreta Wow I didn't know socket.io has so much overhead! This would be a good optimization to make in the future, when my game's more stable :) Thanks for the links. A quick question (might possibly sound dumb): Would you know if a larger packet size results in longer transmission times and hence more lag? In other words, will switching to pure WebSockets reduce latency in my game? \$\endgroup\$
    – Wakaka
    Commented Oct 26, 2013 at 14:34

1 Answer 1

1
\$\begingroup\$

My advice would be to separate the rendering and interaction code from the simulation code. The simulation code is what is truly shared, whilst the rest is client only. As you say, the tricky part will be managing the links between the two.

In an RTS I built once this was done successfully via an event system. Important events were published by the simulator and these were used to update the UI etc. These included "UnitDestroyed", "ResourcesChanged", etc. Some things should not be done purely using events. For example, I used the "UnitCreated" event to build any renderers required for the new unit. The renderers stored a reference to the simulation unit so that they could update each tick.

\$\endgroup\$
4
  • \$\begingroup\$ Cool, your approach sounds similar to my little workaround :D Except that instead of adding a client-side mixin to the unit, you built renderers separately for each unit? Does that mean there there is now a UnitRenderer instantiated for each unit? Where would you put non-rendering related (but still client-side) functions for the unit? (for example, the isMyUnit I mentioned) \$\endgroup\$
    – Wakaka
    Commented Oct 26, 2013 at 13:11
  • \$\begingroup\$ Yes I had separate classes for these functions (mixins may have been better but the language I was using didn't support them). isMyUnit is a function of the unit and the client. Each unit should have an assigned team (it could just be an integer) and the user interface will also have an assigned team. To perform the check, just compare the two. The simulation should be entirely client independent. \$\endgroup\$
    – user36159
    Commented Oct 26, 2013 at 13:31
  • \$\begingroup\$ Thanks for the clarification! Nice to know that someone has actually tried this idea and succeeded :) I guess I will go ahead with the mixins and event system. A question about the event system: do all your classes (like UnitRenderer) each maintain a list of subscriptions? \$\endgroup\$
    – Wakaka
    Commented Oct 26, 2013 at 14:25
  • \$\begingroup\$ I used the EventBus in Google Guava. Each instance would register itself to the event bus. Probably not the most efficient solution but it did the trick. \$\endgroup\$
    – user36159
    Commented Oct 26, 2013 at 14:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .