Deep Dive on the Road Blocks Feature
This is a technical deep dive of the recently released v0.0.29 road blocks feature . v0.0.30 which will be a bug fix and minor enhancement to the road block feature is due out later this month.
Inspiration: I've wanted to do something with Chaos destruction for a long time. Police tactics and mechanics are a big part of Police Chase Simulator. I wanted to create a foundation that I could build on for future enhancements.
Early Learnings: I'd watched a couple videos on the Unreal "Fracture" mode that creates destructible meshes that crumble using the Chaos destruction system. Particularly this Udemy course section was very instructive to get me going and experimenting with prototypes: [Section 14: Breakable Actors] - https://www.udemy.com/course/unreal-engine-5-the-ultimate-game-developer-course/learn/lecture/33116802
This taught me about UGeometryCollectionComponent that is the core of the fracture system (analogous to UStaticMeshComponent for regular meshes) and field systems and forces that influence how the component breaks. I started by following along is this section with my own prototype sandbox and got a feel for how the fields and forces influenced breaking geometry and how to use the fracture mode in Unreal to set up the fracture mesh for simple objects like cubes from the third person template project.
I then took these learnings to Police Chase Simulator. I knew I wanted to create some destructible barricades that police would put on the roads so I started by having a simple rectangular static mesh and fractured it and put some instances of the geometry collection static mesh in a simple map with just a road and a player start. I experimented with smashing into it with the car and how it affected the physics and damage. The vehicles already have a damage component that responds to physics impulses and reduces the vehicle health so out of the box I had something working. I also did some background research on how real concreate barriers work. Turns out they are NOT supposed to crumble into hundreds of tiny pieces, but instead stay intact and redirect the energy of the vehicle to absorb it without causing the vehicle to flip over or for the barriers to penetrate into the vehicle causing harm to the occupants. How Road Barriers Stopped Killing Drivers
However, it was fun smashing into the barricades and it added new depth to the game so I decided to continue on the feature.
I initially estimated getting an MVP of the feature: Simple barricades using free assets that spawn to fill the width of the road on predetermined spawn points as the chase escalates would take 24 hours. Turned out this took over 90 hours to get the 0.0.29 release out and that's not including some follow on tech debt tickets that I broke out from the initial one that has taken over 100 hours total at this point. The main thing I underestimated was getting this system integrated correctly with the police and traffic AI.
The full scope of what I wanted to do was
- Create a spawning volume actor that I could place in a map indicating where destructible barriers could be spawned and make it configurable by specifying one or more assets that would make up the barricade. The barricade should be randomly placed within the volume along the road direction and fill up all the lanes of the road.
- The barriers themselves should be destructible but cause damage to any vehicles that collide with them.
- The barriers should be spawned during a police chase as the chase escalates in duration and/or number of cars involved in the chase. The spawning should be optimized to not block police that are searching but impede the progress of the player. When the chase ends, all the obstacles should be removed from the map.
- Traffic should yield to the barriers and not attempt to blast through them, including the partially smashed versions. They should however go through if it is completely clear.
- Police should attempt to navigate around the barriers unless the barriers are partially smashed or if they are already close enough to the barriers, most likely by crashing near there.
Overall the main challenges were
- Getting the barriers to line up along the road direction and not too close together where they would crumble against each other
- Integrating the barriers into the police navigation system
- Orchestrating the spawning of the barriers in a positionally optimized way
Obstacle actor design
The concept of obstacles is abstracted behind an interface IVehicleObstacle. This contains functions to get the bounds of the obstacle, query for whether it is active within a given bounds(not damaged to where it is no longer a functioning obstacle), and dynamic multicast delegates that can be subscribed to for whether the obstacle is deactivated (e.g. when a chase ends), destroyed (e.g. a crumbled concrete barrier), or removed (which means the obstacle actor was removed from the map e.g. AActor::Destroy). To present a unified interface to clients, a composite design pattern was applied so that individual concrete barrier pieces that derived from BP_Barricade_Base in blueprints could be added to fill the width of the road and still be contained within a single logical object implementing IVehicleObstacle. Currently all the blueprints derive from ABreakableVehicleObstacle that itself derives from ABreakableActor that encapsulates the chaos destruction system.
A couple of challenges that came out of the obstacles themselves were
- Tweaking amount of damage delivered to vehicles when colliding with the barricades. I wanted it to be substantial to dissuade players from hitting them at high speeds but still not overly punishing. I already have a mechanism to nerf or buff damages on individual collisions based on actor or component names in blueprint properties, but needed to extend this to UGeometryCollectionComponents as well.
- Player can get stuck on fractured pieces of the barricade which is frustrating. I already have a UUnstickVehicleComponent that is attached to the player controller to give players a "nudge" in the direction they are trying to move (accelerate or reverse) when they are stuck on something. Just needed to update the blueprint property to include the barricades as another "nudgeable" condition
Spawning and obstacle tracking system
The spawning is part of a larger CQRS (Command query read segregation) designed system where there are separate "reader" and "writer" interfaces since traffic AI are concerned with reading if there is an obstacle along the spline that they are currently traveling or about to make a turn onto. Police are also interested in this when deciding where to search for the player. The police dispatch, though, is mainly concerned with figuring out where to spawn the barricades if either car count or elapsed time criteria are met (which are defined in blueprint properties).
The spawning and checking for obstacles behavior are defined in two interfaces but are both implemented by a single component that is added to the police dispatch actor. The "read" interface IBlockedSplineLocations is defined in a separate game module since it needs to be leveraged by both the traffic and police AI that reside in two different game modules. The obstacle implementations, interfaces, spawning logic, and navigation logic are all themselves in separate game modules for a total of 4 modules added as part of this feature.
The spawn director is responsible for spawning an obstacle nearest the given criteria and leverages the AObstacleSpawner instances placed in the map that are discovered on game start. The ObstacleSpawner uses its configuration to figure out the best VehicleObstacle classes to spawn, how many of them, and rotate and place them correctly so they line up without colliding against each other. Figuring this out was another time sink area that involved more linear algebra and geometry than I initially anticipated. I also had to dig through the engine to figure out how to get the bounding box of a class default object (CDO) that did not exist in the world yet as the bounds were zero initially. Turns out that for UGeometryCollectionComponent, using UGeometryCollectionComponent::UpdateCachedBounds does the trick. I can then hide this behind a function that can be invoked in my GetAABB function that is on the IVehicleObstacle interface that returns an FBox for the bounding box of the actor. Once the ideal set of spawn configurations is determined, then Uworld::SpawnActor can be invoked on each of them to place them in the world.
The UVehicleObstacleDispatchSpawner component is then the orchestrator on the spawning side that ties everything together. It is another component on the APoliceDispatch actor that coordinates all the police actions. It utilizes the previously mentioned interfaces IBlockedSplineLocations, IVehicleObstacleSpawnDirector an another existing interface IPoliceSplineLocations for determining the best place to spawn obstacles when the chase conditions are met. There is already an event that can be subscribed to when the active chase count changes and an timer event is used to kick off a spawning round if elapsed chase time criteria are met. Overall this was pretty straightforward with all the abstractions that had already been built and most of the time was spent tweaking the spawning conditions and criteria for determining the best location for spawning.
Police Navigation around Obstacles
The police navigation part was a tricky area. Some of this is due to the fact I am still using the Nav Mesh for the majority of the police path finding. A custom path finding component was derived to offset the nav mesh computed locations to specific lanes or to weave around traffic, react to light changes, etc. The police also make use of the spline road network for larger navigation decisions like searching for the player when line of sight is lost. I've tried using the ARecastNavMesh DynamicModifiersOnly runtime generation property but this resulted in horrid performance. This will need to be revisited in the future as the current solution does not scale well in total allowed obstacles on the map, though the performance is fine.
The first thing I tried doing was placing a ANavModifierVolume on the road by an obstacle and using a nav area class to change the area to impassable. This worked for existing obstacles but needed to be "turned off" if the spawning volume did not currently have an active obstacle. I next thought about spawning a ANavModifierVolume when the obstacle spawned, but quickly realized this would not work as the nav mesh is completely computed at build time, at least if the runtime generation property is "Static", and I already previously had tried making it dynamic but with unacceptable performance results. I tried also deriving from ANavModifierVolume to customize the existing behavior of that class to the nav area classes that I needed to create, but any derived class of this volume is not recognized by the Engine as a nav modifier volume and has no effect on the Nav Mesh, so you must explicitly use the ANavModifierVolume class when dragging the volume actor into the map.
In order to be able to dynamically flip the nav area classes "on" or "off" for whether they will affect police navigation, I needed to create a custom derived class of UNavigationQueryFilter. Documentation on this system can be found at https://docs.unrealengine.com/5.2/en-US/overview-of-custom-navigation-areas-and-query-filters-in-unreal-engine/ Since each obstacle can be uniquely activated, and the nav filters are based on area classes and not instances of those objects, this meant that each obstacle required a unique nav area class. This is the part that does not scale well. The Engine actually has a limit of 64 unique area classes total in the project (even if they are unused in a given map). This is hardcoded in the RecastNavMesh.h as RECAST_MAX_AREAS. This took a bit of troubleshooting to figure out and had to delete and recreate the nav volume in the map as the ids of the area classes were serialized into the map through a UPROPERTY and there was no way to reset them and purge the deleted entries. This must be done for performance reasons, but this currently limits the number of unique spawning areas I can have in a map. I know the way I am doing it is not the intended usage, but it was the only way I was able to make the feature work currently with reasonable performance. I already have a ticket for myself to revisit in the future if this feature grows. When "InitializeFilter" is called, the query filter uses a UWorldSubsystem (which is Unreal's way of managing "singleton" instances at various scopes in your game) - UPoliceObstacleNavSubsystem that can query which obstacle spawners are active for the purposes of the navigation query, and then I can get the active nav area classes from that. The NavQueryFilter is then added to the various MoveTo behavior tree tasks for the police to use the modified navigation criteria.
I also had the problem of the nav query filters being cached indefinitely by default. This of course was done for performance reasons, but I needed to invalidate the cache whenever an obstacle would change state. At first tried to manage this from a function on the nav system, but then discovered a property on UNavigationQueryFilter: bInstantiateForQuerier that I could set to true in the constructor of my derived class to never cache the results. I found this by tracing the source code of how the nav query filter classes were instantiated. This does not cause a performance impact for me as the obstacle spawning state is already managed through a separate sub system class and so all this amounts to is toggling which nav area ids should be excluded through the FNavigationQueryFilter object passed into InitializeFilter in my NavQueryFilter class. The function will be called each time a new path query is issued.
I also needed to adjust end location of the path query if they happened to land inside an obstacle that was marked as impassable in the nav mesh. I accomplished this by overriding OnPathfindingQuery on my existing UPathFollowingComponent derived class for the police path following. I could use the IBlockedSplineLocations interface to query given a target location and direction that if this is inside an obstacle to snap it to just outside its bounds in the given direction. The regular navigation then would find a path around the obstacle. The start location inside the obstacle case was already handled by the UPoliceObstacleNavSubsystem in the query filter to just allow the police to smash through these obstacles if they needed to navigate through them.
I briefly tried figuring out how to create an editor widget that could automatically spawn a ANavModifierVolume into the map when a spawner volume was placed. I ran into similar problems of the modifier volume not being recognized by the nav mesh and so punted on that feature and just documented for myself on how to create the obstacles.
1) Place the spawner volume in the map and configure it. Assign the expected nav area class to the volume.
2) Drag in a ANavModifierVolume class around the spawner and configure it with the same nav area class. It's not particularly elegant but not overly complicated, especially if you group the actors and then can just duplicate them around the map.
Get Police Chase Simulator
Police Chase Simulator
Cops and robbers style driving game inspired by the "Driver" series. Race around the map and escape your pursuers!
Status | In development |
Author | GameSalutes |
Genre | Racing, Action, Simulation |
Tags | artificial-intelligence, Crime, Driving, Exploration, Open World, Physics, Singleplayer, Unreal Engine |
Languages | English |
More posts
- Polish (v0.0.34)Oct 07, 2023
- Maintenance Release (v0.0.33)Sep 06, 2023
- Optimizing AI in v0.0.32Aug 17, 2023
- Bug Fixes and Optimizations (v0.0.32)Aug 17, 2023
- Crash Damage Modeling! (v0.0.31)Jul 26, 2023
- Bug Fixes and Engine Upgrade (v0.0.30)Jul 15, 2023
- Destructible Road Blocks! (v0.0.29)Jun 24, 2023
- Update with destructible police barricades coming soon!May 31, 2023
- Police will find you! (v0.0.28)May 03, 2023
Leave a comment
Log in with itch.io to leave a comment.