Robert J. Laferriere, Michael L. Keller, Rich Gossweiler, Randy Pausch
Computer Science Department
University of Virginia
Charlottesville, Va 22901
contact author: Randy Pausch
(rjl2b, mlk4v, rich, pausch@virginia.edu)
Table of Contents
An Introductory Tutorial for Developing
Multi-User Virtual Environments
An Introductory Tutorial for Developing
Multi-User Virtual Environments
Note to reviewers: The intended audience for this paper is probably at the college sophomore/junior level. Many computer graphics courses would like to have students implement distributed applications --- this paper is intended to be used, among other purposes, as a supplement in such courses. Therefore, in many cases, we have chosen simpler terminology and/or explained some concepts that would normally be used without explanation in PRESENCE.
This paper is an introductory level tutorial describing how to implement multi-user virtual environments (VEs). The intended audience for this paper is students who are competent programmers and now wish to implement a distributed multi-participant application. We describe the fundamental concepts of distributed computing for multi-player simulations, and provide a concrete example which may be used as a template, including C source code available via the Internet. The template program demonstrates a simple multiple player, distributed application, where each player controls the position of a space ship, and the multiple programs communicate ship position data over the network. The template uses a technique called dead-reckoning to improve performance. We give detailed instructions on how to obtain and modify the template, so that students can quickly create their own distributed applications. We conclude by briefly discussing advanced issues, such as collision detection and physical modelling, reliability, and synchronization.
Virtual Environments (VEs) are interactive computer simulations that immerse users in an alternate, yet believable reality. People move around, look at, and manipulate graphics objects using input devices ranging from the commonly accessible keyboard and mouse to more exotic devices, such as head-mounted displays and instrumented gloves. VEs come in many flavors, ranging from a single person on a single computer, to many people running in parallel across a distributed set of machines.
One potential use for VEs is to allow several people, or players [Blau92], to interact in a single simulation; for example, several students sitting at different computers connected over a network. These multi-user VEs extend interaction beyond the single-user VEs, but introduce more programming complexity because multiple programs on different machines have to be managed, and the participants must coordinate communication among themselves. This introductory tutorial describes a basic mechanism for coordinating multiple programs and the communication between the programs. This paper also provides a template which is intended to provide new authors with a basis for developing their own distributed VEs. We believe that a pragmatic tutorial of this type will allow new authors to explore the domain and that this will benefit the VE community.
When several computers are connected together, the resulting configuration is called a Local Area Network (LAN) or Wide Area Network (WAN) depending on how geographically far apart the computers are located. Typically LANs are within one building, while WANs may span the whole country or even the world. In a system where there are more than two computers connected together, how does one computer identify which computer to send the data? Each computer needs to have a uniquely identifiable number, which acts like a phone number, allowing one computer to call and send data specifically to another computer. Since the UNIX Operating System supports multiple programs (called processes) which run simultaneously on the same machine, this unique identification number must exist, not just for the computer, but for each individual process wishing to communicate with other processes. In fact, a single process may wish to maintain several communication lines simultaneously, and therefore need several unique identification numbers. A unique identification number for each process can be created by using the identification number of the machine (the Internet address, such as 128.143.66.54) in combination with an abstraction called a port. A port is simply an additional number (for example, 6543) which allows a machine to provide several unique numbers for that machine. Programmers can try to open a line of communication by using the machine address and any particular port number. If the port is already in use, then the call fails. Alternatively, programmers can request that the operating system return the next unused port.
Figure 1: multiple processes with lines of communication
The operating system abstraction for a line of communication is called a socket. When two programs wish to communicate, they both create sockets, connect the sockets together, send data, and when done, close the sockets. For example, process A creates a socket using its machine name and a pre-established port number:
#define PORT 6543
char hostname[256];
int socket, newSocket;
gethostname(hostname, 256); /* name for the machine, can be string or number */
socket = CreateSocket(hostname, PORT);
Now process A can wait for another process to connect on the given port:
newSocket = WaitForConnection(socket);
In the meantime, process B can create its own socket and then connect to the waiting process A:
#define PROCESS_A_MACHINE 128.143.66.54
#define PORT 6543
int socket;
socket = ConnectToProcess(PROCESS_B_MACHINE, PORT);
Note that process A now has two sockets, one which may be used for more new connections (socket) and one which is directly connected to process B (newSocket). Also note that both knew to use port 6543 to establish the connection. This implies that authors of the two programs agreed in advance to use that number.
If two sockets are created by two separate programs running on two separate machines, then how does either process know about the other process's new number? When a process starts up, it creates a socket using a port number that other processes need to find out about. This may be done by establishing ahead of time that each new process will use specific port numbers well-known to everyone beforehand. The danger here is that two completely unrelated programs on the same machine may try to use the same port number. One will obtain the port, and the other will fail. Then all of the other processes which want to connect to the failed process will connect to the wrong process. This problem compounds as more player-processes start up, since each process would try to use a fixed port number that could potentially conflict with a port already in use on that machine.
Another technique is to ask the operating system for the first free port number during run-time, and then to write the port number out to a well-known file. If the networked computers have a shared file system --- a system where multiple computers can read and write to the same files --- then a well-known file is simply an agreed upon file name from which to read and write. All connecting processes can read the file to find out about the machines and port numbers of the other participating programs.
A third technique involves a name server. For networked systems which do not support a shared file system, then a special name server process can be created. The name server does the same thing as a file: it serves as a repository for the machine names and port numbers of the processes which want to communicate with each other. Instead of being a file, it is a running process which simply waits for other processes to connect to it. The connecting process can tell the name server what port it will be using, or request the machines and port numbers of other processes. The name server must open one well-known port number which the other processes can use to access it. That one port number must be established between all of the programs in advance, but all of the other processes can dynamically establish their port numbers, substantially reducing the risk of a port number collision.
In addition to figuring out who is talking to whom, the computers must also agree upon a protocol for establishing how they transfer information back and forth. For example, in the real world, people use a radio protocol where they say "roger" to acknowledge that they received and understood the message and "over" when they are done [Santifaller91]. "Over" serves to let the other party know that the first party is not simply pausing, but is done speaking for the moment. For computers, a network message may include a special header or trailer to mark the message boundaries.
Figure 2: network message buffer
"Roger" is sent by the receiving party to acknowledge the receipt of a message. This "acking back" helps maintain order and a steady-state flow of communication between the sender and the receiver.
Figure 3: acking (acknowledging) back
Because the programs may send arbitrarily long messages to each other, for purposes of efficiency, the network may break a long message into fixed-sized packets before sending the data across to the receiver. All of these packets must be received and put back together to form the original message. On networks where there are several data paths from one machine to another, some packets may take faster routes, or even get lost.
Figure 4: multiple routes
The receiver must detect incorrectly ordered packets, and also determine when a packet is lost and request a repeat. This requires additional overhead in the messages and in the time to process the messages. For reliable networks where packets are not lost or re-ordered, this overhead is not necessary. There are two common protocols which deal with the efficiency and reliability of network packets: Transfer Communication Protocol and Internet Protocol (TCP/IP) and User Datagram Protocol (UDP). TCP/IP guarantees that the packets will arrive and that they will arrive in order. An alternative is UDP, which is faster, but provides to guarantees. For the purposes of this tutorial, we will use the more reliable TCP/IP protocol. For a detailed discussion on this protocol and several examples of code, we recommend Stevens' excellent book [Stevens90].
At present, there are two popular methods for implementing distributed Virtual Environments. In a centralized controller model, one computer collects all of the data from the different machines, stores the changes in some collection of data structures (which we'll call the database), and then it sends the results back out to each participating machine, which then drawing the scene's contents, and handles any user input. Although this model allows the programmer to develop a simple data structure to store and handle the data, it is not scalable. As more and more programs enter the simulation, all of the programs suffer, since they all must wait longer on the bottlenecked, centralized computer as it updates the database from each of the other programs.
Figure 5: centralized model
An alternative, more scalable approach incorporates a distributed controller model. Here, each program maintains its own complete, local copy of the database as well as performing the drawing, computation and animation of objects. When a program makes changes to its own database, it sends the data out so that all of the other programs can update their individual databases correctly. This model is scalable, because no single CPU is the bottleneck. The problem now involves the number of messages being sent among players.
One way to reduce the amount of messages being sent is to use an algorithm called dead-reckoning. Dead-reckoning is at the heart of popular simulation mechanisms, such as DIS (the Distributed Interactive Simulation protocol) [IEEE93], and is used in SIMNET[Pope89] and NPSNET[Macedonia95].
Consider a distributed space "dogfight" game with n players, each controlling his own space ship. Player X's program could move its own ship, and then send a message to all of the other n-1 players, telling them all where the ship has moved. Player Y's program would then move its ship and then tell everyone about the change. After each player-program has moved its ship just once for this round, this would result in n-1 messages from n players, or roughly n2 messages. As soon as a player-program moves its ship again, the player-program would have to send out n-1 more messages.
To reduce the amount of messages being sent, player X could instead send just the ship's position and its velocity (direction and speed). Then, when the ship moves again, if it moves along the same path as the previously-established velocity, player X does not need to tell the other players. The other players will already know where X's ship is because they have the ship's old position and current velocity. This ship position calculation, based on a last-known velocity, is called dead-reckoning.
Figure 6: actual path versus dead reckoning path
To accomplish this distributed model of simulation, every player program maintains a complete copy of the database, and each program is in charge of moving all of the objects within its database. A program may have direct control of a certain object (called its "live" object), or it may use dead-reckoning to move an object (called a "ghost", since the object is actually controlled by another process). To determine when an object has changed significantly in value, the program runs the dead-reckoning algorithm on the live object as well, and compares the difference. If the difference is significant, then the program informs the other players to update their ghost-objects with the new position and velocity.
Note that if player X's ship strays only slightly from the dead-reckoning path, then player X may decide that the difference is negligible, and not send messages to the other players. The player only needs to send a message out to the other players when the change is significant.
As a concrete example, the following describes the implementation of a two-dimensional (2D) multi-player, networked game. The code for this example is freely available via the Internet (see a later section entitled How To Obtain the Code). This code was developed in ANSI C using X-Windows graphics primitives and runs on SGI and Sun workstations under UNIX. We chose this level of simplicity so that we may provide template source code that can be used and extended immediately.
In this example, each player (program) controls the position and velocity of a single object, a space ship, but sees all of the participating space ships in the virtual environment.
Figure 7: two views of the simulation
Each player can accelerate, declerate or rotate his or her own ship, and maintains a complete database with all of the ships' positions and velocities. The simulation uses dead-reckoning, so it uses the current velocity to update the position of the other players' ships (ghosts). It must also run the dead-reckoning algorithm on its own live ship, to see if it has deviated significantly from the path the other players are maintaining. If so, then the program must send messages out to inform the players of the new position and velocity.
On a particular computer, the VE is calculating the position of the ghosts using the last reported velocity. The program may draw the ship in one position, then clear the image and draw it again in another position. These drawings are called frames and if the frames are presented fast enough, the illusion of motion is conveyed to the user.
while (!done)
{
ClearScreen();
for (i=0;i<numberOfShips;i++)
{
ship[i].position = CalculateShipPosition(ship[i].velocity);
DrawShip(ship[i]);
}
}
If the velocity of a ship is specified in frames --- that is, the ship moves one centimeter per frame --- then the ship may end up at different locations on different machines. For fast machines drawing at sixty frames per second, in one second the ship will have moved over half a meter. For a slower machine, drawing at ten frames per second, the ship will only be a tenth of a meter along.
To solve this, the ship's speed must be specified in distance per unit of time, rather than distance per frame. Instead of 1cm/frame, we specify velocity as 1cm/second. On fast machines the animation would be smoother than on slower machines, but both would present the ship at the same position.
As we mentioned earlier, each player-program that enters the simulation needs to find out about the other players, so that it can connect to them and communicate messages. The player-program also needs to leave its own machine/port number identification so that new programs can connect to it. We use a file to which everyone can read and write, as storage of this information. This requires a shared file system. If this is not an option, then the developer can use the same programmer's interface but will need to write the name server as a stand-alone process.
The file contains a list of active players, for each one, list a machine and port number. One potential problem is that two new players may attempt to access the file at the same time, resulting in one writing back the file and then the second one overwriting the file and destroying data. We perform a "move file" to guarantee that this condition won't occur. The first player moves the file to a temporary location. Then, should another program try to access the file, the second program will not find the file. The second program will wait until the file returns, knowing that when the file does return, what it will read will be complete, and that no one else will be in the process of updating the file. We assume that two programs cannot move the same file at the same time.
After a program has moved the file, the program reads in the list of players, sends messages to the other players, then writes the file back out with its own machine/port identification. Similarly, when leaving the game, the player-program moves the file, deletes its entry, moves the file back, and tells the other players of its intention to leave. The last player out will leave a zero-length (empty) file. A zero-length file is distinguished from no file, because no file means some other program is currently accessing the file, while zero-length means that there are no players (an initial condition).
To implement this virtual environment we assume that we have the following programmer's interface to carry out the network communication. We have written these functions in C, and are available via FTP. This library of functions is used by the developer when creating a multi-player simulation:
playerList=CreatePlayer(GAME_FILE): This function creates a socket for the player process, obtaining a new port number dynamically. The function then checks for the well-known name file. If the file does not exist, that means someone else is currently accessing it, and so this function waits for the file to return. Once the file exists, this function moves the file (to prevent other writers from accessing the file), reads in the player-list and adds its machine name and port number into the file. This function also sends messages to all of the other players, so that they may add the new player as a ghost to their local database.
DeletePlayer(GAME_FILE): This function removes the player from the file and sends messages to all of the other active players, so that they may remove the player from their local databases. If this is the last player, the function leaves a zero-length file.
AddPlayer(playerList): This function adds a new player to the current player-list.
WriteToPlayers(playerList, message): This function sends the message to all the players in playerlist.
ReadFromPlayers(playerList): This procedure checks for incoming messages from other players and returns the received messages. If no messages are available, null is returned and the program continues (execution does not pause to await a message).
There are the three main parts to participating in the virtual environment - entering the simulation, maintaining current state information with other participants, and finally leaving the simulation. A flowchart of these steps is shown in Figure 8:
Figure 8: flowchart of simulation
When entering the simulation, a participant must create a socket so that others can connect to the new player, read in the game file to find out who is playing, and write out an entry to the game file, so that new players will know about this player. After this, the new player must connect with all of the existing players.
/*
* this involves creating a socket for the player (from the machine and a port number)
* moving and opening the GAME_FILE
* creating a list of players (id, machine, port) which is returned
* adding this player to the GAME_FILE
* connecting to all of the players
* moving the GAME_FILE back
*/
playerList = AddPlayer(GAME_FILE);
Since a new player may enter the simulation at any time, the current players must periodically check their socket for new players. During each simulation cycle a player must get user input which controls the "live" ship, check for messages about a new player connecting, check for messages about ship updates from other players, and draw all of the ships (a ship's position is computed either by a message update, or by using the dead-reckoning algorithm). If the "live" ship controlled by this program has deviated significantly from the dead-reckoning path, then the program sends a message to all of the participating players.
while (!done)
{
message = readFromPlayers(playerSocket); /* read any messages from other players */
switch (message.type)
{
case UPDATE:
FixOwnGhostPosition(message);
break;
case NEW_PLAYER:
AddNewGhost(message);
break;
}
done = getUserInput(); /* user may press `q' to quit */
CalculateAllShipPositions();
if (CompareLiveAndDeadReckoning())
{
WriteAllPlayers(liveShipPosition);
}
DrawShips();
}
The simulation loop executes until the player quits the simulation. Upon exiting, the connections to all other players are closed and the player entry is removed from the simulation file. The process of deleting a player is shown in the following pseudocode:
DeletePlayer(GAME_FILE, playerSocket);
WriteAllPlayers(QUIT);
ClosePlayer(playerSocket);
If this player is the last one in the simulation, then the game file is left as a zero-length (empty) file.
The following illustrations provide snapshots as the execution progresses. Players enter the game, participate in the simulation and leave the game:
When the first player-program enters the simulation, it updates the game file and creates a ship. Since there are no other players, there are no "ghost" ships for this player.
Figure 9: First Player In
When the next player and all players hereafter enter the simulation, they update the game file, connect to the existing players and inform them that it has entered the simulation.
Figure 10: Next Player In
With several players in the simulation, figure 11 shows how different players' screens appear when a ship moves. When a player moves its own ship, if the movement does not deviate much from the dead-reckoning, then no messages are sent. The ship still moves on all screens as a result of the dead reckoning algorithm:
Figure 11: Ship position on different screens
If the ship does deviate significantly from the dead reckoning path, then the player informs all of the other players to update their ghost ships with the new correct ship position and velocity.
Figure 12: Ship Position after Update
When a player-program wishes to leave the simulation, it informs the other players (so that they will remove the ghost ships from their local databases) and removes its entry from the game file.
Note that the game file is only accessed when new players enter the game and old ones leave. It serves one purpose: to let new players find out who is already in the simulation and how to contact them.
Note to reviewers: we are still user-testing the distribution mechanism of the skeleton code with a set of naive users: these instructions will change in the final version to reflect those changes.
To obtain the source code, makefile, datafile and executable, type the following:
· $> ftp uvacs.cs.virginia.edu
· login name: anonymous
· password: (your name and machine, for example dave@virginia.edu)
· $> binary
· $> cd pub
· $> cd multiplayerTutorial
· $> prompt
· $> mget *
· $> quit
Once you have done this, you can run the game by typing game at the command line. If problems arise:
· make sure you have a zero-length (empty) data file called gamefile
· make sure other players have read and execute permissions on the directory, and read and write permission on the gamefile. If you are not familiar with UNIX, you may need help with this, if problems occur.
Start the program from several machines and play the game. After you have experienced the game, try modifying the code. We recommend a simple change at first, such as the shape of the ships, just so you can successfully demonstrate that you are correctly re-building the executable with your changes.
While the previous sections discussed some of the fundamental issues involved when setting up a multi-player simulation, there are several more advanced issues that go beyond the extent of this tutorial. Understanding these issues is not necessary to build your own distributed application from the template, but we mention them briefly.
This simple example did not implement collision detection or perform physically correct modelling, but clearly these are potentially useful features. Collision detection quickly becomes more complicated as the number of objects in the environment increases, and the accuracy with which the checking is performed. The issue of collision detection is discussed in several references [Uchiki83] [García94].
Physically correct modelling is a complex topic. In order to perform realistic smooth updating, state variable characteristics of the object being modelled need to be considered. As an example, Moshell [Moshell94] developed a complex model for dynamic terrains that involves numerical methods to solve complex models of Newtonian objects. For interested readers, there are VE development platforms such as VEOS [Coco] and DIVE [Carlsson93], with more advanced primitives to simplify virtual environment development.
As the distributed programs become more complicated, the chances for a process to fail increases. How do the other players find out that a client no longer exists? One technique is to have a program periodically "ping" the players to make sure that they are still up and running. Another technique is to make it the responsibility of the players to periodically issue "heartbeats" or "I-am-alive" messages to the other players.
If timing data is shared between processes, then their clocks have to be synchronized. One process might think it is 1:00 pm and another might think it is 2:00 pm. A well-known solution which involves passing everyone's clock time around to synchronize clocks is discussed by Lamport [Lamport78].
Imagine the scenario where ship A and ship B are both on a collision course with ship C. If two ships collide, then they are instantly vaporized. If ship A and B hit C at exactly the same time, what happens? If the events are processed in a particular order different results may occur. If the event of A hitting C is processed first, then A and C may be removed from the game and B survives. Conversely, if the B-hits-C event is processed first, then A is left alive. One solution is to "freeze" time and process each event without regard to other events happening during that frozen moment. Within a given simulation-tick all simultaneous events are processed but their results are stored in a separate "new state". By doing it this way, events that happen at the same time are not influenced by the order in which they are processed. A hits C and A and C are marked for deletion. B hits C and B and C are marked for deletion. The new state does not have A, B or C.
We provide new authors with a technical framework to begin designing multi-player systems. By providing some basic background information, a simple 2D multi-player simulation example and an FTP site for the source code, we make it possible for new developers to immediately begin creating their own distributed, multi-player virtual environment.
This work arose from efforts to complete a short term project given to the graphics class at the University of Virginia. Much of the literature addressing distributed systems delved into complex issues at abstract levels, requiring a steep learning curve to even begin to explore multi-player simulations. This initiated the desire to produce an introductory tutorial. We thank the 1994 computer graphics class at the University of Virginia, especially David Shiflett and Jeff White, and we give special thanks to the Virginia User Interface Group for their work on the Alice system [DeLine93].
- [Blau92] Brian Blau, Charles Hughes, Michael Moshell and Curtis Lisle, "Networked Virtual Environments," Computer Graphics, Special Issue on 1992 Symposium on Interactive Computer Graphics, March 1992, pp. 157-160.
- [Carlsson93] Chirster Carlsson and Olof Hagsand, "DIVE--- A Platform for Multi-User Virtual Environments," Computers and Graphics, 6, 1993.
- [Coco] Geoffrey P. Coco, and Dav Lion, "Experiences with Asynchronous Communication Models in VEOS, A Distributed Programming Facility for Uniprocessor LANs," Technical Report FJ-15, University of Washington.
- [DeLine93] Robert DeLine, "Alice: A Rapid Prototyping System for Three-Dimensional Interactive Graphical Environments," Masters Thesis, University of Virginia, May, 1993.
- [García94] Alejandro García-Alonso, Nicolás Serrano and Jaun Flaquer, "Solving the Collision Detection Problem," IEEE Computer Graphics and Applications, 14(3), May 1994, pp. 36-43, T385.I13 (ISSN 0272-1716).
- [IEEE93] Institute of Electrical and Electronics Engineers, International Standard, ANSI/IEEE Std 1278-1993, Standard for Information Technology, Protocols for Distributed Interactive Simulation, March 1993.
- [Lamport78] Leslie Lamport, "Time, Clocks and the Ordering of Events in a Distributed System," Communications of the ACM, 21(7), July 1978, pages 558-565, QA75.A68.
- [Moshell94] Michael Moshell, Brian Blau, Xin Li and Curtis Lisle, "Dynamic Terrain," Simulation, 62(1), pages 29-40, TA34.S54, (ISSN 0037-5497).
- [Pope89] Pope, Arthur, BBN Report No. 7102, The SIMNET Network and Protocols, BBN Systems and Technologies, Cambridge, Massachusetts, July 1989.
- [Santifaller91] M. Santifaller, TCP/IP and NFS: Internetworking in a UNIX Environment, Addison-Wesley, Reading, MA, 1991.
- [Stevens90] Richard W. Stevens, UNIX Network Programming, Prentice-Hall, N.J., 1990, QA76.76.063S755, (ISBN 0-13-949876-1).
- [Uchiki83] T. Uchiki, T. Ohashi and M. Tokoro, "Collision Detection in Motion Simulation," Computers and Graphics, 7(3-4), 1983, pages 285-293.
- [Macedonia95] Michael R. Macedonia, Michael J. Zyda, David R. Pratt, Paul T. Barham and Steven Zeswitz. "NPSNET: A Network Software Architecture for Large Scale Virtual Environments," submitted to the special issue of Presence on Networked VE & Teleoperation