Writing your own client - ogrady/inf3project GitHub Wiki

Architecture

Writing your own client can be done in several steps which are then put together. Of course, you can come up with your very own design, but I propose the following layers for your architecture, which should be built top down:

Here you have employed the Model View Controller pattern, which tries to separate representation, data and functionality. This comes in handy when you are trying to exchange your old, boring monochrome 2D GUI for, let's say, an awesome 3D with the latest shaders and whatnot, without fiddling with all layers of your project. Here our GUI is the View, our backend is the Model, and connector and parser together are the Controller.

Connector

Establishes a TCP-connection to the server and keeps it alive until you disconnect. It is reponsible for sending messages to the server and receiving messages from it. So you will most likely end up with two streams that can be taken from the connection: one for reading, one for writing. While you should be able to do something like this:

function send(string message) {
  writeStream.send(message);
  writeStream.flush();
}

Your reading stream should constantly be listening to what the server says. So you will have something like:

string messageFromServer;
while((messageFromServer = readStream.read()) != NULL) {
  buffer.append(messageFromServer);
}

Obviously, readStream.read() is a blocking operation and will actively wait for input from the server. The whole block should therefore run in an own thread, to avoid freezing the rest of your client. The buffer should store portions of one message. You will most likely read more than once to receive one full message from the server as the server sends messages line by line. So you need some mechanism to determine, that the message has ended. Until then, you should keep putting the messages into your buffer. This buffer can be whatever data-structure you see fit. The list covers (but is not limited to)

  • a string you are simply appending the new string to
  • a list / array of strings
  • a stringbuilder
  • ...

When you consider the buffer full, which means, the buffer is either actually full, as in "I can't fit any more information in there" or the message you are currently reading is complete you should pass the buffer to the parser and empty the buffer.

if(messageComplete()) {
  parser.parse(buffer);
  buffer.empty();
}

Parser

After fully receiving a message from the connector, you have to retrieve the information from that simple plain text you just gathered. For that you should use a parser that employs the EBNF of the server. A pretty straight-forward version is the recursive descent parser. So you are basically looking at the EBNF, trying to figure out, what you can receive from the server and how it should be structured and modeling your classes / structs / maps / record after that. For example: The EBNF (as on the day of 2014-01-30) has one rule PLAYER: "begin:player","points:",INT, "id:",INT,"type:Player","busy:"BOOLEAN,"desc:"STRING,"x:",INT,"y:",INT,"end:player"

From just looking at this you can figure out, that there must be something called "player" (hence begin:player). Apparently, this player has some attributes:

  • a number of points
  • an id
  • a type
  • a busy-state
  • a description
  • an x value, probably the position
  • a y value, see above

So you should probably have some class on your side that looks something like this:

class Player {
 int id, points, x, y;
 string desc, type;
 boolean busy;
}

Now, your parser should be able to browse through the message from the server and collect this data to put together one Player in your client. Or raise an error in case the message is malformed or information is missing. For example: You know, that whenever you just read "begin:player" from the message, the next thing that comes should be the string "points:", followed by an integer. This integer can be taken and used for the Players attribute points. And so on. Every possible message should be parsed into a data-structure for further usage. When you have successfully constructed the structure, you can pass it to the backend, where it is stored.

Backend

Okay, you have received messages from the server and made data-structures from it via the parsers. Now you should store those objects somewhere for further use. That's the job of the backend. Again, put some consideration into what possibilities you have to store and what their advantages and disadvantages are. Would you save the tiles of a map in linked list? And why not? Is it feasible to put players into an array of fixed size? When you are using a map, what will be the keys and what the values? Are you mapping names on the players? Or their IDs? Another (probably incomplete) list for starters:

  • arrays
  • lists
  • maps
  • (in-memory) databases
  • linked lists
  • ...

So you will probably have a set of methods like:

function addPlayer(Player pl) {
  ...
}

function deletePlayer(Player pl) {
  ...
}

function receiveMinigameChallenge(Challenge ch) {
  ...
}

...

And the list continues. Again: this is just one way to do it. You will probably come up with your own designs that has advantages in your very own client-architecture. It might be important to note that the backend doesn't have to store the data in itself. It can connect to a database and store it there as well. Or call a separate class "MyStorageClass" where the lists are contained. Or store the objects in a textfile. Whatever makes you happy.

GUI

For some, this might be the most fun part. After all, this is what people will see when they look at your client. I have seen several clients with bonus features, such as scrollable and zoomable maps, minimaps, popup-chat, soundeffects, background-music, animated sprites, rendered via OpenGL, fancy screens for minigames, and the list goes on. But the most basic things your client should have is one panel to render the map and all entities on it and maybe some textfield to display the chat, plus an input-field to send text-messages and commands yourself.

For rendering your stuff, you can access your backend and retrieve all entities. You just take them and render them at their current position. The backend should update their position if needed, right?

For the map (in a 2D-representation), you should probably devide the size of your plane (the green field in the image above) into equal squares. Say, you have a 2x2 map and your field measures 100x100px. Then you would do something like:

int tileWidth = planeWidth / mapWidthInTiles;
int tileHeight = planeHeight / mapeHeightInTiles;

for(int x = 0; x < mapWidthInTiles; x++)
  for(int y = 0; y < mapHeightInTiles; y++)
    draw(map[x][y], x*tileWidth, y*tileHeight);

Where draw(t, x, y) is a function to draw the upper left corner of the Tile t at the position (x|y).

⚠️ **GitHub.com Fallback** ⚠️