Basic platformer with rope

This commit is contained in:
ASOwnerYT
2026-07-25 16:10:50 +12:00
commit 4818002bbd
503 changed files with 16081 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
class_name GameState
extends Resource
const STATE_NAME : String = "GameState"
const FILE_PATH = "res://scripts/game_state.gd"
@export var level_states : Dictionary = {}
@export var current_level_path : String
@export var checkpoint_level_path : String
@export var total_games_played : int
@export var play_time : int
@export var total_time : int
static func get_level_state(level_state_key : String) -> LevelState:
if not has_game_state():
return
var game_state := get_or_create_state()
if level_state_key.is_empty() : return
if level_state_key in game_state.level_states:
return game_state.level_states[level_state_key]
else:
var new_level_state := LevelState.new()
game_state.level_states[level_state_key] = new_level_state
GlobalState.save()
return new_level_state
static func has_game_state() -> bool:
return GlobalState.has_state(STATE_NAME)
static func get_or_create_state() -> GameState:
return GlobalState.get_or_create_state(STATE_NAME, FILE_PATH)
static func get_current_level_path() -> String:
if not has_game_state():
return ""
var game_state := get_or_create_state()
return game_state.current_level_path
static func get_checkpoint_level_path() -> String:
if not has_game_state():
return ""
var game_state := get_or_create_state()
return game_state.checkpoint_level_path
static func get_levels_reached() -> int:
if not has_game_state():
return 0
var game_state := get_or_create_state()
return game_state.level_states.size()
static func set_checkpoint_level_path(level_path : String) -> void:
var game_state := get_or_create_state()
game_state.checkpoint_level_path = level_path
get_level_state(level_path)
GlobalState.save()
static func set_current_level_path(level_path : String) -> void:
var game_state := get_or_create_state()
game_state.current_level_path = level_path
GlobalState.save()
static func start_game() -> void:
var game_state := get_or_create_state()
game_state.total_games_played += 1
GlobalState.save()
static func continue_game() -> void:
var game_state := get_or_create_state()
game_state.current_level_path = game_state.checkpoint_level_path
GlobalState.save()
static func reset() -> void:
var game_state := get_or_create_state()
game_state.level_states = {}
game_state.current_level_path = ""
game_state.checkpoint_level_path = ""
game_state.play_time = 0
game_state.total_time = 0
GlobalState.save()
+1
View File
@@ -0,0 +1 @@
uid://dfdtk2w0cswkc
+15
View File
@@ -0,0 +1,15 @@
extends LevelManager
func set_current_level_path(value : String) -> void:
super.set_current_level_path(value)
GameState.set_current_level_path(value)
func set_checkpoint_level_path(value : String) -> void:
super.set_checkpoint_level_path(value)
GameState.set_checkpoint_level_path(value)
func get_checkpoint_level_path() -> String:
var state_level_path := GameState.get_checkpoint_level_path()
if not state_level_path.is_empty():
return state_level_path
return super.get_checkpoint_level_path()
+1
View File
@@ -0,0 +1 @@
uid://83jesd4p8faw
+5
View File
@@ -0,0 +1,5 @@
class_name LevelState
extends Resource
@export var color : Color
@export var tutorial_read : bool = false
+1
View File
@@ -0,0 +1 @@
uid://c2e4abybld5at
+45
View File
@@ -0,0 +1,45 @@
class_name Player
extends CharacterBody3D
@export var player_id := 1
@export var gravity_vector := Vector3(0, -20, 0)
@export var move_speed := 8.0
@export var accel := 50.0
@export var friction := 16.0
@export var jump_speed := 12.0
@export var air_control := 0.65
@export var coyote := 0.1
@export var buffer := 0.1
@export var col1 := Color(0.3, 0.6, 1)
@export var col2 := Color(1, 0.47, 0.35)
var ct := 0.0
var bt := 0.0
func _ready():
add_to_group("players")
# $Body.color = col1 if player_id == 1 else col2
func _physics_process(d):
var up = Vector3.UP if gravity_vector.length_squared() < 0.0001 else -gravity_vector.normalized()
up_direction = up
velocity += gravity_vector * d
ct = coyote if is_on_floor() else max(ct - d, 0.0)
bt = max(bt - d, 0.0)
if Input.is_action_just_pressed("p%d_jump" % player_id):
bt = buffer
var ix = Input.get_axis("p%d_left" % player_id, "p%d_right" % player_id)
var a = accel if is_on_floor() else accel * air_control
if abs(ix) > 0.01:
velocity.x = move_toward(velocity.x, ix * move_speed, a * d)
else:
velocity.x = move_toward(velocity.x, 0.0, friction * d)
if bt > 0.0 and ct > 0.0:
velocity = velocity.slide(up) + up * jump_speed
bt = 0.0
ct = 0.0
# plane lock: 2D platformer physics in a 3D scene. Sliding along an angled
# collision normal can otherwise walk the player off the Z=0 plane.
velocity.z = 0.0
move_and_slide()
global_position.z = 0.0
+1
View File
@@ -0,0 +1 @@
uid://bg6mw4gfk50jv
+104
View File
@@ -0,0 +1,104 @@
extends "res://addons/pinjoint-ropephysics/path_3d_rope.gd"
## Tether variant of the pinjoint rope.
##
## Adds two things the base addon does not do:
## - endpoints can be any PhysicsBody3D (the addon only exports RigidBody3D,
## and its `rigidbody_attached_to_start` path overwrites node_b instead of
## setting node_a, which pins the body to world space rather than to the rope)
## - every segment is locked to the X-Y plane so the rope behaves as a 2D rope
##
## Expects to be a direct child of an untransformed parent: the base addon bakes
## its own local `position` into the segment/joint positions and zeroes the node.
## Bodies the two rope ends pin to. NodePath rather than a typed node export so
## the value resolves reliably when set from a .tscn instance override.
@export var attach_start_path : NodePath
@export var attach_end_path : NodePath
var attach_start : PhysicsBody3D
var attach_end : PhysicsBody3D
## Extra rope length as a fraction of the gap between the two endpoints.
## 0.0 spawns the rope taut (pin joints will fight); ~0.15 gives a natural sag.
@export_range(0.0, 1.0, 0.01) var slack := 0.15
## Constrain segments to the X-Y plane (linear Z, angular X and Y).
@export var plane_lock_z := true
@export_flags_3d_physics var segment_collision_layer := 4
@export_flags_3d_physics var segment_collision_mask := 1
# The base script zeroes `position` and bakes it into child positions, so keep
# our own copy to place the end joint we add.
var _origin_offset := Vector3.ZERO
func _ready() -> void:
attach_start = get_node_or_null(attach_start_path) as PhysicsBody3D
attach_end = get_node_or_null(attach_end_path) as PhysicsBody3D
_origin_offset = position
_fit_curve_to_endpoints()
super()
_apply_plane_lock()
_wire_endpoints()
# Rebuild the curve as a sagging arc between the two endpoints so the rope
# always spans wherever the players actually spawn.
func _fit_curve_to_endpoints() -> void:
if attach_start == null or attach_end == null:
return
var a := to_local(attach_start.global_position)
var b := to_local(attach_end.global_position)
var span := a.distance_to(b)
if span < 0.001:
return
# Bow the spawn arc *upward*. A downward sag can start inside level geometry
# (a rope between two grounded players dips below the floor), and segments
# that spawn embedded tunnel straight through it. Starting high is always
# clear, and gravity drapes the rope into place within a few frames.
var mid := (a + b) * 0.5 + Vector3(0, span * slack, 0)
var fitted := Curve3D.new()
# The scene's curve ships with a huge bake_interval, which would make
# sample_baked() miss the midpoint entirely.
fitted.bake_interval = maxf(span / float(number_of_segments) * 0.25, 0.05)
fitted.add_point(a)
fitted.add_point(mid)
fitted.add_point(b)
curve = fitted
# `distance` is an @onready in the base script; recompute it explicitly so
# the value is correct regardless of when that initializer runs.
distance = curve.get_baked_length()
func _apply_plane_lock() -> void:
for segment in segments:
segment.collision_layer = segment_collision_layer
segment.collision_mask = segment_collision_mask
segment.continuous_cd = true
if plane_lock_z:
segment.axis_lock_linear_z = true
segment.axis_lock_angular_x = true
segment.axis_lock_angular_y = true
# The base script offsets look_at by (0.001, 0, -0.001) to dodge a
# degenerate up vector, which nudges segments off the plane.
segment.position.z = 0.0
func _wire_endpoints() -> void:
if attach_start != null:
# node_b first: assigning node_a while node_b is still segments[0] would
# briefly join the body to itself.
joints[0].node_b = attach_start.get_path()
joints[0].node_a = segments[0].get_path()
if attach_end != null and not fixed_end_point:
var end_joint := PinJoint3D.new()
add_child(end_joint)
end_joint.position = curve_points[-1] + _origin_offset
end_joint.node_a = segments[-1].get_path()
end_joint.node_b = attach_end.get_path()
end_joint.set_param(PinJoint3D.PARAM_BIAS, joint_bias_or_stiffness)
end_joint.set_param(PinJoint3D.PARAM_IMPULSE_CLAMP, max_impulse)
joints.append(end_joint)
+1
View File
@@ -0,0 +1 @@
uid://bop72t5pobems