Tutorial 3: Client-Server-Communication

From WiCWiki

Revision as of 06:27, 22 January 2009 by Lt. Sherpa (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

Contents

by Insane Buzzstards

Questions and Annotations

This tutorial can be discussed in this thread.
Please help us making this tutorial as perfect as it can be, by giving hints and writing comments!

Introduction

Client-Server-Communication is an important point for every multiplayer-mod, as you often need to exchange data between the server and the clients. Imagine you have a point-system, where every player gets points for a fulfilled task. The server is the place that calculates and holds all the data for that special system. How does a player know how much points he or one of his opponents has? The answer is quite simple: the server has to tell them all about their points.

General

Server to Client

Introduction

Sending a message from a server to a client is very simple. All you have to do is to call the serverimport's method "ClientPythonCommand" of the player you want to send a message to, f. ex.:

serverimports.thePlayers[3].ClientPythonCommand("myTestCommand", "message from Server")

This example sends a message to player 3.

Explanation

Let's take a look at the definition of that method, which is inside the Player-Class:

def ClientPythonCommand( self, aCommandName, *someArguments ):
   """ 
   Execute a command on the client.
	
   Args:
      aCommandName - (string) The name of widget to hide.
      *someArguments - This can be called with an arbitrary number of arguments.
      ( for more info about arbitrary number of arguments, http://docs.python.org/ )

   Returns:
   Nothing.
   """			
   wicg.ClientPythonCommand( self.__id, aCommandName, someArguments )

As you see, the method takes a commandName and an arbitrary number of arguments, meaning that you can give it as many arguments as you want. So what is the commandName? Well ... the explanation in the method, that it is the name of the widget to hide is wrong, probably a copy-paste-mistake. In reality, the commandName is the name of a method in the "clientimports.py". As you can see in our example, we have created a method with the name "myTestCommand" in "clientimports.py", which takes one parameter/argument:

def myTestCommand(aMessage):
   print aMessage 

What it does is just printing out the argument in the players console (well ... if the print-command is redirected to the console, see our first tutorial.)

So to sum it up in one sentence: when the server calls the "ClientPythonCommand"-method of player 3, as we did in our example, then the "myTestCommand"-method of player 3 will be executed and the message "message from Server" will be printed out in his console.

As you see at the code of the ClientPythonCommand, it does nothing but call the ClientPythonCommand of the wicg-module. You could do it in the same way.

Syntax

Here is a simple demonstration of the syntax for the "ClientPythonCommand"-method:

serverimports.thePlayers[playerId].ClientPythonCommand("MethodNameInClientImports", para1, para2, ..., paraX)

or:

wicg.ClientPythonCommand(playerId, "MethodNameInClientImports", para1, para2, ..., paraX)

Keep in mind: you have to add as many parameters to the method, as the method that shall be called has (except you have default-parameters, of course). In our case we had just one parameter, but you can have none or even more than 20. it depends on you.

Final words

Some final about ClientPythonCommand:

  • The first player is the one with the id 1. Unfortunately there is an inconsistency in how massive treats the playerIds. The OnPlayerJoinsTeam-method of the gamemode, for example, starts the playerIds with 0 - keep this in mind.
  • The method you want to execute doesn't necessarily have to be in "clientimports.py" - it is enough to import the method from another module
  • If you don't want to change the "clientimports.py" at all, then you can simply make a ClientPythonCommand to call the clientimports "PostEvent"-method and register to that event from somewhere else.

Client to Server

Client-Side

Sending information to the server is as simple as getting information from it. All you have to do is to call a method named "SendPlayerEvent":

wic.player.SendPlayerEvent( "MyEvent")

As you see, you have to pass one parameter (for now), which is the name of the event.

Server-Side

Registering the EventHandler

For the server to get this event, you have to set up an event-handler. With the help of an event-handler you can catch the events you send and react accordingly. You have to do this in the "serverimports.py" OnInit-Method.

def OnInit():
   if wicg.IsSinglePlayer():
      PostEvent( 'Init' )
   wic.ServerEventHandler = EventHandler # this is the important line
   return 1

What you basically do is to say that the wic.ServerEventHandler shall be "redirected" to the method "EventHandler" that we will create soon. (Well ... it is no redirection, but a function-pointer for everyone who knows what that is.)

Of course you can just call a function from another module that does this or react to the event if you delete the isSinglePlayer-if.

Writing an EventHandler-method

Here is a very basic event-handler:

def EventHandler( anEventName, someEventData ):
   """ This function will be registered as our generic event
   handler. Any server event without a specialized handler will
   be passed to this function. Events that isn't handled will just pass
   through silently. """
       
   # See if it's a player event
   if anEventName == "OnPlayerEvent":
       anEventString = someEventData[0]
       aPlayer = someEventData[1]

       if anEventString == "MyEvent":
           playerId = aPlayer.Id
           # do something

As you see what you get is "anEventName" and "someEventData". "anEventName" is the type of event that you get, for example an "OnPlayerEvent" - "OnPlayerEvent"'s are the ones that we send from the client via "SendPlayerEvent". To every event, there belongs additional information that is stored in the python-list "someEventData". Whenever we call "SendPlayerEvent" to send an "OnPlayerEvent", it sends some information with it. The first item of the list (at index 0) is the event-string that we sent ("MyEvent", see above). The second item is the player-object, from whom this event comes. The third item is a python-tuple with additional parameters that we can send via "SendPlayerEvent", but more about this later.

As you see, we have to check what "anEventName" is, and if it is an "OnPlayerEvent", then we can get the eventstring and the player from the "someEventData"-list. Next, we have to check what event it was to react accordingly. We check if it was the "MyEvent"-event and if so, we do whatever we want to happen when this event occurs. Remember that we get a whole player-object and not just an Id. That's it. Pretty easy, isn't it?

Sending some more information - not just an eventstring

Okay ... now we want to send some other information with the event, f. ex. the currently selected unit or something. That is a little more complicated.

Client-Side

On the client, we have to add a python-tuple as a second parameter to our "SendPlayerEvent"-call. A tuple is more or less the same as a python-list, except that fact, that you can't change it afterwards. You can access it in the same way as a list:

item = myTuple[3]

But the creation of a tuple is a bit different:

myTuple = ("item1", "item2", 3)

You may see it or not, but there comes a problem with tuples: they are created with normal brackets, which are normally used for structuring your code, f. ex. (3+4)*7. This is no problem when creating tuples with multiple elements, but there will be a problem for single-element-tuples. Now there are two possibilities if you just want to pass one additional parameter:

1.) create a tuple with two elements and just don't use the second one:

myTuple = ("importantElement", "bla")

2.) create a single-item-tuple with this strange syntax:

myTuple = ("importantElement",)

See the comma? it is important - otherwise python would accidentally treat this as a string in parentheses.

In the end, all you have to do is to add a tuple to the function-call of "SendPlayerEvent":

myTuple = ("item1", "item2", 3)
wic.player.SendPlayerEvent("MyEvent", myTuple)
# or create the tuple on the fly:
wic.player.SendPlayerEvent("MyEvent", ("item1", "item2", 3))
wic.player.SendPlayerEvent("MyEvent", ("importantElement",))

Server-Side

On the server-side it mostly stays the same, except that you use the third element of of the "someEventData"-list (at index 2), which is the tuple that you passed as the second parameter of the "SendPlayerAction"-method:

def EventHandler( anEventName, someEventData ):
   
   # stuff from above

       if anEventString == "MyEvent":
           myPassedArgs = someEventData[2]
           myArg1 = myPassedArgs[0]
           myArg2 = someEventData[2][1] # alternative way without another variable
           # do something

That's it! Not very complicated, he?

Table: Events that reach a Server-EventHandler

There are some more events that you can catch via a Server-EventHandler:

anEventName Explanation Argument 1 Argument 2 Argument 3
OnPlayerEvent Thrown, when a PlayerEvent was sent The EventSting that was sent, defining which event The Player that sent the event (object) A tuple containing additional arguments that were sent
OnPlayerReady Thrown when a players says he is ready ? ? nope
OnSpecialAbilityFired Thrown when an unit's special-ability was fired ? nope nope
OnCommandPointCaptured Thrown when a team captures a command-point ? ? nope
OnDestroyableObjectDestroyed Seems to be for buildings with health values ? ? ?
OnGameWin Thrown at the end of the game ? ? ?
OnTAUsed Thrown when a player uses a TA ? ? ?
and probably some more ... ? ? ? ?

Client-Client ;)

When we're talking about event-handlers, we shouldn't forget Client-EventHandlers. Sometimes WiC sends events, f. ex. button-clicks, that you can react to. The whole thing works nearly the same as a Server-EventHandler and Massive already did a good explanation in the GUI-Python-Module. Nevertheless, let's explain it again, as it belongs here.

Registering the EventHandler

To catch a client-event, we have to register an event-handler, just as we do for the server. You have to do this in the "client.py"'s OnInit-Method of the map. Unfortunately the "clientimports"'s OnInit-method doesn't work, what is actually quite sad, as this code should belong there.

def OnInit():
   wic.ClientEventHandler = EventHandler # this is the important line

It is exactly the same as with the Server-EventHandler, except that we are using a different variable: "wic.ClientEventHandler".

Writing an EventHandler-method

Here is a very basic client-event-handler:

def EventHandler( anEventName, someEventData ):

""" This function will be registered as our generic event handler. Any client event without a specialized handler will be passed to this function. Events that isn't handled will just pass through silently. """

# See if it's a button that was pressed. if anEventName == "OnButtonPressed": aGui = someEventData[0] aHashedButtonName = someEventData[1]

# Check if it was the button named Button_CreateTank if aHashedButtonName == wic.common.StringToInt( "Button_CreateTank" ): # do something

Again we have "anEventName" and "someEventData", so this should be clear. If we want to catch a button-event, then we have to check the eventname, if it is "OnButtonPressed". If so, then the first element of "someEventData" is the gui the pressed button belongs to. The second element is the hashed name of the button (meaning a number instead of a string). Now we still gotta check, which button was pressed. So we have a simple if-clause to check, if the hashed-button-name is the same as the hash of the button-name we want to check. Sound strange - but that's the way it is.

That's all.

Table: Events that reach a Client-EventHandler

There are some more events that you can catch via a Client-EventHandler:

anEventName Explanation Argument 1 Argument 2
OnButtonPressed Thrown when a Button is pressed The Gui that the Button is located in The hashed name of the Button
OnWidgetLostFocus Thrown when the mouse leaves a Widget The Gui that the Widget is located in The hashed name of the Widget
OnWidgetGainedFocus Thrown when the mouse is over a Widget The Gui that the Widget is located in The hashed name of the Widget
OnGuiActivated Thrown when a Gui becomes activated The activated Gui The hashed name of the Gui
OnGuiDeactivated Thrown when a Gui becomes deactivated The deactivated Gui The hashed name of the Gui

OnPlayerAction

There is also another way to get events from the clients, and that is the method "serverimports.OnPlayerAction":

def OnPlayerAction( aPlayerID, anActionString ):

This method gets the id of the player (this time starting with 0 - \*arg\* inconsistency) and an action-string, meaning event-name. Unfortunately you don't get a lot information from this function, which makes it often quite useless, but sometimes you just need the events, that come with "anActionString".

Here is a list of the events, that you can catch with this method (thanks to "A New Hope"):

  • MEGAMAP_PRESSED
  • MEGAMAP_CLOSED
  • REINFORCEMENTS_SCREEN_PRESSED
  • SPAWNER_DEPLOY
  • UNITS_DEPLOYED
  • AGENT_SELECTED
  • AGENT_SELECTED_MULTIPLE
Personal tools
User Created Content