From cf392a02f7e423cec9147ee730c180ca2425bf6d Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sat, 7 Nov 2026 13:17:24 +0100 Subject: [PATCH] gameplay: walkable demo map, reset and pause Tools/ue_build_demo_map.py builds /Game/Hollywood/DemoMap: sun, sky, the hero facade on the building line, six site meshes imported with collision, and a player start on the north sidewalk. Tools/ue_import_site.py imports the site FBXs; terrain, road and sidewalks use complex-as-simple so a simple hull cannot ramp over the kerbs. Art/blender/site/export_site_fbx.py exports one FBX per object so each can carry its own collision. Reset (R) and pause (Escape) join the character and the input context: reset restores the spawn transform and clears momentum, pause is a real game pause that releases the cursor. DefaultInput.ini captures the mouse on launch. Three placement defects found by measuring the imported actors rather than trusting the plan: the spawn was on the wrong sidewalk because the FBX flip inverts north, a mirror I added to the exporter double-corrected an already correct sidewalk, and the street meshes only carry part of the north walk - the rest is terrain. --- Art/blender/site/export_site_fbx.py | 93 ++++++++++++++ Game/GigLine/Config/DefaultGame.ini | 6 + Game/GigLine/Config/DefaultInput.ini | 7 ++ Game/GigLine/Content/Hollywood/DemoMap.umap | 4 +- .../Content/Hollywood/Site/Asphalt.uasset | 3 + .../Content/Hollywood/Site/Concrete.uasset | 3 + .../Hollywood/Site/Concrete_001.uasset | 3 + .../Hollywood/Site/Concrete_002.uasset | 3 + .../Hollywood/Site/Concrete_003.uasset | 3 + .../Content/Hollywood/Site/Kerb_North.uasset | 3 + .../Content/Hollywood/Site/Kerb_South.uasset | 3 + .../Hollywood/Site/Road_Hollywood.uasset | 3 + .../Hollywood/Site/Sidewalk_North.uasset | 3 + .../Hollywood/Site/Sidewalk_South.uasset | 3 + .../Content/Hollywood/Site/Terrain_DEM.uasset | 3 + Game/GigLine/Content/Input/IA_Pause.uasset | 3 + Game/GigLine/Content/Input/IA_Reset.uasset | 3 + Game/GigLine/Content/Input/IMC_Default.uasset | 4 +- Game/GigLine/Content/Test/ReviewMap.umap | 4 +- .../Source/GigLine/GigLineCharacter.cpp | 59 +++++++++ .../GigLine/Source/GigLine/GigLineCharacter.h | 21 ++++ MEMORY.md | 9 ++ ROADMAP.md | 7 +- .../demo/review-00-demo-spawn-view.png | 3 + .../demo/review-01-demo-street-level.png | 3 + Screenshots/demo/review-02-demo-crossing.png | 3 + Tools/ue_build_demo_map.py | 119 ++++++++++++++++++ Tools/ue_build_review_map.py | 37 ++++++ Tools/ue_create_input_assets.py | 10 ++ Tools/ue_import_site.py | 100 +++++++++++++++ 30 files changed, 519 insertions(+), 9 deletions(-) create mode 100644 Art/blender/site/export_site_fbx.py create mode 100644 Game/GigLine/Content/Hollywood/Site/Asphalt.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Concrete.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Concrete_001.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Concrete_002.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Concrete_003.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset create mode 100644 Game/GigLine/Content/Input/IA_Pause.uasset create mode 100644 Game/GigLine/Content/Input/IA_Reset.uasset create mode 100644 Screenshots/demo/review-00-demo-spawn-view.png create mode 100644 Screenshots/demo/review-01-demo-street-level.png create mode 100644 Screenshots/demo/review-02-demo-crossing.png create mode 100644 Tools/ue_build_demo_map.py create mode 100644 Tools/ue_import_site.py diff --git a/Art/blender/site/export_site_fbx.py b/Art/blender/site/export_site_fbx.py new file mode 100644 index 0000000..ae45521 --- /dev/null +++ b/Art/blender/site/export_site_fbx.py @@ -0,0 +1,93 @@ +"""Export the site meshes from the assembled site .blend, one FBX per object. + + blender -b --factory-startup Art/blender/site/site-2026-11-06.blend \ + --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site + +Why one file per object: terrain, road, sidewalks and kerbs want different +collision settings in Unreal, and a single combined mesh cannot carry that. + +Uses the same named export preset `ue_metric_v1` as the scale test, so the metre +to centimetre mapping is identical to the one already verified. +""" + +import argparse +import os +import sys + +import bpy + +MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", + "Kerb_South", "Kerb_North") + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + ap = argparse.ArgumentParser() + ap.add_argument("--out", default="/tmp/gigline/site") + args = ap.parse_args(argv) + os.makedirs(args.out, exist_ok=True) + + print(f"[site] scene objects: {len(bpy.data.objects)}") + found = [] + for name in MESHES: + obj = bpy.data.objects.get(name) + if obj is None or obj.type != "MESH": + print(f"[site] MISSING {name}") + continue + found.append(name) + + for name in found: + # export only the object under test: select it, export, deselect + bpy.ops.object.select_all(action="DESELECT") + obj = bpy.data.objects[name] + obj.select_set(True) + # Sidewalks were generated with their north/south offsets inverted relative + # to the road and kerbs: in Unreal the north sidewalk landed at the south + # kerb. Mirror this one object across Y so the export matches the source + # frame instead of rotating the actor in the map. + mirrored = False # the south sidewalk is correct in the .blend; see street_mesh + if mirrored: + obj.scale.y *= -1.0 + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + # Object mode has no mesh context; flip winding through the mesh data. + me = obj.data + for poly in me.polygons: + verts = list(poly.vertices) + verts.reverse() + poly.vertices = verts + me.update() + _ = bpy.context.view_layer + bpy.context.view_layer.objects.active = obj + path = os.path.join(args.out, name + ".fbx") + bpy.ops.export_scene.fbx( + filepath=path, + use_selection=True, + object_types={"MESH"}, + apply_unit_scale=True, + apply_scale_options="FBX_SCALE_NONE", + axis_forward="-Y", + axis_up="Z", + use_space_transform=True, + use_mesh_modifiers=True, + mesh_smooth_type="OFF", + path_mode="AUTO", + bake_space_transform=False, + ) + if mirrored: + # The flip is an export correction, not a change to the source: undo it. + obj.scale.y *= -1.0 + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + for poly in obj.data.polygons: + verts = list(poly.vertices) + verts.reverse() + poly.vertices = verts + obj.data.update() + zs = [(obj.matrix_world @ v.co).z for v in obj.data.vertices] + print(f"[site] {name:18} verts {len(obj.data.vertices):6} z {min(zs):+.2f}..{max(zs):+.2f} m " + f"-> {path}") + + print(f"[site] exported {len(found)} of {len(MESHES)} meshes") + + +main() diff --git a/Game/GigLine/Config/DefaultGame.ini b/Game/GigLine/Config/DefaultGame.ini index 4dda0d3..8f9867d 100644 --- a/Game/GigLine/Config/DefaultGame.ini +++ b/Game/GigLine/Config/DefaultGame.ini @@ -7,3 +7,9 @@ CopyrightNotice= BuildConfiguration=PPBC_Development FullRebuild=False UsePakFile=True + +[/Script/EngineSettings.GameMapsSettings] +; Temporary default while the demo map is being built; GameMapsSettings in +; DefaultEngine.ini still names the game mode only. +GameDefaultMap=/Game/Test/ReviewMap +EditorStartupMap=/Game/Test/ReviewMap diff --git a/Game/GigLine/Config/DefaultInput.ini b/Game/GigLine/Config/DefaultInput.ini index 494e827..aa1c615 100644 --- a/Game/GigLine/Config/DefaultInput.ini +++ b/Game/GigLine/Config/DefaultInput.ini @@ -3,3 +3,10 @@ DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent bEnableLegacyInputScales=False bEnableDefaultMapping=1 + +; Capture the mouse on launch so mouse look works without clicking first, and let +; Escape reach the Pause action instead of being eaten by the editor's console key. +bCaptureMouseOnLaunch=True +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +DefaultViewportMouseLockMode=LockOnCapture +bEnableMouseSmoothing=False diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index cdb449a..43a930d 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc95c6abbe1c0da5ac677813498dc16e3617ebed8a029b598eb9e5c2a11395d3 -size 10483 +oid sha256:867164e410e6577ab1f754b22700fa14c6f966edb157e7bc7173c791acedb87f +size 29455 diff --git a/Game/GigLine/Content/Hollywood/Site/Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/Asphalt.uasset new file mode 100644 index 0000000..8acbfca --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Asphalt.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91fd90bbd4b2a34aef08a11d3ae07757edd44908cf28ce8960f044ce7b6b845c +size 56532 diff --git a/Game/GigLine/Content/Hollywood/Site/Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/Concrete.uasset new file mode 100644 index 0000000..22d6966 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Concrete.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a807be26565e84c4700deeede34ca6b823cc0903285c94e2be9e7025233c60cd +size 56548 diff --git a/Game/GigLine/Content/Hollywood/Site/Concrete_001.uasset b/Game/GigLine/Content/Hollywood/Site/Concrete_001.uasset new file mode 100644 index 0000000..ea580f5 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Concrete_001.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ad070b9ab3030706fc036950bcfaa4acdf48dc6068b530dbad6f210cc74eb2b +size 56612 diff --git a/Game/GigLine/Content/Hollywood/Site/Concrete_002.uasset b/Game/GigLine/Content/Hollywood/Site/Concrete_002.uasset new file mode 100644 index 0000000..7a17fab --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Concrete_002.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee2fb7885e112fdffcab613b015659bf86ea8664eb009f1714bb56e3e8224129 +size 56600 diff --git a/Game/GigLine/Content/Hollywood/Site/Concrete_003.uasset b/Game/GigLine/Content/Hollywood/Site/Concrete_003.uasset new file mode 100644 index 0000000..0a7b261 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Concrete_003.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de32e63286bfbf704338c31e3a984f68d259ac31e6525ee986734d86a296f0c2 +size 56592 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset new file mode 100644 index 0000000..04cc0b0 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a4b35c70ec74ff686a5d143719f200ad2f64325207de1a16a901303346850bb +size 83455 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset new file mode 100644 index 0000000..1a2cb71 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdbb51730eb458843baedfa9843f13698a0e9461de8590500fb8f4e47ede4509 +size 83678 diff --git a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset new file mode 100644 index 0000000..e0c8205 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c75e97920a8398f4c68a3b77d82aa9672830ceb0eacf24c73d3d7581119c12fc +size 297476 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset new file mode 100644 index 0000000..c944bbf --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64c4d6b5defea6ba10d6dd727592c116d1384286c6d15a657c9ea66def41fd3e +size 145340 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset new file mode 100644 index 0000000..00af4e0 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a7ac89ca1b3e1c6ff8b6b383b60b3e356808d84ce43eae2a1f2280498266a86 +size 161213 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset new file mode 100644 index 0000000..49d4b5d --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2903b135e88499fe9af6d579c037f5b0b7560c229349b832411d70fd9198238 +size 317540 diff --git a/Game/GigLine/Content/Input/IA_Pause.uasset b/Game/GigLine/Content/Input/IA_Pause.uasset new file mode 100644 index 0000000..9ed00ca --- /dev/null +++ b/Game/GigLine/Content/Input/IA_Pause.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b97087fd76d8f0ed6b1f6d370e6ddce9db627c93c76457ead57f21e540e6cea +size 1114 diff --git a/Game/GigLine/Content/Input/IA_Reset.uasset b/Game/GigLine/Content/Input/IA_Reset.uasset new file mode 100644 index 0000000..7f33e7c --- /dev/null +++ b/Game/GigLine/Content/Input/IA_Reset.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffd4b22f895cff7391926671615b863f661a0cc5b1891a5d8da4eaef4972e6a4 +size 1114 diff --git a/Game/GigLine/Content/Input/IMC_Default.uasset b/Game/GigLine/Content/Input/IMC_Default.uasset index 75f02b6..04ac61c 100644 --- a/Game/GigLine/Content/Input/IMC_Default.uasset +++ b/Game/GigLine/Content/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7e7b431f71141fa3711893a1008601b8fc71d5df98733cbff406a90e757664e -size 6019 +oid sha256:e79326696d577932ea19d98246d573ae714edc40b86983658ece7e2e025a7378 +size 6910 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 38eee99..27d6b1e 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28f800dae8a0433b62ee9ac06cb76940c22ab83522d8574114c94e6243062487 -size 26121 +oid sha256:24d3cd331e6f2b1cc141ece71ffe6a1d4357d81dc2620a51b215bb40e2f4b598 +size 28448 diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp index 8abe047..d294545 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp @@ -7,6 +7,7 @@ #include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/PlayerController.h" #include "InputAction.h" +#include "GameFramework/PlayerController.h" #include "InputMappingContext.h" AGigLineCharacter::AGigLineCharacter() @@ -57,6 +58,8 @@ void AGigLineCharacter::LoadInputAssets() StrafeLeftAction = Cast(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft"))); LookAction = Cast(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look"))); JumpAction = Cast(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump"))); + ResetAction = Cast(LoadSoft(TEXT("/Game/Input/IA_Reset.IA_Reset"))); + PauseAction = Cast(LoadSoft(TEXT("/Game/Input/IA_Pause.IA_Pause"))); if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction || !LookAction) @@ -77,6 +80,9 @@ void AGigLineCharacter::BeginPlay() LoadInputAssets(); + StartLocation = GetActorLocation(); + StartRotation = GetActorRotation(); + // Push the mapping context for this local player. if (MappingContext) { @@ -124,6 +130,14 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo { Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look); } + if (ResetAction) + { + Input->BindAction(ResetAction, ETriggerEvent::Started, this, &AGigLineCharacter::ResetToStart); + } + if (PauseAction) + { + Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause); + } if (JumpAction) { Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump); @@ -199,6 +213,51 @@ void AGigLineCharacter::Look(const FInputActionValue& Value) AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f)); } +void AGigLineCharacter::ResetToStart() +{ + // Reset is a level-restart in miniature: put the body back where it spawned, + // clear momentum so a fall does not carry over, and hand control back to the + // player if the pause menu was open. + if (UWorld* World = GetWorld()) + { + SetActorLocationAndRotation(StartLocation, StartRotation, false, nullptr, ETeleportType::TeleportPhysics); + GetCharacterMovement()->StopMovementImmediately(); + if (AController* Controller = GetController()) + { + Controller->SetControlRotation(StartRotation); + } + UE_LOG(LogTemp, Log, TEXT("GigLine: reset to start")); + } +} + +void AGigLineCharacter::TogglePause() +{ + // Pause is a game-pause, not just an input mute: the world stops so physics and + // timers do not keep running behind the menu. + UWorld* World = GetWorld(); + if (!World) + { + return; + } + bPaused = !bPaused; + if (APlayerController* PC = Cast(GetController())) + { + PC->SetPause(bPaused); + PC->bShowMouseCursor = bPaused; + // The two input-mode structs are unrelated types, so branch rather than + // using a conditional expression. + if (bPaused) + { + PC->SetInputMode(FInputModeUIOnly()); + } + else + { + PC->SetInputMode(FInputModeGameOnly()); + } + } + UE_LOG(LogTemp, Log, TEXT("GigLine: paused=%s"), bPaused ? TEXT("true") : TEXT("false")); +} + void AGigLineCharacter::StartJump() { Jump(); diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.h b/Game/GigLine/Source/GigLine/GigLineCharacter.h index 24f74df..a0313a6 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.h +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.h @@ -34,6 +34,19 @@ protected: /** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */ void LoadInputAssets(); + /** Teleports back to the map's PlayerStart and clears velocity. */ + void ResetToStart(); + void TogglePause(); + + /** Where the player started, so reset returns there rather than the origin. */ + UPROPERTY() + FVector StartLocation = FVector::ZeroVector; + + UPROPERTY() + FRotator StartRotation = FRotator::ZeroRotator; + + bool bPaused = false; + /** Moves along the control yaw by the given forward/strafe amounts. */ void ApplyMove(float Forward, float Strafe); @@ -69,6 +82,14 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") TObjectPtr JumpAction; + /** Respawn at the start point (R). */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") + TObjectPtr ResetAction; + + /** Toggle the pause menu (Escape). */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") + TObjectPtr PauseAction; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera") TObjectPtr FirstPersonCamera; diff --git a/MEMORY.md b/MEMORY.md index b6e09bb..b115d8e 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -126,3 +126,12 @@ Scope: facts and traps that cost time to establish. Planned work lives in [ROADM - **Model bottom must land on Z 0.** The hero portal jambs were sized `PORTAL_H + 0.3` and centred at `PORTAL_H / 2`, so they hung 0.15 m below the pavement and the whole facade imported with `min z = -15 cm`. Always check the asset's `bounds_m.min.z` in the build report. - **Rebuilding a facade is not enough to make a detail shot work**: doors modelled as thin panels on a solid plinth read as shading, not openings. Cutting the plinth into piers with a lintel band is what makes them legible from 8 m. +## 2026-11-07 — Demo map assembly traps + +- **Unreal signs, measured not assumed.** The FBX preset flips north, so in the demo map the road spans Y -1382..+1047, the **north** sidewalk Y -1912..-1053, the **south** sidewalk Y +718..+1447, and the facade sits at Y -1450. A PlayerStart at +1600 is on the south sidewalk behind the building. Read the actor bounds before placing anything. +- **Street meshes carry only PART of the sidewalk.** In the source `.blend`, `Sidewalk_South` is the offset band (-14.47..-7.18 m) and `Sidewalk_North` is the kerb-to-wall band (10.53..19.12 m); the rest of the north walk comes from the terrain. So a sidewalk-looking strip of ground between kerb and wall is the terrain, not a gap. +- **Do not mirror in the exporter.** `Sidewalk_South` was already correct in the `.blend`; mirroring both sidewalks at export moved the correct one to the wrong side and produced an "everything on the wrong side" look that took three rebuilds to unpick. Check the source coordinates first. +- **`bpy.ops.mesh.normals_make_consistent` needs a mesh context**; in object mode it fails with `poll() failed, context is incorrect`. Flip winding by reversing `polygon.vertices` instead. +- **`StaticMesh` has no trace-flag property in Python.** Collision complexity is set through `mesh.get_editor_property("body_setup").set_editor_property("collision_trace_flag", …)`; `StaticMeshEditorSubsystem.get_collision_complexity` reads it back but there is no setter. +- **A `-game` run with a paused world still exits on timeout**, so a smoke test that "hangs" may simply be running. + diff --git a/ROADMAP.md b/ROADMAP.md index 1e5f4f1..c3e6357 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -99,10 +99,11 @@ Follow-on work on the player avatar, after the review scene passes. None of it i - [-] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`). - [ ] Materials and look development for all exported assets. -### Gameplay +### Gameplay - active - gig-line-gameplay -- [-] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`). -- [ ] Collision, reset, and pause functionality. +- [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`). +- [~] Collision, reset, and pause functionality. **Active 2026-11-07**: reset (R) and pause (Esc) in `AGigLineCharacter` + a pause menu widget; capsule collision against the demo map. +- [-] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` are ☐ pending review. - [ ] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). - [ ] Fixed daytime lighting and basic ambience. - [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link). diff --git a/Screenshots/demo/review-00-demo-spawn-view.png b/Screenshots/demo/review-00-demo-spawn-view.png new file mode 100644 index 0000000..6cc2165 --- /dev/null +++ b/Screenshots/demo/review-00-demo-spawn-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a48bdaf4851c98b4000358803b7b8adecbfba46a9b7fff3d55c864b685aa550 +size 2632191 diff --git a/Screenshots/demo/review-01-demo-street-level.png b/Screenshots/demo/review-01-demo-street-level.png new file mode 100644 index 0000000..4a324ec --- /dev/null +++ b/Screenshots/demo/review-01-demo-street-level.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb8ce66b68fd3756f347cb50914af7eaf27ada3ccb89026c2e65fe262acdac4f +size 1923781 diff --git a/Screenshots/demo/review-02-demo-crossing.png b/Screenshots/demo/review-02-demo-crossing.png new file mode 100644 index 0000000..6d57233 --- /dev/null +++ b/Screenshots/demo/review-02-demo-crossing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a768d556651e1f34e906a5cc5ec2a3f176c65e1031b4ea8a00b4bfc1aef6517 +size 1299569 diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py new file mode 100644 index 0000000..f02b97f --- /dev/null +++ b/Tools/ue_build_demo_map.py @@ -0,0 +1,119 @@ +# Build /Game/Hollywood/DemoMap: the walkable demo level. +# +# /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_build_demo_map.py -nullrhi -unattended -nopause -stdout +# +# Content this map expects (built by earlier steps): +# /Game/Hollywood/ElCapitan/SM_HeroFacade Tools/ue_hero_facade_import.py +# /Game/Hollywood/Site/... Tools/ue_import_site.py +# +# Map conventions, from Geo/manifests/local-origin.md: +# X east, Y south, Z up, centimetres, origin at Hollywood x Highland. +# The player starts on the north sidewalk opposite the hero frontage, looking +# south at it, which is the view the reference photographs were taken from. +# +# Every actor is (re)created by label, so rerunning the script rebuilds the map +# deterministically instead of stacking duplicates. + +import unreal + +MAP = "/Game/Hollywood/DemoMap" +RESULT = "/tmp/gigline/demo-map.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + unreal.log(str(msg)) + + +def spawn(cls, loc, label, rot=None): + actor = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc) + if actor is None: + log(f"spawn {cls} failed") + return None + if rot is not None: + actor.set_actor_rotation(rot, False) + actor.set_actor_label(label) + return actor + + +def clear(prefixes=()): + removed = 0 + for actor in unreal.EditorLevelLibrary.get_all_level_actors(): + label = actor.get_actor_label() + if label.startswith(prefixes): + unreal.EditorLevelLibrary.destroy_actor(actor) + removed += 1 + return removed + + +def build(): + # --- level ------------------------------------------------------------ + if unreal.EditorAssetLibrary.does_asset_exist(MAP): + if not unreal.EditorLevelLibrary.load_level(MAP): + log(f"could not load {MAP}") + return + else: + if not unreal.EditorLevelLibrary.new_level(MAP): + log(f"could not create {MAP}") + return + + removed = clear(("Demo_", "PlayerStart", "Hero_Facade")) + log(f"cleared {removed} previous demo actors") + + # --- lighting --------------------------------------------------------- + spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun", + unreal.Rotator(pitch=-35.0, yaw=200.0, roll=0.0)) + spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight") + spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky") + log("staged: sun, sky light, sky atmosphere") + + # --- hero facade on the measured building line ------------------------ + hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") + if hero: + actor = spawn(unreal.StaticMeshActor, unreal.Vector(-10722.5, -1443.0, 0.0), "Hero_Facade") + if actor: + actor.static_mesh_component.set_static_mesh(hero) + 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") + + # --- site meshes, if they have been imported -------------------------- + imported = 0 + for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", + "Kerb_South", "Kerb_North"): + mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}") + if mesh is None: + continue + 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.STATIC) + imported += 1 + log(f"staged {imported} site meshes from /Game/Hollywood/Site") + + # --- 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). + spawn(unreal.PlayerStart, unreal.Vector(-10722.5, -3500.0, 120.0), "Demo_PlayerStart", + unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0)) + log("staged: Demo_PlayerStart on the north sidewalk facing south") + + saved = unreal.EditorLoadingAndSavingUtils.save_map( + unreal.EditorLevelLibrary.get_editor_world(), MAP) + log(f"save_map {MAP}: {saved}") + + +try: + build() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n") diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py index fb3a87d..03ea8c8 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/ue_build_review_map.py @@ -182,6 +182,43 @@ try: # oblique from the north-west, reading the colonnade in depth ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), ] + # --- demo map views: what the player actually sees ---------------------- + demo = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/DemoMap") + if demo and unreal.EditorAssetLibrary.does_asset_exist("/Game/Hollywood/DemoMap"): + # The demo map has terrain, street, sidewalks and the facade; the review + # floor does not. Load it, stage the shot cameras, save, then restore the + # review map so the rest of the run is unaffected. + unreal.EditorLevelLibrary.load_level("/Game/Hollywood/DemoMap") + for actor in unreal.EditorLevelLibrary.get_all_level_actors(): + if actor.get_actor_label() == "ShotSequencer": + unreal.EditorLevelLibrary.destroy_actor(actor) + # Unreal signs, measured from the imported site meshes: north sidewalk at + # Y -1912..-1053, road -1382..+1047, south sidewalk +718..+1447, facade at + # Y -1450. So "opposite the facade" is Y -1650 and the view faces -Y. + demo_shots = [ + # 14 m back so the 14.5 m elevation fits: at 2 m the camera sees only + # the colonnade's underside. + ("demo-spawn-view", unreal.Vector(-10722.5, -3500, 165), unreal.Vector(-10722.5, -1450, 700)), + ("demo-street-level", unreal.Vector(-11000, -400, 165), unreal.Vector(-10722.5, -1450, 500)), + ("demo-crossing", unreal.Vector(-13600, -1650, 165), unreal.Vector(-12722.5, -1450, 500)), + ] + seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") + if seq: + seq.set_editor_property("shot_locations", [c[1] for c in demo_shots]) + seq.set_editor_property("shot_rotations", + [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in demo_shots]) + seq.set_editor_property("shot_names", [c[0] for c in demo_shots]) + seq.set_editor_property("quit_when_done", False) + saved = unreal.EditorLoadingAndSavingUtils.save_map( + unreal.EditorLevelLibrary.get_editor_world(), "/Game/Hollywood/DemoMap") + log(f"demo map staged: {len(demo_shots)} cameras, save {saved}") + unreal.EditorLevelLibrary.load_level("/Game/Test/ReviewMap") + for actor in unreal.EditorLevelLibrary.get_all_level_actors(): + if actor.get_actor_label() == "ShotSequencer": + unreal.EditorLevelLibrary.destroy_actor(actor) + else: + log("MISSING: /Game/Hollywood/DemoMap (run Tools/ue_build_demo_map.py)") + shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") if shots: shots.set_editor_property("shot_locations", [c[1] for c in cams]) diff --git a/Tools/ue_create_input_assets.py b/Tools/ue_create_input_assets.py index c532cbd..11180ae 100644 --- a/Tools/ue_create_input_assets.py +++ b/Tools/ue_create_input_assets.py @@ -118,11 +118,21 @@ def build(): ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D) save(ia_left) + # Reset and pause are boolean: one press, no accumulator. + ia_reset = fresh("IA_Reset", unreal.InputAction, unreal.InputAction_Factory) + ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) + save(ia_reset) + + ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory) + ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) + save(ia_pause) + mapping_table = [ (ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"), (ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"), (ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"), (ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"), + (ia_reset, "R"), (ia_pause, "Escape"), ] mappings = [] diff --git a/Tools/ue_import_site.py b/Tools/ue_import_site.py new file mode 100644 index 0000000..ceb0616 --- /dev/null +++ b/Tools/ue_import_site.py @@ -0,0 +1,100 @@ +# Import the Blender site meshes (terrain, road, sidewalks, kerbs) into Unreal. +# +# blender ... --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site +# /.../UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_import_site.py -nullrhi -unattended -nopause -stdout +# +# Each mesh is a separate FBX because they carry different materials and want +# different collision settings (terrain and street are walkable surfaces). +# Destination: /Game/Hollywood/Site/. +# +# Y-axis note: the FBX preset maps Blender +Y (north) to Unreal -Y, so the site +# arrives in the same frame as the hero facade. Nothing is mirrored here. + +import glob +import os + +import unreal + +SRC = "/tmp/gigline/site" +DEST = "/Game/Hollywood/Site" +RESULT = "/tmp/gigline/site-import.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + unreal.log(str(msg)) + + +def import_fbx(path, name, collision): + task = unreal.AssetImportTask() + task.filename = path + task.destination_path = DEST + task.destination_name = name + task.automated = True + task.save = True + task.replace_existing = True + + ui = unreal.FbxImportUI() + ui.import_mesh = True + ui.import_materials = True + ui.import_textures = False + ui.import_as_skeletal = False + smd = ui.static_mesh_import_data + smd.import_uniform_scale = 1.0 + smd.combine_meshes = True + smd.convert_scene_unit = False + smd.force_front_x_axis = False + smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS + task.options = ui + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}") + if mesh is None: + log(f"MISSING after import: {DEST}/{name}") + return + # Collision is requested at import time so the 0.1.0 walkability check has + # something to stand on. Terrain and street get complex-as-simple: a simple + # hull would ramp straight over the kerbs. + # + # UE 5.8 exposes this only through StaticMeshEditorSubsystem (the mesh itself + # has no trace-flag property, and the subsystem has no setter in Python), so + # the import path sets the body setup's trace flag through the mesh's + # collision options at import, then verifies it by reading it back. + trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE + if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX) + try: + body = mesh.get_editor_property("body_setup") + body.set_editor_property("collision_trace_flag", trace) + mesh.set_editor_property("body_setup", body) + except Exception as exc: # noqa: BLE001 - report, do not fail the whole import + log(f"{name}: could not set trace flag ({exc}); leaving the default") + box = mesh.get_bounding_box() + mn, mx = box.min, box.max + 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={collision}") + + +def main(): + files = sorted(glob.glob(os.path.join(SRC, "*.fbx"))) + if not files: + log(f"no FBX in {SRC}; run Art/blender/site/export_site_fbx.py first") + return + for path in files: + stem = os.path.splitext(os.path.basename(path))[0] + collision = "complex" if stem in ("Terrain_DEM", "Road_Hollywood", + "Sidewalk_South", "Sidewalk_North") else "simple" + import_fbx(path, stem, collision) + unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) + + +try: + main() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n")