UDK – Projectiles

Projectiles are little more than Actors that are spawned dynamically, 

move around for a certain amount of time and then die, possibly inflicting damage.

 In UT3 for instance, the Shock Rifle’s Shock Ball, or the Link Gun’s plasma ball are projectiles.

In today’s lesson, we will discuss when it’s best to use projectiles,

 we’ll have a look at collision in general (as projectiles obviously rely heavily on it),

 and see how projectiles do their job.

If you haven’t already done so, I strongly recommend you read the bit on Damage Dealing.

Projectiles vs Instant Hit

I think we can agree on the fact that mechanically speaking,

 the vast majority of weapons we can find in a game is using a projectile of some sort (bullet, arrow, plasma ball, etc). 

However, since assault rifle bullets are so fast, is worth making an object for them, handling its movement and all? Propably not.

That’s why game programmers had the idea of the instant hit shot (sometimes called Trace shot) 

which simply draws a straight line from the weapon, and affect the first object it touched. This method has

 been around since, well the dawn of FPSs. As a rule of thumb, if the projectile shot by your weapon is too 

fast to be seen by the player, you’ll probably want to use an Instant Hit fire type. UT3′s Minigun and assault rifles work like that.

That said, there might be cases where you still want to use projectiles. In games that claim to have realistic ballistics (e.g.: ArmA 2, Battlefield series), 

projectiles are actual projectiles, even if you don’t see them. 

This allows for the projectile to actually travel across the level over time,

 possibly being affected by things such as gravity along the way.

As far as Unreal Engine 3 is concerned, a projectile does not need a mesh to work.

 Only a CollisionComponent.

Also, being a subclass of Actor, Projectiles can potentially take damage, 

enabling interesting gameplay (e.g: Unreal’s Shock Combo). And by default,

 bCanBeDamaged is set to true for projectiles (although TakeDamage is not overriden, so it won’t do anything actually).

Projectile Class

A Projectile is an Actor which defines a few additional properties regarding the following topics:

  • Motion
  • Damage
  • Collision

We’ll talk about collision more extensively later in this page.

Motion
  • float Speed: in UU/s
  • float MaxSpeed: in UU/s

The Actor class already handles linear movement through the Velocity property. 

All we need to do to put our projectile in motion is to set this variable.

 The Actor class also defines a LifeSpan property, holding the time after which the Actor dies.

 Projectiles use LifeSpan and MaxSpeed to determine their maximum range.

UDKProjectile provides a variable that allows the projectile to have an accelaration (AccelRate, needs to be implemented).

 It also provides and a few variables that will let you implement guided projectiles. 

The code chunk below is from UDKProjectile.uc, UDK October 2010:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//=======================================================
// Seeking Projectile Support
 
/** Currently tracked target - if set, projectile will seek it */
var actor SeekTarget;
/** Tracking strength for normal targets.*/
var float BaseTrackingStrength;
/** Tracking strength for vehicle targets with bHomingTarget=true */
var float HomingTrackingStrength;
/** Initial direction of seeking projectile */
var vector InitialDir;
/** The last time a lock message was sent (to vehicle with bHomingTarget=true) */
var float LastLockWarningTime;
/** How long before re-sending the next Lock On message update */
var float LockWarningInterval;

Damage
  • float Damage
  • float DamageRadius
  • float MomentumTransfer
  • class<DamageType> MyDamageType

You should know what all of these are for.

Firing a Projectile

While spawning the projectile itself is in essence quite easy, 

spawning the projectile at the right place and in the right direction is actually more complex than it sounds. 

Let’s have a look at how the Weapon.ProjectileFire() function work.

First and foremost, there is an obvious but capital concept one needs to keep in mind: In almost every situation,

 the line of sight and the projectile’s trajectory are NOT aligned (or collinear, for maths people). 

Even if they do share the same end point (the hit location), the starting points are very likely to be different.

For the line of sight, it’s usually the player point of view, and for the trajectory, 

it’s wherever the projectile is supposed to come out of (like the gun’s barrel).

The default implementation of ProjectileFire() is trying to make sure the point I’m aiming at is where my projectile is headed. 

This is summarized by the figure below (behold my amazing drawing skills!):

weapon_trace

The [StartTrace,EndTrace] vector being our actual aim, 

we need to find the right orientation from RealStartLoc to make sure the projectile is headed to EndTrace (doesn’t mean it necessarily will, 

especially if external factors affect the projectile’s motion).

For the record, a real life, bullet-based firearm behaves like this (the drawing is obviously exaggerated):

real_weapon_ballistics

Here’s how ProjectileFire() retrieves the required information:

StartTrace

It is retrevied by calling Pawn.GetWeaponStartTraceLocation(). 

That is asking the Pawn, which will in turn ask its controller (Controller.GetPlayerViewpoint()). 

That’s because depending on who controls the Pawn (AI or Player), the StartTrace won’t be the same.

For the player, it’s usually the position of the camera.

 However that might not be a valid position in cases such as top-down perspective games. 

Just override the function to specify an appropriate start point.

NOTE: This is also done in the case of an Instant Fire.

AimDir

Retrieved by the GetAdjustedAim(StartTrace) function, which asks for the pawn’s aim rotation, 

which in turns lets the controller modify the aim. Then spread is added, and we have our AimDir.

RealStartLoc

Retrieved by the GetPhysicalFireStartLoc(AimDir). Default implementation is native, 

so I can’t really tell what it returns. You’ll have to override it anyway.

EndTrace

Now, if StartTrace and RealStartLoc are different (and they probably will be), 

we need to adjust AimDir so it will send the projectile where we aimed. to do that, 

we need to know where we are supposed to hit.

 We doi it by getting EndTrace (calculated from translating StartTrace along AimDir over a distance equal to the weapon’s range.

 With that, we can start a trace that will tell us what’s the point of impact. Then, AimDir is adujsted according to that.

Once all this is done, when can spawn and activate the projectile. The hard part in all that is to find the right RealStartLoc.

Example

Simple projectile with mesh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SandboxPaintballProjectile extends UDKProjectile;
 
DefaultProperties
{
     Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
         bEnabled=TRUE
     End Object
     Components.Add(MyLightEnvironment)
 
     begin object class =StaticMeshComponent Name=BaseMesh
         StaticMesh=StaticMesh 'SandboxContent.Meshes.SM_PaintBall'
         LightEnvironment=MyLightEnvironment
     end object
 
     Components.Add(BaseMesh)
}

Here is the weapon’s relevant code. I’m using the paintball gun I made in the   weapons tutorial  :
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
class SandboxPaintBallGun extends UDKWeapon;
 
simulated event vector GetPhysicalFireStartLoc( optional vector AimDir)
{
     local SkeletalMeshComponent compo;
     local SkeletalMeshSocket socket;
 
     compo = SkeletalMeshComponent(Mesh);
 
     if (compo != none )
     {
         socket = compo.GetSocketByName( 'MussleFlashSocket' );
 
         if (socket != none )
         {
             return compo.GetBoneLocation(socket.BoneName);
         }
     }
}
 
DefaultProperties
{
     FiringStatesArray( 1 )=WeaponFiring
     WeaponFireTypes( 1 )=EWFT_Projectile
     WeaponProjectiles( 1 )= class 'Sandbox.SandboxPaintballProjectile'
     FireInterval( 1 )= 0.01
     Spread( 1 ) = 0
}

And that’s what we get:

Projectile collision

When a projectile hits something, it can trigger two different events: Touch, 

and Hitwall (both defined in the Actor class). If the touched Actor has bWorldGeometry set to true, 

the collision will trigger the HitWall event. Otherwise, the Touch event will be triggered.

The other difference between two events is that HitWall does not provide the hit location, only the normal.

The Projectile class already defines a CylinderComponent as a Collision for the projectile,

 but with a radius and height to 0, meaning the collision is reduced to a point. You might want to override that.

IMPORTANT NOTE: Projectile’s Touch and Hitwall events are specified as singular meaning they can’t be called recursively. 

As a consequence, you won’t be able to call super.Touch nor super.HitWall (the compiler won’t let you).

That means that if you need to override one of these event, you must copy/paste any relevant code from the parent class’ implementation.

Touch Event

Does some network related checks. If those tests are successful,

 it calls the ProcessTouch() function whose default implentation calls the Explode() function if the actor touched is not the projectile’s instigator.

Hitwall Event

The default implementation checks if the hit actor is a static mesh that can become dynamic, 

and if so activates it. Then, regardless of that outcome, it calls the actor’s TakeDamage(), then calls Explode().

Explode() Function

Calls ProjectileHurtRadius() (which is a wrapper to HurtRadius() that does dodgy things to ensure a maximum of actors get hit by the projectile).

 Obviously, if your actor isn’t meant to do any radius damage, 

you can simply override the function and just create your own function and call it instead of Explode.

Example

I’ve overridden the HitWall event to make the projectile bounce on world geometry, deals some damage on the hit actor, and place a decal):


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
class SandboxPaintballProjectile extends UDKProjectile;
 
simulated function ProcessTouch(Actor Other, Vector HitLocation, Vector HitNormal)
{
     if ( Other != Instigator )
     {
         WorldInfo.MyDecalManager.SpawnDecal ( DecalMaterial 'SandboxContent.Materials.DM_Paintball_Splash' ,
HitLocation, rotator (-HitNormal), 128 , 128 , 256 , false , FRand() * 360 , none );

         Other.TakeDamage( Damage, InstigatorController, Location, MomentumTransfer * Normal(Velocity), MyDamageType,, self );
         Destroy();
     }
}
 
simulated event HitWall( vector HitNormal, actor Wall, PrimitiveComponent WallComp)
{
     Velocity = MirrorVectorByNormal(Velocity,HitNormal); //That's the bounce
     SetRotation(Rotator(Velocity));
     TriggerEventClass( class 'SeqEvent_HitWall' , Wall);
}
 
DefaultProperties
{
     Begin Object Name=CollisionCylinder
         CollisionRadius= 8
         CollisionHeight= 16
     End Object
 
     Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
         bEnabled=TRUE
     End Object
 
     Components.Add(MyLightEnvironment)
 
     begin object class =StaticMeshComponent Name=BaseMesh
         StaticMesh=StaticMesh 'SandboxContent.Meshes.SM_PaintBall'
         LightEnvironment=MyLightEnvironment
     end object
 
     Components.Add(BaseMesh)
 
     Damage= 25
     MomentumTransfer= 10
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值