For direct access use https://forums.oldunreal.com
It's been quite a while since oldunreal had an overhaul, but we are moving to another server which require some updates and changes. The biggest change is the migration of our old reliable YaBB forum to phpBB. This system expects you to login with your username and old password known from YaBB.
If you experience any problems there is also the usual "password forgotten" function. Don't forget to clear your browser cache!
If you have any further concerns feel free to contact me: Smirftsch@oldunreal.com

[227j] New state in class PlayerPawn: CustomPlayerState

The section related to UnrealScript and modding. This board is for coders to discuss and exchange experiences or ask questions.
Post Reply
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

[227j] New state in class PlayerPawn: CustomPlayerState

Post by Masterkent »

I created this topic in order to describe the customizable PlayerPawn state introduced in 227j and open a discussion about it if anyone has any questions or suggestions regarding this feature.

The purpose of adding a customizable state to PlayerPawn is to allow extending the functionality of PlayerPawn without writing special subclasses of PlayerPawn. The main problem of subclassing from such basic things as PlayerPawn is a lack of scalability: two mods may be incompatible with each other due to requiring their own mutually exclusive specific sets of subclasses. Moreover, writing subclasses of PlayerPawn would be an overkill for tiny mods such as a custom inventory like jetpack (or flight boots).

When extending the functionality of PlayerPawn can be done by defining a new state, state functions might delegate the underlying operations to member functions of a special actor.

Consider jetpack as an example. State PlayerPawn.PlayerFlying seems to be close to the suitable implementation of the flight movement:

Code: Select all

state PlayerFlying
{
ignores SeePlayer, HearNoise, Bump, StartClimbing;

	function AnimEnd()
	{
		PlaySwimming();
	}

	event PlayerTick( float DeltaTime )
	{
		if ( bUpdatePosition )
			ClientUpdatePosition();

		PlayerMove(DeltaTime);
	}

	function PlayerMove(float DeltaTime)
	{
		local vector X,Y,Z;

		GetAxes(ViewRotation,X,Y,Z);

		aForward *= 0.2;
		aStrafe  *= 0.2;
		aLookup  *= 0.24;
		aTurn    *= 0.24;

		Acceleration = aForward*X + aStrafe*Y + aUp*Z;
		if ( bPressedJump && aUp<=0.01 )
			bPressedJump = False;
		// Update rotation.
		UpdateRotation(DeltaTime, 2);

		if ( Role < ROLE_Authority ) // then save this move and replicate it
			ReplicateMove(DeltaTime, Acceleration, DODGE_None, rot(0,0,0));
		else
			ProcessMove(DeltaTime, Acceleration, DODGE_None, rot(0,0,0));
	}

	event BeginState()
	{
		SetPhysics(PHYS_Flying);
		if (!IsAnimating())
			PlaySwimming();
		bCanFly = true;
		//log("player flying");
	}

	event EndState()
	{
		bCanFly = false;
	}
}
We might want to apply several tweaks to this state:

1. Holding jump/crouch buttons should affect only vertical movement (along the z-axis):

Code: Select all

	function PlayerMove(float DeltaTime)
	{
		local vector X,Y,Z;

		GetAxes(ViewRotation,X,Y,Z);

		aForward *= 0.2;
		aStrafe  *= 0.2;
		aLookup  *= 0.24;
		aTurn    *= 0.24;

-		Acceleration = aForward*X + aStrafe*Y + aUp*Z;
+		Acceleration = aForward * X + aStrafe * Y + aUp * vect(0, 0, 1);
2. Holding a crouch button should not reduce movement speed:

Code: Select all

+	function HandleWalking()
+	{
+		bIsWalking = false;
+	}
3. The player should play the swimming animation when swimming forward inside a water zone and the waiting animation in all other cases:

Code: Select all

	function AnimEnd()
	{
-		PlaySwimming();
+		bAnimTransition = false;
+
+		if (!Region.Zone.bWaterZone || Acceleration Dot vector(ViewRotation) <= 0)
+		{
+			if (GetAnimGroup(AnimSequence) == 'TakeHit')
+			{
+				bAnimTransition = true;
+				TweenToWaiting(0.2);
+			}
+			else
+				PlayWaiting();
+		}
+		else
+		{
+			if (GetAnimGroup(AnimSequence) == 'TakeHit')
+			{
+				bAnimTransition = true;
+				TweenToSwimming(0.2);
+			}
+			else
+				PlaySwimming();
+		}
	}

Code: Select all

	function PlayerMove(float DeltaTime)
	{
		....
+
+		if (Region.Zone.bWaterZone)
+			SwimAnimUpdate(vector(ViewRotation) Dot Acceleration <= 0);
	}

Code: Select all

	event BeginState()
	{
		....
+		if (!IsAnimating())
+			PlayWaiting();
	}

Code: Select all

+	event ZoneChange(ZoneInfo NewZone)
+	{
+		if (NewZone.bWaterZone != Region.Zone.bWaterZone)
+		{
+			if (!NewZone.bWaterZone && Region.Zone.bWaterZone && GetAnimGroup(AnimSequence) != 'Waiting')
+				TweenToWaiting(0.1);
+		}
+		global.ZoneChange(NewZone);
+	}
4. AirSpeed of the player should be different in water zones and outside of water zones; when the flight is over, the default values should be restored:

Code: Select all

+	static function SetFlightProperties(Pawn P)
+	{
+		P.bCanFly = true;
+
+		if (P.Physics != PHYS_Flying)
+			P.SetPhysics(PHYS_Flying);
+
+		if (P.FootRegion.Zone.bWaterZone || P.Region.Zone.bWaterZone)
+			P.AirSpeed = 400;
+		else
+			P.AirSpeed = 600;
+	}

Code: Select all

	event BeginState()
	{
-		SetPhysics(PHYS_Flying);
-		if (!IsAnimating())
-			PlaySwimming();
-		bCanFly = true;
-		//log("player flying");
+		SetFlightProperties(self);[code]
[code]
	event EndState()
	{
+		AirSpeed = default.AirSpeed;
+		bCanFly = default.bCanFly;
-		bCanFly = false;
	}

Code: Select all

+	event FootZoneChange(ZoneInfo NewZone)
+	{
+		if (NewZone.bWaterZone != FootRegion.Zone.bWaterZone)
+			SetFlightProperties(self);
+	}
+
+	event ZoneChange(ZoneInfo NewZone)
+	{
+		if (NewZone.bWaterZone != Region.Zone.bWaterZone)
+		{
+			SetFlightProperties(self);
All these tweaks together are reflected below:

Code: Select all

state PlayerUsingJetpack
{
-	ignores SeePlayer, HearNoise, Bump, StartClimbing;
-
	function AnimEnd()
	{
-		PlaySwimming();
+		bAnimTransition = false;
+
+		if (!Region.Zone.bWaterZone || Acceleration Dot vector(ViewRotation) <= 0)
+		{
+			if (GetAnimGroup(AnimSequence) == 'TakeHit')
+			{
+				bAnimTransition = true;
+				TweenToWaiting(0.2);
+			}
+			else
+				PlayWaiting();
+		}
+		else
+		{
+			if (GetAnimGroup(AnimSequence) == 'TakeHit')
+			{
+				bAnimTransition = true;
+				TweenToSwimming(0.2);
+			}
+			else
+				PlaySwimming();
+		}
	}

	event PlayerTick( float DeltaTime )
	{
		if ( bUpdatePosition )
			ClientUpdatePosition();

		PlayerMove(DeltaTime);
	}

	function PlayerMove(float DeltaTime)
	{
		local vector X,Y,Z;

		GetAxes(ViewRotation,X,Y,Z);

		aForward *= 0.2;
		aStrafe  *= 0.2;
		aLookup  *= 0.24;
		aTurn    *= 0.24;

-		Acceleration = aForward*X + aStrafe*Y + aUp*Z;
+		Acceleration = aForward * X + aStrafe * Y + aUp * vect(0, 0, 1);
-		if ( bPressedJump && aUp<=0.01 )
			bPressedJump = False;
		// Update rotation.
		UpdateRotation(DeltaTime, 2);

		if ( Role < ROLE_Authority ) // then save this move and replicate it
			ReplicateMove(DeltaTime, Acceleration, DODGE_None, rot(0,0,0));
		else
			ProcessMove(DeltaTime, Acceleration, DODGE_None, rot(0,0,0));
+
+		if (Region.Zone.bWaterZone)
+			SwimAnimUpdate(vector(ViewRotation) Dot Acceleration <= 0);
	}

	event BeginState()
	{
-		SetPhysics(PHYS_Flying);
-		if (!IsAnimating())
-			PlaySwimming();
-		bCanFly = true;
-		//log("player flying");
+		SetFlightProperties(self);
+		if (!IsAnimating())
+			PlayWaiting();
	}

	event EndState()
	{
+		AirSpeed = default.AirSpeed;
+		bCanFly = default.bCanFly;
-		bCanFly = false;
	}

+	event FootZoneChange(ZoneInfo NewZone)
+	{
+		if (NewZone.bWaterZone != FootRegion.Zone.bWaterZone)
+			SetFlightProperties(self);
+	}
+
+	event ZoneChange(ZoneInfo NewZone)
+	{
+		if (NewZone.bWaterZone != Region.Zone.bWaterZone)
+		{
+			SetFlightProperties(self);
+			if (!NewZone.bWaterZone && Region.Zone.bWaterZone && GetAnimGroup(AnimSequence) != 'Waiting')
+				TweenToWaiting(0.1);
+		}
+		global.ZoneChange(NewZone);
+	}
+
+	function HandleWalking()
+	{
+		bIsWalking = false;
+	}
+
+	static function SetFlightProperties(Pawn P)
+	{
+		P.bCanFly = true;
+
+		if (P.Physics != PHYS_Flying)
+			P.SetPhysics(PHYS_Flying);
+
+		if (P.FootRegion.Zone.bWaterZone || P.Region.Zone.bWaterZone)
+			P.AirSpeed = 400;
+		else
+			P.AirSpeed = 600;
+	}
}
Now it's time to show how this state can be implemented without writing subclasses of PlayerPawn, using the proposed interface:

Code: Select all

class CustomPlayerStateInfo expands Info;

var PlayerPawn PlayerOwner;
var float InactiveTime;

auto state Inactive
{
	event BeginState()
	{
		InactiveTime = 0;
	}

	// When RemoteRole == ROLE_AutonomousProxy, Tick is called only client-side
	event Tick(float DeltaTime)
	{
		if (Role < ROLE_Authority && PlayerOwner != none && PlayerOwner.IsInState('CustomPlayerState'))
		{
			if (PlayerOwner.CustomPlayerStateInfo == none || PlayerOwner.CustomPlayerStateInfo.bDeleteMe)
				PlayerOwner.SetCustomPlayerStateInfo(self);
			else if (PlayerOwner.CustomPlayerStateInfo.Class != Class)
			{
				InactiveTime += DeltaTime / FMax(0.1, Level.TimeDilation);
				if (InactiveTime >= 0.5)
					PlayerOwner.SetCustomPlayerStateInfo(self);
			}
			else
				InactiveTime = 0;
		}
	}
}

state Active
{
	event BeginState()
	{
		Handle_BeginState();
	}

	event EndState()
	{
		Handle_EndState();
	}
}


event PostNetBeginPlay()
{
	PlayerOwner = PlayerPawn(Owner);
	if (PlayerOwner != none)
		PlayerOwner.SetCustomPlayerStateInfo(self);
}

final static function Default_PlayerTick(PlayerPawn Player, float DeltaTime)
{
	if (Player.bUpdatePosition)
		Player.ClientUpdatePosition();

	if (Player.Role < ROLE_Authority)
		Player.ReplicateMove(DeltaTime, vect(0, 0, 0), DODGE_None, rot(0, 0, 0));
	else
		Player.ProcessMove(DeltaTime, vect(0, 0, 0), DODGE_None, rot(0, 0, 0));
}

// -----------------------------------------------------------------------------
// Handle events

function Handle_AnimEnd()
{
	PlayerOwner.GlobalFunc_AnimEnd();
}

function Handle_BaseChange()
{
	PlayerOwner.GlobalFunc_BaseChange();
}

function Handle_BeginState();

function Handle_Bump(Actor A)
{
	PlayerOwner.GlobalFunc_Bump(A);
}

function Handle_EndState();

function Handle_Falling()
{
	PlayerOwner.GlobalFunc_Falling();
}

function Handle_FootZoneChange(ZoneInfo NewZone)
{
	PlayerOwner.GlobalFunc_FootZoneChange(NewZone);
}

function Handle_HeadZoneChange(ZoneInfo NewZone)
{
	PlayerOwner.GlobalFunc_HeadZoneChange(NewZone);
}

function Handle_Landed(vector HitNormal)
{
	PlayerOwner.GlobalFunc_Landed(HitNormal);
}

function Handle_PainTimer()
{
	PlayerOwner.GlobalFunc_PainTimer();
}

function Handle_PlayerCalcView(out actor ViewActor, out vector CameraLocation, out rotator CameraRotation)
{
	PlayerOwner.GlobalFunc_PlayerCalcView(ViewActor, CameraLocation, CameraRotation);
}

function Handle_PlayerTick(float DeltaTime)
{
	Default_PlayerTick(PlayerOwner, DeltaTime);
}

function Handle_RenderOverlays(Canvas Canvas)
{
	PlayerOwner.GlobalFunc_RenderOverlays(Canvas);
}

function Handle_Touch(Actor A)
{
	PlayerOwner.GlobalFunc_Touch(A);
}

function Handle_UnTouch(Actor A)
{
	PlayerOwner.GlobalFunc_UnTouch(A);
}

function Handle_UpdateEyeHeight(float DeltaTime)
{
	PlayerOwner.GlobalFunc_UpdateEyeHeight(DeltaTime);
}

function Handle_ZoneChange(ZoneInfo NewZone)
{
	PlayerOwner.GlobalFunc_ZoneChange(NewZone);
}

// -----------------------------------------------------------------------------
// Handle exec functions

function Handle_ActivateInventoryItem(class InvItem)
{
	PlayerOwner.GlobalFunc_ActivateInventoryItem(InvItem);
}

function Handle_ActivateItem()
{
	PlayerOwner.GlobalFunc_ActivateItem();
}

function Handle_AltFire(optional float F)
{
	PlayerOwner.GlobalFunc_AltFire(F);
}

function Handle_FeignDeath();

function Handle_Fire(optional float F)
{
	PlayerOwner.GlobalFunc_Fire(F);
}

function Handle_Grab()
{
	PlayerOwner.GlobalFunc_Grab();
}

function Handle_Suicide()
{
	PlayerOwner.GlobalFunc_Suicide();
}

function Handle_Taunt(name Sequence)
{
	PlayerOwner.GlobalFunc_Taunt(Sequence);
}

function Handle_ThrowWeapon()
{
	PlayerOwner.GlobalFunc_ThrowWeapon();
}

function Handle_Walk()
{
	PlayerOwner.GlobalFunc_Walk();
}

// -----------------------------------------------------------------------------
// Handle regular functions

function Handle_AddVelocity(vector V)
{
	PlayerOwner.GlobalFunc_AddVelocity(V);
}

simulated function bool Handle_AdjustHitLocation(out vector HitLocation, vector TraceDir)
{
	return PlayerOwner.GlobalFunc_AdjustHitLocation(HitLocation, TraceDir);
}

function Handle_ChangedWeapon()
{
	PlayerOwner.GlobalFunc_ChangedWeapon();
}

function Handle_Died(Pawn Killer, name DamageType, vector HitLocation)
{
	PlayerOwner.GlobalFunc_Died(Killer, DamageType, HitLocation);
}

function Handle_DoJump(optional float F)
{
	PlayerOwner.GlobalFunc_DoJump(F);
}

function Handle_HandleWalking()
{
	PlayerOwner.GlobalFunc_HandleWalking();
}

function Handle_PlayChatting()
{
	PlayerOwner.GlobalFunc_PlayChatting();
}

function Handle_ProcessMove(float DeltaTime, vector NewAccel, eDodgeDir DodgeMove, rotator DeltaRot)
{
	PlayerOwner.GlobalFunc_ProcessMove(DeltaTime, NewAccel, DodgeMove, DeltaRot);
}

function Handle_StartClimbing(LadderTrigger Ladder)
{
	PlayerOwner.GlobalFunc_StartClimbing(Ladder);
}

function Handle_TakeDamage(int Damage, Pawn InstigatedBy, vector HitLocation, vector Momentum, name DamageType)
{
	PlayerOwner.GlobalFunc_TakeDamage(Damage, InstigatedBy, HitLocation, Momentum, DamageType);
}

function Handle_UpdateRotation(float DeltaTime, float MaxPitch)
{
	PlayerOwner.GlobalFunc_UpdateRotation(DeltaTime, MaxPitch);
}

defaultproperties
{
	RemoteRole=ROLE_AutonomousProxy
}
This interface is used by new state Engine.PlayerPawn.CustomPlayerState:

Code: Select all

state CustomPlayerState
{
	event AnimEnd()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_AnimEnd();
	}

	singular event BaseChange()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_BaseChange();
	}

	event BeginState()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.GotoState('Active');
	}

	event Bump(Actor A)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Bump(A);
	}

	event EndState()
	{
		if (CustomPlayerStateInfo != none)
		{
			if (CustomPlayerStateInfo.Role < ROLE_Authority)
				CustomPlayerStateInfo.GotoState('Inactive');
			else
				CustomPlayerStateInfo.Destroy();
			CustomPlayerStateInfo = none;
		}
	}

	event Falling()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Falling();
	}

	event FootZoneChange(ZoneInfo NewZone)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_FootZoneChange(NewZone);
	}

	event HeadZoneChange(ZoneInfo NewZone)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_HeadZoneChange(NewZone);
	}

	event Landed(vector HitNormal)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Landed(HitNormal);
	}

	event PainTimer()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_PainTimer();
	}

	event PlayerCalcView(out actor ViewActor, out vector CameraLocation, out rotator CameraRotation)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_PlayerCalcView(ViewActor, CameraLocation, CameraRotation);
		else
			global.PlayerCalcView(ViewActor, CameraLocation, CameraRotation);
	}

	event PlayerTick(float DeltaTime)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_PlayerTick(DeltaTime);
		else
			class'CustomPlayerStateInfo'.static.Default_PlayerTick(self, DeltaTime);
	}

	event RenderOverlays(Canvas Canvas)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_RenderOverlays(Canvas);
		else
			global.RenderOverlays(Canvas);
	}

	event Touch(Actor A)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Touch(A);
	}

	event UnTouch(Actor A)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_UnTouch(A);
	}

	event UpdateEyeHeight(float DeltaTime)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_UpdateEyeHeight(DeltaTime);
		else
			global.UpdateEyeHeight(DeltaTime);
	}

	event ZoneChange(ZoneInfo NewZone)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ZoneChange(NewZone);
	}

	exec function ActivateInventoryItem(class InvItem)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ActivateInventoryItem(InvItem);
	}

	exec function ActivateItem()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ActivateItem();
	}

	exec function AltFire(optional float F)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_AltFire(F);
	}

	exec function FeignDeath()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_FeignDeath();
	}

	exec function Fire(optional float F)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Fire(F);
	}

	exec function Grab()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Grab();
	}

	exec function Suicide()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Suicide();
	}

	exec function Taunt(name Sequence)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Taunt(Sequence);
	}

	exec function ThrowWeapon()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ThrowWeapon();
	}

	exec function Walk()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Walk();
	}

	function AddVelocity(vector V)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_AddVelocity(V);
	}

	simulated function bool AdjustHitLocation(out vector HitLocation, vector TraceDir)
	{
		if (CustomPlayerStateInfo != none)
			return CustomPlayerStateInfo.Handle_AdjustHitLocation(HitLocation, TraceDir);
		return global.AdjustHitLocation(HitLocation, TraceDir);
	}

	function ChangedWeapon()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ChangedWeapon();
	}

	function Died(Pawn Killer, name DamageType, vector HitLocation)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_Died(Killer, DamageType, HitLocation);
	}

	function DoJump(optional float F)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_DoJump(F);
	}

	function HandleWalking()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_HandleWalking();
	}

	function PlayChatting()
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_PlayChatting();
	}

	function ProcessMove(float DeltaTime, vector NewAccel, eDodgeDir DodgeMove, rotator DeltaRot)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_ProcessMove(DeltaTime, NewAccel, DodgeMove, DeltaRot);
	}

	function StartClimbing(LadderTrigger Ladder)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_StartClimbing(Ladder);
	}

	function TakeDamage(int Damage, Pawn InstigatedBy, vector HitLocation, vector Momentum, name DamageType)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_TakeDamage(Damage, InstigatedBy, HitLocation, Momentum, DamageType);
	}

	function UpdateRotation(float DeltaTime, float MaxPitch)
	{
		if (CustomPlayerStateInfo != none)
			CustomPlayerStateInfo.Handle_UpdateRotation(DeltaTime, MaxPitch);
	}
}
Variable CustomPlayerStateInfo is declared in class PlayerPawn:

Code: Select all

var CustomPlayerStateInfo CustomPlayerStateInfo;
State functions of Engine.PlayerPawn.CustomPlayerState delegate calls to member functions of CustomPlayerStateInfo. We can override some of these member functions in subclasses of Engine.CustomPlayerStateInfo. For example, the corresponding handler for the following definition of ZoneChange

Code: Select all

	event ZoneChange(ZoneInfo NewZone)
	{
		if (NewZone.bWaterZone != Region.Zone.bWaterZone)
		{
			SetFlightProperties(self);
			if (!NewZone.bWaterZone && Region.Zone.bWaterZone && GetAnimGroup(AnimSequence) != 'Waiting')
				TweenToWaiting(0.1);
		}
		global.ZoneChange(NewZone);
	}
would be

Code: Select all

function Handle_ZoneChange(ZoneInfo NewZone)
{
	if (NewZone.bWaterZone != PlayerOwner.Region.Zone.bWaterZone)
	{
		SetFlightProperties(PlayerOwner);
		if (!NewZone.bWaterZone && PlayerOwner.Region.Zone.bWaterZone && PlayerOwner.GetAnimGroup(PlayerOwner.AnimSequence) != 'Waiting')
			PlayerOwner.TweenToWaiting(0.1);
	}
	PlayerOwner.GlobalFunc_ZoneChange(NewZone);
}
That is, we perform the following transformations to the normal state function (in order):

1. The name of the function is prefixed by Handle_

F.e., ZoneChange becomes Handle_ZoneChange

2. Every occurrence of global. is replaced by GlobalFunc_

F.e., global.ZoneChange(NewZone) becomes GlobalFunc_ZoneChange(NewZone)

3. Every access to a variable or a global-scope non-static member function of PlayerPawn without using the self keyword explicitly is transformed into the corresponding member access using the self keyword.

F.e., GlobalFunc_ZoneChange(NewZone) becomes self.GlobalFunc_ZoneChange(NewZone)

4. Every use of the self keyword is replaced with variable name PlayerOwner.

F.e. self.GlobalFunc_ZoneChange(NewZone) becomes PlayerOwner.GlobalFunc_ZoneChange(NewZone)

Here's the transformed implementation of our state PlayerUsingJetpack:

Code: Select all

class UsingJetpack expands CustomPlayerStateInfo;

function Handle_AnimEnd()
{
	PlayerOwner.bAnimTransition = false;

	if (!PlayerOwner.Region.Zone.bWaterZone || PlayerOwner.Acceleration Dot vector(PlayerOwner.ViewRotation) <= 0)
	{
		if (PlayerOwner.GetAnimGroup(PlayerOwner.AnimSequence) == 'TakeHit')
		{
			PlayerOwner.bAnimTransition = true;
			PlayerOwner.TweenToWaiting(0.2);
		}
		else
			PlayerOwner.PlayWaiting();
	}
	else
	{
		if (PlayerOwner.GetAnimGroup(PlayerOwner.AnimSequence) == 'TakeHit')
		{
			PlayerOwner.bAnimTransition = true;
			PlayerOwner.TweenToSwimming(0.2);
		}
		else
			PlayerOwner.PlaySwimming();
	}
}

function Handle_PlayerTick(float DeltaTime)
{
	if (PlayerOwner.bUpdatePosition)
		PlayerOwner.ClientUpdatePosition();

	PlayerMove(DeltaTime);
}

function PlayerMove(float DeltaTime)
{
	local vector X, Y, Z;

	GetAxes(PlayerOwner.ViewRotation, X, Y, Z);

	PlayerOwner.aForward *= 0.2;
	PlayerOwner.aStrafe  *= 0.2;
	PlayerOwner.aLookup  *= 0.24;
	PlayerOwner.aTurn    *= 0.24;

	PlayerOwner.Acceleration = PlayerOwner.aForward * X + PlayerOwner.aStrafe * Y + PlayerOwner.aUp * vect(0, 0, 1);
	PlayerOwner.bPressedJump = False;
	// Update rotation.
	PlayerOwner.UpdateRotation(DeltaTime, 2);

	if (PlayerOwner.Role < ROLE_Authority) // then save this move and replicate it
		PlayerOwner.ReplicateMove(DeltaTime, PlayerOwner.Acceleration, DODGE_None, rot(0,0,0));
	else
		PlayerOwner.ProcessMove(DeltaTime, PlayerOwner.Acceleration, DODGE_None, rot(0,0,0));

	if (PlayerOwner.Region.Zone.bWaterZone)
		PlayerOwner.SwimAnimUpdate(vector(PlayerOwner.ViewRotation) Dot PlayerOwner.Acceleration <= 0);
}

function Handle_BeginState()
{
	SetFlightProperties(PlayerOwner);
	if (!PlayerOwner.IsAnimating())
		PlayerOwner.PlayWaiting();
}

function Handle_EndState()
{
	PlayerOwner.AirSpeed = PlayerOwner.default.AirSpeed;
	PlayerOwner.bCanFly = PlayerOwner.default.bCanFly;
}

function Handle_FootZoneChange(ZoneInfo NewZone)
{
	if (NewZone.bWaterZone != PlayerOwner.FootRegion.Zone.bWaterZone)
		SetFlightProperties(PlayerOwner);
}

function Handle_ZoneChange(ZoneInfo NewZone)
{
	if (NewZone.bWaterZone != PlayerOwner.Region.Zone.bWaterZone)
	{
		SetFlightProperties(PlayerOwner);
		if (!NewZone.bWaterZone && PlayerOwner.Region.Zone.bWaterZone && PlayerOwner.GetAnimGroup(PlayerOwner.AnimSequence) != 'Waiting')
			PlayerOwner.TweenToWaiting(0.1);
	}
	PlayerOwner.GlobalFunc_ZoneChange(NewZone);
}

function Handle_HandleWalking()
{
	PlayerOwner.bIsWalking = false;
}

static function SetFlightProperties(Pawn P)
{
	P.bCanFly = true;

	if (P.Physics != PHYS_Flying)
		P.SetPhysics(PHYS_Flying);

	if (P.FootRegion.Zone.bWaterZone || P.Region.Zone.bWaterZone)
		P.AirSpeed = 400;
	else
		P.AirSpeed = 600;
}
Note that PlayerMove and SetFlightProperties are not global-scope functions of Engine.PlayerPawn, so we don't need to prefix them with Handle_.

Other handlers may use the default implementation that generally just calls the corresponding global function of Engine.PlayerPawn:

Code: Select all

final function GlobalFunc_ActivateInventoryItem(class InvItem)
{
	global.ActivateInventoryItem(InvItem);
}

final function GlobalFunc_ActivateItem()
{
	global.ActivateItem();
}

final function GlobalFunc_AddVelocity(vector V)
{
	global.AddVelocity(V);
}

final simulated function bool GlobalFunc_AdjustHitLocation(out vector HitLocation, vector TraceDir)
{
	return global.AdjustHitLocation(HitLocation, TraceDir);
}

final function GlobalFunc_AltFire(optional float F)
{
	global.AltFire(F);
}

final function GlobalFunc_AnimEnd()
{
	global.AnimEnd();
}

final function GlobalFunc_BaseChange()
{
	global.BaseChange();
}

final function GlobalFunc_Bump(Actor A)
{
	global.Bump(A);
}

final function GlobalFunc_ChangedWeapon()
{
	global.ChangedWeapon();
}

final function GlobalFunc_Died(Pawn Killer, name DamageType, vector HitLocation)
{
	global.Died(Killer, DamageType, HitLocation);
}

final function GlobalFunc_DoJump(optional float F)
{
	global.DoJump(F);
}

final function GlobalFunc_Falling()
{
	global.Falling();
}

final function GlobalFunc_Fire(optional float F)
{
	global.Fire(F);
}

final function GlobalFunc_FootZoneChange(ZoneInfo NewZone)
{
	global.FootZoneChange(NewZone);
}

final function GlobalFunc_Grab()
{
	global.Grab();
}

final function GlobalFunc_HeadZoneChange(ZoneInfo NewZone)
{
	global.HeadZoneChange(NewZone);
}

final function GlobalFunc_HandleWalking()
{
	global.HandleWalking();
}

final function GlobalFunc_Landed(vector HitNormal)
{
	global.Landed(HitNormal);
}

final function GlobalFunc_PainTimer()
{
	global.PainTimer();
}

final function GlobalFunc_PlayerCalcView(out actor ViewActor, out vector CameraLocation, out rotator CameraRotation)
{
	global.PlayerCalcView(ViewActor, CameraLocation, CameraRotation);
}

final function GlobalFunc_PlayChatting()
{
	global.PlayChatting();
}

final function GlobalFunc_ProcessMove(float DeltaTime, vector NewAccel, eDodgeDir DodgeMove, rotator DeltaRot)
{
	global.ProcessMove(DeltaTime, NewAccel, DodgeMove, DeltaRot);
}

final simulated function GlobalFunc_RenderOverlays(Canvas Canvas)
{
	global.RenderOverlays(Canvas);
}

final function GlobalFunc_StartClimbing(LadderTrigger Ladder)
{
	global.StartClimbing(Ladder);
}

final function GlobalFunc_Suicide()
{
	global.Suicide();
}

final function GlobalFunc_TakeDamage(int Damage, Pawn InstigatedBy, vector HitLocation, vector Momentum, name DamageType)
{
	global.TakeDamage(Damage, InstigatedBy, HitLocation, Momentum, DamageType);
}

final function GlobalFunc_Taunt(name Sequence)
{
	global.Taunt(Sequence);
}

final function GlobalFunc_ThrowWeapon()
{
	global.ThrowWeapon();
}

final function GlobalFunc_Touch(Actor A)
{
	global.Touch(A);
}

final function GlobalFunc_UnTouch(Actor A)
{
	global.UnTouch(A);
}

final function GlobalFunc_UpdateEyeHeight(float DeltaTime)
{
	global.UpdateEyeHeight(DeltaTime);
}

final function GlobalFunc_UpdateRotation(float DeltaTime, float MaxPitch)
{
	global.UpdateRotation(DeltaTime, MaxPitch);
}

final function GlobalFunc_Walk()
{
	global.Walk();
}

final function GlobalFunc_ZoneChange(ZoneInfo NewZone)
{
	global.ZoneChange(NewZone);
}
The only recommended way to enter state CustomPlayerState is by calling the GotoCustomPlayerState function defined in Engine.PlayerPawn:

Code: Select all

function bool GotoCustomPlayerState(class<CustomPlayerStateInfo> PlayerStateInfoClass)
{
	if (PlayerStateInfoClass == none)
		return false;

	if (CustomPlayerStateInfo == none || CustomPlayerStateInfo.bDeleteMe || CustomPlayerStateInfo.Class != PlayerStateInfoClass)
		return SetCustomPlayerStateInfo(Spawn(PlayerStateInfoClass, self));

	if (GetStateName() != 'CustomPlayerState')
		GotoState('CustomPlayerState');
	else if (!CustomPlayerStateInfo.IsInState('Active'))
		CustomPlayerStateInfo.GotoState('Active');
	else
		return false;

	return true;
}
This function can be invoked server-side and client-side. By default, CustomPlayerStateInfo's RemoteRole is ROLE_AutonomousProxy, so when an instance of CustomPlayerStateInfo is created server-side, it's replicated to the owner's client, and then the client-side actor adjusts the state of the client's PlayerPawn to match the server-side one. Therefore, when we don't need a client-side prediction of state changes, we can rely on server-side calls to GotoCustomPlayerState.

The code of activation of Jetpack could look like this:

Code: Select all

class Jetpack expands Pickup;

#exec AUDIO IMPORT FILE="Sounds\Jetpack.wav" NAME="JetpackSnd"

var() sound FlightSound;
var Actor EffectsActor;

state Activated
{
	event BeginState()
	{
		local Pawn OwnerPawn;

		OwnerPawn = Pawn(Owner);
		if (OwnerPawn == none || --Charge <= 0)
		{
			UsedUp();
			return;
		}

		if (PlayerPawn(Owner) != none)
		{
			// if the owner is a PlayerPawn, let it use the special state
			PlayerPawn(Owner).GotoCustomPlayerState(class'UsingJetpack');
			if (UsingJetpack(PlayerPawn(Owner).CustomPlayerStateInfo) == none)
			{
				// if we failed to enter the special state for some reason, deactivate the Jetpack
				// and keep the owner's properties intact
				Activate();
				return;
			}
		}
		else
		{
			// if the owner is not a PlayerPawn, modify its properties to make it flying
			SetFlightProperties(OwnerPawn);
		}

		EnableInventoryEffects();

		bActive = true;
		SetTimer(0.1, true);
	}

	static function SetFlightProperties(Pawn P)
	{
		P.bCanFly = true;

		if (P.Physics != PHYS_Flying)
			P.SetPhysics(PHYS_Flying);

		if (P.FootRegion.Zone.bWaterZone || P.Region.Zone.bWaterZone)
			P.AirSpeed = 400;
		else
			P.AirSpeed = 600;
	}
Since both definitions of SetFlightProperties in class UsingJetpack and state Jetpack.Activated are identical to each other, it makes sense to define a single global-scope static function SetFlightProperties in class Jetpack and call it in both classes.

We should support two ways to deactivate Jetpack: by leaving the state Jetpack.Activated (as a result of calling Activate or UsedUp) and by leaving the special state UsingJetpack (as a result of setting the PlayerPawn to use other state). In either case deactivation PlayerPawn's Jetpack should imply end of the period when Jetpack.Activated and the special player state are used, but leaving these states can be done in a different order, so we should ensure that leaving any of these states invokes an appropriate change of the other state (and, of course, an infinite recursion should be avoided). In case of Jetpack.Activated, we can define EndState as follows:

Code: Select all

	event EndState()
	{
		local Pawn OwnerPawn;

		DisableInventoryEffects();
		OwnerPawn = Pawn(Owner);
		if (OwnerPawn == none)
		{
			bActive = false;
			return;
		}

		OwnerPawn.AirSpeed = OwnerPawn.default.AirSpeed;
		OwnerPawn.bCanFly = OwnerPawn.default.bCanFly;

		if (bActive)
		{
			bActive = false;

			if (Owner.Region.Zone.bWaterZone)
			{
				Owner.SetPhysics(PHYS_Swimming);
				Owner.GoToState('PlayerSwimming');
			}
			else
			{
				Owner.SetPhysics(PHYS_Falling);
				Owner.GoToState('PlayerWalking');
			}
		}
	}
Calling this function first corresponds to the normal execution order which should take place most of the time. In this case, EndState takes the responsibility to determine the next state of the owner.

UsingJetpack.Handle_EndState can be defined as:

Code: Select all

function Handle_EndState()
{
	local Inventory Inv;

	if (Level.NetMode == NM_Client)
		return;

	for (Inv = PlayerOwner.Inventory; Inv != none; Inv = Inv.Inventory)
		if (Inv.Class == class'Jetpack' && Inv.bActive)
		{
			Inv.bActive = false;
			Inv.Activate();
		}
}
This function may be invoked before entering Jetpack.Activated.EndState when the state of the PlayerPawn owning the Jetpack is changed externally.

This is how we can define Jetpack.uc:

Code: Select all

class Jetpack expands Pickup;

#exec AUDIO IMPORT FILE="Sounds\Jetpack.wav" NAME="JetpackSnd"

var() sound FlightSound;
var Actor EffectsActor;

state Activated
{
	event BeginState()
	{
		local Pawn OwnerPawn;

		OwnerPawn = Pawn(Owner);
		if (OwnerPawn == none || --Charge <= 0)
		{
			UsedUp();
			return;
		}

		if (PlayerPawn(Owner) != none)
		{
			PlayerPawn(Owner).GotoCustomPlayerState(class'UsingJetpack');
			if (UsingJetpack(PlayerPawn(Owner).CustomPlayerStateInfo) == none)
			{
				Activate();
				return;
			}
		}

		SetFlightProperties(OwnerPawn);
		EnableInventoryEffects();

		bActive = true;
		SetTimer(0.1, true);
	}

	event EndState()
	{
		local Pawn OwnerPawn;

		DisableInventoryEffects();
		OwnerPawn = Pawn(Owner);
		if (OwnerPawn == none)
		{
			bActive = false;
			return;
		}

		OwnerPawn.AirSpeed = OwnerPawn.default.AirSpeed;
		OwnerPawn.bCanFly = OwnerPawn.default.bCanFly;
		if (bActive)
		{
			bActive = false;

			if (Owner.Region.Zone.bWaterZone)
			{
				Owner.SetPhysics(PHYS_Swimming);
				Owner.GoToState('PlayerSwimming');
			}
			else
			{
				Owner.SetPhysics(PHYS_Falling);
				Owner.GoToState('PlayerWalking');
			}
		}
	}

	event Timer()
	{
		if (Pawn(Owner) == none || --Charge <= 0)
			UsedUp();
		else
			SetFlightProperties(Pawn(Owner));
	}
}

state DeActivated
{
Begin:
}

function Activate()
{
	if (Owner.bCollideWorld && !Owner.IsInState('FeigningDeath') &&
		(Owner.Physics == PHYS_Walking || Owner.Physics == PHYS_Falling || Owner.Physics == PHYS_Swimming || Owner.Physics == PHYS_Flying))
	{
		super.Activate();
	}
}

static function SetFlightProperties(Pawn P)
{
	P.bCanFly = true;

	if (P.Physics != PHYS_Flying)
		P.SetPhysics(PHYS_Flying);

	if (P.FootRegion.Zone.bWaterZone || P.Region.Zone.bWaterZone)
		P.AirSpeed = 400;
	else
		P.AirSpeed = 600;
}

function EnableInventoryEffects()
{
	EffectsActor = Spawn(class'Triggers', Owner,, Owner.Location);
	if (EffectsActor != none)
	{
		EffectsActor.SetPhysics(PHYS_Trailer);
		EffectsActor.RemoteRole = ROLE_SimulatedProxy;
		EffectsActor.AmbientSound = FlightSound;
	}
}

function DisableInventoryEffects()
{
	if (EffectsActor != none)
	{
		EffectsActor.Destroy();
		EffectsActor = none;
	}
}

defaultproperties
{
	ActivateSound=None
	AmbientGlow=64
	bActivatable=True
	bAutoActivate=True
	bDisplayableInv=True
	bMeshEnviroMap=True
	Charge=600
	CollisionRadius=22.000000
	CollisionHeight=7.000000
	ExpireMessage="Jetpack ran out of fuel"
	FlightSound=Sound'Jetpack.JetpackSnd'
	Icon=Texture'Jetpack.JetpackIcon'
	ItemName="Jetpack"
	MaxDesireability=0.500000
	Mesh=LodMesh'UnrealShare.KevSuit'
	PickupMessage="You picked up a Jetpack"
	PickupSound=Sound'UnrealShare.Pickups.GenPickSnd'
	PickupViewMesh=LodMesh'UnrealShare.KevSuit'
	RemoteRole=ROLE_DumbProxy
	RespawnTime=120.000000
	SwimSound=Sound'Jetpack.JetpackBubblesSnd'
	Texture=Texture'UnrealShare.NewGreen'
}
A small demo:
User avatar
Rocky
OldUnreal Member
Posts: 99
Joined: Mon Nov 18, 2013 9:21 am

Re: [227j] New state in class PlayerPawn: CustomPlayerState

Post by Rocky »

Nice feature. Just a stupid question. Will the player with high ping get any movements lags when playing online?
User avatar
Masterkent
OldUnreal Member
Posts: 1469
Joined: Fri Apr 05, 2013 12:41 pm

Re: [227j] New state in class PlayerPawn: CustomPlayerState

Post by Masterkent »

Will the player with high ping get any movements lags when playing online?
You can implement nearly immediate client-side response with a custom state, but making movements completely lag-free is impossible.

For example, the implementation of Jetpack above lets your client-side PlayerPawn move right after you pushed a movement button without waiting for a response from the server. However, the server cannot determine that your movement has started before the corresponding signal reaches the server, so server-side movement will be delayed.
Last edited by Masterkent on Sun Sep 16, 2018 7:53 pm, edited 1 time in total.
User avatar
Alien3674
OldUnreal Member
Posts: 23
Joined: Thu Aug 10, 2017 11:06 pm
Location: Russia

Re: [227j] New state in class PlayerPawn: CustomPlayerState

Post by Alien3674 »

Maybe not at all on the topic.
For some simple tasks in the player's class, you need to write \ reassign the class pack (for the changes to take effect). It is necessary to take into account what character the player chooses ... In general, UT2004 and later used the separation of the class of the pastern into the controller class (player, or AI) and the actual model class (in which the choice of animations was described, etc.). Very convenient, and what really is not enough.
Post Reply

Return to “UScript Board”