using Godot; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; public enum JumpKind { Ground, Turnback, Midair } public partial class PlayerPhysicsController : CharacterBody2D { // [Export] // public float RunMultiplier { get; set; } = 1.84f; [Signal] public delegate void OnTurnbackJumpEventHandler(bool onJumpPoint, bool isOnWall); [Signal] public delegate void OnJumpEventHandler(JumpKind kind, bool isOnGround, bool isOnWall); [ExportCategory("Accelerations and Impulses")] [Export] public float WalkAcceleration { get; set; } = 3f; [Export] public float StoppingDeceleration { get; set; } = 8f; [Export] public float CancellationAcceleration { get; set; } = 4f; [Export] public float AirAcceleration { get; set; } = 0.5f; [Export] public float JumpHeight { get; set; } = 5.5f; [ExportCategory("Max Speeds")] [Export] private float MaxFallSpeed { get; set; } = 8f; [Export] private float MaxRiseSpeed { get; set; } = 16f; [Export] private float MaxRunSpeed { get; set; } = 16f; [Export] private float MaxWalkSpeed { get; set; } = 8f; [ExportCategory("Objects")] [Export] public Label DebugTextLabel { get; set; } [Export] public AnimatedSprite2D Sprite { get; set; } [Export] public PredictiveCollisions Predictor { get; set; } [ExportCategory("Physics Tweaks")] [Export] public float Gravity { get; set; } = 30.625f; private float TilesToMeters => 9.81f / Gravity; private float PxToMeters => (1 / UnitToPx) * TilesToMeters; [Export] public float UnitToPx { get; set; } = 32; private float _timeOffGround = 0.0f; [Export] public float CoyoteTiming { get; set; } = 0.1f; [Export] public float Deadzone { get; set; } = 0.1f; private float _lv = 0.0f; private void RecalculateLv() { _lv = Mathf.Sqrt(Mathf.Pow(Gravity * 0.1f / 2, 2) + 2 * Gravity * JumpHeight); } public override void _Ready() { RecalculateLv(); base._Ready(); } public override void _Process(double delta) { var grounded = this.IsOnFloor(); var absvel = new Vector2(Mathf.Abs(Velocity.X), Mathf.Abs(Velocity.Y)); var xAxis = Input.GetAxis("move_left", "move_right"); Sprite.FlipH = xAxis < 0; if (grounded && absvel.X > 0.1) { Sprite.Animation = "walk"; Sprite.SpeedScale = absvel.X switch { < 10 => 1, < 150 => 2, < 600 => 4, _ => 4 }; if (!Sprite.IsPlaying()) Sprite.Play(); } else { Sprite.SpeedScale = 1; } if (grounded && absvel.X < 0.1) { Sprite.Animation = "idle"; if (!Sprite.IsPlaying()) Sprite.Play(); } if (!grounded && Velocity.Y > 0.1) { Sprite.Animation = "fall"; } if (!grounded && Velocity.Y < 0.1) { Sprite.Animation = "jump"; } } public override void _PhysicsProcess(double delta) { var xAxis = Input.GetAxis("move_left", "move_right"); var yAxis = Input.GetAxis("move_up", "move_down"); var lookAxis = Input.GetAxis("look_up", "look_down"); // var jumpStrength = -Input.GetActionStrength("jump"); RecalculateLv(); float velx = (Velocity.X / UnitToPx); float vely = (Velocity.Y / UnitToPx) + Gravity * (float)delta; if (Mathf.Abs(velx) < 0.1) velx = 0; var grounded = this.IsOnFloor(); var coyoteGrounded = _timeOffGround < CoyoteTiming; if (grounded || coyoteGrounded) { if (grounded) _timeOffGround = 0; else _timeOffGround += (float)delta; if (Input.IsActionJustPressed("jump")) { EmitSignal(SignalName.OnJump, (int)JumpKind.Ground, this.IsOnFloor(), this.IsOnWall()); vely = -_lv; } if (!grounded && coyoteGrounded && Input.IsActionJustReleased("jump") && vely < 0) { vely *= 1/4f; } var accel = WalkAcceleration; if (xAxis * Velocity.X < 0) accel = StoppingDeceleration; velx += xAxis * accel * (float)delta; } else { if (Input.IsActionJustReleased("jump") && vely < 0) { vely *= 1/4f; } if (Input.IsActionJustPressed("turn_back") && vely > 0 && velx != 0) { var side = velx > 0 ? PredictiveCollisions.Location.BottomRight : PredictiveCollisions.Location.BottomLeft; if (Predictor.AnyHit(side) && !this.IsOnWall()) { EmitSignal(SignalName.OnTurnbackJump, true, this.IsOnWall()); EmitSignal(SignalName.OnJump, (int)JumpKind.Turnback, this.IsOnFloor(), this.IsOnWall()); var hits = Predictor.GetLastCastHits(side); _ = hits.First(x => { if (x.Obj is null) return false; var collider = x.AsGodotDictionary()["collider"].AsGodotObject(); if (collider.HasMeta("JumpPointAnimatable")) { var animPath = collider.GetMeta("JumpPointAnimatable").AsNodePath(); var anim = collider.Call(MethodName.GetNode, animPath).As(); var frame = anim.Frame; void anf() { anim.Stop(); anim.Frame = frame; anim.AnimationLooped -= anf; }; anim.AnimationLooped += anf; anim.Play(); } return true; }); // has jump point float theta = Mathf.Atan2(-yAxis, Mathf.Abs(xAxis)); float margin = 20 * (Mathf.Pi / 180f); if (theta < margin) theta = margin; if (theta > Mathf.Pi / 2 - margin) theta = Mathf.Pi/2 - margin; Vector2 turnjump = new Vector2(-Mathf.Sign(velx) * Mathf.Cos(theta), -Mathf.Sin(theta) - 0.2f) * _lv; GD.Print($"Theta: {theta * (180 / Mathf.Pi)}, turnjump: {turnjump}"); velx = turnjump.X; vely = turnjump.Y; } else { // does not have jump point // float theta = Mathf.Atan2(-yAxis, Mathf.Abs(xAxis)); // float margin = 20 * (Mathf.Pi / 180f); // if (theta < margin) theta = margin; // if (theta > Mathf.Pi / 2 - margin) theta = Mathf.Pi/2 - margin; // Vector2 turnjump = new Vector2(-Mathf.Sign(Velocity.X) * Mathf.Cos(theta), -Mathf.Sin(theta) - 0.2f) * _lv; // GD.Print($"Theta: {theta * (180 / Mathf.Pi)}, turnjump: {turnjump}"); // velx = turnjump.X; // vely = turnjump.Y; } } if (Input.IsActionJustPressed("jump") && Mathf.Abs(vely) < 2) { EmitSignal(SignalName.OnJump, (int)JumpKind.Midair, this.IsOnFloor(), this.IsOnWall()); vely = -_lv; } velx += xAxis * AirAcceleration * (float)delta; } if (grounded && Mathf.Abs(xAxis) < Deadzone) { var autostop = -Mathf.Sign(velx) * StoppingDeceleration * (float)delta; if (Mathf.Abs(autostop) > Mathf.Abs(velx)) velx = 0f; else velx += autostop; } var maxXSpeed = Input.IsActionPressed("run") ? MaxRunSpeed : MaxWalkSpeed; var maxYSpeed = vely > 0 ? MaxFallSpeed : MaxRiseSpeed; var (absvelx, absvely) = (Mathf.Abs(velx), Mathf.Abs(vely)); float xcancel = 0, ycancel = 0; if (absvelx > maxXSpeed) xcancel = -Math.Sign(velx) * CancellationAcceleration * (float)delta; if (absvely > maxYSpeed) ycancel = -Math.Sign(vely) * CancellationAcceleration * (float)delta; if (Mathf.Abs(xcancel) + 0.1 > absvelx) velx = 0; else velx += xcancel; if (Mathf.Abs(ycancel) + 0.1 > absvely) vely = 0; else vely += ycancel; DebugTextLabel.Text = $"grounded: {grounded}\n" + $"vely: {vely}\n" + $"velx: {velx}\n" + $"lv : {_lv:F2} or {_lv/(float)delta:F2}\n" + $"lcrc : {Predictor.AnyHit(PredictiveCollisions.Location.BottomLeft)}/{Predictor.AnyHit(PredictiveCollisions.Location.BottomRight)}"; // Force = new Vector2(0, forcey); Velocity = new Vector2(velx, vely); Velocity *= UnitToPx; MoveAndSlide(); } }