Vizard 7 » Reference » Networking » Multi User environment
7.6

Multi User Environment

Using viznet we can recreate the Network communication tutorial to easily support multiple users by using the client server approach.

Server

The first thing to do is to add the required imports, start the Vizard engine, set the background, and position the display so that the full map will be visible as the server is used to watch other interactions.

import viz
import viznet
viz.go()
viz.clearcolor(.2,.2,.4)

viz.MainView.setPosition([70,45,-60])
viz.MainView.lookAt([0,0,10])

Next we create a list of objects which must be identical to the list of objects on the client. These are the allowable character displays for each client. We then add the map, and finally an empty dictionary to store client information in.

objectList = ['duck.wrl','logo.wrl','vcc_male.cfg','vcc_female.cfg']
maze = viz.add('tankmaze.wrl')
objects = {}

Here we define specific events which will be used in the interaction between the client and the server. These events must be defined in the same way on each client.

UPDATE = viznet.id('update')
CLIENT_DISCONNECTED = viznet.id('client_disconnected')
CHAT = viznet.id('chat')

Now we start the server and handle client joining and client leaving events. The onStopClient function preforms some cleanup actions as described in the comments.

viznet.server.start(port_in=14950,port_out=14951)

def onStartClient(e):
    print('** Client joined:',e.sender)
viz.callback(viznet.CLIENT_CONNECT_EVENT,onStartClient)

def onStopClient(e):
    print('** Client left:',e.sender)
    viznet.server.send(CLIENT_DISCONNECTED,client=e.sender) # Alert other clients
    objects[e.sender].remove() # Remove object from our map
    del objects[e.sender] # Delete user from array
viz.callback(viznet.CLIENT_DISCONNECT_EVENT,onStopClient)

The update function essentially updates a client's position, however if this is the first time the client's position is updated, it also adds the client to the map. Adding the user this way makes it so we don't have to have another event to handle when the client has picked a character. Further more this way we don't have to send a list of already connected clients to all new clients.

def update(e):
    try:
        objects[e.sender].setPosition(e.pos)
        objects[e.sender].setEuler(e.ori)
    except KeyError: # If key doesn't exist add the user's object
        objects[e.sender] = viz.add(objectList[e.object],scale=[2,2,2],pos=e.pos,euler=e.ori)
        objects[e.sender].tooltip = e.sender # Sets the tool tip for this object
viz.callback(UPDATE,update)

Here we import and define the tool tip for the server.

# Tool Tip
import viztip
tth = viztip.ToolTip()
tth.start()
tth.setDelay(0)

The following portion of the server handles the chat features. The updateChat function updates the string to be displayed. The variable text is the on screen messages which is limited at 5, and the variable input displays the messages input by the server as they are input. The onChat function is called whenever the CHAT event is called which was defined previously.

# Chat Stuff
def updateChat(msg): # Function to update the chat display
    text.message('\n'.join(text.getMessage().split('\n')[1:])+'\n'+msg)

text = viz.addText('\n\n\n\n',viz.SCREEN) # Add to screen
text.alignment(viz.TEXT_LEFT_BOTTOM)
text.fontSize(25)
text.setPosition(.05,.08)

input = viz.addText('> _',viz.SCREEN) # Add to screen
input.fontSize(25);
input.setPosition(.032,.05)
input.visible(0) # We only want visible when in type mode

def onChat(e): # Update the message display
    updateChat(e.client+': '+e.chat)
viz.callback(CHAT,onChat)

This portion handles the input of messages. To start typing a message one must first press 'y' thus enabling talking mode. Once in talking mode one can exit by either sending the message with the enter key, or canceling the message by pressing escape.

 

Note that this example only handles the basic keyboard characters.

talking = False
def onKeyDown(key):
    global talking
    if key == 'y' and not talking: # Start talking mode
        talking = True
        input.message('> _')
        input.visible(1)
    elif key == viz.KEY_ESCAPE and talking: # Exit talking mode
        talking = False
        input.visible(0)
        return True
    elif talking and key == viz.KEY_RETURN: # Send message
        talking = False
        viznet.server.send(CHAT,client='SERVER',chat=input.getMessage()[2:-1])
        input.visible(0)
        # Update display with our message
        updateChat('SERVER'+': '+input.getMessage()[2:-1])
    elif talking and key == viz.KEY_BACKSPACE: # Delete a character
        if len(input.getMessage()) > 3:
            input.message(input.getMessage()[:-2]+'_')
    elif len(key) > 1: # Handle special keys
        return False
    elif talking and 32 <= ord(key) <= 126: # Input message
        input.message(input.getMessage()[:-1]+key+'_')
viz.callback(viz.KEYDOWN_EVENT,onKeyDown,priority=-6)

Client

The client starts the same by setting up the background, adding the map and creating an empty dictionary of objects.

import viz
import viznet
import vizinfo
import vizinput

viz.go()
viz.clearcolor(.2,.2,.4)
maze = viz.add('tankmaze.wrl')
objects = {}

Then a timer is defined which is used to send updated position and orientation data, followed by the user defined events, which are identical to those on the server.

UPDATE = viznet.id('update')
CLIENT_DISCONNECTED = viznet.id('client_disconnected')
CHAT = viznet.id('chat')

Here is a clean way to connect to a server. This continues to probe the user for a server to connect to until it is connected, or the user no longer wishes to try. If the user does not connect the application is closed.

SERVER_NAME=vizinput.input('Enter Server Name')
while not viznet.client.connect(SERVER_NAME,port_in=14951,port_out=14950):
    if vizinput.ask('Could not connect to ' + SERVER_NAME + ' would you like to try another?'):
        SERVER_NAME=vizinput.input('Enter Server Name')
    else:
        viz.quit()
        break

Here we create the object list, which is identical to that on the server, and ask the user which object they would like to be.

objectList = ['duck.wrl','logo.wrl','vcc_male.cfg','vcc_female.cfg']
obj_choice=vizinput.choose('Connected. Select your character',objectList)

The timer portion sends updated information to all clients at the specified interval. Notice that we additionally add the client parameter so that other clients know where the message originated. We also send which index in the object array we selected so that the server and other clients know which object we are.

def sendUpdate():
    ori= viz.MainView.getEuler()
    pos = viz.MainView.getPosition()
    viznet.client.sendAll(UPDATE,client=viz.getComputerName(),object=obj_choice,pos=pos,ori=ori)
vizact.ontimer(.05,sendUpdate)

Here the client handles updates. Since the client uses a sendAll the client receives updates about itself. Thus we must first ignore messages that originated from itself. Then like we did with the server update function we update the position and orientation if the object already exists otherwise, we add the new client's object.

def update(e):
    if e.client != viz.getComputerName():
        try:
            objects[e.client].setPosition(e.pos)
            objects[e.client].setEuler(e.ori)
        except KeyError:
            objects[e.client] = viz.add(objectList[e.object],scale=[2,2,2],pos=e.pos,euler=e.ori)
            objects[e.client].tooltip = e.client
viz.callback(UPDATE,update)

The client_disconnected function performs some simple cleanup when another client disconnects, and the onStopServer function closes the client.

def client_disconnected(e):
    objects[e.client].remove()
    del objects[e.client]
viz.callback(CLIENT_DISCONNECTED,client_disconnected)

def onStopServer(e):
    print('Disconnected from server')
    viz.quit()
viz.callback(viznet.SERVER_DISCONNECT_EVENT,onStopServer)

The tool tip functionality is then added.

import viztip
tth = viztip.ToolTip()
tth.start()
tth.setDelay(0)

Finally the chat is defined. This is nearly identical to the server's chat except that we use the client's sendAll function, rather than the server's send function, and because of that the client does not need to handle its own messages.

def updateChat(msg):
    text.message('\n'.join(text.getMessage().split('\n')[1:])+'\n'+msg)

text = viz.addText('\n\n\n\n',viz.SCREEN)
text.alignment(viz.TEXT_LEFT_BOTTOM)
text.fontSize(25)
text.setPosition(.05,.08)

input = viz.addText('> _',viz.SCREEN)
input.fontSize(25)
input.setPosition(.032,.05)
input.visible(0)

def onChat(e):
    updateChat(e.client+': '+e.chat)
viz.callback(CHAT,onChat)

talking = False
def onKeyDown(key):
    global talking
    if key == 'y' and not talking:
        talking = True
        input.message('> _')
        input.visible(1)
    elif key == viz.KEY_ESCAPE and talking:
        talking = False
        input.visible(0)
        return True
    elif talking and key == viz.KEY_RETURN:
        talking = False
        viznet.client.sendAll(CHAT,client=viz.getComputerName(),chat=input.getMessage()[2:-1])
        input.visible(0)
    elif talking and key == viz.KEY_BACKSPACE:
        if len(input.getMessage()) > 3:
            input.message(input.getMessage()[:-2]+'_')
    elif len(key) > 1:
        return False
    elif talking and 32 <= ord(key) <= 126:
        input.message(input.getMessage()[:-1]+key+'_')
viz.callback(viz.KEYDOWN_EVENT,onKeyDown,priority=-6)

See also

In this section:

Networking basics

viznet

Networking Command Table

Cluster basics

Other sections:

Tutorial: Network communication