Added loading screen

This commit is contained in:
Rob Kelly 2025-04-21 18:07:11 -06:00
parent dcd5d5d57d
commit cfb7b28971
12 changed files with 371 additions and 24 deletions

View File

@ -9,6 +9,7 @@ UI_OFF,Off
UI_BACK,"⏎ Back" UI_BACK,"⏎ Back"
UI_LOCKED,Locked UI_LOCKED,Locked
UI_QUIT,Quit UI_QUIT,Quit
UI_LOADING,Loading
, ,
PAUSE_HEADING,Paused PAUSE_HEADING,Paused
PAUSE_RESUME,Resume PAUSE_RESUME,Resume

1 keys en
9 UI_BACK ⏎ Back
10 UI_LOCKED Locked
11 UI_QUIT Quit
12 UI_LOADING Loading
13
14 PAUSE_HEADING Paused
15 PAUSE_RESUME Resume

View File

@ -16,7 +16,7 @@ warnings/threads/thread_model=2
[application] [application]
config/name="Grunk" config/name="Grunk"
run/main_scene="uid://884jqafhtrv0" run/main_scene="uid://qpq2cm1hgeha"
config/project_settings_override="user://settings.godot" config/project_settings_override="user://settings.godot"
config/features=PackedStringArray("4.4", "Forward Plus") config/features=PackedStringArray("4.4", "Forward Plus")
run/max_fps=60 run/max_fps=60

View File

@ -1,8 +1,12 @@
class_name Game extends Node class_name Game extends Node
## Interface to the game as an application. ## Interface to the game as an application.
@export_category("Game Scenes") @export_file("*.tscn") var start_scene: String
@export var world_scene: PackedScene
var _loading_resources: Dictionary[String, Promise] = {}
@onready var content: Node = %Content
@onready var loading_screen: Control = %LoadingScreen
## Handy typed singleton access. ## Handy typed singleton access.
static var settings: GameSettingsType: static var settings: GameSettingsType:
@ -16,5 +20,91 @@ static var runtime: GameRuntimeType:
static var instance: Game static var instance: Game
class Promise:
var _callbacks: Array[Callable] = []
var _end_callbacks: Array[Callable] = []
func then(fn: Callable) -> Promise:
_callbacks.push_back(fn)
return self
func finally(fn: Callable) -> Promise:
_end_callbacks.push_back(fn)
return self
func resolve(res: Variant) -> void:
for fn: Callable in _callbacks + _end_callbacks:
fn.call(res)
class ScenePromise:
extends Promise
func resolve(res: Variant) -> void:
@warning_ignore("unsafe_cast")
var instance: Node = (res as PackedScene).instantiate()
super.resolve(instance)
func _ready() -> void: func _ready() -> void:
Game.instance = self Game.instance = self
_initial_load.call_deferred()
func _initial_load() -> void:
queue_scene(start_scene)
## Unload the running scene & queue up a new scene to be loaded in the background.
##
## The loading screen will be shown until the scene is loaded.
func queue_scene(path: String) -> Promise:
return queue_load(path, ScenePromise.new(), "PackedScene").finally(_finish_scene_load)
## Queue a resource to be loaded in the background.
##
## Returns a `Promise` which can be used to attach callbacks
## which will be called with the resource after it is loaded.
func queue_load(path: String, promise: Promise = null, type_hint: String = "") -> Promise:
if not promise:
promise = Promise.new()
_loading_resources[path] = promise
ResourceLoader.load_threaded_request(path, type_hint)
return promise
func _unload_content() -> void:
for child: Node in content.get_children():
child.queue_free()
func _finish_scene_load(scene_instance: Node) -> void:
# Unpause in case the previous scene was paused.
get_tree().paused = false
# Reset time scale in case it's been changed.
Engine.time_scale = 1.0
content.add_child(scene_instance)
scene_instance.reparent(content)
func _process(_delta: float) -> void:
if _loading_resources:
loading_screen.visible = true
for key: String in _loading_resources.keys():
match ResourceLoader.load_threaded_get_status(key):
ResourceLoader.THREAD_LOAD_LOADED:
_loading_resources[key].resolve(ResourceLoader.load_threaded_get(key))
_loading_resources.erase(key)
ResourceLoader.THREAD_LOAD_FAILED:
assert(false, "Failed loading resource: " + key)
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
assert(false, "Can't load invalid resource: " + key)
_:
# Continue loading
pass
if not _loading_resources:
loading_screen.visible = false

View File

@ -1,8 +1,236 @@
[gd_scene load_steps=3 format=3 uid="uid://qpq2cm1hgeha"] [gd_scene load_steps=14 format=3 uid="uid://qpq2cm1hgeha"]
[ext_resource type="Script" uid="uid://dxl25lkyped4" path="res://src/game/game.gd" id="1_qnjlk"] [ext_resource type="Script" uid="uid://dxl25lkyped4" path="res://src/game/game.gd" id="1_qnjlk"]
[ext_resource type="PackedScene" uid="uid://884jqafhtrv0" path="res://src/world/world.tscn" id="2_s6lek"] [ext_resource type="FontFile" uid="uid://oq8ue2qrfijg" path="res://assets/fonts/Silkscreen/Silkscreen-Regular.ttf" id="2_s6lek"]
[ext_resource type="Script" uid="uid://ctf1if4ly6nun" path="res://src/game/loading_screen.gd" id="3_kgj8g"]
[sub_resource type="Theme" id="Theme_s6lek"]
Label/colors/font_color = Color(0.137255, 0.984314, 0.34902, 1)
Label/constants/outline_size = 16
Label/font_sizes/font_size = 32
Label/fonts/font = ExtResource("2_s6lek")
[sub_resource type="Animation" id="Animation_s6lek"]
resource_name = "initial_display"
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("MarginContainer/HBoxContainer/Loading:visible_ratio")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.8, 1),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [0.0, 0.0, 1.0]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [0]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [false, true, false, true, false, true, false, true, false]
}
[sub_resource type="Animation" id="Animation_kgj8g"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("MarginContainer/HBoxContainer/Loading:visible_ratio")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [0.0]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [0]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [false]
}
[sub_resource type="Animation" id="Animation_l80un"]
resource_name = "ellipsis_loop"
length = 1.2
loop_mode = 1
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("MarginContainer/HBoxContainer/Loading:visible_ratio")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [1.0]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [false]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 0.3, 0.6, 0.9),
"transitions": PackedFloat32Array(1, 1, 1, 1),
"update": 1,
"values": [0, 1, 2, 3]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_kgj8g"]
_data = {
&"RESET": SubResource("Animation_kgj8g"),
&"ellipsis_loop": SubResource("Animation_l80un"),
&"initial_display": SubResource("Animation_s6lek")
}
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_s6lek"]
animation = &"ellipsis_loop"
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_kgj8g"]
animation = &"initial_display"
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_l80un"]
advance_mode = 2
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_xptat"]
switch_mode = 2
advance_mode = 2
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_l80un"]
states/ellipsis_loop/node = SubResource("AnimationNodeAnimation_s6lek")
states/ellipsis_loop/position = Vector2(630, 100)
states/initial_display/node = SubResource("AnimationNodeAnimation_kgj8g")
states/initial_display/position = Vector2(399, 100)
transitions = ["Start", "initial_display", SubResource("AnimationNodeStateMachineTransition_l80un"), "initial_display", "ellipsis_loop", SubResource("AnimationNodeStateMachineTransition_xptat")]
[node name="Game" type="Node"] [node name="Game" type="Node"]
script = ExtResource("1_qnjlk") script = ExtResource("1_qnjlk")
world_scene = ExtResource("2_s6lek") start_scene = "uid://884jqafhtrv0"
[node name="Content" type="Node" parent="."]
unique_name_in_owner = true
[node name="LoadingScreen" type="Control" parent="."]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_s6lek")
script = ExtResource("3_kgj8g")
[node name="ColorRect" type="ColorRect" parent="LoadingScreen"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.0196078, 0.0431373, 0.0627451, 1)
[node name="MarginContainer" type="MarginContainer" parent="LoadingScreen"]
layout_mode = 1
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_top = -40.0
offset_right = 40.0
grow_vertical = 0
theme_override_constants/margin_left = 32
theme_override_constants/margin_bottom = 32
[node name="HBoxContainer" type="HBoxContainer" parent="LoadingScreen/MarginContainer"]
layout_mode = 2
[node name="Prompt" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"]
layout_mode = 2
text = ">"
[node name="Blinker" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"]
visible = false
layout_mode = 2
text = "_"
[node name="Loading" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"]
layout_mode = 2
text = "UI_LOADING"
visible_characters = 0
visible_characters_behavior = 1
visible_ratio = 0.0
[node name="Ellipsis" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"]
layout_mode = 2
text = "..."
visible_characters = 0
visible_characters_behavior = 1
visible_ratio = 0.0
[node name="LoaderAnimation" type="AnimationPlayer" parent="LoadingScreen"]
libraries = {
&"": SubResource("AnimationLibrary_kgj8g")
}
[node name="LoaderTree" type="AnimationTree" parent="LoadingScreen/LoaderAnimation"]
unique_name_in_owner = true
root_node = NodePath("%LoaderTree/../..")
tree_root = SubResource("AnimationNodeStateMachine_l80un")
anim_player = NodePath("..")
[connection signal="visibility_changed" from="LoadingScreen" to="LoadingScreen" method="_on_visibility_changed"]

View File

@ -0,0 +1,9 @@
extends Control
@onready var loader_tree: AnimationTree = %LoaderTree
@onready var state_machine: AnimationNodeStateMachinePlayback = loader_tree["parameters/playback"]
func _on_visibility_changed() -> void:
if state_machine:
state_machine.start("initial_display", true)

View File

@ -0,0 +1 @@
uid://ctf1if4ly6nun

View File

@ -4,14 +4,12 @@ const PITCH_LIMIT := deg_to_rad(85.0)
const FOCUS_SENSITIVITY := 0.2 const FOCUS_SENSITIVITY := 0.2
const FOCUS_ACCELERATION := 8 const FOCUS_ACCELERATION := 8
@onready var player: Player = owner
@onready var _target := Vector2(rotation.x, rotation.y) @onready var _target := Vector2(rotation.x, rotation.y)
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion: if event is InputEventMouseMotion:
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and player.look_enabled: if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and Player.instance.look_enabled:
camera_motion((event as InputEventMouseMotion).relative) camera_motion((event as InputEventMouseMotion).relative)
elif event is InputEventMouseButton: elif event is InputEventMouseButton:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
@ -21,7 +19,7 @@ func camera_motion(motion: Vector2) -> void:
var x_sensitivity: float = Game.settings.mouse_sensitivity_x var x_sensitivity: float = Game.settings.mouse_sensitivity_x
var y_sensitivity: float = Game.settings.mouse_sensitivity_y var y_sensitivity: float = Game.settings.mouse_sensitivity_y
var invert_pitch: bool = Game.settings.invert_pitch var invert_pitch: bool = Game.settings.invert_pitch
if player.firing: if Player.instance.firing:
# Focus movement when firing # Focus movement when firing
# Game mechanic, should not be user-configurable. # Game mechanic, should not be user-configurable.
x_sensitivity = FOCUS_SENSITIVITY x_sensitivity = FOCUS_SENSITIVITY
@ -42,7 +40,7 @@ func reset_pitch(tween_duration: float) -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
var mouse_accel: float = Game.settings.mouse_acceleration var mouse_accel: float = Game.settings.mouse_acceleration
if player.firing: if Player.instance.firing:
mouse_accel = FOCUS_ACCELERATION mouse_accel = FOCUS_ACCELERATION
var weight := 1 - exp(-mouse_accel * delta) var weight := 1 - exp(-mouse_accel * delta)

View File

@ -6,7 +6,6 @@ const VELOCITY_FACTOR := 2.0
var _on_right_foot := false var _on_right_foot := false
@onready var player: Player = owner
@onready var left_foot: FootController = %LeftFoot @onready var left_foot: FootController = %LeftFoot
@onready var right_foot: FootController = %RightFoot @onready var right_foot: FootController = %RightFoot
@ -16,12 +15,12 @@ var _on_right_foot := false
func play_footstep() -> void: func play_footstep() -> void:
if player.sneaking: if Player.instance.sneaking:
return return
var foot := right_foot if _on_right_foot else left_foot var foot := right_foot if _on_right_foot else left_foot
var relative_speed := player.velocity.length() - MUTE_VELOCITY var relative_speed := Player.instance.velocity.length() - MUTE_VELOCITY
if relative_speed < 0: if relative_speed < 0:
return return

View File

@ -16,13 +16,11 @@ var timescale: float:
get: get:
return self["parameters/timescale/scale"] return self["parameters/timescale/scale"]
@onready var player: Player = owner
func _process(delta: float) -> void: func _process(delta: float) -> void:
var speed := player.velocity.length() var speed := Player.instance.velocity.length()
var weight := 1 - exp(-BLEND_ACCELERATION * delta) var weight := 1 - exp(-BLEND_ACCELERATION * delta)
if player.is_on_floor(): if Player.instance.is_on_floor():
var timescale_target := speed * VELOCITY_TIMESCALE_FACTOR var timescale_target := speed * VELOCITY_TIMESCALE_FACTOR
timescale = lerpf(timescale, timescale_target, weight) timescale = lerpf(timescale, timescale_target, weight)
else: else:
@ -30,7 +28,7 @@ func _process(delta: float) -> void:
if Game.settings.enable_head_bob: if Game.settings.enable_head_bob:
var blend_target := 0.0 var blend_target := 0.0
if player.is_on_floor(): if Player.instance.is_on_floor():
blend_target = speed * VELOCITY_BLEND_FACTOR blend_target = speed * VELOCITY_BLEND_FACTOR
blend = lerpf(blend, blend_target, weight) blend = lerpf(blend, blend_target, weight)

View File

@ -183,6 +183,12 @@ func _signal_death() -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
# REMOVEME
if Input.is_action_just_pressed("ui_page_down"):
get_grabbed()
if Input.is_action_just_pressed("ui_page_up"):
World.instance.save_progress()
# Will be null if no valid interactor is selected. # Will be null if no valid interactor is selected.
var interactive: Interactive = interact_ray.get_collider() as Interactive var interactive: Interactive = interact_ray.get_collider() as Interactive
hud.select_interactive(interactive) hud.select_interactive(interactive)

View File

@ -344,10 +344,10 @@ anchor_left = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -401.486 offset_left = -402.339
offset_top = -302.289 offset_top = -299.253
offset_right = -401.486 offset_right = -402.339
offset_bottom = -302.289 offset_bottom = -299.253
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("4_ud8na") script = ExtResource("4_ud8na")

View File

@ -2,6 +2,7 @@ class_name World extends Node
## Access and flow control for the game world. ## Access and flow control for the game world.
@export var pause_enabled := true @export var pause_enabled := true
@export var save_path := "user://saved_game.tscn"
@export var manager: WorldManager @export var manager: WorldManager
@export var spook_manager: SpookManager @export var spook_manager: SpookManager
@ -61,4 +62,20 @@ func on_player_death() -> void:
func on_game_over() -> void: func on_game_over() -> void:
# TODO: reload from last checkpoint # TODO: reload from last checkpoint
# in the mean time, just reload the level # in the mean time, just reload the level
load_level(current_level) Game.instance.queue_scene(save_path)
func _reown_tree(node: Node) -> void:
for c: Node in node.get_children():
c.set_owner(self)
_reown_tree(c)
func save_progress() -> void:
# No way this works, right?
print("Preparing world for save...")
var save := PackedScene.new()
_reown_tree(self)
save.pack(self)
print("Writing save to ", save_path)
ResourceSaver.save(save, save_path)