bw logo

Chapter 4. Implementing a chat system (CHAT_CONSOLE)

At this stage we have a basic client-server game working, so it is a good time to write our first entity methods and learn how method calls propagate in BigWorld.

As an easy first example, we will write a simple chat system that allows players to talk to the other players around them. The implementation is in two parts:

  • Implementing a basic GUI for displaying and entering chat messages on the client.

  • Writing the entity methods to propagate the messages between clients and the server.

4.1. GUI text console

The chat console GUI needs to be able to display a few lines of text and be able to accept and display the player's input on a separate line, the edit line. It is implemented in tutorial/res/script/client/Helpers/ChatConsole.py.

That chat console displays through two GUI components. A window that darkens the background so the text is easier to read and a multiline text component which is a child of the window. These are both set up in the __init__() method.

import string
import BigWorld
import GUI
import Keys
import collections

class ChatConsole( object ):

    sInstance = None

    def __init__( self, numVisibleLines = 4 ):

		self.numVisibleLines = numVisibleLines
		self.lines = collections.deque()
		self.editString = ""

		self.box = GUI.Window( "system/maps/col_white.bmp" )
		self.box.position = ( -1, -1, 0 )
		self.box.verticalAnchor = "BOTTOM"
		self.box.horizontalAnchor = "LEFT"
		self.box.colour = ( 0, 0, 0, 128 )
		self.box.materialFX = "BLEND"
		self.box.width = 2
		self.box.script = self

		self.box.text = GUI.Text()
		self.box.text.verticalPositionMode = "CLIP"
		self.box.text.horizontalPositionMode = "CLIP"
		self.box.text.position = ( -1, -1, 0 )
		self.box.text.verticalAnchor = "BOTTOM"
		self.box.text.horizontalAnchor = "LEFT"
		self.box.text.colourFormatting = True
		self.box.text.multiline = True

		GUI.addRoot( self.box )

		self.active = True
		self.update()
		self.box.height = self.box.text.height * ( numVisibleLines + 1 )
		self.editing( False )
    

The Avatar and Personality scripts calls three of the chat console's methods:

  • instance() returns the chat console singleton, creating it on the first call

  • editing() controls the visibility of the console and it is activated when the player hits the return key. If no parameter is given it returns the current state

  • write() causes a line of text to be displayed and it is called from the Avatar's say() method which is in turn called when a chat message is received from the server.

    @classmethod
    def instance( cls ):
        """
        Static access to singleton instance.
        """

        if not cls.sInstance:
            cls.sInstance = ChatConsole()

        return cls.sInstance


    def editing( self, state = None ):

        if state is None:
            return self.active
        else:
            self.active = state
            self.box.visible = state


    def write( self, msg ):

        self.lines.append( msg )

        # Rotate out the oldest line if the ring is full
        if len( self.lines ) > self.numVisibleLines:
            self.lines.popleft()

        self.editing( True )
        self.update()

    

When the chat console is visible it also parses key events. Printable characters are added to the edit line and removed with the backspace. The return key sends the edit line to the server for propagation and puts it into the main display. This is done with the commitLine(). When the line is committed or anything else changes the update() method will correctly set the text field of the text component. Finally the escape key closes the chat console.

    def commitLine( self ):

        # Send the line of input as a chat message
        BigWorld.player().cell.say( unicode( self.editString ) )

        # Display it locally and clear it
        self.write( "You say: " + self.editString )
        self.editString = ""


    def update( self ):

        if self.active is False:
            return

        self.box.text.text = ""

        # Redraw all lines in the ring
        for line in self.lines:
            self.box.text.text = self.box.text.text + line + "\n"

        # Draw the edit line
        self.box.text.text = self.box.text.text + "\cffff00ff;" + self.editString + "_" + "\cffffffff;"


    def handleKeyEvent( self, event ):

        if event.isMouseButton():
            return False

        if self.active is False:
            return False

        if event.isKeyDown():
            if event.key == Keys.KEY_ESCAPE:
                self.editing( False )
            elif event.key == Keys.KEY_RETURN:
                self.commitLine()
            elif event.key == Keys.KEY_BACKSPACE:
                self.editString = self.editString[:len( self.editString ) - 1]
            elif event.character is not None:
                self.editString = self.editString + event.character

            self.update()
            return True

        return False
    

4.2. Modifications to the Avatar entity

We need to implement methods on both the client and the server to make our chat system work:

  • The server-side methods are responsible for receiving messages and forwarding them to other clients whose player entities are close enough to the speaker.

  • The client-side methods are responsible for displaying incoming messages on-screen.

Before implementing these methods, they need to be declared in tutorial/res/scripts/entity_defs/Avatar.def:

...
   <ClientMethods>
      <!-- Chat to people within 50 metres -->
      <say>
         <Arg> UNICODE_STRING </Arg> <!-- message -->
         <DetailDistance> 50     </DetailDistance>
      </say>
   </ClientMethods>
   <CellMethods>
      <!-- Cell part of the chat implementation -->
      <say>
         <Exposed/>
         <Arg>            UNICODE_STRING </Arg>
      </say>
   </CellMethods>
...

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

The step above adds the method definitions to the previously empty client and cell method sections. The cell method definition includes the <Exposed/> tag, which exposes the method to the client. Without this, the method cannot be called from the client. The definition file also uses BigWorld's method LODing feature, by declaring a <DetailDistance> of 50m, which means that referring to self.allClients or self.otherClients from within this method will not refer to all clients in that entity's AoI, just those within 50m.

Having declared these methods, we must now provide their implementations. In tutorial/res/scripts/cell/Avatar.py, add the following:

...
   def say( self, id, message ):
      if self.id == id:
         self.otherClients.say( message )

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

Even though we prototyped the cell method to take only the message as an argument in the definition file, our implementation expects another argument (id) before the declared arguments. This is because this method was declared as <Exposed/>, and the ID passed as an argument is that of the client who called the exposed method. Please note that this may not be the client who is attached to this Avatar, so we add a check to make sure the calling client is in fact the owner of this entity.

Note

We only forward the message to self.otherClients, not to self.allClients. This is because in our earlier implementation of ChatConsole.editCallback in tutorial/res/scripts/client/Helpers/ChatConsole.py (for details, see GUI text console) when the user enters a line of text it is immediately displayed on his client, so we do not want to send the message back to him. Therefore, we only need to call the say method on other clients.

Now we implement the client entity's say method in tutorial/res/scripts/client/Avatar.py:

class Avatar( BigWorld.Entity ):
   ...
   def say( self, msg ):
      ChatConsole.ChatConsole.instance().write( "%d says: %s" % (self.id, msg) )
	

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

Now you should have a basic usable chat system. Connect a couple of clients to a running server and test it out!