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 bool predicting = false; private readonly List touchingActors = new List(); //public int currentInputFrame; //private int currentInputFrame2; public IPlayerInput Input; //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 = 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(); Console.Print("player awake, playerid: " + PlayerId); rootActor = Actor.GetChild("RootActor"); rigidBody = Actor.As(); playerActor = Actor.As(); cameraHolder = rootActor.GetChild("CameraHolder"); // Setup input with no controller //SetInput(NetworkReplicator.GetObjectOwnerClientId(this.Parent)); //rigidBody.CollisionEnter += OnCollisionEnter; //rigidBody.TriggerEnter += OnTriggerEnter; //rigidBody.TriggerExit += OnTriggerExit; } public void SetInput(uint playerId) { //if (playerId == 0) // input = new PlayerInput(); Assert.IsTrue(playerId != uint.MaxValue); PlayerId = playerId; bool isServerScene = Scene.Name == "ServerScene"; if (isServerScene) worldStateManager = NetworkManager.serverWorldStateManager; else worldStateManager = NetworkManager.clientWorldStateManager; if (PlayerId == NetworkManager.LocalPlayerClientId && !isServerScene)//if (NetworkReplicator.GetObjectRole(this.Parent) == NetworkObjectRole.OwnedAuthoritative)// if (playerId == NetworkManager.LocalPlayerClientId) { Console.Print("local player?: " + playerId.ToString()); //string demoPath = System.IO.Path.Combine(AssetManager.DemoPath, $"{DateTimeOffset.Now.UtcTicks}.gdem"); //input = new PlayerInputLocal(playerActor, demoPath); // TODO: support recording Input = new PlayerInput2();//new PlayerInputLocal(playerActor); } else { Console.Print("network player: " + playerId.ToString()); Input = new PlayerInputNetwork2(playerId, worldStateManager); } Assert.IsTrue(worldStateManager != null); } #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() { //input.OnUpdate(); //if (Input is PlayerInputDemo /*&& currentInputFrame2 >= currentInputFrame*/) // return; Input.SetFrame(worldStateManager.ClientFrame); 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() { 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(); 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*/ && worldStateManager.ClientFrame > 0 && worldStateManager.ServerFrame > 0) { ulong currentFrame = worldStateManager.ServerFrame; for (; currentFrame < worldStateManager.ClientFrame; currentFrame++) { if (!worldStateManager.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; predicting = true; currentFrame = worldStateManager.ServerFrame; for (; currentFrame < lastFrame; currentFrame++) { //if (!Input.GetState(currentFrame, out var pastInputState, out var pastActorState)) var frameInfo = worldStateManager.GetPlayerFrame(PlayerId, currentFrame); if (frameInfo == null) { Console.Print($"unexpected predict failure: {currentFrame}"); break; } //if (currentFrame == inputState.frame) // movementState.jumped = movementState.jumped; if (currentFrame == worldStateManager.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, worldStateManager.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, worldStateManager.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}"); worldStateManager.RecordPlayerInput(PlayerId, worldStateManager.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); }*/ } 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(); 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(); BoxCollider boxCollider = Actor.GetChild(); MeshCollider meshCollider = Actor.GetChild(); 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(); foreach (PhysicsColliderActor collider in colliders) { if (collider.Parent == Actor) continue; collidersFiltered.Add(collider); } colliders = collidersFiltered.ToArray(); if (colliders.Length == 0) collided = false; } return collided; } /// /// Sweeps the player rigidbody in world and returns geometry which was hit during the trace. /// /// Player actor /// Start position /// End position /// 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 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(); BoxCollider boxCollider = Actor.GetChild(); MeshCollider meshCollider = Actor.GetChild(); if (capsuleCollider && capsuleCollider.IsActive) { Quaternion rotation = capsuleCollider.LocalOrientation * Quaternion.Euler(0f, 90f, 0f); DebugDraw.DrawWireTube(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.DrawWireTube(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 (worldStateManager.IsClient) { var serverBbox = boxCollider.OrientedBox.GetBoundingBox(); var frameInfo = worldStateManager.GetPlayerFrame(PlayerId, worldStateManager.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(); 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 = worldStateManager.ClientTime + (frame * (1.0f / Time.PhysicsFPS)); 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 } }