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)