Multiplayer Board Game Design Questions

Started by
8 comments, last by Nokobon 13 years, 2 months ago
Hi,

I'm new to this board as well as a newbie to network programming.
I choose C++ with SDL to make a game. My goal is a simple board game (so it's turn based) that supports multiplayer games via network (using SDL_net).

Now I'm stuck in the conceptual design phase.
I'd like to use a client-/server-architecture, but I'm really unsure about how to split the game on both and what they need to send over the network.

Well, of course the client is responsible for user input and output and I think the game logic is on server site, which is datastructures(board, player, tokens,...), rules, etc.
But I just can't figure out what the communication between client and server will look like.

For example: What information should the client send to the server when the player moves a token?
- just the event that a mouseclick occured and where it pointed to
- something like a message that a specific token (maybe identified by a number) has new coordinates
- or even complete objects like a token or the board (is that possible?)
- ???

I've read many tutorials on network and gameprogramming in the last few days, but neither gave me a light bulb.
I have no idea how to implement such network stuff...

As I have never done something like that before I would be glad to get some hints from you guys.

Thanks in advance.
Advertisement
There are several routes you can take.


Probably the two simplest are to have the host do everything but display and the clients display in a completely independent way; or to have each machine run their own simulation and coordinate transactions between the two. You shouldn't need any additional complexity for a simple board game.


You can have everybody send their requests to the game host, and have the game host send the game state to everybody. You don't want every button press because you'll want to have button presses that access menus and such. You'd need to break your game down into transactions that can go between them, such as a client sending "I am rolling dice", and the server sending "Player 2 rolled 4 and 3". In this situation you could implement the entire game both in text and in graphics without much difficulty. You should also have a few commands that will repeat information about the game board and the game state in case of a problem. If a player drops out you will need the server to replace them, and the dropped player must terminate the game because they are no longer hooked up to the simulation.

You can have everybody run a complete simulation on their game, with one simulation being authoritative. You can similarly share a random number generator between machines to keep it in sync. You still need to figure out a transaction based system that explains what you are going to send around. It is more difficult, but I've taken this route on a game before. It does add significant complexity, but if you must handle the case of continuing a game offline it is worth it.


You will also need your own simple protocols to keep the game in sync. That is something that you need to work out on your own, generally through experimentation and trial-and-error. It is best to keep it simple and verify your protocol by hand until you have worked out every problem you can imagine.

For example: What information should the client send to the server when the player moves a token?
- just the event that a mouseclick occured and where it pointed to
- something like a message that a specific token (maybe identified by a number) has new coordinates
- or even complete objects like a token or the board (is that possible?)


In general, the client is responsible for turning user input ("mouse clicks") into game-level semantic events ("move object A to position B").
Semantic events are what are sent to the server. Usually, the server will validate that this is in fact a valid event, and calculate the outcome.
Every so often (when objects change, or 10 times a second, or whatever), the server will send a list of what states have changed to the client so it can update its version of the game state.
Finally, it's the role of the client to turn a semantic game state ("object A is in position B") into a display state ("model X is playing at world position Y using animation Z")

These general rules of thumb can be modified to suit the particular character and architecture of the game, but it's a good place to start for most games.
enum Bool { True, False, FileNotFound };
Thank you! Your answers already helped me a lot.

If I understand you correctly I need to define all state transactions (or as hbplus0603 called it: semantic game events) that every one in the game needs to be informed of. In a board game that should be an acceptable amount...

For example:
"Player 1 chose token A" would not be such a transaction (as it's not important to the other players), but "Player 1 moved token A to X,Y" would be.

As I'm used to think object-oriented the idea to make a class inherited by an abstract "transaction" for every concrete transaction comes to my mind.
But I wonder if this is the common way...
Can you send those objects over network with SDLNet_TCP_Send(TCPsocket sock, const void *data, int len)?
Do I need some kind of serialization?
Or do I need to stay more low level and define some sort of protocoll that says something like: The first byte of the data says what type of event took place and the next 3 Bytes define what exactly happened...
At least I guess sending a string like "Player A rolled the Dice" is not what I what to do. :D

Sorry if these are dumb questions, but I lack in experience of network communication.

As I'm used to think object-oriented the idea to make a class inherited by an abstract "transaction" for every concrete transaction comes to my mind.


Don't get trapped in a "pure Objects" thinking. In some sense, it's been known for a long time that objects have failed.

Yes, you will need to find a way to serialize your events. Whether it's a text representation, a traditional exhale/inhale object graph (generally very inefficient!) or something like a data structure defined per packet, doesn't make as much of a difference in a strategy game as it does in a fast-paced game.
I'd try to express things as structs:

struct PacketHeader {
unsigned short size;
unsigned short type;
BEGINVISITOR(packetHeader)
VISIT(size)
VISIT(type)
ENDVISITOR()
};


struct MoveTokenPacket {
enum { ID = 1 };
PacketHeader hdr;
unsigned short token;
unsigned short targetX;
unsigned short targetY;

BEGINVISITOR(MoveTokenPacket)
VISIT(hdr)
VISIT(token)
VISIT(targetX)
VISIT(targetY)
ENDVISITOR()
};

struct TextChatPacket {
enum { ID = 2 };
PacketHeader hdr;
std::string text;

BEGINVISITOR(TextChatPacket)
VISIT(hdr)
VISIT(text)
ENDVISITOR()
};



Then I'd implement visitors to serialize to/from a byte stream, using some simple macros:


#define BEGINVISITOR(class_name) \
template<typename Visitor> void visit(Visitor &v) { \
v.in_class(#class_name);

#define VISIT(x) \
v.visit(x, #x);

#define ENDVISITOR(x) \
v.end_class(); \
}

class stream_out {
public:
std::vector<char> data;
template<typename T> visit(T const &t, char const *name) {
t.visit(*this);
}
// define the implementation for each data type you intend to use
template<> visit<unsigned char> visit(unsigned char const &ch, char const *name) {
data.append((char const *)&ch, (char const *)&ch + 1);
}
template<> visit<unsigned short> visit(unsigned short const &ch, char const *name) {
data.append((char const *)&ch, (char const *)&ch + sizeof(unsigned short));
}
template<> visit<std::string> visit(std::string const &str, char const *name) {
if (str.length() > 255) throw std::exception("too long string");
unsigned char length = str.length();
visit(length);
data.append(str.c_str(), str.c_str() + str.length());
}


This is but one of many ways that the tie between data, events and network packets can be made.
enum Bool { True, False, FileNotFound };
Thank you for that detailed answer!

Let me get this right...

In your example the class stream_out serializes the packet to a vector and for deserialization we need another class on the receivers side.
There the received data will be casted to a vector and then be taken into a packet-struct again.
The PacketHeader will contain for example sizeof(MoveTokenPacket) as size and 1 as type.

Maybe my knowledge about C++ is not adequate enough...
I don't get why you use an enum for the packet id, why not unsigned short?
And I wonder what kind of function append(char const*, char const*) is.
I would declare data as std::verctor<char*> and use push_back(char*) to add an element of the packet to the data vector.

Please tell me if I'm wrong...
Enums are better than numeric values because it's virtually impossible to accidentally store an invalid number in there.

The append function here adds char data to the end of the vector of chars, so that in the end you have one long set of chars to send, which you can do in one call, passing the data directly to the networking call. That's quite different from having a vector of char* which is pretty useless really - if you push a char* onto a vector then you're just storing a set of pointers to the strings, not the strings themselves. Not only does this mean you don't have a single string which you can write out, but it can lead to all sorts of errors as your pointers can end up pointing to invalid memory and so on. This is a common mistake made by beginners - perhaps ask on the "For Beginners" forum why using char* for strings is a bad idea!

Enums are better than numeric values because it's virtually impossible to accidentally store an invalid number in there.

The append function here adds char data to the end of the vector of chars, so that in the end you have one long set of chars to send, which you can do in one call, passing the data directly to the networking call. That's quite different from having a vector of char* which is pretty useless really - if you push a char* onto a vector then you're just storing a set of pointers to the strings, not the strings themselves. Not only does this mean you don't have a single string which you can write out, but it can lead to all sorts of errors as your pointers can end up pointing to invalid memory and so on. This is a common mistake made by beginners - perhaps ask on the "For Beginners" forum why using char* for strings is a bad idea!


Okay, so I understand why to user vector<char>:
It's like a dynamic array and it guarantees that the chararacters are stored in a row in memory.
In fact we also could use char[] instead, but the disadvantage would be that it has a fixed size, right?

But I still don't get why to use "append" here? It's a function of the string class, but you use it on a vector...

But I still don't get why to use "append" here? It's a function of the string class, but you use it on a vector...


Yeah, that's a typo. It's "insert(vec.end(), from, to)" on the vector template.

I use an enum because you can't easily define the value of a member variable within the declaration of the class in C++. Also, a variable cannot be used as a constant in future template specializations, for example.
enum Bool { True, False, FileNotFound };

Yeah, that's a typo. It's "insert(vec.end(), from, to)" on the vector template.

I use an enum because you can't easily define the value of a member variable within the declaration of the class in C++. Also, a variable cannot be used as a constant in future template specializations, for example.


Okay, now I do understand.
Thank you!

This topic is closed to new replies.

Advertisement