AI War 2:Checking For Dead Entities

From Arcen Wiki
Jump to navigation Jump to search

There are various times you might want to check if a unit is dead, particularly in background code.

Danger Spots! Important!

In one example, you might have a list of GameEntity_Squad objects on some faction's implementation class. You might be using this as a list of "ships that popped out of constructor X," for instance. If you just keep that list and never check for dead entities, then two things will happen:

1. After 15 seconds of an entity being dead, it may re-enter the game from the central pool as a completely new entity. So what was a Raider unit is now a Command Station somewhere else. But your code still thinks that you've got a reference to a Raider unit, and tries to manipulate it in a way that is wholly inappropriate (sending it places for the wrong faction, etc, etc).

2. Even sooner than 15 seconds, if the entity is dead but you're not checking that, you might be making decisions based on bad info. You still have a reference to an intact object, for instance, and so you say "my total strength is 5 ships, let's send them somewhere." But if you don't check for death, then you might actually have a case where three of those entities are dead and so you're making a really bad decision sending only 2 things when you think you're sending 5.

The Responsibility Is Yours (Calling Code)

Essentially it's up to you to notice when an entity has died, and remove it from any of the lists you have. Think of the calling code as being on one side of a one-way mirror. When an entity dies, not only CAN'T the central game reach through the mirror and take the entity out of your lists, but it doesn't even know how many lists there might be, in whose code, etc.

There's a 15 second grace period after a unit dies before it can be used again from out of the pools, and that's meant to give you loads of times to check for death and react to that (purging it from your lists, etc). Usually the long term planning threads will run every few seconds, so checking each time they go by that everything you're looking at is still-living will solve most problems.

The Easy Solution (Just Check Frequently)

Essentially if you're checking for dead entities every time the long term planning thread goes by (which is good practice anyway for decision-making of your AIs not overestimating its own strength), then this problem pretty much goes away.

The easiest two ways to check that are these two:

1. To see if a unit has already been put back into the pool (so it's definitely dead), you can check for HasBeenRemovedFromSim being true. You'll also want to check if ToBeRemovedAtEndOfThisFrame is true, because that means the unit is going away "for some reason" soon. It could be that the units are turning into a stack, and so one of the units "died," or it could be that it actually got shot to death. Checking if either of these two bools is true is the fastest approach.

2. There is a GetHasBeenDestroyed() that is absolutely comprehensive in telling if a unit has died, and catches all the potential edge cases such as "health has been reduced to zero but it's not flagged to be removed quite yet." It might be overkill, but it's the most thorough check. Checking if this method returns true is slightly slower, but is completely comprehensive and can be used instead of the checks to the other two bools (it checks those inside this, among other things).

The Robust Solution (SquadWrappers)

Checking for death and/or things getting pulled out of the pool is one of the many reasons I use SquadWrapper instead of directly referencing squads in some cases. If there's something that would be silly-expensive to check every frame (such as EntityOrders), and you want to be able to reference a Squad and know that you're not getting "some other squad that the unit turned into after dying, being in the pool for 15 seconds, and then popping back out..." then you'll need to use a SquadWrapper.

These are structs, so they just go on the stack, not creating any heap memory. But they do come with some CPU cost.

The squad wrapper has a PrimaryKeyID on it, and when you call GetSquad() it makes sure that the entity that is in there still matches the PrimaryKeyID. If not, it returns null. Because then we know the object was sent back to the pool and came out again, and so we don't get a bad reference to something that is now something else. This way you could have a reference to a specific squad that you might not check for minutes or hours at a time (lots of queued orders?) and it's all okay when you do get around to checking it. If the unit died, then GetSquad() just returns null.

Remember to check for that, and understand that there is a minor performance impact on the CPU here, and these are a really great way to handle long-running infrequently-checked references to units.