Building a Network Test App with Leadwerks
Thought I could write my first Blog entry and report about my ventures into network programming with Leadwerks, ENet and Lua. Also, provide some source code. Okay, here we go:
What I wanted to do
My test app should be a client-server setup where clients can connect to the server, see each other's avatar, send chat lines and move around their avatar on a plane. Nothing fancy really, just to get into the topic.
The ENet network library (http://http://enet.bespin.org/) should be used, and network functions should be exposed from C to Leadwerk's lua engine (as I wanted to do the main 'game coding' in lua).
The clients are intended to run on Windows, while the server should first be run as console app on Win, might later be moved to a remote rented Linux server.
The Network Concept
For the first testing, I tried to make the server part very "light-weight", and leave many things to the client(s). Probably not the right thing to do for a full scale MMOG (mainly due to cheat concerns) - but, well, this test app is far from that anyway ;-)
So, the server will - aside from some administrative tasks - mainly receive messages from a client and distribute to the others. As for the communication, a rather simple protocol layer was created on top of ENet (i.e., a definition of message formats and identifiers).
Note that not everything a client sends will be distributed - it would first be checked for (some very basic) consistency.
Network Messages
Seven network messages were defined, some for client->server or server->client only, some for both ways. The messages are wrapped into ENet's 'ENetPacket' and sent, to be evaluated by the receiver.
- MSG_TYPE_ID_REQ(a clients requests a unique identification number from the server)
- MSG_TYPE_NAME (a client sends its chat name)
- MSG_TYPE_POS (a client or the server sends info about position/rotation of an avatar)
- MSG_TYPE_CHAT (a client or the server sends info about a chat line)
- MSG_TYPE_CLIENT_ON (the server sends info about a new client online - also used to inform a new client about existing ones)
- MSG_TYPE_CLIENT_OFF (the server sends info about a client leaving)
- MSG_TYPE_CLIENT_ID (the server sends a unique identification number to the client that requsted one)
The message formats used would look like so (complete source available below):
struct MSG_POS // Sending a client pos and rot info { BYTE cMsgType = MSG_TYPE_POS; UINT16 ClientID; float fPosX, fPosY, fPosZ; float fRotX, fRotY, fRotZ; };
The Server
This is a win console C app, which should - with some tweaking - compile and run on Linux also. Actually, I started out with doing it on Ubuntu on a VMWare Player, but moved to Win console later on. Just for the ease of use.
The server will detect when a new client connected (using ENet functionality), and wait for the initial 'request for ID' message. After assignment, it informs the other clients about the new one (and vice versa). On server side, only the unique ID and the client's chat name are stored.
When receiving e.g. a "Chatline" message, the server would check if the sender's client ID and the message type both are valid, then distribute the message to all clients. Upon disconnection of a client (again using ENet functionality), remaining clients would be informed also.
The Client
Networking here is done as a two-stage concept: Extending the Leadwerks App with some networking C functions and exposing some of them to lua (where the main 'game programming' is to happen). Note that Leadwerks also comes with the ENet headers and libs, so it is not necessary to include it here. You can just use it in your C code.
As for the connection between C and lua, two possibilities must be considered: Calling a (new) network function from lua and calling back from C into lua when e.g. a certain message arrived.
Exposing a new function to lua would look like this (returning the connected state of the network):
lua_register(Interpreter::L, "NW_IsConnected", NW_IsConnected); extern "C" int NW_IsConnected(lua_State* L) { lua_pushboolean(L, gl_Networking.m_bConnected); return 1; }
Preparing a callback from C into lua looked like so (this is a callback when a new client is reported by the server). Note that (obviously) the global function "NW_Callback_Connected" must be present in the lua code, to be called.
void Callback_Connected(unsigned short int iID) { lua_getglobal(Leadwerks::Interpreter::L, "NW_Callback_Connected") if (!lua_isfunction(Leadwerks::Interpreter::L, -1)) { lua_pop(Leadwerks::Interpreter::L, 1); return; } lua_pushnumber(Leadwerks::Interpreter::L, iID); /* do the call (1 argument, 0 result) */ if (lua_pcall(Leadwerks::Interpreter::L, 1, 0, 0) != 0) { printf("NW_Callback_Connected ERROR: %s\n", lua_tostring(Leadwerks::Interpreter::L, -1)); return; } }
For the test app, several functions were exposed to lua from the added network C code, and also several possible callbacks are provided. To avoid any multi-threading issues, the function "NW_Update()" must periodically called from lua (e.g. in WorldUpdate). Within there, callbacks are then triggered.
Callbacks, invoked from network (when NW_Update() called) NW_Callback_Connected(id) // called when we are connected to the network NW_Callback_ClientJoined(id) // called when a client joined the network NW_Callback_ClientLeft(id) // called when a client left the network NW_Callback_ClientPosRot(id,Px,Py,Pz,Rx,Ry,Rz) // called when a client pos/rot message arrived NW_Callback_ClientName(id,name) // called when a client sent its name NW_Callback_ClientChat(id,chatline) // called when a client sent a chatline
Functions, to be called from lua NW_Connect() // do this once, e.g. in Script:Start() NW_IsConnected() // call this to see if server connection is established NW_Update() // do this regularly, e.g. in Script:UpdateWorld() NW_SendPosRot(Px,Py,Pz,Rx,Ry,Rz) // when pos/rot changes, send all e.g. 200ms NW_SendName(name) // send your client name once (15 chars max); NW_SendChatline(chatline) // send chatlines (255 chars max) to other clients
Within lua, it is then the client's task to respond to e.g. a 'NW_Callback_ClientJoined' message. Like, create a new avatar and move it around.
The client is sending its own position every 200ms, to be distributed further by the server. Within the client, only very few measures have been taken to compensate for network lag. It will interpolate incoming positions for remote avatars (moving them with constant speed), but does not really apply such high-sophisticated methods like movement prediction and whatnot.
To move one's own avatar, use WASD. To submit a chat line, hit enter (submit with enter again, cancel with esc). Note that as for now, a connection attempt is only done at startup. So the server must be started before any client. Also, 'localhost' as server IP is currently hardcoded.
Final Words
While I did dive into ENet before, this was my rather first time with all that lua mumbo-jumbo. Actually, after getting used to it, writing scripts in lua is quite fast (relaxing, even). Think I will stick to the idea of doing some basic stuff in C, but adding game logic in lua. Now on to that movement prediction et al!
Links
A short video, showing the whole mess in action:
Link to executables (server+client): http://www.mikoweb.eu/tmp/LWEN_Networking_Exe.zip (10MB)
Link to Source (VS2013+LW): http://www.mikoweb.eu/tmp/LWEN_Networking_Source.zip (25MB)
Pictures
The test app (here, 3 clients and the server running on Win)
- 13
7 Comments
Recommended Comments