src / player / player.tscn

Diagram

Diagram

Assets

200
200
200
200
200
200
200

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

$Camera
func _process(delta: float) -> void:
	if shaking_time:
		shaking_time = maxf(0.0, shaking_time - delta)
		shaking()

Scene Tree

  • Player CharacterBody2D

    • CanvasLayer CanvasLayer

    • 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

    • Camera res://src/player/camera/camera.tscn

    • AnimationPlayer AnimationPlayer

    • AnimationTree AnimationTree

Signal Connections

$.:[dashed]⇒$.
func _on_dashed() -> void:
	vibrate(&'Dashed')
	tween_sprite(&'dashed')
	%Camera.shake(&'dash')
$.:[failed]⇒$.
func _on_failed() -> void:
	playable = false
	state = State.FAILED
	velocity *= -1
	%Camera.shake(&'failure')
	%AnimationTree.active = false
	%AnimationPlayer.play(&'burst')
$.:[jumped]⇒$.
func _on_jumped() -> void:
	tween_sprite(&'jumped')
$.:[landed]⇒$.
func _on_landed() -> void:
	vibrate(&'Landed')
	tween_sprite(&'landed')
	var effect = LandedEffect.instantiate()
	effect.spawn(self)
	idle()
$.:[reached]⇒$.
func _on_reached(area: Area2D) -> void:
	restart_position = area.position
$AreaCollision:[area_entered]⇒$.
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)
$FrameSubject:[area_entered]⇒$.
func _on_frame_subject_area_entered(area: Area2D) -> void:
	%Camera.add_frame(area)
$FrameSubject:[area_exited]⇒$.
func _on_frame_subject_area_exited(area: Area2D) -> void:
	%Camera.remove_frame(area)

Animations

Animation_l271a

Diagram

burst

Diagram

climb_left

Diagram

climb_right

Diagram

fall_left

Diagram

fall_right

Diagram

idle_left

Diagram

idle_right

Diagram

jump_left

Diagram

jump_right

Diagram

run_left

Diagram

run_right

Diagram

Properties

Table 1. Root properties
Name Value

collision_layer

3

collision_mask

17

script

GhostEffect

LandedEffect

fatigue_curve

SubResource("Curve_ylhto")

Table 2. $CanvasLayer/OpeningCurtain properties
Name Value

unique_name_in_owner

true

pattern

2

Table 3. $CanvasLayer/RestartCurtain properties
Name Value

unique_name_in_owner

true

pattern

3

Table 4. $Sprites properties
Name Value

unique_name_in_owner

true

position

Vector2(-12, -72)

scale

Vector2(6, 6)

Table 5. $Sprites/SpriteStates properties
Name Value

unique_name_in_owner

true

Table 6. $Sprites/SpriteStates/IdleSprite2D properties
Name Value

material

SubResource("ShaderMaterial_jm5te")

texture

Player idle
Figure 1. res://assets/images/player/Player_idle.png

hframes

27

frame

25

Table 7. $Sprites/SpriteStates/RunSprite2D properties
Name Value

visible

false

material

SubResource("ShaderMaterial_c3wmo")

texture

Player run
Figure 2. res://assets/images/player/Player_run.png

hframes

8

Table 8. $Sprites/SpriteStates/JumpSprite2D properties
Name Value

visible

false

material

SubResource("ShaderMaterial_osywq")

texture

Player jump
Figure 3. res://assets/images/player/Player_jump.png

hframes

2

Table 9. $Sprites/SpriteStates/FallSprite2D properties
Name Value

visible

false

material

SubResource("ShaderMaterial_jpi3j")

texture

Player fall
Figure 4. res://assets/images/player/Player_fall.png

hframes

2

Table 10. $Sprites/SpriteStates/ClimbSprite2D properties
Name Value

visible

false

material

SubResource("ShaderMaterial_jpi3j")

scale

Vector2(0.8, 0.8)

texture

Player climb
Figure 5. res://assets/images/player/Player_climb.png

offset

Vector2(2, 4)

hframes

6

Table 11. $Sprites/SpriteEffects properties
Name Value

unique_name_in_owner

true

position

Vector2(2.1666667, 4)

Table 12. $Sprites/SpriteEffects/BurstSprite2D properties
Name Value

visible

false

position

Vector2(-1.6666666, -2.8333335)

texture

Burst
Figure 6. res://assets/images/player/effects/Burst.png

hframes

53

frame

11

Table 13. $Sprites/SpriteEffects/SweatSprite2D properties
Name Value

visible

false

position

Vector2(7.1666665, -8.166667)

sprite_frames

SubResource("SpriteFrames_jcdrv")

autoplay

default

frame_progress

0.8250497

Table 14. $CollisionShape2D properties
Name Value

position

Vector2(2, -51)

shape

SubResource("CapsuleShape2D_opnj4")

Table 15. $AreaCollision properties
Name Value

collision_layer

18

collision_mask

40

Table 16. $AreaCollision/CollisionShape2D properties
Name Value

visible

false

position

Vector2(0, -56)

shape

SubResource("CapsuleShape2D_l271a")

Table 17. $FrameSubject properties
Name Value

collision_layer

4

collision_mask

4

Table 18. $FrameSubject/CollisionShape2D properties
Name Value

visible

false

position

Vector2(3.5, -51.5)

shape

SubResource("RectangleShape2D_x42xx")

Table 19. $FrontRayCast properties
Name Value

unique_name_in_owner

true

position

Vector2(0, -50)

target_position

Vector2(-35, 0)

Table 20. $Camera properties
Name Value

unique_name_in_owner

true

position

Vector2(0, -46)

Table 21. $AnimationPlayer properties
Name Value

unique_name_in_owner

true

libraries

{ &"": SubResource("AnimationLibrary_u5nx7") }

Table 22. $AnimationTree properties
Name Value

unique_name_in_owner

true

root_node

NodePath("%AnimationTree/..")

tree_root

SubResource("AnimationNodeStateMachine_mwkb6")

anim_player

NodePath("../AnimationPlayer")

parameters/Climb/blend_position

-0.16339356

parameters/Fall/blend_position

-0.21995288

parameters/Idle/blend_position

-0.27003

parameters/Jump/blend_position

0.2168107

parameters/Run/blend_position

-0.7195601

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)