Add a savegame system using slots and a main menu, allow player to move RigidBody2Ds using apply_impulse(), increased physics tick, enabled FXAA, and more.

This commit is contained in:
ChaoticByte 2024-10-01 23:19:37 +02:00
parent 35ebe26340
commit 5a67d46d6f
No known key found for this signature in database
13 changed files with 291 additions and 25 deletions

87
core/globals/gamestate.gd Normal file
View file

@ -0,0 +1,87 @@
extends Node
# gamestate
var last_entrypoint: String
func reset_gamestate():
# defaults
last_entrypoint = "intro_start"
# save/load slot
const slot_min: int = 0
const slot_max: int = 3
var current_slot: int = -1
var current_slot_locked: bool = false
func _check_slot_idx(idx: int) -> bool:
# returns if slot index is valid (in bounds)
if idx < slot_min or idx > slot_max:
push_error("Slot index %s is invalid." % current_slot)
return false
return true
func save_slot():
# flush game state to slot
if _check_slot_idx(current_slot):
var slot_path: String = "user://slot%s.save" % current_slot
var f = FileAccess.open(slot_path, FileAccess.WRITE)
if f == null:
push_error("Couldn't open %s" % slot_path)
quit(1)
return
# create serializable data structure
var data: Dictionary = {
"last_entrypoint": last_entrypoint
}
var data_serialized = JSON.stringify(data)
if data_serialized == null:
push_error("Couldn't serialize save data for slot %s" % current_slot)
quit(1)
return
f.store_string(data_serialized)
func load_slot():
# load gamestate from slot
if _check_slot_idx(current_slot):
reset_gamestate() # init gamestate with defaults
var slot_path: String = "user://slot%s.save" % current_slot
if FileAccess.file_exists(slot_path):
var f = FileAccess.open(slot_path, FileAccess.READ)
if f == null:
push_error("Couldn't open %s" % slot_path)
quit(1)
return
var data_serialized = f.get_as_text()
var data = JSON.parse_string(data_serialized)
if data == null:
push_error("Couldn't deserialize slot %s" % slot_path)
quit(1)
return
# read values
last_entrypoint = data["last_entrypoint"]
else:
save_slot() # new game; save defaults
func reset_slot(idx: int):
# reset a save slot
if _check_slot_idx(idx):
if current_slot == idx:
reset_gamestate()
# delete slot file
var slot_path: String = "user://slot%s.save" % idx
if FileAccess.file_exists(slot_path):
DirAccess.remove_absolute(slot_path)
#
func quit(code: int):
# defer get_tree().quit() to not lose the last error/stdout
# see https://github.com/godotengine/godot/issues/90667
await get_tree().create_timer(0.1).timeout
get_tree().quit(code)
func _ready() -> void:
reset_gamestate()

View file

@ -1,59 +1,79 @@
extends Node extends Node
const mainmenu_player_pos: Vector2 = Vector2(0, 128 + 32)
class Entrypoint extends Object: class Entrypoint extends Object:
var scene_name: String var scene_name: String
var player_position: Vector2 var player_position: Vector2
var reset_physics: bool var initial_velocity: Vector2
func _init( func _init(
scene_name_: String, scene_name_: String,
player_position_: Vector2, player_position_: Vector2,
reset_physics_: bool initial_velocity_: Vector2 = Vector2.ZERO
) -> void: ) -> void:
self.scene_name = scene_name_ self.scene_name = scene_name_
self.player_position = player_position_ self.player_position = player_position_
self.reset_physics = reset_physics_ self.initial_velocity = initial_velocity_
const SCENES = { const SCENES = {
"intro": "uid://c6w7lrydi43ts" "intro": "uid://c6w7lrydi43ts",
"test": "uid://dqf665b540tfg",
} }
var ENTRYPOINTS = { var ENTRYPOINTS = {
"intro_start": Entrypoint.new("intro", Vector2(0, 0), true) "intro_start": Entrypoint.new("intro", Vector2.ZERO),
"test": Entrypoint.new("test", Vector2.ZERO),
} }
var MENU_SCENE: PackedScene = preload("res://menu/menu.tscn")
# load that stuff # load that stuff
var level_root: Node func _pre_load_checks() -> bool:
var player: Node2D if NodeRegistry.level_root_container == null:
push_error("Can't load level, level_root is not registered yet.")
return false
if NodeRegistry.player == null:
push_error("Can't load entrypoint, player is not registered yet.")
return false
return true
func load_scene(scn_name: String) -> bool: # returns true on success func load_scene(scn_name: String) -> bool: # returns true on success
if level_root == null: if not _pre_load_checks():
push_error("Can't load level, level_root is not registered yet.")
return false return false
if not scn_name in SCENES: if not scn_name in SCENES:
push_error("Level " + scn_name + " doesn't exist.") push_error("Level " + scn_name + " doesn't exist.")
return false return false
unload_scene() unload_scene()
var scn = load(SCENES[scn_name]) var scn = load(SCENES[scn_name])
level_root.add_child(scn.instantiate()) NodeRegistry.level_root_container.add_child(scn.instantiate())
return true return true
func unload_scene(): func unload_scene():
for c in level_root.get_children(): if NodeRegistry.level_root_container != null:
for c in NodeRegistry.level_root_container.get_children():
c.queue_free() c.queue_free()
func load_entrypoint(ep_name: String) -> bool: # returns true on success func load_entrypoint(ep_name: String) -> bool: # returns true on success
if not ep_name in ENTRYPOINTS: if not ep_name in ENTRYPOINTS:
push_error("Entrypoint " + ep_name + " doesn't exist.") push_error("Entrypoint " + ep_name + " doesn't exist.")
return false return false
if player == null: if not _pre_load_checks():
push_error("Can't load entrypoint, player is not registered yet.")
return false return false
var e: Entrypoint = ENTRYPOINTS[ep_name] var e: Entrypoint = ENTRYPOINTS[ep_name]
if load_scene(e.scene_name): if load_scene(e.scene_name):
player.position = e.player_position NodeRegistry.player.position = e.player_position
if e.reset_physics: NodeRegistry.player.velocity = e.initial_velocity
player.reset_physics() Gamestate.last_entrypoint = ep_name
Gamestate.save_slot() # save game
return true return true
else: else:
return false return false
func load_menu():
if not _pre_load_checks():
return false
unload_scene()
NodeRegistry.level_root_container.add_child(MENU_SCENE.instantiate())
NodeRegistry.player.position = mainmenu_player_pos
return true

View file

@ -0,0 +1,4 @@
extends Node
var player: CharacterBody2D
var level_root_container: Node

View file

@ -1,5 +1,9 @@
extends Node2D extends Node2D
func _on_area_2d_body_entered(body: Node2D) -> void: func _on_area_2d_body_entered(body: Node2D) -> void:
if body == Levels.player: if body == NodeRegistry.player:
Levels.load_entrypoint("intro_start") NodeRegistry.player.die()
func _on_next_level_body_entered(body: Node2D) -> void:
if body == NodeRegistry.player:
Levels.load_entrypoint("test")

View file

@ -24,4 +24,11 @@ color = Color(1, 0.1, 0, 1)
polygon = PackedVector2Array(256, 384, 360, 384, 360, 440, 256, 440) polygon = PackedVector2Array(256, 384, 360, 384, 360, 440, 256, 440)
color = Color(1, 0.1, 0, 1) color = Color(1, 0.1, 0, 1)
[node name="next_level" type="Area2D" parent="."]
[node name="Polygon" parent="next_level" instance=ExtResource("1_cup10")]
polygon = PackedVector2Array(416, 400, 416, 424, 440, 424, 440, 400)
color = Color(0.484431, 0.687354, 1, 1)
[connection signal="body_entered" from="Area2D" to="." method="_on_area_2d_body_entered"] [connection signal="body_entered" from="Area2D" to="." method="_on_area_2d_body_entered"]
[connection signal="body_entered" from="next_level" to="." method="_on_next_level_body_entered"]

10
levels/test.tscn Normal file
View file

@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://dqf665b540tfg"]
[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_xm2ft"]
[node name="Test" type="Node2D"]
[node name="StaticBody2D" type="StaticBody2D" parent="."]
[node name="Polygon" parent="StaticBody2D" instance=ExtResource("1_xm2ft")]
polygon = PackedVector2Array(-104, 232, -104, 320, 552, 320, 552, 232)

View file

@ -1,6 +1,6 @@
extends Node2D extends Node2D
func _ready() -> void: func _ready() -> void:
Levels.level_root = $LevelRoot NodeRegistry.level_root_container = $LevelRoot
Levels.player = $Player NodeRegistry.player = $Player
Levels.load_entrypoint("intro_start") Levels.load_menu()

5
menu/menu.gd Normal file
View file

@ -0,0 +1,5 @@
extends Node2D
func _physics_process(_delta: float) -> void:
if NodeRegistry.player.position.y > 1000:
NodeRegistry.player.position = Vector2(0, 0)

55
menu/menu.tscn Normal file
View file

@ -0,0 +1,55 @@
[gd_scene load_steps=4 format=3 uid="uid://bqmpoix37kutp"]
[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_8p275"]
[ext_resource type="Script" path="res://menu/menu.gd" id="1_g2w4y"]
[ext_resource type="PackedScene" uid="uid://c40fli7qcma78" path="res://menu/slot/slot.tscn" id="3_rc4dm"]
[node name="Menu" type="Node2D"]
position = Vector2(0, -1)
script = ExtResource("1_g2w4y")
[node name="Label" type="Label" parent="."]
offset_left = -192.0
offset_top = -15.0
offset_right = 192.0
offset_bottom = 65.0
theme_override_font_sizes/font_size = 52
text = "Main Menu"
horizontal_alignment = 1
vertical_alignment = 2
[node name="StaticBody2D" type="StaticBody2D" parent="."]
[node name="Polygon" parent="StaticBody2D" instance=ExtResource("1_8p275")]
polygon = PackedVector2Array(-256, 209, 320, 209, 320, 193, -272, 193, -272, 601, -304, 601, -304, 617, -256, 617)
[node name="SaveGameSlots" type="Node2D" parent="."]
position = Vector2(-448, 177)
[node name="Saves" type="Label" parent="SaveGameSlots"]
offset_left = -64.0
offset_top = -176.0
offset_right = 64.0
offset_bottom = -112.0
theme_override_font_sizes/font_size = 42
text = "Saves"
horizontal_alignment = 1
vertical_alignment = 2
[node name="Slot0" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")]
slot_label = "0"
[node name="Slot1" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")]
position = Vector2(0, 128)
slot_idx = 1
slot_label = "1"
[node name="Slot2" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")]
position = Vector2(0, 256)
slot_idx = 2
slot_label = "2"
[node name="Slot3" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")]
position = Vector2(0, 384)
slot_idx = 3
slot_label = "3"

21
menu/slot/slot.gd Normal file
View file

@ -0,0 +1,21 @@
extends Node2D
@export var slot_idx: int
@export var slot_label: String = ""
func _ready() -> void:
$Label.text = str(slot_label)
func _on_area_2d_load_body_entered(body: Node2D) -> void:
# load slot & start game
if body == NodeRegistry.player:
Gamestate.current_slot = slot_idx
Gamestate.load_slot()
Levels.load_entrypoint(Gamestate.last_entrypoint)
func _on_area_2d_delete_body_entered(body: Node2D) -> void:
# reset slot on disk
if body == NodeRegistry.player:
Gamestate.reset_slot(slot_idx)
NodeRegistry.player.position = Levels.mainmenu_player_pos

35
menu/slot/slot.tscn Normal file
View file

@ -0,0 +1,35 @@
[gd_scene load_steps=3 format=3 uid="uid://c40fli7qcma78"]
[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_2cb7u"]
[ext_resource type="Script" path="res://menu/slot/slot.gd" id="1_3dgi0"]
[node name="Slot" type="Node2D"]
script = ExtResource("1_3dgi0")
[node name="StaticBody2D" type="StaticBody2D" parent="."]
[node name="platform" parent="StaticBody2D" instance=ExtResource("1_2cb7u")]
polygon = PackedVector2Array(44, 16, -44, 16, -44, 48, -64, 48, -64, 16, -80, 16, -80, 56, 80, 56, 80, 16, 64, 16, 64, 48, 44, 48)
[node name="Area2D_Load" type="Area2D" parent="."]
[node name="Polygon" parent="Area2D_Load" instance=ExtResource("1_2cb7u")]
polygon = PackedVector2Array(44, 48, 64, 48, 64, 24, 44, 24)
color = Color(0, 1, 0, 1)
[node name="Area2D_Delete" type="Area2D" parent="."]
[node name="Polygon" parent="Area2D_Delete" instance=ExtResource("1_2cb7u")]
polygon = PackedVector2Array(-64, 48, -44, 48, -44, 24, -64, 24)
color = Color(1, 0, 0, 1)
[node name="Label" type="Label" parent="."]
offset_left = -16.0
offset_top = -32.0
offset_right = 16.0
text = "_"
horizontal_alignment = 1
vertical_alignment = 1
[connection signal="body_entered" from="Area2D_Load" to="." method="_on_area_2d_load_body_entered"]
[connection signal="body_entered" from="Area2D_Delete" to="." method="_on_area_2d_delete_body_entered"]

View file

@ -1,9 +1,16 @@
extends CharacterBody2D extends CharacterBody2D
# die
func die():
Levels.load_entrypoint(Gamestate.last_entrypoint)
# movement and stuff
@export var movement_speed = 300.0 @export var movement_speed = 300.0
@export var jump_velocity = -350.0 @export var jump_velocity = -350.0
@export var max_jumps: int = 2 @export var max_jumps: int = 2
@export var rigidbody_impulse_mult: float = 0.04
@onready var ceiling_raycast1 = $RayCastUp1 @onready var ceiling_raycast1 = $RayCastUp1
@onready var ceiling_raycast2 = $RayCastUp2 @onready var ceiling_raycast2 = $RayCastUp2
@ -34,6 +41,12 @@ func _physics_process(delta: float) -> void:
velocity.y = jump_velocity velocity.y = jump_velocity
jumps += 1 jumps += 1
move_and_slide() move_and_slide()
# affect rigid bodies
func reset_physics(): # adapted solution from
velocity = Vector2.ZERO # https://kidscancode.org/godot_recipes/4.x/physics/character_vs_rigid/index.html
for i in get_slide_collision_count():
var collision = get_slide_collision(i)
var collider = collision.get_collider()
if collider is RigidBody2D:
var impulse = -collision.get_normal() * (velocity.length() / collider.mass) * rigidbody_impulse_mult
collider.apply_central_impulse(impulse)

View file

@ -19,6 +19,8 @@ config/icon="res://icon.svg"
[autoload] [autoload]
Levels="*res://core/globals/levels.gd" Levels="*res://core/globals/levels.gd"
NodeRegistry="*res://core/globals/node_registry.gd"
Gamestate="*res://core/globals/gamestate.gd"
[debug] [debug]
@ -54,6 +56,8 @@ player_right={
[physics] [physics]
common/physics_ticks_per_second=120
common/max_physics_steps_per_frame=16
common/physics_jitter_fix=0.0 common/physics_jitter_fix=0.0
2d/physics_engine="Rapier2D" 2d/physics_engine="Rapier2D"
common/physics_interpolation=true common/physics_interpolation=true
@ -61,6 +65,7 @@ common/physics_interpolation=true
[rendering] [rendering]
environment/defaults/default_clear_color=Color(0, 0, 0, 1) environment/defaults/default_clear_color=Color(0, 0, 0, 1)
anti_aliasing/quality/screen_space_aa=1
anti_aliasing/size/mode=3 anti_aliasing/size/mode=3
anti_aliasing/stretch/aspect="keep" anti_aliasing/stretch/aspect="keep"
anti_aliasing/stretch/mode="viewport" anti_aliasing/stretch/mode="viewport"