As game developers, we are continuously challenged to create richer and richer game worlds. Whether we are developing a 16-player multiplayer game, or a 10,000-player persistent world, making richer game worlds efficiently means we must be increasingly intelligent about how we distribute the everchanging state of our game objects. This problem is further complicated by the diversity of the network connection characteristics of each player. In this article, I’ll describe a technique for managing the distribution of object state using an encapsulation mechanism called an object view. Object views provide a means for managing the distribution of object state on a perobject basis that is flexible and transparent to the game object. In order to describe what they are and how they are used, we’ll also peer into the workings of a distributed object system designed for multiplayer games.
Net Profit, Net Loss
A s with many other areas of computing, some of the most significant problems inherent in distributing simulations have to do with resource management. In the case of networking, our primary concerns are with the limitations of the game clients and especially the nasty problem of controlling bandwidth utilization.
For most subscriptionbased massively multiplayer (MMP) games, bandwidth limitations are not based upon physical limits; rather they are based upon band width costs. This means that proper bandwidth management translates into real dollars in a very big and measurable way.
Other techniques, such as those for masking lag and smoothing movement, are also essential for creating great multiplayer games. But for these to be effective, accountability must be had in the underlying implementation for the bandwidth limitation, whether constrained artificially or by the physical medium itself. After all, the bits have to actually arrive at their destination before they can do any good. Proper bandwidth management isn’t just a networking problem, it’s a wholegame problem.
But what does all this accountability have to do with object views? Before we get into the nuts and bolts of object views, let’s talk a little about why we need them.
So Many Objects, So Little Time
At Monolith, we have been using object views as a fundamental construct in the development of our distributed object system. A Distributed-object system is a game system that manages the housekeeping chores related to the distribution of object state. It is the principal user of the relevantset creation mechanisms, which in our implementation are provided by the world representation (see Figure 1). Relevant sets are collections of objects whose state changes need to be distributed immediately (if not sooner) if we are to ensure that a remote client’s view of the simulation matches the actual state of the simulation. The topic of relevantset generation is so large that it warrants its own separate discussion, so I won’t be delving into it very much here.
Figure 1. Major game components involved with object state distribution.
In multiplayer games, we generally associate a connected playerclient with a single, playercharactercentered view of the simulation. Each client only needs to render a limited portion of the game world at any one point in time. Consequently, the state of all the game objects that are relevant to the rendered portion of the simulation must be uptodate.
Direct data management vs.RPC. Distributed-object system implementations for both games and distributed simulations typically manage distribution of object state data rather than simply providing a generalpurpose remote procedure call (RPC)based mechanism. Why? The answer is rooted not only in our fundamental need to make the best possible use of the available bandwidth, but also, as we will see later, in our need to design a system that makes it as simple as possible for us to specify exactly how we want the component parts of our game objects to be distributed.
The responsibilities of a Distributed-object system are conceptually quite simple:
- Obtain a relevant set of objects for a client.
- For each object in the relevant set, distribute any state that has changed since the last time that object was distributed to that client.
- Repeat the preceding steps for each client.
As simple as the system seems conceptually, the devil really is in the details. Even if we are strictly using visibility based relevance determination, the full relevant set for a given client at any instant can be enormous. As an example, consider what happens when you direct your playercharacter to stroll up to the top of a nearby hill. As you crest the hill, the number of visible objects is likely to increase dramatically. Unfortunately, the amount of available bandwidth remains somewhat constant over time, so the distribution of objects in the relevant set must be managed carefully, using prioritization techniques that allow the most important state to be sent immediately and the less important state to be transmitted as soon as possible thereafter.
Multiplayer game objects
.We’ve discussed that a key functionality of the dis tributedobject system is identifying, prioritizing, and selecting game objects from relevant sets, restricting the set of game objects to only those that need to be distributed. But only some of the components from a given object will need to be distributed. What are these components? To answer that, let’s take a look at a simple game object.
Figure 2. A simple game object.
Figure 2 shows the basic component parts of a simple game object that you might find in a generic multiplayer game. The object consists of three major groups of component items:
and displayrelated items. These are component items related to the
visual state of the game object, including movement and position information.
They very much need to be distributed. For playercharacter objects,
this includes values that may only be displayed on a HUD (headsup display)
of the player controlling that character.
logic and AIrelated items.These are component items related to the
game state of the object. In a purely serverbased simulation, these
items would seldom (if ever) be distributed to clients,but could be
distributed to a trusted entity, such as another server.
- Housekeeping items. These are component items, such as reference counts and pointers to internal structures. They are not distributed.
As our playercharacter roves around within the simulation, it will encounter new game objects, spend a little time hanging around near them, leave the area, and very likely reencounter many of the same game objects sometime later on. Since we only want to be sent updates for the items that have changed since the last time we encountered the object, something will have to remember the state that the object was in the last time we encountered it. To complicate matters, one client may have very different distribution requirements from another client for the same object. This is where object views come in.
An object view is an instance of a custom class that knows how to access one or more components of a game object and track any changes to those components. Every object view is attached to a game object, and every object view also has a remote counterpart that is attached to a game object with a similar set of components. As changes occur to the states of the tracked components, the object view is responsible for communicating those changes to its remote counterpart. The counterpart is then responsible for applying those changes to the game object to which it is attached.
The distributed-object system itself is designed to interact with object views, not game objects. How the object view interacts with each game object is strictly a contract between the object view and the game object. The distributed-object system only distributes object views. To access the gameobject components (given a reference to a game object) efficiently at run time, each object view instance is created with full knowledge of which components of the game object it needs to track and how to access them. Hence, implicit in the nature of the object view is the notion of a binding to the gameobject components that the object view will track.
The abstraction from the game object that the object view provides to the distributed object system is one of its most significant benefits. An object view and its counterpart can each be bound to a different type of object and still communicate with each other for managing state distribution. This eliminates the requirement to use identical objects on both the client and the server. For us, this was an important design consideration, since our clientside objects differ significantly from their server side counterparts.
Object view operations. Figure 3 shows how object views interact with game objects and the distributed object system at a high level. Note that there is a onetomany relationship of object views to game objects on the server, and a onetoone relationship on the client. In client/server architectures, servers maintain connections to many clients, but the client typically has only one connection to a server. The object view functions as a local proxy that remembers the state of each game object’s distributed components from the last time it was distributed to a particular client. Since state distribution will only occur when game objects are relevant to a client, the state of each object view is potentially unique.
Figure 3. Object views in action (client/server).
When an object enters the relevant set for a client, the Distributed-object system first locates the clientspecific object views for that game object, creating a new one if one does not already exist. Newly created object views on the server represent objects that will need to be created and fully initialized on the client before they can be rendered.
Either way, the process of determining exactly what state updates are needed and how the determination is made is strictly a contract between the object view and the game object. In order to ensure that the object view is granted the flexibility it needs, the Distributed-object system requires every object view to provide two basic operations: packto and unpackfrom.
The packto operation is called when the object view needs to be provided an opportunity to distribute its state. The object view determines whether or not any state updates are required, and is then responsible for marshaling those updates directly into the transmission buffer, packing them as tightly as possible in the process. Only the sending object view and its receiving counterpart on the other end of the connection can be trusted to understand the format of this data. The object view’s unpackfrom operation is called up when state updates are received. This is typically a simple process of analyzing the received data and applying the updates to the appropriate components of the target game object. This also turns out to be a great time for an object view to provide event notifications to the game object — or to anywhere else in the game — whenever one or more specific components are updated.
A third basic operation that each object view should provide is solid diagnostics. Object view operations are deliberately mysterious to the rest of the system components, and only the object views themselves may understand the format of the data they utilize to communicate state updates. Because of this, marshaling errors will have downstream effects that can be difficult to debug without good diagnostics.
Tracking state changes.When it comes time to distribute the state of the game object, each object view will need to determine whether the components it is tracking have changed since the last time the packto operation was called. This requires the object view to remember something about the previous state of those components. There are a variety of techniques that the object view can utilize to track state changes; invasive techniques require special support from the game objects, whereas game objects operate obliviously to noninvasive techniques.
The determination of which tracked components have changed state will normally take place during the packto operation, and while the game object remains relevant for a client, the packto operation for its views will be called frequently. For this reason, the packto operation must be very efficient.
The most straightforward technique is for the object view to maintain its own copy of the game object components that it is tracking. If sufficient memory is available and the tracked items can be compared very efficiently, this noninvasive mechanism is hard to beat. Since the exact previous state of each variable is always available, the object view can be certain that it is only distributing state that differs on the target.
Adding a change counter to the game object is an evasive technique we have found particularly useful. We use this for complex objects that are tested frequently but whose state changes relatively infrequently. Each object view also has a change counter, and each time the state is distributed the view’s counter is set to the current value of the game object’s counter. By comparing the two counters, a very fast check can be made to see if any new changes have occurred. This technique could be used as an optimization for any object that is tracking more than a few items, but it does require that each game object be modified to ensure that its change counter is updated every time any of the tracked components are updated. Another invasive technique that we have seen utilized involves maintaining a bit set of change flags. This technique requires that the game object be designed to manage a bit set that is stored with the game object itself. Each bit in the set corresponds to a distributed component part. The object view keeps its own copy of the bit set and checks to see if its own copy matches that of the game object during the packto operation, in order to determine which component parts have changed.
Unfortunately, this technique suffers from three drawbacks. First, you must ensure that the corresponding bit is set every time a distributed component variable is updated. Second, if a component switches back and forth between a small set of states, then there is a significant chance that a value marked as changed would be sent to the target object even though it actually switched back to being in the same state as the target. This process wastes bandwidth. The third drawback is the most serious. The “changed” component bits on each object need to be cleared as soon as possible for optimal distribution, but they can only be safely cleared when state has been distributed to all clients.
Because of that fact, this technique is really only practical for smallscale simulations where all clients need to be kept continuously up to date with the current state of all game objects.
Directionality. In client/server architectures we normally don’t distribute object state from clients to servers for game objects other than the playercharacter object. Having multiple clients send competing updates to the same game object on a server might seem like a very strange thing to do in your game, but it might make complete sense for others, especially in peertopeer architectures. Though we tend to be quite securityparanoid when developing MMP games, there are no hardandfast rules. If sufficient safeguards are in place, any client could manage state updates and distribute those updates to a server or to other clients.
Complex objects. Game objects are typically hierarchical in nature. Any gameobject component may itself be an object with its own component parts. This makes managing access to the items slightly more complicated than dealing with, for example, a set of items that are primitive types. You can generate or manually create custom object views for every game object and access each of the subcomponent items directly, one at a time. But if the same constituent objects are used as subcomponents in a wide variety of game objects, it is possible to implement your object views in a way that allows you to reuse a lot of code. To do this, you will need to create object views for the full range of component item types used by your game objects.
This includes all object types and primitive types. Once this is done, complex objects can be managed by creating hierarchical object views that mirror the component hierarchy of the object.
Lifespan of an object view. Over the course of time, a player will potentially encounter tens of thousands of objects in a large simulation. A server would need to maintain all the object views permanently for every game object if it wanted to avoid the expense of recreating them. This is memoryintensive not only for servers, but also potentially for clients as well.
Fortunately, this problem can be handled fairly effectively using an active cache of object views. Old object views are then automatically purged from the cache over time if the game objects they track are not reencountered for extended periods.
Using Object Views
At the instant an object view needs to be created, a perfect opportunity exists to make some intelligent decisions. By checking the connection characteristics of the client, the distributed object system can select an object view that is tailored for supporting specific clients. This also means that clients with unique communications requirements could conceivably coexist in the same game environment, sharing the game objects with clients that have completely different communications requirements. This could, for example, allow a client on a handheld device to share the game world with clients connected via a PC or game console.
Multiple object views,movement,and predictive contracts.The game objects in the preceding examples had only one object view bound to them. This was strictly for simplicity. In fact, the ability to divide the distribution responsibilities for an object’s components into multiple object views is a powerful feature. One use of multiple object views is for prioritizing gameobject state related to movement. Because of its visual importance, movementrelated object state is usually distributed at a higher priority than any other object state. When the Distributed-object system creates or selects the object views for a game object, it utilizes a priority associated with each view to determine the initial order in which the object view set will be processed. By giving movementrelated components their own object view and giving movementrelated object views highest priority, movementrelated information for all of the objects in the relevant set can be distributed first.
Object views are also natural places to handle prediction. In addition, managing movement prediction in the object view makes it possible to utilize different predictive contracts for clients with differing connection characteristics. For example, you could utilize a prediction technique for a client on a modem connection that was completely different from one with a broadband connection simply by selecting the appropriate type of object view when one needs to be created.
Name that tuning. Previously I mentioned that one of the reasons that we want our Distributed-object system to manage distribution of our state data was because of our need to design a system that would let us easily specify how we want our game objects to be distributed. Applying distribution attributes to the data is necessary if we are to help tune how object state is distributed at run time. Tuning is a critical responsibility that is shared between the relevantset mechanism and the Distributed-object system.
To try to maintain a steady flow of traffic through the network, a measured allotment of bandwidth is calculated for each cycle. If a cycle exceeds its allotment, that affects the bandwidth allotment for the next cycle. When allotments are exceeded, the relevantset mechanism must trim the set of objects to those that it determines are the most urgent to distribute. If the relevantset mechanism undercompensates (that is, provides an excess of objects to distribute) for the available bandwidth on that cycle, the tuning support mechanisms of the Distributed-object system and object views come into play. This also holds true when bandwidth is being underutilized. In this way, the two systems work together continuously to make optimal use of bandwidth.
The ability to tune how an object’s state is distributed at run time is very important. By providing some specific information about how we want each game object to be distributed, we should be able to tune the system for optimal distribution. Here are some useful attributes that an object view can use for tuning how individual components, or groups of components, are distributed:
A distribution priority can be set for each component item to designate
which items are more important to distribute. When an object view is
faced with needing to reduce the amount of bandwidth being consumed,
it can select from the highestpriority items. As long as the object
remains in the relevant set, lowerpriority items will eventually be
distributed during later cycles.
ability to specify whether or not a component item’s state should be
distributed reliably (guaranteed) or if it can be distributed unreliably
(not guaranteed), is a significant tuning option. When eligible for
distribution, unreliable items will only need to be sent once. Delivery
of the state update is never confirmed, so item state will not be present
in case of a delivery failure. This attribute can have a great impact
on over all bandwidth utilization in times of significant packet loss,
but it must be used carefully. It is typically used for items that change
very frequently and when a missed update has minimal impact. An object
view could also choose to set the reliability attribute conditionally
at run time.
Some component items will need to be distributed as a unit with others.
The group attribute specifies that on a given cycle, unless all the
member items of the group can be distributed, none should be distributed.
For object views that support bidirectional state updates, the direction
attribute can ensure that an object view only works in one direction.
For example, the object view for a player object might need to be bidirectional
when it connects to the client represented by that object, but unidirectional
when distributing state belonging to a “foreign” player representing
a different client. This can also be a security consideration on a server,
preventing hacked clients from using bidirectional object views illegitimately.
- Initialization only. Components such as object IDs that will not change during the lifetime of the object can be tagged with the initializationonly attribute. After the initial distribution, these items will not need to be tracked by the object view, resulting in greater processing efficiency. You should also provide a declarative means of assigning attributes to the distributed components of the game object.
Ideally, this is part of the definition of the game object itself. A custom scripting language capable of defining game objects can build distributionattribute assignments directly into the language itself. UnrealScript, for example, provides a replication statement, where deliveryrelated attributes can be specified for individual items of the class. In our own implementation, these attributes are assigned when the game object is defined using an internal compilation tool that generates source code for both the game object and its object views. Where dragons dwell.Until a reliable transport protocol with predictable delivery is available over the Internet, simulations with timecritical delivery requirements will continue to use unreliable protocols, such as User Datagram Protocol (UDP).
Many complications can arise when object state is distributed using unreliable communications. Ideally, we want to use our limited bandwidth for transmitting only the most recent state of our game objects. Retransmission, due to packet loss, of old packets containing old state is a very poor way to solve the problem. Here too, object views have proved to be a very useful tool. In addition to their componenttracking responsibilities, they can also keep track of the success — or failure — of the delivery of state information to their remote counterparts. How do they do this? I’ll leave the answer as an exercise for a rainyday.
The View,the Proud ...
In this article, I’ve discussed how object views can be utilized as part of a distributed-object system to help encapsulate management of the distribution of object state. We also looked at how they can be used in the implementation of a Distributed-object system. At Monolith, we have found object views to be a very valuable tool in the implementation of our own Distributed-object system. Object views have provided us with an extraordinary amount of flexibility, allowing us to create simpleyet elegant solutions to a variety of the problems we needed to solve.