/** * SPPlayerController * PlayerController class for the Isometric Demo player. */ class SPPlayerController extends GamePlayerController; /***************************************************** * Targetting/attacking declarations *****************************************************/ /** Stores intersection of a ray cast from the mouse in 3d coordinate with the world geometry. * This becomes our target location when moving. */ var transient Vector MouseLocationWorld; /** Stores the actor last found from mouse cursor traces. */ var transient Actor MouseOverActor; /** Stores the value of MouseLocationWorld at the time of the last click. */ var transient Vector AttackLocation; /** Stores the value of MouseOverActor at the time of the last click. */ var transient Actor SelectedActor; /** The amount of time to remain locked toward a target location after the weapon fire time is up. */ var const float LockTargetLocationDelay; /***************************************************** * Movement var declarations *****************************************************/ /** Minimum distance to target location to warrant moving. */ var const float MinimumDistanceToMove; /** The interval at which we will process actions that occur during a movement button hold. */ var const float MovementHoldCheckInterval; /** Tracks when the movement button has been pressed. */ var transient bool bMovementButtonPressed; /** Tracks how long the movement button has been pressed. */ var transient float TimeMovementButtonPressed; /** The amount of time considered to be a single movment button click and not a button hold. */ var const float MovementSingleClickDuration; /** Direction that the player should keep moving when movement direction is locked. */ var transient Vector LockedMovementDir; /** If pawn falls below this velocity while moving in a straight line, we assume that it has gotten stuck. */ var const float StraightLineMinimumVelocity; /** If pawn falls below this velocity while pathfinding, we assume that it has gotten stuck. */ var const float PathfindingMinimumVelocity; /***************************************************** * Navmesh var declarations *****************************************************/ /** Final destination for nav-mesh pathfinding. */ var transient Vector NavMeshFinalDestination; /** Next point along the nav-mesh path. */ var transient Vector NavMeshNextDestination; /***************************************************** * Player input functions *****************************************************/ /** * Receives attack button presses from the user. */ exec function AttackButtonPressed() { local SPPawn TargetPawn; if (Pawn == none || Pawn.Weapon == none || SPWeapon(Pawn.Weapon) == none || Pawn.Weapon.IsFiring()) return; // Only shoot at an actor if it implements SPTargetableInterface if (MouseOverActor != none && SPTargetableInterface(MouseOverActor) != none) { TargetPawn = SPPawn(MouseOverActor); // If we're attacking a pawn, target its best attack spot bone (specified by AttackTargetBoneName). if (TargetPawn != none) AttackLocation = TargetPawn.Mesh.GetBoneLocation(TargetPawn.AttackTargetBoneName); // Otherwise just aim the attack to hit the actor at its origin. else AttackLocation = MouseOverActor.Location; SelectedActor = MouseOverActor; // If we are not already attacking, face the target. SPPawn(Pawn).TurnWithAPurpose(AttackLocation, ReadyToAttack); } // If not clicking on an actor, we'll attack in the direction of the mouse else { AttackLocation = MouseLocationWorld; // Temporarily set the attack location height to the pawn's eye height, so it doesn't pitch trying to look at it. AttackLocation.Z = Pawn.GetPawnViewLocation().Z; // If we are not already attacking, face the target. SPPawn(Pawn).TurnWithAPurpose(AttackLocation, ReadyToAttack); // Now that we've done our turn, store an attack location level with the player character's // weapon (GetWeaponStartTraceLocation). // This will allow the player to shoot into the distance by clicking on a closer point on the ground. AttackLocation.Z = Pawn.GetWeaponStartTraceLocation().Z; SelectedActor = none; } // Lock the pawn's direction for the duration of the attack (the weapon's attack time plus a buffer). SPPawn(Pawn).LockDirection(Pawn.Weapon.FireInterval[0] + LockTargetLocationDelay); } /** * Called from the pawn when it has turned and is ready attack. */ function ReadyToAttack() { // We've turned to our target. Fire! if (Pawn != none) SPWeapon(Pawn.Weapon).Attack(); // Done attacking; clear our record of what and where we clicked. AttackLocation = Vect(0.0, 0.0, 0.0); SelectedActor = none; } /** * Handles movement button presses from the user. */ exec function MovementButtonPressed() { bMovementButtonPressed = true; TimeMovementButtonPressed = 0.0; // Begin the move. SetDestinationPosition(MouseLocationWorld); GotoState('MovingStraightLine'); // Start a timer that will loop as long as the movement button is continuously held. SetTimer(MovementHoldCheckInterval, true, nameof(MovementButtonHeld)); } /** * Handles when the movement button is held continuously. */ function MovementButtonHeld() { local Vector VectorToDest; if (Pawn == none) return; TimeMovementButtonPressed += MovementHoldCheckInterval; // Update the direction we are moving while holding the movement button down. VectorToDest.X = MouseLocationWorld.X - Pawn.Location.X; VectorToDest.Y = MouseLocationWorld.Y - Pawn.Location.Y; VectorToDest.Z = 0; // Only move if the mouse is outside a the minimum distance. // NOTE: our direction vector is scaled by twice the minimum distance, however, // because otherwise the pawn will cover the distance before this timer fires again // causing him to stop and start repeatedly. if (VSize2D(VectorToDest) > MinimumDistanceToMove) SetDestinationPosition(Pawn.Location + Normal(VectorToDest) * MinimumDistanceToMove * 2); // Keep moving! Don't exit locked movement state, though. if (!IsInState('MovingContinuouslyLocked')) GotoState('MovingStraightLine', 'Begin'); } /** * Receives movement button releases from the user. */ exec function MovementButtonReleased() { local Vector PawnEyeLocation; if (Pawn == none) return; bMovementButtonPressed = false; // Clear the timer that catches movement button holds. ClearTimer(nameof(MovementButtonHeld)); // If the button was only depressed long enough to be considered a click... if (TimeMovementButtonPressed < MovementSingleClickDuration) { // Calculate the pawn eye location for checking line of sight. PawnEyeLocation = Pawn.Location + Pawn.EyeHeight * vect(0,0,1); // Check line of sight against world geometry. If LOS is clear finish the move. if (FastTrace(MouseLocationWorld, PawnEyeLocation,, true)) { SetDestinationPosition(MouseLocationWorld); GotoState('MovingStraightLine', 'Begin'); } // If we do not have a clear LOS start using nav-mesh pathfinding. else if (!IsInState('MovingNavMesh')) GotoState('MovingNavMesh'); } // If we just finished holding the button, then stop moving. else GotoState(''); } /** * Start moving and lock movement direction. */ exec function LockMovementDirectionAndStartMoving() { if (Pawn == none) return; // This function gets called when the movement button is released. bMovementButtonPressed = false; // Determine the movement direction that we're locking in. Ignore Z. LockedMovementDir.X = MouseLocationWorld.X - Pawn.Location.X; LockedMovementDir.Y = MouseLocationWorld.Y - Pawn.Location.Y; LockedMovementDir.Z = 0.0; // Determine the movement direction that we're locking in. Only use Yaw. SPPawn(Pawn).FaceYaw(Rotator(LockedMovementDir).Yaw); GotoState('MovingContinuouslyLocked'); } /** * If already in a moving state, enter a new movment state with a locked direction. */ exec function LockMovementDirectionWhileMoving() { local Vector Destination; if (Pawn == none || (!IsInState('MovingStraightLine') && !IsInState('MovingContinuously'))) return; Destination = GetDestinationPosition(); // If we were already holding the movement button, clear the associated timer. if (IsTimerActive(nameof(MovementButtonHeld))) ClearTimer(nameof(MovementButtonHeld)); // Determine the movement direction that we're locking in. Ignore Z. LockedMovementDir.X = Destination.X - Pawn.Location.X; LockedMovementDir.Y = Destination.Y - Pawn.Location.Y; LockedMovementDir.Z = 0.0; GotoState('MovingContinuouslyLocked'); } /** * Unlock movement direction and exit continuous movement. */ exec function UnlockMovementDirection() { // Stop moving if (IsInState('MovingContinuouslyLocked')) GotoState(''); } /** * Zooms in the player's camera. */ exec function CameraZoomIn() { SPIsometricCamera(PlayerCamera).ZoomIn(); } /** * Zooms out the player's camera. */ exec function CameraZoomOut() { SPIsometricCamera(PlayerCamera).ZoomOut(); } /***************************************************** * /end player input *****************************************************/ /***************************************************** * Access functions *****************************************************/ /** * Get the mouse click location out of the controller. */ simulated function Vector GetAttackLocation() { return AttackLocation; } /** * Get the actor being targetted by the player's mouse. */ simulated function Actor GetSelectedActor() { return SelectedActor; } /***************************************************** * /end access functions *****************************************************/ /***************************************************** * Movement functions/states *****************************************************/ /** * Prevent player rotation from being affected by default player input. */ function UpdateRotation(float DeltaTime); /** * Prevent player rotation from being affected by default player input. */ function ProcessViewRotation(float DeltaTime, out Rotator out_ViewRotation, Rotator DeltaRot); /** * Make the player face the mouse location while the button is being held down. */ function PlayerMove(float DeltaTime) { super.PlayerMove(DeltaTime); if (Pawn != none && bMovementButtonPressed) SPPawn(Pawn).FaceYaw(Rotator(MouseLocationWorld - Pawn.Location).Yaw); } /** * Respond to a movement button click by moving straight to a destination within LOS. */ state MovingStraightLine { /** * If state is terminated, disable the timer to check if the pawn gets stuck. */ event EndState(name NextStateName) { if (IsTimerActive(nameof(CheckForStuckage))) ClearTimer(nameof(CheckForStuckage)); // Stop the pawn's movement unless we're starting a different movement // (otherwise we'd see a little animation blip between the two states). if (Pawn != none && NextStateName != 'MovingContinuouslyLocked' && NextStateName != 'MovingNavMesh') Pawn.ZeroMovementVariables(); } /** * Check regularly to see if we've met a minimum velocity, then assume that we've * become blocked and quit moving. */ function CheckForStuckage() { if (Pawn == none || VSize(Pawn.Velocity) < StraightLineMinimumVelocity) GotoState(''); } Begin: // Because we can restart this state from the Begin label, set this timer // here instead of in BeginState(). SetTimer(0.5, true, nameof(CheckForStuckage)); // If we're not already there, move toward the point clicked. MoveTo(GetDestinationPosition()); GotoState(''); } /** * Respond to a movement button hold by moving to the set destination. */ state MovingContinuouslyLocked extends MovingStraightLine { // In case the movement button was pressed prior to movement direction being // locked, ignore the release of the movement button. That will be handled by // UnlockMovementDirection() instead. ignores MovementButtonReleased; /** * Update the destination position. */ function PlayerMove(float DeltaTime) { global.PlayerMove(DeltaTime); if (Pawn != none) SetDestinationPosition(Pawn.Location + LockedMovementDir); else GotoState(''); } Begin: // Clear the movement button held timer lest we get sucked into state MovingStraightLine // when we're done here. if (IsTimerActive(nameof(MovementButtonHeld))) ClearTimer(nameof(MovementButtonHeld)); // Make the player move in the locked direction. while (Pawn != none && VSize2D(GetDestinationPosition() - Pawn.Location) > MinimumDistanceToMove) MoveTo(GetDestinationPosition()); } /***************************************************** * /end movement *****************************************************/ /***************************************************** * Nav-mesh pathfinding state *****************************************************/ state MovingNavMesh { /** * When state begins, set a timer to check if the pawn gets stuck. */ event BeginState(name PreviousStateName) { SetTimer(0.5, true, nameof(CheckForStuckage)); } /** * If state is terminated, disable the timer to check if the pawn gets stuck. */ event EndState(name LastStateName) { if (IsTimerActive(nameof(CheckForStuckage))) ClearTimer(nameof(CheckForStuckage)); // Stop the pawn's movement if (Pawn != none) { // Without this call, future move attempts via // the MovingStraightLine or MovingContinuously states will not work. NavigationHandle.FindPath(); Pawn.ZeroMovementVariables(); } } /** * Check regularly to see if we've met a minimum velocity, then assume that we've * become blocked and quit moving. */ function CheckForStuckage() { if (Pawn == none || VSize(Pawn.Velocity) < PathfindingMinimumVelocity) GotoState(''); } /** * Perform the actual nav-mesh pathfinding to determine the next point in the path. */ function Vector GeneratePathToLocation(Vector Destination) { local Vector NextDest; if (Pawn == none) { GotoState(''); return NextDest; } NextDest = Destination; // Make sure we have a valid navigation handle if (NavigationHandle == None) InitNavigationHandle(); if ((NavigationHandle != None) && !NavigationHandle.PointReachable(Destination)) { class'NavMeshPath_Toward'.static.TowardPoint(NavigationHandle, Destination); class'NavMeshGoal_At'.static.AtLocation(NavigationHandle, Destination, MinimumDistanceToMove, true); if (NavigationHandle.FindPath()) NavigationHandle.GetNextMoveLocation(NextDest, Pawn.GetCollisionRadius()); NavigationHandle.ClearConstraints(); } return NextDest; } Begin: NavMeshFinalDestination = GetDestinationPosition(); // While we have a valid pawn and haven't reached the target yet... while(Pawn != None && VSize2D(NavMeshFinalDestination - Pawn.Location) > MinimumDistanceToMove) { // Get the next point in the path. NavMeshNextDestination = GeneratePathToLocation(NavMeshFinalDestination); // Make the player face that next point. SPPawn(Pawn).FaceYaw(Rotator(NavMeshNextDestination - Pawn.Location).Yaw); // Latently head for the next point. MoveTo(NavMeshNextDestination); } // Exit this state GotoState(''); } /***************************************************** * /end nav-mesh *****************************************************/ /***************************************************** * Death and Taxes *****************************************************/ /** * If our pawn has died, schedule a respawn. */ function PawnDied(Pawn inPawn) { super.PawnDied(inPawn); SetTimer(10.0, false, nameof(RespawnTimer)); // Reset controller state bMovementButtonPressed = false; if (IsTimerActive(nameof(MovementButtonHeld))) ClearTimer(nameof(MovementButtonHeld)); LockedMovementDir=Vect(0.0,0.0,0.0); GotoState(''); } /** * Tracks when it is time for the player to respawn. */ function RespawnTimer() { // Inform the GameInfo that the player needs to respawn. SPGameInfo(WorldInfo.Game).RestartPlayer(self); } /***************************************************** * /end dying/respawn *****************************************************/ defaultproperties { CameraClass=class'SPGame.SPIsometricCamera' LockTargetLocationDelay=0.5 MinimumDistanceToMove=64.0 MovementHoldCheckInterval=0.2 MovementSingleClickDuration=0.4 StraightLineMinimumVelocity=175.0 PathfindingMinimumVelocity=60.0 }