Best design for the client/server interaction in a turn-based game?
July 1, 2010 4:40 PM
I'm programming a one-player, turn-based, client-server game for fun and self-edification. I need a little help with communicating changes to the game model from server to client.
Here's what I have so far:
All game logic happens on the server. The client is just, well, a client—it pipes input from the user to the server, receives a representation of the game world from the server, and displays that representation in a meaningful way to the user.
So, the client keeps track of the game model, but only to the extent that it needs to. It needs to know the character's immediate surroundings in order to draw the map, and if the user clicks on the "Inventory" button, it'll retrieve the inventory model from the server so it can display it. Et cetera.
Most of the time, the client just sits there, waiting for a command from the user. When it receives one, it issues a request to the server. That request consists of a command name (such as "moveNorth" or "equipItem"), plus zero or more parameters. For example, the server might expect two parameters for equipItem: the ID of the item being equipped, and the name of the inventory slot the player is trying to put it in.
The server receives the request, determines the outcome, and responds to the client. That response consists of:
I can think of a few ways of handling this:
a. The server returns a complete representation of the entire game model in response to every action, and the client redraws the entire view. This is pretty obviously the Wrong Answer.
b. The server tells the client which parts of the model have changed (e.g., the local map), but doesn't provide any further details. The client will have to issue a separate request (e.g., "getLocalMap") if and when it needs to know the details.
(Maybe talking to an NPC adds an item to the character's inventory. This sets an "inventoryDirty" flag on the client. The next time the player pulls up the inventory window, the client sees this flag and realizes that its current cache of the inventory model is out-of-date—so it refreshes that local model via a "getInventory" request before showing the inventory view.)
c. The server tells the client which parts of the model have changed, and provides full information about those changes. This means fewer requests, but it also means that my client-server protocol (and the underlying code) will be much more complicated.
So, my three-part question:
i. Which, if any, of the above strategies sound like a Good Idea? Is there a strategy (d) that I have overlooked? (b) seems like my best bet, but perhaps someone can offer a more nuanced perspective, or a better idea. (I'm obviously inexperienced with this stuff, so please be gentle.)
ii. Maybe parts 1 and 2 of the server's response are unnecessary? Perhaps success and failure are meaningless, and all that matters to the client is how the model has changed.
iii. Can you recommend a good Web site / mailing list / other resource for talking to other programmers about these kinds of things? Most such resources are oriented toward specific languages and platforms, but the languages and platforms I'm using (which are not important to the question) are very rarely used for this kind of thing. What I really need is a forum that's concerned with high-level architectural concepts, rather than the details of implementation.
Here's what I have so far:
All game logic happens on the server. The client is just, well, a client—it pipes input from the user to the server, receives a representation of the game world from the server, and displays that representation in a meaningful way to the user.
So, the client keeps track of the game model, but only to the extent that it needs to. It needs to know the character's immediate surroundings in order to draw the map, and if the user clicks on the "Inventory" button, it'll retrieve the inventory model from the server so it can display it. Et cetera.
Most of the time, the client just sits there, waiting for a command from the user. When it receives one, it issues a request to the server. That request consists of a command name (such as "moveNorth" or "equipItem"), plus zero or more parameters. For example, the server might expect two parameters for equipItem: the ID of the item being equipped, and the name of the inventory slot the player is trying to put it in.
The server receives the request, determines the outcome, and responds to the client. That response consists of:
- a flag indicating whether the action was successful;
- if the action wasn't successful, an error code (e.g., perhaps the moveNorth action failed because there's a solid object in the way); and
- any resulting changes to the model that the client will be interested in.
I can think of a few ways of handling this:
a. The server returns a complete representation of the entire game model in response to every action, and the client redraws the entire view. This is pretty obviously the Wrong Answer.
b. The server tells the client which parts of the model have changed (e.g., the local map), but doesn't provide any further details. The client will have to issue a separate request (e.g., "getLocalMap") if and when it needs to know the details.
(Maybe talking to an NPC adds an item to the character's inventory. This sets an "inventoryDirty" flag on the client. The next time the player pulls up the inventory window, the client sees this flag and realizes that its current cache of the inventory model is out-of-date—so it refreshes that local model via a "getInventory" request before showing the inventory view.)
c. The server tells the client which parts of the model have changed, and provides full information about those changes. This means fewer requests, but it also means that my client-server protocol (and the underlying code) will be much more complicated.
So, my three-part question:
i. Which, if any, of the above strategies sound like a Good Idea? Is there a strategy (d) that I have overlooked? (b) seems like my best bet, but perhaps someone can offer a more nuanced perspective, or a better idea. (I'm obviously inexperienced with this stuff, so please be gentle.)
ii. Maybe parts 1 and 2 of the server's response are unnecessary? Perhaps success and failure are meaningless, and all that matters to the client is how the model has changed.
iii. Can you recommend a good Web site / mailing list / other resource for talking to other programmers about these kinds of things? Most such resources are oriented toward specific languages and platforms, but the languages and platforms I'm using (which are not important to the question) are very rarely used for this kind of thing. What I really need is a forum that's concerned with high-level architectural concepts, rather than the details of implementation.
It sounds like what you're working on might fit into at least the broader definitions of a roguelike, in which case:
http://groups.google.com/group/rec.games.roguelike.development/topics
is an excellent community.
posted by 256 at 5:25 PM on July 1, 2010
http://groups.google.com/group/rec.games.roguelike.development/topics
is an excellent community.
posted by 256 at 5:25 PM on July 1, 2010
Start with (a) and finish the rest of the game. Then come back and do (c).
I think the return code is ok.
posted by fleacircus at 5:38 PM on July 1, 2010
I think the return code is ok.
posted by fleacircus at 5:38 PM on July 1, 2010
I'm a little confused about the architecture here. It's a single player game but it's networked? If it's not networked then why bother with the complication of client/server?
In general if the game is networked then you can't have each player action wait on a server response because the latency will get really annoying -- even if it's small, if it happens for every single action it will feel very laggy. So you really don't find much of the dumb/thin client type architecture. To be honest, networked multiplayer games (RPG/FPS) use the words client and server but really that's kind of a misnomer because there is a complete game engine on both ends capable of fully simulating play. When the player takes action the client calculates the result and displays it to the player instantaneously, and then 100ms or so later when it receives an update from the server who also performed the same calculation, it has to reconcile the two game states. Most of the time the two calculated the same thing and there's nothing to reconcile, but if another player is involved that is where the two independent simulations can diverge and the client has to update its game state to match that of the server. But all that work is really necessary to have the game feel responsive and smooth because the latency of waiting on the server would make it just unbearable.
Now, that's all for games where fighting happens in real time, so it might not necessarily apply to a turn based game. But again you say that you're having the client contact the server for even things like messing around in inventory and moving, and those kind of UI things will feel very cheap and annoying if they have to wait for 1x RTT every time. I mean, if you're set on having a dumb client architecture go ahead, but the UI is just going to feel weird so you should consider having some or all of the game rules computed in the client (and verified with the server so that they don't diverge.)
posted by Rhomboid at 5:39 PM on July 1, 2010
In general if the game is networked then you can't have each player action wait on a server response because the latency will get really annoying -- even if it's small, if it happens for every single action it will feel very laggy. So you really don't find much of the dumb/thin client type architecture. To be honest, networked multiplayer games (RPG/FPS) use the words client and server but really that's kind of a misnomer because there is a complete game engine on both ends capable of fully simulating play. When the player takes action the client calculates the result and displays it to the player instantaneously, and then 100ms or so later when it receives an update from the server who also performed the same calculation, it has to reconcile the two game states. Most of the time the two calculated the same thing and there's nothing to reconcile, but if another player is involved that is where the two independent simulations can diverge and the client has to update its game state to match that of the server. But all that work is really necessary to have the game feel responsive and smooth because the latency of waiting on the server would make it just unbearable.
Now, that's all for games where fighting happens in real time, so it might not necessarily apply to a turn based game. But again you say that you're having the client contact the server for even things like messing around in inventory and moving, and those kind of UI things will feel very cheap and annoying if they have to wait for 1x RTT every time. I mean, if you're set on having a dumb client architecture go ahead, but the UI is just going to feel weird so you should consider having some or all of the game rules computed in the client (and verified with the server so that they don't diverge.)
posted by Rhomboid at 5:39 PM on July 1, 2010
It's a single player game but it's networked? If it's not networked then why bother with the complication of client/server?
A fair point. It's technically a multiplayer game, but players affect each other indirectly. Players' actions have effects on the shared game world, but players don't see each other on the map, don't interact in realtime, and can play on independent schedules.
posted by ixohoxi at 5:43 PM on July 1, 2010
A fair point. It's technically a multiplayer game, but players affect each other indirectly. Players' actions have effects on the shared game world, but players don't see each other on the map, don't interact in realtime, and can play on independent schedules.
posted by ixohoxi at 5:43 PM on July 1, 2010
I would think of this as an event driven system: The server sends events to the client to describe changes to the client's state, and the client interprets events and takes appropriate action. So events would be along the lines of, "You lost 10 hp" or "The grue licks its lips menacingly".
If you don't want to invent your own language specifically to describe events, you might want to set up a remote object proxy that knows how to automatically serialize method invocations and push them over the wire. In other words, your server would model the client's state as an object, and make method calls to that object to update the client state. Those method calls on the server-side would be stubs that bundle up the parameters passed to them and forward them to the client. Depending on the language you're using, this could be easy or hard to automate.
Rhomboid is right that your client shouldn't wait for the OK from a server before it locally performs an action, but it's not realistic or secure to expect your client to have complete game information locally --- they certainly don't with an FPS or MMORPG --- so you're still going to have to wait on the server to learn the outcome of an action sometimes.
posted by qxntpqbbbqxl at 6:23 PM on July 1, 2010
If you don't want to invent your own language specifically to describe events, you might want to set up a remote object proxy that knows how to automatically serialize method invocations and push them over the wire. In other words, your server would model the client's state as an object, and make method calls to that object to update the client state. Those method calls on the server-side would be stubs that bundle up the parameters passed to them and forward them to the client. Depending on the language you're using, this could be easy or hard to automate.
Rhomboid is right that your client shouldn't wait for the OK from a server before it locally performs an action, but it's not realistic or secure to expect your client to have complete game information locally --- they certainly don't with an FPS or MMORPG --- so you're still going to have to wait on the server to learn the outcome of an action sometimes.
posted by qxntpqbbbqxl at 6:23 PM on July 1, 2010
Most game engines only send out differences, what changed in a turn. (with the occasional sync logic involved). For higher framerate games, they also use dead-reckoning when it comes to movement, so that only the first "move forward" needs to be sent, after that both server and client can keep computing the incremented position (with error boundaries etc). But if it is turn-based you don't need that.
posted by lundman at 8:02 PM on July 1, 2010
posted by lundman at 8:02 PM on July 1, 2010
I'm assuming you are using an OO type system here.
Every action that mutates the game world presumably does so through a method on an object somewhere. That method needs to write to a transaction log, which the client picks up and applies to its own internal model (hopefully it's the same code in both places.)
You'll still need to be able to copy the entire relevant game world down, for startup, reconnect, and out-of-sync issues.
To get ultrafancy, your player's object (the player itself is an object in the game world, presumably) steps into situation and registers itself with the relevant game world objects for callback on update. That is, you step into the room and tell the room that all updates in that room should go to it. Likewise, every child object inside that room knows to pass transactions up to the room scope.
Does that make any sense?
posted by joshu at 9:17 PM on July 1, 2010
Every action that mutates the game world presumably does so through a method on an object somewhere. That method needs to write to a transaction log, which the client picks up and applies to its own internal model (hopefully it's the same code in both places.)
You'll still need to be able to copy the entire relevant game world down, for startup, reconnect, and out-of-sync issues.
To get ultrafancy, your player's object (the player itself is an object in the game world, presumably) steps into situation and registers itself with the relevant game world objects for callback on update. That is, you step into the room and tell the room that all updates in that room should go to it. Likewise, every child object inside that room knows to pass transactions up to the room scope.
Does that make any sense?
posted by joshu at 9:17 PM on July 1, 2010
I'm not sure I'd dismiss approach (a) so quickly.
You could have a very thin client which basically receives the entire contents of the screen (or at least the parts that have changed) from the server each turn, and only needs to transmit specific inputs from the player. This works well with text, especially if the players are on the same continent as the server. If you haven't already, check out the roguelikes that can be played via a generic ssh client - they often do exactly the kind of "thin client, single player, shared world" thing that it sounds like you want. The server provides the screen contents, the client provides keypresses. Everyone is happy.
This approach can also work with a graphical display if it's possible to encode the contents of the display in a fairly small amount of information. For example, if the display uses tiled graphics and leaves details which don't affect gameplay (e.g. animations) up to the client, you could just have the server provide enough information to assemble a screen full of tiles, and any appropriate text, each turn and let the client do the rest of the work.
If you get to write your own client (unlike the roguelikes I mentioned) there are all sorts of optimisations you can apply, like storing known inventory or discovered but not currently visible map data on the client side. Any changes should be transmitted from server to client once per turn, and you can let the client side handle the player's access to the information on the principle that the player gets to know everything the client does. Basically this means using something like approach (a) for rich, volatile information (e.g. the player's surroundings) and (c) for more predictable information (e.g. inventory contents). This will be more efficient than sending a query every time the player checks their inventory.
Anyway, I'm a complete amateur at this stuff, but I've written a roguelike and a couple of networked games (using pure procedural programming; OO frightens me) and the above is what I'd do unless I was able to simplify the structure of the game to the point where (c) becomes possible. (b) sounds like it could be more trouble than it's worth.
posted by A Thousand Baited Hooks at 7:56 AM on July 2, 2010
You could have a very thin client which basically receives the entire contents of the screen (or at least the parts that have changed) from the server each turn, and only needs to transmit specific inputs from the player. This works well with text, especially if the players are on the same continent as the server. If you haven't already, check out the roguelikes that can be played via a generic ssh client - they often do exactly the kind of "thin client, single player, shared world" thing that it sounds like you want. The server provides the screen contents, the client provides keypresses. Everyone is happy.
This approach can also work with a graphical display if it's possible to encode the contents of the display in a fairly small amount of information. For example, if the display uses tiled graphics and leaves details which don't affect gameplay (e.g. animations) up to the client, you could just have the server provide enough information to assemble a screen full of tiles, and any appropriate text, each turn and let the client do the rest of the work.
If you get to write your own client (unlike the roguelikes I mentioned) there are all sorts of optimisations you can apply, like storing known inventory or discovered but not currently visible map data on the client side. Any changes should be transmitted from server to client once per turn, and you can let the client side handle the player's access to the information on the principle that the player gets to know everything the client does. Basically this means using something like approach (a) for rich, volatile information (e.g. the player's surroundings) and (c) for more predictable information (e.g. inventory contents). This will be more efficient than sending a query every time the player checks their inventory.
Anyway, I'm a complete amateur at this stuff, but I've written a roguelike and a couple of networked games (using pure procedural programming; OO frightens me) and the above is what I'd do unless I was able to simplify the structure of the game to the point where (c) becomes possible. (b) sounds like it could be more trouble than it's worth.
posted by A Thousand Baited Hooks at 7:56 AM on July 2, 2010
'You Ain't Gonna Need It' applies here -- unless option (a) is a burden, why not just serialize the game state and send it over the wire? If you have so much game state that this is cumbersome, that's the time to ask this question to yourself.
Language specifics come into play at that point, but it's not hard to send key => object pairs and teach your 'game state' object to understand which keys correspond to which of its members and replace them. A more significant problem is the concurrency when more than one user changes the same bit of game state.
posted by zvs at 2:33 PM on July 2, 2010
Language specifics come into play at that point, but it's not hard to send key => object pairs and teach your 'game state' object to understand which keys correspond to which of its members and replace them. A more significant problem is the concurrency when more than one user changes the same bit of game state.
posted by zvs at 2:33 PM on July 2, 2010
Thanks, all.
The original concept was something like Nethack meets Operation Overkill/Land of Devastation (two old BBS door games—they were multi-player RPGs). In OO/LoD, players could build bases on the world map (and try to raid each other's bases), form alliances (what we would call "clans" today) to pool resources, backstab their clan, etc. Major events that occurred in the course of one player's gameplay (say, a nuclear explosion) had lasting effects on the shared game world. I was going for something similar—a kind of asynchronous multiplayer where you only see the effects other players leave on the game world, not the players themselves.
But after considering the advice here (and tinkering with my existing code a bit more), I've decided to eliminate the client-server paradigm, and just make a standalone game. The main motivation is to hone my skills with various technologies and architectural concepts, and I was probably (as usual) biting off too much at once.
Things have been falling much more easily into place since I eliminated the server from the equation. If I end up with a playable game (unlikely, given my track record for finishing this kind of thing), I'll post it to MeFi Projects.
Thanks again!
posted by ixohoxi at 8:58 AM on July 13, 2010
The original concept was something like Nethack meets Operation Overkill/Land of Devastation (two old BBS door games—they were multi-player RPGs). In OO/LoD, players could build bases on the world map (and try to raid each other's bases), form alliances (what we would call "clans" today) to pool resources, backstab their clan, etc. Major events that occurred in the course of one player's gameplay (say, a nuclear explosion) had lasting effects on the shared game world. I was going for something similar—a kind of asynchronous multiplayer where you only see the effects other players leave on the game world, not the players themselves.
But after considering the advice here (and tinkering with my existing code a bit more), I've decided to eliminate the client-server paradigm, and just make a standalone game. The main motivation is to hone my skills with various technologies and architectural concepts, and I was probably (as usual) biting off too much at once.
Things have been falling much more easily into place since I eliminated the server from the equation. If I end up with a playable game (unlikely, given my track record for finishing this kind of thing), I'll post it to MeFi Projects.
Thanks again!
posted by ixohoxi at 8:58 AM on July 13, 2010
This thread is closed to new comments.
Partial model updates aren't that hard. The name of the property you're updating, and its new value. That's about all you need. In more complex structures, read "path to the property" instead of "name of the property".
posted by Leon at 5:17 PM on July 1, 2010