伤害处理

来源:互联网 发布:tensorflow c api 编辑:程序博客网 时间:2024/04/29 15:39

Hi there, last time we’ve learned how to create weapons. 

However, we still don’t know how to make a weapon that hurts. Yet.

I’ll cover this area in 3 parts. First, We’ll have a look at how what tools UDK gives us to handle damage.

 Then we’ll see how to take damage. And finally, we’ll see how to inflict damage.

Hit/Damage-related classes and structures

Unreal Engine’s take on damage dealing is ruled by the following philosophy: 

There should be little to no interdependency between the damage dealer and its victim. 

So when Actor A hurts Actor B, Actor A gathers as much information as possible about the hit, 

and then give it all to Actor B that will handle all by itself all the consequences of receiving that damage.

UDK has built-in support for damage dealing, and offers several functions to handle it. However,

 NO concept of health is supported at Actor level. The Pawn class however does implement some

 management of health (we’ll have a look at it further down). But if you want health management for

 a very simple actor, you’ll have to implement it yourself. Or subclass Pawn, which sounds to me like overkill, and as such a very bad idea.

Several structures and classes have been defined to regroup information.

TraceHitInfo structure

This structure holds several optional information about what has been hit and where, such as

 the Material (to trigger different sounds or define a different behaviour (e.g. bouncing on specific surfaces)) 

or the bone hit (for localized damage). Below is the structure definition taken from Actor.uc (UDK Oct 2010):

1
2
3
4
5
6
7
8
9
structnative transient TraceHitInfo
{
    varMaterial Material; // Material we hit.
    varPhysicalMaterial PhysMaterial; // The Physical Material that was hit
    varint Item; // Extra info about thing we hit.
    varint LevelIndex; // Level index, if we hit BSP.
    varname BoneName; // Name of bone if we hit a skeletal mesh.
    varPrimitiveComponent HitComponent; // Component of the actor that we hit.
};


ImpactInfo structure

Stores what has been hit, where and from which direction. Below is the structure definition taken from Actor.uc (UDK Oct 2010):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Hit definition struct. Mainly used by Instant Hit Weapons. */
structnative transient ImpactInfo
{
    /** Actor Hit */
    varActor HitActor;
 
    /** world location of hit impact */
    varvector HitLocation;
 
    /** Hit normal of impact */
    varvector HitNormal;
 
    /** Direction of ray when hitting actor */
    varvector RayDir;
 
    /** Start location of trace */
    varvector StartTrace;
 
    /** Trace Hit Info (material, bonename...) */
    varTraceHitInfo HitInfo;
};

DamageType Class

As the comment says at the top of the file, this static class is an information holder. Each time damage is dealt, you must specify what kind of damage is dealt, regardless of how the damage has been dealt. Note that DamageType is an abstract class, which means you MUST create a subclass of it for your game.

The concept of Damage Type has several upsides:

  • Different weapons can have common damage effects.
  • Conversely, one weapon can have the same behaviour but different effects (e.g. Elemental damage)
  • By knowing the kind of damage that hit an actor, you can set up immunities.
  • It’s useful if you need to get stats from the damage taken.

The only downside I can think of is that you might end up writing lots of DamageType subclasses (UDK’s UTGame has 20, and there are only 3 “real” weapons).

In the base DamageType class, the most noteworthy properties are the following:

  • bool bArmorStops: tells if this damage type ignores armour (note that this one might be a bit out of place as the concept of armour only exists in UTGame).
  • bCausedByWorld: Set to true if the damage hasn’t been caused by an Actor.
  • float KDamageImpulse: used to move KActors upon impact.
  • float RadialDamageImpulse: used to move KActors upon impact, in the event of an area effect.
  • float VehicleDamageScaling: Percentage of damage taken by a vehicle (1.0 = 100%).
  • bool bCausesFracture: if set to true, this damage type can break Fracture Meshes
  • float FracturedMeshDamage: tells how much it affects fractured meshes. Defaults to 1.0.

Chances are you’ll be mostly using your own variables. 

The Engine Package already defines Damage Types for being crushed or telefragged, falling, or killing one’s self.

Taking Damage

The Actor class defines a few functions that take care of receiving damage. 

Yes, the Actor class. Which means nearly ANYTHING in the game can potentially take damage,

 even weapons (Bethesda’s games, anyone?). Let’s have a look at those functions.

TakeDamage

The obvious one. It’s called by the Actor that causes the damage. Has the following parameters:

  • int DamageAmount: self-explanatory. Note that this is an int.
  • Controller EventInstigator: who (player) hit me?
  • vector HitLocation: where have I been hit?
  • vector Momentum: with how much force have I been hit? (for physics-related effects, mainly)
  • class<DamageType> DamageType: self-explanatory
  • optional TraceHitInfo HitInfo: additional info on the hit.
  • optional Actor DamageCauser: what hit me?

Default implentation does nothing but notify Kismet that the Actor has been hit. So if you override this function,

 don’t forget to call super.TakeDamage.

TakeRadiusDamage

This is a wrapper that, as the function’s description says “by default scales damage based on distance from HurtOrigin to Actor’s location.

 [...] This then calls TakeDamage() to go through the same damage pipeline” .


1
simulatedfunction TakeRadiusDamage ( Controller InstigatedBy, floatBaseDamage,
floatDamageRadius, classDamageType, floatMomentum, vectorHurtOrigin, 
boolbFullDamage, Actor DamageCauser, optionalfloat DamageFalloffExponent=1.f )



Nothing much to say here. Just note that here the DamageCauser is not optional this time, 

and the Momentum is only a float as the vector is created according to the position of the actor and the impact point of origin.

HealDamage

While I probably would have used TakeDamage with a negative amount to heal damage, 

there is a specific function for that (whose default implementation does nothing). 

The upside of the HealDamage function is that it requires much less information.

 The only parameters are the amount of healing, the  healer’s controller, 

and the “Damage” Type (if for instance you can heal life and armor separately).

Example: Spinning box

Here’s a very simple example of implementation of damage taking. I have a box that when shot at starts to spin.

 The more damage it takes, the faster it turns. The rotation speed decreases overt time, so if I stop shooting,

 it will eventually stop. Nothing exciting, although I can imagine this kind of mechanic used for weapon activated switches.

Here’s the code:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
classSandboxSpinningBox extendsActor placeable;
 
varfloat RotatingSpeed;
varfloat MaxSpeed;
varfloat SpeedFade;
 
autostate Spinning
{
    eventTakeDamage(intDamageAmount, Controller EventInstigator, vectorHitLocation, 
vectorMomentum, classDamageType, optionalTraceHitInfo HitInfo, optionalActor DamageCauser)
    {
        super.TakeDamage(DamageAmount,EventInstigator, HitLocation,Momentum,DamageType,HitInfo,DamageCauser);
 
        WorldInfo.Game.Broadcast(self,"Damage Taken:"@DamageAmount); RotatingSpeed += DamageAmount*100;
    }
 
    eventTick(floatDeltaTime)
    {
        localRotator final_rot;
        final_rot = Rotation;
 
        RotatingSpeed = FMax(RotatingSpeed - SpeedFade* DeltaTime,0);
        final_rot.Yaw = final_rot.Yaw + RotatingSpeed*DeltaTime;
        SetRotation(final_rot);
    }
}
 
DefaultProperties
{
    BeginObject Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
        bEnabled=TRUE
    EndObject
 
    LightEnvironment=MyLightEnvironment
 
    Components.Add(MyLightEnvironment)
 
    beginobject Class=StaticMeshComponent Name=BaseMesh
        StaticMesh=StaticMesh'EngineMeshes.Cube'
        LightEnvironment=MyLightEnvironment
    endobject
    Components.Add(BaseMesh)
    CollisionComponent=BaseMesh
 
    bCollideActors=true
    bBlockActors=true
 
    RotatingSpeed=0.0
    SpeedFade=5000.0
    MaxSpeed=10000
}



And here’s the result:

Pawns taking damage

Pawns being a bit more complex Actors, the Pawn class has some additional stuff to do in order to deal with damage.

 It has a basic implementation of an Health System. Default Health value (and max health) is 100. 

The Pawn class overrides the TakeDamage function. It does mainly physics stuff, but here are a few things we should keep in mind:

  • It ignores negative damage amount (considers it as 0). So don’t try to heal someone this way.
  • It lets lots of things adjust the amount of damage actually taken. In order:
    • The driven vehicle (if there is any)
    • The GameInfo (mostly to give the mutators a chance to affect damage (InstaGib, for instance))
    • The pawn itself, through the AdjustDamage function (in case the pawn has some inventory items that can affect its resistance to damage).

After the actual damage has been taken, it is subtracted from the pawn’s health. If it doesn’t reach 0,

 things that need to know that the pawn has been hit (the driven vehicle and the pawn’s controller) are notified. 

Pawns also store which Controller was the last to hit them (to figure out who got the frag).

If the Health is down to 0, it means (at least in the default implementation) that the pawn is dead.

 The funeral process starts by calling the Died function, which does lots of stuff. Mainly cleanly destroying the pawn,

 triggering Kismet events, sending obituaries to whoever cared about the poor chap:

  • Its weapon via Weapon.HolderDied (so it stops firing).
  •  If the weapon must be dropped, ThrowWeaponOnDeath is called right after.
  • The vehicle he was driving via DrivenVehicle.DriverDied.
  • The GameInfo via GameInfo.Killed (e.g.: if that was the hostage you were supposed to save).
  • Its inventory manager via InvManager.OwnerDied (e.g.: dropping loot).

Then it calls PlayDying which will send the Pawn in the Dying state, where dying anims, sounds and ragdolls should be handled.

NOTE: Although it doesn’t seem to be used anywhere, 

it’s good to know that the Pawn class defines a function called TakeRadiusDamageOnBones. 

Quite self explanatory, and proably quite useful for localized damage.

Below is a small video where I trace the damage taken by a pawn.

Inflicting damage

Basics

Inflicting damage, in its most basic form, is actually plain simple.

 All you need to do is to call the soon-to-be damaged actor’s TakeDamage function (or TakeRadiusDamage).

 The less plain simple part is finding all the information we need pass to the function. Let’s have a look at how it works in the case of weapons.

Instant hit shots

We’ve seen in the tutorial on weapons that the InstantFire function makes a Trace to find everything the shot has hit, 

put the information together in an ImpactInfo structure, and then calls ProcessInstantHit for every ImpactInfo it created.

The defaultImplementation of ProcessInstantHit does a few things:

  1. It computes the total damage (base damage * number of hits).
  2. It checks if the hit actor is a static mesh that can become dynamic (one of the engine’s quite recent features).
  3. Regardless of the outcome of the previous step, it calls the hit actor’s TakeDamage.

Looking at the code, we see it’s using some firemode dependant arrays:

  • InstantHitDamage: The amount of damage for the fire mode.
  • InstantHitMomentum: The force applied by a shot of this fire mode upon impact. Note that these are floats that are multiplied by the hit’s direction.
  • InstantHiDamageTypes: The DamageType class for the given fire mode.

Why isn’t there similar arrays for projectiles? Simply because this information is stored within the projectile instance.

Inflict Radius Damage

There’s also an application a function for that. It’s called HurtRadius, 

and is a member of the Actor class. Meaning that as well as taking damage,

 pretty much everything you place in the game can potentially inflict radius damage. The function takes the following parameters:

  • float BaseDamage: In default implementation, it’s the damage at the epicentre of the “explosion”.
  • float DamageRadius: Radius of the “explosion” in Unreal Units.
  • class<DamageType> DamageType: you should know what that is by now.
  • float Momentum: same for that one.
  • vector HurtOrigin: The point of origin of the “explosion”. You’ll most likely want to use the actor’s location.
  • optional Actor IgnoredActor: an actor that should ignore the effects of the “explosion”.
  • optional Controller InstigatedByController: who started to blow things up.
  • optional bool bDoFullDamage: you know that one from TakeRadiusDamage.

The default implementation of HurtRadius gets all actors in the radius, and for each eligible actor, it calls its TakeRadiusDamage.

“But what the hell makes an actor eligible?” Well, firstly, not being the explosion’s instigator.

 Secondly, not being World Geometry. Thirdly, and most importantly, having the bCanBeDamaged property set to true.

 We haven’t heard about that one before, because it’s only tested in HurtRadius. 

For consistency’s sake, it might be a good idea to check its value in TakeDamage as well.

In the event of one of the actors caught in the radius damage triggers itself radius damage, 

a variable (bHurtEntry) prevents an actor from being affected more than once by one chain reaction.

 It’s important to keep that if you override the function, as all this is done in the same frame.

Example of Radius Damage

I’ve set up a simple actor that goes from green to red according to the damage it has taken. and when it “dies”, it damages actors around it.

Here’s the actor’s code:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
classSandboxExplodingCylinder extendsActor placeable;
 
varfloat Health;
varfloat MaxHealth;
 
varStaticMeshComponent ColorMesh;
 
varMaterialInstanceConstant mat;
 
simulatedfunction PostBeginPlay()
{
    mat = ColorMesh.CreateAndSetMaterialInstanceConstant(0);
}
 
eventTakeDamage(intDamageAmount, Controller EventInstigator, vectorHitLocation, vectorMomentum,
classDamageType, optionalTraceHitInfo HitInfo, optionalActor DamageCauser)
{
    super.TakeDamage(DamageAmount,EventInstigator, HitLocation,Momentum,DamageType,HitInfo,DamageCauser);
    Health = FMax(Health-DamageAmount,0);
    WorldInfo.Game.Broadcast(self,Name$": Health:"@Health);
 
    mat.SetScalarParameterValue('Damage',(MaxHealth - Health)/100);
 
    if(Health == 0)
    {
        HurtRadius ( 75,128, DamageType, 0, Location, None, EventInstigator, false);
        GotoState('Dead');
    }
}
 
autostate Alive
{
}
 
stateDead
{
    ignoresTakeDamage,TakeRadiusDamage;
 
begin:
    ColorMesh.SetHidden(true);//Note that it's only hidden, it's still there and colliding.
}
 
DefaultProperties
{
    BeginObject class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
        bEnabled=TRUE
    EndObject
 
    LightEnvironment=MyLightEnvironment
    Components.Add(MyLightEnvironment)
 
    beginobject class=StaticMeshComponent Name=BaseMesh
        StaticMesh=StaticMesh'SandboxContent.Meshes.SM_ExplodingCylinder'
        LightEnvironment=MyLightEnvironment
    endobject
 
    ColorMesh=BaseMesh
    Components.Add(BaseMesh)
    CollisionComponent=BaseMesh
 
    bCanBeDamaged=true
    bCollideActors=true
    bBlockActors=true
    Health=100
    MaxHealth=100
}


And here’s the result:

0 0