I’ve implemented lag compensation for my attack abilities in my game by fetching a historic collision world and performing a CastCollider for that world. Everything works fine EXCEPT when my enemy entities are destroyed by the attack. Enemy destruction results in the following BlobAssetReference error(s):
InvalidOperationException: The BlobAssetReference is not valid. Likely it has already been unloaded or released.
My attack trigger collider lasts for multiple frames so what I think is happening is:
enemy entity destroyed
collision world from past is generated which still contains references to the destroyed entities
past entities no longer have valid BlobAssetReference for Colliders for CastCollider to work on as they have been destroyed in the present
error
Is this why I’m getting the BlobAssetReference error? And if so, is there a simple/elegant way to solve this that I’m not seeing? I was considering adding a destroy delay to my entities and moving them offscreen for a couple seconds to ensure they’re at least still available for my collision history CastCollider?
Below is my full system for reference.
[UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup))]
unsafe public partial struct LagCompensationSystem : ISystem {
NativeList<ColliderCastHit> castHits;
void OnCreate(ref SystemState state) {
state.RequireForUpdate<NetworkTime>();
castHits = new NativeList<ColliderCastHit>(Allocator.Persistent);
}
void OnUpdate(ref SystemState state) {
var collisionHistory = SystemAPI.GetSingleton<PhysicsWorldHistorySingleton>();
var physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>().PhysicsWorld;
var networkTime = SystemAPI.GetSingleton<NetworkTime>();
var delayLookup = SystemAPI.GetComponentLookup<CommandDataInterpolationDelay>();
var predictingTick = networkTime.ServerTick;
if (!networkTime.IsFirstTimeFullyPredictingTick) return;
foreach (var (triggerBuffer, triggerCollider, triggerTransform, ownerEntity, triggerEntity) in SystemAPI
.Query<DynamicBuffer<TriggerBufferElement>, RefRO<PhysicsCollider>, RefRO<LocalTransform>, RefRO<OwnerEntity>>()
.WithAll<Simulate>()
.WithEntityAccess()) {
// get delay
uint delay = delayLookup.HasComponent(ownerEntity.ValueRO.Value) ?
delayLookup[ownerEntity.ValueRO.Value].Delay : 0;
// get collision world
collisionHistory.GetCollisionWorldFromTick(predictingTick, delay, ref physicsWorld, out var pastCollisionWorld);
// setup ray cast
float3 rayFrom = triggerTransform.ValueRO.Position;
float3 rayTo = triggerTransform.ValueRO.Position + new float3(0, 0.01f, 0);
// populate input
ColliderCastInput input = new ColliderCastInput() {
Collider = triggerCollider.ValueRO.ColliderPtr,
Orientation = triggerTransform.ValueRO.Rotation,
Start = rayFrom,
End = rayTo,
};
// do collision cast against historic colliders
pastCollisionWorld.CastCollider(input, ref castHits);
foreach (var castHit in castHits) {
// return if we already have the response entity in our buffer
bool isHitAlready = false;
foreach (var trigger in triggerBuffer) {
if (trigger.ResponseEntity == castHit.Entity) isHitAlready = true;
}
if (isHitAlready) continue;
// add trigger
triggerBuffer.Add(new TriggerBufferElement {
isHandled = false,
Position = castHit.Position,
Normal = castHit.SurfaceNormal,
ResponseEntity = castHit.Entity,
});
}
castHits.Clear();
}
}
}
Correct! And in a funny coincidence, we’ve been discussing this exact problem internally this week. The central problem is:
Netcode’s assumption / POV is that historic collision worlds should be cacheable and query-able, without raising exceptions.
Physics’ assumption / POV is that entities being deleted should invalidate the cached physics world.
Which poses a problem for you (and other users of Lag Compensation). Proper resolution is incoming, but your workaround sounds like it should work. Defer entity deletion until after the collision history ring buffer is recycled. I’d recommend Disabling the entity instead, though, to prevent it from being added to future collision worlds at all.
There is not such a logic exposed right now. All variables and structs are pretty much internal. But if you are willing to modify the package you can add some logic to detect that.
The PhysicWorldHistorySystem run inside the prediction loop and it store the physics world state of the last tick (actually the last X ticks since the last time it runs, in case of low framerate)
You can infer what slots are going to be re-used or recycled for the history buffer for the current server tick by using something like:
var currentTick = serverTick;
var lastTick = serverTick.Decrement();
var indexThatIsRecycled = (int)(lastTick.TickIndexForValidTick % CollisionHystorySize);
This just give the idea. The PhysicWorldHistorySystem use a loop and can write more than one slot per frame, so it is slightly more complex.
In general though, given the entity spawn tick X, and the default number of collision history slot, you need to keep alive that entity until X + CollisionHistory.Size ticks (that is pretty much 0.5 sec roughly with the default settings).
I assume that only works in combination with @NikiWalker 's suggestion of disabling the entities first, so they won’t be referenced anymore?
Else, the entities would still be referenced in the PhysicsWorldHistory snapshots when destroying them.
Also, there is the RetainBlobAssetSystem that seems to be designed to solve issues like this.
However, there doesn’t seem to be a way for users to use/configure this system anymore.
Small update: We’re working with physics on improvements to allow netcode to “deep copy” the physics CollisionWorld (API call in PhysicsWorldHistory.CloneCollisionWorld) when cloning, retaining blob assets for all N (16) historic frames, leading to correct query results (which may be on since-deleted entities), thus allowing you to remove your hack. No promises & no ETA yet.
Long-term: We’re thinking about adding support for optionally retaining IComponentData values for historic entities too, so you can query additional data (e.g. “was my victim entity invulnerable at that time in history?”) during Lag Compensation. Is this something you (or others) have run into, and needed?
That’s a use-case I should have run into, but hadn’t realised . Having that ability would technically make things more fair for players, so it seems like a good addition for sure.
Also, if such a thing were to be added, I’d want to be able to have fine-grained control on precisely what data is kept in history, as I imagine the memory usage of something like this could be heavy.