The three virtues of a programmer: Laziness, Impatience, and Hubris. – Larry Wall

Legacy:HitscanSplash

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

I was asked by players on a server I frequent to do some balancing to the ZenCodersWeapons2004 weapons pack when I discovered that the weapons in that pack had some really obscene damage values and it was causing issues with players only using those weapons on the server.

One of the requests that was made was to add splash damage to the (hitscan) primary fire mode on the Zen Benelli Shotgun. Since I have yet to see a weapon in UT with this implemented I thought it would be a good experiment to try on my own. I quickly found the Weapon_Mutator_Tutorial where presumably one creates a Minigun weapon that causes splash damage, but found that it didn't work, or at least not well, for my own purposes.

I then went about adapting the InstantFire class from the ShockRifle and using the MinigunHE tutorial as a base to produce a weapon that actually worked for the most part. The most difficult issue I encountered in creating this weapon is that when playing on a dedicated server, the explosive hit-effects I had set up would not appear when (a) a direct hit against enemy player pawns was scored (b) while on a dedicated server.

Although I'd already given the weapon a beam-effect to make the exact positioning of the trace apparent to the player, I didn't think it was appropriate not to have a hit-effect, especially given that the weapon was being specifically made/altered for a dedicated server. After trying many different approaches and completely failing to get the hit-effects to work under the specified conditions (it should be noted that the Shock Rifle also does not display hit-effects under these conditions), I hacked up a workaround by utilizing the emitter from the Lightning Gun. Since the Lightning Gun emitter (NewLightningBolt) always displays a certain portion of the texture it uses (XEffects.LightningBoltT) at both the start and endpoints, I figured if I changed the emitter to utilize a texture with an "explosive" effect at the end I could get at least a nominal "hit-effect" to work when a player gets a dead-on hit on an enemy pawn while playing on a dedicated server. This wasn't an ideal solution but it does work. Someone who is more ambitious about this could probably try a similar approach and tweak the emitter to make a much nicer visual effect than I bothered with.

Here is the finished class for the HitScan portion of the weapon. If you'd like to edit or clean up this code, feel free to. It is provided as-is as a simple example of a working hitscan firemode with a splash damage effect; I'm sure someone who wanted to put more effort into it could get it working better.

//This class is based off of InstantFire, and is in effect a blending of both the Lightning Gun & Shock Rifle's primary fire classes.
 
class BShotgunPrimaryFire extends InstantFire;
 
var() class<xEmitter> HitEmitterClass< SEMI >
var(tweak) float offsetadj;
var() class<ShotgunBeamEffect> BeamEffectClass< SEMI >
var() class<Actor> ExplosionClass< SEMI >
var() class<Projector> ExplosionDecalClass< SEMI >
var float DamageRadius;
var float SplashDamage;
var float ReloadAnimDelay;
 
simulated function SpawnBeamEffect(Vector Start, Rotator Dir, Vector HitLocation, Vector HitNormal, int ReflectNum)
{
    local ShotgunBeamEffect Beam;
 
    Super.SpawnBeamEffect( Start, Dir, HitLocation, HitNormal, ReflectNum);
 
    if (Weapon != None)
    {
        Beam = Weapon.Spawn(BeamEffectClass,,, Start, Dir);
        if (ReflectNum != 0) Beam.Instigator = None; // prevents client side repositioning of beam start
            Beam.AimAt(HitLocation, HitNormal);
    }
 
    Explode( HitLocation, HitNormal );
}
 
simulated function Explode(vector HitLocation, vector HitNormal)
{
    if ( Instigator.EffectIsRelevant(HitLocation,false) )
 
    {
        Spawn(ExplosionClass,,,HitLocation + HitNormal*16,rotator(HitNormal));
    }
 
    BlowUp(HitLocation);
}
 
function BlowUp(vector HitLocation)
{
    Weapon.HurtRadius(SplashDamage, DamageRadius, DamageType, Momentum, HitLocation ); //Splashdamage=55
}
 
function DoTrace(Vector Start, Rotator Dir)
{
    local Vector X,Y,Z, End, HitLocation, HitNormal, RefNormal;
    local Actor Other; //mainArcHitTarget;
    local int Damage;
    local bool bDoReflect;
    local xEmitter hitEmitter;
    local class<Actor> tmpHitEmitClass< SEMI >
    local float tmpTraceRange;
    local vector arcEnd; //mainArcHit;
    local vector EffectOffset;
    local int ReflectNum;
 
    ReflectNum=0;
 
    if ( class'PlayerController'.Default.bSmallWeapons )
        EffectOffset = Weapon.SmallEffectOffset;
    else
        EffectOffset = Weapon.EffectOffset;
 
    Weapon.GetViewAxes(X, Y, Z);
    if ( Weapon.WeaponCentered() )
        arcEnd = (Instigator.Location +
            EffectOffset.Z * Z);
    else if ( Weapon.Hand == 0 )
    {
        if ( class'PlayerController'.Default.bSmallWeapons )
            arcEnd = (Instigator.Location +
                EffectOffset.X * X);
        else
            arcEnd = (Instigator.Location +
                EffectOffset.X * X
                - 0.5 * EffectOffset.Z * Z);
    }
    else
        arcEnd = (Instigator.Location +
            Instigator.CalcDrawOffset(Weapon) +
            EffectOffset.X * X +
            Weapon.Hand * EffectOffset.Y * Y +
            EffectOffset.Z * Z);
 
    tmpHitEmitClass = HitEmitterClass< SEMI >
    tmpTraceRange = TraceRange;
 
    while (true)
    {
        bDoReflect = false;
        X = Vector(Dir);
        End = Start + TraceRange * X;
 
        Other = Trace(HitLocation, HitNormal, End, Start, true);
 
        if ( Other != None && (Other != Instigator || ReflectNum > 0) )
        {
            if (bReflective && Other.IsA('xPawn') && xPawn(Other).CheckReflect(HitLocation, RefNormal, DamageMin*0.25))
            {
                bDoReflect = false;
                HitNormal = Vect(0,0,0);
            }
            else if (!Other.bWorldGeometry)
            {
                Damage = (DamageMin + Rand(DamageMax - DamageMin)) * DamageAtten;
                Other.TakeDamage(Damage, Instigator, HitLocation, Momentum*X, DamageType); //Hit
                HitNormal = Vect(0,0,0);
            }
            else if ( WeaponAttachment(Weapon.ThirdPersonActor) != None )
				WeaponAttachment(Weapon.ThirdPersonActor).UpdateHit(Other,HitLocation,HitNormal);
        }
        else
        {
            HitLocation = End;
            HitNormal = Vect(0,0,0);
        }
 
        SpawnBeamEffect(Start, Dir, HitLocation, HitNormal, ReflectNum);
 
        if ( Weapon == None )
            return;
        hitEmitter = xEmitter(Weapon.Spawn(tmpHitEmitClass,,, arcEnd, Rotator(HitNormal)));
        if ( hitEmitter != None )
            hitEmitter.mSpawnVecA = HitLocation;
        if ( HitScanBlockingVolume(Other) != None )
            return;
 
        else
        {
            break;
        }
    }
}
 
//This function is not critical to the functioning of the splash damage on the hitscan weapon
function PlayFiring()
{
    Super.PlayFiring();
 
    if ( Weapon.AmmoAmount(0) > 0 )
        Weapon.PlayOwnedSound(Sound'WeaponSounds.BReload9', SLOT_Misc,,,,1.1,false);
 
    if (Weapon.HasAnim(ReloadAnim))
    {
	   SetTimer(ReloadAnimDelay, false);
    }
}
 
 
//This function is not critical to the functioning of the splash damage on the hitscan weapon
function Timer()
{
	if (Weapon.ClientState == WS_ReadyToFire && Instigator !=None && UnrealPlayer(Weapon.Instigator.controller) != None)
	{
	    if (UnrealPlayer(Weapon.Instigator.controller) != None && level.TimeSeconds - UnrealPlayer(Weapon.Instigator.controller).LastKillTime < 1)
	    {
    		Weapon.PlayAnim(ReloadAnim, ReloadAnimRate, TweenTime);
    		ClientPlayForceFeedback(ReloadForce);
	    }
	}
}
 
defaultproperties
{
    AmmoClass=Class'BenelliAmmo'
    DamageType=Class'DamTypeShotgunShell'
    HitEmitterClass=class'NewShotgunBolt'
    FireSound=Sound'ZWeaponsSnd.m3-1'
    FireForce="NewSniperShot"  // jdf
 
    BeamEffectClass=class'ShotgunBeamEffect' //Tracer beam created along the trace path
    ExplosionClass=class'ONSGrenadeExplosionEffect'
    ExplosionDecalClass=class'RocketMark'
 
    DamageMin=8
    DamageMax=10
 
    SplashDamage=55
    DamageRadius=192.000 //Values smaller than roughly 96 here may not be noticeable in game.
 
    AmmoPerFire=1
    TraceRange=10240
    Momentum=+30000.000
    FireRate=1.0
    FireAnimRate=1.25
 
    BotRefireRate=0.4
    AimError=850
    WarnTargetPct=+0.5
 
    ShakeOffsetMag=(X=-15.0,Y=0.0,Z=10.0)
    ShakeOffsetRate=(X=-4000.0,Y=0.0,Z=4000.0)
    ShakeOffsetTime=3.2
    ShakeRotMag=(X=0.5,Y=0.0,Z=0.0)
    ShakeRotRate=(X=-4000.0,Y=0.0,Z=0.0)
    ShakeRotTime=2
 
    ReloadAnimDelay=0.400000
    ReloadAnim="AltFire"
    ReloadAnimRate=0.700000
}

The primary fire class also references the following class, which is a modified ShockRifleBeam class. This class is responsible for creating the "explosion" effect visible where the primary fire trace hits. (Note that the explosion effect does not seem to spawn if the primary fire scores a hit on a pawn.)

class ShotgunBeamEffect extends ShockBeamEffect;
 
simulated function SpawnImpactEffects(rotator HitRot, vector EffectLoc)
{
    Spawn(class'RocketMark',,, EffectLoc, Rotator(-HitNormal));
    Spawn(class'ONSGrenadeExplosionEffect',,, EffectLoc, Rotator(-HitNormal));
    Spawn(class'RocketSmokeRing',,, EffectLoc, Rotator(HitNormal));
}
 
simulated function SpawnEffects()
{
    local xWeaponAttachment Attachment;
 
    if (Instigator != None)
    {
        if ( Instigator.IsFirstPerson() )
        {
            if ( (Instigator.Weapon != None) && (Instigator.Weapon.Instigator == Instigator) )
                SetLocation(Instigator.Weapon.GetEffectStart());
            else
                SetLocation(Instigator.Location);
            Spawn(MuzFlashClass,,, Location);
        }
        else
        {
            Attachment = xPawn(Instigator).WeaponAttachment;
            if (Attachment != None && (Level.TimeSeconds - Attachment.LastRenderTime) < 1)
                SetLocation(Attachment.GetTipLocation());
            else
                SetLocation(Instigator.Location + Instigator.EyeHeight*Vect(0,0,1) + Normal(mSpawnVecA - Instigator.Location) * 25.0);
            Spawn(MuzFlash3Class);
        }
    }
 
    if ( EffectIsRelevant(mSpawnVecA + HitNormal*2,false) && (HitNormal != Vect(0,0,0)) )
        SpawnImpactEffects(Rotator(HitNormal),mSpawnVecA + HitNormal*2);
}
 
defaultproperties
{
    RemoteRole=ROLE_SimulatedProxy
    bReplicateInstigator=true
    bReplicateMovement=false
    bNetTemporary=true
    LifeSpan=0.75
    NetPriority=3.0
 
    mParticleType=PT_Beam
    mStartParticles=1
    mAttenKa=0.1
    mSizeRange(0)=18.0 //24
    mSizeRange(1)=36.0 //48
    mRegenDist=150.0
    mLifeRange(0)=0.75
    mColorRange(0)=(B=0)
    mColorRange(1)=(B=0)
    mMaxParticles=3
 
    CoilClass=none
    MuzFlashClass=none
    MuzFlash3Class=none
    Texture=Texture'ShockBeamTex'
    Skins(0)=Texture'ShockBeamTex'
    Style=STY_Additive
    bUnlit=true
}

-'Truncated for now. Going to think about how to link this together with the MinigunHE stuff, since the aim of my project here and the MinigunHE are very similar.'-

MythOpus: Doesn't the flak cannon and rocket launcher of splash damage?

Wail: I don't understand your question. The Flak Cannon and Rocket Launcher have splash damage, but neither of those weapons have hitscan firing modes (Enforcers/ARs, Shock primary, Minigun, Sniper/LtG). This code is intended to allow you to have an Area-of-Effect/Splash-Damage explosion at the point of impact of an instant-hit trace.

MythOpus: Thanks for clearing that up.  :)