bw logo

Chapter 6. A Basic NPC Entity (BASIC_NPC)

This chapter will cover the basic steps of creating a non-player entity. While the entity presented is quite simple in terms of functionality, it covers all the common essentials required in order to get a new entity up and running in the engine (including exporting the model from 3D Studio Max and configuring the model to work correctly).

6.1. Design

Before creating an entity, we need to determine what functionality is required. For this tutorial we will to create an NPC which will greet a player when they get within a certain radius (think of a person who stands in a supermarket entrance greeting people).

Entity requirements:

  • It should be placeable in the World Editor so that the run-time instance is created by the SpaceLoader entity on the server.

  • Model should be loaded asynchronously on the client.

  • The entity will not move. It should stand on the spot as placed in the World Editor.

  • A server-side trap should be used to trigger a greet action. The server should notify all clients in the area that it has greeted a player (including which player).

  • When the entity greets a player, a wave animation should be played on all clients in the area.

  • A message, generated on the server, should be displayed above the Greeter's head for a couple of seconds.

  • It should be possible to deactivate (and reactivate) the Greeter from the client, but only if the client is within the trigger radius.

We shall give this entity the class name Greeter.

6.2. Art

For this tutorial, we have provided the 3D Studio Max source file to the Barbarian model (a fantasy themed human). The model needs to prepared for use by the engine and needs to be configured to satisfy the requirements of the Greeter entity. This section assumes the BigWorld exporters have already been installed (see the Content Tools Reference Guide).

Detailed documentation about the exporters and tools can be found in the Content Tools Reference Guide and the Content Creation Manual.

Note

If you want to skip this section, the barbarian model has been pre-prepared for this tutorial (in C:/bigworld/tutorial/res/characters).

6.2.1. Exporting the model

  1. Open tutorial/sourceart/barbarian.max in 3D Studio Max.

  2. Copy the textures to tutorial/res/characters. The textures must be in the target directory before exporting (the exporter will display an error message and fail if they are not).

  3. Re-apply the textures to the model so that the Max scene points to the textures copied in the step above.

  4. Go to File Export and choose the BigWorld visual exporter.

  5. Save the model to tutorial/res/characters/barbarian.model.

6.2.2. Configuring the model

In order to automatically play an idle animation and to create the action required for the Greeter entity to wave, we need to add animations to the model and configure the appropriate actions. This is done in the Model Editor.

While an animation is a raw sequence of key frames, an action is a higher level concept. Actions are animation wrappers that contain extra information such as animation blending and what game-play situations will trigger the animation (e.g. idling, walking or running based on the velocity of the entity).

The Greeter will have two actions: an Idle action which is automatically selected when the entity is standing still, and a Wave action which will be explicitly invoked from the Greeter's Python scripts.

  1. Open up tutorial/res/characters/barbarian.model in Model Editor.

  2. Before we can setup the actions, we need to add references to the idle and wave animations. In the Animations tab, click the "New animation" button and select the tutorial/res/characters/idle_a.animation animation file. A new animation will be added to the list which can be previewed in the 3D view. Repeat this for the m_waveonehand.animation file.

  3. To setup an Idle action that is automatically invoked when the entity is standing still,

    1. Open the Actions tab in Model Editor.

    2. Click the New Action button and select the m_idle animation in the pop-up dialog. Set the action name to Idle.

    3. Select the new Idle action from the list.

    4. Setup the parameters in the Match section to allow the action matcher to automatically select the action when the entity is not moving. To do this set the following values:

      • Minimum speed=0.0, Maximum speed=0.0

      • Minimum turn=-360.0, Maximum turn=360.0

      • Minimum direction=-360.0, Maximum direction=360.0

      As you can see, the action will be picked whenever the speed of the entity is exactly zero and is facing in any direction.

  4. To setup a Wave action that is invoked explicitly by the Python scripts (i.e. not automatically picked by the engine),

    • Click the New Action button and select the m_wave animation in the pop-up dialog. Set the action name to Wave.

    No match settings need to be set for this action since we will manually invoke the action from the Python scripts.

6.3. Scripts

In order to insert the model as an entity into a space, we need to create the entity scripts. These are written in Python and perform game-specific logic and are split up into three parts: base, cell, and client.

Please refer to the Python API reference documents[9] for detailed information on the API's mentioned here.

6.3.1. entities.xml

First off, we need to tell the engine about our new entity. Every entity must be defined in the entities.xml file located at tutorial/res path. We add the Greeter to the ClientServerEntities block since the entity will exist on both client and server.

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

Remember that since the entity name corresponds with a Python class name, the name used here must conform with Python naming rules.

6.3.2. Entity definition

In order to allow the engine to know what methods and properties the entity has, we need to create a special file known as the entity definition file. In some ways this file is the most important part of an entity, as it defines how properties and methods are handled by the engine (e.g. property type, whether or not a property or method is exposed to clients, prioritisation of remote method calls and property updates, and configuring distance based LoD parameters for individual properties).

See the Server Programming Guide chapter The Entity Definition File for a detailed description of entity definitions.

For the Greeter entity, create a new file named Greeter.def and place it in tutorial/res/scripts/entity_defs/. We will define the following information for our entity:

  • Three properties:

    • A radius property which controls the trigger region for the Greeter. This is exposed to the World Editor so that it can be tweaked by the world builder. Its type is FLOAT, it has a default value of 3 metres and is declared as CELL_PRIVATE (since this property is only needed on the cell part of the entity and does not need to be publicly accessible by other entities).

      Note

      To provide a more intuitive interface for the World Editor, some extra meta-data has been defined for this property. The RADIUS widget allows the property to be manipulated via a visual spherical widget.

    • A property named activated which is a boolean property representing whether or not the Greeter is currently active. It's flags is set to ALL_CLIENTS so that changes to this property on the cell are automatically propagated to the clients.

    • The createOnCell property which indicates which space the entity should be created in (a requirement for entities loaded via the SpaceLoader entity).

  • Two methods:

    • A client-side method named greet. This will be remotely called by the server on all nearby clients whenever the entity greets a player (i.e. whenever the server-side trap is triggered). It takes two parameters, the ID of the entity is greeting, and a personalised greet message.

    • A method called toggleActive which is exposed to the client which allows the client to toggle the Greeter on and off. By default methods are not callable by the client (for security purposes), so the <Exposed> keyword is used to explicitly expose it to clients. It does not take any arguments.

<root>
    <Properties>
        <radius>
            <Type>          FLOAT
                <Widget>    RADIUS
                    <colour>    255 0 0 192   </colour>
                    <gizmoRadius>    2        </gizmoRadius>
                </Widget>
            </Type>
            <Flags>          CELL_PRIVATE    </Flags>
            <Default>        3.0             </Default>
            <Editable>       true            </Editable>
        </radius>
        
        <activated>
            <Type>           INT8            </Type>
            <Flags>          ALL_CLIENTS     </Flags>
            <Default>        1               </Default>
        </activated>

        <createOnCell>
             <Type>    MAILBOX        </Type>
             <Flags>   BASE           </Flags>
        </createOnCell>
    </Properties>

    <ClientMethods>
        <greet>
            <Arg> UINT32 </Arg> <!-- Entity ID of who we are greeting -->
            <Arg> STRING </Arg> <!-- Our greeting message -->
        </greet>
    </ClientMethods>

    <CellMethods>
        <toggleActive>
            <Exposed/>
        </toggleActive>
    </CellMethods>

    <BaseMethods>
    </BaseMethods>
</root>

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

6.3.3. Base part

The base part of the entity is the first part that gets created by the server. The base is created on one of the BaseApp processes, and is used to define entity logic which does not require spatial information (e.g. character inventory). The base part of an entity does not migrate between BaseApps after it has been created.

The base script for the Greeter entity is very simple and performs two tasks:

  • It creates the cell part of the entity within the cell specified by the createOnCell property (as setup by the EntityLoader class when it loads the entity information from the space's chunk file).

  • It destroys itself when the cell part of the entity disappears.

import BigWorld

class Greeter( BigWorld.Base ):
    def __init__( self ):
        BigWorld.Base.__init__( self )
        self.createCellEntity( self.createOnCell )

    def onLoseCell( self ):
        self.destroy()

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

6.3.4. Cell part

The cell part of an entity represents the current position, orientation, and movement for an entity within a particular space. Managed by the CellApp processes, the cell part of an entity can be moved between CellApp processes at any time based on CPU load. Generally, all entity logic that requires access to spatial information is implemented in the cell part of an entity (e.g. any code that needs to find out about other nearby entities, such as AI).

The cell part of the Greeter performs the following tasks:

  • Creates a trap when the entity is created using the radius specified in the World Editor.

  • Greets any Avatars that walk into the trap by calling greet on all clients that have the Greeter entity within their AoI.

  • Allow clients to toggle activated state of the entity, but only if they are within the radius. Note that exposing a method to the client implicitly adds an argument which is the ID of the Avatar entity which invoked the method. This can (and should) be used to validate that the Avatar is actually allowed to perform the desired command (remember, never trust the client).

Cell entities must derive from the BigWorld.Entity class.

import BigWorld
import Avatar
import random

MESSAGES = [ "Hello BigWorld", "Have a nice day" ]

class Greeter( BigWorld.Entity ):

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

        # Setup the trap
        self.addProximity( self.radius, 0 )

    def onEnterTrap( self, entityEntering, range, controllerID ):
        # If we are not active, do nothing.
        if not self.activated:
            return

        # Filter by entity class type
        if not isinstance( entityEntering, Avatar.Avatar ):
            return

        # Notify clients.
        self.allClients.greet( entityEntering.id, random.choice(MESSAGES) )

    def toggleActive( self, sourceID ):
        # Get the entity who called us. If the entity can't be found then they
        # obviously not near by so just bail out.
        try:
            sourceEntity = BigWorld.entities[ sourceID ]
        except KeyError:
            return
            
        # Get the distance between ourself and the Avatar
        dist = sourceEntity.position.distTo( self.position )
        
        # Do a check to make sure they are close enough.
        if dist > self.radius:
            return
            
        # All good, toggle our state. The activated property will be automatically 
        # propagated to all clients once this server tick is complete.
        self.activated = not self.activated

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

6.3.5. Client part

The client part of the entity is automatically created by the engine whenever an entity appears within your Avatar's area of interest (AoI). It is the job of the client scripts to coordinate all resources and logic required to represent the entity on the client based on the information provided by the server.

6.3.5.1. Entity module

The client-side of an entity must derive from BigWorld.Entity. The bare-bones Greeter module script looks like this:

# Greeter.py

import BigWorld
import GUI
import Math

class Greeter( BigWorld.Entity ):
    def __init__( self ):
        BigWorld.Entity.__init__( self )

Basic structure of tutorial/res/scripts/cell/Greeter.py

6.3.5.2. Prerequisites list

To avoid stalling the main thread client when the entity is created, we will use the prerequisites functionality to load the model asynchronously in the background loading thread. This is done by implementing the prerequisites method which returns a list of resources to be loaded. This means that whenever the server notifies the client that a Greeter entity has entered the AoI for the client, the client will first schedule the resources to be loaded asynchronously.

GREETER_MODEL_NAME = "characters/barbarian.model"

class Greeter( BigWorld.Entity ):
    ....

    def prerequisites( self ):
        return [ GREETER_MODEL_NAME ]

6.3.5.3. Entering and leaving the world

Once the prerequisite resources have been loaded, the onEnterWorld method is called. Since the entity class instance can leave the AoI and then re-enter the AoI, the bulk of the initialisation code will be done in here rather than in __init__ (so it can re-initialised each time).

For the Greeter entity, the primary entity model (Entity.model) is set, and a network filter is setup. Since the entity will not be moving around, we can use a simple DumbFilter which simply snaps the entity to the last network update.

class Greeter( BigWorld.Entity ):
    ....

    def onEnterWorld( self, prereqs ):
        # Setup our model.
        self.model = BigWorld.Model( GREETER_MODEL_NAME )

        # Setup an appropriate filter.
        self.filter = BigWorld.DumbFilter()

    def onLeaveWorld( self ):
        # Clean up.
        self.model = None
        self.filter = None

6.3.5.4. Implementing greet

The bulk of the client-side logic for the Greeter entity will go in the implementation of the greet method. This method is remotely called from the cell part whenever an Avatar enters the trap.

class Greeter( BigWorld.Entity ):
    ....

    def greet( self, targetID, msg ):
        # Grab the entity instance, if for some reason we don't have it just do nothing.
        try:
            targetEntity = BigWorld.entities[targetID]
        except KeyError:
            return
            
        # Try to play the Wave action. If it doesn't exist, print a warning.
        try:
            self.model.Wave()
        except AttributeError:
            print "WARNING: Greeter model missing Wave action (%s)" % self.model.sources
    
        # Display the greet message above our head.
        addressee = targetEntity.name        
        if targetID == BigWorld.player().id:
            addressee += "! Yes you"
            
        self._displayMessage( "Hey %s! '%s'!" % (addressee, msg) )

6.3.5.5. Displaying the message

The script that displays a text message above the Greeter's head will be implemented in a private helper method called _displayMessage (note the usage of an underscore to denote a private member - this is not required but it is a useful convention to follow). The TextGUIComponent class from the GUI module will be used and will be inserted into the 3D scene using the GUI.Attachment class (as opposed to being rendered in screen space). The text is attached to the root node of the entity model and is positioned above the head of the model by inspecting the model's height attribute.

class Greeter( BigWorld.Entity ):
    ....

    def _displayMessage( self, msg ):
        # First make sure any previous message is cleared.
        self._clearMessage()
        
        # Create our text component. Since we want to display it in the world
        # we shall explicitly set our width and height in world units.
        text = GUI.Text( msg )
        text.explicitSize = True
        text.size = ( 0, 0.5 )             # Specifying 0 for x to auto-calculate aspect ratio.
        text.colour = (255, 0, 0, 255)     # Change the colour.
        text.filterType = "LINEAR"         # Don't use point filtering.
        text.verticalAnchor = "BOTTOM"     # Position relative to the bottom of the text.
        
        # The origin of our model is at our feet. To place the text above
        # our head, move it up on the Y by our model's height.
        text.position = (0, self.model.height + 0.1, 0)
        
        # Setup our GUI->World attachment. Tell it that we want the GUI 
        # component to always face the camera.
        atch = GUI.Attachment()
        atch.component = text
        atch.faceCamera = True
        
        # Attach to our model's root node.
        self.model.root.attach( atch )
        
        # Save a reference to the attachment so we can clean it up later.
        self._messageAttachment = atch
        
        # Setup the timer.
        self._setMessageHideTimer()

To make the message disappear after a certain amount of time, the BigWorld.callback function is used. The hide message timer functionality is wrapped up in some additional helper methods.

  • _clearMessage clears any existing message attachment above the entity's head.

  • _setMessageHideTimer sets up the timer, while first cancelling any existing timer.

  • _cancelMessageTimer cancels the timer by passing the previously created timer handle into BigWorld.cancelCallback.

  • _handleMessageHideTimer is the Python callable that is given to BigWorld.callback. It is executed after the timer has elapsed, clearing the stored timer handle and removing the current message.

class Greeter( BigWorld.Entity ):
    ....

    def _clearMessage( self ):
        self._cancelMessageTimer()
        if self._messageAttachment is not None:
            self.model.root.detach( self._messageAttachment )
            self._messageAttachment = None

    def _setMessageHideTimer( self, timeout=5.0 ):
        self._cancelMessageTimer()
        self._messageTimerHandle = \
            BigWorld.callback( timeout, self._handleMessageHideTimer )

    def _cancelMessageTimer( self ):
        if self._messageTimerHandle is not None:
            BigWorld.cancelCallback( self._messageTimerHandle )
            self._messageTimerHandle = None

    def _handleMessageHideTimer( self ):
        self._messageTimerHandle = None
        self._clearMessage()

6.3.5.6. Handling activation change

The engine will automatically notify the entity script whenever a property has been changed by the server. It does this by looking for a method on the entity class named set_propertyName which is expected to take a single parameter for the previous value of the property. The Greeter script will take advantage of this notification and display a message whenever the activated state has changed.

    def set_activated( self, oldValue ):
        if self.activated:
            self._displayMessage( "Alright! I'm now ready to GREET." )
        else:
            self._displayMessage( "Shutting up now." )

6.3.6. Editor script

The editor script for an entity allows programmatic control over how the entity behaves in the World Editor. For the Greeter entity, the script will simply override the default model used to represent the entity in the editor (it otherwise defaults to a red box).

Editor scripts are located in res/scripts/editor.

class Greeter:
    def modelName( self, props ):
        return "characters/barbarian.model"

6.4. Testing

To test the entity it will first need to be placed into a space in World Editor. Open the spaces/main and place the entity by dragging the Greeter entity into the scene from the Resources tab. Save the space.

If you are not using a Windows mount, update the resources on the server side and then restart the server. If all is well, you should be able to connect as per-normal and see the Greeter in the space.

Greeter entity in action

If you do not see the entity, there are a couple of things to check:

  • Check the server startup logs for any Python exceptions.

  • Check the cell logs to make sure the entity is actually being created. You should see a message along the lines of:

    CellApp INFO Cell::createEntity: New Greeter
            (2)
  • Check the client for any client-side Python errors (e.g. bring up the in-game client console or use Debug View).

Note that at this point the only way to toggle the activation state is to use the in-game Python console. For example, on the client,

>>> $B.entities.items() # Find the ID for the Greeter
[(2402, Greeter at 0x088CFFE8), (2405, PlayerAvatar at 0x088CFC10)]
>>> greeter = $B.entities[2402]
>>> greeter.cell.toggleActive()

6.5. Possible improvements

While the entity satisfies the basic requirements, there are some improvements that could be made.

  • The most obvious improvement would be to allow the user to toggle the active state of the Greeter entity by clicking on the entity. This could be achieved by leveraging the entity targeting system of the client. See the Client Python API documentation for BigWorld.target.

  • If many player entities enter the trap at the same time, the client will try to greet everyone at once. Instead of simply playing the wave animation immediately when the greet method is called on the client, the client-side script could be designed so that greets are queued up so that the next greet will not commence until the previous greet has completed. This could be achieved by passing a callback into model.Wave() so that the scripts get notified when the current action has completed. See the Client Python API for ActionQueuer.__call__ for information on how you can use action callbacks.

  • Currently the Greeter entity simply plays the Wave action. It would be nice if the entity looked towards you while it is greeting you. A head tracker can be created by using the BigWorld.Tracker class coupled with the BigWorld.TrackerNodeInfo class.

  • The player can cause the Greeter to spam greetings if they quickly move in and out of the trap radius. To avoid this problem, the cell part of the entity should keep track of recent greets (associate an entity ID with a time stamp). It should only re-greet a player if some time has elapsed since the previous greeting. This list should be added as a new property in Greeter.def, and additional logic placed in Greeter.onEnterTrap.