Basic platformer with rope
This commit is contained in:
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfdtk2w0cswkc
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://83jesd4p8faw
|
||||
@@ -0,0 +1,5 @@
|
||||
class_name LevelState
|
||||
extends Resource
|
||||
|
||||
@export var color : Color
|
||||
@export var tutorial_read : bool = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2e4abybld5at
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://bg6mw4gfk50jv
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bop72t5pobems
|
||||
Reference in New Issue
Block a user