363 lines
13 KiB
Ucode
363 lines
13 KiB
Ucode
//=============================================================================
|
|
// KFImpactEffectManager
|
|
//=============================================================================
|
|
// Manages impact effects
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
// - Andrew "Strago" Ladenberger
|
|
//=============================================================================
|
|
|
|
class KFImpactEffectManager extends Actor
|
|
native(Effect)
|
|
config(Game);
|
|
|
|
/** Information about hit effects for this class */
|
|
var KFImpactEffectInfo DefaultImpactEffects;
|
|
|
|
var float MaxImpactEffectDistance;
|
|
var float MaxDecalRangeSq;
|
|
|
|
var bool bAlignToSurfaceNormal;
|
|
var bool bSuppressSounds;
|
|
|
|
/** Decal Limits */
|
|
var globalconfig int MaxImpactEffectDecals;
|
|
|
|
/** Decal manager */
|
|
var transient DecalManager ImpactEffectDecalManager;
|
|
|
|
/** Data for most recent impact sound played (used to limit sounds) */
|
|
var transient AkEvent MostRecentImpactSound;
|
|
var transient vector MostRecentImpactLocation;
|
|
var transient float MostRecentImpactTime;
|
|
var transient int NumDuplicateImpactSounds;
|
|
|
|
/** Proximity within which to limit impact sounds */
|
|
var float MinRepeatImpactSoundDistanceSq;
|
|
/** Duration within which to limit impact sounds */
|
|
var float MinRepeatImpactSoundInterval;
|
|
/** Number of duplicate sounds to allow within time period and proximity */
|
|
var int MaxDuplicateImpactSounds;
|
|
|
|
event PostBeginPlay()
|
|
{
|
|
Super.PostBeginPlay();
|
|
|
|
// Create decal managers
|
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
|
{
|
|
// Wound decal manager
|
|
ImpactEffectDecalManager = Spawn(class'DecalManager', self,, vect(0,0,0), rot(0,0,0));
|
|
ImpactEffectDecalManager.MaxActiveDecals = MaxImpactEffectDecals;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawn any effects that occur at the impact point. It's called from the pawn.
|
|
*/
|
|
simulated function PlayImpactEffects(const vector HitLocation, const Pawn EffectInstigator, optional vector HitNormal, optional KFImpactEffectInfo CustomImpactEffects, optional bool bWorldImpactsOnly)
|
|
{
|
|
local vector NewHitLoc, FireDir, WaterHitNormal;
|
|
local Actor HitActor;
|
|
local TraceHitInfo HitInfo;
|
|
local MaterialImpactEffect ImpactEffect;
|
|
local KFImpactEffectInfo ImpactEffectInfo;
|
|
local KFFracturedMeshActor FracturedMeshActor;
|
|
local int i;
|
|
local bool bIsWeaponHandlingEffects;
|
|
|
|
// allow optional parameter to override impact effects
|
|
ImpactEffectInfo = (CustomImpactEffects != None) ? CustomImpactEffects : DefaultImpactEffects;
|
|
|
|
if( IsZero(HitNormal) && EffectInstigator != none )
|
|
{
|
|
HitNormal = Normal(EffectInstigator.Location - HitLocation);
|
|
}
|
|
FireDir = -1 * HitNormal;
|
|
|
|
if ( ImpactEffectInfo.BulletWhipSnd != None )
|
|
{
|
|
CheckBulletWhip(FireDir, HitLocation, EffectInstigator, ImpactEffectInfo);
|
|
}
|
|
|
|
if ( EffectInstigator != None && ImpactEffectIsRelevant(EffectInstigator, HitLocation, false, MaxImpactEffectDistance) )
|
|
{
|
|
if ( ImpactEffectInfo.bMakeSplash && !WorldInfo.bDropDetail && EffectInstigator.IsPlayerPawn() && EffectInstigator.IsLocallyControlled() )
|
|
{
|
|
HitActor = Trace(NewHitLoc, WaterHitNormal, HitLocation, EffectInstigator.Location+EffectInstigator.eyeheight*vect(0,0,1), true,, HitInfo, TRACEFLAG_PhysicsVolumes | TRACEFLAG_Bullet);
|
|
if ( WaterVolume(HitActor) != None )
|
|
{
|
|
WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffectInfo.SplashEffectTemplate, NewHitLoc, rotator(vect(0,0,1)));
|
|
}
|
|
}
|
|
|
|
bIsWeaponHandlingEffects = KFPawn(EffectInstigator).MyKFWeapon != none ? KFPawn(EffectInstigator).MyKFWeapon.bForceHandleImpacts : false;
|
|
|
|
// Trace using the Instigator as the TraceOwner so that melee weapons don't collide with Instigator
|
|
HitActor = EffectInstigator.Trace(NewHitLoc, HitNormal, (HitLocation - (HitNormal * 32)), HitLocation + (HitNormal * 32), !bWorldImpactsOnly,, HitInfo, TRACEFLAG_Bullet);
|
|
if( HitActor != none && HitActor.bCanBeDamaged && HitActor.IsA('Pawn') && !bIsWeaponHandlingEffects )
|
|
{
|
|
return; // pawns impacts are handled by the pawn (see PlayTakeHitEffects)
|
|
}
|
|
|
|
// handle "partial" remote client simulation for fracture meshes
|
|
if ( WorldInfo.NetMode == NM_Client && HitActor != none && HitActor.bCanBeDamaged && !EffectInstigator.IsLocallyControlled() )
|
|
{
|
|
FracturedMeshActor = KFFracturedMeshActor(HitActor);
|
|
if ( FracturedMeshActor != None )
|
|
{
|
|
FracturedMeshActor.SimulateRemoteHit(HitLocation, HitNormal, HitInfo);
|
|
}
|
|
}
|
|
|
|
// figure out the impact sound to use
|
|
GetImpactEffect(HitInfo.PhysMaterial, ImpactEffect, ImpactEffectInfo);
|
|
|
|
// Allow phys materials to override default impact effects
|
|
if( ImpactEffect == ImpactEffectInfo.DefaultImpactEffect
|
|
&& HitInfo.PhysMaterial != none )
|
|
{
|
|
// Impact effect override
|
|
if( HitInfo.PhysMaterial.ImpactEffect != none )
|
|
{
|
|
ImpactEffect.ParticleTemplate = HitInfo.PhysMaterial.ImpactEffect;
|
|
}
|
|
|
|
// Sound override
|
|
if( HitInfo.PhysMaterial.ImpactSound != none )
|
|
{
|
|
ImpactEffect.Sound = HitInfo.PhysMaterial.ImpactSound;
|
|
}
|
|
}
|
|
|
|
// Pawns handle their own hit effects
|
|
if ( HitActor != None &&
|
|
((Pawn(HitActor) == None || bIsWeaponHandlingEffects ) || Vehicle(HitActor) != None) &&
|
|
AllowImpactEffects(HitActor, HitLocation, HitNormal) )
|
|
{
|
|
if (ImpactEffect.ParticleTemplate != None)
|
|
{
|
|
if (!bAlignToSurfaceNormal)
|
|
{
|
|
HitNormal = normal(FireDir - ( 2 * HitNormal * (FireDir dot HitNormal) ) ) ;
|
|
}
|
|
WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffect.ParticleTemplate, HitLocation, rotator(HitNormal), HitActor);
|
|
}
|
|
|
|
//Spawn any global emitters
|
|
for (i = 0; i < ImpactEffectInfo.GlobalImpactEffectEffects.Length; ++i)
|
|
{
|
|
WorldInfo.MyEmitterPool.SpawnEmitter(ImpactEffectInfo.GlobalImpactEffectEffects[i], HitLocation, rotator(HitNormal), HitActor);
|
|
}
|
|
|
|
if ( !WorldInfo.bDropDetail
|
|
&& (Pawn(HitActor) == None)
|
|
&& (EffectInstigator != none && VSizeSQ(EffectInstigator.Location - HitLocation) < MaxDecalRangeSq)
|
|
&& (((WorldInfo.GetDetailMode() != DM_Low) && !class'Engine'.static.IsSplitScreen()) || (EffectInstigator.IsLocallyControlled()
|
|
&& EffectInstigator.IsHumanControlled())) )
|
|
{
|
|
SpawnImpactDecal(HitLocation, HitNormal, HitActor, ImpactEffect, HitInfo);
|
|
}
|
|
}
|
|
|
|
if( ShouldPlayImpactSound(ImpactEffect.Sound, HitLocation) )
|
|
{
|
|
PlayImpactSound(EffectInstigator, HitLocation, FireDir, HitActor, ImpactEffect.Sound, ImpactEffectInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Determines whether impact sound should be played or not, based on most recent impact */
|
|
simulated function bool ShouldPlayImpactSound( AkBaseSoundObject HitSound, vector HitLocation )
|
|
{
|
|
if( HitSound == none || bSuppressSounds )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// limit the number of duplicate impact effects within a short period of time and close proximity
|
|
if( (HitSound != MostRecentImpactSound) ||
|
|
(VSizeSq(HitLocation - MostRecentImpactLocation) > MinRepeatImpactSoundDistanceSq) ||
|
|
(`TimeSince(MostRecentImpactTime) > MinRepeatImpactSoundInterval) )
|
|
{
|
|
MostRecentImpactSound = AkEvent(HitSound);
|
|
MostRecentImpactLocation = HitLocation;
|
|
MostRecentImpactTime = WorldInfo.TimeSeconds;
|
|
NumDuplicateImpactSounds = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
if( NumDuplicateImpactSounds + 1 <= MaxDuplicateImpactSounds )
|
|
{
|
|
NumDuplicateImpactSounds += 1;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Play sound effect associated with this impact
|
|
*/
|
|
simulated function PlayImpactSound(Pawn EffectInstigator, vector HitLocation, vector FireDir, Actor HitActor, AkBaseSoundObject ImpactSound, KFImpactEffectInfo ImpactEffectInfo)
|
|
{
|
|
local Vehicle V;
|
|
|
|
V = Vehicle(HitActor);
|
|
// if hit a vehicle controlled by the local player, always play it full volume
|
|
if (V != None && V.IsLocallyControlled() && V.IsHumanControlled())
|
|
{
|
|
PlayerController(V.Controller).PlaySoundBase(ImpactSound, true);
|
|
}
|
|
else
|
|
{
|
|
PlaySoundBase(ImpactSound, true,,, HitLocation);
|
|
}
|
|
}
|
|
|
|
simulated function CheckBulletWhip(vector FireDir, vector HitLocation, Pawn EffectInstigator, KFImpactEffectInfo ImpactEffectInfo)
|
|
{
|
|
local KFPlayerController PC;
|
|
|
|
ForEach LocalPlayerControllers(class'KFPlayerController', PC)
|
|
{
|
|
if ( !WorldInfo.GRI.OnSameTeam(Owner,PC) )
|
|
{
|
|
PC.CheckBulletWhip(ImpactEffectInfo.BulletWhipSnd, EffectInstigator.Location, FireDir, HitLocation, EffectInstigator);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play sound effect associated with this impact
|
|
*/
|
|
simulated function SpawnImpactDecal(vector HitLocation, vector HitNormal, Actor HitActor, const out MaterialImpactEffect ImpactEffect, optional TraceHitInfo HitInfo )
|
|
{
|
|
local MaterialInterface MI;
|
|
local MaterialInstanceTimeVarying MITV_Decal;
|
|
local int DecalMaterialsLength;
|
|
local float DecalSize, DecalThickness;
|
|
|
|
// if we have a decal to spawn on impact
|
|
DecalMaterialsLength = ImpactEffect.DecalMaterials.length;
|
|
if( DecalMaterialsLength > 0 )
|
|
{
|
|
MI = ImpactEffect.DecalMaterials[Rand(DecalMaterialsLength)];
|
|
if( MI != None )
|
|
{
|
|
DecalSize = RandRange(ImpactEffect.DecalMinSize, ImpactEffect.DecalMaxSize);
|
|
if ( ShouldExtendDecalThickness(HitActor) )
|
|
{
|
|
DecalThickness = DecalSize * 2.2f;
|
|
}
|
|
else
|
|
{
|
|
DecalThickness = 10.f;
|
|
}
|
|
|
|
if( MaterialInstanceTimeVarying(MI) != none )
|
|
{
|
|
// hack, since they don't show up on terrain anyway
|
|
if ( Terrain(HitActor) == None )
|
|
{
|
|
MITV_Decal = new(self) class'MaterialInstanceTimeVarying';
|
|
MITV_Decal.SetParent( MI );
|
|
|
|
ImpactEffectDecalManager.SpawnDecal( MITV_Decal, HitLocation, rotator(-HitNormal), DecalSize,
|
|
DecalSize, DecalThickness, false, (ImpactEffect.bNoDecalRotation) ? 0.f : (FRand() * 360.0), HitInfo.HitComponent, true, false, HitInfo.BoneName, HitInfo.Item, HitInfo.LevelIndex );
|
|
//here we need to see if we are an MITV and then set the burn out times to occur
|
|
MITV_Decal.SetScalarStartTime( ImpactEffect.DecalDissolveParamName, ImpactEffect.DecalDuration );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImpactEffectDecalManager.SpawnDecal( MI, HitLocation, rotator(-HitNormal), DecalSize,
|
|
DecalSize, DecalThickness, false, (ImpactEffect.bNoDecalRotation) ? 0.f : (FRand() * 360.0), HitInfo.HitComponent, true, false, HitInfo.BoneName, HitInfo.Item, HitInfo.LevelIndex, ImpactEffect.DecalDuration );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On landscape with certain non-uniform scale (e.g. Zed Landing) decals have clipping
|
|
* issues. Increasing the thickness, and therefore bounds, resolves the issue at a
|
|
* small perf cost.
|
|
*/
|
|
static function bool ShouldExtendDecalThickness(Actor HitActor)
|
|
{
|
|
// using actor flags to reject most actors before casting
|
|
if ( HitActor != None && !HitActor.bGameRelevant && HitActor.bStatic && HitActor.IsA('Landscape') )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the impact sound that should be used for hits on the given physical material
|
|
* UnrealScript returns structs by value, so use an out param instead
|
|
*/
|
|
static simulated function GetImpactEffect(PhysicalMaterial HitMaterial, out MaterialImpactEffect out_ImpactEffect, KFImpactEffectInfo ImpactEffectInfo )
|
|
{
|
|
local int i;
|
|
local KFPhysicalMaterialProperty PhysicalProperty;
|
|
|
|
if (HitMaterial != None)
|
|
{
|
|
PhysicalProperty = KFPhysicalMaterialProperty(HitMaterial.GetPhysicalMaterialProperty(class'KFPhysicalMaterialProperty'));
|
|
}
|
|
|
|
// @todo: add alt fire
|
|
if (ImpactEffectInfo != none)
|
|
{
|
|
if (PhysicalProperty != None)
|
|
{
|
|
i = ImpactEffectInfo.ImpactEffects.Find('MaterialType', PhysicalProperty.MaterialType);
|
|
if (i != -1)
|
|
{
|
|
out_ImpactEffect = ImpactEffectInfo.ImpactEffects[i];
|
|
return;
|
|
}
|
|
}
|
|
|
|
out_ImpactEffect = ImpactEffectInfo.DefaultImpactEffect;
|
|
}
|
|
}
|
|
|
|
simulated function bool AllowImpactEffects(Actor HitActor, vector HitLocation, vector HitNormal)
|
|
{
|
|
return !WorldInfo.bDropDetail && (PortalTeleporter(HitActor) == None);
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Melee impacts
|
|
*********************************************************************************************/
|
|
|
|
/**
|
|
* Called by ProcessMeleeHit to spawn effects
|
|
* Network: Local Player and Server
|
|
*/
|
|
simulated function PlayMeleeImpact(ImpactInfo Impact, Pawn EffectInstigator, KFWeapon InstigatorWeap, AkEvent HitWorldSound, AkEvent HitPawnSound, CameraShake Shake)
|
|
{
|
|
// @todo: implement this using per material settings (see PlayImpactEffects)
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
DefaultImpactEffects=KFImpactEffectInfo'FX_Impacts_ARCH.Light_bullet_impact'
|
|
|
|
MaxImpactEffectDistance=4000.0
|
|
bAlignToSurfaceNormal=true
|
|
MaxDecalRangeSQ=400000000.0
|
|
|
|
// Actor
|
|
bTickIsDisabled=true
|
|
|
|
MinRepeatImpactSoundDistanceSq=10000 // 1m
|
|
MinRepeatImpactSoundInterval=0
|
|
MaxDuplicateImpactSounds=0
|
|
}
|