481 lines
14 KiB
C#
481 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices.JavaScript;
|
|
using Godot;
|
|
using RustyOptions;
|
|
|
|
namespace PolarBears.PlayerControllerAddon;
|
|
|
|
public partial class PlayerController : CharacterBody3D
|
|
{
|
|
// User API to important child nodes.
|
|
public Node3D Head;
|
|
public Bobbing Bobbing;
|
|
public FieldOfView FieldOfView;
|
|
public Stamina Stamina;
|
|
public StairsSystem StairsSystem;
|
|
public MantleSystem MantleSystem;
|
|
public DashSystem DashSystem;
|
|
public CapsuleCollider CapsuleCollider;
|
|
public Gravity Gravity;
|
|
public HealthSystem HealthSystem;
|
|
public Mouse Mouse;
|
|
|
|
[Export(PropertyHint.Range, "0,20,0.1,or_greater")]
|
|
public float WalkSpeed { get; set; } = 5.0f;
|
|
[Export(PropertyHint.Range, "0,20,0.1,or_greater")]
|
|
public float SprintSpeed { get; set; } = 7.2f;
|
|
[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
|
|
public float CrouchSpeed { get; set; } = 2.5f;
|
|
[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
|
|
public float CrouchTransitionSpeed { get; set; } = 20.0f;
|
|
|
|
[Export(PropertyHint.Range, "0,5,0.1,or_greater")]
|
|
public float DoubleJumpSpeedFactor { get; set; } = 2f;
|
|
|
|
[Export(PropertyHint.Range, "1,50,1,or_greater")]
|
|
public float ControllerSensitivity { get; set; } = 20f;
|
|
|
|
private bool _canDoubleJump = true;
|
|
private bool _movementEnabled = true;
|
|
|
|
private bool _isTweening = false;
|
|
private record TweenInputs(Vector3 Location, float Duration);
|
|
|
|
private Queue<TweenInputs> _tweenInputs = new Queue<TweenInputs>();
|
|
|
|
private bool _shouldMantle = false;
|
|
private Vector3 _dashLocation = Vector3.Zero;
|
|
private Vector3 _mantleLocation = Vector3.Zero;
|
|
|
|
private float _currentSpeed;
|
|
|
|
private const float DecelerationSpeedFactorFloor = 15.0f;
|
|
private const float DecelerationSpeedFactorAir = 7.0f;
|
|
|
|
private float _lastFrameWasOnFloor = -Mathf.Inf;
|
|
|
|
private const int NumOfHeadCollisionDetectors = 4;
|
|
private RayCast3D[] _headCollisionDetectors;
|
|
|
|
private Vector3 _inputMove = Vector3.Zero;
|
|
private float _inputRotateY = 0.0f;
|
|
private float _inputRotateFloorplane = 0.0f;
|
|
|
|
private bool _isAiming = false;
|
|
|
|
public void OnInputMove(Vector3 value)
|
|
{
|
|
_inputMove = value;
|
|
}
|
|
|
|
public void OnInputRotateY(float value)
|
|
{
|
|
_inputRotateY = value;
|
|
}
|
|
|
|
public void OnInputRotateFloorplane(float value)
|
|
{
|
|
_inputRotateFloorplane = value;
|
|
}
|
|
|
|
public void OnInputAimPressed()
|
|
{
|
|
_isAiming = true;
|
|
GD.Print("Aim pressed");
|
|
}
|
|
public void OnInputAimReleased()
|
|
{
|
|
_isAiming = false;
|
|
GD.Print("Aim released");
|
|
}
|
|
public void OnInputAimCanceled()
|
|
{
|
|
_isAiming = false;
|
|
GD.Print("Aim canceled");
|
|
}
|
|
|
|
|
|
|
|
public override void _Ready()
|
|
{
|
|
_currentSpeed = WalkSpeed;
|
|
|
|
Head = GetNode<Node3D>("Head");
|
|
|
|
_headCollisionDetectors = new RayCast3D[NumOfHeadCollisionDetectors];
|
|
|
|
for (int i = 0; i < NumOfHeadCollisionDetectors; i++)
|
|
{
|
|
_headCollisionDetectors[i] = GetNode<RayCast3D>(
|
|
"HeadCollisionDetectors/HeadCollisionDetector" + i);
|
|
}
|
|
|
|
// Getting dependencies of the components(In godot we manage this from upwards to downwards not vice versa)
|
|
Camera3D camera = GetNode<Camera3D>("Head/CameraSmooth/Camera3D");
|
|
|
|
RayCast3D stairsBelowRayCast3D = GetNode<RayCast3D>("StairsBelowRayCast3D");
|
|
RayCast3D stairsAheadRayCast3D = GetNode<RayCast3D>("StairsAheadRayCast3D");
|
|
|
|
Node3D cameraSmooth = GetNode<Node3D>("Head/CameraSmooth");
|
|
|
|
AnimationPlayer animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
|
|
|
|
// Getting universal setting from GODOT editor to be in sync
|
|
float gravitySetting = (float)ProjectSettings.GetSetting("physics/3d/default_gravity");
|
|
|
|
ColorRect vignetteRect = GetNode<ColorRect>(
|
|
"Head/CameraSmooth/Camera3D/CLVignette(Layer_1)/HealthVignetteRect");
|
|
|
|
ColorRect distortionRect = GetNode<ColorRect>(
|
|
"Head/CameraSmooth/Camera3D/CLDistortion(Layer_2)/HealthDistortionRect");
|
|
|
|
ColorRect blurRect = GetNode<ColorRect>("Head/CameraSmooth/Camera3D/CLBlur(Layer_2)/BlurRect");
|
|
|
|
Node3D mapNode = GetTree().Root.FindChild("Map", true, false) as Node3D;
|
|
|
|
// Getting components
|
|
|
|
Bobbing = GetNode<Bobbing>("Bobbing");
|
|
Bobbing.Init(camera);
|
|
|
|
FieldOfView = GetNode<FieldOfView>("FieldOfView");
|
|
FieldOfView.Init(camera);
|
|
|
|
Stamina = GetNode<Stamina>("Stamina");
|
|
Stamina.SetSpeeds(WalkSpeed, SprintSpeed);
|
|
|
|
StairsSystem = GetNode<StairsSystem>("StairsSystem");
|
|
StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth);
|
|
|
|
MantleSystem = GetNode<MantleSystem>("MantleSystem");
|
|
MantleSystem.Init(Head);
|
|
|
|
DashSystem = GetNode<DashSystem>("DashSystem");
|
|
DashSystem.Init(Head, camera);
|
|
|
|
CapsuleCollider = GetNode<CapsuleCollider>("CapsuleCollider");
|
|
|
|
Gravity = GetNode<Gravity>("Gravity");
|
|
Gravity.Init(gravitySetting);
|
|
|
|
HealthSystem = GetNode<HealthSystem>("HealthSystem");
|
|
|
|
HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams()
|
|
{
|
|
Gravity = Gravity,
|
|
Parent = this,
|
|
Camera = camera,
|
|
AnimationPlayer = animationPlayer,
|
|
Head = Head,
|
|
VignetteRect = vignetteRect,
|
|
DistortionRect = distortionRect,
|
|
BlurRect = blurRect,
|
|
};
|
|
|
|
HealthSystem.Init(healthSystemParams);
|
|
|
|
Mouse = GetNode<Mouse>("Mouse");
|
|
Mouse.Init(Head, camera, HealthSystem.IsDead);
|
|
}
|
|
|
|
private void DisableMovement()
|
|
{
|
|
_movementEnabled = false;
|
|
}
|
|
public void EnableMovement()
|
|
{
|
|
_movementEnabled = true;
|
|
}
|
|
|
|
public void EndTween()
|
|
{
|
|
EnableMovement();
|
|
_isTweening = false;
|
|
}
|
|
|
|
private void TweenToLocation(TweenInputs inputs)
|
|
{
|
|
var (location, duration) = inputs;
|
|
|
|
var tween = GetTree().CreateTween();
|
|
var callback = new Callable(this, MethodName.EndTween);
|
|
|
|
tween.TweenProperty(this, "position", location, duration);
|
|
tween.TweenCallback(callback);
|
|
|
|
DisableMovement();
|
|
_isTweening = true;
|
|
tween.Play();
|
|
}
|
|
|
|
private void QueueTween(TweenInputs inputs)
|
|
{
|
|
_tweenInputs.Enqueue(inputs);
|
|
}
|
|
|
|
private void QueueTween(Vector3 location, float duration)
|
|
{
|
|
QueueTween(new TweenInputs(location, duration));
|
|
}
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
{
|
|
if (_tweenInputs.Count > 0 && !_isTweening)
|
|
TweenToLocation(_tweenInputs.Dequeue());
|
|
|
|
if (Input.IsActionPressed("aim_dash"))
|
|
{
|
|
(_shouldMantle, _dashLocation, _mantleLocation) = DashSystem.PrepareDash();
|
|
}
|
|
|
|
if (Input.IsActionJustReleased("aim_dash"))
|
|
{
|
|
DashSystem.Dash();
|
|
QueueTween(_dashLocation, 0.1f);
|
|
if (_shouldMantle)
|
|
{
|
|
QueueTween(_mantleLocation, 0.1f);
|
|
}
|
|
}
|
|
|
|
var mantleLocationResult = MantleSystem.FindMantleInFrontOfPlayer();
|
|
if (isOnFloorCustom())
|
|
{
|
|
_lastFrameWasOnFloor = Engine.GetPhysicsFrames();
|
|
_canDoubleJump = true;
|
|
}
|
|
|
|
// Adding the gravity
|
|
if (!isOnFloorCustom())
|
|
{
|
|
Velocity = new Vector3(
|
|
x: Velocity.X,
|
|
y: Velocity.Y - (Gravity.CalculateGravityForce() * (float)delta),
|
|
z: Velocity.Z);
|
|
}
|
|
|
|
bool doesCapsuleHaveCrouchingHeight = CapsuleCollider.IsCrouchingHeight();
|
|
|
|
bool isPlayerDead = HealthSystem.IsDead();
|
|
|
|
// Handle Jump input
|
|
if (Input.IsActionJustPressed("jump")
|
|
&& !doesCapsuleHaveCrouchingHeight
|
|
&& !isPlayerDead)
|
|
{
|
|
if (mantleLocationResult.IsSome(out var mantleLocation))
|
|
{
|
|
var duration = 0.1f * mantleLocation.DistanceTo(Position);
|
|
QueueTween(mantleLocation, duration);
|
|
}
|
|
else if (isOnFloorCustom())
|
|
{
|
|
Velocity = new Vector3(
|
|
x: Velocity.X,
|
|
y: Gravity.CalculateJumpForce() * (float)delta,
|
|
z: Velocity.Z);
|
|
}
|
|
else if (!isOnFloorCustom()
|
|
&& _canDoubleJump)
|
|
{
|
|
_canDoubleJump = false;
|
|
Velocity = new Vector3(
|
|
x: Velocity.X,
|
|
y: Gravity.CalculateJumpForce() * (float)delta * DoubleJumpSpeedFactor,
|
|
z: Velocity.Z);
|
|
}
|
|
}
|
|
|
|
bool isHeadTouchingCeiling = IsHeadTouchingCeiling();
|
|
bool doesCapsuleHaveDefaultHeight = CapsuleCollider.IsDefaultHeight();
|
|
|
|
// The code below is required to quickly adjust player's position on Y-axis when there's a ceiling on the
|
|
// trajectory of player's jump and player is standing
|
|
if (isHeadTouchingCeiling && doesCapsuleHaveDefaultHeight)
|
|
{
|
|
Velocity = new Vector3(
|
|
x: Velocity.X,
|
|
y: Velocity.Y - 2.0f,
|
|
z: Velocity.Z);
|
|
}
|
|
|
|
if (!isPlayerDead)
|
|
{
|
|
|
|
// Used both for detecting the moment when we enter into crouching mode and the moment when we're already
|
|
// in the crouching mode
|
|
if (Input.IsActionPressed("crouch") ||
|
|
(doesCapsuleHaveCrouchingHeight && isHeadTouchingCeiling))
|
|
{
|
|
CapsuleCollider.Crouch((float)delta, CrouchTransitionSpeed);
|
|
_currentSpeed = CrouchSpeed;
|
|
}
|
|
// Used both for the moment when we exit the crouching mode and for the moment when we just walk
|
|
else
|
|
{
|
|
CapsuleCollider.UndoCrouching((float)delta, CrouchTransitionSpeed);
|
|
_currentSpeed = WalkSpeed;
|
|
}
|
|
}
|
|
|
|
// Each component of the boolean statement for sprinting is required
|
|
if (Input.IsActionPressed("sprint") && !isHeadTouchingCeiling &&
|
|
!doesCapsuleHaveCrouchingHeight && !isPlayerDead)
|
|
{
|
|
_currentSpeed = SprintSpeed;
|
|
}
|
|
|
|
// Vector2 inputLookDir = Input.GetVector("look_left", "look_right", "look_up", "look_down");
|
|
Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane);
|
|
Mouse.LookAround(-1 * ControllerSensitivity * inputLookDir);
|
|
|
|
// Basis is a 3x4 matrix. It contains information about scaling and rotation of head.
|
|
// By multiplying our Vector3 by this matrix we're doing multiple things:
|
|
// a) We start to operate in global space;
|
|
// b) We're applying to Vector3 the current rotation of "head" object;
|
|
// c) We're applying to Vector3 the current scaling of "head" object;
|
|
Vector3 direction = Head.Transform.Basis * _inputMove;
|
|
|
|
if (isPlayerDead)
|
|
{
|
|
direction = Vector3.Zero;
|
|
}
|
|
|
|
if (isOnFloorCustom())
|
|
{
|
|
// Set velocity based on input direction when on the floor
|
|
if (direction.Length() > 0)
|
|
{
|
|
float availableSpeed = Stamina.AccountStamina(delta, _currentSpeed);
|
|
|
|
float newX = direction.X * availableSpeed;
|
|
float newZ = direction.Z * availableSpeed;
|
|
|
|
Velocity = new Vector3(newX, Velocity.Y, newZ);
|
|
}
|
|
// If there is no input, smoothly decelerate the character on the floor
|
|
else
|
|
{
|
|
float xDeceleration = Mathf.Lerp(Velocity.X, direction.X * _currentSpeed,
|
|
(float)delta * DecelerationSpeedFactorFloor);
|
|
float zDeceleration = Mathf.Lerp(Velocity.Z, direction.Z * _currentSpeed,
|
|
(float)delta * DecelerationSpeedFactorFloor);
|
|
|
|
Velocity = new Vector3(xDeceleration, Velocity.Y, zDeceleration);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float xDeceleration = Mathf.Lerp(Velocity.X, direction.X * _currentSpeed,
|
|
(float)delta * DecelerationSpeedFactorAir);
|
|
float zDeceleration = Mathf.Lerp(Velocity.Z, direction.Z * _currentSpeed,
|
|
(float)delta * DecelerationSpeedFactorAir);
|
|
|
|
Velocity = new Vector3(xDeceleration, Velocity.Y, zDeceleration);
|
|
}
|
|
|
|
if (isPlayerDead)
|
|
{
|
|
MoveAndSlide();
|
|
return;
|
|
}
|
|
|
|
Bobbing.CameraBobbingParams cameraBobbingParams = new Bobbing.CameraBobbingParams
|
|
{
|
|
Delta = (float)delta,
|
|
IsOnFloorCustom = isOnFloorCustom(),
|
|
Velocity = Velocity
|
|
};
|
|
|
|
Bobbing.PerformCameraBobbing(cameraBobbingParams);
|
|
|
|
FieldOfView.FovParameters fovParams = new FieldOfView.FovParameters
|
|
{
|
|
IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
|
|
Delta = (float)delta,
|
|
SprintSpeed = SprintSpeed,
|
|
Velocity = Velocity
|
|
};
|
|
|
|
FieldOfView.PerformFovAdjustment(fovParams);
|
|
|
|
StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams
|
|
{
|
|
IsOnFloorCustom = isOnFloorCustom(),
|
|
IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
|
|
CurrentSpeedGreaterThanWalkSpeed = _currentSpeed > WalkSpeed,
|
|
IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
|
|
Delta = (float)delta,
|
|
FloorMaxAngle = FloorMaxAngle,
|
|
GlobalPositionFromDriver = GlobalPosition,
|
|
Velocity = Velocity,
|
|
GlobalTransformFromDriver = GlobalTransform,
|
|
Rid = GetRid()
|
|
};
|
|
|
|
// TODO: SnapUpStairsCheck influences the ability of player to crouch because of `stepHeightY <= 0.01` part
|
|
// Ideally, it should not. SnapUpStairsCheck and SnapDownStairsCheck should be called, when player is actually
|
|
// on the stairs
|
|
|
|
StairsSystem.UpStairsCheckResult upStairsCheckResult = StairsSystem.SnapUpStairsCheck(upStairsCheckParams);
|
|
|
|
if (upStairsCheckResult.UpdateRequired)
|
|
{
|
|
upStairsCheckResult.Update(this);
|
|
}
|
|
else
|
|
{
|
|
MoveAndSlide();
|
|
|
|
StairsSystem.DownStairsCheckParams downStairsCheckParams = new StairsSystem.DownStairsCheckParams
|
|
{
|
|
IsOnFloor = IsOnFloor(), // TODO: replace on IsOnFloor Custom
|
|
IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(),
|
|
LastFrameWasOnFloor = _lastFrameWasOnFloor,
|
|
CapsuleDefaultHeight = CapsuleCollider.GetDefaultHeight(),
|
|
CurrentCapsuleHeight = CapsuleCollider.GetCurrentHeight(),
|
|
FloorMaxAngle = FloorMaxAngle,
|
|
VelocityY = Velocity.Y,
|
|
GlobalTransformFromDriver = GlobalTransform,
|
|
Rid = GetRid()
|
|
};
|
|
|
|
StairsSystem.DownStairsCheckResult downStairsCheckResult = StairsSystem.SnapDownStairsCheck(
|
|
downStairsCheckParams);
|
|
|
|
if (downStairsCheckResult.UpdateIsRequired)
|
|
{
|
|
downStairsCheckResult.Update(this);
|
|
}
|
|
}
|
|
|
|
StairsSystem.SlideCameraParams slideCameraParams = new StairsSystem.SlideCameraParams
|
|
{
|
|
IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(),
|
|
CurrentSpeedGreaterThanWalkSpeed = _currentSpeed > WalkSpeed,
|
|
BetweenCrouchingAndNormalHeight = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(),
|
|
Delta = (float)delta
|
|
};
|
|
|
|
StairsSystem.SlideCameraSmoothBackToOrigin(slideCameraParams);
|
|
}
|
|
|
|
private bool IsHeadTouchingCeiling()
|
|
{
|
|
for (int i = 0; i < NumOfHeadCollisionDetectors; i++)
|
|
{
|
|
if (_headCollisionDetectors[i].IsColliding())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool isOnFloorCustom()
|
|
{
|
|
return IsOnFloor() || StairsSystem.WasSnappedToStairsLastFrame();
|
|
}
|
|
}
|