//============================================================================= // KFMeleeHelperBase //============================================================================= // Contains common solutions for melee hit detection and Effects // @note - Uses "within actor" and instigator so that this object can be // associated with either a Pawn or a Weapon. //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFMeleeHelperBase extends Object within Actor dependson(KFAnimNotify_MeleeImpact) native config(Game); /// ABOUT HIT DETECTION /** #1 FOV (aka Area, Cone) Hit Detection * @brief: Optimized method that iterates through pawns and rates them based on * distance to the center of the cone. Highly flexible, works well for single * impact weapons (e.g. pistol whip). Single or multiple targets. * @see: FindVictimByFOV, RateMeleeVictim, TraceMeleeAttackHitZones */ /** #2 Weapon Instant Trace * @brief: Similar to InstantFire used by guns, this is a zero-extent line trace usually to the * center of the screen. Works well for non critical impacts such as world geometry * and ragdolls. Single target only. * @see: DoWeaponInstantTrace */ /** #3 Hitbox Collision Detection * @brief: Advanced type that follows the attack animation with moving hit boxes. Maintains a * list of actors that have been hit which resets with each new attack. Multiple targets. * @see: TickHitboxCollisionDetection, ProcessHitboxCollision */ /** #4 Enemy (aka Direct) Impact * @brief: AI can directly damage their target Enemy after some basic validation. * @see: CheckEnemyImpact */ /** #5 Swipe Hit Detection * @brief: A hybrid of FOV collision and Hitbox collision that can be used to hit multiple * targets over a time interval with. Multiple targets. * @see: DoSwipeImpact */ /********************************************************************************************* * FOV (aka Area, Cone) Hit Detection *********************************************************************************************/ /** The maximum range at which this attack can be used **/ var() const float MaxHitRange; /** Optional dot product check for melee hit detection */ var() const float DefaultFOVCosine; /********************************************************************************************* * Hitbox Collision Detection *********************************************************************************************/ struct native MeleeHitboxInfo { var vector BoneOffset; var vector LastLoc; }; var const array HitboxChain; /** Trace extent used for all hitboxes */ var vector HitboxExtent; /** Distance between each new hitbox */ var float HitboxSpacing; /** Name of the bone to start the hitbox chain from */ var name HitboxBoneName; /** Direction along the bone to create the hitbox chain */ var EAxis HitboxBoneAxis; /** If true, use TRACE_ComplexCollision */ var bool bHitboxCollideComplex; /** If true, only the last hitbox uses extent trace collision */ var bool bOnlyUseHitboxExtentAtHead; /** If true, perform an extra "fasttrace" before damaging a pawn */ var bool bDoHitboxObstructionTrace; /** If true, hitboxes only collide with pawns */ var bool bHitboxPawnsOnly; /** Cached list of actors that have collided with this attack */ var const array ActorsCollidedWith; /********************************************************************************************* * Impact effects *********************************************************************************************/ /** This is the camera shake that we play when we hit a melee attack **/ var() const CameraShake MeleeVictimCamShake; /** This is the camera shake that we play when we hit a melee attack **/ var() const CameraShake MeleeImpactCamShake; /** Content references for this weapon's impact effects */ var KFImpactEffectInfo WorldImpactEffects; /** If TRUE, meleeing a FSM with this weapons will cause fracture. */ var bool bAllowMeleeToFracture; /** If TRUE, this attack animation already hit something */ var bool bHasAlreadyHit; /********************************************************************************************* * Debug ********************************************************************************************* */ // `log conditions. More efficient than using log tags, because the msg is not evaluated. var bool bLogMelee; var bool bDebugShowCollision; /********************************************************************************************* * C++ *********************************************************************************************/ cpptext { } /** * Returns the location that should be the start of traces for MeleeAttackImpact() */ simulated function vector GetMeleeStartTraceLocation() { // use eye location instead of GetWeaponStartTraceLocation so it works in third person return Instigator.Location + vect(0,0,1) * Instigator.BaseEyeHeight; } /** * GetAdjustedAim begins a chain of function class that allows the weapon, the pawn and the controller to make * on the fly adjustments to where this weapon is pointing. */ simulated function Rotator GetMeleeAimRotation() { local rotator R; // Start the chain, see Pawn.GetAdjustedAimFor() if( Instigator != None ) { R = Instigator.GetBaseAimRotation(); } return R; } native function SetMeleeRange( float NewRange ); /** * Returns the default melee attack hit detection range for this helper */ simulated function float GetMeleeRange() { return MaxHitRange; } /** * Separate check for destructible objects, fixes issues with notifies/delays/etc. Only * attack an object once per swing, etc. */ function bool MeleeAttackDestructibles() { } /** Use the currently equipped weapon to perform a InstantFire() like line trace */ function DoWeaponInstantTrace(vector StartTrace, vector EndTrace, optional out ImpactInfo out_Impact) { if ( Instigator != None && Instigator.Weapon != None ) { // @note: the return has not been processed for hit zones, but we're // only using this for simplified world traces so it works. out_Impact = Instigator.Weapon.CalcWeaponFire(StartTrace, EndTrace); } } /********************************************************************************************* * FOV (aka Area, Cone) Hit Detection *********************************************************************************************/ /** Custom trace that excludes pawns, but not other actors. Useful for destructibles. */ native function Actor TraceNoPawns(out vector HitLocation, out vector HitNormal, vector TraceEnd, vector TraceStart); /** Returns a victim used to apply melee damage to */ simulated function Pawn FindVictimByFOV(vector StartTrace, vector EndTrace, optional float Range=MaxHitRange, optional float FOVCosine=DefaultFOVCosine) { local Pawn P, BestVictim; local float NewRating, BestRating; BestRating = 0.f; // @perf - AllPawns is ~ 10X faster than VisibleCollidingActors, 10X TraceActors, 2X CollidingActors // - Use AllPawns when we only need pawn collision (fixed cost at any range) // - Use CollidingActors when we need to hit pawns and/or world objects // - Use TraceActors sparingly! (may give better results than CollidingActors) // - Avoid VisibleCollidingActors (RateMeleeVictim handles this) foreach WorldInfo.AllPawns( class'Pawn', P, StartTrace, Range ) { NewRating = RateMeleeVictim(P, StartTrace, EndTrace, Range, FOVCosine, BestRating); if ( NewRating > BestRating ) { BestVictim = P; BestRating = NewRating; } } if( bLogMelee ) { DrawDebugCone(StartTrace, EndTrace - StartTrace, Range, Acos(FOVCosine), Acos(FOVCosine), 16, MakeColor(64,64,64,255), TRUE); } return BestVictim; } /** Returns a victim used to apply melee damage to */ simulated function float RateMeleeVictim(Pawn Victim, vector StartTrace, vector EndTrace, float Range, float FOVCosine, optional float RatingToBeat) { local float VictimRating; local vector VictimLocation, DirToVictim, AimDir; // First make sure Pawn is relevant (ie not dead) if ( Victim == Instigator || Victim.bDeleteMe || Victim.bTearOff ) return -1.f; // If we're player-controlled, filter out teammates if( Instigator.IsHumanControlled() && Victim.GetTeamNum() == Instigator.GetTeamNum() ) { return -1.f; } `log("Melee considering:"@Victim, bLogMelee); // Discard if the z-distance is too great VictimLocation = GetMeleeHitTestLocation(Victim); if ( Abs(VictimLocation.Z - Instigator.Location.Z) > Instigator.CylinderComponent.CollisionHeight * 1.5 ) { `log("rejected within vertical melee range", bLogMelee); return -1.f; } // Add the victim's CollisionRadius - Gives better results (closer to CalcWeaponFire). Range += Victim.CylinderComponent.CollisionRadius; // Discard if the best victim is closer VictimRating = (Range * Range) - VSizeSq2D(VictimLocation - StartTrace); if ( VictimRating < RatingToBeat ) { `log("rejected victim with lower rating", bLogMelee); return -1.f; } // If our check FOV is a cone, then perform a simple dot product if( FOVCosine > 0.f ) { AimDir = Normal(EndTrace - StartTrace); DirToVictim = Normal(VictimLocation - StartTrace); // Make sure victim is in FOV if( DirToVictim dot AimDir < FOVCosine ) { `log("rejected:"@Victim@"dot:"@DirToVictim dot AimDir, bLogMelee); return -1.f; } else { `log("accepted:"@Victim@"dot:"@DirToVictim dot AimDir, bLogMelee); } } // Finally trace to make sure there are no obstructions. ie acquiring someone through a wall if ( !FastTrace(VictimLocation, StartTrace) ) { `log("rejected:"@Victim@"melee obstruction: ", bLogMelee); return -1.f; } return VictimRating; } /** * Returns location to use when testing if this pawn was hit by a melee attack. Useful to override, e.g., for * small creatures close to the ground. */ simulated function vector GetMeleeHitTestLocation(Pawn P) { return P.Location; } /** * Brute force method to find a valid ImpactList for this victim. */ simulated function bool TraceMeleeAttackHitZones(Pawn P, vector StartTrace, vector EndTrace, optional out ImpactInfo out_Impact, optional name BoneName) { local array ImpactList; // if we have a bone name (e.g zero-extent hitboxes) we can improve the results by using the bone loc if ( BoneName != '' ) { EndTrace = P.Mesh.GetBoneLocation(BoneName); } // First, trace once along the supplied vector to try and find a hit zone if ( !TraceAllPhysicsAssetInteractions(P.Mesh, EndTrace, StartTrace, ImpactList, vect(0,0,0), true) ) { // As a fallback trace to a generic location on the mesh EndTrace = P.Mesh.GetBoneLocation(class'KFPawn'.default.TorsoBoneName); if ( IsZero(EndTrace) ) { EndTrace = P.Location; } TraceAllPhysicsAssetInteractions(P.Mesh, EndTrace, StartTrace, ImpactList, vect(0,0,0), true); } if ( ImpactList.Length > 0 ) { out_Impact = ImpactList[0]; return true; } return false; } /********************************************************************************************* * Hitbox Collision Detection *********************************************************************************************/ /** Called from NotifyTick to trace along each hitbox */ native function TickHitboxCollisionDetection(SkeletalMeshComponent SkelComp, float DeltaTime); /** Called from Notify to initialize hitbox settings */ native function BeginHitboxCollisionDetection(SkeletalMeshComponent SkelComp); /** Called once to place hitboxes */ native function CreateHitboxChain(SkeletalMeshComponent SkelComp); /** Swaps the hitboxes with a new hitboxchain array */ native function SetHitboxChain( const array NewChain ); /** returns true if a new actor was successfully added */ //native function bool AddHitboxCollidingActor(Actor A); native function bool HasCollidedWithHitbox(Actor A); /** Recieve collision event and register impact with CSHD */ event ProcessHitboxCollision(Actor HitActor, vector StartTrace, vector EndTrace, vector HitLocation, vector HitNormal, const out TraceHitInfo HitInfo, optional out ImpactInfo Impact) { if( HitActor == None ) { return; } // Convert Trace Information to ImpactInfo type. Impact.HitActor = HitActor; Impact.HitLocation = HitLocation; Impact.HitNormal = HitNormal; Impact.RayDir = Normal(EndTrace-StartTrace); Impact.StartTrace = StartTrace; Impact.HitInfo = HitInfo; `log(GetFuncName()@HitActor@HitInfo.Item@HitInfo.BoneName, bLogMelee); } /** Called when a hitbox notify starts to allow script to perform a custom world trace */ event InitWorldTraceForHitboxCollision(); /********************************************************************************************* * Impact FX *********************************************************************************************/ /** * Called by ProcessMeleeHit to spawn effects * Network: Local Player and Server */ simulated function PlayMeleeHitEffects(Actor Target, vector HitLocation, vector HitDirection, optional bool bShakeInstigatorCamera=true) { local Pawn Victim; local PlayerController PC; local FracturedStaticMeshActor FracActor; // Victim camera shake (Server-only - via RPC) if( WorldInfo.NetMode != NM_Client ) { // If we hit a pawn Victim = Pawn( Target ); if( Victim != none ) { // Shake victim (Server) if( Victim != None && Victim.Controller != None ) { PC = PlayerController(Victim.Controller); if( PC != None ) { PC.ClientPlayCameraShake(MeleeVictimCamShake, 1.f, true); } } } } // Local client impacts if( Instigator.IsHumanControlled() && Instigator.IsLocallyControlled() ) { // Fracture meshes if we hit them if ( bAllowMeleeToFracture ) { FracActor = FracturedStaticMeshActor(Target); if ( FracActor != None ) { class'KFMeleeHelperBase'.static.MeleeFractureMeshImpact(FracActor, HitLocation, HitDirection); return; } } // Play world geometry impacts for local players if ( !(Target.bCanBeDamaged && Target.IsA('Pawn')) ) { `ImpactEffectManager.PlayImpactEffects(HitLocation, Instigator, HitDirection, WorldImpactEffects); } } } /** Takes some chunks out of a fracture mesh */ static function MeleeFractureMeshImpact( FracturedStaticMeshActor FracActor, vector HitLocation, vector HitNormal ) { if( FracActor != none ) { FracActor.BreakOffPartsInRadius( HitLocation - (HitNormal * 15.0), 35.0, 100.0, TRUE); FracActor.SetLoseChunkReplacementMaterial(); } } defaultproperties { bAllowMeleeToFracture=true Begin Object Class=CameraShake Name=MeleeImpactCamShake0 bSingleInstance=true OscillationDuration=0.35f RotOscillation={(Pitch=(Amplitude=250.f,Frequency=60.f), Yaw=(Amplitude=150.f,Frequency=70.f), Roll=(Amplitude=150.f,Frequency=100.f))} End Object MeleeVictimCamShake=MeleeImpactCamShake0 DefaultFOVCosine=0.6f MaxHitRange=150 //HitboxExtent=(X=8,Y=8,Z=8) HitboxSpacing=30 HitboxBoneName=RW_Damage HitboxBoneAxis=AXIS_Z bDoHitboxObstructionTrace=TRUE //bDebugShowCollision=TRUE MeleeImpactCamShake=CameraShake'FX_CameraShake_Arch.Melee.Default_Melee' WorldImpactEffects=KFImpactEffectInfo'FX_Impacts_ARCH.Blunted_melee_impact' }