make player use move_and_collide instead of move_and_slide and reimplement most of collision handling

This commit is contained in:
2025-08-22 21:34:46 +10:00
parent 951445fefa
commit 9fc68675fb
4 changed files with 157 additions and 115 deletions

View File

@@ -10,12 +10,13 @@
[sub_resource type="RectangleShape2D" id="RectangleShape2D_3vyb7"]
size = Vector2(18, 26)
[node name="Player" type="CharacterBody2D" node_paths=PackedStringArray("_trajectory")]
[node name="Player" type="CharacterBody2D" node_paths=PackedStringArray("_trajectory", "_shape")]
collision_layer = 16
floor_max_angle = 1.55334
floor_snap_length = 0.0
script = ExtResource("1_g2els")
_trajectory = NodePath("Trajectory")
_shape = NodePath("CollisionShape2D")
[node name="JumpParticlesHolder" type="Node2D" parent="."]

View File

@@ -2,12 +2,13 @@ class_name Player
extends CharacterBody2D
signal collided(side: Side, force: Vector2)
signal jumped(strength: float, velocity: Vector2)
signal jumped(strength: float, _velocity: Vector2)
static var instance: Player
@export_group("References")
@export var _trajectory: Trajectory
@export var _shape: CollisionShape2D
@export_group("Metrics")
@export var _move_speed: float = 1.5 * 60.0
@@ -18,12 +19,17 @@ static var instance: Player
@export var _jump_full_charge_frames: float = 35
@export var _wall_bounce_velocity_loss: float = 0.5
var _velocity: Vector2 = Vector2.ZERO
var _direction: float = 0
var _is_charging_jump: bool = false
var _charge_strength: float = 0
var _jump_released: bool = false
var _floor_angle: float = 0
var _is_on_floor: bool = false
var _is_on_wall: bool = false
var _is_on_ceiling: bool = false
var _freemove_enabled: bool = false
@onready var _saved_state: Vector2 = global_position
@@ -47,12 +53,12 @@ static func process_movement(
# falling
new_velocity.y = (
0.0
if on_floor and is_zero_approx(floor_angle)
if on_floor and is_zero_approx(floor_angle) and new_velocity.y >= 0.0
else move_toward(new_velocity.y, fall_speed, delta * fall_acceleration)
)
# moving
if on_floor and is_zero_approx(floor_angle):
if on_floor and is_zero_approx(floor_angle) and new_velocity.y >= 0.0:
new_velocity.x = direction * move_speed if not is_charging_jump else 0.0
# sliding
@@ -68,7 +74,6 @@ static func process_movement(
static func process_collision(
prev_velocity: Vector2,
new_velocity: Vector2,
hit_floor: bool,
hit_slope: bool,
@@ -77,18 +82,63 @@ static func process_collision(
floor_angle: float,
wall_bounce_velocity_loss: float,
) -> Vector2:
if hit_floor:
if hit_floor and not hit_slope and not hit_wall and not hit_ceiling:
new_velocity = Vector2.ZERO
if hit_slope:
new_velocity.x = new_velocity.y * signf(floor_angle)
if hit_ceiling:
new_velocity.y = 0
new_velocity.x *= wall_bounce_velocity_loss
if hit_wall:
new_velocity.x = -prev_velocity.x * wall_bounce_velocity_loss
new_velocity.x = -new_velocity.x
if hit_ceiling or hit_wall:
new_velocity.x *= wall_bounce_velocity_loss
return new_velocity
static func shapecast(
pos: Vector2,
direction: Vector2,
shape: Shape2D,
space_state: PhysicsDirectSpaceState2D
) -> bool:
var floor_query := PhysicsShapeQueryParameters2D.new()
floor_query.collision_mask = 1
floor_query.transform = Transform2D(0, pos)
floor_query.motion = direction
floor_query.shape = shape
floor_query.margin = -0.08
return space_state.intersect_shape(floor_query).size() > 0
static func raycast_down(
pos: Vector2, shape: RectangleShape2D, space_state: PhysicsDirectSpaceState2D
) -> Dictionary:
var half_width := shape.size.x / 2.0
var from := pos + Vector2.UP * (shape.size.x + 1)
var to := pos + Vector2.DOWN * (shape.size.x + 1)
var query_center := PhysicsRayQueryParameters2D.create(from, to)
var result_center := space_state.intersect_ray(query_center)
if result_center:
return result_center
var query_left := PhysicsRayQueryParameters2D.create(
from + Vector2.LEFT * half_width, to + Vector2.LEFT * half_width
)
var result_left := space_state.intersect_ray(query_left)
if result_left:
return result_left
var query_right := PhysicsRayQueryParameters2D.create(
from + Vector2.RIGHT * half_width, to + Vector2.RIGHT * half_width
)
var result_right := space_state.intersect_ray(query_right)
if result_right:
return result_right
return {}
func _ready() -> void:
instance = self
Debugger.add_event("collided")
@@ -102,28 +152,23 @@ func _ready() -> void:
func _physics_process(delta: float) -> void:
if _freemove_enabled:
velocity = (
(
Input
. get_vector("move_left", "move_right", "move_up", "move_down")
. normalized()
)
* _jump_speed
* (4.0 if Input.is_action_pressed("jump") else 1.0)
var direction := (
Input
. get_vector("move_left", "move_right", "move_up", "move_down")
. normalized()
)
_velocity = (
direction * _jump_speed * (4.0 if Input.is_action_pressed("jump") else 1.0)
)
move_and_slide()
return
var is_on_floor_prev := is_on_floor_only()
var is_on_ceiling_prev := is_on_ceiling()
var is_on_wall_prev := is_on_wall()
_gather_input(delta)
_gather_input(delta, is_on_floor_prev)
var new_velocity := process_movement(
velocity,
_velocity = process_movement(
_velocity,
delta,
is_on_floor_prev,
_is_on_floor,
_floor_angle,
_direction,
_is_charging_jump,
@@ -136,59 +181,25 @@ func _physics_process(delta: float) -> void:
_fall_acceleration,
)
if _jump_released:
jumped.emit(_charge_strength, new_velocity)
jumped.emit(_charge_strength, _velocity)
# apply velocity
velocity = new_velocity
move_and_slide()
move_and_collide(_velocity * delta)
# collisions
if is_on_floor() and not is_zero_approx(_floor_angle):
velocity = new_velocity
var is_on_floor_changed := not is_on_floor_prev and is_on_floor()
if is_on_floor_changed and is_on_floor():
_floor_angle = -get_floor_normal().angle_to(Vector2.UP)
var hit_floor := is_on_floor_changed and is_zero_approx(_floor_angle)
var hit_slope := is_on_floor_changed and not is_zero_approx(_floor_angle)
var hit_ceiling := not is_on_ceiling_prev and is_on_ceiling()
var hit_wall := (
not is_on_wall_prev
and is_on_wall()
and not is_on_floor()
and not is_zero_approx(new_velocity.x)
)
velocity = process_collision(
new_velocity,
velocity,
hit_floor,
hit_slope,
hit_ceiling,
hit_wall,
_floor_angle,
_wall_bounce_velocity_loss,
)
if hit_floor:
_trajectory.visible = false
collided.emit(SIDE_BOTTOM, new_velocity)
if hit_ceiling:
collided.emit(SIDE_TOP, new_velocity)
if hit_wall:
collided.emit(SIDE_LEFT if velocity.x > 0 else SIDE_RIGHT, new_velocity)
_handle_collision()
queue_redraw()
Debugger.text("position", global_position)
Debugger.text("velocity", new_velocity)
Debugger.text("velocity_modified", velocity)
Debugger.text("velocity", _velocity)
Debugger.text("floor_angle", _floor_angle)
Debugger.text("direction", _direction)
Debugger.text("is_charging_jump", _is_charging_jump)
Debugger.text("charge_strength", _charge_strength)
Debugger.text("is_on_ceiling", is_on_ceiling())
Debugger.text("is_on_ceiling_only", is_on_ceiling_only())
Debugger.text("is_on_floor", is_on_floor())
Debugger.text("is_on_floor_only", is_on_floor_only())
Debugger.text("is_on_wall", is_on_wall())
Debugger.text("is_on_wall_only", is_on_wall_only())
Debugger.text("floor_angle", _floor_angle)
Debugger.text("is_on_floor", _is_on_floor, 2)
Debugger.text("is_on_wall", _is_on_wall, 2)
Debugger.text("is_on_ceiling", _is_on_ceiling, 2)
func _unhandled_input(event: InputEvent) -> void:
@@ -198,18 +209,18 @@ func _unhandled_input(event: InputEvent) -> void:
_saved_state = global_position
if event.is_action_pressed("load_state"):
global_position = _saved_state
velocity = Vector2.ZERO
_velocity = Vector2.ZERO
if event.is_action_pressed("toggle_freemove"):
_freemove_enabled = not _freemove_enabled
func _gather_input(delta: float, on_floor: bool) -> void:
func _gather_input(delta: float) -> void:
_direction = signf(Input.get_axis("move_left", "move_right"))
# jump charge start
if (
Input.is_action_just_pressed("jump")
and on_floor
and _is_on_floor
and is_zero_approx(_floor_angle)
):
_is_charging_jump = true
@@ -226,9 +237,67 @@ func _gather_input(delta: float, on_floor: bool) -> void:
# jump charge release
_jump_released = (
_is_charging_jump
and on_floor
and _is_on_floor
and is_zero_approx(_floor_angle)
and (Input.is_action_just_released("jump") or _charge_strength >= 1.0)
)
if _jump_released:
_is_charging_jump = false
_is_on_wall = false
func _handle_collision() -> void:
var is_on_floor_prev := _is_on_floor
var is_on_ceiling_prev := _is_on_ceiling
var is_on_wall_prev := _is_on_wall
var space_state := get_world_2d().direct_space_state
_is_on_floor = shapecast(
_shape.global_position, Vector2.DOWN, _shape.shape, space_state
)
_is_on_wall = (
shapecast(_shape.global_position, Vector2.LEFT, _shape.shape, space_state)
or shapecast(_shape.global_position, Vector2.RIGHT, _shape.shape, space_state)
)
_is_on_ceiling = shapecast(
_shape.global_position, Vector2.UP, _shape.shape, space_state
)
var is_on_floor_changed := not is_on_floor_prev and _is_on_floor
if is_on_floor_changed and _is_on_floor:
var raycast_result := raycast_down(
global_position, _shape.shape as RectangleShape2D, space_state
)
if raycast_result:
var normal := raycast_result["normal"] as Vector2
_floor_angle = -normal.angle_to(Vector2.UP)
else:
_floor_angle = 0
var hit_floor := is_on_floor_changed and is_zero_approx(_floor_angle)
var hit_slope := is_on_floor_changed and not is_zero_approx(_floor_angle)
var hit_ceiling := not is_on_ceiling_prev and _is_on_ceiling
var hit_wall := (
not is_on_wall_prev
and _is_on_wall
and (not _is_on_floor or _jump_released)
and not is_zero_approx(_velocity.x)
)
_velocity = process_collision(
_velocity,
hit_floor,
hit_slope,
hit_ceiling,
hit_wall,
_floor_angle,
_wall_bounce_velocity_loss,
)
if hit_floor:
_trajectory.visible = false
collided.emit(SIDE_BOTTOM, _velocity)
if hit_ceiling:
collided.emit(SIDE_TOP, _velocity)
if hit_wall:
collided.emit(SIDE_LEFT if _velocity.x > 0 else SIDE_RIGHT, _velocity)

View File

@@ -54,7 +54,7 @@ func _draw() -> void:
func _on_player_collided(side: Side, force: Vector2) -> void:
Debugger.text("collision force.length()", force.length())
Debugger.text("collision force.length()", force.length(), 2)
if force.length() < _collision_particles_min_force and side == SIDE_BOTTOM:
return
var particles_transform := _particles_transform_bottom
@@ -72,7 +72,7 @@ func _on_player_collided(side: Side, force: Vector2) -> void:
func _on_player_jumped(strength: float, force: Vector2) -> void:
Debugger.text("jump force.length()", force.length())
Debugger.text("jump force.length()", force.length(), 2)
if force.length() < _jump_particles_min_force:
return
var particles := _jump_particles.instantiate() as GPUParticles2D

View File

@@ -198,6 +198,7 @@ func _draw() -> void:
var is_on_floor: bool = false
var direction: float = -1.0 if flip else 1.0
var floor_angle: float = 0
var space_state := get_world_2d().direct_space_state
for i in range(_steps):
pos += velocity * delta
@@ -224,7 +225,7 @@ func _draw() -> void:
)
if _do_collisions:
var motion_proportion := _cast_motion(pos_prev, pos)
var motion_proportion := _cast_motion(pos_prev, pos, space_state)
if not is_equal_approx(motion_proportion, 1.0):
var motion := (pos - pos_prev) * motion_proportion
pos = pos_prev + motion
@@ -232,12 +233,15 @@ func _draw() -> void:
var hit_floor: bool = false
var hit_ceiling: bool = false
var shape_pos := to_global(pos) + Vector2.UP * (_player_size.y / 2.0)
# hitting floor
if velocity.y >= 0:
var result := _shapecast(pos + Vector2.UP, pos + Vector2.DOWN)
var result := Player.shapecast(
shape_pos, Vector2.DOWN, _player_shape, space_state
)
if result:
var raycast_result := _raycast(
pos + Vector2.UP * 10, pos + Vector2.DOWN * 10
var raycast_result := Player.raycast_down(
to_global(pos), _player_shape, space_state
)
if raycast_result:
var normal := raycast_result["normal"] as Vector2
@@ -248,9 +252,8 @@ func _draw() -> void:
is_on_floor = false
# hitting ceiling
if velocity.y < 0:
var result := _shapecast(
pos - Vector2(0, _player_size.y) + Vector2.DOWN,
pos - Vector2(0, _player_size.y) + Vector2.UP
var result := Player.shapecast(
shape_pos, Vector2.UP, _player_shape, space_state
)
if result:
hit_ceiling = true
@@ -260,7 +263,6 @@ func _draw() -> void:
velocity = (
Player
. process_collision(
velocity,
velocity,
hit_floor,
hit_slope,
@@ -299,39 +301,9 @@ func _draw() -> void:
draw_polyline(points, _line_color)
func _shapecast(from: Vector2, to: Vector2) -> Array[Dictionary]:
var space_state := get_world_2d().direct_space_state
var query := PhysicsShapeQueryParameters2D.new()
query.collision_mask = 1
var origin := to_global(from)
origin.y -= _player_size.y / 2.0
query.transform = Transform2D(0, origin)
query.motion = to - from
query.shape = _player_shape
return space_state.intersect_shape(query)
func _raycast(from: Vector2, to: Vector2) -> Dictionary:
var space_state := get_world_2d().direct_space_state
var query_left := PhysicsRayQueryParameters2D.create(
to_global(from) + Vector2.LEFT * _player_size.x / 2,
to_global(to) + Vector2.LEFT * _player_size.x / 2
)
var result_left := space_state.intersect_ray(query_left)
if result_left:
return result_left
var query_right := PhysicsRayQueryParameters2D.create(
to_global(from) + Vector2.RIGHT * _player_size.x / 2,
to_global(to) + Vector2.RIGHT * _player_size.x / 2
)
var result_right := space_state.intersect_ray(query_right)
if result_right:
return result_right
return {}
func _cast_motion(from: Vector2, to: Vector2) -> float:
var space_state := get_world_2d().direct_space_state
func _cast_motion(
from: Vector2, to: Vector2, space_state: PhysicsDirectSpaceState2D
) -> float:
var query := PhysicsShapeQueryParameters2D.new()
query.collision_mask = 1
var origin := to_global(from)