ShiVa Networking part 1: Your First Connection

Single player games can transport you into fantastic worlds and tell incredible stories. What could be better than sharing these experiences with your friends? Unfortunately, multiplayer programming is one of the more demanding tasks in the life of a game developer, and your game will succeed or break with the network code. ShiVa offers many tools to help you design multi-user applications. Over the next few weeks, this tutorial series will guide you through many of them, explain the API and concepts behind them, and provide useful code snippets you can reuse in your own programs. Let's get started!

Differences to ShiVa 1.9 Server

Back in the ShiVa 1.9 days, we offered a separate download for the ShiVa Server. A couple of ShiVa 2.0 betas ago, we decided to include our Server binaries in the regular ShiVa 2.0 Editor distribution package. You can find both GUI (with graphical user interface) and CLI (command-line text output) versions as well as their starter scripts (Linux) in the ShiVa Editor directory. While early server binaries relied on the Editor for software registration and licensing, we are happy to announce that ShiVa Server no longer requires activation, at least for the beta period.

Our 1.9 Server was a completely different product and did not rely on a 1.9 Editor installation in any way. This has also changed. The 2.0 Server relies heavily on libraries that come bundled with the Editor. The size of the standard installation has therefor grown from a few MB to several GB. Fortunately, the majority of that are engines and platform-specific libraries you will never use in a server environment. You can safely remove the Data/Authoring directory to recover around 3GB of storage space.

Listen vs Dedicated servers

There are actually two different ways to program a client-server application in ShiVa. The first method lets you connect directly to an instance of the game running on another device, which pulls double duty as a server and as a client for the player gaming on the device. This style of multiplayer can be found in most older shooters like Quake or Counter-Strike. It often requires a masterserver to which new clients connect first, which stores a list of all currently active servers, their IPs, game info etc.

The second method relies on an instance of the ShiVa Server binary running on a local or remote machine as a background process. This is often called a "dedicated server". They are often run on low power, lower performance, remote machines due to the low impact of processing power these dedicated servers have, since none of the game's content and graphics are actually computed or rendered on the dedicated server.

This tutorial will deal primarily with the latter style of servers. Furthermore, Linux is by far the operating system you are most likely to encounter when getting your own remote server, followed by Windows. The tutorial will reflect that and focus on dedicated servers running on a Linux machine in CLI mode.

Server Installation

As you already know, ShiVa 2.0 Server is included in the Editor distribution package. Download the ShiVa 2.0 installer/beta archive from our website, unpack/install it on your target machine, and reclaim some space by removing the Authoring directory, as we previously mentioned.

The installer script will create some handy shortcuts on your Linux machine:

shiva2 (Editor GUI)
shiva2server (Server GUI)
shiva2serverCLI (Server CLI, starting with beta 9)

The GUI server may seem like the easiest option. Most remote linux servers do not offer a graphical frontend however. Besides, the GUI currently does not offer a real-time logging display which makes it very easy to diagnose problems.

To start the CLI server, you can use shiva2serverCLI shortcut from your terminal or SSH connection (beta 9+). Alternatively, you can CD to the installation directory and execute the ./ShiVaServerCLI.sh script as root. However, we recommend using the --help command first, which returns a list of all available options:

Server Setup

All network communication must run through dedicated ports. By default, ShiVa Server is running on port 5354 (tcp), however this can be changed easily through the --port option.
Each network service on a machine must have its own port. Make sure you are not trying to open a ShiVa Server on a port that is already used by a network service, such as the neighbouring 5355. A handy list of commonly used ports can be found on wikipedia.

In modern systems, all ports are monitored and usually closed by a firewall. When you start the server on Windows, you will get the usual network alert:

On Linux however, you will most likely get nothing of the sort, and your clients simply won’t be able to connect. The easiest way to get around this is by running the ShiVa Server as root.

To keep the network traffic low, try to modify the tickrate to a ms value that corresponds to your style of game. A 10ms tickrate allows for 100fps split second reaction gameplay like you would normally find on a shooter, 20ms would correspond to 50 fps and most third person action adventures, while 40ms to 25 fps which could be enough for real time strategy, chat applications, and turn-based games.

You can lock your game to a certain appID, so other ShiVa multiplayer games cannot accidentally connect to your server. You can find the appID for your game in the game's Properties panel. A username/password system on the other hand would have to be integrated on the game code level.

All other options should be self-explanatory: limit the number of players that are allowed to connect to your server, define a logfile path, etc. For this tutorial, the server is started with these options:

To check the IP of your server machine, you can run ipconfig (windows) or ifconfig (linux) and keep looking for an IP4-style IP address.

Connection Concepts

Our server is now running and waiting for connections. Clients will first have to find the machine running the ShiVa Server through its IP and port, then join (or create) a session on the server. To identify each user, they will receive a unique ID as soon as they connect. After they have connected, players can optionally join a scene.

For all these events, there are corresponding user AI handlers available, which handle both connect and disconnect events. For this tutorial, these handlers are only concerned with logging:

Connection AI

For our client application, we will mainly need 3 APIs: network.*, server.* and session.* - but first, some housekeeping. To keep the AI tidy, we will store all data pertaining to the server, the player etc. in separate hashtables. It is always a good idea to initialize your AI variables in onInit, like so:

  1. --------------------------------------------------------------------------------
  2. function NETWORK_connect.onInit ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. -- fill server info table with some default data
  6. local serverinfo = this._htServer ( )
  7. hashtable.add ( serverinfo, "ip", "127.0.0.1" )
  8. hashtable.add ( serverinfo, "port", "5354" )
  9. hashtable.add ( serverinfo, "session", "Default" )
  10. hashtable.add ( serverinfo, "isConnected", false )
  11. hashtable.add ( serverinfo, "isPending", false )
  12.  
  13. --------------------------------------------------------------------------------
  14. end
  15. --------------------------------------------------------------------------------

The connection itself should be moved out of the onEnterFrame main loop into its own state. OnEnter reads the hashtable and connects tries to connect to the server IP and port, connected through the industry-standard : syntax:

  1. --------------------------------------------------------------------------------
  2. function NETWORK_connect.connection_onEnter ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. log.message ( "entering network connection state" )
  6.  
  7. -- connect to IP and port
  8. local serverinfo = this._htServer ( )
  9. local ip = hashtable.get ( serverinfo, "ip" )
  10. local port = hashtable.get ( serverinfo, "port" )
  11. network.setCurrentServer ( ip ..":" ..port )
  12.  
  13. --------------------------------------------------------------------------------
  14. end
  15. --------------------------------------------------------------------------------

onLoop is the busiest function, since it has to handle various network and session states. First you have to check whether a server has been found. Second, we have to check the connection status. Since making a connection takes a bit of time, we must allow the application to remain in the pending state until it is finished. onLoop will query the state every frame and return early when no change has occurred. When the connection has been completed successfully, we need to do the same with the session: get status, awaiting connection/pending, completing connection. For a single-session game, the onLoop code could look something like this:

  1. --------------------------------------------------------------------------------
  2. function NETWORK_connect.connection_onLoop ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. local serverinfo = this._htServer ( )
  6.  
  7.  
  8. -- check if there is actually a server
  9.  
  10. local hCurrentServer = network.getCurrentServer ( )
  11. if ( hCurrentServer == nil ) then
  12. log.warning ( "Connection loop: Server handle is NIL!" )
  13. -- early exit
  14. this.idle ( )
  15. return
  16. end
  17.  
  18.  
  19. -- not connected
  20.  
  21. local stat = server.getStatus ( hCurrentServer )
  22.  
  23. if ( stat == server.kStatusNone ) then
  24. log.warning ( "Connection loop: Server Status == None" )
  25. -- another early exit
  26. this.idle ( )
  27. return
  28.  
  29. elseif ( stat == server.kStatusPending ) then
  30. if ( hashtable.get ( serverinfo, "isPending" ) == false ) then
  31. log.message ( "Connection loop: Network is pending..." )
  32. hashtable.set ( serverinfo, "isPending", true )
  33. end
  34. --wait
  35.  
  36.  
  37. -- finally connected
  38.  
  39. elseif ( stat == server.kStatusConnected ) then
  40. hashtable.set ( serverinfo, "isPending", false )
  41.  
  42. -- get session handle
  43.  
  44. local hCurrentSession = server.getCurrentSession ( hCurrentServer )
  45. local sess = hashtable.get ( serverinfo, 'session' )
  46.  
  47. if ( hCurrentSession == nil ) then
  48. log.message ( "Connection loop: Entering '" ..sess .."' session (was NIL)" )
  49. server.setCurrentSession ( hCurrentServer, sess )
  50.  
  51. else
  52. local hCurrentSessionStatus = session.getStatus ( hCurrentSession )
  53.  
  54. if ( hCurrentSessionStatus == session.kStatusNone ) then
  55. log.message ( "Connection loop: Entering '" ..sess .."' session (was NONE)" )
  56. server.setCurrentSession ( hCurrentServer, sess )
  57.  
  58. elseif ( hCurrentSessionStatus == session.kStatusPending ) then
  59. if ( hashtable.get ( serverinfo, "isPending" ) == false ) then
  60. log.message ( "Connection loop: Session Pending..." )
  61. hashtable.set ( serverinfo, "isPending", true )
  62. end
  63.  
  64. elseif ( hCurrentSessionStatus == session.kStatusConnected ) then
  65. hashtable.set ( serverinfo, "isPending", false )
  66. hashtable.set ( serverinfo, "isConnected", true )
  67. this.idle ( )
  68. --start playing
  69. end
  70.  
  71. end -- session
  72.  
  73. else
  74. log.warning ( "Connection loop: Unrecognized network status return!" )
  75. -- early exit
  76. this.idle ( )
  77. return
  78. end -- server
  79.  
  80. --------------------------------------------------------------------------------
  81. end
  82. --------------------------------------------------------------------------------

Connection UI

This sample would not work properly without a way to enter the port and IP, so we will have to construct a simple HUD with these components and a button to attempt the connection. The additional Session edit box is there for next week, so don’t worry about it right now.

We highly recommend keeping the UI code in a separate AI from the connection AI, so you can re-use individual parts later on and in other games. This introduces a few problems however. You need to send events between those two AIs, which increases complexity, and you need to share some information about the state of e.g. the connection, the connection button, or the user input. Furthermore, the AI names may not change, otherwise your events will not reach their intended target. But these problems are nothing a few control strings which store e.g. the variable names and a generic AIModel name search loop cannot solve.

Your first connection

With the server running, your connection AI handling the pending network and session states, and your UI allowing to define servers easily, it is time to make your first connection. If ShiVa cannot establish a connection, have a look at the log console, which will most likely tell you the reason in form of a warning (yellow color):

If everything goes well however, you will recognize a successful attempt by an ID change. When other clients enter the session, your events will trigger as well:

The CLI server will have a similar output:

Congratulations, you just made your first network connection with ShiVa Server!