qa: make the demo walkable — terrain collision root cause, spawn, scripted QA
The reported fall-through was a misdiagnosis: the player never fell. The imported Terrain_DEM had only a bounding-box simple collision proxy, so a downward trace anywhere in the block hit the terrain at Z 819 (the mesh bbox max) and the street sat under an invisible 8 m lid. Standing on that box looked plausible, which is why it read as working. - add UGigLineAssetTools (C++): force CTF_UseComplexAsSimple on a static mesh. UE 5.8 dropped bUseComplexAsSimpleCollision and Python cannot set the flag; the builder also saves the asset, since the flag lives there and a map-only save silently reverts it. - spawn (-13336, -1150, 600): inside the carved corridor (the cut is a 10.7 m half-width around the road centreline), lands on the road at 456. - fix ResetToStart returning to the mid-air PlayerStart captured in BeginPlay: StartLocation is now captured on the first grounded frame. - HighResShot 1280x720 instead of "1280 720", which is a multiplier. - add -qaselftest: scripted walk/reset/pause on a core ticker plus a swept ground transect. Results: walk 746 cm/2 s with Z steady, reset 0 cm from spawn, pause/unpause 1/0, transect 84 samples 0 holes. - rebase onto main, keeping the frontage materials and placement fixes.
This commit is contained in:
1 parent
fee26ade10
commit
52b1212cf3
34 files changed
+591
-92
No files matched your search
@@ -51,3 +51,10 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| --- | --- |
|
||||
| `render_screenshots.py` / `render_screenshots.sh` | Rebuilds the review map, runs the game, captures the shot list into `Screenshots/` |
|
||||
| `judge_screenshot.py` | Vision-model pre-check: judges a screenshot against an expected-scene description (`--image` + `--expect`, or `--shots` for every shot in the README). Reads `OPENROUTER_API_KEY` from `.env` (ignored). Optional helper — a human must still approve. |
|
||||
|
||||
## Gameplay QA
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `UnrealEditor <uproject> /Game/Hollywood/DemoMap -game -qaselftest` | Headless behaviour check: drives forward input for 2 s, then resets and pauses. Logs `GigLine QA: ... PASS/FAIL` lines and a final `RESULT`. Runs only with `-qaselftest`, so normal play is unaffected. |
|
||||
| `GigLine SIM` / `GigLine GROUND` log lines | Emitted every second by `AGigLineCharacter` (TEMP QA): position, fall state, movement mode, and what the ground probe hits underneath. Remove before release. |
|
||||
+38
-11
@@ -146,7 +146,7 @@ def build():
|
||||
log(f"could not create {MAP}")
|
||||
return
|
||||
|
||||
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_"))
|
||||
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "ShotSequencer"))
|
||||
log(f"cleared {removed} previous demo actors")
|
||||
|
||||
# --- lighting: fixed daytime ------------------------------------------
|
||||
@@ -256,7 +256,7 @@ def build():
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade")
|
||||
if actor:
|
||||
actor.static_mesh_component.set_static_mesh(hero)
|
||||
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
|
||||
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
log("staged: Hero_Facade at the building line")
|
||||
else:
|
||||
log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py")
|
||||
@@ -301,13 +301,33 @@ def build():
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
||||
if mesh is None:
|
||||
continue
|
||||
# The imported terrain carries a bounding-box simple collision proxy,
|
||||
# which puts an invisible lid ~8 m over the whole block: the player
|
||||
# walks on that box instead of the street. Force trace against the real
|
||||
# triangles so the carved street corridor is actually open.
|
||||
if "Terrain" in name:
|
||||
asset = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
||||
if asset:
|
||||
unreal.GigLineAssetTools.set_complex_collision_as_simple(asset, True)
|
||||
# The flag lives on the asset, not the map: persist it or the
|
||||
# next session reloads the bbox proxy and the street is buried.
|
||||
saved_asset = unreal.EditorAssetLibrary.save_loaded_asset(asset)
|
||||
log(f"terrain collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} "
|
||||
f"(saved asset: {saved_asset})")
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
|
||||
if actor:
|
||||
actor.static_mesh_component.set_static_mesh(mesh)
|
||||
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
|
||||
comp = actor.static_mesh_component
|
||||
comp.set_static_mesh(mesh)
|
||||
# Immutable level geometry must be STATIC to reliably expose its
|
||||
# complex-as-simple collision body to CharacterMovement; staging
|
||||
# these MOVABLE let the player fall through the world.
|
||||
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
||||
comp.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
|
||||
comp.set_collision_object_type(unreal.CollisionChannel.ECC_WORLD_STATIC)
|
||||
comp.set_collision_profile_name("BlockAll")
|
||||
mat = built.get(mat_name)
|
||||
if mat:
|
||||
actor.static_mesh_component.set_material(0, mat)
|
||||
comp.set_material(0, mat)
|
||||
imported += 1
|
||||
log(f"staged {imported} site meshes with explicit surface materials")
|
||||
|
||||
@@ -362,13 +382,20 @@ def build():
|
||||
# --- player start: north sidewalk, facing the hero frontage -------------
|
||||
# Signs were measured from the imported meshes rather than assumed: the FBX
|
||||
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
|
||||
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447. The
|
||||
# facade stands at Y -1450. Stand on the north sidewalk and look north (-Y).
|
||||
# North sidewalk (Y -1912..-1053), facing south (+Y) across the road at the
|
||||
# hero on the south side. Yaw 90 looks along +Y.
|
||||
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1700.0, 120.0), "Demo_PlayerStart",
|
||||
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447.
|
||||
# The hero facade is on the SOUTH side at Y +1307. Stand on the north
|
||||
# sidewalk and face south (+Y, yaw 90) across the road at it.
|
||||
# CRITICAL: the terrain is only carved inside the street corridor, and
|
||||
# terrain_from_dem.py --corridor 10.7 is a HALF-width around the road
|
||||
# centreline (Y ~= -167), so the cut band is Y -1237..+902. A PlayerStart at
|
||||
# Y -1700 sits OUTSIDE it, under ~8 m of uncut terrain: the capsule spawns
|
||||
# embedded, depenetration pops it to Z ~909 (terrain top 819 + half-height
|
||||
# 87.5) and it never touches the sidewalk. Y -1150 is on the north sidewalk
|
||||
# (Y -1912..-1053) AND inside the cut, so the pavement is the ground.
|
||||
# Z 600 is clear air; the capsule drops onto the pavement and settles.
|
||||
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1150.0, 600.0), "Demo_PlayerStart",
|
||||
unreal.Rotator(pitch=0.0, yaw=90.0, roll=0.0))
|
||||
log("staged: Demo_PlayerStart on the north sidewalk facing south (+Y)")
|
||||
log("staged: Demo_PlayerStart on the cut north sidewalk, facing south (+Y)")
|
||||
|
||||
# A lockable LFS .umap checks out read-only, which makes save_map silently
|
||||
# return False — chmod u+w first or the staged actors never persist.
|
||||
|
||||
@@ -85,7 +85,11 @@ def build():
|
||||
# component and D/A carry strafe, combined by the character. A 2D value would
|
||||
# need per-key axis modifiers, and modifiers do not survive the save path here.
|
||||
ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_move.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
# Boolean (pressed = 1), not AXIS1D: the C++ handler reads Get<bool>() and
|
||||
# moves at full walk speed. An axis action from a keyboard key can deliver 0
|
||||
# depending on the trigger, which showed up as "character stands, W does not
|
||||
# move" in QA. WASD are four separate per-direction actions below.
|
||||
ia_move.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_move)
|
||||
|
||||
ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory)
|
||||
@@ -107,15 +111,15 @@ def build():
|
||||
# Two actions feed the one-dimensional move: IA_Move for forward/back, and a
|
||||
# second action for strafe, both reporting on the value's single component.
|
||||
ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_strafe)
|
||||
|
||||
ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_back.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
ia_back.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_back)
|
||||
|
||||
ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory)
|
||||
ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
|
||||
ia_left.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||
save(ia_left)
|
||||
|
||||
# Reset and pause are boolean: one press, no accumulator.
|
||||
|
||||
+12
-2
@@ -50,6 +50,12 @@ def import_fbx(path, name, collision="complex"):
|
||||
smd.convert_scene_unit = False
|
||||
smd.force_front_x_axis = False
|
||||
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
|
||||
# Generate collision at import. A default FBX static-mesh import creates NO
|
||||
# collision primitives, so CTF_USE_COMPLEX_AS_SIMPLE later has nothing to use
|
||||
# and the player falls through the walkable surfaces (observed at runtime:
|
||||
# ground trace under the spawn hits nothing). auto_generate_collision bakes
|
||||
# collision geometry into the mesh so the walkable plane actually blocks.
|
||||
smd.auto_generate_collision = True
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
@@ -71,8 +77,12 @@ def import_fbx(path, name, collision="complex"):
|
||||
log(f"{name:18} import ok size=({mx.x - mn.x:.0f}, {mx.y - mn.y:.0f}, {mx.z - mn.z:.0f}) cm "
|
||||
f"min_z={mn.z:.1f} collision=none")
|
||||
return
|
||||
trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE
|
||||
if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX)
|
||||
# Walkable surfaces block with SIMPLE + COMPLEX collision so the player's
|
||||
# capsule always has ground: CFG_USE_SIMPLE_AND_COMPLEX uses the auto-
|
||||
# generated simple collision AND the mesh triangles. (Complex-as-simple
|
||||
# alone only worked if the mesh carried complex collision, which a default
|
||||
# FBX static-mesh import does not.)
|
||||
trace = unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX
|
||||
try:
|
||||
body = mesh.get_editor_property("body_setup")
|
||||
body.set_editor_property("collision_trace_flag", trace)
|
||||
|
||||
Reference in new issue
Block a user