Hello Sanctuary seekers!
Sticking with the theme of AI, I'd like to share how we maim and kill AI in the After Sanctuary world.
Let's have a look through some code.
First up, we have an interface that all health scripts can implement.
An interface acts like a contract.
Every script which implements it has to implement its methods and variables.
public interface IDamageable
{
public bool IsDead { get;}
void TakeDamage(float amount, GameObject attacker, bool canTakeLimbs = false);
}
The TakeDamage method takes in a damage amount, the GameObject which caused the damage and a bool which describes whether this type of damage can remove limbs and heads. It's set to false by default so we don't need to include it in our attack code unless we want to change it to true.
The advantage of inheriting from the IDamageable interface is that we can search the interface rather than searching for each script that inherits from it when we want to actually cause damage.
Different type of AI or destructible objects in the world may use different health scripts that do different things when met with the same conditions.
var health = hit.collider.GetComponent<IDamageable>();
if (health != null)
{
health.TakeDamage(25f, this.transform.root.gameObject, false);
}
Here we get the IDamageable and cause some damage from one of the weapon scripts in the game.
Over in the ZombieHealth class this calls the take damage method which returns and does nothing if this AI agent is already dead.
If the agent is still alive, it depletes its health by the amount of damage passed into the method, and checks if the health is <0.
If it is, we call the Die method passing in the attacker gameobject.
We use the attacker gameobject to get the position of the attacker and add a little impulse to the ragdoll from that direction.
public void TakeDamage(float amount, GameObject attacker, bool canTakeLimbs = false)
{
if (IsDead) return;
amount = Mathf.Max(0f, amount);
currentHealth -= amount;
if (logDamage) Debug.Log(quot;{name}: Took {amount} dmg from {(attacker ? attacker.name : "unknown")} -> {currentHealth}/{maxHealth}");
if (currentHealth <= 0f)
{
Die(attacker);
}
else
{
//TODO: Implement knock back and hit reactions.
}
}
void Die(GameObject attacker)
{
if (IsDead) return;
IsDead = true;
// Stop AI from moving.
if (clearPathOnDeath && mover) mover.Stop();
if (disableMoverOnDeath && mover) mover.enabled = false;
// Stop both sensors
if (disableVisionOnDeath && vision) vision.enabled = false;
if (disableHearingOnDeath && hearing) hearing.enabled = false;
// Trigger death anim (optional) then immediately switch to ragdoll
if (animator && !string.IsNullOrEmpty(deathTrigger))
animator.SetTrigger(deathTrigger);
// Switch to ragdoll (physics takes over; body remains)
if (ragdoll) ragdoll.SetRagdoll(true, zeroVelocities: true);
// Disable CharacterController and any colliders.
var cc = GetComponent<CharacterController>();
if (cc) cc.enabled = false;
if (disableThisCollider)
{
var col = GetComponent<Collider>();
if (col) col.enabled = false;
}
// Add a little impulse from attacker direction.
if (addDeathImpulse && ragdoll)
{
Vector3 hitFrom = attacker ? attacker.transform.position : transform.position - transform.forward;
Vector3 toZombie = (transform.position - hitFrom);
Vector3 dir = toZombie.sqrMagnitude > 0.001f ? toZombie.normalized : (-transform.forward);
dir.y += Mathf.Max(0f, impulseUpBoost);
Vector3 point = transform.position + Vector3.up * 1.2f; // chest-ish
ragdoll.AddImpulse(point, dir.normalized * impulseMagnitude, 0.3f);
}
// change layer/tag to "Corpse" etc. (commented; add if needed)
// gameObject.tag = "Corpse";
// gameObject.layer = LayerMask.NameToLayer("Corpse");
if (logDamage) Debug.Log(quot;{name}: DIED");
}
In the above Die() method, we stop movement, stop the sensors, we can play a death animation if we want to, The script has bools to control this feature but in this case we switch to ragdoll without playing an animation.
Disable the character controller and add the impulse we talked about earlier.
I can also change the physics layer of the zombie so the player can interact with it and loot it for example.
We can also log any events by turning on a bool in the inspector.
There's also a Revive() method that I could use but it's not used....yet...
public void Revive(float healthFraction = 1f)
{
IsDead = false;
currentHealth = Mathf.Clamp(healthFraction, 0.01f, 1f) * maxHealth;
var cc = GetComponent<CharacterController>();
if (cc) cc.enabled = true;
if (ragdoll) ragdoll.SetRagdoll(false, zeroVelocities: true);
if (vision) vision.enabled = true;
if (hearing) hearing.enabled = true;
if (mover) mover.enabled = true;
}
This just turns everything back on.
I think I've covered everything I wanted to.
If you have any questions please leave a comment and remember you can join our Discord server here: https://discord.gg/S8cCdyzB
Until next time sanctuary seekers,
Stay safe.
0 comments