Scenery (all procedural, seeded, no imported assets): - Terrain shader: grass/dry grass/dirt/sand/rock strata/snow + detail normals - 5 tree types, faceted mossy rocks, cliff outcrops, sea stacks, bushes, logs and stumps, batched as MultiMesh per 48 m cell - Wind-animated grass and flowers streamed in tiles around the player - Lake (depth-shaded water with shore foam), ocean to the horizon, cloud sky shader, volumetric fog, glow, softer shadows - Swimming: float at the surface, swim-jump to climb out World bounds: - Steep mountain rim with an invisible wall partway up; one side opens to a sheer sea cliff with the wall 1 m inside the edge; whole chunk is an island so the mesh edge is never visible - Respawn safety net if the player leaves the walkable area or the map - Fix: the cliff edge now tracks the wall in the headlands (it used to drop away inside the wall, leaving a pit with lake water hanging over it) Tests: tests/test_boundary.gd (24-direction escape attempt) and tests/test_terrain_edges.gd (no ground below waterline near the wall). Upgrades the project to Godot 4.7 (project.godot rewritten by the 4.7 importer, .uid files added) and documents it in the setup guide.
117 lines
4.7 KiB
GDScript
117 lines
4.7 KiB
GDScript
extends Node3D
|
|
## World controller.
|
|
##
|
|
## Owns spawn ordering so the player never falls through ungenerated ground:
|
|
## waits for the terrain, places the scenery, then drops the player onto a dry,
|
|
## flat spot facing the lake. Also aims the sun, and respawns the player if they
|
|
## somehow leave the map or fall below it (a safety net behind the mountain rim
|
|
## and invisible wall in terrain.gd).
|
|
##
|
|
## Dev hook: `godot --path . -- --shot=/abs/prefix` renders a few fixed views to
|
|
## <prefix>_<view>.png and quits. Handy for checking visual changes.
|
|
|
|
@export var sun_elevation_deg: float = 34.0
|
|
@export var sun_azimuth_deg: float = 40.0 ## 0 = behind the spawn view, + = to its right
|
|
|
|
@onready var terrain: Node3D = $Terrain
|
|
@onready var props: Node3D = $Props
|
|
@onready var player: CharacterBody3D = $Player
|
|
@onready var sun: DirectionalLight3D = $Sun
|
|
|
|
var _spawn := Vector3.ZERO
|
|
var _spawn_yaw := 0.0
|
|
|
|
func _ready() -> void:
|
|
_aim_sun()
|
|
player.set_physics_process(false)
|
|
set_physics_process(false)
|
|
if terrain.built:
|
|
_on_terrain_ready()
|
|
else:
|
|
terrain.ready_built.connect(_on_terrain_ready)
|
|
|
|
func _aim_sun() -> void:
|
|
var el := deg_to_rad(sun_elevation_deg)
|
|
var az := deg_to_rad(sun_azimuth_deg)
|
|
var to_sun := Vector3(sin(az) * cos(el), sin(el), cos(az) * cos(el))
|
|
sun.global_basis = Basis.looking_at(-to_sun, Vector3.UP)
|
|
|
|
func _on_terrain_ready() -> void:
|
|
var t0 := Time.get_ticks_msec()
|
|
_spawn = terrain.find_spawn(Vector2.ZERO)
|
|
var lake: Vector2 = terrain.lake_center
|
|
var to_lake := lake - Vector2(_spawn.x, _spawn.z)
|
|
_spawn_yaw = atan2(-to_lake.x, -to_lake.y)
|
|
player.water_level = terrain.water_level
|
|
props.place_on(terrain, player, _spawn)
|
|
print("[world] scenery placed in %d ms" % (Time.get_ticks_msec() - t0))
|
|
_respawn()
|
|
player.set_physics_process(true)
|
|
set_physics_process(true)
|
|
_maybe_capture()
|
|
|
|
func _respawn() -> void:
|
|
player.global_position = _spawn + Vector3(0.0, 1.5, 0.0)
|
|
player.velocity = Vector3.ZERO
|
|
player.rotation = Vector3(0.0, _spawn_yaw, 0.0)
|
|
player.get_node("Camera3D").rotation.x = 0.0
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
var p := player.global_position
|
|
var half: float = terrain.size * 0.5
|
|
if p.y < terrain.min_height - 30.0 or absf(p.x) > half or absf(p.z) > half \
|
|
or not terrain.inside_boundary(p.x, p.z, -4.0):
|
|
push_warning("[world] player left the map at %s; respawning" % p)
|
|
_respawn()
|
|
|
|
# --- dev screenshot hook ----------------------------------------------------
|
|
|
|
func _maybe_capture() -> void:
|
|
var prefix := ""
|
|
for a in OS.get_cmdline_user_args():
|
|
if a.begins_with("--shot="):
|
|
prefix = a.trim_prefix("--shot=")
|
|
if prefix.is_empty():
|
|
return
|
|
var lake: Vector2 = terrain.lake_center
|
|
# [name, eye position (y = height above ground), look-at point] ; null = spawn view
|
|
var views := [
|
|
["spawn", null, null],
|
|
["aerial", Vector3(0, 110, 150), Vector3(0, 0, -20)],
|
|
["lake", Vector3(lake.x - 30.0, 3.0, lake.y + 22.0), Vector3(lake.x, -2.0, lake.y)],
|
|
["forest", Vector3(-40, 1.7, 30), Vector3(-70, 4.0, 10)],
|
|
["rim", Vector3(0, 2.0, 70), Vector3(0, 25.0, 125)],
|
|
]
|
|
# Sea-cliff views: from the cliff top out to sea, and from the water back at the cliff.
|
|
var sa: float = terrain.sea_angle_deg
|
|
var top: Vector2 = terrain.coast_point(sa + 12.0, -6.0)
|
|
var out: Vector2 = terrain.coast_point(sa + 12.0, 200.0)
|
|
views.append(["seacliff_top", Vector3(top.x, 1.7, top.y), Vector3(out.x, terrain.sea_level - 6.0, out.y)])
|
|
var boat: Vector2 = terrain.coast_point(sa - 6.0, 38.0)
|
|
var face: Vector2 = terrain.coast_point(sa + 4.0, 0.0)
|
|
var boat_eye := Vector3(boat.x, terrain.sea_level + 3.0 - terrain.sample_height(boat.x, boat.y), boat.y)
|
|
views.append(["seacliff_from_sea", boat_eye, Vector3(face.x, terrain.sea_level + 8.0, face.y)])
|
|
# Along the cliff edge into the western headland (where the old pit was).
|
|
var walk: Vector2 = terrain.coast_point(sa - 20.0, -8.0)
|
|
var head: Vector2 = terrain.coast_point(sa - 52.0, 0.0)
|
|
views.append(["headland", Vector3(walk.x, 1.7, walk.y), Vector3(head.x, 2.0, head.y)])
|
|
await get_tree().create_timer(4.0).timeout
|
|
set_physics_process(false) # views may sit outside the walkable area
|
|
var cam: Camera3D = player.get_node("Camera3D")
|
|
for v in views:
|
|
if v[1] != null:
|
|
player.set_physics_process(false)
|
|
var eye: Vector3 = v[1]
|
|
eye.y += terrain.sample_height(eye.x, eye.z)
|
|
var dir: Vector3 = (v[2] as Vector3) - eye
|
|
player.global_position = eye - Vector3(0.0, cam.position.y, 0.0)
|
|
player.rotation = Vector3(0.0, atan2(-dir.x, -dir.z), 0.0)
|
|
cam.rotation.x = atan2(dir.y, Vector2(dir.x, dir.z).length())
|
|
props._update_grass(100000)
|
|
await get_tree().create_timer(1.0).timeout
|
|
await RenderingServer.frame_post_draw
|
|
var path := "%s_%s.png" % [prefix, v[0]]
|
|
get_viewport().get_texture().get_image().save_png(path)
|
|
print("[world] saved ", path)
|
|
get_tree().quit()
|