A Zombie Horde approaches! In almost every tradition zombie film the zombies tend to horde together in groups and wander around. This is...

Horde Behavior

A Zombie Horde approaches!
In almost every tradition zombie film the zombies tend to horde together in groups and wander around. This is something I've always wanted to bring to my mod and I've finally gotten around to implementing it.

AI Packages

Packages are sets of instructions that influence the behavior of actors in the game. You can think of them as configurable AI that gives actors (like zombies) a list of possible actions to perform.

Many actors in the game, including Feral Ghouls, have default packages assigned. One in particular has a lot of useful functionality already called the Default Master Package which I'll talk about below.

Linked References

Step one is figuring out how to make them move. It really isn't a horde if the zombies just stand around. What I wanted to accomplish was zombies that walk around aimlessly if they are idle (not in combat).

This is where the aforementioned packages come in. The Default Master Package has procedures for Follow and Patrol. Both of these procedures work by making the actor path to a linked reference. A linked reference can be just about anything and there are methods in Papyrus to GetLinkedRef and SetLinkedRef.

Patrol (aka. make them wander)

The patrol package is triggered if a zombie has a linked ref set to something like an Idle_Marker. Idle Markers are hidden objects that mark a particular location or object. They can be explicitly assigned as a linked reference to an actor or idle actors can "discover" them on their own if they happen to be in the area (like settlement workshops).

In my case, I spawn an Idle Marker in a random direction away from a zombie I want to make walk and then assign the marker as the zombie's linked reference. This is enough to make the zombie slowly walk (regardless of the Speed setting on the holotape).

Once they get to the marker, they stand around. Of course I want them to continue wandering aimlessly until they bump into something (ie. enter combat). To do this, I use the RegisterForDistanceLessThanEvent between the zombie and the Idle Marker. This event triggers when the zombie gets within a certain radius I define of the marker. When it triggers, I simply move the marker. This pattern repeats and the zombie continues to follow the continuously moving marker.

The direction they move is random at first and then they generally stay in the same general direction afterward. I assigned a number to the basic cardinal directions and used an Actor Value assigned to the marker to keep track of the direction. The first move is completely random, then the subsequent moves can be straight ahead (no change) or 1 value deviation from the last direction.

7 (NW)0 (N)1 (NE)
6 (W)2 (E)
5 (SW)4 (S)3 (SE)

Dealing With Patrol Problems

Unfortunately, there are times when a zombie is unable to reach the Idle Marker or at least get close enough to trigger the DistanceLessThan event. In those cases, the zombie will just stand around and stop moving so we need to do something about it.

I created another Actor Value for timestamp and assigned it to the marker to keep track of the last modified time. This timestamp is set when the marker is created and any time it is moved.

I already have a hidden quest with a trigger to do some processing every 10 seconds or so. I updated this function to add a check for all markers (of a type that I spawned). If a marker's timestamp is older than some threshold I define then the marker is deleted and I send a Custom Event.

All zombies that are spawned listen for my custom ZombieMarkerDeleted event that is generated by my quest's script and respond to it. In the case of a zombie that is wandering, it begins the wandering process all over again by spawning a brand new marker in a random direction.

From Wandering Zombies to a Horde

OK, so at this point we have every zombie wandering in it's own direction randomly. To make it a horde they really need to band together some how and move in groups.

To accomplish this, I took advantage of the Follow procedure which works similar to the Patrol above. If you set a linked reference for a zombie to another zombie then it will follow it. It's really that simple.

But wait, who should follow who? I came up with a simple solution to allow the zombies to determine whether to follow or start patrolling themselves. The zombie effect script that gets attached to them (see Dynamic Script Attachment) now sets their FeralGhoulFaction rank randomly from 0 to 100 and that rank is used to determine who follows who.

  • Check for nearby zombies and follow the first one you come across with a higher rank than my own.
  • If no zombies are nearby with a higher rank, then start to patrol (wander by spawning an Idle Marker to path to). 
  • Note that any zombies that have decided to follow me will therefor also start to patrol behind me.

Dealing With Follow Problems

The only real problem you run into when following another zombie is what to do if that zombie dies. The easy solution for this is to register for the OnDying event and follow someone else (or start to patrol ourselves).

Issues With Quests

After release, a bug was reported where a player was sent to clear zombies from an area but when they got there the zombies were nowhere to be found. As you might have guessed, the horde behavior caused them to wander out of the area before the player got there.

To address this and avoid interrupting quests, I decided to disable the horde behavior for certain zombies so that they would be there when expected. To determine which zombies to not move, I ended up discovering by some research that many of the quest related actors in the game are linked to their location with a Location Ref Type. There types allow quests to quickly determine if all actors in a group are still alive (eg. functions like GetRefTypeAliveCount). You can check if any actor is used as a Location Ref Type pretty easily using GetLocRefTypes.

Another exclusion I ended up adding is for zombies that spawn in a Clearable or Dungeon location. These locations often rely on the actors being present to be "cleared" and can easily be checked by GetCurrentLocation and HasKeyword.
  • do NOT follow or patrol if:
    • Actor (or replacement) has a least one linked Location Ref Type
    • Actor (or replacement) spawned in a Clearable or Dungeon location.
The "or replacement" refers to the optional Spawn Replacements holotape settings. If used, the replacement is taken into account in determining mobility. Note that if replacement levels larger than one for one (eg. two for one) are used, then the "extra" zombies are not tied to the original actor in any way and can and will wander freely.

Wrap Up

The combination of patrolling and following described in this article gives zombies an initial wandering horde behavior that is much preferred over just standing around. It adds a dynamic unpredictability to every play-through as well in that you'll never know where zombies might appear even if you've memorized all the spawn points.

I'll likely continue tweaking this behavior as bugs are reported and ideas come to me. I already have thoughts about adding additional marker types for noise and possible dead bodies. Noise markers could serve to attract zombies from farther away than the game's default detection range (but not make them hostile, just make them lumber toward the sounds in the distance). Dead bodies could attract nearby zombies for a feast as Idle Markers can have Idle Animations associated with them (eg. the Feral Ghoul crouching and eating animations).

0 comments:

Infinite zombies spawning to replace molerats at the Rotten Landfill location. Sometimes, too many zombies is a bad thing. Like when sai...

Rotten Zombie Spawner

Infinite zombies spawning to replace molerats at the Rotten Landfill location.
Sometimes, too many zombies is a bad thing. Like when said zombie horde crashes your game. This is unfortunately what was happening at the Rotten Landfill thanks to how the quest there worked in conjunction with animal spawn replacements.

The Issue

There is a quest used at the Rotten Landfill to stage a fight between a settler and some molerats. The idea is to have this fight continue until the player is close enough to witness it and potentially intervene.

The quest marks the settler as immortal and spawns a few molerats that are marked to re-spawn when killed. When the player gets within 6000 units the fight is triggered and will continue forever with the molerats continuously re-spawning as the immortal settler eventually kills them. When the player gets within 3000 units the fight becomes real in that the settler is mortal and the infinite re-spawns are stopped (and instead become X waves as described on the wiki).

When using the animal spawn replacement option via the Zombie Walkers holotape, these molerats are replaced by zombies. During that replacement, the molerat being replaced isn't just disabled (disappearing from view) but also killed. Since the molerat is killed, it triggers the quest to re-spawn the molerat which in turn gets killed and replaced with a zombie. This triggers the molerat to re-spawn again... well, you probably see where this infinite loop is going.

Why kill the replacement?

Good question. I knew some quests involve creatures that might be replaced by my mod. To avoid those quests getting stuck, I wanted to make sure they didn't end up waiting for a death event that would never arrive. The first example of this I can think of is the Sanctuary bloatfly quest with Codsworth.

So how do we fix Rotten Landfill?

The easy solution would have been to simply exclude the location from spawn replacements, but I'm too masochistic for that. Instead what I ended up doing is delaying the kill until the zombie that replaced the molerat was killed.

This was actually fairly straightforward to implement. At time of replacement, the creature being replaced is still disabled and I added a linked reference to the creature being replaced to the zombie that was replacing it with a new keyword. When that zombie died, I added code to check for that type of child linked ref and kill it also if present. Note that this only applies to the first zombie replacing that actor, so if you are using 2 for 1 replacements, the second zombie's death will do nothing to the replaced actor.

This approach not only solves the Rotten Landfill issue but also keeps quests from getting stuck. It should also be universally applicable for other re-spawns that might be used in other areas. Finally, it makes sense and flows better while you are playing the game. Now when you meet Codsworth in Sanctuary, you will search the neighborhood killing zombies instead of bloatflies instead of the quest starting out in a "I already searched the neighborhood" state.

0 comments:

Immortal Zombie (reaver armor) with the gunshot wounds to the head to prove it. Zombies That Magically Heal Sometimes zombies just do...

Immortal Zombie Bug

Immortal Zombie (reaver armor) with the gunshot wounds to the head to prove it.

Zombies That Magically Heal

Sometimes zombies just don't want to die, even when you shoot them in the head numerous times. In the case of the Reaver style zombies (encFeralGhoul04), they get knocked down and then get back up with full health restored. This is the zombie type I've personally noticed this bug with and it is 100% repeatable with them. All other zombie variants die as expected with a headshot.

Tracking Down the Issue

Tracking this down involved a lot of trial and error, but eventually it came down to what I believe to be a bug in the game with Deferred Kill and this particular Feral Ghoul actor type. Deferred Kill basically makes it so an actor does not die until you tell it to. If headshots only mode is enabled, then deferred kill is started when the zombie is initialized.

To end the deferred kill, I registerd for the OnCripple event. This event is triggered when any limb (including the head) is crippled by damage. I noticed for the Reaver style armored zombies that this event was triggering twice and that the deferred kill seemed to not be honored.
Event OnCripple(ActorValue akActorValue, bool abCrippled) 
[ZombieEffectScript] HandleCrippleEvent([Actor < (FF00134A)>],[ActorValue <PerceptionCondition (0000036C)>],True) -> 0.000000
[ZombieEffectScript] HandleCrippleEvent([Actor < (FF00134A)>],[ActorValue <PerceptionCondition (0000036C)>],False) -> 100.000000
 As you can see above, the first call has 'True' for the crippled bool flag for the PerceptionCondition (head) which is good. In that handler I EndDeferredKill and then call Kill if the actor is still alive. This apparently isn't good enough for the Reaver zombies as they regenerated their health and limbs, did NOT end deferred kill and got back up off the ground. This regeneration is what I think triggers the second onCripple event which notifies the code that the head is now un-crippled (abCripple is 'False').

After some experimenting in game, I noticed that you could in fact kill these zombies if you shot it multiple times in the head in succession (while it was dropping to the ground). I then experimented with shooting them in the torso a bit first and then the head which revealed a pretty consistent way to dispatch them as they died almost always with a single head shot if their health was down a bit first.

Fixing the Problem

As an experiment, I updated my onCripple handler to remove any remaining health before ending the deferred kill state and that fixed the issue. Apparently triggering EndDeferredKill when their remaining health was 0 always worked for the Reavers, even though the other types didn't care how much health was remaining. I think it wasn't the EndDeferredKill call itself so much as the subsequent call to Kill that seemed to behave differently.

Here's what I ended up with:
; helper function to handle both onCripple and onPartialCripple
Function HandleCrippleEvent(Actor akSender, ActorValue akActorValue, bool abCrippled)
Debug.OpenUserLog("joefor")
Debug.TraceUser("joefor", "[ZombieEffectScript] HandleCrippleEvent(" + akSender + "," + akActorValue+ "," + abCrippled + ") -> " + akSender.GetValue(akActorValue))
If( abCrippled )
If( akActorValue == HeadConditionAV )
; head is crippled - make sure zombie dies
KillZombie()
Else
; other body part crippled - apply knockdown
ZombieActor.PushActorAway(ZombieActor, 1)
NPCFeralGhoulInjuredDownNotOutEnter.Play(ZombieActor)
EndIf
EndIf
EndFunction
Function KillZombie()
float healthAmount = ZombieActor.GetValue(HealthAV)
Debug.OpenUserLog("joefor")
Debug.TraceUser("joefor", "[ZombieEffectScript] KillZombie(" + ZombieActor + ", health=" + healthAmount)
; there's a bug with Feral Ghoul 04 not dying if health remains before deferred kill ends
; make sure remaining health is zero to trigger deferred kill
ZombieActor.DamageValue( HealthAV, healthAmount )
; end deferred kill to allow zombie to die
ZombieActor.EndDeferredKill()
; now, make sure zombie dies
If( !ZombieActor.IsDead() )
ZombieActor.Kill()
EndIf
EndFunction 

Version 1.15 Coming Soon

I'm wrapping up some testing and will be deploying in the next day or so. Here's what will be included:

  • The fix described above for immortal zombies
  • fix for Suffolk County Charter School (humans in underwear spawning instead of zombies)
  • NEW optional horde behavior - zombies follow each other and wander around instead of standing still where they spawned (I may blog separately about this)

Update 1/1/2017: more immortal zombies

More immortal zombies, one of which is missing half its skull.
Another immortal zombie bug was reported and has been fixed. This one has to do with situations where the OnCripple event is not received by the script and therefore the zombie never leaves the deferred kill state. Subsequent OnCripple events are not received for the head if you continue to shoot the head.

To reproduce this, I used a missile launcher and aimed for the chest in a group of zombies. The immense amount of damage seems to sometimes cause this. I'm still not 100% sure why the OnCripple for the head (PerceptionCondition) is not received by the script. I noticed that every time it happens, the script does receive OnCripple for the body (EnduranceCondition). Maybe the game assumes if the torso is crippled (which is rare) then the creature will die anyway so it doesn't bother sending a separate one for the head?

In either case, to fix the problem I added one more event registration to the script. This one is for the OnDeferredKill event. This event gets triggered when an actor is in the deferred kill state and receives damage that would kill it if it weren't in that state. It also happens to fire over and over again as damage is received, even if the health goes into the negatives. Basically, I use this function to check the state of the head and end deferred kill if it is destroyed.

Let's hope this is the last of the stubborn zombies that refuse to die.

Update 1/27/2017: crippled vs dismembered

Even after my last update, still some immortal zombies were reported albeit rarely and usually when using certain explosive weapons like Spray N' Pray. Upon further research I noticed that in VATS only their torso was target-able as in the screenshot below.

Immortal zombie with only the torso target-able in VATS.
I began doing research into what would cause the head to not be there and quickly realized that it is actually dismembered (like the legs/arms in the screenshot above). After adding some debug logging I then saw that sometimes the head health would be above zero even though the head was dismembered!
OnDeferredKill([Actor < (FF000F36)>],[Actor < (00000014)>]),  PerceptionCondition = 20.053139, HeadDismembered = True
I then updated my function to check if the head is destroyed by checking EITHER:

  • Head health is 0 (PerceptionCondition)  ... OR ...
  • Head is dismembered (Head1 for feral ghouls and humans)
I then tested by spawning groups of 20-30 zombies at a time, waiting a bit for my scripts to attach and initialize on all of them, and then mowing them down with Spray N' Pray. Before the changes I was seeing 1 or 2 in every group in the immortal state in the screenshot above. After the change I could not produce a single occurrence even after spawning several waves.

0 comments:

I have published Zombie Walkers to the Nexus mods site to hopefully expand the PC user audience as many players on PC use the Nexus Mod Mana...

Zombie Walkers on Nexus

I have published Zombie Walkers to the Nexus mods site to hopefully expand the PC user audience as many players on PC use the Nexus Mod Manager instead of Bethesda's built-in mods menu.

http://www.nexusmods.com/fallout4/mods/20485

3 comments:

Pages (8)1234567 »