Issues when pushing boxes

Godot Version

4.7

Question

Hello there! I am trying to make pushable boxes in my 2d platformer. I’m currently using Rigidbodies, watched a few youtube tutorials, but none of them worked. Max i did was get the box pushable, but it was too slow and jittery.

Here’s my player script (yes, I’m using C#)

using Godot;
using System;
using System.Threading.Tasks;

public partial class Player : CharacterBody2D {
    public bool InTransition = false;
    private float _highestFallVelocity = 0.0f;

    [ExportGroup("Movement Settings")]
    [Export] public float Spd = 360.0f;
    [Export] public float JumpPower = 480.0f;
    [Export(PropertyHint.Range, "0,1")] public float JumpCutoff = 0.5f;
    [Export] public float FallMultiplier = 4.5f;
    [Export] public float MaxFallSpeed = 1500.0f;

    [ExportGroup("Detection Settings")]
    [Export] public ShapeCast2D GroundCheck;
    [Export] public Area2D Hurtbox;

    [ExportGroup("Ledge Correction")]
    [Export] public float LedgeCheckHeight = 30.0f;
    [Export] public float LedgeCheckSide = 18.0f;
    [Export] public float LedgeNudgeForce = 30.0f;

    [ExportGroup("Feel Settings")]
    [Export] public float CoyoteTime = 0.2f;
    public float CoyoteCounter = 0.0f;
    [Export] public float JumpBufferTime = 0.2f;
    public float JumpBufferCounter = 0.0f;

    [ExportGroup("Movement Polish")]
    [Export] public float Acceleration = 3000.0f;
    [Export] public float Deceleration = 3000.0f;

    [ExportGroup("Animations")]
    [Export] public Node2D VisualTransform;
    [Export] public float TweenSpeed = 15.0f;
    [Export] public Vector2 JumpStretch = new Vector2(0.5f, 1.5f);
    [Export] public float MaxFallStretch = 0.5f;

    [ExportGroup("Rotation Settings")]
    [Export] public float RotationSmoothSpeed = 5.0f;
    public float TargetZRotation = 0.0f;

    [ExportGroup("Damage Feedback")]
    [Export] public Vector2 KnockbackForce = new Vector2(400.0f, 300.0f);
    [Export] public float DamageStunDuration = 0.3f;
    [Export] public Vector2 DamageStretch = new Vector2(0.5f, 1.5f);
    [Export] public float InvincibilityDuration = 2.0f;
    public bool IsStunned = false;
    public bool IsInvincible = false;
    public Tween BlinkTween;

    [ExportGroup("Unlocks")]
    [Export] public bool RotateScreenUnlocked = false;
    [Export] public bool DashUnlocked = false;
    [Export] public bool WallJumpUnlocked = false;
    [Export] public bool JumpBoostUnlocked = false;

    [ExportGroup("Dash Settings")]
    [Export] public float DashSpeed = 1200.0f;
    [Export] public float DashDuration = 0.2f;
    [Export] public float DashCooldown = 1.0f;
    [Export] public Vector2 DashStretch = new Vector2(1.5f, 0.5f);

    [ExportGroup("Wall Detection")]
    [Export] public ShapeCast2D WallCheck;

    [ExportGroup("Wall Jump Settings")]
    [Export] public float WallSlideSpeed = 120.0f;
    [Export] public Vector2 WallJumpForce = new Vector2(600.0f, 720.0f);
    [Export] public float WallJumpDuration = 0.2f;
    [Export(PropertyHint.Range, "0,1")] public float WallJumpCutoff = 0.8f;
    [Export] public Vector2 WallSlideStretch = new Vector2(0.5f, 1.2f);

    [ExportGroup("Audio Settings")]
    [Export] public AudioStreamPlayer2D FootstepAudio;
    [Export] public Timer FootstepTimer;
    [Export] public float FootstepDelay = 0.35f;
    [Export] public float MinPitch = 0.85f;
    [Export] public float MaxPitch = 1.15f;

    [ExportGroup("Audio Streams")]
    [Export] public AudioStream SfxFootstepGrass;
    [Export] public AudioStream SfxFootstepStone;
    [Export] public AudioStream SfxJump;
    [Export] public AudioStream SfxLand;

    [ExportGroup("Pushable Box")]
    [Export] public float PushForce = 1200.0f;
    [Export] public float MaxVelocity = 1200.0f;

    public float PullSpeed { get; private set; } = 0.0f;
    public void SetPullSpeed(float speed) => PullSpeed = speed;

    public Vector2 TargetScale = Vector2.One;
    public float Xinput = 0.0f;
    public float FacingDir = 1.0f;
    public bool LastJumpWasWallJump = false;

    public bool IsDashing = false;
    public bool CanDash = true;

    public bool IsWallSliding = false;
    public bool IsWallJumping = false;
    public float WallJumpCounter = 0.0f;

    public float DefaultGravity = (float)ProjectSettings.GetSetting("physics/2d/default_gravity");

    private AudioStreamPlayer _dynamicJumpAudio;
    private bool _wasGroundedLastFrame = true;

    public override void _Ready() {
        AddToGroup("player");

        if (SfxJump != null) {
            _dynamicJumpAudio = new AudioStreamPlayer();
            AddChild(_dynamicJumpAudio);
            _dynamicJumpAudio.Bus = "SFX";
            _dynamicJumpAudio.VolumeDb = -1.0f;
        }

        var roomChangeGlobal = GetNode<RoomChangeGlobal>("/root/RoomChangeGlobal");
        var globalStats = GetNode<GlobalStats>("/root/GlobalStats");

        if (roomChangeGlobal.Activate) {
            GlobalPosition = roomChangeGlobal.PlayerPos;
            TargetZRotation = roomChangeGlobal.PersistentRotation;
            GlobalRotation = TargetZRotation;

            float playerEnterDir = roomChangeGlobal.PlayerEnterDir;
            if (playerEnterDir != 0.0f) {
                StartTransitionWalk(playerEnterDir);
            }

            bool shouldJump = false;
            bool isUpsideDown = Mathf.Cos(GlobalRotation) < -0.1f;
            string playerExitEdge = (string)roomChangeGlobal.Get("PlayerExitEdge");

            switch (playerExitEdge) {
                case "left":
                case "right":
                    shouldJump = true;
                    break;
                case "up":
                    shouldJump = !isUpsideDown;
                    break;
                case "down":
                    shouldJump = isUpsideDown;
                    break;
            }

            if (shouldJump) {
                var spaceState = GetWorld2D().DirectSpaceState;
                float checkDistance = 40.0f;

                Vector2 origin = GlobalPosition;
                Vector2 rightVector = GlobalTransform.X;

                var queryLeft = PhysicsRayQueryParameters2D.Create(origin, origin - (rightVector * checkDistance));
                var hitLeft = spaceState.IntersectRay(queryLeft);

                var queryRight = PhysicsRayQueryParameters2D.Create(origin, origin + (rightVector * checkDistance));
                var hitRight = spaceState.IntersectRay(queryRight);

                float jumpPushDir = 0.0f;

                if (hitLeft.Count > 0 && hitRight.Count == 0)
                    jumpPushDir = 1.0f;
                else if (hitRight.Count > 0 && hitLeft.Count == 0)
                    jumpPushDir = -1.0f;
                else if (hitLeft.Count == 0 && hitRight.Count == 0)
                    jumpPushDir = GD.Randf() > 0.5f ? 1.0f : -1.0f;

                Vector2 localVel = Velocity.Rotated(-GlobalRotation);
                localVel.Y = -JumpPower;
                localVel.X = jumpPushDir * (Spd * 0.8f);

                Velocity = localVel.Rotated(GlobalRotation);
                TargetScale = JumpStretch;

                if (jumpPushDir != 0.0f) {
                    FacingDir = jumpPushDir;
                }
            }

            roomChangeGlobal.Set("Activate", false);
            roomChangeGlobal.Set("PlayerExitEdge", "none");
            globalStats.PlayerDied += _OnPlayerDeath;
        }
    }

    public override void _Process(double delta) {
        _HandleInput();
        _HandleVisuals((float)delta);
    }

    public override void _PhysicsProcess(double delta) {
        float fDelta = (float)delta;
        Vector2 localVel = Velocity.Rotated(-GlobalRotation);
        bool isGrounded = IsTouchingGround();

        bool isPushing = Mathf.Abs(Xinput) > 0.1f;
        float checkDir = isPushing ? Mathf.Sign(Xinput) : FacingDir;

        bool isTouchingWallCheck = IsTouchingWall(checkDir);

        if (isTouchingWallCheck && !isGrounded && localVel.Y >= 0.0f && isPushing && WallJumpUnlocked) {
            IsWallSliding = true;
            FacingDir = checkDir;
        }
        else {
            IsWallSliding = false;
        }

        if (isGrounded) {
            CoyoteCounter = CoyoteTime;
            _HandleFootsteps(localVel.X);
        }
        else {
            CoyoteCounter -= fDelta;
            if (localVel.Y > _highestFallVelocity) {
                _highestFallVelocity = localVel.Y;
            }
        }

        JumpBufferCounter -= fDelta;

        if (IsDashing) {
            Velocity = localVel.Rotated(GlobalRotation);
            MoveAndSlide();
            _wasGroundedLastFrame = IsTouchingGround();
            return;
        }

        if (JumpBufferCounter > 0.0f) {
            if (WallJumpUnlocked && (IsWallSliding || (isTouchingWallCheck && !isGrounded))) {
                IsWallJumping = true;
                IsWallSliding = false;
                WallJumpCounter = WallJumpDuration;

                localVel.X = -FacingDir * WallJumpForce.X;
                localVel.Y = -WallJumpForce.Y;

                TargetScale = JumpStretch;
                JumpBufferCounter = 0.0f;
                CoyoteCounter = 0.0f;
                FacingDir = -FacingDir;
                LastJumpWasWallJump = true;
                _highestFallVelocity = 0.0f;
                _PlayJumpSfx();
            }
            else if (CoyoteCounter > 0.0f) {
                localVel.Y = -JumpPower;
                TargetScale = JumpStretch;
                JumpBufferCounter = 0.0f;
                CoyoteCounter = 0.0f;
                LastJumpWasWallJump = false;
                _highestFallVelocity = 0.0f;
                _PlayJumpSfx();
            }
        }

        if (IsWallJumping) {
            WallJumpCounter -= fDelta;
            if (WallJumpCounter <= 0.0f) {
                IsWallJumping = false;
            }
        }

        if (!IsWallJumping) {
            if (IsStunned) {
                localVel.X = Mathf.MoveToward(localVel.X, 0.0f, 600.0f * fDelta);
            }
            else {
                float targetSpeed = Xinput * Spd;
                float accelRate = Mathf.Abs(targetSpeed) > 0.01f ? Acceleration : Deceleration;
                localVel.X = Mathf.MoveToward(localVel.X, targetSpeed, accelRate * fDelta);
            }
        }

        if (IsWallSliding) {
            if (localVel.Y > WallSlideSpeed) {
                localVel.Y = WallSlideSpeed;
                _highestFallVelocity = 0.0f;
            }
        }
        else if (!isGrounded) {
            float gravityToApply = DefaultGravity;

            if (localVel.Y > 0.0f) {
                float fallProgress = Remap(localVel.Y, 0.0f, MaxFallSpeed, 1.0f, 1.5f);
                gravityToApply *= FallMultiplier * fallProgress;
            }

            localVel.Y += gravityToApply * fDelta;

            if (localVel.Y > MaxFallSpeed) {
                localVel.Y = MaxFallSpeed;
            }
        }

        if (localVel.Y < 0.0f && !IsWallJumping) {
            _ApplyLedgeCorrection();
        }

        float intentionalHorizontalSpeed = localVel.X;

        Velocity = localVel.Rotated(GlobalRotation);
        UpDirection = Vector2.Up.Rotated(GlobalRotation);
        MoveAndSlide();

        for (int i = 0; i < GetSlideCollisionCount(); i++) {
            KinematicCollision2D collision = GetSlideCollision(i);

            if (collision.GetCollider() is RigidBody2D box && box.IsInGroup("PushBox")) {
                Vector2 normal = collision.GetNormal();

                if (Mathf.Abs(normal.X) > 0.5f && Mathf.Sign(Xinput) == -Mathf.Sign(normal.X)) {
                    if (Mathf.Abs(box.LinearVelocity.X) < MaxVelocity) {
                        box.ApplyCentralImpulse(collision.GetNormal() * -PushForce);
                    }
                }
            }
        }

        bool isGroundedNow = IsTouchingGround();
        if (!_wasGroundedLastFrame && isGroundedNow) {
            _PlayLandSfx(_highestFallVelocity);
            _highestFallVelocity = 0.0f;
        }

        _wasGroundedLastFrame = isGroundedNow;
    }

    private void _HandleInput() {
        if (InTransition) return;

        Xinput = Input.GetAxis("ui_left", "ui_right");

        if (Mathf.Cos(GlobalRotation) < -0.1f) {
            Xinput = -Xinput;
        }

        if (Mathf.Abs(Xinput) > 0.1f) {
            FacingDir = Mathf.Sign(Xinput);
        }

        if (Input.IsActionJustPressed("dash") && DashUnlocked && CanDash && !IsDashing) {
            _ = PerformDash();
        }

        if (Input.IsActionJustPressed("jump")) {
            JumpBufferCounter = JumpBufferTime;
        }

        if (Input.IsActionJustReleased("jump")) {
            Vector2 localVel = Velocity.Rotated(-GlobalRotation);
            {
                localVel.Y *= JumpCutoff;
                Velocity = localVel.Rotated(GlobalRotation);
                CoyoteCounter = 0.0f;
            }
        }

        if (Input.IsActionJustPressed("rotate_screen") && RotateScreenUnlocked) {
            TargetZRotation += Mathf.DegToRad(180.0f);
        }
    }

    public async Task PerformDash() {
        CanDash = false;
        IsDashing = true;

        float dashDir = Mathf.Abs(Xinput) > 0.1f ? Mathf.Sign(Xinput) : FacingDir;

        Vector2 localVel = Vector2.Zero;
        localVel.X = dashDir * DashSpeed;
        Velocity = localVel.Rotated(GlobalRotation);

        TargetScale = DashStretch;

        await ToSignal(GetTree().CreateTimer(DashDuration), SceneTreeTimer.SignalName.Timeout);

        IsDashing = false;

        await ToSignal(GetTree().CreateTimer(DashCooldown), SceneTreeTimer.SignalName.Timeout);
        CanDash = true;
    }

    private void _HandleVisuals(float delta) {
        GlobalRotation = Mathf.LerpAngle(GlobalRotation, TargetZRotation, delta * RotationSmoothSpeed);

        if (IsStunned) return;

        Vector2 localVel = Velocity.Rotated(-GlobalRotation);
        bool isGrounded = IsTouchingGround();

        if (IsWallSliding) {
            TargetScale = WallSlideStretch;
        }
        else if (!isGrounded && localVel.Y > 0.0f) {
            float fallFactor = Mathf.InverseLerp(0.0f, MaxFallSpeed, localVel.Y);
            float newY = 1.0f + (fallFactor * MaxFallStretch);
            float newX = 1.0f / newY;
            TargetScale = new Vector2(newX, newY);
        }
        else {
            TargetScale = TargetScale.MoveToward(Vector2.One, delta * (TweenSpeed / 2.0f));
        }

        VisualTransform.Scale = VisualTransform.Scale.Lerp(TargetScale, delta * TweenSpeed);

        float yOffset = (VisualTransform.Scale.Y - 1.0f) * -0.5f;
        float targetXOffset = IsWallSliding ? ((1.0f - WallSlideStretch.X) * 0.5f * -FacingDir) : 0.0f;

        float currentXOffset = VisualTransform.Position.X;
        float newXOffset = Mathf.Lerp(currentXOffset, targetXOffset, delta * TweenSpeed);

        VisualTransform.Position = new Vector2(newXOffset, yOffset);
    }

    private void _ApplyLedgeCorrection() {
        var spaceState = GetWorld2D().DirectSpaceState;

        Vector2 origin = GlobalPosition;
        Vector2 up = -GlobalTransform.Y;
        Vector2 right = GlobalTransform.X;

        var queryMid = PhysicsRayQueryParameters2D.Create(origin, origin + (up * LedgeCheckHeight));
        var hitMid = spaceState.IntersectRay(queryMid);

        if (hitMid.Count > 0) {
            Vector2 leftOffset = origin + (right * -LedgeCheckSide);
            Vector2 rightOffset = origin + (right * LedgeCheckSide);

            var queryLeft = PhysicsRayQueryParameters2D.Create(leftOffset, leftOffset + (up * LedgeCheckHeight));
            var hitLeft = spaceState.IntersectRay(queryLeft);

            var queryRight = PhysicsRayQueryParameters2D.Create(rightOffset, rightOffset + (up * LedgeCheckHeight));
            var hitRight = spaceState.IntersectRay(queryRight);

            if (hitRight.Count > 0 && hitLeft.Count == 0) {
                GlobalPosition += right * -LedgeNudgeForce * (float)GetPhysicsProcessDeltaTime();
            }
            else if (hitLeft.Count > 0 && hitRight.Count == 0) {
                GlobalPosition += right * LedgeNudgeForce * (float)GetPhysicsProcessDeltaTime();
            }
        }
    }

    private void _HandleFootsteps(float currentHorizontalSpeed) {
        if (FootstepAudio == null || FootstepTimer == null) return;

        if (Mathf.Abs(currentHorizontalSpeed) > 10.0f && !IsDashing && FootstepTimer.IsStopped()) {
            string surfaceType = GetSurfaceMaterialBelow();
            AudioStream streamToPlay = null;

            if (surfaceType == "grass")
                streamToPlay = SfxFootstepGrass;
            else if (surfaceType == "stone")
                streamToPlay = SfxFootstepStone;

            if (streamToPlay != null) {
                FootstepAudio.Stream = streamToPlay;
                FootstepAudio.PitchScale = (float)GD.RandRange(MinPitch, MaxPitch);
                FootstepAudio.Play();
                FootstepTimer.Start(FootstepDelay);
            }
        }
    }

    public string GetSurfaceMaterialBelow() {
        if (GroundCheck == null || !GroundCheck.IsColliding()) return "";

        GodotObject collider = GroundCheck.GetCollider(0);

        if (collider is TileMapLayer tileMapLayer) {
            Vector2 hitPoint = GroundCheck.GetCollisionPoint(0);
            hitPoint.Y += 2.0f;
            Vector2I mapPos = tileMapLayer.LocalToMap(tileMapLayer.ToLocal(hitPoint));
            TileData tileData = tileMapLayer.GetCellTileData(mapPos);

            if (tileData != null) {
                Variant material = tileData.GetCustomData("surface");
                return material.VariantType != Variant.Type.Nil ? material.AsString() : "";
            }
        }
        else if (collider is TileMap tileMap) {
            Vector2 hitPoint = GroundCheck.GetCollisionPoint(0);
            hitPoint.Y += 2.0f;
            Vector2I mapPos = tileMap.LocalToMap(tileMap.ToLocal(hitPoint));
            TileData tileData = tileMap.GetCellTileData(0, mapPos);

            if (tileData != null) {
                Variant material = tileData.GetCustomData("surface");
                return material.VariantType != Variant.Type.Nil ? material.AsString() : "";
            }
        }

        return "";
    }

    public bool IsTouchingGround() {
        if (GroundCheck == null || !GroundCheck.IsColliding()) return false;

        Vector2 localVel = Velocity.Rotated(-GlobalRotation);
        if (localVel.Y < -5.0f) return false;

        return true;
    }

    public bool IsTouchingWall(float dir = 0.0f) {
        if (WallCheck == null) return false;

        float signDir = Mathf.Sign(dir);
        if (signDir == 0.0f) signDir = 1.0f;

        Vector2 currentPos = WallCheck.Position;
        currentPos.X = Mathf.Abs(currentPos.X) * signDir;
        WallCheck.Position = currentPos;

        Vector2 targetPos = WallCheck.TargetPosition;
        targetPos.X = Mathf.Abs(targetPos.X) * signDir;
        WallCheck.TargetPosition = targetPos;

        WallCheck.ForceShapecastUpdate();
        return WallCheck.IsColliding();
    }

    public float Remap(float value, float istart, float istop, float ostart, float ostop) {
        return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
    }

    public async void StartTransitionWalk(float direction) {
        InTransition = true;
        Xinput = direction;
        FacingDir = direction;

        var roomChangeGlobal = GetNode("/root/RoomChangeGlobal");
        string playerExitEdge = (string)roomChangeGlobal.Get("PlayerExitEdge");
        double cutsceneDuration = playerExitEdge != "none" ? 0.5 : 0.4;

        await ToSignal(GetTree().CreateTimer(cutsceneDuration), SceneTreeTimer.SignalName.Timeout);

        if (InTransition) {
            InTransition = false;
            Xinput = 0.0f;
        }
    }

    public bool IsOnCameraResetSurface() {
        if (GroundCheck == null || !GroundCheck.IsColliding()) return false;

        Node2D collider = (Node2D)GroundCheck.GetCollider(0);
        return collider.IsInGroup("camera_reset");
    }

    private void _PlayJumpSfx() {
        if (_dynamicJumpAudio == null || SfxJump == null) return;

        _dynamicJumpAudio.Stream = SfxJump;
        _dynamicJumpAudio.PitchScale = (float)GD.RandRange(MinPitch, MaxPitch);
        _dynamicJumpAudio.VolumeDb = -2.0f;
        _dynamicJumpAudio.Play();
    }

    private void _PlayLandSfx(float fallVelocity) {
        if (_dynamicJumpAudio == null || SfxLand == null) return;

        float minimumTriggerSpeed = 3072.0f;

        if (fallVelocity > minimumTriggerSpeed) {
            _dynamicJumpAudio.Stream = SfxLand;

            float calculatedPitch = Remap(fallVelocity, minimumTriggerSpeed, MaxFallSpeed, 1.1f, 0.6f);
            calculatedPitch += (float)GD.RandRange(-0.05f, 0.05f);
            _dynamicJumpAudio.PitchScale = Mathf.Clamp(calculatedPitch, 0.5f, 1.2f);

            float calculatedVolume = Remap(fallVelocity, minimumTriggerSpeed, MaxFallSpeed, -4.0f, 4.0f);
            _dynamicJumpAudio.VolumeDb = Mathf.Clamp(calculatedVolume, -10.0f, 6.0f);

            _dynamicJumpAudio.Play();
        }
    }

    private void _OnHurtboxAreaEntered(Area2D enemyHitbox) {
        if (IsDashing || InTransition || IsStunned || IsInvincible) return;

        var globalStats = GetNode("/root/GlobalStats");
        if ((int)globalStats.Get("current_health") <= 0) return;

        int damageToTake = 1;
        if (enemyHitbox.Get("damage").VariantType != Variant.Type.Nil) {
            damageToTake = (int)enemyHitbox.Get("damage");
        }

        globalStats.Call("take_damage", damageToTake);
        globalStats.Call("trigger_hit_freeze", 10);

        _ApplyDamageFeedback(enemyHitbox);
        _TriggerIframes();
    }

    private async void _ApplyDamageFeedback(Area2D enemyHitbox) {
        IsStunned = true;

        float hitDir = Mathf.Sign(GlobalPosition.X - enemyHitbox.GlobalPosition.X);
        if (hitDir == 0.0f) {
            hitDir = -FacingDir;
        }

        FacingDir = -hitDir;

        Tween hitTween = CreateTween().SetParallel(true);
        hitTween.SetProcessMode(Tween.TweenProcessMode.Physics);
        hitTween.SetIgnoreTimeScale();

        Vector2 displacement = new Vector2(hitDir * 60.0f, -40.0f).Rotated(GlobalRotation);
        Vector2 targetPos = GlobalPosition + displacement;

        hitTween.TweenProperty(this, "global_position", targetPos, 0.16f).SetTrans(Tween.TransitionType.Linear);

        VisualTransform.Scale = DamageStretch;
        float targetHitRotation = Mathf.DegToRad(45.0f) * hitDir;
        hitTween.TweenProperty(VisualTransform, "rotation", targetHitRotation, 0.08f).SetTrans(Tween.TransitionType.Linear);

        await ToSignal(GetTree().CreateTimer(0.16f, true, false, true), SceneTreeTimer.SignalName.Timeout);

        Vector2 localVel = Velocity.Rotated(-GlobalRotation);
        localVel.X = hitDir * (KnockbackForce.X * 0.7f);
        localVel.Y = -KnockbackForce.Y * 0.5f;
        Velocity = localVel.Rotated(GlobalRotation);

        float remainingStun = Mathf.Max(0.05f, DamageStunDuration - 0.16f);
        await ToSignal(GetTree().CreateTimer(remainingStun), SceneTreeTimer.SignalName.Timeout);

        IsStunned = false;

        Tween recoverTween = CreateTween();
        recoverTween.TweenProperty(VisualTransform, "rotation", 0.0f, 0.15f).SetTrans(Tween.TransitionType.Sine);
    }

    private void _OnPlayerDeath() {
        InTransition = true;
        Xinput = 0.0f;
        Velocity = Vector2.Zero;
    }

    private async void _TriggerIframes() {
        IsInvincible = true;

        if (BlinkTween != null && BlinkTween.IsValid()) {
            BlinkTween.Kill();
        }

        BlinkTween = CreateTween().SetLoops();
        BlinkTween.TweenProperty(VisualTransform, "modulate:a", 0.2f, 0.08f);
        BlinkTween.TweenProperty(VisualTransform, "modulate:a", 1.0f, 0.08f);

        await ToSignal(GetTree().CreateTimer(InvincibilityDuration), SceneTreeTimer.SignalName.Timeout);

        IsInvincible = false;

        if (BlinkTween != null && BlinkTween.IsValid()) {
            BlinkTween.Kill();
        }

        Color clr = VisualTransform.Modulate;
        clr.A = 1.0f;
        VisualTransform.Modulate = clr;
    }
}

(main thing you wanna look at is in physics process, after move and slide. don’t mind my messy code lol)