bw logo

Chapter 3. A basic client-server game (CLIENT_SERVER)

In A Basic Client-Only Game (CLIENT_ONLY) we set up a basic client resources tree that would allow us to walk around a simple space using a first-person view. In this chapter of the tutorial we will extend the game to the server, so that multiple clients can log in and see each other walking around.

3.1. Server Installation and Configuration

Prior to progressing through this part of the tutorial it is necessary to install and configure the BigWorld server. If you haven't already done this please proceed to the Server Installation Guide

At this point it is also relevant to address the issue of sharing files between Linux and Windows machines. Since there are many files that are read by both the client and the server (tutorial/res/scripts/entity_defs/*, space data, etc), it is necessary to keep them all on a single file system that is shared between the client and server, rather than having to keep them synchronised manually. Please refer to the Client Programming Guide's section on Shared Development Environments for more information on this topic.

For the purposes of this tutorial, we will assume that you have mounted your Windows directory tree at $HOME/bigworld_windows_share on your Linux file system.

3.2. A Space entity.

In BigWorld, spaces are separate coordinate systems. Each space can have one or more geometry mappings (as created in the World Editor). Cell entities are associated with a single space at any one time. These may be used to implement things like planets, mission instances, apartments or game sharding.

A new space is created by creating a cell entity in a new space. It is typical to have an entity type that is responsible for space creation.

3.2.1. entities.xml

Every entity must be defined in the entities.xml file located at tutorial/res path. In this case the Space entity exists only on the server, so it should be defined within the ServerOnlyEntities block:

<root>
   <ClientServerEntities>
      <Avatar/>
   </ClientServerEntities>
   <ServerOnlyEntities>
      <Space/>
   </ServerOnlyEntities>
</root>

3.2.2. Entity definition

The Space entity type has a single string property spaceDir. This will be used to indicate which space geometry to load.

<root>
   <Properties>
      <spaceDir>
         <Type>          STRING          </Type>
         <Flags>         BASE            </Flags>
      </spaceDir>
   </Properties>

   <ClientMethods>
   </ClientMethods>

   <CellMethods>
      <addGeometryMapping>
         <Arg>           STRING          </Arg>
      </addGeometryMapping>
   </CellMethods>

   <BaseMethods>
   </BaseMethods>
<root>

Example tutorial/res/scripts/entity_defs/Space.def

3.2.3. Base part

The base entity calls self.createInNewSpace() to create a new space, put its cell entity in it, and tells the cell to add a space geometry mapping. It registers itself globally as "DefaultSpace" so that the base entity can easily be found later.

import BigWorld

class Space( BigWorld.Base ):

    def __init__( self ):
        
        BigWorld.Base.__init__( self )

        # Create this entity in a new space
        self.createInNewSpace()
        self.cell.addGeometryMapping( self.spaceDir )
    
        self.registerGlobally( "DefaultSpace", self.onRegistered )
    
    def onRegistered( self, succeeded ):
        if not succeeded:
            print "Failed to register space."
            self.destroyCellEntity()

    def onLoseCell( self ):

        # Once our cell entity is destroyed, it's safe to clean up the Proxy.
        # We can't just call self.destroy() in onClientDeath() above, as
        # destroyCellEntity() is asynchronous and the cell entity would still
        # exist at that point.
        self.destroy()

Example tutorial/res/scripts/base/Space.py

3.2.4. Cell part

The cell entity maps the geometry to load after receiving a call from the base to addGeometryMapping(), with an appropriate path to a geometry (e.g. spaces/main).

import BigWorld

class Space( BigWorld.Entity ):
        
    def __init__( self, nearbyEntity ):
        BigWorld.Entity.__init__( self )

        # This is the first entity created for the space
        assert( nearbyEntity is None )
        
    def onDestroy( self ):
        # Destroy the space and all entities in it
        self.destroySpace()

    def addGeometryMapping( self, geometryToMap ):
        # The base informs us what geometry to map.
        BigWorld.addSpaceGeometryMapping( self.spaceID, None, geometryToMap )

Example tutorial/res/scripts/cell/Space.py

3.3. Server-side personality scripts

Just like the client, the server uses personality scripts to perform bootstrap functionality on each CellApp and BaseApp. For the moment, we are only interested in the onBaseAppReady callback in the BaseApp personality script, which we will use to create a space.

Our initial revision of tutorial/res/scripts/base/BWPersonality.py is displayed below:

# Base bootstrap script
import BigWorld

def onInit( isReload ):
   pass

def onBaseAppReady( isBootstrap, didAutoLoadEntitiesFromDB ):
   # Only on the first baseapp
   if isBootstrap:
     # Create a Space entity that will create a space with our geometry.
     BigWorld.createBaseLocally( "Space", spaceDir = "spaces/main" )

Example tutorial/res/scripts/base/BWPersonality.py

A Space entity is created with the spaceDir property set to "spaces/main".

Our initial revision of tutorial/res/scripts/cell/BWPersonality.py is displayed below:

# Base bootstrap script
import BigWorld

def onInit( isReload ):
   pass

def onCellAppReady( isFromDB ):
   pass

Example tutorial/res/scripts/cell/BWPersonality.py

Our implementation of the scripts is trivial and provides only stub implementations of callbacks that will be explained later.

For a complete list of the available personality script callbacks, see the documentation for the BWPersonality module in BaseApp Python API, CellApp Python API, and Client Python API.

3.4. The server-side Avatar scripts

The next step is to define the server-side logic that goes with our Avatar class. Even if we did not want to define any server-side logic for our Avatar, we would still need to provide at least stub implementations of Avatar.py in the base and cell directories so that the base and cell parts of our Avatar entity can be created.

First we need to define the base part of the Avatar in tutorial/res/scripts/base/Avatar.py:

import BigWorld

# Must derive from BigWorld.Proxy instead of BigWorld.Base if this entity type
# is to be controlled by the player.
class Avatar( BigWorld.Proxy ):

   def __init__( self ):
      BigWorld.Proxy.__init__( self )

      # Set our spawn position.
      self.cellData[ "position" ] = (0,0,0)

      # Spawn in the default space.
      self.createCellEntity( BigWorld.globalBases[ "DefaultSpace" ].cell )

   def onClientDeath( self ):
      # We ensure our cell entity is destroyed when the client disconnects. 
      self.destroyCellEntity()

   def onLoseCell( self ):
      # Once our cell entity is destroyed, it is safe to clean up the Proxy. We cannot
      # just call self.destroy() in onClientDeath() above, as destroyCellEntity() 
      # is asynchronous and the cell entity would still exist at that point.
      self.destroy()

Example tutorial/res/scripts/base/Avatar.py

The constructor for the base entity creates the cell entity in our space created earlier. It was registered in BigWorld.globalBases as "DefaultSpace".

There is a little bit of housekeeping here too — we have provided implementations for the onClientDeath and onLoseCell callbacks, which clean up the cell and base parts of the entity when the client disconnects from the server.

At this stage we do not need to define any interesting logic on the cell entity, so we provide a stub implementation in tutorial/res/scripts/cell/Avatar.py:

import BigWorld

class Avatar( BigWorld.Entity ):
   def __init__( self, nearbyEntity ):
      BigWorld.Entity.__init__( self )

Example tutorial/res/scripts/cell/Avatar.py

3.5. Connecting the client to the server

We need to add code to our basic client to have it connect to a server. If you have used FantasyDemo, you will have experienced the various GUI-based methods that can be used to connect to a server. Since we are not writing GUI code yet, we will just enter the address of our server into tutorial/res/scripts_config.xml and have the personality script read it from there.

We will also add an entry to control whether the client should attempt to connect to a server, or just explore the space offline as in the previous stage of the tutorial.

The relevant changes to tutorial/res/scripts_config.xml are displayed below:

...
   <server>
      <online> true </online>
      <!-- Whether the client actually connects to the server. -->
      <host> 10.40.3.23 </host>
      <!-- The server to connect to. Ideally we would allow this to be entered via an in-game
           GUI (or leverage the server discovery stuff) but for now we'll just hardcode it. -->
   </server>
...

Example tutorial/res/scripts_config.xml

Note

If you are using multiple users on the same server machine, you will need to specify the port as well as the IP address. The port for the LoginApp can be found by inspecting the loginapp/nubExternal/address watcher value on the Web Console. For example, if the IP address is 10.40.3.23 and it is on port 20013, then put 10.40.3.23:20013 inside the <host> tag. You may need to update the port after restarting the server.

The next step is to implement the function call initOnline in the client personality script tutorial/res/scripts/client/BWPersonality.py and switch between calling it and calling initOffline based on the online option in tutorial/res/scripts_config.xml.

To achieve that, make the changes to tutorial/res/scripts/client/BWPersonality.py as illustrated below.

...
def init( scriptConfig, engineConfig, prefs ):
   if scriptConfig.readBool( "server/online" ):
      initOnline( scriptConfig )
   else:
      initOffline( scriptConfig )
...

def initOnline( scriptConfig ):
   class LoginParams( object ):
      pass

   def onConnect( stage, step, err = "" ):
      pass

   # Connect to the server with an empty username and password.  This works
   # because the server has been set up to allow logins for any user/pass.
   BigWorld.connect( scriptConfig.readString( "server/host" ),
                     LoginParams(), onConnect )

Example tutorial/res/scripts/client/BWPersonality.py

Notice that we no longer need to do client-side space creation, geometry mapping, or entity creation; these functions now happen on the server side. The client will automatically perform the necessary client-side actions based on the server-side game state.

3.6. Going 3rd person

The last line of initOffline in the personality script sets the camera to use first-person mode. We chose to do this in the first part of the tutorial because we wanted to get a client up and running as quickly and simply as possible, and using first-person mode allowed us to ignore the issue of rendering the player himself.

However, since we are now implementing a client-server game where multiple clients can log in and inhabit the same space, it will be helpful if they have models so that they can see each other!

We have provided a basic biped model in res/characters/bipedgirl.model, which we will use for all Avatars. Edit the enterWorld callback for the Avatar class in tutorial/res/scripts/client/Avatar.py as follows:

...
class Avatar( BigWorld.Entity ):

   def enterWorld( self ):

      # Set the position/movement filter to correspond to an avatar
      self.filter = BigWorld.AvatarFilter()

      # Load up the bipedgirl model
      self.model = BigWorld.Model( "characters/bipedgirl.model" )
...

Example tutorial/res/scripts/client/Avatar.py

3.7. Server-side XML configuration

The BigWorld server uses the file your_game/res/server/bw.xml for configuring options on the various server components. For a comprehensive list of configuration options along with a detailed description, see the Server Operations Guide's section Server Configuration with bw.xml.

Typically, the bw.xml file includes a BigWorld provided default configuration file which contains recommended default values for all the available configuration options. This is achived by using the <parentFile> tag as follows:

<root>
   ...
   <parentFile> server/development_defaults.xml </parentFile>

You will notice that in the example above, the included file is development_defaults.xml. This file provides good working defaults for a game development environment that will generate more warnings and intentionally crash the server in certain circumstances to ensure that critical issues are caught prior to the release of a game. The development defaults file however is only a small file that modifies a subset of values from the file bigworld/res/server/production_defaults.xml. The production defaults file aims to provide a comprehensive set of options and default values to be used for a game in a live production environment and can be used as a reference point when searching for a specific option.

While it is anticipated that the majority of configuration options will not need to be modified, if you need to change a value or are simply curious as to the purpose of an option, complete documentation for the BigWorld server configuration options can be found in the Server Operations Guide's section Server Configuration with bw.xml.

To get our basic game up and running, we need to set a few options to specify what entity type the player should be connected to once logged in, and to allow players to log in with unknown usernames (just for convenience while developing).

Save the following in tutorial/res/server/bw.xml:

<root>
   <parentFile> server/development_defaults.xml </parentFile>
   <billingSystem>
      <entityTypeForUnknownUsers>  Avatar </entityTypeForUnknownUsers>
      <shouldAcceptUnknownUsers>   true   </shouldAcceptUnknownUsers>
      <shouldRememberUnknownUsers> false  </shouldRememberUnknownUsers>
   </billingSystem>
</root>

Example tutorial/res/server/bw.xml

3.8. Starting and connecting to the server

At this point of the tutorial, it is assumed that you have set up your Linux machine as described in the Server Installation Guide. In particular, this assumes you have installed BWMachined on your Linux machine and have installed the Web Console somewhere on the local network. For details on Web Console see the Server Operations Guide's section Cluster Administration Tools, in WebConsole).

Before we can start the server, we need to specify where the server should get its binaries and resources from. This is a concept similar to the paths.xml files used by the client and tools.

We firstly need to know the directory the game resources are located on the Linux machine. If you have been developing the game resources on your Windows machine and have shared them using the setup_win_dev script, the resources are most likely located in $HOME/bigworld_windows_share. Check the directory where you believe the resources are located actually contain the correct files. For example:

$ ls $HOME/bigworld_windows_share
bigworld  fantasydemo  my_game  readme.html  rpm  template  tutorial

We now run the bw_configure script providing the location of the game resources we wish to use. This will differ slightly depending on the BigWorld Edition you are using.

3.8.1. Indie Edition

$ bw_configure
Game resource path [~/my_game/res]: ~/bigworld_windows_share/tutorial/res
Writing to /home/fred/.bwmachined.conf succeeded

Installation root : /opt/bigworld/current/server
BigWorld resources: /opt/bigworld/current/server/res
Game resources    : /home/fred/bigworld_windows_share/tutorial/res

The contents of the file $HOME/.bwmachined.conf[7] has now become:

# Generated by ./bw_configure
/opt/bigworld/current/server;/home/fred/bigworld_windows_share/tutorial/res:/opt/bigworld/current/server/res

Example $HOME/.bwmachined.conf

This file can then be edited whenever required to update the resource paths as your game development proceeds.

3.8.2. Commercial/Indie Source Edition

$ bw_configure
Installation root [~/mf]: ~/mf
Game resource path [~/my_game/res]: ~/mf/tutorial/res
Writing to /home/fred/.bwmachined.conf succeeded

Installation root : /home/fred/mf
BigWorld resources: /home/fred/mf/bigworld/res
Game resources    : /home/fred/mf/tutorial/res

The contents of the file $HOME/.bwmachined.conf[8] has now become:

# Generated by ./bw_configure
/opt/bigworld/current/server;/home/fred/bigworld_windows_share/tutorial/res:/opt/bigworld/current/server/res

Example $HOME/.bwmachined.conf

This file can then be edited whenever required to update the resource paths as your game development proceeds.

3.8.3. Starting a Server

You can now use the WebConsole's ClusterControl module to start the server. You should see six active processes in the process listing. Once the server is up and running, run the client and you should be able to connect to the server and control a basic biped Avatar from a 3rd person perspective. Connect multiple clients and watch each other moving around.



[7] Note the leading . in the filename.

[8] Note the leading . in the filename.