Skip to content

5. Creating a fully destructible networked environment

Josip Petanjek edited this page Apr 26, 2023 · 1 revision

Not fully translated.

Introduction

This article is an extension of the previous article on advance networked features, it should be noted that nearly all objects depend or will in future iterations depend on the stable physics algorithm. We will work on a deformable terrain, destructible objects and buildable objects.

  • Deformable terrain and destructible objects:

  • Buildable objects:

  • Ballistics

Terrain deformation

Terrain deformation is a key feature for achieving a dynamic world/sandbox, giving the impression that the environment can react to anything that happens to it. In most classic networked games the terrain remains static, not even the strongest weapon can deform it.

The idea is that when we hit a projectile, we simply deform the terrain around the place of the hit, in proportion to the damage it would do, no complex voxels or minecraft esque scale. It should be mentioned that it was performed several times in networked games that made a budget for it, primarily the Battlefield series and the ArmA series of games.

Technology and algorithm

Procedural mesh technology will be used to perform terrain deformation [97], a set of vertices and edges that can be modified during execution. Furthermore, an algorithm is used to modify them, based on the algorithm defined by the user "CodingWithRus" [98], and this chapter deals more with the problem of networking.

The algorithm starts with the detection of the event - receiving the damage, we record the location and amount of damage and start the modification of the shape with respect to these parameters. The shape modification is performed by passing through each vertex of the terrain, and if its distance from the point of impact is within the radius (defined as a terrain variable that actually represents its density) then we apply force over it. The force is calculated as the ratio of the deformation force (how much to multiply the damage over the terrain) and the distance from the place of impact. This force is applied in the Z-axis direction - downwards, and is smoothed with the "Smoothing" variable for less jagged edges. As we go through each vertex, the changes are saved, thus modifying the appearance of the terrain and replication occurs to clients (via the "On Rep Mod Verts" method), who apply the same event/changes only to their local terrain, resulting in synchronisation.

Figure 62 On Rep Mod Verts method performed on the client side Figure 62 On Rep Mod Verts method performed on the client side

It should be noted that the mode of receiving radial damage at the UE4 level has been modified. Therefore, in the existing missile system, when hit, the damage is inflicted in the center of the building, which is not enough for our needs, because we would like the terrain to receive damage exactly at those points where it was hit. Therefore, a system has been implemented that calculates the exact location of the hit on the object, ie finds the point on the figure that is closest to the point of the hit. This was also recommended by UE4 in the comments of the character class itself (Actor.cpp), and was performed:

Figure 63 Actor.cpp modified for terrain deformation Figure 63 Actor.cpp modified for terrain deformation

We generally work with terrain that consists of 100 peaks and covers 100m square, but it is also possible to derive terrain from any static shape (eng. Static mesh).

First iteration - replication of input / event based replication

The first attempt consisted of simulation on both the server and the users. So, if we have the same initial conditions, and we give the same inputs, in the same order, we are guaranteed the same result.

All of the above is true, but we come to an interesting problem, in order to preserve the order of operations, we must put them in an array and perform all those operations that were not performed on our client. This is effectively the same as the deterministic approach, but with a potentially much larger input set.

And here we see why you often cannot rejoin a deterministic simulation while it is in progress - the amount of inputs made constitutes a huge packet with inputs that need to be replayed. The problem is that you can't send just one change in a field, but the whole field.

Serialisation in this case seems like a good option. In short serialisation ensures that all packages will arrive. So, we effectively want to send only simulation inputs, not a whole string - sending one vector and one floating point number is certainly less than sending a growing list of vectors, which would eventually result in sending larger and larger packets. But one should be careful, because while serialisation guarantees that all calls will be made, it does not guarantee the sequence of the list, therefore, it is possible that synchronisation will collapse because the initial conditions (vector positions) will be different, resulting in different results [99]. Alternatively, it is possible to track a sequence on the user side via an identifier, but it should be noted that the list of events is potentially infinitely large.

If we disregard the join in progress problem, it is important to notice that this type of system - event based replication - suffers from a very serious flaw, the fact that it relies on repeated reliable broadcasts. This means that these packets must be sent and received before a particular replication channel can move forward with operations. This can cause huge strains on the network since these packets naturally cannot be bandwidth limited and represent a fixed cost to our bandwidth, rising the minimum requirements for a connection.

Figure 64 Event storage structure Figure 64 Event storage structure

Figure 65 Structure processing Figure 65 Structure processing

Second implementation - replication of output / effect based replication - serialised vector field

The solution is therefore the following. The simulation is performed only on the server and the result (ie all 100 vectors) is saved in the replicated vector field, this field is automatically serialised because the vectors are the standard structure in UE4, so only the changes needed for each user will be sent separately. os, because only it changes).

The key to the solution is that in this case the order is not important, because eventually the result will be the same - the synchronized field of the vector. The cost of this solution is higher, since each change affects approximately 8 vectors, we arrive at a relatively high cost, effectively equal to the cost of tracking the movements of 3 players in a single tick. But since we guarantee simulation synchronisation, and this is certainly a lower price than the first attempt, the result is satisfactory.

Figure 66 Deformed terrain in the game Figure 66 Deformed terrain in the game

The result is visible in the following video [100].

To detect the receipt of damage, we use events that cover both types of damage, explosion and damage based on a single point.

Figure 67 Damage events Figure 67 Damage events

In order to be able to process the damage, we must first determine where it occurred in the terrain, that is, turn the coordinates of the world into coordinates in the terrain, we do this through the inverse transformation of the location.

Figure 68 Inverse location transformation Figure 68 Inverse location transformation

For all replicated vertices, we find their distance from that location and check that it is within the radius we allowed.

Figure 69 Passing through all vertices Figure 69 Passing through all vertices

Furthermore, we perform the deformation of the terrain according to a very simple formula that was stated earlier.

Figure 70 Terrain deformation Figure 70 Terrain deformation

Finally, we are updating the local vertices.

Figure 71 Updating vertices Figure 71 Updating vertices

Future iteration - a merging of two techniques

The core issue we have is high cost of join in progress for the event based technique and a high cost of event based replication for the effect based technique. We could circumvent this by using events when the game is in progress, and use state based replication when joining in progress. This could effectively be done by using conditional replication, specifically "COND_InitialOnly" that mandates a replicated property be only replicated with the initial packet. In this theoretical model the limitation on the repeated reliable transmission still remains, and should be tackled by combining similar inputs in to one as well as possibly limiting the amount of inputs acceptable per tick (biggest impact effects first logic). This technique was not executed because of time constraints and complexity of execution.

The limitation of this implementation comes to the fore in its size, so if you would like to cover the whole world with this terrain, then we have two options: Increase the size of the terrain to cover the world Put N instances of terrain in the world

Increasing the terrain to cover the whole world is not a practical option for several reasons. Originally the large terrain should be refreshed whenever it is modified, if it is a 64 km square terrain, which is the standard of large worlds, we come to an unacceptably large operation, while this could be mitigated by using sections, there is a bigger problem; Keep in mind that each object is replicated from the server to the client given its distance from that object, more precisely the center of that object (remember the spherical distance), so a large field would almost never be close enough to refresh, and so not would perform its function, and if it were close enough, the amount of data to be sent to the client would be unacceptably large:

64km * 1000 peaks = 64000 floating point numbers

So the only sensible solution is to cover the world with N instances of the terrain. The problem they would then encounter is that the neighbouring terrains should be modified together so that the marginal peaks do not "crack" and create a hole in the world. One of the less obvious limitations of this solution is that tunnels would not be possible unless specific restrictions are made over the terrain.

It would be advantageous for designers to implement further constraints such as the ability to define terrain peaks that cannot be moved, or can only be moved to a certain value on the Z axis or angle to other peaks, etc., to ensure some sensibility for world design during its destruction. Furthermore, as UE5 moves to a deterministic approach, they could return to the first iteration but should address the problem of periodic state saving to ensure durability.

Finally, it should be mentioned that in the world of procedural forms, specifically for UE4, many innovations and improvements have recently appeared [101], and the possibilities for improvement are wide, and could be applied to destructive objects. Similar to the performance in Rainbow 6 Siege, where each bullet dynamically modifies the shape [102].

Buildable objects

Building facilities provides users with a source of creativity and freedom in shaping the world. It allows players to build objects with a variety of functionalities that can help them while playing, from creating shelter to respawn.

We want the construction to take place in a separate view, where the user is first shown an overview of what the object will look like when it is built, with the ability to place them multiple in a single row as a repeatable pattern, similar to Company Of Heroes. When they are installed, we want the possibility of their construction and demolition.

The first iteration - static objects

In order to accomplish all of the above in the specification, we will create several classes to manage various aspects of this functionality.

In the "Buildable" class, we will define everything that makes up an object that can be built. We need a form that represents the construction overview, a health component for the building that will receive construction and demolition data, and forms for the various phases of construction.

Figure 72 Buildable.cpp class constructor Figure 72 Buildable.cpp class constructor

So in the class constructor we define all components and shapes, it should be noted that all shapes are hidden via "SetVisibility", and all collisions are excluded.

In a collision, we come to an interesting problem, because it does not replicate because it is designed for the needs of UE4 [103], and we need to change it during gameplay, because it needs to be turned off for hidden phases, and turned on when they occur. We solve this by replicating the boolean list, one boolean for each phase, and using the "On_Rep" function which is activated when replication occurs. In this function - "OnRep_Collision", the collision profile is set depending on the set values.

Figure 73 Setting replicated collision variables Figure 73 Setting replicated collision variables

Figure 74 Function activated by collision replication Figure 74 Function activated by collision replication

We can now report the default functionalities. To place the object, we use the "Place" function, which changes the visibility and collision profile of the object, and we set the initial health, defined as 10% of total health.

Figure 75 Object placement function Figure 75 Object placement function

It should be noted that the function of the health component has been modified because in its initial implementation, during construction, full health was immediately given. We corrected this by setting a new variable "startHealth" which defines the initial health (0).

Figure 76 Health components Figure 76 Health components

Now all we have to do is process the cases of construction and receiving damages. Construction takes place in the "Build" function where only health is specified and collision profiles and phase visibility are updated.

Figure 77 Updating collision profiles Figure 77 Updating collision profiles

The case of receiving damage is actually identical to that in the character, and the implementation is very similar.

Figure 78 Damage functions Figure 78 Damage functions

In order to be able to use this facility it is necessary to create a component for the client that will organize the construction view and call the functionality of the facility. We will report this using the "BuildManagerComponent" class, its work is done in its beat, first waiting for the construction view to turn on, with the "O" key, after which the user is shown an overview of the building object, the so-called "ToggleBuildMode" project settings. By pressing the middle mouse button, the user confirms the initial position of the building to be built (redefined in the project settings), this calls the "RequestBuild" function.

Figure 79 Project settings Figure 79 Project settings

Figure 80 Function for starting the construction inspection Figure 80 Function for starting the construction inspection

Figure 81 Construction request function Figure 81 Construction request function

In order for it to be displayed, the user's camera, and the point he is looking at, are first supplied via the line trace method. Once we have defined the position, we can create an object, remember, we only do this locally, on the user side, not the server side. It should be notedFigure 82 Finding a user view

Figure 82 Finding the users view Figure 82 Finding the users view

Figure 83 Overview of the constructed object Figure 83 Overview of the constructed object

In case the position the user is looking at moves (while holding the middle mouse button), then more objects are created between the start and current point, so that there is no empty space between the objects. It should be noted that objects can only be placed at a limited distance from the character.

Figure  84 Function for placing multiple objects in a row Figure 84 Function for placing multiple objects in a row

Figure  85 Placing multiple objects in a row Figure 85 Placing multiple objects in a row

In order to avoid objects that break through other objects, we will again use a line trace, which starts from the height of the building object, and is drawn towards the bottom (Z axis).

Figure 86 Penetration case processing Figure 86 Penetration case processing

Figure  87 Penetration processing function using line traces Figure 87 Penetration processing function using line traces

When we are satisfied with the placement of objects, we release the middle mouse button, which activates the function "ReleaseBuild", this function asks for a manager of objects that will create them on the server side, it does it through the function "Server_PlaceBuildable".

Figure 88 Function for placing an object on the server side Figure 88 Function for placing an object on the server side

This is actually a request to create these objects so that they can be displayed to all other clients.

Figure 89 Implementation of the function for placing the object on the server side Figure 89 Implementation of the function for placing the object on the server side

This brings us to the "BuildableManager" class, which manages these objects and takes care of their creation on the server side, this is done through the "SpawnRequest" function.

Figure 90 Function for creating objects in the creator Figure 90 Function for creating objects in the creator

Figure 91 Placed objects in the game Figure 91 Placed objects in the game

After all, once we have set up all the facilities, we need to build them, for that we need a special weapon that will perform that function. This is done using the "MyInstantWeapon" class, which inherits the "Weapon" class for similar functionality. The difference is of course that these weapons do not cause damage, but build objects if they are hit, and of course if they can be built.

Figure 92 Modified weapons for building objects
Figure 92 Modified weapons for building objects

Figure 93 Constructed object
Figure 93 Constructed object

Future iterations - physical objects

The disadvantage of the previous iteration is that we have actually created static objects, which do not react to physical events in the world. The current implementation was actually one of the first features to be implemented, and that is why it was not performed so initially.

So a future iteration should create a physical object, as we defined in the object physics simulation network, which would optionally be locked to the Z axis for consistency of shelter, but it should be noted that axis locking can disrupt the behavior of other physical objects.

Finally, depending on the design of the game’s functionality, it would be necessary to create a system of resource consumption to build, which would limit users from creating countless objects.

Destructible objects

Destructible facilities occupy the other side of building construction, but have almost the same effect on the end user. So it provides a deep opportunity for tactical creativity and freedom to move around the world.

First iteration - PhysX APEX + DENT

PhysX APEX is the current system of destructible objects, fully supported in UE4 [104], and will therefore be used in this project.

It takes any object and turns it into a set of debris, which are connected through a simple hierarchy and connection graph, which actually defines how the object can be destroyed.

Figure 94 Object converted into a set of debris Figure 94 Object converted into a set of debris

Figure 95 Hierarchy and connection graph [105] Figure 95 Hierarchy and connection graph [105]

Due to the simpler performance of this part of the work, the DENT connector, available on the UE market [106], is used, it simply replicates the information about the fracture of an individual debris.

The fundamental problem with APEX is that it is actually a separate and mostly closed API over which we have no control, and it is difficult to configure because it is written in a completely different language and environment. The consequence of this design is that destruction is very limited and difficult to configure for networked needs.

The crux of all networked problems actually stems from a lack of control over the aforementioned debris. Namely, we cannot track debris as ordinary objects, and therefore we cannot track their position, ie we cannot replicate their state on the server to clients, so there is a loss of position synchronization. Notice the different positions of the wreckage of the destroyed building in the following figure.

Figure 96 Different debris positions on clients Figure 96 Different debris positions on clients

Next, we come to the concept of Large Chunk Treshold, it defines that the debris below the threshold has a collision, while the debris above it has no collision. Because of these limitations, we are forced into a very small number of possible configurations of destructible objects.

So the recommended configuration is as follows:

  • Do not simulate debris after it separates from the main object, because then their position is unknown.
  • All debris should be connected directly to the support chunk, so that it is never possible for two debris to remain connected but separated from the main object, as the first case would occur.

One such destructible object is available in the "Content / TakeoverPrototype / DestructibleEnvironment / StaticMeshPhysics / BlockoutWallTest" path. The following figure shows the configuration.

Figure 97 Properly configured destructible object
Figure 97 Properly configured destructible object

Further in DENT is configured as shown in the following figure.

Figure 98 Destructible object configuration
Figure 98 Destructible object configuration

In order to use this configuration within a larger object, it is necessary to configure the collision, using collision filtering [107], so that no collisions between physical bodies occur, the configuration is available in the path "Content / TakeoverPrototype / DestructibleEnvironment / StaticMeshPhysics / MyStaticMeshPhysics" "Struct DestructibleWallFinal", and visible in the following figure.

Figure 99 Conflict configuration
Figure 99 Conflict configuration

It should be noted that the root to which these walls are attached is of the collision type "Destructible". This root is needed because we want destructible objects to be part of a dynamic world, that is, to be compatible with terrain deformation and the like. The result is visible in the following video[108].

Figure 100 Synchronized destructible object [108]
Figure 100 Synchronized destructible object [108]

It is possible to configure a destructible object with more debris, but then it should be completely destroyed when it receives damage, this is beneficial for smaller objects. It should be noted that there is a tool called PhysXLab [109] with which it is possible to manually shape the debris so that the above problems never occur, but for the purposes of this paper the default configuration is satisfactory.

Current generation - PhysX Blast

PhysX Blast is the current generation of destructible objects, it provides better performance than APEX and has slightly better networking control [105].

This feature is available as an add-on for UE4 on the Nvidia GameWorks platform [110]. But it is not addressed in this paper, because the fundamental problems have not been solved, it is still a locked platform that we cannot modify for our own needs, and therefore no time will be spent on its implementation.

Future iteration - Chaos destruction

Looking to the future, in conjunction with the Chaos physical system, we also get an embedded system for destructible objects. This solution is fully integrated in the UE [111], in the sense that it is open type, so anyone can modify it for their needs, furthermore it can be fully controlled in the UE, without external tools.

For us, the most important fact is that this solution is suitable for use in networked environments, so we are not limited to just some configurations.

Of course, this means that debris can be tracked even after separation from the building. Furthermore, the debris is integrated into other aspects of the game such as:

  • AI navigation [112]
  • Effect system
  • Sound system
  • Generating various events to which the environment can react, for example when a debris hits the floor, a character reaction can be generated and the like.
  • Sustainability support (monitoring system)
  • Cached Simulation where extremely complex simulations can be performed partially before they are performed.

Figure 101 Basic concepts of "Chaos" [112] Figure 101 Basic concepts of "Chaos" [112]

In terms of mode, it doesn't differ much from PhysX, and the connectivity graph is used again, its real advantage lies in the deep integration.

Simulating projectiles and balistics

The projectile and ballistics simulation system is critical, not only in terms of game realism, but also in terms of game safety, and simulation details, it is used in simulating bullets and piercing objects.

Projectiles

We can implement projectiles in two ways, as a physical object and a Line Trace, which we will now compare.

A projectile as a physical object is an ordinary physical object that is given speed when exiting a weapon, and can interact with the environment.

Figure 102 Projectile as a physical object in play
Figure 102 Projectile as a physical object in play

A line trace is another method by which projectiles can be implemented, it is a simple line that is drawn from one point to another, and it is examined with which all objects on that path collide.

Figure 103 Line trace in the game
Figure 103 Line trace in the game

It is obvious that each bullet receives various influences, such as air friction, gravity and wind and the like, and it takes some time to reach its target. All of the above a physical projectile can handle with its functionality because it is part of a physical system. In this sense, the line trace is precise in short stages, and each weapon has an almost infinite range, without any influence of gravity, and without specifying the target, because each projectile immediately reaches its target.

On the security side of the game, physical missiles are very safe, because they can be fired from the server side and therefore be almost impossible to exploit. The line trail depends on the user side, so if the user does not see the target, he cannot hit it because it determines the beginning and end of the path that the projectile passes, this also opens various security problems for cheating players. It is possible to fire a line trace on the server side, but then we rely on the fact that the server also sees everything, which is not always the case. In addition, if it is on the server side then there may be a greater response when firing and hitting, which nullifies all the advantages of this method.

Due to all these facts, the physical simulation of the projectile was chosen. The already existing simulation of the projectile in the project is satisfactory for the needs of the project, and only the behavior of the hits, ie ballistics, has been modified.

Ballistics

The goal of a ballistics system is to simulate the penetration and damage that a missile strike would cause to an object. Its application is wide, from piercing walls to tank armor and the like. It generally raises the level of detail and realism of the game and if applied widely enough and implemented with a destruction system can be great. This system was mostly inspired by the game "War Thunder" [113].

The vast majority of available designs rely on a line track, not on physically simulated objects, which far facilitates performance, but is clearly not conducive to our design.

There is one publicly available implementation that uses physically simulated objects, the "Mipmap" implementation [114]. In short, in it, the author relies on a collision bounce event (when a physical object bounces off another), and then decides whether the projectile will pierce the target or not. The disadvantage of this method is that after piercing the projectile must be wiped, and re-created on the other side of the pierced object, which is very detrimental to the networking performance. Let's remember that the creation of objects must be on the server side, and for that it is necessary to send position data, with speed and similar attributes. This would be disastrous for detailed environments, where almost any object can be breached.

Because of this, a new method has been devised, which relies on the collision overlap event. When a collision occurs we do not stop the projectile, i.e. we have ruled out any physical blocking of the projectile, but we can still monitor this through the overlap event.

Figure 104 Collision component of a bullet
Figure 104 Collision component of a bullet

In this way we do not send any data on the creation of a new missile, its position data is sent depending on its network settings.

So, when overlapping a projectile with an object, an overlap event is activated on the projectile, then we save the overlap data, it should be noted that further code is executed only on the server side.

Figure 105 Overlap event Figure 105 Overlap event

Here we need to look at some of the points we will need for the various calculations.

Figure 106 Points needed for calculations, own production
Figure 106 Points needed for calculations, own production

  • A is the Impact Point, it indicates the point that the projectile initially hit.

  • B is the point of maximum penetration, ie if it can reach it, we consider that it has penetrated the object.

  • C is the point on the back of the object we are piercing, it is used to calculate the speed after piercing.

Since we immediately know point A when overlapping, it is now necessary to find point B by calculating the maximum penetration. This is done in a separate function for code purity. This method is based on the simplified Pizard method for medium-velocity projectiles [115]. Here we take into account the area of ​​the bullet tip, the speed of the bullet, its weight and the material of the object we are piercing (for now, all objects are made of steel). It is possible to improve this with more precise formulas [116] but for the purposes of this project this is satisfactory.

Figure 107 Calculation of maximum penetration Figure 107 Calculation of maximum penetration

We can now calculate the point of maximum penetration, using the direction of movement of the bullet, which we multiply by the value obtained and add to point A.

Figure  108 Calculation of the maximum penetration point
Figure 108 Calculation of the maximum penetration point

To find point C, we will use a multi-line trace, which will return the field of hits of the objects with which it collided between B and A. When we find our object in that field, we examine whether the location of the hit is the same as the initial location. point B. This is done because the collision inside the object is activated immediately, which means that we did not break through the object. It should be mentioned that there may be a case where there is a gap between A and B, which the projectile could pierce, but due to the design of the system it is considered impossible, this can be avoided by taking only the last collision with the shape.

Figure 109 Finding a point on the back of an object
Figure 109 Finding a point on the back of an object

In case we did not break through the object, it should be damaged, the client should be notified and the projectile should be destroyed.

Figure  110 Case of non-penetration of the object
Figure 110 Case of non-penetration of the object

When notifying the client, we create visual effects and sound. This creates much less information than the aforementioned projectile creation method.

Figure 111 Creating effects on the client side Figure 111 Creating effects on the client side

In case we pierced the object, we have to reduce the projectile's speed, we do it according to the ratio of maximum penetration with the distance of points A and C. The ratio is multiplied by the speed, and if it is equal to zero, we start destroying the projectile again.

Figure  112 Case of object penetration
Figure 112 Case of object penetration

If the projectile continued outside the facility, then we must inflict damage on it and notify the client.

Figure 113 Continuation of the projectile
Figure 113 Continuation of the projectile

When informing the client, we create a visual effect and sound again, a red dot is created to help with the visualization, but it should be mentioned that it is on the client's side, and can be inaccurate. The accuracy of the system can be read from the log on the screen.

Figure 114 Penetration effects on the user side
Figure 114 Penetration effects on the user side