Assets grouped by role, within the constraint that FBX materials resolve
their textures by path relative to the FBX itself (verified by forcing a
reimport and reading back the albedo paths):
assets/level/ scene.fbx + Level1_v1.fbx + the platform maps they look
up as siblings
assets/logos/ the three attribution logos
assets/ chara_red.fbx, background.fbx — these reach up into
../sourceimages and so cannot move
sourcefiles/ scene.blend, behind a .gdignore
The .blend move is a bug fix, not tidying. Godot hands every importable
.blend to a headless Blender, and that call never returns on this project
— a fresh clone stalls on first import, and the machine still had orphaned
Blender processes from an earlier attempt. Nothing loads the .blend: the
level geometry is baked into level_1.tscn.
player.gd: spell out ct/bt/d/ix/a, type the physics step, group the
members, and point the blue skin at sourceimages/low_blue_mat_* — the same
files assets/Chara_blue/ was a byte-identical copy of.
Co-Authored-By: Claude Opus 5 <[email protected]>
120 lines
4.4 KiB
GDScript
120 lines
4.4 KiB
GDScript
class_name Player
|
|
extends CharacterBody3D
|
|
|
|
## Player-vs-player stacking. A rider standing on us would otherwise block our
|
|
## own move_and_slide (its collider reads as a slanted wall), so while carried we
|
|
## drop the players layer from our mask — the rider keeps colliding with us, the
|
|
## test is one-way. Carrying the rider along is CharacterBody3D's own
|
|
## moving-platform handling; adding it again here just launches them off.
|
|
const PLAYERS_LAYER := 2
|
|
const CARRIED_LINGER := 0.1
|
|
|
|
## The character model ships textured in red. Player 1 wears the blue variant,
|
|
## which exists only as a pair of base-color maps sitting beside the red ones
|
|
## under the same name — every other map (normal/roughness/metallic/height) is
|
|
## shared between the two, so albedo is all that gets swapped.
|
|
const BLUE_SKIN_PLAYER_ID := 1
|
|
const RED_MATERIAL_PREFIX := "low_chara_mat_"
|
|
const BLUE_MATERIAL_PREFIX := "low_blue_mat_"
|
|
|
|
@export var player_id := 1
|
|
@export var gravity_vector := Vector3(0, -15, 0)
|
|
@export var move_speed := 8.0
|
|
@export var accel := 50.0
|
|
@export var friction := 16.0
|
|
@export var jump_speed := 8.0
|
|
@export var air_control := 0.65
|
|
@export var coyote := 0.1
|
|
@export var buffer := 0.1
|
|
|
|
# Coyote time and jump buffer remaining, in seconds.
|
|
var coyote_t := 0.0
|
|
var buffer_t := 0.0
|
|
|
|
# Time left on the one-way stacking exemption. See PLAYERS_LAYER.
|
|
var carried_t := 0.0
|
|
|
|
## Cleared when the run ends. Physics keeps running — the player still falls,
|
|
## slides to a stop and gets dragged by whatever is left of the rope — only
|
|
## input is ignored.
|
|
var controls_enabled := true : set = set_controls_enabled
|
|
|
|
|
|
# Drops the buffered jump on the way out, so a press from the frame before the
|
|
# controls were cut does not still fire afterwards.
|
|
func set_controls_enabled(value : bool) -> void:
|
|
controls_enabled = value
|
|
if not value:
|
|
buffer_t = 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
add_to_group("players")
|
|
if player_id == BLUE_SKIN_PLAYER_ID:
|
|
_apply_blue_skin(self)
|
|
|
|
|
|
# Walks the model and re-points every surface that uses a red base-color map at
|
|
# its blue counterpart. Surfaces without one (the visor, the hands the two
|
|
# variants share) keep the material they were imported with.
|
|
func _apply_blue_skin(node : Node) -> void:
|
|
for child in node.get_children():
|
|
_apply_blue_skin(child)
|
|
var mesh_instance := node as MeshInstance3D
|
|
if mesh_instance == null or mesh_instance.mesh == null:
|
|
return
|
|
for surface in mesh_instance.mesh.get_surface_count():
|
|
var material := mesh_instance.get_active_material(surface) as BaseMaterial3D
|
|
if material == null or material.albedo_texture == null:
|
|
continue
|
|
var blue_path := material.albedo_texture.resource_path.replace(
|
|
RED_MATERIAL_PREFIX, BLUE_MATERIAL_PREFIX
|
|
)
|
|
if not ResourceLoader.exists(blue_path):
|
|
continue
|
|
var blue_material := material.duplicate() as BaseMaterial3D
|
|
blue_material.albedo_texture = load(blue_path)
|
|
mesh_instance.set_surface_override_material(surface, blue_material)
|
|
|
|
|
|
func _physics_process(delta : float) -> void:
|
|
var up := Vector3.UP if gravity_vector.length_squared() < 0.0001 else -gravity_vector.normalized()
|
|
up_direction = up
|
|
velocity += gravity_vector * delta
|
|
|
|
coyote_t = coyote if is_on_floor() else maxf(coyote_t - delta, 0.0)
|
|
buffer_t = maxf(buffer_t - delta, 0.0)
|
|
if controls_enabled and Input.is_action_just_pressed("p%d_jump" % player_id):
|
|
buffer_t = buffer
|
|
|
|
var input_x := Input.get_axis("p%d_left" % player_id, "p%d_right" % player_id) if controls_enabled else 0.0
|
|
var rate := accel if is_on_floor() else accel * air_control
|
|
if absf(input_x) > 0.01:
|
|
velocity.x = move_toward(velocity.x, input_x * move_speed, rate * delta)
|
|
else:
|
|
velocity.x = move_toward(velocity.x, 0.0, friction * delta)
|
|
|
|
if buffer_t > 0.0 and coyote_t > 0.0:
|
|
velocity = velocity.slide(up) + up * jump_speed
|
|
buffer_t = 0.0
|
|
coyote_t = 0.0
|
|
|
|
carried_t = maxf(carried_t - delta, 0.0)
|
|
set_collision_mask_value(PLAYERS_LAYER, carried_t <= 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()
|
|
|
|
# Riding is handled by CharacterBody3D's own moving-platform support, so the
|
|
# only thing to do here is tell whoever we landed on that it is carrying us.
|
|
for i in get_slide_collision_count():
|
|
var collision := get_slide_collision(i)
|
|
if collision.get_normal().dot(up) < 0.7:
|
|
continue
|
|
var below := collision.get_collider() as Player
|
|
if below != null:
|
|
below.carried_t = CARRIED_LINGER
|
|
global_position.z = 0.0
|