From 40cb6f77ac9b5de3ed3d59640792da4bd4453be7 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 25 Oct 2024 11:26:28 +0100 Subject: [PATCH 1/6] Player: Cut off jump if space is released before apex This technique is borrowed from the MOVEMENT 2 demo available in the Godot Asset Library. It makes jumping feel a little more natural: you can do a little jump by tapping space, or a long jump by holding it a little longer. --- scripts/player.gd | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/player.gd b/scripts/player.gd index 4897618..e0635c7 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -13,6 +13,11 @@ extends CharacterBody2D ## be influenced by the [member GameLogic.gravity]. @export_range(-1000, 1000, 10, "suffix:px/s") var jump_velocity = -880.0 +## How much should the character's jump be reduced if you let go of the jump +## key before the top of the jump? [code]0[/code] means “not at all”; +## [code]100[/code] means “upwards movement completely stops”. +@export_range(0, 100, 5, "suffix:%") var jump_cut_factor: float = 20 + # Get the gravity from the project settings to be synced with RigidBody nodes. var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") var original_position: Vector2 @@ -68,6 +73,11 @@ func _physics_process(delta): if Input.is_action_just_pressed("ui_accept") and is_on_floor(): velocity.y = jump_velocity + # Reduce velocity if the player lets go of the jump key before the apex. + # This allows controlling the height of the jump. + if Input.is_action_just_released("ui_accept") and velocity.y < 0: + velocity.y *= (1 - (jump_cut_factor / 100.00)) + # Get the input direction and handle the movement/deceleration. # As good practice, you should replace UI actions with custom gameplay actions. var direction = Input.get_axis("ui_left", "ui_right") From 2d77b9a5bd347c7d9f0bc4d8d917ada087231f05 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 25 Oct 2024 11:43:32 +0100 Subject: [PATCH 2/6] =?UTF-8?q?Player:=20Implement=20=E2=80=9Ccoyote=20tim?= =?UTF-8?q?e=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is common in platform games to allow the player a few frames' grace after walking off the edge of a ledge where they can still jump as if they were still standing on the ledge. This technique is inspired by the MOVEMENT 2 demo available in the Asset Library. --- scripts/player.gd | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/player.gd b/scripts/player.gd index e0635c7..8a2f8a1 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -18,6 +18,14 @@ extends CharacterBody2D ## [code]100[/code] means “upwards movement completely stops”. @export_range(0, 100, 5, "suffix:%") var jump_cut_factor: float = 20 +## How long after the character walks off a ledge can they still jump? +## This is often set to a small positive number to allow the player a little +## margin for error before they start falling. +@export_range(0, 0.5, 1 / 60.0, "suffix:s") var coyote_time: float = 5.0 / 60.0 + +# If positive, the player is either on the ground, or left the ground less than this long ago +var coyote_timer: float = 0 + # Get the gravity from the project settings to be synced with RigidBody nodes. var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") var original_position: Vector2 @@ -65,19 +73,23 @@ func _physics_process(delta): if Global.lives <= 0: return - # Add the gravity. - if not is_on_floor(): - velocity.y += gravity * delta + # Handle jump + if is_on_floor(): + coyote_timer = (coyote_time + delta) - # Handle jump. - if Input.is_action_just_pressed("ui_accept") and is_on_floor(): + if Input.is_action_just_pressed("ui_accept") and coyote_timer > 0: velocity.y = jump_velocity + coyote_timer = 0 # Reduce velocity if the player lets go of the jump key before the apex. # This allows controlling the height of the jump. if Input.is_action_just_released("ui_accept") and velocity.y < 0: velocity.y *= (1 - (jump_cut_factor / 100.00)) + # Add the gravity. + if coyote_timer <= 0: + velocity.y += gravity * delta + # Get the input direction and handle the movement/deceleration. # As good practice, you should replace UI actions with custom gameplay actions. var direction = Input.get_axis("ui_left", "ui_right") @@ -100,10 +112,13 @@ func _physics_process(delta): move_and_slide() + coyote_timer -= delta + func reset(): position = original_position velocity = Vector2.ZERO + coyote_timer = 0 func _on_lives_changed(): From 6dd6ea2237546e1e7902bdea2265d7598aa698c4 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 25 Oct 2024 11:43:32 +0100 Subject: [PATCH 3/6] Player: Implement jump buffering Previously, you could only jump if you were already on the ground when you press the jump button. This seems logical but is actually very annoying: if you press jump 1 frame too early, then you miss. It is common in platformers to allow a few frames' grace, so that you can press the jump key a few frames early and still start a jump when the player touches the ground. To implement this, count down from 0.1s (6 ticks at the default 60 ticks per second) when the jump action is just pressed. This technique is borrowed from MOVEMENT 2. --- scripts/player.gd | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/player.gd b/scripts/player.gd index 8a2f8a1..4f82113 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -23,9 +23,17 @@ extends CharacterBody2D ## margin for error before they start falling. @export_range(0, 0.5, 1 / 60.0, "suffix:s") var coyote_time: float = 5.0 / 60.0 +## If the character is about to land on the floor, how early can the player +## the jump key to jump as soon as the character lands? This is often set to +## a small positive number to allow the player a little margin for error. +@export_range(0, 0.5, 1 / 60.0, "suffix:s") var jump_buffer: float = 5.0 / 60.0 + # If positive, the player is either on the ground, or left the ground less than this long ago var coyote_timer: float = 0 +# If positive, the player pressed jump this long ago +var jump_buffer_timer: float = 0 + # Get the gravity from the project settings to be synced with RigidBody nodes. var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") var original_position: Vector2 @@ -77,9 +85,13 @@ func _physics_process(delta): if is_on_floor(): coyote_timer = (coyote_time + delta) - if Input.is_action_just_pressed("ui_accept") and coyote_timer > 0: + if Input.is_action_just_pressed("ui_accept"): + jump_buffer_timer = (jump_buffer + delta) + + if jump_buffer_timer > 0 and coyote_timer > 0: velocity.y = jump_velocity coyote_timer = 0 + jump_buffer_timer = 0 # Reduce velocity if the player lets go of the jump key before the apex. # This allows controlling the height of the jump. @@ -113,12 +125,14 @@ func _physics_process(delta): move_and_slide() coyote_timer -= delta + jump_buffer_timer -= delta func reset(): position = original_position velocity = Vector2.ZERO coyote_timer = 0 + jump_buffer_timer = 0 func _on_lives_changed(): From 58cb737c63083d713d076d3a090dc50e93c66035 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 25 Oct 2024 12:58:53 +0100 Subject: [PATCH 4/6] Player: Add acceleration With the defaults it takes 5 frames to accelerate to the maximum speed. --- scripts/player.gd | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/player.gd b/scripts/player.gd index 4f82113..0e9a118 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -9,6 +9,9 @@ extends CharacterBody2D @export_range(0, 1000, 10, "suffix:px/s") var speed: float = 500.0: set = _set_speed +## How fast does your character accelerate? +@export_range(0, 5000, 1000, "suffix:px/s²") var acceleration: float = 5000.0 + ## How high does your character jump? Note that the gravity will ## be influenced by the [member GameLogic.gravity]. @export_range(-1000, 1000, 10, "suffix:px/s") var jump_velocity = -880.0 @@ -106,9 +109,13 @@ func _physics_process(delta): # As good practice, you should replace UI actions with custom gameplay actions. var direction = Input.get_axis("ui_left", "ui_right") if direction: - velocity.x = direction * speed + velocity.x = move_toward( + velocity.x, + sign(direction) * speed, + abs(direction) * acceleration * delta, + ) else: - velocity.x = move_toward(velocity.x, 0, speed) + velocity.x = move_toward(velocity.x, 0, acceleration * delta) if velocity == Vector2.ZERO: _sprite.play("idle") From b14ffa852de0f4917a700134c094a83cb0988623 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Fri, 25 Oct 2024 14:07:47 +0100 Subject: [PATCH 5/6] Player: Add optional double jump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If enabled, the player can jump a second time while still in the air from a previous jump. A crude shower of particles is emitted to indicate ✨ magic ✨. --- components/player/player.tscn | 15 +++++++++++++++ scripts/player.gd | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/components/player/player.tscn b/components/player/player.tscn index 408eb4d..3228bdb 100644 --- a/components/player/player.tscn +++ b/components/player/player.tscn @@ -14,6 +14,21 @@ floor_constant_speed = true floor_snap_length = 32.0 script = ExtResource("1_w3ms2") +[node name="DoubleJumpParticles" type="CPUParticles2D" parent="."] +unique_name_in_owner = true +emitting = false +amount = 60 +lifetime = 0.2 +one_shot = true +explosiveness = 0.54 +randomness = 0.25 +emission_shape = 1 +emission_sphere_radius = 36.72 +particle_flag_align_y = true +gravity = Vector2(0, 1) +scale_amount_max = 5.0 +color = Color(1, 1, 0, 1) + [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] unique_name_in_owner = true position = Vector2(0, -64) diff --git a/scripts/player.gd b/scripts/player.gd index 0e9a118..046428a 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -31,18 +31,25 @@ extends CharacterBody2D ## a small positive number to allow the player a little margin for error. @export_range(0, 0.5, 1 / 60.0, "suffix:s") var jump_buffer: float = 5.0 / 60.0 +## Can your character jump a second time while still in the air? +@export var double_jump: bool = false + # If positive, the player is either on the ground, or left the ground less than this long ago var coyote_timer: float = 0 # If positive, the player pressed jump this long ago var jump_buffer_timer: float = 0 +# If true, the player is already jumping and can perform a double-jump +var double_jump_armed: bool = false + # Get the gravity from the project settings to be synced with RigidBody nodes. var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") var original_position: Vector2 @onready var _sprite: AnimatedSprite2D = %AnimatedSprite2D @onready var _initial_sprite_frames: SpriteFrames = %AnimatedSprite2D.sprite_frames +@onready var _double_jump_particles: CPUParticles2D = %DoubleJumpParticles func _set_sprite_frames(new_sprite_frames): @@ -87,14 +94,20 @@ func _physics_process(delta): # Handle jump if is_on_floor(): coyote_timer = (coyote_time + delta) + double_jump_armed = false if Input.is_action_just_pressed("ui_accept"): jump_buffer_timer = (jump_buffer + delta) - if jump_buffer_timer > 0 and coyote_timer > 0: + if jump_buffer_timer > 0 and (double_jump_armed or coyote_timer > 0): velocity.y = jump_velocity coyote_timer = 0 jump_buffer_timer = 0 + if double_jump_armed: + double_jump_armed = false + _double_jump_particles.emitting = true + elif double_jump: + double_jump_armed = true # Reduce velocity if the player lets go of the jump key before the apex. # This allows controlling the height of the jump. From 20f1ae45c26a81e39183e8f8d6ec6b05d83f0b62 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Sat, 26 Oct 2024 09:54:00 +0100 Subject: [PATCH 6/6] Treat stomping an enemy as a jump Previously, when the player stomped an enemy the enemy would set the player's vertical velocity directly. Instead, have the enemy call a new stomp() method on the player, which internally triggers a jump having disarmed the double-jump. This makes a difference in the following cases: - If the player has a jump buffered, stomping on the enemy now consumes that jump. - If the player is holding the jump key (because they buffered a jump) they can curtail the height of bouncing off the enemy. - If double jump is enabled, it is now armed by stomping an enemy. --- components/enemy/enemy.gd | 2 +- scripts/player.gd | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/components/enemy/enemy.gd b/components/enemy/enemy.gd index d7b047b..6358732 100644 --- a/components/enemy/enemy.gd +++ b/components/enemy/enemy.gd @@ -71,7 +71,7 @@ func _on_gravity_changed(new_gravity): func _on_hitbox_body_entered(body): if body.name == "Player": if squashable and body.velocity.y > 0 and body.position.y < position.y: - body.velocity.y = body.jump_velocity + body.stomp() queue_free() elif player_loses_life: Global.lives -= 1 diff --git a/scripts/player.gd b/scripts/player.gd index 046428a..0c665c7 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -86,6 +86,22 @@ func _on_gravity_changed(new_gravity): gravity = new_gravity +func _jump(): + velocity.y = jump_velocity + coyote_timer = 0 + jump_buffer_timer = 0 + if double_jump_armed: + double_jump_armed = false + _double_jump_particles.emitting = true + elif double_jump: + double_jump_armed = true + + +func stomp(): + double_jump_armed = false + _jump() + + func _physics_process(delta): # Don't move if there are no lives left. if Global.lives <= 0: @@ -100,14 +116,7 @@ func _physics_process(delta): jump_buffer_timer = (jump_buffer + delta) if jump_buffer_timer > 0 and (double_jump_armed or coyote_timer > 0): - velocity.y = jump_velocity - coyote_timer = 0 - jump_buffer_timer = 0 - if double_jump_armed: - double_jump_armed = false - _double_jump_particles.emitting = true - elif double_jump: - double_jump_armed = true + _jump() # Reduce velocity if the player lets go of the jump key before the apex. # This allows controlling the height of the jump.