1
0
KF2-Dev-Scripts/KFGame/Classes/KFSM_RagdollKnockdown.uc

377 lines
12 KiB
Ucode
Raw Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// KFSM_RagdollKnockdown
//=============================================================================
// Sends pawn into a temporary ragdoll
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Andrew "Strago" Ladenberger
//=============================================================================
class KFSM_RagdollKnockdown extends KFSpecialMove;
/** If falling past this distance during knockdown kill pawn */
var(Physics) float KnockdownMaxZ;
/** maximum number of pawns that can use this SM at once */
var const int MaxKnockdownPawns;
var transient vector KnockdownStartLoc;
/** Dazed particle effect */
var transient ParticleSystemComponent DazedPSC;
//failsafe
var int MaxKnockDownTests;
var int CurrentKnockdownTests;
protected function bool InternalCanDoSpecialMove()
{
local Pawn P;
local int KnockdownCount;
local bool bMayHavePartialKinematics;
// If we have a dismembered limb (partial kinematics), then we don't want to apply
// impulses via SetRBLinearVelocity. For now, just prevent the knockdown.
// @note: have to make some assumptions because gore is handled clientside
bMayHavePartialKinematics = KFPOwner.IsHeadless();
if ( bMayHavePartialKinematics )
{
return false;
}
// fast (no-cast) count of knocked down pawns
foreach PawnOwner.WorldInfo.AllPawns( class'Pawn', P )
{
if ( P.Physics == PHYS_RigidBody && !P.bPlayedDeath )
{
KnockdownCount++;
}
}
// handle MaxKnockdownPawns
if ( KnockdownCount >= MaxKnockdownPawns )
{
return false;
}
return Super.InternalCanDoSpecialMove();
}
/** Notification called when Special Move starts */
function SpecialMoveStarted(bool bForced, Name PrevMove )
{
local TWDeferredWorkManager DeferredWorkManager;
DeferredWorkManager = TWDeferredWorkManager(KFPOwner.WorldInfo.DeferredWorkManager);
// KnockdownImpulse is replicated before this move (if this has problems go back to repnotify KnockdownImpulse)
DeferredWorkManager.SetTimer(0.0001, false, nameof(DeferApplyKnockdownImpulse), self);
// Spawn the dazed particle effect
if( KFPOwner.WorldInfo.NetMode != NM_DedicatedServer )
{
DazedPSC = class'KFSM_Stunned'.static.AttachDazedEffect( KFPawn_Monster(KFPOwner) );
}
}
/** Notification called when Special Move starts */
function SpecialMoveEnded(Name PrevMove, Name NextMove)
{
local TWDeferredWorkManager DeferredWorkManager;
DeferredWorkManager = TWDeferredWorkManager(KFPOwner.WorldInfo.DeferredWorkManager);
DeferredWorkManager.ClearTimer(nameof(DeferApplyKnockdownImpulse), self);
// Make sure dazed particle effect is always deactivated
if( DazedPSC != none && DazedPSC.bIsActive )
{
DazedPSC.DeactivateSystem();
}
// failsafe in case knockdown needs to be cleaned up
if ( PawnOwner.Mesh.PhysicsWeight >= 1.f && NextMove != 'KFSM_RecoverFromRagdoll' )
{
PawnOwner.ClearTimer(nameof(EndKnockdown), self);
PawnOwner.ClearTimer(nameof(KnockdownTimer), self);
TermKnockdownRagdoll(KFPOwner);
}
}
function DeferApplyKnockdownImpulse()
{
ApplyKnockdownImpulse(KFPOwner.KnockdownImpulse);
}
/** Applies the contents of KnockdownImpulse. */
function ApplyKnockdownImpulse(const out KnockdownImpulseInfo Info)
{
local vector PointImpulse;
local name PointImpulseBoneName;
// KnockdownImpulse should alwasy be non-zero
if (IsZero(Info.LinearVelocity) && IsZero(Info.AngularVelocity))
{
`warn(self@"invalid KnockdownImpulse for"@PawnOwner);
}
if (PawnOwner.Physics != PHYS_RigidBody || PawnOwner.Mesh != PawnOwner.CollisionComponent)
{
// first send them to ragdoll
PlayFallDown();
}
if (PawnOwner.Physics == PHYS_RigidBody)
{
KnockdownStartLoc = PawnOwner.Location;
// whole-body velocities
//@note: we set instead of adding for angular velocity as it's generally zero for newly created ragdolls
// and if it weren't, the client wouldn't be able to reliably derive the same value so it would just cause mismatches
PawnOwner.Mesh.SetRBAngularVelocity(Info.AngularVelocity, false);
if (!IsZero(Info.LinearVelocity))
{
PawnOwner.Mesh.SetRBLinearVelocity(Info.LinearVelocity, true);
}
if ( !IsZero(Info.ImpulseStrength) )
{
// handle radial and point impulses exclusively (network optimization)
if ( Info.bIsRadialImpulse )
{
PawnOwner.Mesh.AddRadialImpulse(Info.ImpulsePosition, Info.ImpulseStrength.X, Info.ImpulseStrength.Y, RIF_Linear, true);
}
else
{
// Apply the same scaling factor we use for dead ragdolls
PointImpulse = Info.ImpulseStrength * KFPOwner.KnockdownImpulseScale;
if ( Info.PointImpulseHitZone != 255 )
{
PointImpulseBoneName = KFPOwner.HitZones[Info.PointImpulseHitZone].BoneName;
// make sure it's a valid RB bone
PointImpulseBoneName = KFPOwner.GetRBBoneFromBoneName(PointImpulseBoneName);
}
PawnOwner.Mesh.AddImpulse(PointImpulse, Info.ImpulsePosition, PointImpulseBoneName, true);
}
}
if (KFPOwner.Role == ROLE_Authority )
{
// check for rest on an interval, but also set a maximum time
PawnOwner.SetTimer(1.5f, false, nameof(EndKnockdown), self);
PawnOwner.SetTimer(0.20f, true, nameof(KnockdownTimer), self);
}
}
else
{
`warn(self@"failed to transition to RigidBody for"@GetFuncName());
KFPOwner.EndSpecialMove();
}
}
protected function PlayFallDown()
{
//Reset phys asset for the same reason as we would if dying
if (KFPOwner.bReinitPhysAssetOnDeath && KFPOwner.CharacterArch != none && KFPOwner.CharacterArch.PhysAsset != none)
{
KFPOwner.Mesh.SetPhysicsAsset(KFPOwner.CharacterArch.PhysAsset, , true);
}
KFPOwner.PrepareRagdoll();
if( KFPOwner.InitRagdoll() )
{
// Move into post so that we are hitting physics from last frame, rather than animated from this
PawnOwner.Mesh.SetTickGroup(TG_PostAsyncWork);
PawnOwner.SetTickGroup(TG_PostAsyncWork);
if( PawnOwner.Mesh.PhysicsAssetInstance != None )
{
// Make sure all motors are off on joints.
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularPositionDrive(FALSE, FALSE, PawnOwner.Mesh, TRUE);
// Turn on velocity motors, to add some joint friction
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularDriveParams(0.f, 0.f, 0.004f, PawnOwner.Mesh, TRUE);
// Don't set motors on dangly bits.
PawnOwner.Mesh.PhysicsAssetInstance.SetAllMotorsAngularVelocityDrive(TRUE, TRUE, PawnOwner.Mesh, TRUE);
}
PawnOwner.Mesh.bUpdateJointsFromAnimation = FALSE;
// we purposefully do NOT turn collide with RBBC_Pawn on
// we will innerpentrate other ragdolled pawns as we do not collide with the Pawn channel. This is acceptable because if we do
// collide with other ragdolled pawns there is a chance that we will have the physics system forcibly push us apart causing one
// or both the pawns to fly through the world
PawnOwner.Mesh.SetRBChannel(RBCC_KnockedDownPawn);
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_KnockedDownPawn, TRUE);
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_Pawn, FALSE);
PawnOwner.Mesh.SetRBCollidesWithChannel(RBCC_DeadPawn, FALSE);
//ReduceConstraintLimits();
}
else
{
`Warn("Failed to init ragdoll for"@self);
}
}
protected event KnockdownFailsafe()
{
PawnOwner.Died(PawnOwner.LastHitBy, class'KFDT_Falling', PawnOwner.Location);
}
/** Timer called to check if ragdoll has come to a rest */
protected function KnockdownTimer()
{
CurrentKnockdownTests++;
if ( PawnOwner.Physics != PHYS_RigidBody || VSizeSq( PawnOwner.Velocity) < 100.f || !PawnOwner.Mesh.RigidBodyIsAwake() || MaxKnockdownTests == CurrentKnockdownTests)
{
EndKnockdown(true);
}
}
/** Initiate recover from ragdoll on the server */
protected function EndKnockdown(optional bool bForceEnd = false)
{
CurrentKnockdownTests = 0;
//don't let them recover if they shouldn't
if (!bForceEnd)
{
if (!(PawnOwner.Physics != PHYS_RigidBody || VSizeSq(PawnOwner.Velocity) < 100.f || !PawnOwner.Mesh.RigidBodyIsAwake()))
{
`log("failed to recover from knockdown " @PawnOwner.Physics @VSizeSq(PawnOwner.Velocity) < 100.f @PawnOwner.Mesh.RigidBodyIsAwake());
return;
}
}
PawnOwner.ClearTimer(nameof(EndKnockdown), self);
PawnOwner.ClearTimer(nameof(KnockdownTimer), self);
// Deactivate dazed particle effect
if( DazedPSC != none )
{
DazedPSC.DeactivateSystem();
}
// if we've fallen a significant distance then assume we're dead
if ( Abs(KnockdownStartLoc.Z - PawnOwner.Location.Z) > KnockdownMaxZ )
{
KnockdownFailsafe();
}
KFPOwner.KnockdownImpulse.LinearVelocity = vect(0,0,0);
KFPOwner.KnockdownImpulse.AngularVelocity = vect(0,0,0);
// If we have a dismembered limb (partial kinematics), then we can't get back up
// note: It should be possible to handle this using bEnableFullAnimWeightBodies/bAlwaysFullAnimWeight
// on the dismembered limb, but for now just kill the pawn.
if ( KFPOwner.HasInjuredHitZones() )
{
KnockdownFailsafe();
}
// if we're still alive at this point
if ( PawnOwner.IsAliveAndWell() && PawnOwner.Physics == PHYS_RigidBody )
{
KFPOwner.DoSpecialmove(SM_RecoverFromRagdoll, TRUE);
if ( KFPOwner.SpecialMove != SM_RecoverFromRagdoll )
{
TermKnockdownRagdoll(KFPOwner);
`log(GetFuncName() @ "Failed to find special move class for: SM_RecoverFromRagdoll" @ self);
}
}
// If something else changed owner's physics mode - should not happen!
else if ( PawnOwner.Physics != PHYS_RigidBody )
{
// if this happens the first place to look is a SetPhysics() call in AIC state Action_Idle
`Warn("Knockdown timer called outside of PHYS_RigidBody"@PawnOwner.Physics@KFPOwner.SpecialMove);
KnockdownFailsafe();
}
}
/** Restore normal pawn physics */
static function TermKnockdownRagdoll(KFPawn P)
{
local SkeletalMeshComponent Mesh;
local SkeletalMeshComponent DefaultMesh;
if ( P.bPlayedDeath )
{
return; // Skip TermKnockdown after death and use full ragdoll
}
// this code is a lot like TermRagdoll, but without turning off the PhysicsAssetInstance
P.SetPhysics(PHYS_Falling);
P.Velocity = vect(0,0,0);
P.Acceleration = vect(0,0,0);
P.CollisionComponent = P.CylinderComponent;
Mesh = P.Mesh;
DefaultMesh = P.default.Mesh;
// Restore things we turned off when we went into rag doll
if( !DefaultMesh.bHasPhysicsAssetInstance )
{
Mesh.SetHasPhysicsAssetInstance(FALSE);
}
Mesh.MinDistFactorForKinematicUpdate = DefaultMesh.MinDistFactorForKinematicUpdate;
Mesh.bUpdateKinematicBonesFromAnimation = DefaultMesh.bUpdateKinematicBonesFromAnimation;
Mesh.bUpdateJointsFromAnimation = DefaultMesh.bUpdateJointsFromAnimation;
Mesh.PhysicsWeight = DefaultMesh.PhysicsWeight;
// Restore non-default collision settings to the way it was before knockdown
RestoreRBCollision(P);
// Kick dead bodies
//Mesh.bUpdateKinematicBonesFromAnimation = true;
//Mesh.SetRBCollidesWithChannel(RBCC_DeadPawn, TRUE);
// restore danglers
if( Mesh != None && Mesh.PhysicsAssetInstance != None )
{
Mesh.PhysicsAssetInstance.SetFullAnimWeightBonesFixed(FALSE, Mesh);
}
// If Pawn was using partial kinamatics for gore (e.g. Headless), reset those settings now
if ( P.bHasBrokenConstraints && P.HasInjuredHitZones() )
{
P.InitPartialKinematics();
P.Mesh.UpdateMeshForBrokenConstraints();
}
}
/** If RB settings were modified while alive (e.g. Ragdoll knockdown) restore settings */
static function RestoreRBCollision(KFPawn P)
{
P.Mesh.SetRBCollidesWithChannel(RBCC_KnockedDownPawn, FALSE);
if( !P.Mesh.bUpdateKinematicBonesFromAnimation )
{
// No RB collision
P.Mesh.SetRBChannel(RBCC_Nothing);
}
else
{
// Go back to being a pawn, and can collide with other live pawns (for flappy bits),
// and corpses, but not knocked down alive people )
P.Mesh.SetRBChannel(RBCC_Pawn);
P.Mesh.SetRBCollidesWithChannel(RBCC_Pawn, P.default.Mesh.RBCollideWithChannels.Pawn);
}
P.UpdateMeshForFleXCollision();
}
defaultproperties
{
Handle=KFSM_Knockdown
KnockdownMaxZ=750.f
MaxKnockdownPawns=5
bCanOnlyWanderAtEnd=true
bDisablesWeaponFiring=true
MaxKnockdownTests=25
}