From db83628775975d22ed614c87b2787ff89e45d878 Mon Sep 17 00:00:00 2001 From: Diane Nguyen Date: Tue, 7 Jul 2026 19:03:53 -0700 Subject: [PATCH] Platformer 2.0 --- .editorconfig | 4 ++ .gitattributes | 2 + .gitignore | 3 ++ common/state.gd | 13 ++++++ common/state.gd.uid | 1 + common/state_machine.gd | 71 ++++++++++++++++++++++++++++++ common/state_machine.gd.uid | 1 + common/state_transition.gd | 15 +++++++ common/state_transition.gd.uid | 1 + common/transition_condition.gd | 18 ++++++++ common/transition_condition.gd.uid | 1 + icon.svg | 1 + icon.svg.import | 43 ++++++++++++++++++ main_menu.tscn | 24 ++++++++++ player/player_controller.gd | 37 ++++++++++++++++ player/player_controller.gd.uid | 1 + player/setup_state.gd | 37 ++++++++++++++++ player/setup_state.gd.uid | 1 + player/states/fall.gd | 8 ++++ player/states/fall.gd.uid | 1 + player/states/idle.gd | 8 ++++ player/states/idle.gd.uid | 1 + project.godot | 31 +++++++++++++ 23 files changed, 323 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 common/state.gd create mode 100644 common/state.gd.uid create mode 100644 common/state_machine.gd create mode 100644 common/state_machine.gd.uid create mode 100644 common/state_transition.gd create mode 100644 common/state_transition.gd.uid create mode 100644 common/transition_condition.gd create mode 100644 common/transition_condition.gd.uid create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 main_menu.tscn create mode 100644 player/player_controller.gd create mode 100644 player/player_controller.gd.uid create mode 100644 player/setup_state.gd create mode 100644 player/setup_state.gd.uid create mode 100644 player/states/fall.gd create mode 100644 player/states/fall.gd.uid create mode 100644 player/states/idle.gd create mode 100644 player/states/idle.gd.uid create mode 100644 project.godot diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/common/state.gd b/common/state.gd new file mode 100644 index 0000000..3c62063 --- /dev/null +++ b/common/state.gd @@ -0,0 +1,13 @@ +extends Resource +class_name State + +@export var id: StringName + +func enter(_data: Object) -> void: + pass + +func exit(_data: Object) -> void: + pass + +func process(_data: Object, _delta: float) -> void: + pass diff --git a/common/state.gd.uid b/common/state.gd.uid new file mode 100644 index 0000000..98880db --- /dev/null +++ b/common/state.gd.uid @@ -0,0 +1 @@ +uid://b2htg6oquvdho diff --git a/common/state_machine.gd b/common/state_machine.gd new file mode 100644 index 0000000..48de036 --- /dev/null +++ b/common/state_machine.gd @@ -0,0 +1,71 @@ +extends RefCounted +class_name StateMachine + +const MIN_PRIORITY = -2147483648 + +signal state_changed(f_id: StringName, t_id: StringName) + +var states: Dictionary[StringName, State] = {} +var transitions: Array[StateTransition] = [] +var current_state: State + +var _pending_id: StringName = &"" +var _pending_priority = MIN_PRIORITY + +func setup(state_list: Array[State], transition_list: Array[StateTransition], initial_id: StringName) -> void: + states.clear() + for s in state_list: + if s != null: + states[s.id] = s + + transitions = transition_list + + current_state = states.get(initial_id) + if current_state == null and not states.is_empty(): + current_state = states.values()[0] + push_warning("State machine initial state '%s' not found, falling back to '%s'" % [initial_id, current_state.id]) + +func process(data: Object, delta: float) -> void: + if current_state == null: + return + + current_state.process(data, delta) + _evaluate_transitions(data) + +# Escape hatch for an immediate transition +func request_transition(to_id: StringName, priority: int = 999) -> void: + if priority >= _pending_priority: + _pending_id = to_id + _pending_priority = priority + +func _evaluate_transitions(data: Object) -> void: + var best_id: StringName = &"" + var best_priority: int = MIN_PRIORITY + + # Is there a next state pending that isn't our current state? + if _pending_id != &"" and _pending_id != current_state.id and states.has(_pending_id): + best_id = _pending_id + best_priority = _pending_priority + + for t in transitions: + # Skip the current state + if not t.matches_current(current_state.id) or t.to == current_state.id: + continue + if not states.has(t.to): + push_warning("StateMachine: transition targets unknown state '%s'" % t.to) + continue + if t.priority < best_priority: + continue + if t.is_satisfied(data): + best_id = t.to + best_priority = t.priority + + _pending_id = &"" + _pending_priority = MIN_PRIORITY + + if best_id != &"" and best_id != current_state.id: + var from_id = current_state.id + current_state.exit(data) + current_state = states[best_id] + current_state.enter(data) + state_changed.emit(from_id, current_state.id) diff --git a/common/state_machine.gd.uid b/common/state_machine.gd.uid new file mode 100644 index 0000000..5c1d305 --- /dev/null +++ b/common/state_machine.gd.uid @@ -0,0 +1 @@ +uid://cp2okhs1ekwad diff --git a/common/state_transition.gd b/common/state_transition.gd new file mode 100644 index 0000000..9f91f2f --- /dev/null +++ b/common/state_transition.gd @@ -0,0 +1,15 @@ +extends Resource +class_name StateTransition + +const ANY_STATE: StringName = &"*" + +@export var from: StringName = ANY_STATE +@export var to: StringName +@export var priority: int = 0 +@export var condition: TransitionCondition + +func matches_current(current_id: StringName) -> bool: + return from == ANY_STATE or from == current_id + +func is_satisfied(data: Object) -> bool: + return condition == null or condition.evaluate(data) diff --git a/common/state_transition.gd.uid b/common/state_transition.gd.uid new file mode 100644 index 0000000..2266ce6 --- /dev/null +++ b/common/state_transition.gd.uid @@ -0,0 +1 @@ +uid://e2c8iukkr01l diff --git a/common/transition_condition.gd b/common/transition_condition.gd new file mode 100644 index 0000000..2405402 --- /dev/null +++ b/common/transition_condition.gd @@ -0,0 +1,18 @@ +extends Resource +class_name TransitionCondition + +var _callable: Callable + +func _init(callable: Callable = Callable()) -> void: + _callable = callable + + +func evaluate(data: Object) -> bool: + if _callable.is_valid(): + var result = _callable.call(data) + if result is bool: + return result + else: + push_warning("Transition callable did not return boolean! Returning false.") + return false + return true diff --git a/common/transition_condition.gd.uid b/common/transition_condition.gd.uid new file mode 100644 index 0000000..e675f2a --- /dev/null +++ b/common/transition_condition.gd.uid @@ -0,0 +1 @@ +uid://bi3vhketkwgp6 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..8451467 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qmbdc8qsq5x3" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/main_menu.tscn b/main_menu.tscn new file mode 100644 index 0000000..0591369 --- /dev/null +++ b/main_menu.tscn @@ -0,0 +1,24 @@ +[gd_scene format=3 uid="uid://6xemlk773ni5"] + +[ext_resource type="Script" uid="uid://bcipombqbxffa" path="res://player/player_controller.gd" id="1_06t4h"] + +[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_lbwmp"] + +[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_lbwmp"] + +[node name="Node2D" type="Node2D" unique_id=38411652] + +[node name="CharacterBody2D" type="CharacterBody2D" parent="." unique_id=1863609037] +position = Vector2(499, 159) +script = ExtResource("1_06t4h") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="CharacterBody2D" unique_id=1734081974] +shape = SubResource("CapsuleShape2D_lbwmp") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="CharacterBody2D" unique_id=356868445] + +[node name="StaticBody2D" type="StaticBody2D" parent="." unique_id=1508606128] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D" unique_id=889471660] +position = Vector2(0, 576) +shape = SubResource("WorldBoundaryShape2D_lbwmp") diff --git a/player/player_controller.gd b/player/player_controller.gd new file mode 100644 index 0000000..27380b3 --- /dev/null +++ b/player/player_controller.gd @@ -0,0 +1,37 @@ +extends CharacterBody2D + +@export var gravity = 900.0 + +var movement_fsm: StateMachine +@onready var animation_player: AnimationPlayer = $AnimationPlayer + +var FSMSetup = preload("res://player/setup_state.gd") +var movement_states: Array[State] = [] +var movement_transitions: Array[StateTransition] = [] +var movement_init_state: StringName = &"idle" + +func _ready() -> void: + movement_states = FSMSetup.build_states() + movement_transitions = FSMSetup.build_transitions() + + movement_fsm = StateMachine.new() + movement_fsm.setup(movement_states, movement_transitions, movement_init_state) + if movement_fsm.current_state: + movement_fsm.current_state.enter(self) + + +func _physics_process(delta: float) -> void: + if(movement_fsm): + movement_fsm.process(self, delta) + print("Current state: %s" % movement_fsm.current_state.id) + +func play_animation(anim_name: String) -> void: + if animation_player == null: + push_warning("State '%s': player_anims is null, cannot play '%s'" % [movement_fsm.current_state.id, anim_name]) + return + # TODO: + # null checks: no sprite frames + # no animation by name + + if animation_player.current_animation != anim_name: + animation_player.play(anim_name) diff --git a/player/player_controller.gd.uid b/player/player_controller.gd.uid new file mode 100644 index 0000000..cf059e0 --- /dev/null +++ b/player/player_controller.gd.uid @@ -0,0 +1 @@ +uid://bcipombqbxffa diff --git a/player/setup_state.gd b/player/setup_state.gd new file mode 100644 index 0000000..062302b --- /dev/null +++ b/player/setup_state.gd @@ -0,0 +1,37 @@ +extends Resource + +static var IdleState = preload("res://player/states/idle.gd") +static var FallState = preload("res://player/states/fall.gd") + +static func build_states() -> Array[State]: + var list: Array[State] = [] + list.append(_named(IdleState.new(), &"idle")) + list.append(_named(FallState.new(), &"fall")) + return list + +static func build_transitions() -> Array[StateTransition]: + var list: Array[StateTransition] = [] + + var t_is_on_floor = TransitionCondition.new(func(data): + return data.is_on_floor() + ) + var t_n_is_on_floor = TransitionCondition.new(func(data): + return !data.is_on_floor() + ) + + list.append(_edge(&"idle", &"fall", t_n_is_on_floor, 10)) + list.append(_edge(&"fall", &"idle", t_is_on_floor, 10)) + + return list + +static func _named(state: State, id: StringName) -> State: + state.id = id + return state + +static func _edge(from: StringName, to: StringName, condition: TransitionCondition, priority: int) -> StateTransition: + var t:= StateTransition.new() + t.from = from + t.to = to + t.condition = condition + t.priority = priority + return t diff --git a/player/setup_state.gd.uid b/player/setup_state.gd.uid new file mode 100644 index 0000000..c76e9af --- /dev/null +++ b/player/setup_state.gd.uid @@ -0,0 +1 @@ +uid://d3vpouroevnpp diff --git a/player/states/fall.gd b/player/states/fall.gd new file mode 100644 index 0000000..00aa848 --- /dev/null +++ b/player/states/fall.gd @@ -0,0 +1,8 @@ +extends State + +func enter(char: Object) -> void: + char.play_animation("fall") + +func process(char: Object, delta: float) -> void: + char.velocity.y += 900.0 * delta + char.move_and_slide() diff --git a/player/states/fall.gd.uid b/player/states/fall.gd.uid new file mode 100644 index 0000000..14af0a0 --- /dev/null +++ b/player/states/fall.gd.uid @@ -0,0 +1 @@ +uid://c1dkyfrnd4mcu diff --git a/player/states/idle.gd b/player/states/idle.gd new file mode 100644 index 0000000..535cb37 --- /dev/null +++ b/player/states/idle.gd @@ -0,0 +1,8 @@ +extends State + +func enter(char: Object) -> void: + char.play_animation("idle") + char.velocity.x = 0.0 + +func process(char: Object, delta: float) -> void: + char.move_and_slide() diff --git a/player/states/idle.gd.uid b/player/states/idle.gd.uid new file mode 100644 index 0000000..6506143 --- /dev/null +++ b/player/states/idle.gd.uid @@ -0,0 +1 @@ +uid://bq7nhigvlxpq5 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..b5bb2f3 --- /dev/null +++ b/project.godot @@ -0,0 +1,31 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="New Game Project 2" +run/main_scene="uid://6xemlk773ni5" +config/features=PackedStringArray("4.7", "GL Compatibility") +config/icon="res://icon.svg" + +[display] + +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility"