logo
Main

Forums

Downloads

Unreal-Netiquette

Donate for Oldunreal:
Donate

borderline

Links to our wiki:
Wiki

Walkthrough

Links

Tutorials

Unreal Reference

Usermaps

borderline

Contact us:
Submit News
Page Index Toggle Pages: 1 Send TopicPrint
Normal Topic Issue #17. Troubles with implementing invulnerability using GameRules.PreventDeath (Read 129 times)
Masterkent
Developer Team
Offline



Posts: 1050
Location: Russia
Joined: Apr 5th, 2013
Gender: Male
Issue #17. Troubles with implementing invulnerability using GameRules.PreventDeath
Apr 4th, 2018 at 2:28pm
Print Post  
Overridden functions GameRules.PreventDeath and GameRules.ModifyDamage can prevent death of a pawn

Code
Select All
function Died(pawn Killer, name damageType, vector HitLocation)
{
	local pawn OtherPawn;
	local GameRules GR;

	if ( bDeleteMe || Level.NetMode==NM_Client )
		return; //already destroyed

	if ( Level.Game!=None && Level.Game.GameRules!=None )
	{
		for ( GR=Level.Game.GameRules; GR!=None; GR=GR.NextRules )
			if ( GR.bHandleDeaths && GR.PreventDeath(Self,Killer,damageType) )
			{
				Health = Max(1,Health);
				Return;
			} 


and taking a damage

Code
Select All
function TakeDamage( int Damage, Pawn instigatedBy, Vector hitlocation,
					 Vector momentum, name damageType)
{
	local int actualDamage;
	local bool bAlreadyDead;
	local GameRules GR;

	if ( bDeleteMe || Level.NetMode==NM_Client )
		return;
	if ( Level.Game!=None && Level.Game.GameRules!=None )
	{
		for ( GR=Level.Game.GameRules; GR!=None; GR=GR.NextRules )
			if ( GR.bModifyDamage )
				GR.ModifyDamage(Self,instigatedBy,Damage,hitlocation,damageType,momentum);
	} 


PreventDeath and ModifyDamage together can be used to make the pawn invulnerable to many destructive factors - the pawn won't lose health/armor or die. However, there are 4 functions that unconditionally change Health, independently of PreventDeath and ModifyDamage:

1. A mover whose MoverEncroachType is ME_CrushWhenEncroach may call PlayerPawn's KilledBy:

Code
Select All
	else if ( MoverEncroachType == ME_CrushWhenEncroach )
	{
		// Kill it.
		Other.KilledBy( Instigator );
		return false;
	} 


Code
Select All
function KilledBy( pawn EventInstigator )
{
	Health = 0;
	Died( EventInstigator, 'suicided', Location );
} 


PreventDeath may save the player from dying. But in order to restore the original value of Health that was changed by the assignment

Code
Select All
Health = 0; 


the mod has to store it beforehand somehow. It's possible to save Health of every protected Pawn on every game tick, but such a solution looks a bit expensive and it does not guarantee correct results if Health is increased shortly before the call to KilledBy (operations can be done in the following order: saving the value of Health -> increasing Health -> calling KilledBy).

2. A pawn may call gibbedBy to perform telefragging by other pawn:

Code
Select All
event EncroachedBy( actor Other )
{
	if ( Pawn(Other) != None )
		gibbedBy(Other);

} 


Code
Select All
function gibbedBy(actor Other)
{
	local pawn instigatedBy;
	if ( Role < ROLE_Authority )
		return;
	instigatedBy = pawn(Other);
	if (instigatedBy == None)
		instigatedBy = Other.instigator;
	health = -1000; //make sure gibs
	Died(instigatedBy, 'Gibbed', Location);
} 


This implementation implies the same issue: Health is unconditionally changed to -1000 and PreventDeath is not informed of the previous value.

3. When a player reaches an area that is outside of any valid zone, FellOutOfWorld is called:

Code
Select All
event FellOutOfWorld()
{
	if (PlayerReplicationInfo != none)
	{
		Health = -1;
		SetPhysics(PHYS_None);
		Died(None, 'fell', Location);
	}
	else
	{
		Died(None, 'fell', Location);
		Destroy();
	}
} 


Again, setting Health to -1 makes recovering the original value harder. In the second branch, Destroy is called even if death was prevented.

4. When a pawn "touches" a TriggeredDeath actor, pawn's Health and inventory may be affected:

Code
Select All
	function Touch( Actor Other )
	{
		local inventory Inv;
		local Pawn P;
		local int VNum;

		// Something has contacted the death trigger.
		// If it is a PlayerPawn, have it screen flash and
		// die.
		if ( Other.bIsPawn )
		{
			P = Pawn(Other);
			P.Weapon = None;
			P.SelectedItem = None;
			for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
				Inv.Destroy();

			if ( P.Health <= 0 )
				return;
			if ( Other.IsA('PlayerPawn') )
			{
				Enable('Tick');
				While ( (VNum < 7) && (Victim[VNum] != None) )
				VNum++;
				Victim[Vnum] = PlayerPawn(Other);
				TimePassed[VNum] = 0;
			}
			else
				KillVictim(P);

			P.Health = 1;
			if ( P.bIsPlayer )
			{
				if ( P.bIsFemale )
					Other.PlaySound( FemaleDeathSound, SLOT_Talk );
				else
					Other.PlaySound( MaleDeathSound, SLOT_Talk );
			}
			else
				P.Playsound(P.Die, SLOT_Talk);
		}
		else if ( bDestroyItems )
			Other.Destroy();
	}

	function KillVictim(Pawn Victim)
	{
		Victim.NextState = '';
		Victim.Health = -1;
		Victim.Died(None, DeathName, Victim.Location);
		Victim.HidePlayer();
	}

	function Tick( float DeltaTime )
	{
		local Float CurScale;
		local vector CurFog;
		local float  TimeRatio;
		local int i;
		local bool bFoundVictim;

		for ( i=0; i<8; i++ )
			if ( Victim[i] != None )
			{
				if ( Victim[i].Health > 1 )
					Victim[i] = None;
				else
				{
					// Check the timing
					TimePassed[i] += DeltaTime;
					if ( TimePassed[i] >= ChangeTime )
					{
						TimeRatio = 1;
						Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
						if ( Victim[i].Health > 0 )
							KillVictim(Victim[i]);
						Victim[i] = None;
					}
					else
					{
						bFoundVictim = true;
						// Continue the screen flashing
						TimeRatio = TimePassed[i]/ChangeTime;
						CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
						CurFog   = (EndFlashFog  -StartFlashFog  )*TimeRatio + StartFlashFog;
						Victim[i].ClientFlash( CurScale, 1000 * CurFog );
					}
				}
			}
		if ( !bFoundVictim )
			Disable('Tick');
	}
} 


Recovering the pawn's inventory is also a very tricky thing.

Suggested resolution:

Option #1:

- Modify PlayerPawn.KilledBy, Pawn.GibbedBy, Pawn.FellOutOfWorld, and the functions of state TriggeredDeath.Enabled as indicated below:

Code
Select All
function KilledBy( pawn EventInstigator )
{
+	local int OldHealth;
+
+	OldHealth = Health;
	Health = 0;
	Died(EventInstigator, 'suicided', Location);
+	if (Health > 0)
+		Health = OldHealth;
} 


Code
Select All
function gibbedBy(actor Other)
{
	local pawn instigatedBy;
+	local int OldHealth;

	if ( Role < ROLE_Authority )
		return;
	instigatedBy = pawn(Other);
-	if (instigatedBy == None)
+	if (instigatedBy == None && Other != None)
		instigatedBy = Other.instigator;
-	health = -1000; //make sure gibs
-	Died(instigatedBy, 'Gibbed', Location);
+
+	if (Health > 0)
+	{
+		OldHealth = Health;
+		Health = -1000; //make sure gibs
+		Died(instigatedBy, 'Gibbed', Location);
+		if (Health > 0)
+			Health = OldHealth;
+	}
} 


Code
Select All
event FellOutOfWorld()
{
+	local int OldHealth;
+
+	if (Role != ROLE_Authority)
+		return;
	if (PlayerReplicationInfo != none)
	{
-		Health = -1;
		SetPhysics(PHYS_None);
-		Died(None, 'fell', Location);
+		if (Health > 0)
+		{
+			OldHealth = Health;
+			Health = -1;
+			Died(None, 'fell', Location);
+			if (Health > 0)
+				Health = OldHealth;
+		}
	}
-	else
+	else if (Health > 0)
	{
		Died(None, 'fell', Location);
-		Destroy();
+		if (Health <= 0)
+			Destroy();
	}
} 


Code
Select All
auto state Enabled
{
	function Touch( Actor Other )
	{
		local inventory Inv;
		local Pawn P;
-		local int VNum;
+		local GameRules GR;

		// Something has contacted the death trigger.
		// If it is a PlayerPawn, have it screen flash and
		// die.
		if ( Other.bIsPawn )
		{
			P = Pawn(Other);
+			if (P.Health <= 0)
+				return;
+
+			for (GR = Level.Game.GameRules; GR != none; GR = GR.NextRules )
+				if (GR.bHandleDeaths && GR.PreventDeath(P, none, DeathName))
+					return;
+
			P.Weapon = None;
			P.SelectedItem = None;
			for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
				Inv.Destroy();

-			if ( P.Health <= 0 )
-				return;
-			if ( Other.IsA('PlayerPawn') )
-			{
-				Enable('Tick');
-				While ( (VNum < 7) && (Victim[VNum] != None) )
-				VNum++;
-				Victim[Vnum] = PlayerPawn(Other);
-				TimePassed[VNum] = 0;
-			}
-			else
-				KillVictim(P);
-
-			P.Health = 1;
-			if ( P.bIsPlayer )
-			{
-				if ( P.bIsFemale )
-					Other.PlaySound( FemaleDeathSound, SLOT_Talk );
-				else
-					Other.PlaySound( MaleDeathSound, SLOT_Talk );
-			}
-			else
-				P.Playsound(P.Die, SLOT_Talk);
+			if (PlayerPawn(Other) != none)
+				InitTriggeredPlayerPawnDeath(PlayerPawn(Other));
+			else
+				KillVictim(P);
		}
		else if ( bDestroyItems )
			Other.Destroy();
	}
+
+	function InitTriggeredPlayerPawnDeath(PlayerPawn P)
+	{
+		local TriggeredPlayerPawnDeath TriggeredPlayerPawnDeath;
+		local int VNum;
+
+		if (Class == class'TriggeredDeath')
+		{
+			foreach P.ChildActors(class'TriggeredPlayerPawnDeath', TriggeredPlayerPawnDeath)
+				return;
+			TriggeredPlayerPawnDeath = Spawn(class'TriggeredPlayerPawnDeath', P);
+		}
+		if (TriggeredPlayerPawnDeath != none)
+		{
+			TriggeredPlayerPawnDeath.Victim = P;
+			TriggeredPlayerPawnDeath.TriggeredDeath = self;
+		}
+		else
+		{
+			Enable('Tick');
+			While ( (VNum < 7) && (Victim[VNum] != None) )
+			VNum++;
+			Victim[Vnum] = P;
+			TimePassed[VNum] = 0;
+		}
+
+		P.Health = 1;
+		if ( P.bIsFemale )
+			P.PlaySound( FemaleDeathSound, SLOT_Talk );
+		else
+			P.PlaySound( MaleDeathSound, SLOT_Talk );
+	}

	function KillVictim(Pawn Victim)
	{
+		local int OldHealth;
+
+		if (Victim.Health <= 0)
+			return;
+		OldHealth = Victim.Health;
		Victim.NextState = '';
		Victim.Health = -1;
		Victim.Died(None, DeathName, Victim.Location);
-		Victim.HidePlayer();
+		if (Victim.Health > 0)
+			Victim.Health = OldHealth;
	}

	function Tick( float DeltaTime )
	{
		local Float CurScale;
		local vector CurFog;
		local float  TimeRatio;
		local int i;
		local bool bFoundVictim;

		for ( i=0; i<8; i++ )
			if ( Victim[i] != None )
			{
				if ( Victim[i].Health > 1 )
					Victim[i] = None;
				else
				{
					// Check the timing
					TimePassed[i] += DeltaTime;
					if ( TimePassed[i] >= ChangeTime )
					{
-						TimeRatio = 1;
						Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
						if ( Victim[i].Health > 0 )
							KillVictim(Victim[i]);
						Victim[i] = None;
					}
					else
					{
						bFoundVictim = true;
						// Continue the screen flashing
						TimeRatio = TimePassed[i]/ChangeTime;
						CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
						CurFog   = (EndFlashFog  -StartFlashFog  )*TimeRatio + StartFlashFog;
						Victim[i].ClientFlash( CurScale, 1000 * CurFog );
					}
				}
			}
		if ( !bFoundVictim )
			Disable('Tick');
	}
} 


- Add new class TriggeredPlayerPawnDeath:

Code
Select All
class TriggeredPlayerPawnDeath expands Triggers;

var PlayerPawn Victim;
var TriggeredDeath TriggeredDeath;
var float TimePassed;

event Tick(float DeltaTime)
{
	local float CurScale;
	local vector CurFog;
	local float TimeRatio;

	if (Victim == none || Victim.bDeleteMe || Victim.Health > 1 ||
		TriggeredDeath == none || TriggeredDeath.bDeleteMe)
	{
		Destroy();
		return;
	}

	// Check the timing
	TimePassed += DeltaTime;
	if (TimePassed >= TriggeredDeath.ChangeTime)
	{
		Victim.ClientFlash(TriggeredDeath.EndFlashScale, 1000 * TriggeredDeath.EndFlashFog);
		if (Victim.Health > 0)
			KillVictim();
		Destroy();
	}
	else
	{
		// Continue the screen flashing
		TimeRatio = TimePassed / TriggeredDeath.ChangeTime;
		CurScale = (TriggeredDeath.EndFlashScale - TriggeredDeath.StartFlashScale) * TimeRatio + TriggeredDeath.StartFlashScale;
		CurFog   = (TriggeredDeath.EndFlashFog   - TriggeredDeath.StartFlashFog  ) * TimeRatio + TriggeredDeath.StartFlashFog;
		Victim.ClientFlash(CurScale, 1000 * CurFog);
	}
}

function KillVictim()
{
	local int OldHealth;

	if (Victim.Health <= 0)
		return;
	OldHealth = Victim.Health;
	Victim.Health = -1;
	Victim.Died(None, TriggeredDeath.DeathName, Victim.Location);
	if (Victim.Health > 0)
		Victim.Health = OldHealth;
} 


(Using a separate actor for each player guarantees that any reasonable number of players can be properly handled)

This option implies that if PreventDeath prevents death, the value of Health is restored to the original value (PreventDeath cannot establish the resulting Health).

Option #2:

- Add new variable to Engine.Pawn:

Code
Select All
var transient int HealthBeforeDying; 


- Modify PlayerPawn.KilledBy, Pawn.GibbedBy, Pawn.FellOutOfWorld, and the functions of state TriggeredDeath.Enabled as indicated below:

Code
Select All
function KilledBy( pawn EventInstigator )
{
+	HealthBeforeDying = Health;
	Health = 0;
	Died(EventInstigator, 'suicided', Location);
+	HealthBeforeDying = 0;
} 


Code
Select All
function gibbedBy(actor Other)
{
	local pawn instigatedBy;

	if ( Role < ROLE_Authority )
		return;
	instigatedBy = pawn(Other);
-	if (instigatedBy == None)
+	if (instigatedBy == None && Other != None)
		instigatedBy = Other.instigator;
-	health = -1000; //make sure gibs
-	Died(instigatedBy, 'Gibbed', Location);
+
+	if (Health > 0)
+	{
+		HealthBeforeDying = Health;
+		Health = -1000; //make sure gibs
+		Died(instigatedBy, 'Gibbed', Location);
+		HealthBeforeDying = 0;
+	}
} 


Code
Select All
event FellOutOfWorld()
{
+	if (Role != ROLE_Authority)
+		return;
	if (PlayerReplicationInfo != none)
	{
-		Health = -1;
		SetPhysics(PHYS_None);
-		Died(None, 'fell', Location);
+		if (Health > 0)
+		{
+			HealthBeforeDying = Health;
+			Health = -1;
+			Died(None, 'fell', Location);
+			HealthBeforeDying = 0;
+		}
	}
-	else
+	else if (Health > 0)
	{
		Died(None, 'fell', Location);
-		Destroy();
+		if (Health <= 0)
+			Destroy();
	}
} 


Code
Select All
auto state Enabled
{
	function Touch( Actor Other )
	{
		local inventory Inv;
		local Pawn P;
-		local int VNum;
+		local GameRules GR;

		// Something has contacted the death trigger.
		// If it is a PlayerPawn, have it screen flash and
		// die.
		if ( Other.bIsPawn )
		{
			P = Pawn(Other);
+			if (P.Health <= 0)
+				return;
+
+			for (GR = Level.Game.GameRules; GR != none; GR = GR.NextRules )
+				if (GR.bHandleDeaths && GR.PreventDeath(P, none, DeathName))
+					return;
+
			P.Weapon = None;
			P.SelectedItem = None;
			for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
				Inv.Destroy();

-			if ( P.Health <= 0 )
-				return;
-			if ( Other.IsA('PlayerPawn') )
-			{
-				Enable('Tick');
-				While ( (VNum < 7) && (Victim[VNum] != None) )
-				VNum++;
-				Victim[Vnum] = PlayerPawn(Other);
-				TimePassed[VNum] = 0;
-			}
-			else
-				KillVictim(P);
-
-			P.Health = 1;
-			if ( P.bIsPlayer )
-			{
-				if ( P.bIsFemale )
-					Other.PlaySound( FemaleDeathSound, SLOT_Talk );
-				else
-					Other.PlaySound( MaleDeathSound, SLOT_Talk );
-			}
-			else
-				P.Playsound(P.Die, SLOT_Talk);
+			if (PlayerPawn(Other) != none)
+				InitTriggeredPlayerPawnDeath(PlayerPawn(Other));
+			else
+				KillVictim(P);
		}
		else if ( bDestroyItems )
			Other.Destroy();
	}
+
+	function InitTriggeredPlayerPawnDeath(PlayerPawn P)
+	{
+		local TriggeredPlayerPawnDeath TriggeredPlayerPawnDeath;
+		local int VNum;
+
+		if (Class == class'TriggeredDeath')
+		{
+			foreach P.ChildActors(class'TriggeredPlayerPawnDeath', TriggeredPlayerPawnDeath)
+				return;
+			TriggeredPlayerPawnDeath = Spawn(class'TriggeredPlayerPawnDeath', P);
+		}
+		if (TriggeredPlayerPawnDeath != none)
+		{
+			TriggeredPlayerPawnDeath.Victim = P;
+			TriggeredPlayerPawnDeath.TriggeredDeath = self;
+		}
+		else
+		{
+			Enable('Tick');
+			While ( (VNum < 7) && (Victim[VNum] != None) )
+			VNum++;
+			Victim[Vnum] = P;
+			TimePassed[VNum] = 0;
+		}
+
+		P.Health = 1;
+		if ( P.bIsFemale )
+			P.PlaySound( FemaleDeathSound, SLOT_Talk );
+		else
+			P.PlaySound( MaleDeathSound, SLOT_Talk );
+	}

	function KillVictim(Pawn Victim)
	{
+		if (Victim.Health <= 0)
+			return;
+		Victim.HealthBeforeDying = Victim.Health;
		Victim.NextState = '';
		Victim.Health = -1;
		Victim.Died(None, DeathName, Victim.Location);
-		Victim.HidePlayer();
+		Victim.HealthBeforeDying = 0;
	}

	function Tick( float DeltaTime )
	{
		local Float CurScale;
		local vector CurFog;
		local float  TimeRatio;
		local int i;
		local bool bFoundVictim;

		for ( i=0; i<8; i++ )
			if ( Victim[i] != None )
			{
				if ( Victim[i].Health > 1 )
					Victim[i] = None;
				else
				{
					// Check the timing
					TimePassed[i] += DeltaTime;
					if ( TimePassed[i] >= ChangeTime )
					{
-						TimeRatio = 1;
						Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
						if ( Victim[i].Health > 0 )
							KillVictim(Victim[i]);
						Victim[i] = None;
					}
					else
					{
						bFoundVictim = true;
						// Continue the screen flashing
						TimeRatio = TimePassed[i]/ChangeTime;
						CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
						CurFog   = (EndFlashFog  -StartFlashFog  )*TimeRatio + StartFlashFog;
						Victim[i].ClientFlash( CurScale, 1000 * CurFog );
					}
				}
			}
		if ( !bFoundVictim )
			Disable('Tick');
	}
} 


- Add new class TriggeredPlayerPawnDeath:

Code
Select All
class TriggeredPlayerPawnDeath expands Triggers;

var PlayerPawn Victim;
var TriggeredDeath TriggeredDeath;
var float TimePassed;

event Tick(float DeltaTime)
{
	local float CurScale;
	local vector CurFog;
	local float TimeRatio;

	if (Victim == none || Victim.bDeleteMe || Victim.Health > 1 ||
		TriggeredDeath == none || TriggeredDeath.bDeleteMe)
	{
		Destroy();
		return;
	}

	// Check the timing
	TimePassed += DeltaTime;
	if (TimePassed >= TriggeredDeath.ChangeTime)
	{
		Victim.ClientFlash(TriggeredDeath.EndFlashScale, 1000 * TriggeredDeath.EndFlashFog);
		if (Victim.Health > 0)
			KillVictim();
		Destroy();
	}
	else
	{
		// Continue the screen flashing
		TimeRatio = TimePassed / TriggeredDeath.ChangeTime;
		CurScale = (TriggeredDeath.EndFlashScale - TriggeredDeath.StartFlashScale) * TimeRatio + TriggeredDeath.StartFlashScale;
		CurFog   = (TriggeredDeath.EndFlashFog   - TriggeredDeath.StartFlashFog  ) * TimeRatio + TriggeredDeath.StartFlashFog;
		Victim.ClientFlash(CurScale, 1000 * CurFog);
	}
}

function KillVictim()
{
	if (Victim.Health <= 0)
		return;
	Victim.HealthBeforeDying = Victim.Health;
	Victim.Health = -1;
	Victim.Died(None, TriggeredDeath.DeathName, Victim.Location);
	Victim.HealthBeforeDying = 0;
} 


This option implies that if PreventDeath prevents death, the value of Health is set to the maximum of 1 and the value of Health after returning from PreventDeath (PreventDeath can establish the resulting Health within the range [1; MaxInt]; in particular, PreventDeath can set the value of Health to the value of HealthBeforeDying).

A non-positive value of HealthBeforeDying means that the health before dying is unknown (Died may be called from third-party mods that are not aware of HealthBeforeDying, so it's important to reset HealthBeforeDying when the value stored in this variable is not needed anymore and further changes of Health can make the value of HealthBeforeDying irrelevant).
  
Back to top
 
IP Logged
 
Page Index Toggle Pages: 1
Send TopicPrint
Bookmarks: del.icio.us Digg Facebook Google Google+ Linked in reddit StumbleUpon Twitter Yahoo