Real-World Engines - Multiplayer Game Programming: Architecting Networked Games (2016)

Multiplayer Game Programming: Architecting Networked Games (2016)

Chapter 11. Real-World Engines

While larger game studios still largely develop their own internal game engines, for smaller studios it is increasingly common to utilize an off-the-shelf engine. For most genres of networked games, it can be much more time- and cost-effective for a smaller studio to utilize an existing engine. In this case, the code the network engineer will write is at a much higher-level than the majority of this book.

This chapter takes a look at two very popular engines used in many games today, Unreal 4 and Unity, and investigates how networked multiplayer functionality can be implemented in both of these engines.

Unreal Engine 4

The Unreal Engine has existed in one form or another since the 1998 release of the video game Unreal. However, over the years the engine has changed in many different ways. This section specifically discusses Unreal Engine 4, which was released in 2014. For the remainder of this chapter, “Unreal” will be used in reference to the engine, not the video game that shares its name. A developer using Unreal generally does not have to worry about lower-level networking details. Instead, the developer is concerned with higher-level gameplay code and making sure that it works correctly in a networked environment. This is analogous to the game simulation layer of the Tribes networking model.

Because of this, the majority of this section looks at the higher-level aspects of adding networking to an Unreal Engine game. However, in the interest of completeness, it’s worthwhile to look at the lower-level details and how they correspond to many of the topics covered in Chapters 1 to 10. A reader more interested in the lower-level aspects of networking in Unreal Engine can also create a developer account for free at www.unrealengine.com in order to gain full access to the source code.

Sockets and Basic Networking

In order to provide support for a large number of platforms, it is necessary for Unreal to abstract the implementation details of the underlying socket implementation. An interface class called ISocketSubsystem has implementations for the different platforms that Unreal supports. This is in some ways analogous to the Berkeley Sockets code presented in Chapter 3. Recall that there are slight differences between the socket API on Windows versus Mac or Linux, so the socket subsystem in Unreal needs to take this into account.

The socket subsystem is responsible for creating sockets as well as addresses. The Create function of the socket subsystem returns a pointer to the created FSocket class, which can then have data sent and received using standard functions with names such as Send, Recv, and so on. Unlike the code implemented in Chapter 3, TCP and UDP socket functionality is not provided in separate classes.

Similarly, there is a UNetDriver class that is responsible for receiving, filtering, processing, and sending packets. This can be thought as similar to the NetworkManager class implemented in Chapter 6, though it is a bit lower level. As is the case with the socket subsystem, there are different implementations based on the underlying transport whether it is IP or a gamer service transport such as that used by Steam which is covered in Chapter 12, “Gamer Services.”

There is quite a bit of other lower-level code related to transmitting messages. There is a large set of classes related to transport-agnostic messaging. The details of this are fairly complex, so if you are interested, you should consult the Unreal documentation on this particular feature athttps://docs.unrealengine.com/latest/INT/API/Runtime/Messaging/index.html.

Game Objects and Topology

Unreal uses some fairly specific terms to reference the key gameplay classes in the engine, so before diving in deeper it’s worthwhile to discuss this terminology. An Actor is more or less the base game object class. Every object that exists in the game world, whether static or dynamic, visible or not, is a subclass of Actor. One important subclass of Actor is Pawn, which is an Actor that can be controlled by something. Specifically, this means that Pawn has a member pointing to an instance of a Controller class. Controller also is a subclass of Actor, which is due to the fact that a Controller is still a game object that needs to be updated. A Controller could be a PlayerController or an AIController, among other things, depending on what is controlling the Pawn in question. A very small subset of the Unreal class hierarchy is illustrated in Figure 11.1.

Image

Figure 11.1 Highlights of the Unreal class hierarchy

To solidify how all these classes work together, consider a simple example of a single-player dodgeball game. Suppose a player presses the spacebar to throw a dodgeball. The spacebar input event might be passed to a PlayerController. The PlayerController will then notify thePlayerPawn that it should throw a dodgeball. This will cause the PlayerPawn to spawn a DodgeBall, which is a subclass of Actor. Although there is more happening behind the scenes in the engine, this should provide a basic understanding of how these key classes interact with each other.

For networked games, Unreal only supports the client-server model. There are two different modes the server can run in: dedicated server and listen server. In a dedicated server, the server runs as a process separate from any and all clients. Usually, a dedicated server is run on a separate machine entirely, though that is not a requirement. In the listen server mode, one of the game instances is both the server and one of the clients. There are some subtle differences between games running in dedicated server mode as opposed to listen server, but that is beyond the scope of this section.

Actor Replication

Given that Unreal uses a client-server model, it follows that there needs to be a way for the server to transmit updates on actors to all of the clients. This is appropriately referred to as actor replication. Unreal does a few different things to try to reduce the number of actors that need to be replicated at any one time. As with the Tribes model, Unreal tries to determine the set of actors that are relevant to any one client. Furthermore, if there is an actor that will only ever be relevant to one particular client, it is possible to spawn the actor on that client, rather than on the server. An example where this second approach might be utilized is for an actor that is a wrapper for a temporary particle effect. It is also possible to further tweak the relevancy of an actor with a few different flags. For example, bAlwaysRelevant will greatly increase the likelihood an actor will be relevant (though contrary to name of the variable, it does not actually guarantee the actor will always be relevant).

Relevancy leads to the next important concept of roles. In a networked multiplayer game, there will be several separate instances of the game running at once. Each of these instances can query the role for each actor in order to determine who has the authority over the actor. It’s important to understand that the role for a particular actor can be different depending on the game instance which is querying the role. If we return to the dodgeball example, in a networked multiplayer version of dodgeball, the ball would be spawned on the server. Thus, if the server asked about the role of the dodgeball, it would see that it has role “authority” meaning the server is the final authority for the dodgeball actor. However, every other client would see a role of “simulated proxy,” meaning that they are simply simulating the ball and are not the authority of the ball’s behavior. The three roles are as follows:

Image Authority. The game instance is the authority for the actor.

Image Simulated proxy. When on a client, this means that the server is the authority for the actor. A simulated proxy means that the client may simulate some aspects of the actor, such as movement.

Image Autonomous proxy. An autonomous proxy is very similar to a simulated proxy, though it implies that it is a proxy that is receiving input events directly from the current game instance, so the player’s input should be taken into account when the proxy is simulated.

This does not mean that in a multiplayer game the server is always the authority for every actor. In the case of the local particle effect actor, it may make sense for the client to spawn the actor, in which case the client would see role “authority” and the server would not even know the particle effect actor existed.

However, every actor that the server has role “authority” on will be replicated to all clients, when relevant. Inside of these actors, it is possible to specify which properties should or should not replicate. In this way, bandwidth can be conserved by only replicating properties that are critical to properly simulating the actor. Actor replication in Unreal is only ever from the server to the client—there is no way for the client to create an actor and then replicate it to the server (or other clients).

It is also possible for more advanced replication configuration beyond just copying properties. For example, it is possible to only replicate a property based on particular conditions. It is also possible to have a custom function be executed on the client whenever a particular property is replicated from the server. As gameplay code in Unreal Engine 4 is written in C++, the engine uses a complex set of macros to track all of the different replication properties. So when adding a variable in a class’ header file, you can also tag the variable with appropriate replication information via the macros. Unreal also has a fairly powerful flowchart-based scripting system called Blueprint—surprisingly, much of the multiplayer functionality is also accessible via this scripting system.

Conveniently, Unreal already implements client prediction for actor movement. Specifically, if the bReplicateMovement flag is set on an actor, it will replicate and predict movement of simulated proxies based on replicated velocity information. If necessary, it is also possible to override the method by which client prediction is implemented for character movement. However, the default implementation is a good starting point for most games.

Remote Procedure Calls

As in discussed in Chapter 5, “Object Replication,” remote procedure calls are instrumental in making replication work. So it should not be a surprise that Unreal has a fairly powerful system for remote procedure calls. There are three types of RPCs in Unreal: server, client, and multicast.

A server function is a function that is called on a client, and executed on the server, with one big caveat: The server does not let any client call a server RPC on any actor in the world. This would too easily lead to potential cheating, among other issues. Instead, only the client that is theowner of the actor can successfully execute a server RPC on the actor. Note that the owner is not the same thing as the game instance that is role authority. Rather, the owner is the PlayerController that is associated with the actor in question. For example, if PlayerController A controls PlayerPawn A, then the client that is driving PlayerController A is considered the owner of PlayerPawn A. If we return to the dodgeball game example, this means that only Client A can call the ThrowDodgeBall server RPC on PlayerPawn A—any calls toThrowDodgeBall that Client A might try to invoke on any other PlayerPawn would be ignored.

A client function is the inverse of a server function. When the server calls a client function, the procedure call is sent to the client who is the owner of the actor in question. For example, when the server determines in the dodgeball game that player C is eliminated, it might invoke a clientfunction on player C so that the owning client of player C can display the “Eliminated!” message on screen.

As the name implies, a multicast function will be sent to multiple game instances. In particular, a multicast function is a function that is called on the server, but executed on the server and all of the clients. Multicast functions are used to notify every client about a particular event—for example, a multicast function might be used when the server wants every client to locally spawn a particle effect actor.

Combined, these three different types of RPCs allow for a great deal of flexibility. It’s also notable that Unreal provides a choice on whether or not an RPC is reliable. This means that low-priority events could have their RPCs marked as unreliable, which could improve performance when packet loss occurs.

Unity

The Unity game engine was first released in 2005. In the last few years, it has become a very popular game engine used by many developers. As with Unreal, the engine provides some synchronization and RPC functionality built-in, though there are some distinct differences from the approach used by Unreal. Unity 5.1 introduced a new networking library called UNET, and as such this section focuses on this newer library. In UNET, there are two different APIs: a higher-level API that can handle most networked game usage cases, as well as a lower-level transport API that can be used for custom communication over the Internet, as required. The majority of this section will focus on the higher-level API.

While the core Unity game engine is largely written in C++, Unity developers are not provided access to this C++ code. Developers using Unity will typically write the bulk of their code in C#, though it is also possible to use a version of JavaScript, as well. Most serious Unity developers will go with the C# option. Programming gameplay logic in C# instead of C++ presents both advantages and disadvantages, though this is irrelevant to the task at hand.

Transport Layer API

The transport layer API provided by UNET is a wrapper for platform-specific sockets. As one might expect, there are functions for creating connections with other hosts, and this can be used to send and receive data. One of the decisions that can be made when creating a connection is the reliability of the connection. Rather than specifically requesting a UDP or TCP connection, you can instead specify how you wish to use the connection. You can create a communication channel and request one of many values from the QosType enum. Possible values include:

Image Unreliable. Send messages without any guarantees.

Image UnreliableSequenced. Messages are not guaranteed to arrive, but out-of-order messages are dropped. This is useful for voice communication.

Image Reliable. The message is guaranteed to arrive as long as the connection is not disconnected.

Image ReliableFragmented. A reliable message that can be fragmented into several packets. This is useful when wanting to transmit large files over the network, as it can be reassembled on the receiving end.

Connections can be established via the NetworkTransport.Connect function call. This will return a connection ID, which can then be used as a parameter for other NetworkTransport functions such as Send, Receive, and Disconnect. On a Receive call, the returned value is a NetworkEventType, which can either encapsulate the data or an event such as a disconnection.

Game Objects and Topology

One big difference from Unreal is the way that game objects are set up in Unity. While Unreal has a relatively monolithic hierarchy when it comes to the game objects and actors, Unity takes a more modular approach. The GameObject class in Unity is largely a container for Componentclasses. All behaviors are delegated to the components that are contained in the GameObject in question. This can allow for a much better delineation between different aspects of a game object’s behavior, though it can sometimes make programming systems more difficult when there are dependencies between multiple components. Normally, a GameObject has one or more components that inherit from MonoBehaviour that drive any custom functionality for that GameObject. So for example, rather than having a PlayerCat class that directly inherits fromGameObject, you would have a PlayerCat component that inherits from MonoBehaviour. Then the PlayerCat component could be attached to any game objects that should behave like a PlayerCat.

In the higher-level networking API, Unity uses a NetworkManager class to encapsulate the state of a networked game. The NetworkManager can run in three different modes: as a standalone client, a standalone (dedicated) server, or a combined “host” that is both a client and a server. This means that Unity essentially supports the same dedicated server or listen server modes that are supported by Unreal.

Spawning Objects and Replication

Because Unity uses a client-server topology, it means that spawning objects in a networked Unity game is very different from spawning them in a single-player game. Specifically, when a game object is spawned on the server via the NetworkServer.Spawn function, it means that this game object will be tracked by the server with a generated network instance ID. Furthermore, a game object spawned in this manner should be replicated and spawned to all of the clients as well. In order for the correct game object to be spawned on the client, you are required to register the correct prefab for the game object. A prefab in Unity can be thought of as a collection of components, data, and scripts that the game object uses—this can include things like the 3D model, sound effects, and behavior scripts used by the game object. By registering the prefab on the client, it ensures that all of the object’s data is ready for use in the event that the server notifies the client to spawn an instance of that game object.

Once an object is spawned on the server, the properties in its behavior can be replicated to the client via a few different methods. In order for this to work, however, the behavior must inherit from NetworkBehaviour instead of the usual MonoBehaviour. Once this is done, the simplest way to replicate variables is to flag each variable you wish to replicate with the [SyncVar] attribute. This will work on built-in types as well as Unity types such as Vector3. Any variables that are marked as SyncVars will automatically have value changes replicated to the clients. There is no need for you to mark the value as dirty. However, keep in mind that while SyncVar can also be used for a user-defined struct, the entire contents of the struct will be copied as one set of data. So if you have a struct with 10 members, but only one member changes, it would transmit all 10 members over the network, which may waste bandwidth.

In the event you require more fine-grained control over how variables replicate, you can override the OnSerialize and OnDeserialize member functions to manually read and write the variables you wish to synchronize. This can allow for customized functionality, but it cannot be combined with SyncVar—so you have to choose one or the other.

Remote Procedure Calls

Unity also has support for remote procedure calls, though the terminology is slightly different than the terms used in this book. In Unity, a command is an action sent from a client to the server, and only works for objects controlled by that player. In contrast, a client RPC function is an action sent from the server to a client. As with SyncVar, these types of RPC functions are only supported in subclasses of NetworkBehaviour.

The system for flagging functions as either type of remote procedure call is fairly similar to synchronizing variables. To flag a function as a command, it should have the [Command] attribute and additionally the function should begin with a prefix Cmd, such as CmdFireWeapon. Similarly, a function can be flagged with the [ClientRpc] attribute and should begin with Rpc in the event that it’s a client RPC. In either case, the function can be called like a standard function call in C# and it will automatically create the network data and execute it remotely.

Matchmaking

The UNET library also provides some matchmaking functionality that is typically associated with a gamer service, a topic covered in much greater detail in Chapter 12, “Gamer Services.” This is in contrast to Unreal, which instead provides wrappers for established gamer services based on the platform in question. The matchmaker in Unity can be used to request and list the current game sessions. Once a suitable session is found, it is then possible to join the game. This functionality can be added to a MonoBehaviour subclass via the NetworkMatch class. This will then trigger callbacks such as OnMatchCreate, OnMatchList, and OnMatchJoined.

Summary

For smaller game development studios, using an off-the-shelf game engine can be a reasonable decision. In such a case, the responsibility of the network engineer is at a higher level than the majority of this book. Rather than worrying about how to implement sockets or basic data serialization, the engineer must know how to allow for game functionality to run on a networked game in their engine of choice.

The Unreal Engine has existed for nearly 20 years. The fourth version of the engine, released in 2014, provides full source code in C++. Although there are platform-specific wrappers for functionality such as sockets and addresses, the expectation is generally that the developer will not directly utilize these classes.

Unreal’s networking model supports a client-server topology, which can either use a dedicated server or a listen server. The Unreal version of a game object, Actor, has a hierarchy that includes many different subclasses. An important aspect of this functionality is the idea of a network role. Authority means the game instance is the authority over an object, whereas simulated and autonomous proxies are used when a client simply is mirroring an object from the server. The Actor class also has built-in support for replication of objects. Some functionality, such as movement, can be replicated by setting a Boolean, while custom parameters can also be marked to replicate. Furthermore, there is support for a variety of remote procedure calls.

Unity has existed since 2005, and over the last few years has become a popular game engine. Developers using Unity generally will write all of their gameplay code in C#. In Unity 5.1, a new network library called UNET was introduced, which provides a great deal of high-level networking functionality, though there is also a low-level transport layer that is available.

The transport layer abstracts the creation of sockets and instead allows the developer to transmit data in several modes including reliable and unreliable, but most games implemented in Unity will likely not directly access this. Instead, most developers will use the higher-level API which, as with Unreal, supports both dedicated server and a listen server. All behaviors that need networking support should inherit from the NetworkBehaviour class. This adds functionality for replication, which can be handled either via the [SyncVar] attribute or custom serialization functions. A similar approach is also utilized for remote procedure calls, both from the server to the client, and the client to the server. Finally, Unity provides some built-in matchmaking functionality that can be used as a lighter-weight option to using a full gamer service.

Review Questions

1. Both Unreal and Unity only provide built-in support for a client-server topology, and not a peer-to-peer topology. Why do you think this is the case?

2. In Unreal, what are the different roles that actors can have in a networked game, and what is their importance?

3. Describe the different usage cases for remote procedure calls in Unreal.

4. Describe how the game object and component model function in Unity. What might be the advantages and disadvantages of such a system?

5. How does Unity implement variable synchronization and remote procedure calls?

Additional Readings

Epic Games. “Networking & Multiplayer.” Unreal Engine. https://docs.unrealengine.com/latest/INT/Gameplay/Networking/. Accessed September 14, 2015.

Unity Technologies. “Multiplayer and Networking.” Unity Manual. http://docs.unity3d.com/Manual/UNet.html. Accessed September 14, 2015.