1560 lines
60 KiB
C#
1560 lines
60 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Diagnostics;
|
|
using FlaxEngine;
|
|
using FlaxEngine.Assertions;
|
|
using FlaxEngine.Networking;
|
|
using Console = Game.Console;
|
|
|
|
namespace Game;
|
|
|
|
public class PlayerMovementParameters
|
|
{
|
|
// FIXME, should be much smaller but needed to avoid issues with box collider edges against brush edges diagonally
|
|
public float collisionMargin { get; } = 0.031f * 1.666f * 1.85f;
|
|
public float slopeNormal { get; } = 0.7f;
|
|
|
|
public float moveSpeed { get; } = 320f;
|
|
public Float3 gravity { get; } = new Float3(0, -800.0f, 0f);
|
|
/*
|
|
// QW
|
|
public float friction = 4f;
|
|
public float stopspeed = 100f;
|
|
public float accelerationGround = 10f;
|
|
public float jumpVelocity = 270f;
|
|
|
|
public float maxAirSpeed = 320f;
|
|
public float maxAirStrafeSpeed = 30f; //Q2+
|
|
public float airAcceleration = 0f; //Q2+
|
|
public float airStopAcceleration = 0f; //Q2+
|
|
public float airStrafeAcceleration = 0f; //CPM?
|
|
public float strafeAcceleration = 10f; //QW
|
|
public float airControl = 0f; //CPM
|
|
public float stepSize = 16f;
|
|
public float autoJumpTime = 0.4f;
|
|
|
|
public int airStep = 0;
|
|
*/
|
|
|
|
// GOA
|
|
public float friction { get; } = 6f;
|
|
public float stopspeed { get; } = 100f;
|
|
public float accelerationGround { get; } = 12f;
|
|
public float jumpVelocity { get; } = 270f;
|
|
public float jumpBoostTime { get; } = 0.5f;
|
|
public float jumpBoostVelocity { get; } = 100f;
|
|
public int jumpBoostMaxJumps { get; } = 2;
|
|
public bool jumpStairBehavior { get; } = true;
|
|
public bool jumpAdditive { get; } = true;
|
|
|
|
public float maxAirSpeed { get; } = 320f;
|
|
public float maxAirStrafeSpeed { get; } = 30f;
|
|
public float airAcceleration { get; } = 0.4f;
|
|
public float airStopAcceleration { get; } = 2.5f;
|
|
public float airStrafeAcceleration { get; } = 0f;
|
|
public float strafeAcceleration { get; } = 10f;
|
|
public float airControl { get; } = 0f;
|
|
public float stepHeight { get; } = 16f;
|
|
public float autoJumpTime { get; } = 0.4f;
|
|
|
|
public int airStep { get; } = 2;
|
|
}
|
|
|
|
public struct PlayerMovementState
|
|
{
|
|
public Float3 position;
|
|
public Float3 currentVelocity;
|
|
//public Quaternion orientation;
|
|
public Float3 viewAngles; // yaw, pitch, roll
|
|
public float lastJumped = -1f;
|
|
public float lastLanded = -1f;
|
|
public int numJumps;
|
|
public bool jumped;
|
|
public bool onGround;
|
|
|
|
|
|
public PlayerMovementState()
|
|
{
|
|
}
|
|
}
|
|
|
|
public struct TraceInfo
|
|
{
|
|
public RayCastHit[] hitInfos;
|
|
public bool startSolid;
|
|
|
|
// closest hit
|
|
public float fraction;
|
|
public Float3 endPosition;
|
|
public Float3 hitNormal;
|
|
public Float3 hitPosition;
|
|
|
|
// furthest hit
|
|
//public float maxFraction;
|
|
//public Float3 maxHitNormal;
|
|
//public Float3 maxEndPosition;
|
|
}
|
|
|
|
public class PlayerMovement : Script
|
|
{
|
|
private PlayerMovementParameters movementParameters = new PlayerMovementParameters();
|
|
public PlayerMovementState movementState = new PlayerMovementState();
|
|
|
|
private readonly bool demoDeltasCorrect = true;
|
|
private readonly bool demoDeltasVerify = true;
|
|
|
|
|
|
//private WorldStateManager worldStateManager;
|
|
|
|
private World _world;
|
|
private World World => _world ??= this.GetWorld();
|
|
|
|
private bool predicting = false;
|
|
|
|
private readonly List<PhysicsColliderActor> touchingActors = new List<PhysicsColliderActor>();
|
|
//public int currentInputFrame;
|
|
//private int currentInputFrame2;
|
|
|
|
[NoSerialize, HideInEditor]
|
|
public IPlayerInput Input => playerActor?.Input ?? PlayerInputNone.Instance;
|
|
|
|
//private bool physicsInteractions = false;
|
|
|
|
public bool OnGround => movementState.onGround;
|
|
|
|
//private int lastInputFrame;
|
|
|
|
private float simulationTime = 0f;
|
|
|
|
|
|
private RigidBody rigidBody;
|
|
private Actor cameraHolder;
|
|
|
|
private PlayerActor playerActor;
|
|
public Actor rootActor;
|
|
|
|
public Float3 viewAngles;
|
|
private Float3 viewAnglesLastFrame;
|
|
|
|
public uint PlayerId => playerActor ? playerActor.PlayerId : 0;
|
|
|
|
[ReadOnly]
|
|
public float CurrentVelocity
|
|
{
|
|
get => movementState.currentVelocity.Length;
|
|
set { }
|
|
}
|
|
|
|
[ReadOnly]
|
|
public float UPS
|
|
{
|
|
get
|
|
{
|
|
Float3 horizontalSpeed = movementState.currentVelocity;
|
|
horizontalSpeed.Y = 0f;
|
|
return horizontalSpeed.Length;
|
|
}
|
|
set { }
|
|
}
|
|
|
|
public override void OnAwake()
|
|
{
|
|
base.OnAwake();
|
|
|
|
rootActor = Actor.GetChild("RootActor");
|
|
rigidBody = Actor.As<RigidBody>();
|
|
playerActor = Actor.As<PlayerActor>();
|
|
cameraHolder = rootActor.GetChild("CameraHolder");
|
|
|
|
// Setup input with no controller
|
|
//SetInput(NetworkReplicator.GetObjectOwnerClientId(this.Parent));
|
|
|
|
|
|
//rigidBody.CollisionEnter += OnCollisionEnter;
|
|
//rigidBody.TriggerEnter += OnTriggerEnter;
|
|
//rigidBody.TriggerExit += OnTriggerExit;
|
|
}
|
|
|
|
#if false
|
|
public void SetInput(string demoFile)
|
|
{
|
|
Input = new PlayerInputDemo(demoFile);
|
|
|
|
/*movementState.position = input.GetCurrentActorState().position;
|
|
currentVelocity = input.GetCurrentActorState().velocity; f
|
|
SetCameraEulerAngles(input.GetCurrentActorState().viewAngles);*/
|
|
movementState.position = Input.GetCurrentInputState().verificationPosition;
|
|
Actor.Position = movementState.position;
|
|
//rootActor.Orientation = input.GetCurrentInputState().verificationOrientation;
|
|
movementState.currentVelocity = Input.GetCurrentInputState().verificationVelocity;
|
|
SetCameraEulerAngles(Input.GetCurrentInputState().verificationViewAngles);
|
|
}
|
|
#endif
|
|
|
|
public override void OnEnable()
|
|
{
|
|
//var playerId = NetworkReplicator.GetObjectOwnerClientId(this.Parent);
|
|
//SetInput(playerId);
|
|
//Console.Print("hai: " + playerActor.hai);
|
|
//SetInput(playerActor.PlayerId);
|
|
}
|
|
public override void OnDisable()
|
|
{
|
|
base.OnDisable();
|
|
|
|
if (Input != null && Input is PlayerInputLocal) // FIXME
|
|
(Input as PlayerInputLocal).StopRecording();
|
|
}
|
|
|
|
private void OnTriggerEnter(PhysicsColliderActor colliderActor)
|
|
{
|
|
//if (colliderActor.AttachedRigidBody == null)
|
|
// return;
|
|
touchingActors.Add(colliderActor);
|
|
Console.Print("trogger: ");
|
|
}
|
|
|
|
private void OnTriggerExit(PhysicsColliderActor colliderActor)
|
|
{
|
|
//if (colliderActor.AttachedRigidBody == null)
|
|
// return;
|
|
|
|
touchingActors.Remove(colliderActor);
|
|
Console.Print("untrogger: ");
|
|
}
|
|
|
|
private void OnCollisionEnter(Collision collision)
|
|
{
|
|
//Console.Print("collision: ");
|
|
}
|
|
|
|
public override void OnDestroy()
|
|
{
|
|
base.OnDestroy();
|
|
}
|
|
|
|
public override void OnStart()
|
|
{
|
|
ResetRotation(Actor.Orientation.EulerAngles);
|
|
}
|
|
|
|
public void ResetRotation(Float3 eulerAngles)
|
|
{
|
|
//viewAngles = eulerAngles;
|
|
//viewAnglesLastFrame = eulerAngles;
|
|
|
|
SetCameraEulerAngles(new Float3(eulerAngles.Y, eulerAngles.X, eulerAngles.Z));
|
|
viewAnglesLastFrame = viewAngles;
|
|
}
|
|
|
|
public override void OnUpdate()
|
|
{
|
|
Console.Print("playerMovement OnUpdate");
|
|
//input.OnUpdate();
|
|
|
|
|
|
//if (Input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/)
|
|
// return;
|
|
|
|
if (World.IsServer)
|
|
viewAngles = viewAngles;
|
|
|
|
Input.SetFrame(World.Frame);
|
|
Input.UpdateState();
|
|
|
|
/*if (input.frame > 0)
|
|
{
|
|
PlayerActorState actorState = input.GetCurrentActorState();
|
|
movementState.position = actorState.position;
|
|
currentVelocity = actorState.velocity;
|
|
viewYaw = actorState.viewYaw;
|
|
viewPitch = actorState.viewPitch;
|
|
viewRoll = actorState.viewRoll;
|
|
}*/
|
|
|
|
viewAngles = viewAnglesLastFrame;
|
|
|
|
//if (Input is not PlayerInputNetwork2)
|
|
{
|
|
|
|
PlayerInputState2 inputState = Input.GetState();
|
|
|
|
//if (inputState.viewDeltaX != 0 || inputState.viewDeltaY != 0)
|
|
// inputState = inputState;
|
|
|
|
|
|
ApplyInputToCamera(inputState);
|
|
|
|
/*Input.RecordCurrentActorState(new PlayerActorState
|
|
{
|
|
position = movementState.position,
|
|
velocity = movementState.currentVelocity,
|
|
orientation = rootActor.Orientation,
|
|
viewAngles = viewAngles,
|
|
lastJumpTime = movementState.lastJumped,
|
|
numJumps = movementState.numJumps,
|
|
jumped = movementState.jumped,
|
|
//viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z)
|
|
});*/
|
|
}
|
|
|
|
/*input.RecordCurrentActorState(new PlayerActorState()
|
|
{
|
|
position = movementState.position,
|
|
velocity = currentVelocity,
|
|
orientation = rootActor.Orientation,
|
|
viewYaw = viewYaw,
|
|
viewPitch = viewPitch,
|
|
viewRoll = viewRoll
|
|
});*/
|
|
//currentInputFrame2++;
|
|
}
|
|
|
|
public override void OnFixedUpdate()
|
|
{
|
|
Console.Print("playerMovement OnFixedUpdate");
|
|
float timeDeltaDiff = Time.DeltaTime - 1.0f / Time.PhysicsFPS;
|
|
if (Time.PhysicsFPS > 0 && Math.Abs(timeDeltaDiff) > 0.0001f)
|
|
Console.Print("Time.DeltaTime is not stable: " + timeDeltaDiff);
|
|
|
|
//Input.OnFixedUpdate();
|
|
PlayerInputState2 inputState = Input.GetState();
|
|
bool predict = World.IsClient && false;
|
|
if (predict)
|
|
{
|
|
// Get the latest frame we have predicted
|
|
ulong lastFrame = World.ServerFrame;
|
|
for (; lastFrame < World.Frame; lastFrame++)
|
|
{
|
|
if (!World.HasPlayerFrame(PlayerId, lastFrame))
|
|
//if (!Input.GetState(currentFrame, out var pastInputState, out var pastActorState))
|
|
{
|
|
//Console.Print($"not predicting");
|
|
predict = false;
|
|
break;
|
|
}
|
|
}
|
|
if (predict)
|
|
{
|
|
var oldAngles = viewAngles;
|
|
var oldPos = movementState.position;
|
|
var oldVel = movementState.currentVelocity;
|
|
|
|
ulong currentFrame = World.ServerFrame;
|
|
if (currentFrame == World.ServerFrame)
|
|
{
|
|
// Reset all state to latest confirmed state from server
|
|
var frameInfo = World.GetPlayerFrame(PlayerId, currentFrame);
|
|
movementState.position = frameInfo.movementState.position;
|
|
movementState.currentVelocity = frameInfo.movementState.currentVelocity;
|
|
movementState.lastJumped = frameInfo.movementState.lastJumped;
|
|
movementState.numJumps = frameInfo.movementState.numJumps;
|
|
movementState.jumped = frameInfo.movementState.jumped;
|
|
Actor.Position = movementState.position;
|
|
SetCameraEulerAngles(frameInfo.movementState.viewAngles, true);
|
|
//cameraHolder.Orientation = Quaternion.Euler(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z);
|
|
//ApplyInputToCamera(pastInputState, true);
|
|
|
|
//if (pastActorState.viewAngles != new Float3(90f, 0f, 0f))
|
|
// Console.Print($"moved server frame: {currentFrame}, {pastActorState.viewAngles}");
|
|
|
|
currentFrame++;
|
|
//rootActor.Orientation = pastActorState.orientation;
|
|
//viewAngles = pastActorState.viewAngles;
|
|
//viewAngles = new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z);
|
|
}
|
|
|
|
// Predict the frames since last received frame from server up to latest client frame
|
|
predicting = true;
|
|
for (; currentFrame < lastFrame; currentFrame++)
|
|
{
|
|
var frameInfo = World.GetPlayerFrame(PlayerId, currentFrame);
|
|
if (frameInfo == null)
|
|
{
|
|
Console.Print($"unexpected predict failure: {currentFrame}");
|
|
break;
|
|
}
|
|
|
|
ApplyInputToCamera(frameInfo.inputState, true);
|
|
SimulatePlayerMovement(frameInfo.inputState, frameInfo.frame);
|
|
}
|
|
predicting = false;
|
|
|
|
var posDelta = (movementState.position - oldPos);
|
|
var velDelta = (movementState.currentVelocity - oldVel);
|
|
if (posDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final position");
|
|
if (velDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final velocity");
|
|
|
|
// Run simulation for the upcoming frame
|
|
//if (input is not PlayerInputNetwork)
|
|
{
|
|
/*if ((movementState.position - oldPos).Length > 0.001)
|
|
Console.Print($"mispredicted final position");
|
|
if ((currentVelocity - oldVel).Length > 0.001)
|
|
Console.Print($"mispredicted final velocity");*/
|
|
|
|
ApplyInputToCamera(inputState, true);
|
|
|
|
// Ensure orientation is always up-to-date after predicting
|
|
//rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0);
|
|
cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z);
|
|
|
|
SimulatePlayerMovement(inputState, World.Frame); // MAYBE?
|
|
}
|
|
|
|
var viewDelta = (viewAngles - oldAngles);
|
|
if (viewDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final viewangles: {viewAngles} <- {oldAngles}");
|
|
}
|
|
}
|
|
|
|
if (!predict)
|
|
{
|
|
SetCameraEulerAngles(viewAnglesLastFrame, true);
|
|
ApplyInputToCamera(inputState, true);
|
|
SimulatePlayerMovement(inputState, World.Frame); // MAYBE?
|
|
}
|
|
|
|
viewAnglesLastFrame = viewAngles;
|
|
|
|
#if false
|
|
if (false && Input is PlayerInputNetwork2)
|
|
{
|
|
#if false
|
|
bool canpredict = true;
|
|
if (false && input.Predict && GameModeManager.ClientFrame > 0 && GameModeManager.ServerFrame > 0)
|
|
{
|
|
ulong maxFrame = /*NetworkManager.IsServer ? GameModeManager.playerLastReceivedFrames[PlayerId] :*/ GameModeManager.ClientFrame;
|
|
ulong currentFrame = GameModeManager.ServerFrame;
|
|
for (; currentFrame <= maxFrame; currentFrame++)
|
|
{
|
|
if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState))
|
|
{
|
|
//Console.Print($"not predicting");
|
|
//canpredict = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ulong lastFrame = currentFrame;
|
|
if (input is PlayerInputNetwork)
|
|
{
|
|
//canpredict = true;
|
|
//lastFrame = GameModeManager.ServerFrame+1;
|
|
}
|
|
|
|
predicting = true;
|
|
currentFrame = GameModeManager.ServerFrame;
|
|
for (; currentFrame < lastFrame; currentFrame++)
|
|
{
|
|
if (!input.GetState(currentFrame, out var pastInputState, out var pastActorState))
|
|
{
|
|
Console.Print($"unexpected predict failure: {currentFrame}");
|
|
break;
|
|
}
|
|
|
|
if (currentFrame == inputState.frame)
|
|
jumped = jumped;
|
|
|
|
if (currentFrame == GameModeManager.ServerFrame)
|
|
{
|
|
movementState.position = pastActorState.position;
|
|
currentVelocity = pastActorState.velocity;
|
|
lastJumped = pastActorState.lastJumpTime;
|
|
numJumps = pastActorState.numJumps;
|
|
jumped = pastActorState.jumped;
|
|
SetCameraEulerAngles(pastActorState.viewAngles, true);
|
|
|
|
if (movementState.position.Length < 0.1)
|
|
jumped = jumped;
|
|
|
|
continue;
|
|
//rootActor.Orientation = pastActorState.orientation;
|
|
//viewAngles = pastActorState.viewAngles;
|
|
//viewAngles = new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z);
|
|
}
|
|
else
|
|
ApplyInputToCamera(pastInputState, true);
|
|
//SetCameraEulerAngles(pastActorState.viewAngles, true);
|
|
|
|
//if (currentVelocity.Length > 0)
|
|
// currentVelocity = currentVelocity;
|
|
|
|
//else
|
|
// ApplyInputToCamera(pastInputState, true);
|
|
SimulatePlayerMovement(pastInputState);
|
|
|
|
break;
|
|
|
|
/*if ((movementState.position - pastActorState.position).Length > 0.001)
|
|
Console.Print($"mispredicted position");
|
|
if ((currentVelocity - pastActorState.velocity).Length > 0.001)
|
|
Console.Print($"mispredicted velocity");
|
|
if ((viewAngles - pastActorState.viewAngles).Length > 0.001)
|
|
Console.Print($"mispredicted viewangles: {viewAngles - oldAngles}");*/
|
|
|
|
//Console.Print($"predicted: {currentFrame}");
|
|
}
|
|
|
|
/*if (input is PlayerInputNetwork)
|
|
{
|
|
currentFrame = lastFrame - 1;
|
|
if (input.GetState(currentFrame, out var lastInputState, out var lastActorState))
|
|
{
|
|
for (; currentFrame < GameModeManager.ClientFrame; currentFrame++)
|
|
{
|
|
ApplyInputToCamera(lastInputState, true);
|
|
SimulatePlayerMovement(lastInputState);
|
|
}
|
|
}
|
|
}*/
|
|
|
|
predicting = false;
|
|
}
|
|
else
|
|
{
|
|
//ApplyInputToCamera(inputState, true);
|
|
//SimulatePlayerMovement(inputState);
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
if (movementState.position.Length < 0.1)
|
|
movementState.jumped = movementState.jumped;
|
|
|
|
//viewAngles = viewAnglesLastFrame;
|
|
//ApplyInputToCamera(inputState);
|
|
|
|
//viewAngles = viewAnglesLastFrame;
|
|
bool canpredict = true;
|
|
if (true /*&& Input.Predict*/ /*&& !NetworkManager.IsDemoPlaying*/ && World.Frame > 0 && World.ServerFrame > 0)
|
|
{
|
|
// Get the latest frame we have predicted
|
|
ulong currentFrame = World.ServerFrame;
|
|
for (; currentFrame < World.Frame; currentFrame++)
|
|
{
|
|
if (!World.HasPlayerFrame(PlayerId, currentFrame))
|
|
//if (!Input.GetState(currentFrame, out var pastInputState, out var pastActorState))
|
|
{
|
|
//Console.Print($"not predicting");
|
|
canpredict = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ulong lastFrame = currentFrame;
|
|
if (canpredict)
|
|
{
|
|
var oldAngles = viewAngles;
|
|
var oldPos = movementState.position;
|
|
var oldVel = movementState.currentVelocity;
|
|
|
|
// Predict the frames since last received frame from server up to latest client frame
|
|
predicting = true;
|
|
currentFrame = World.ServerFrame;
|
|
for (; currentFrame < lastFrame; currentFrame++)
|
|
{
|
|
//if (!Input.GetState(currentFrame, out var pastInputState, out var pastActorState))
|
|
var frameInfo = World.GetPlayerFrame(PlayerId, currentFrame);
|
|
if (frameInfo == null)
|
|
{
|
|
Console.Print($"unexpected predict failure: {currentFrame}");
|
|
break;
|
|
}
|
|
|
|
//if (currentFrame == inputState.frame)
|
|
// movementState.jumped = movementState.jumped;
|
|
|
|
if (currentFrame == World.ServerFrame)
|
|
{
|
|
movementState.position = frameInfo.movementState.position;
|
|
movementState.currentVelocity = frameInfo.movementState.currentVelocity;
|
|
movementState.lastJumped = frameInfo.movementState.lastJumped;
|
|
movementState.numJumps = frameInfo.movementState.numJumps;
|
|
movementState.jumped = frameInfo.movementState.jumped;
|
|
Actor.Position = movementState.position;
|
|
SetCameraEulerAngles(frameInfo.movementState.viewAngles, true);
|
|
//cameraHolder.Orientation = Quaternion.Euler(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z);
|
|
//ApplyInputToCamera(pastInputState, true);
|
|
|
|
//if (pastActorState.viewAngles != new Float3(90f, 0f, 0f))
|
|
// Console.Print($"moved server frame: {currentFrame}, {pastActorState.viewAngles}");
|
|
|
|
if (movementState.position.Length < 0.1)
|
|
movementState.jumped = movementState.jumped;
|
|
|
|
continue;
|
|
//rootActor.Orientation = pastActorState.orientation;
|
|
//viewAngles = pastActorState.viewAngles;
|
|
//viewAngles = new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z);
|
|
}
|
|
else
|
|
ApplyInputToCamera(frameInfo.inputState, true);
|
|
//SetCameraEulerAngles(pastActorState.viewAngles, true);
|
|
|
|
//if (currentVelocity.Length > 0)
|
|
// currentVelocity = currentVelocity;
|
|
|
|
//else
|
|
// ApplyInputToCamera(pastInputState, true);
|
|
SimulatePlayerMovement(frameInfo.inputState, frameInfo.frame);
|
|
|
|
/*if ((movementState.position - pastActorState.position).Length > 0.001)
|
|
Console.Print($"mispredicted position");
|
|
if ((currentVelocity - pastActorState.velocity).Length > 0.001)
|
|
Console.Print($"mispredicted velocity");
|
|
if ((viewAngles - pastActorState.viewAngles).Length > 0.001)
|
|
Console.Print($"mispredicted viewangles: {viewAngles - oldAngles}");*/
|
|
|
|
//Console.Print($"predicted: {currentFrame}");
|
|
}
|
|
|
|
predicting = false;
|
|
|
|
var posDelta = (movementState.position - oldPos);
|
|
var velDelta = (movementState.currentVelocity - oldVel);
|
|
if (posDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final position");
|
|
if (velDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final velocity");
|
|
|
|
|
|
//if (input is not PlayerInputNetwork)
|
|
{
|
|
/*if ((movementState.position - oldPos).Length > 0.001)
|
|
Console.Print($"mispredicted final position");
|
|
if ((currentVelocity - oldVel).Length > 0.001)
|
|
Console.Print($"mispredicted final velocity");*/
|
|
|
|
ApplyInputToCamera(inputState, true);
|
|
|
|
// Ensure orientation is always up-to-date after predicting
|
|
//rootActor.Orientation = Quaternion.Euler(0, viewAngles.X, 0);
|
|
cameraHolder.Orientation = Quaternion.Euler(viewAngles.Y, viewAngles.X, viewAngles.Z);
|
|
|
|
SimulatePlayerMovement(inputState, World.Frame); // MAYBE?
|
|
}
|
|
|
|
var viewDelta = (viewAngles - oldAngles);
|
|
if (viewDelta.Length > 0.001)
|
|
Console.Print($"mispredicted final viewangles: {viewAngles} <- {oldAngles}");
|
|
|
|
//if (viewAngles != new Float3(90f, 0f, 0f))
|
|
// Console.Print($"moved client frame: {GameModeManager.ClientFrame}, {viewAngles}");
|
|
|
|
//Console.Print($"current: {inputState.frame}");
|
|
//if (GameModeManager.ClientFrame - GameModeManager.ServerFrame > 0)
|
|
// Console.Print($"current diff: {GameModeManager.ClientFrame - GameModeManager.ServerFrame}");
|
|
}
|
|
}
|
|
else
|
|
canpredict = false;
|
|
|
|
if (!canpredict)
|
|
{
|
|
SetCameraEulerAngles(viewAnglesLastFrame, true);
|
|
ApplyInputToCamera(inputState, true);
|
|
SimulatePlayerMovement(inputState, World.Frame); // MAYBE?
|
|
}
|
|
|
|
if (movementState.position.Length < 0.1)
|
|
movementState.jumped = movementState.jumped;
|
|
|
|
//if (currentVelocity.Length > 0)
|
|
// Console.Print($"velocity at frame {GameModeManager.ClientFrame}");
|
|
|
|
/*if (input.GetState(GameModeManager.ClientFrame - 1, out var pastInputState2, out var pastActorState2))
|
|
{
|
|
movementState.position = pastActorState2.position;
|
|
currentVelocity = pastActorState2.velocity;
|
|
rootActor.Orientation = pastActorState2.orientation;
|
|
viewAngles = new Float3(pastActorState2.viewAngles.Y, pastActorState2.viewAngles.X, pastActorState2.viewAngles.Z);
|
|
//viewAngles = viewAnglesLastFrame;
|
|
}
|
|
else
|
|
Console.Print($"poop");*/
|
|
|
|
//viewAngles = oldAngles;'
|
|
//viewAngles = viewAnglesLastFrame;
|
|
//ApplyInputToCamera(inputState, true);
|
|
//SetCameraEulerAngles(new Float3(pastActorState.viewAngles.Y, pastActorState.viewAngles.X, pastActorState.viewAngles.Z), true);
|
|
//SetCameraEulerAngles(new Float3(viewAnglesLastFrame.Y, viewAnglesLastFrame.X, viewAnglesLastFrame.Z), true);
|
|
//SimulatePlayerMovement(inputState);
|
|
|
|
/*Input.RecordCurrentActorState(new PlayerActorState
|
|
{
|
|
position = movementState.position,
|
|
velocity = movementState.currentVelocity,
|
|
orientation = rootActor.Orientation,
|
|
viewAngles = viewAngles,
|
|
lastJumpTime = movementState.lastJumped,
|
|
numJumps = movementState.numJumps,
|
|
jumped = movementState.jumped,
|
|
//viewAngles = new Float3(viewAngles.Y, viewAngles.X, viewAngles.Z)
|
|
});*/
|
|
|
|
//Console.Print($"recording frame {input.frame}, client: {GameModeManager.ClientFrame}, server: {GameModeManager.ServerFrame}");
|
|
World.RecordPlayerInput(PlayerId, World.Frame, inputState, movementState); // MAYBE?
|
|
Input.ResetState();
|
|
}
|
|
|
|
if (movementState.position.Length < 0.1)
|
|
movementState.jumped = movementState.jumped;
|
|
|
|
if (Input is PlayerInputNetwork2)
|
|
movementState.jumped = movementState.jumped;
|
|
|
|
|
|
|
|
|
|
//lastInputFrame = currentInputFrame;
|
|
//currentInputFrame++;
|
|
|
|
viewAnglesLastFrame = viewAngles;
|
|
|
|
/*if (input.GetState(GameModeManager.ServerFrame, out var pastInputState2, out var pastActorState2))
|
|
{
|
|
movementState.position = pastActorState2.position;
|
|
currentVelocity = pastActorState2.velocity;
|
|
SetCameraEulerAngles(pastActorState2.viewAngles, true);
|
|
}*/
|
|
#endif
|
|
}
|
|
|
|
public void ApplyInputToCamera(PlayerInputState2 inputState, bool wrapAround = true)
|
|
{
|
|
if (inputState.ViewDelta == Float2.Zero)
|
|
return;
|
|
|
|
float viewPitch = Mathf.Clamp(viewAngles.Y + inputState.ViewDelta.Y, -90.0f, 90.0f);
|
|
float viewYaw = viewAngles.X + inputState.ViewDelta.X;
|
|
SetCameraEulerAngles(new Float3(viewYaw, viewPitch, viewAngles.Z), wrapAround);
|
|
}
|
|
|
|
public void SetCameraEulerAngles(Float3 angles, bool wrapAround = true)
|
|
{
|
|
if (viewAngles == angles)
|
|
return;
|
|
|
|
// Very slight drift
|
|
if (wrapAround)
|
|
{
|
|
angles.X = Mathf.Mod(angles.X, 360.0f);
|
|
angles.Y = Mathf.Mod(angles.Y, 360.0f);
|
|
angles.Z = Mathf.Mod(angles.Z, 360.0f);
|
|
}
|
|
|
|
// Root orientation must be set first
|
|
rootActor.Orientation = Quaternion.Euler(0, angles.X, 0);
|
|
if (!predicting)
|
|
{
|
|
cameraHolder.Orientation = Quaternion.Euler(angles.Y, angles.X, angles.Z);
|
|
}
|
|
//Console.Print(angles.X.ToString());
|
|
|
|
viewAngles = angles;//new Float3(angles.Y, angles.X, angles.Z);
|
|
//viewAnglesLastFrame = angles;
|
|
}
|
|
|
|
private static bool SweepPlayerCollider(PlayerActor actor, Float3 start, Vector3 end, out RayCastHit[] hits)
|
|
{
|
|
Vector3 delta = end - start;
|
|
float distance = delta.Length;
|
|
Vector3 direction = delta.Normalized;
|
|
|
|
if (distance < 0.00000001f)
|
|
{
|
|
hits = new RayCastHit[0];//Array.Empty<RayCastHit>();
|
|
return false;
|
|
}
|
|
|
|
bool collided = false;
|
|
CapsuleCollider capsuleCollider = actor.capsuleCollider;
|
|
BoxCollider boxCollider = actor.boxCollider;
|
|
MeshCollider meshCollider = actor.meshCollider;
|
|
if (capsuleCollider && capsuleCollider.IsActive)
|
|
collided = Physics.CapsuleCastAll(start,
|
|
capsuleCollider.Radius, capsuleCollider.Height,
|
|
direction, out hits, capsuleCollider.Orientation, distance,
|
|
uint.MaxValue,
|
|
false);
|
|
else if (meshCollider && meshCollider.IsActive)
|
|
collided = Physics.ConvexCastAll(start,
|
|
meshCollider.CollisionData, meshCollider.Scale,
|
|
direction, out hits, meshCollider.Orientation, distance,
|
|
uint.MaxValue,
|
|
false);
|
|
else if (boxCollider && boxCollider.IsActive)
|
|
collided = Physics.BoxCastAll(start,
|
|
boxCollider.OrientedBox.Extents,
|
|
direction, out hits, boxCollider.Orientation, distance,
|
|
uint.MaxValue,
|
|
false);
|
|
else
|
|
throw new Exception("Player does not have a collider");
|
|
|
|
|
|
|
|
return collided;
|
|
}
|
|
|
|
private bool SweepPlayerCollider(Float3 position, out PhysicsColliderActor[] colliders)
|
|
{
|
|
bool collided = false;
|
|
CapsuleCollider capsuleCollider = Actor.GetChild<CapsuleCollider>();
|
|
BoxCollider boxCollider = Actor.GetChild<BoxCollider>();
|
|
MeshCollider meshCollider = Actor.GetChild<MeshCollider>();
|
|
PhysicsColliderActor colliderActor = null;
|
|
if (capsuleCollider && capsuleCollider.IsActive)
|
|
{
|
|
colliderActor = capsuleCollider;
|
|
collided = Physics.OverlapCapsule(position,
|
|
capsuleCollider.Radius, capsuleCollider.Height,
|
|
out colliders, capsuleCollider.Orientation,
|
|
uint.MaxValue,
|
|
false);
|
|
}
|
|
else if (meshCollider && meshCollider.IsActive)
|
|
{
|
|
colliderActor = meshCollider;
|
|
collided = Physics.OverlapConvex(position,
|
|
meshCollider.CollisionData, meshCollider.Scale,
|
|
out colliders, meshCollider.Orientation,
|
|
uint.MaxValue,
|
|
false);
|
|
}
|
|
else if (boxCollider && boxCollider.IsActive)
|
|
{
|
|
colliderActor = boxCollider;
|
|
collided = Physics.OverlapBox(position,
|
|
boxCollider.OrientedBox.Extents,
|
|
out colliders, boxCollider.Orientation,
|
|
uint.MaxValue,
|
|
false);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Player does not have a collider");
|
|
}
|
|
|
|
if (collided)
|
|
{
|
|
var collidersFiltered = new List<PhysicsColliderActor>();
|
|
foreach (PhysicsColliderActor collider in colliders)
|
|
{
|
|
if (collider.Parent == Actor)
|
|
continue;
|
|
|
|
collidersFiltered.Add(collider);
|
|
}
|
|
|
|
colliders = collidersFiltered.ToArray();
|
|
if (colliders.Length == 0)
|
|
collided = false;
|
|
}
|
|
|
|
return collided;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sweeps the player rigidbody in world and returns geometry which was hit during the trace.
|
|
/// </summary>
|
|
/// <param name="actor">Player actor</param>
|
|
/// <param name="start">Start position</param>
|
|
/// <param name="end">End position</param>
|
|
/// <returns></returns>
|
|
private static TraceInfo TracePlayer(PlayerActor actor, Vector3 start, Vector3 end)
|
|
{
|
|
TraceInfo traceInfo = new TraceInfo();
|
|
|
|
Vector3 delta = end - start;
|
|
float maxDistance = delta.Length;
|
|
Vector3 direction = delta.Normalized;
|
|
|
|
bool collided = SweepPlayerCollider(actor, start, end, out traceInfo.hitInfos);
|
|
if (collided)
|
|
{
|
|
var hitInfosFiltered = new List<RayCastHit>();
|
|
RayCastHit closest = new RayCastHit();
|
|
closest.Distance = float.MaxValue;
|
|
foreach (RayCastHit hitInfo in traceInfo.hitInfos)
|
|
{
|
|
//if (hitInfo.Collider == colliderActor)
|
|
// continue;
|
|
if (hitInfo.Collider.Parent == actor)
|
|
continue;
|
|
|
|
hitInfosFiltered.Add(hitInfo);
|
|
|
|
if (hitInfo.Distance < closest.Distance && hitInfo.Distance != 0.0f)
|
|
closest = hitInfo;
|
|
}
|
|
|
|
if (hitInfosFiltered.Count == 0 /*|| closest.Distance == float.MaxValue*/)
|
|
{
|
|
collided = false; // self-collision?
|
|
}
|
|
/*else if (closest.Distance == float.MaxValue)
|
|
{
|
|
bool startSolid = SweepPlayerCollider(start, out PhysicsColliderActor[] colliders);
|
|
if (startSolid)
|
|
{
|
|
traceInfo.hitInfos = hitInfosFiltered.ToArray();
|
|
|
|
traceInfo.fraction = 0f;
|
|
traceInfo.hitNormal = Float3.Zero;
|
|
traceInfo.hitPosition = start;
|
|
traceInfo.endPosition = start;
|
|
traceInfo.startSolid = true;
|
|
|
|
if (delta.Y >= 0f)
|
|
Console.Print("ovr: " + colliders[0].Parent.Name);
|
|
}
|
|
else
|
|
collided = false;
|
|
}*/
|
|
else //if (closest.Distance > 0f)
|
|
{
|
|
if (closest.Distance == float.MaxValue)
|
|
foreach (RayCastHit hitInfo in hitInfosFiltered)
|
|
if (hitInfo.Distance < closest.Distance)
|
|
closest = hitInfo;
|
|
|
|
traceInfo.hitInfos = hitInfosFiltered.ToArray();
|
|
|
|
traceInfo.fraction = closest.Distance / maxDistance;
|
|
traceInfo.hitNormal = closest.Normal;
|
|
traceInfo.hitPosition = closest.Point;
|
|
traceInfo.endPosition = start + delta * traceInfo.fraction;
|
|
|
|
if (traceInfo.fraction == 0f && maxDistance > 0f)
|
|
traceInfo.startSolid = true;
|
|
|
|
//if (delta.Y >= 0f)
|
|
// Console.Print("col: " + closest.Collider.Parent.Name + ", " + closest.Distance.ToString("G9"));
|
|
}
|
|
/*else
|
|
{
|
|
traceInfo.startSolid = true;
|
|
traceInfo.fraction = 0f;
|
|
}*/
|
|
}
|
|
|
|
if (!collided)
|
|
{
|
|
traceInfo.hitInfos = new RayCastHit[0];
|
|
traceInfo.fraction = 1f;
|
|
traceInfo.endPosition = end;
|
|
}
|
|
|
|
return traceInfo;
|
|
}
|
|
|
|
#if FLAX_EDITOR
|
|
public override void OnDebugDraw()
|
|
{
|
|
base.OnDebugDraw();
|
|
|
|
CapsuleCollider capsuleCollider = Actor.GetChild<CapsuleCollider>();
|
|
BoxCollider boxCollider = Actor.GetChild<BoxCollider>();
|
|
MeshCollider meshCollider = Actor.GetChild<MeshCollider>();
|
|
if (capsuleCollider && capsuleCollider.IsActive)
|
|
{
|
|
Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f);
|
|
DebugDraw.DrawWireCapsule(capsuleCollider.Position, rotation, capsuleCollider.Radius,
|
|
capsuleCollider.Height, Color.GreenYellow * 0.8f);
|
|
}
|
|
else if (meshCollider && meshCollider.IsActive)
|
|
{
|
|
//Quaternion rotation = meshCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f);
|
|
DebugDraw.DrawWireCylinder(meshCollider.Position, meshCollider.Orientation, capsuleCollider.Radius,
|
|
capsuleCollider.Height + capsuleCollider.Radius * 2, Color.GreenYellow * 0.8f);
|
|
//DebugDraw.DrawWireCapsule(meshCollider.Position, rotation, meshCollider.Radius, meshCollider.Height, Color.GreenYellow * 0.8f);
|
|
}
|
|
else if (boxCollider && boxCollider.IsActive)
|
|
{
|
|
var clientBbox = boxCollider.OrientedBox.GetBoundingBox();
|
|
|
|
if (false)
|
|
{
|
|
#if false
|
|
if (worldStateManager.ServerFrame > 0 && worldStateManager.ClientFrame > 0)
|
|
for (ulong frame = worldStateManager.ServerFrame; frame < worldStateManager.ClientFrame; frame++)
|
|
{
|
|
if (!Input.GetState(frame, out var pastInputState, out var pastActorState))
|
|
continue;
|
|
|
|
var bbox = clientBbox;
|
|
bbox.Center = pastActorState.position;
|
|
|
|
Float4 color1 = new Float4(Color.Red.R, Color.Red.G, Color.Red.B, Color.Red.A);
|
|
Float4 color2 = new Float4(Color.Blue.R, Color.Blue.G, Color.Blue.B, Color.Blue.A);
|
|
Float4 color3 = Float4.Lerp(color1, color2, (float)(frame - worldStateManager.ServerFrame) / (float)(worldStateManager.ClientFrame - worldStateManager.ServerFrame));
|
|
Color color = new Color(color3.X, color3.Y, color3.Z, color3.W);
|
|
DebugDraw.DrawBox(bbox, color * 1f);
|
|
}
|
|
#endif
|
|
}
|
|
else if (World.IsClient)
|
|
{
|
|
var serverBbox = boxCollider.OrientedBox.GetBoundingBox();
|
|
var frameInfo = World.GetPlayerFrame(PlayerId, World.ServerFrame);
|
|
if (frameInfo != null)
|
|
serverBbox.Center = frameInfo.movementState.position;
|
|
|
|
if (serverBbox.Center == clientBbox.Center)
|
|
DebugDraw.DrawBox(clientBbox, Color.Magenta * 0.6f);
|
|
else
|
|
{
|
|
//DebugDraw.DrawBox(serverBbox, Color.Red * 0.6f);
|
|
//DebugDraw.DrawBox(clientBbox, Color.Blue * 0.6f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private static SlideMoveHit StepSlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity,
|
|
bool onGround)
|
|
{
|
|
if (velocity.IsZero)
|
|
return SlideMoveHit.Nothing;
|
|
|
|
Vector3 gravityDirection = movementParameters.gravity.Normalized;
|
|
|
|
Vector3 originalPosition = position;
|
|
Vector3 originalVelocity = velocity;
|
|
|
|
SlideMoveHit slideMoveHit = SlideMove(actor, movementParameters, ref position, ref velocity);
|
|
if (slideMoveHit == SlideMoveHit.Nothing)
|
|
// TODO: step down here
|
|
return slideMoveHit;
|
|
|
|
// hit something, try to step up
|
|
float effectiveStepHeight = movementParameters.stepHeight;
|
|
if (!onGround)
|
|
{
|
|
// TODO: implement clipping here
|
|
/*if (pmove.jump_time > 0 && pmove.jump_time <= movevars.cliptime)
|
|
{
|
|
float zvel = pmove.velocity[2];
|
|
VectorCopy(originalvel, pmove.velocity);
|
|
pmove.velocity[2] = min(pmove.velocity[2], zvel); // nullifies vertical clipping
|
|
}*/
|
|
|
|
if (!slideMoveHit.HasFlag(SlideMoveHit.Step))
|
|
return slideMoveHit;
|
|
|
|
if (movementParameters.airStep < 2)
|
|
{
|
|
//effectiveStepHeight = ?
|
|
}
|
|
}
|
|
|
|
|
|
Vector3 stepDelta = -gravityDirection * effectiveStepHeight;
|
|
|
|
Vector3 slidePosition = position;
|
|
Vector3 slideVelocity = velocity;
|
|
position = originalPosition;
|
|
velocity = originalVelocity;
|
|
|
|
// step up
|
|
Vector3 stepUp = position + stepDelta;
|
|
TraceInfo traceUp = TracePlayer(actor, position, stepUp);
|
|
if (traceUp.fraction > 0f)
|
|
position = traceUp.endPosition;
|
|
|
|
// try moving from step up position
|
|
SlideMoveHit slideMoveStepHit = SlideMove(actor, movementParameters, ref position, ref velocity);
|
|
|
|
// step down
|
|
Vector3 stepDown = position - stepDelta;
|
|
TraceInfo traceDown = TracePlayer(actor, position, stepDown);
|
|
if (traceDown.fraction < 1f && -Float3.Dot(gravityDirection, traceDown.hitNormal) < movementParameters.slopeNormal)
|
|
{
|
|
// can't step down, slide move like normally
|
|
Console.Print("no stepping 1, frac: " + traceDown.fraction + ", dot: " +
|
|
-Float3.Dot(gravityDirection, traceDown.hitNormal) +
|
|
", norm: " + traceDown.hitNormal);
|
|
position = slidePosition;
|
|
velocity = slideVelocity;
|
|
return slideMoveHit;
|
|
}
|
|
|
|
if (traceDown.fraction > 0f)
|
|
position = traceDown.endPosition;
|
|
|
|
// add some margin from the ground in order to avoid getting stuck after stepping up
|
|
if (traceDown.fraction < 1f)
|
|
position.Y += movementParameters.collisionMargin;
|
|
|
|
// ??
|
|
float d1 = -Float3.Dot(gravityDirection, position);
|
|
float d2 = -Float3.Dot(gravityDirection, originalPosition);
|
|
if (d1 < d2)
|
|
{
|
|
//Console.Print("no stepping 2, " + d1 + " < " + d2);
|
|
position = slidePosition;
|
|
velocity = slideVelocity;
|
|
return slideMoveHit;
|
|
}
|
|
|
|
Vector3 slidePosition2 = slidePosition; //down
|
|
Vector3 stepPosition2 = position; //up
|
|
|
|
// FIXME, negate movementParameters.gravity
|
|
slidePosition2.Y = 0f;
|
|
stepPosition2.Y = 0f;
|
|
|
|
// take the slide movement results if furthest away from original position
|
|
//if ((stepPosition2 - originalPosition).Length < (slidePosition2 - originalPosition).Length)
|
|
if ((slidePosition2 - originalPosition).Length >= (stepPosition2 - originalPosition).Length)
|
|
{
|
|
//Console.Print("no stepping 3");
|
|
position = slidePosition;
|
|
velocity = slideVelocity;
|
|
return slideMoveHit;
|
|
}
|
|
|
|
//return slideMoveStepHit;
|
|
return slideMoveHit;
|
|
}
|
|
|
|
private static SlideMoveHit SlideMove(PlayerActor actor, PlayerMovementParameters movementParameters, ref Vector3 position, ref Vector3 velocity)
|
|
{
|
|
if (velocity.IsZero)
|
|
return SlideMoveHit.Nothing;
|
|
|
|
Vector3 originalPosition = position;
|
|
Vector3 originalVelocity = velocity;
|
|
SlideMoveHit slideMoveHit = SlideMoveHit.Nothing;
|
|
|
|
float timeleft = Time.DeltaTime;
|
|
|
|
var hitNormals = new List<Float3>();
|
|
|
|
for (int bump = 0; bump < 4; bump++)
|
|
{
|
|
Vector3 startPos = position;
|
|
Vector3 endPos = position + velocity * timeleft;
|
|
|
|
TraceInfo trace = TracePlayer(actor, startPos, endPos);
|
|
// TODO: handle portals here
|
|
|
|
float fraction = trace.fraction;
|
|
Vector3 hitNormal = trace.hitNormal;
|
|
|
|
if (trace.startSolid)
|
|
{
|
|
velocity = Float3.Zero;
|
|
break;
|
|
}
|
|
|
|
if (fraction > 0f)
|
|
{
|
|
position = trace.endPosition;
|
|
hitNormals.Clear(); // this is present in some forks, not in Q3
|
|
}
|
|
|
|
if (fraction >= 1f)
|
|
break;
|
|
|
|
timeleft *= 1.0f - fraction;
|
|
|
|
if (trace.hitNormal.Y > movementParameters.slopeNormal)
|
|
slideMoveHit |= SlideMoveHit.Floor;
|
|
else if (Math.Abs(trace.hitNormal.Y) < 0.0001f)
|
|
slideMoveHit |= SlideMoveHit.Step;
|
|
else
|
|
slideMoveHit |= SlideMoveHit.Other;
|
|
|
|
// this doesn't seem to do anything, we never have any hitNormals stored here
|
|
bool hitPreviousNormal = false;
|
|
foreach (Float3 normal in hitNormals)
|
|
if (Float3.Dot(hitNormal, normal) > 0.99)
|
|
{
|
|
// nudge away from the same wall we hit earlier and try again
|
|
velocity += hitNormal;
|
|
hitPreviousNormal = true;
|
|
break;
|
|
}
|
|
|
|
if (hitPreviousNormal)
|
|
continue;
|
|
|
|
hitNormals.Add(hitNormal);
|
|
if (hitNormals.Count != 1)
|
|
Console.Print("hitNormals: " + hitNormals.Count);
|
|
|
|
int plane;
|
|
Vector3 normalMargin = Float3.Zero;
|
|
for (plane = 0; plane < hitNormals.Count; plane++)
|
|
{
|
|
Vector3 normal = hitNormals[plane];
|
|
|
|
// clip velocity
|
|
velocity -= normal * Float3.Dot(velocity, normal);
|
|
//velocity = Float3.ProjectOnPlane(velocity, normal);
|
|
|
|
//traceOffset = normal * 1f;
|
|
normalMargin += normal;
|
|
//position += normal * 0.031f;
|
|
|
|
int plane2;
|
|
for (plane2 = 0; plane2 < hitNormals.Count; plane2++)
|
|
{
|
|
if (plane == plane2)
|
|
continue;
|
|
|
|
if (Float3.Dot(velocity, hitNormals[plane2]) < 0f)
|
|
break;
|
|
}
|
|
|
|
if (plane2 == hitNormals.Count)
|
|
break;
|
|
}
|
|
|
|
// push off slightly away from the walls to not get stuck
|
|
position += normalMargin.Normalized * movementParameters.collisionMargin;
|
|
//Console.Print("pushin");
|
|
|
|
if (plane == hitNormals.Count)
|
|
{
|
|
if (hitNormals.Count == 2)
|
|
{
|
|
Vector3 dir = Float3.Cross(hitNormals[0], hitNormals[1]);
|
|
//dir.Normalize();
|
|
float dist = Float3.Dot(dir, velocity);
|
|
velocity = dist * dir;
|
|
}
|
|
else
|
|
{
|
|
velocity = Float3.Zero;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// prevents bouncing against the wall
|
|
if ( /*velocity.Length > 0f && */Float3.Dot(velocity, originalVelocity) <= 0f)
|
|
{
|
|
velocity = Float3.Zero;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return slideMoveHit;
|
|
}
|
|
|
|
public void SimulatePlayerMovement(PlayerInputState2 inputState, ulong frame)
|
|
{
|
|
simulationTime = World.GetFrameTime(frame);
|
|
|
|
Vector3 inputDirection =
|
|
new Float3(inputState.MoveRight, 0.0f, inputState.MoveForward);
|
|
Vector3 moveDirection = rootActor.Transform.TransformDirection(inputDirection);
|
|
Vector3 position = movementState.position;
|
|
Vector3 velocity = movementState.currentVelocity; //rigidBody.LinearVelocity;
|
|
Vector3 wishVelocity = !inputDirection.IsZero ? moveDirection.Normalized * movementParameters.moveSpeed : Vector3.Zero;
|
|
|
|
if (position != rigidBody.Position)
|
|
Console.Print("PlayerMovement: rigidbody position does not match with movement state position");
|
|
|
|
// categorize position
|
|
Vector3 lastVelocity = velocity;
|
|
movementState.onGround = true;
|
|
|
|
TraceInfo traceGround = CategorizePosition(position, ref velocity);
|
|
|
|
bool jumpAction = inputState.Jump;
|
|
|
|
if (movementState.jumped && !jumpAction)
|
|
movementState.jumped = false; // jump released
|
|
else if (movementState.jumped && simulationTime - movementState.lastJumped >= movementParameters.autoJumpTime)
|
|
movementState.jumped = false; // jump timeout
|
|
|
|
// jump
|
|
if (movementState.onGround && jumpAction && !movementState.jumped)
|
|
if (OnJump(traceGround, ref position, ref velocity))
|
|
{
|
|
//Console.Print($"{inputState.frame} jumped " + ", predicting: " + predicting + ", vel: " + velocity.Y);
|
|
movementState.jumped = true;
|
|
movementState.lastJumped = simulationTime;
|
|
movementState.numJumps++;
|
|
}
|
|
|
|
if (simulationTime - movementState.lastJumped > movementParameters.jumpBoostTime)
|
|
movementState.numJumps = 0;
|
|
|
|
//if (/*onGround && */lastGround != onGround)
|
|
if (movementState.onGround)
|
|
{
|
|
// ground friction
|
|
ApplyFriction(movementParameters, ref velocity);
|
|
|
|
// ground acceleration
|
|
ApplyAcceleration(ref velocity, wishVelocity.Normalized, wishVelocity.Length, float.MaxValue,
|
|
movementParameters.accelerationGround);
|
|
}
|
|
else // air movement
|
|
{
|
|
ApplyAirAcceleration(movementParameters, ref velocity, wishVelocity);
|
|
}
|
|
|
|
StepSlideMove(Actor as PlayerActor, movementParameters, ref position, ref velocity, movementState.onGround);
|
|
|
|
|
|
TraceInfo traceGround2 = CategorizePosition(position, ref velocity);
|
|
|
|
movementState.position = position;
|
|
rigidBody.Position = position;
|
|
movementState.currentVelocity = velocity;
|
|
//rigidBody.LinearVelocity = velocity;
|
|
|
|
const float landingVelocityThreshold = 120f;
|
|
const float landingHardVelocityThreshold = 500f;
|
|
if (movementState.currentVelocity.Y - lastVelocity.Y > landingVelocityThreshold)
|
|
if (simulationTime - movementState.lastJumped > 0.01)
|
|
{
|
|
bool hardLanding = movementState.currentVelocity.Y - lastVelocity.Y > landingHardVelocityThreshold;
|
|
OnLanded(movementState.currentVelocity - lastVelocity, hardLanding);
|
|
movementState.lastLanded = simulationTime;
|
|
}
|
|
}
|
|
|
|
private TraceInfo CategorizePosition(Float3 position, ref Vector3 velocity)
|
|
{
|
|
Vector3 groundDelta = movementParameters.gravity.Normalized; //movementParameters.gravity.Normalized * (collisionMargin * 2);
|
|
//if (velocity.Y < 0f)
|
|
// groundDelta = movementParameters.gravity.Normalized * velocity.Y * Time.DeltaTime;
|
|
TraceInfo traceGround = TracePlayer(Actor as PlayerActor, position, position + groundDelta);
|
|
//Console.PrintDebug(1, true, "startSolid: " + traceGround.startSolid);
|
|
|
|
if (!traceGround.startSolid && traceGround.fraction < 1f &&
|
|
-Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal)
|
|
{
|
|
// clip velocity
|
|
|
|
Vector3 bounce = groundDelta;
|
|
//Vector3 velocityProjected = Float3.ProjectOnPlane(velocity, normal);
|
|
float backoff = Float3.Dot(bounce, traceGround.hitNormal) * 2f;
|
|
bounce -= traceGround.hitNormal * backoff;
|
|
//velocity = velocityProjected;
|
|
|
|
Vector3 point = position + groundDelta +
|
|
(1f - traceGround.fraction) * bounce;
|
|
|
|
Console.Print("backoff: " + backoff);
|
|
|
|
// retrace
|
|
traceGround = TracePlayer(Actor as PlayerActor, position, position + point);
|
|
}
|
|
|
|
if (!traceGround.startSolid && (traceGround.fraction >= 1f ||
|
|
-Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) < movementParameters.slopeNormal))
|
|
// falling or sliding down a slope
|
|
movementState.onGround = false;
|
|
//Console.PrintDebug(1, true, "fall or slide");
|
|
else
|
|
//if (onGround != !traceGround.startSolid)
|
|
// Console.Print("slidefrac: " + traceGround.fraction);
|
|
movementState.onGround = !traceGround.startSolid;
|
|
//Console.PrintDebug(1, true, "issolid? :" + traceGround.startSolid);
|
|
|
|
//if (onGround && !slope)
|
|
// velocity.Y = 0f;
|
|
|
|
// TODO: snap to ground here?
|
|
return traceGround;
|
|
}
|
|
|
|
private bool OnJump(TraceInfo traceGround, ref Vector3 position, ref Vector3 velocity)
|
|
{
|
|
float jumpVel = movementParameters.jumpVelocity;
|
|
if (simulationTime - movementState.lastJumped < movementParameters.jumpBoostTime)
|
|
jumpVel += movementParameters.jumpBoostVelocity;
|
|
|
|
// Reset velocity from movementParameters.gravity
|
|
if (-Float3.Dot(movementParameters.gravity.Normalized, velocity) < 0 &&
|
|
Float3.Dot(velocity, traceGround.hitNormal) < -0.1)
|
|
{
|
|
velocity = Float3.ProjectOnPlane(velocity, traceGround.hitNormal);
|
|
}
|
|
|
|
if (movementParameters.jumpAdditive)
|
|
{
|
|
velocity += Float3.Up * jumpVel;
|
|
if (velocity.Y < jumpVel)
|
|
velocity.Y = jumpVel;
|
|
}
|
|
else
|
|
velocity = Float3.Up * jumpVel + new Float3(1, 0, 1) * velocity;
|
|
|
|
movementState.onGround = false;
|
|
|
|
// Allow stairs to eat the first jump to allow easy stair jumps
|
|
if (movementParameters.jumpStairBehavior && movementParameters.jumpBoostMaxJumps >= 2 && movementState.numJumps == 0 &&
|
|
-Float3.Dot(movementParameters.gravity.Normalized, traceGround.hitNormal) > 0.85)
|
|
{
|
|
// Try stepping into stairs without vertical velocity
|
|
Vector3 stairCheckPosition = position;
|
|
Vector3 stairCheckVelocity = velocity.Normalized * (movementParameters.stepHeight / Time.DeltaTime);
|
|
stairCheckVelocity.Y = 0f;
|
|
|
|
SlideMoveHit blocked = StepSlideMove(Actor as PlayerActor, movementParameters, ref stairCheckPosition, ref stairCheckVelocity, true);
|
|
float movedUp = stairCheckPosition.Y - position.Y;
|
|
|
|
if (movedUp > 0 && blocked.HasFlag(SlideMoveHit.Step))
|
|
{
|
|
velocity.Y = 0f;
|
|
movementState.onGround = true;
|
|
}
|
|
}
|
|
|
|
if (!predicting)
|
|
// Avoid overlapping with recent landing sound
|
|
if (simulationTime - movementState.lastLanded > 0.3)
|
|
PlayJumpLandSound(false, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void OnLanded(Float3 landingVelocity, bool hardLanding)
|
|
{
|
|
if (!predicting)
|
|
PlayJumpLandSound(true, hardLanding);
|
|
}
|
|
|
|
private void PlayJumpLandSound(bool landing, bool hardLanding)
|
|
{
|
|
if (!landing)
|
|
movementState.lastLanded = -1; // Reset so double jumps have double sounds
|
|
|
|
float volume1 = 0.8f;
|
|
float volume2 = volume1;
|
|
Float2 pitchRange = new Float2(0.9f, 1.05f);
|
|
Float2 secondStepDelayRange = new Float2(0.031f, 0.059f);
|
|
|
|
if (landing)
|
|
volume2 *= 0.6f;
|
|
else
|
|
volume1 *= 0.6f;
|
|
|
|
AudioManager.PlaySound("jumpland", Actor, 0, AudioFlags.None, rootActor.Position, volume1, pitchRange);
|
|
if (landing)
|
|
{
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
/*AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);
|
|
AudioManager.PlaySoundDelayed(secondStepDelayRange, "jumpland", Actor, 0, rootActor.Position, volume2,
|
|
pitchRange);*/
|
|
}
|
|
}
|
|
|
|
private static void ApplyFriction(PlayerMovementParameters movementParameters, ref Vector3 velocity)
|
|
{
|
|
float currentSpeed = velocity.Length;
|
|
|
|
float control = currentSpeed < movementParameters.stopspeed ? movementParameters.stopspeed : currentSpeed;
|
|
float drop = control * movementParameters.friction * Time.DeltaTime;
|
|
|
|
float newspeed = currentSpeed - drop;
|
|
if (newspeed < 0)
|
|
newspeed = 0;
|
|
|
|
if (currentSpeed < 0.0001f)
|
|
velocity *= 0;
|
|
else
|
|
velocity *= newspeed / currentSpeed;
|
|
}
|
|
|
|
private static void ApplyAirAcceleration(PlayerMovementParameters movementParameters, ref Vector3 velocity, Vector3 wishVelocity)
|
|
{
|
|
float wishspeed = wishVelocity.Length;
|
|
if (wishspeed > movementParameters.maxAirSpeed)
|
|
wishspeed = movementParameters.maxAirSpeed;
|
|
|
|
Vector3 wishDir = wishVelocity.Normalized;
|
|
float wishspeedAirControl = wishspeed;
|
|
|
|
if (movementParameters.airAcceleration != 0)
|
|
{
|
|
// Q2+ air acceleration
|
|
float accel = movementParameters.airAcceleration;
|
|
if (Float3.Dot(velocity, wishDir) < 0)
|
|
accel = movementParameters.airStopAcceleration;
|
|
|
|
if (movementParameters.airStrafeAcceleration != 0 && Mathf.Abs(wishVelocity.X) > 0 && wishVelocity.Y == 0)
|
|
{
|
|
// only strafe movement
|
|
if (wishspeed > movementParameters.maxAirStrafeSpeed)
|
|
wishspeed = movementParameters.maxAirStrafeSpeed;
|
|
|
|
accel = movementParameters.airStrafeAcceleration;
|
|
}
|
|
|
|
ApplyAcceleration(ref velocity, wishDir, wishspeed, float.MaxValue, accel);
|
|
}
|
|
|
|
// QW air acceleration
|
|
if (movementParameters.strafeAcceleration != 0f)
|
|
{
|
|
ApplyAcceleration(ref velocity, wishDir, wishspeed, movementParameters.maxAirStrafeSpeed,
|
|
movementParameters.strafeAcceleration);
|
|
}
|
|
|
|
// air control while holding forward/back buttons
|
|
//if (airControl != 0 && moveDirection.X == 0 && Mathf.Abs(moveDirection.Y) > 0)
|
|
// PM_Aircontrol(wishdir, wishspeedAirControl);
|
|
|
|
// apply movementParameters.gravity
|
|
velocity += movementParameters.gravity * Time.DeltaTime;
|
|
//Console.Print(Time.DeltaTime.ToString());
|
|
}
|
|
|
|
private static void ApplyAcceleration(ref Vector3 velocity, Vector3 wishDir, float wishspeed,
|
|
float maxWishspeed, float acceleration)
|
|
{
|
|
float wishspeedOrig = wishspeed;
|
|
if (wishspeed > maxWishspeed)
|
|
wishspeed = maxWishspeed;
|
|
|
|
float currentSpeed = Float3.Dot(velocity, wishDir);
|
|
float addSpeed = wishspeed - currentSpeed;
|
|
if (addSpeed <= 0f)
|
|
return;
|
|
|
|
float accelSpeed = acceleration * wishspeedOrig * Time.DeltaTime;
|
|
if (accelSpeed > addSpeed)
|
|
accelSpeed = addSpeed;
|
|
|
|
velocity += accelSpeed * wishDir;
|
|
}
|
|
|
|
[Flags]
|
|
private enum SlideMoveHit
|
|
{
|
|
Nothing = 0,
|
|
Step = 1,
|
|
Floor = 2,
|
|
Other = 4
|
|
}
|
|
}
|