src / player / player.tscn
Diagram
Overridden virtual functions
_ready
func _ready() -> void:
playable = true
turn_right()
if not owner: return
if last_owner_name == owner.name:
position = restart_position
else:
restart_position = position
last_owner_name = owner.name
_physics_process
func _physics_process(delta: float) -> void:
var dx := Input.get_axis(&'move_left', &'move_right')
var dy := Input.get_axis(&'move_up', &'move_down')
var direction := Vector2(dx, dy)
var last_state := state
state = compute_state(delta, direction)
match state:
State.CLIMBING:
dx = 0
velocity.y = dy * CLIMBING_SPEED
State.DASHED:
velocity = direction.normalized() * DASH_SPEED
State.DASHING:
velocity *= DASH_ATTENUATION * delta
State.EXHAUSTED:
dx = 0
State.FAILED:
velocity.x = lerp(velocity.x, 0.0, 0.2)
velocity.y = lerp(velocity.y, 0.0, 0.2)
State.FALLING:
if last_state == State.CLIMBING \
and dy < 0:
velocity.y = JUMP_VELOCITY * 0.8
else:
var move_x := dx * SPEED * 0.8
if velocity.x * dx < 0: move_x *= 0.1
velocity.x = lerp(velocity.x, move_x, 0.2)
if velocity.y < JUMP_VELOCITY * 0.85:
velocity.y += GRAVITY * 1.15 * delta
else:
velocity.y += GRAVITY * delta
State.JUMPED:
velocity.y = JUMP_VELOCITY
State.KICKED:
velocity.y = JUMP_VELOCITY * 0.9
velocity.x = get_wall_normal().x * DASH_SPEED * 0.4
State.LOCKED:
dx = 0
velocity = Vector2(0, 0)
_: pass
if not playable: dx = 0
if not (state == State.DASHED \
or state == State.DASHING \
or state == State.FALLING \
or state == State.KICKED
):
if dx: velocity.x = dx * SPEED
else: velocity.x = move_toward(velocity.x, 0, SPEED*0.2)
if state == State.JUMPED:
velocity += jump_boost
jump_boost = Vector2(0, 0)
animate(playable)
move_and_slide()
_process
func _process(delta: float) -> void:
if shaking_time:
shaking_time = maxf(0.0, shaking_time - delta)
shaking()
Instantiators
Scene Tree
-
Player CharacterBody2D
-
CanvasLayer CanvasLayer
-
OpeningCurtain res://src/curtain/opening/opening.tscn
-
RestartCurtain res://src/curtain/opening/opening.tscn
-
-
Sprites Node2D
-
SpriteStates Node2D
-
IdleSprite2D Sprite2D
-
RunSprite2D Sprite2D
-
JumpSprite2D Sprite2D
-
FallSprite2D Sprite2D
-
ClimbSprite2D Sprite2D
-
-
SpriteEffects Node2D
-
BurstSprite2D Sprite2D
-
SweatSprite2D AnimatedSprite2D
-
-
-
CollisionShape2D CollisionShape2D
-
AreaCollision Area2D
-
CollisionShape2D CollisionShape2D
-
-
FrameSubject Area2D
-
CollisionShape2D CollisionShape2D
-
-
FrontRayCast RayCast2D
-
AnimationPlayer AnimationPlayer
-
AnimationTree AnimationTree
-
Signal Connections
func _on_dashed() -> void:
vibrate(&'Dashed')
tween_sprite(&'dashed')
%Camera.shake(&'dash')
func _on_failed() -> void:
playable = false
state = State.FAILED
velocity *= -1
%Camera.shake(&'failure')
%AnimationTree.active = false
%AnimationPlayer.play(&'burst')
func _on_jumped() -> void:
tween_sprite(&'jumped')
func _on_landed() -> void:
vibrate(&'Landed')
tween_sprite(&'landed')
var effect = LandedEffect.instantiate()
effect.spawn(self)
idle()
func _on_reached(area: Area2D) -> void:
restart_position = area.position
func _on_area_collision_area_entered(area: Area2D) -> void:
if area.get_collision_layer_value(4): # Failure
failed.emit()
elif area.get_collision_layer_value(6): # CheckPoint
reached.emit(area)
func _on_frame_subject_area_entered(area: Area2D) -> void:
%Camera.add_frame(area)
func _on_frame_subject_area_exited(area: Area2D) -> void:
%Camera.remove_frame(area)
Animations
Animation_l271a
burst
climb_left
climb_right
fall_left
fall_right
idle_left
idle_right
jump_left
jump_right
run_left
run_right
Properties
| Name | Value |
|---|---|
collision_layer |
|
collision_mask |
|
script |
|
GhostEffect |
|
LandedEffect |
|
fatigue_curve |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
pattern |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
pattern |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
position |
|
scale |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
| Name | Value |
|---|---|
material |
|
texture |
|
hframes |
|
frame |
|
| Name | Value |
|---|---|
visible |
|
material |
|
texture |
|
hframes |
|
| Name | Value |
|---|---|
visible |
|
material |
|
texture |
|
hframes |
|
| Name | Value |
|---|---|
visible |
|
material |
|
texture |
|
hframes |
|
| Name | Value |
|---|---|
visible |
|
material |
|
scale |
|
texture |
|
offset |
|
hframes |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
position |
|
| Name | Value |
|---|---|
visible |
|
position |
|
texture |
|
hframes |
|
frame |
|
| Name | Value |
|---|---|
visible |
|
position |
|
sprite_frames |
|
autoplay |
|
frame_progress |
|
| Name | Value |
|---|---|
position |
|
shape |
|
| Name | Value |
|---|---|
collision_layer |
|
collision_mask |
|
| Name | Value |
|---|---|
visible |
|
position |
|
shape |
|
| Name | Value |
|---|---|
collision_layer |
|
collision_mask |
|
| Name | Value |
|---|---|
visible |
|
position |
|
shape |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
position |
|
target_position |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
position |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
libraries |
|
| Name | Value |
|---|---|
unique_name_in_owner |
|
root_node |
|
tree_root |
|
anim_player |
|
parameters/Climb/blend_position |
|
parameters/Fall/blend_position |
|
parameters/Idle/blend_position |
|
parameters/Jump/blend_position |
|
parameters/Run/blend_position |
|
player.gd
extends CharacterBody2D
class_name Player
@export var GhostEffect : PackedScene
@export var LandedEffect : PackedScene
@export var fatigue_curve: Curve
signal dashed
signal jumped
signal landed
signal failed
signal reached(area: Area2D)
enum State {
CLIMBING,
DASHED,
DASHING,
EXHAUSTED,
FAILED,
FALLING,
GROUNDING,
JUMPED,
KICKED,
LOCKED,
}
const SPEED = 700.0
const DASH_SPEED = SPEED * 4
const CLIMBING_SPEED = SPEED / 6
const JUMP_VELOCITY = -1200.0
const GRAVITY = 4000
const ALLOWED_TIME_TO_JUMP = 0.13
const DASH_TIME = 0.1
const DASH_ATTENUATION = Vector2(55, 50)
const MAX_DASH_COUNT = 1
const MAX_CLIMBING_TIME = 7.0
const DEFAULT_SCALE = Vector2(6.0, 6.0)
var state : State = State.GROUNDING
var falling_time := 0.0
var dash_rest_time := 0.0
var dash_rest_count := MAX_DASH_COUNT
var climb_rest_time := 0.0
var sprite_tween : Tween
var restart_position : Vector2
var jump_boost : Vector2
static var last_owner_name : String
static var playable : bool = false
func _ready() -> void:
playable = true
turn_right()
if not owner: return
if last_owner_name == owner.name:
position = restart_position
else:
restart_position = position
last_owner_name = owner.name
func _physics_process(delta: float) -> void:
var dx := Input.get_axis(&'move_left', &'move_right')
var dy := Input.get_axis(&'move_up', &'move_down')
var direction := Vector2(dx, dy)
var last_state := state
state = compute_state(delta, direction)
match state:
State.CLIMBING:
dx = 0
velocity.y = dy * CLIMBING_SPEED
State.DASHED:
velocity = direction.normalized() * DASH_SPEED
State.DASHING:
velocity *= DASH_ATTENUATION * delta
State.EXHAUSTED:
dx = 0
State.FAILED:
velocity.x = lerp(velocity.x, 0.0, 0.2)
velocity.y = lerp(velocity.y, 0.0, 0.2)
State.FALLING:
if last_state == State.CLIMBING \
and dy < 0:
velocity.y = JUMP_VELOCITY * 0.8
else:
var move_x := dx * SPEED * 0.8
if velocity.x * dx < 0: move_x *= 0.1
velocity.x = lerp(velocity.x, move_x, 0.2)
if velocity.y < JUMP_VELOCITY * 0.85:
velocity.y += GRAVITY * 1.15 * delta
else:
velocity.y += GRAVITY * delta
State.JUMPED:
velocity.y = JUMP_VELOCITY
State.KICKED:
velocity.y = JUMP_VELOCITY * 0.9
velocity.x = get_wall_normal().x * DASH_SPEED * 0.4
State.LOCKED:
dx = 0
velocity = Vector2(0, 0)
_: pass
if not playable: dx = 0
if not (state == State.DASHED \
or state == State.DASHING \
or state == State.FALLING \
or state == State.KICKED
):
if dx: velocity.x = dx * SPEED
else: velocity.x = move_toward(velocity.x, 0, SPEED*0.2)
if state == State.JUMPED:
velocity += jump_boost
jump_boost = Vector2(0, 0)
animate(playable)
move_and_slide()
func compute_state(delta: float, direction: Vector2) -> State:
if state == State.FAILED: return State.FAILED
if state == State.LOCKED: return State.LOCKED
if is_just_input_action(&'move_dash') \
and dash_rest_count > 0 \
and direction:
compute_dashed()
return State.DASHED
elif dash_rest_time > 0:
dash_rest_time -= delta
return State.DASHING
else:
if not is_on_floor() and not state == State.CLIMBING:
falling_time += delta
else:
if state == State.FALLING: landed.emit()
falling_time = 0
if not state == State.CLIMBING:
dash_rest_count = MAX_DASH_COUNT
if direction.y > 0: may_drop_collisions()
var on_wall = %FrontRayCast.is_colliding()
if is_just_input_action(&'move_jump') \
and falling_time < ALLOWED_TIME_TO_JUMP:
jumped.emit()
return State.JUMPED
elif is_just_input_action(&'move_jump') \
and on_wall:
jumped.emit()
return State.KICKED
elif on_wall and is_input_action(&'move_climb') \
and (velocity.y >= 0 or state == State.CLIMBING) \
and not (state == State.FALLING and falling_time < 0.2):
falling_time = 0
climb_rest_time -= delta
if climb_rest_time > 0:
return State.CLIMBING
else:
return State.EXHAUSTED
if not is_on_floor():
return State.FALLING
else:
climb_rest_time = MAX_CLIMBING_TIME
return State.GROUNDING
func may_drop_collisions() -> void:
for i in get_slide_collision_count():
var collider = get_slide_collision(i).get_collider()
if collider.is_in_group('droppable'):
collider.drop()
func is_just_input_action(key: StringName) -> bool:
return playable and state != State.EXHAUSTED \
and Input.is_action_just_pressed(key)
func is_input_action(key: StringName) -> bool:
return playable and state != State.EXHAUSTED \
and Input.is_action_pressed(key)
func animate(force: bool = true) -> void:
if state == State.LOCKED: return
if force: animate_with(velocity)
update_sprite_states_shader_parameters()
if dash_rest_time > 0 && int(dash_rest_time*100)%3 > 0:
spawn_ghost_effect()
func animate_with(_velocity: Vector2) -> void:
if _velocity.x:
%FrontRayCast.target_position.x = sign(_velocity.x) \
* abs(%FrontRayCast.target_position.x)
for st in ['Idle', 'Run', 'Jump', 'Fall', 'Climb']:
%AnimationTree.set('parameters/%s/blend_position' % [st], _velocity.x)
%AnimationTree.get('parameters/playback').travel(get_state_by(_velocity))
func compute_dashed() -> void:
dashed.emit()
dash_rest_time = DASH_TIME
dash_rest_count -= 1
func idle() -> void:
%AnimationPlayer.play(&'RESET')
animate_with(Vector2(0, 0))
func walk_right() -> void:
animate_with(Vector2(1, 0))
func walk_left() -> void:
animate_with(Vector2(-1, 0))
func turn_right() -> void:
walk_right(); idle()
func turn_left() -> void:
walk_left(); idle()
func restart() -> void:
%RestartCurtain.play()
await get_tree().create_timer(1).timeout
position = restart_position
playable = true
jump_boost = Vector2(0, 0)
unlock()
idle.call_deferred()
func dash(direction: Vector2) -> void:
compute_dashed()
state = State.DASHED
velocity = direction.normalized() * DASH_SPEED
func lock() -> void:
state = State.LOCKED
%AnimationTree.active = false
func unlock() -> void:
state = State.EXHAUSTED
%AnimationTree.active = true
func vibrate(key: String):
var values = [0, 0, 0]
match key:
&'Dashed': values = [0.4, 0.6, 0.2]
&'Landed': values = [0.1, 0.3, 0.1]
Input.start_joy_vibration(0, values[0], values[1], values[2])
func get_state_by(_velocity) -> String:
if state == State.CLIMBING: return &'Climb'
if _velocity:
if _velocity.y < 0: return &'Jump'
if _velocity.y > 0: return &'Fall'
else: return &'Run'
else: return &'Idle'
func tween_sprite(key: StringName) -> void:
if sprite_tween: sprite_tween.kill()
sprite_tween = %Sprites.create_tween()
var coef = Vector2(1, 1)
match key:
&'dashed':
var dir := velocity.normalized()
coef = dir.abs().clamp(Vector2(0.75, 0.75), Vector2(1.0, 1.0))
&'landed': coef = Vector2(1.08, 0.92)
&'jumped': coef = Vector2(0.65, 1.45)
sprite_tween.tween_property(%Sprites, 'scale', coef * DEFAULT_SCALE, 0.10) \
.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
sprite_tween.tween_property(%Sprites, 'scale', DEFAULT_SCALE, 0.16) \
.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE)
func update_sprite_states_shader_parameters() -> void:
for sprite in %SpriteStates.get_children():
sprite.material.set_shader_parameter(&'dashed', is_dashed())
var fatigue = 0.0
if state == State.CLIMBING:
fatigue = fatigue_curve.sample(climb_rest_time / MAX_CLIMBING_TIME)
sprite.material.set_shader_parameter(&'fatigue', fatigue)
func spawn_ghost_effect() -> void:
var ghost = GhostEffect.instantiate()
ghost.spawn(self)
func current_sprite_state() -> Sprite2D:
return %SpriteStates.get_children().filter(
func(c): return c.visible)[0]
func is_dashed() -> bool:
return true if dash_rest_count < MAX_DASH_COUNT else false
func _on_dashed() -> void:
vibrate(&'Dashed')
tween_sprite(&'dashed')
%Camera.shake(&'dash')
func _on_jumped() -> void:
tween_sprite(&'jumped')
func _on_landed() -> void:
vibrate(&'Landed')
tween_sprite(&'landed')
var effect = LandedEffect.instantiate()
effect.spawn(self)
idle()
func _on_failed() -> void:
playable = false
state = State.FAILED
velocity *= -1
%Camera.shake(&'failure')
%AnimationTree.active = false
%AnimationPlayer.play(&'burst')
func _on_reached(area: Area2D) -> void:
restart_position = area.position
func _on_frame_subject_area_entered(area: Area2D) -> void:
%Camera.add_frame(area)
func _on_frame_subject_area_exited(area: Area2D) -> void:
%Camera.remove_frame(area)
func _on_area_collision_area_entered(area: Area2D) -> void:
if area.get_collision_layer_value(4): # Failure
failed.emit()
elif area.get_collision_layer_value(6): # CheckPoint
reached.emit(area)






