Workshop 2: Simple 3D Chat
Welcome ANet User. In this second workshop. We will create a simple 3D chat application in which you can move a selected character and chat with other players. Topics like synchronizing clients, level changing, kicking clients will also be explained. Basic requirements are Lite-C knowledge, an ANet Version (Demo, Standard or Professional) and a full version of 3D Gamestudio A7/A8.
If you don't know how a plugin function works, look into the ANet manual. (Tip: A function can be found easily through the search function of the manual!)
2.0 Let's start
First of all download the final project from the download page of the ANet homepage. Then start the 3dchat.exe ("lite-c") which is in the .zip file of the download. A window in which you can choose between Server, Client, and Server/Client is opened. Now select Server/Client. After that input your player name and press [Enter]. Now select a character you like. The window should look like this now (depending on the character you've selected):
Please start the .exe file again (ATTENTION: Don't close the first window!) and select "Client". Enter "localhost" for the IP address and confirm with [Enter]. After that you can input a different player name and select a character. After you have selected a character, the client should connect with the server over the localhost. After that you can move your character through the level with [w] [a] [s] [d] and control the camera with the mouse. If you press [C] you can input a chat message:
The chat message is displayed under the player name on all connected clients and on the server. The player name is "flying" over the head of the character. The player name and the chat message are not shown on the client that created the entity.
Activate the window in which you have started the Server/Client and press [L]. After that a new level is loaded on all clients and on the server. If you press , you can kick the client with the ClientID 1 (in our case the 2nd window). If you press [Q] you can close the window.
Now that you know how the application works, we can start to program. Because I don't want to blow up the size of the tutorial too much, only the most important functions of the source code are explained. (All functions which aren't explained here are commented in the source code!)
First of all enet_init() must be called. This function initializes the ENet Library. If this function isn't called, no ENet function can be used. After that the system- and userevents are set.
An event in ANet works the same way like an entity event in Gamestudio. On a certain action the set function is called and the corresponding parameters with information about the action. For example EVENT_CONNECTED will be called on the server if a client connects. The ClientID of the client is returned in the parameter sender.
Attention: Every ANet event function must have "var sender, char* msg, var length" as parameters.
With enet_svset_event() or enet_clset_event() you can assign a function to a certain event on the server or the client. All events after event number 16 are user events. These events aren't called through an internal action like connecting of a client or through a level change but through sending an event with enet_svsend_event() or enet_clsend_event().
After all functions are assigned to their events the initializing is finished. Mentionable is also the warning system of ANet. This system warns you if you call an ANet function the wrong way. With enet_set_warning() you can switch the warning system ON or OFF. In the development version it's better to switch the warning system ON but in the release version it shouldn't be activated.
With enet_deinit() the ENet library can be de-initialized. This function doesn't need to be used because if you close the engine, the allocated memory is automatically freed. If you use the plugin for a short time only, you can use this function to reduce the needed RAM of your application.
3.2 Initializing a connection
There are two functions: one function that initializes and controls a server, and a second one that initializes and controls a client. In start_server() the first level is loaded because the level must be loaded before enet_set_level() can be used. After that the function waits three frames for the level to load.
Next a host running as server is initialized with enet_init_server(). Every PC in a network is called host. The server will pass the data that comes from a client to all the other clients. The server also manages the player names and the global entities. With enet_set_level() the file name of the current loaded level is saved.
start_client() is similar to start_server(). The only difference is that a client is initialized and not a server, and enet_client_poll() is called every frame instead of enet_server_poll(). Also enet_client_poll() has to be called EVERY frame because this function controls the client.
For initializing a server start_server(), for a client start_client(), and for starting in client-server mode both functions have to be called.
ATTENTION: The order is important! First of all the server has to be initialized and only after that the client. Additionally you should wait four frames between the call of start_server() and start_client() so that the level can be loaded and the server can be initialized.
With enet_get_connection() you can find out which type of host is initialized: server, client or both. Please note that this only indicates which host is initialized and not if a connection is established! If you want to find out if a client has connected with a server you can use enet_get_clientid(). It returns ANET_ERROR if no connection is established or the ClientID (>= 0) if the client has a connection. On the server you can use enet_check_client() to check if a client is connected or not. On the other hand you can use EVENT_CONNECTED/EVENT_DISCONNECTED. These are called on the client or on the server if a client has connected/disconnected with/from a server.
Normally you need both methods. The event method is used if you want to call/execute something if a client connects/disconnects (for example for showing a message, removing an entity, etc.). The other method is normally used in loop conditions (for example if a player should move as long as a connection to the server is established): while(enet_get_clientid() != ANET_ERROR)
3.3 Connection system events and synchronizing
The connection system events are those events that are called if the connection to a client has changed. There is the EVENT_CONNECTED and the EVENT_DISCONNECTED event. If EVENT_CONNECTED is called on the server, a client connected with the server. If EVENT_DISCONNECTED is called on the server, the client disconnected. On the client EVENT_CONNECTED and EVENT_DISCONNECTED are only called if the client itself connected/disconnected with the server. If any other client connects with the server, the event is not called on the other clients.
The parameter sender returns the ClientID of the related client. Thereby in the example above in client_disconnected() the player entity of the client can easily be removed. The parameter msg of event number 3 returns the name of the level file which is loaded on the server (set through enet_set_level()).
Let's see how correct synchronizing of a client with the server works. First of all we need the EVENT_SYNCHRONIZED. It's called on the server and on the client if the synchronizing of a client is complete.
Okay, let's say that a server is already initialized. Now we want a client connection with the server. Because of that, we initialise a client on another PC or on the same but in a different window. Now the client tries to connect with the server in the background.
Once the connection is established, the function connected_with_server() is called (EVENT_CONNECTED). In the function connected_with_server() the playername is set first of all through enet_set_playername(). After that the function checks if a client and not a client/server is initialized. Now it loads the same level that is loaded on the server. The name of the .wmb file of the level is returned with the parameter msg. Now three frames must be waited until the level is loaded.
The check is needed because if the application runs in client/server mode, the level is already loaded. Can you remember? The level is loaded in start_server()! Additionally a synchronizing is needless because no entity could be created anyhow.
Okay, now enet_ent_synchronize() is called. This function lets the server send all entity data to the client. After the synchronizing is complete, synchronizing_complete() is called (EVENT_SYNCHRONIZED). After that you can use all entity functions.
ATTENTION: No entity functions should be used before the synchronizing is complete because no entities are created yet.
3.4 Creating a character
After our client is synchronized a player is created through create_player(). The character that was selected in the menu will be created.
First, we should make sure that the players aren't created into each other. Therefore all of the four possible clients have their own starting point for their character. After that the character that was selected in the menu (saved in the variable character) is created on all PCs.
The function move_player() gets called on all PCs.
ATTENTION: Don't forget that you shouldn't pass the function pointer but the function name in enet_ent_create()!
3.5 Moving a character
Now we have to wait until the entity gets a global pointer! This is very important because the global pointer isn't available immediately after the creation of the entity. After that the local entity pointer is locally saved in the array players. The index of the array is the ClientID of the creator of the player entity. Thus you can get access to the entity pointer only if you know the ClientID of the creator.
Now the function is split with the "if" statement into two parts: The first part only runs on the client that created the entity, and the second part runs on all the other PC's.
Let's talk about the first part: The entity can be rotated by moving the mouse along the x-axis and can be moved through pressing [w] [a] [s] or [d]. Gravity moves the entity to the ground. If one of the four keys is pressed, the walk animation is played. Once the walk animation should be played skill is set to 1. When this happens skill is sent through enet_send_skills() to all PC's so that they also play the walk animation.
If the position of the entity changed, the new entity position is sent through enet_send_pos() to all. If the angles of the entity have changed they are sent through enet_send_angle() to all. With DONTSEND_Z and ONLYSEND_PAN it's possible to prevent sending of the z-coordinate and the roll and tilt angle. This reduces traffic. The camera stays behind the player and camera.tilt can be changed if the mouse is moved along the y-axis.
This method of position updating is not bad for the beginning but normally not used. The problem of this method is that it needs a lot of traffic. Additionally it generates jerky movement if the latency is high. This problem can be solved through smoothing the movement. Therefore you interpolate between the old position and the new sent position. Attention: enet_send_pos() can't be used here, because the entity is immediately set to the new position. It's better to use three skills for the position.
The smoothing method solves the problem of the jerky movement but not the traffic problem. Because of this the most common method is dead-reckoning. The basic idea is that not the whole position is sent, but only the status of the entity. For example you use a skill that is 1 if the entity moves forward, 2 if it moves backwards, and only send this skill if it has changed. On the other machines the corresponding movement of the entity executed. You can easily see that you only have to send one skill sometimes and not a whole position vector with three elements (x,y,z) every frame. This of course needs much less traffic!
The only problems of dead-reckoning are over shootings = If a client moves further although it has already stopped on the client which controls the entity. This can be because of high latency. Because of this you should additionally update the position with enet_send_pos() every time the movement remarkable changes (for example a collision with a wall).
This was a small overview about the topic dead-reckoning. However there are a lot of good tutorials on the internet concerning this topic! In the ANet Versions Standard and Professional a dead-reckoning Template is available that does the work for you. A HowTo regarding to this template can be found in the manual.
Part 2 of our player_move() function: First of all, a TEXT object is created. It displays the player name of the creator of the entity and the chat message. The handle of the TEXT object is saved in skill. This is needed so that we can get access to the TEXT object outside player_move().
In the while loop the TEXT object is placed above the head of the entity. The distance between the entity head and the TEXT object is reduced with the distance between camera and entity. Next it gets the player name of the creator of the entity. For this enet_get_playername() and enet_ent_creatorid() is used. Finally the entity gets animated if skill is 1.
3.6 Sending/showing a chat message
A chat system can easily be realized through enet_send_event(). Therefore we define a user event. I have used event number 17. But you can also use any other one. It only has to be between 16 and 250 and must not be already used! The event function is called receive_chatmsg() and is called once a chat message was received.
Let's see what chat_startup() does: This function is called (like a starter function in C-Script) automatically at the beginning. First it waits until a server or a client is initialized. After that it waits in a loop until the key [C] is pressed. If this key is pressed it starts the input of the chat message. After the input is finished the message is sent through enet_send_event() to all. The event type should be 17 or what you have chosen. Because of enet_send_event() the function set to event number 17 (receive_chatmsg()) is called, a msg contains the chat message.
In receive_chatmsg() the chat message is copied into the second string of the TEXT object of the entity. Thus the chat message is displayed over the head of the entity. Then it waits three seconds and if no new chat message is displayed, the string gets clipped => no message visible any more.
3.7 Kicking a client
Also kicking a client can be done easily with ANet. In the function kick_startup() it waits until a host is initialized. If a client was initialized, the function gets terminated. Otherwise it waits for a key press (, ,  or ) and disconnects the client with enet_disconnect_client(). The string that is sent through enet_disconnect_client() is passed on the clients EVENT_DISCONNECTED. It can be used for sending information about the reason of the disconnection.
3.8 Level change
For making a level change on all machines, the server only has to load the new level and call enet_set_level(). After that the level change event is called on all clients. The parameter msg returns the name of the level file which should be loaded. Now you only have to load the new level in the event.
In our example the players array is cleared additionally because otherwise it would contain invalid pointers. Additionally the TEXT objects of the individual players are deleted.
3.9 Quitting the application
The best method would be an event being sent to the server if the application is closed so that it breaks the connection to the client immediately. Therefore it's not needed to wait until the timeout runs out. Because of this, the player entity and all the other stuff from the client is removed immediately (see client_disconnected()). After the call of enet_clsend_event() you have to wait one frame until the packet is sent. Now the engine can be shutdown. You don't have to wait until the server disconnects the client!
4.0 Final words
I hope that I have brought you all the important functions and processes of ANet. If you want to write big multiplayer games, it's very important that you now all the details of your network tool. Because of this you should try out all the functions yourself and practice with them before you start with a big multiplayer project. If you understood the internal processes, you will find bugs in your code much easier. Additionally in time you will get a feeling for the functions and how they should be used.
Look at all the available multiplayer examples from time to time so that you can learn all the tricks in multiplayer programming. Read other multiplayer tutorials and network techniques. Then the only boundaries for your own perfect multiplayer game will be your own imagination!
A lot of fun and good luck with your multiplayer projects!