Table of Contents
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.
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'ssay()
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
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!