Merge branch 'gameplay-crossing-0.1.0': designated street crossing + review map fixes
This commit is contained in:
commit
1dbe737b85
41 files changed
+390
-110
No files matched your search
@@ -15,6 +15,8 @@
|
||||
# Every actor is (re)created by label, so rerunning the script rebuilds the map
|
||||
# deterministically instead of stacking duplicates.
|
||||
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
MAP = "/Game/Hollywood/DemoMap"
|
||||
@@ -22,6 +24,16 @@ RESULT = "/tmp/gigline/demo-map.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
# EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi
|
||||
# commandlets (they return without switching worlds, so staged actors land in
|
||||
# whatever world is current and get saved over the wrong map). The
|
||||
# LevelEditorSubsystem actually switches.
|
||||
LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
||||
|
||||
|
||||
def switch_level(path):
|
||||
return LES.load_level(path)
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
unreal.log(str(msg))
|
||||
@@ -51,11 +63,11 @@ def clear(prefixes=()):
|
||||
def build():
|
||||
# --- level ------------------------------------------------------------
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(MAP):
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP):
|
||||
if not switch_level(MAP):
|
||||
log(f"could not load {MAP}")
|
||||
return
|
||||
else:
|
||||
if not unreal.EditorLevelLibrary.new_level(MAP):
|
||||
if not switch_level(MAP):
|
||||
log(f"could not create {MAP}")
|
||||
return
|
||||
|
||||
@@ -83,7 +95,7 @@ def build():
|
||||
# --- site meshes, if they have been imported --------------------------
|
||||
imported = 0
|
||||
for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
|
||||
"Kerb_South", "Kerb_North"):
|
||||
"Kerb_South", "Kerb_North", "Crossing_Markings"):
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
||||
if mesh is None:
|
||||
continue
|
||||
@@ -103,6 +115,12 @@ def build():
|
||||
unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0))
|
||||
log("staged: Demo_PlayerStart on the north sidewalk facing south")
|
||||
|
||||
# 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.
|
||||
umap = os.path.join(unreal.Paths.project_dir(), MAP.replace("/Game/", "Content/") + ".umap")
|
||||
if os.path.exists(umap) and not os.access(umap, os.W_OK):
|
||||
os.chmod(umap, 0o644)
|
||||
log(f"map writable: chmod u+w {umap}")
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), MAP)
|
||||
log(f"save_map {MAP}: {saved}")
|
||||
|
||||
@@ -21,10 +21,34 @@ MAP_PATH = "/Game/Test/ReviewMap"
|
||||
lines = []
|
||||
|
||||
|
||||
# EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi
|
||||
# commandlets (they return without switching worlds, so staged actors land in
|
||||
# whatever world is current and get saved over the wrong map). The
|
||||
# LevelEditorSubsystem actually switches.
|
||||
LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
||||
|
||||
|
||||
def switch_level(path):
|
||||
return LES.load_level(path)
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
|
||||
|
||||
def ensure_writable(path):
|
||||
# A packed .umap left read-only (git sets 644->444 on checkout, or the
|
||||
# editor re-flags it) makes save_map silently return False with no error.
|
||||
# chmod u+w it so the staged camera list and actors actually persist.
|
||||
p = os.path.join(unreal.Paths.project_dir(), path.replace("/Game/", "Content/") + ".umap")
|
||||
try:
|
||||
mode = os.stat(p).st_mode & 0o777
|
||||
if mode & 0o200 == 0:
|
||||
os.chmod(p, 0o644)
|
||||
log(f"map writable: chmod u+w {p} (was {oct(mode)})")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def spawn(cls, loc, label=None, scale=None, rot=None):
|
||||
a = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc)
|
||||
if a is None:
|
||||
@@ -149,6 +173,18 @@ try:
|
||||
else:
|
||||
log("MISSING: /Script/GigLine.GigLineCharacter")
|
||||
|
||||
# --- NPC prefab -----------------------------------------------------------
|
||||
# The reusable NPC person: a collision capsule + visible MPFB body, standing
|
||||
# feet on Z 0, placed clear of the pawn so the prefab reads on its own.
|
||||
npc = None
|
||||
npc_class = unreal.load_class(None, "/Script/GigLine.GigLineNPCPerson")
|
||||
if npc_class:
|
||||
npc = spawn(npc_class, unreal.Vector(-4500, 300, 0), "NPC_Person")
|
||||
if npc:
|
||||
log("staged: NPC_Person (AGigLineNPCPerson) at Z 0")
|
||||
else:
|
||||
log("MISSING: /Script/GigLine.GigLineNPCPerson")
|
||||
|
||||
# --- camera list ---------------------------------------------------------
|
||||
# Every camera is a (name, location, target) triple; the look-at rotation is
|
||||
# computed from the pair so a camera cannot drift out of aim when one number
|
||||
@@ -162,6 +198,9 @@ try:
|
||||
("scale-cubes", unreal.Vector(0, -900, 200), unreal.Vector(0, 600, 80)),
|
||||
# the pawn the game spawns, with the grounded cube beside it
|
||||
("player-placement", unreal.Vector(320, -520, 140), unreal.Vector(0, 0, 80)),
|
||||
# the NPC prefab: capsule + visible body, feet on Z 0, at double scale
|
||||
# distance because it stands off to the west of the main staging
|
||||
("npc-prefab", unreal.Vector(-4200, 600, 160), unreal.Vector(-4500, 300, 90)),
|
||||
# low, close on feet and cube base: ground contact must be obvious
|
||||
("ground-contact", unreal.Vector(180, -260, 35), unreal.Vector(0, 40, 20)),
|
||||
# shadow comparison, low angle so both shadows read
|
||||
@@ -182,13 +221,33 @@ 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)),
|
||||
]
|
||||
# --- review shot cameras + save -------------------------------------------
|
||||
# Order matters: the review scene is staged in the default (unsaved) world.
|
||||
# Save it as ReviewMap BEFORE touching DemoMap — switching back to a map
|
||||
# that only exists in memory fails silently, and the final save would then
|
||||
# write the demo world over ReviewMap (this exact corruption happened once).
|
||||
shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
|
||||
if shots:
|
||||
shots.set_editor_property("shot_locations", [c[1] for c in cams])
|
||||
shots.set_editor_property(
|
||||
"shot_rotations",
|
||||
[unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams])
|
||||
shots.set_editor_property("shot_names", [c[0] for c in cams])
|
||||
shots.set_editor_property("quit_when_done", True)
|
||||
log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}")
|
||||
|
||||
ensure_writable(MAP_PATH)
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), MAP_PATH)
|
||||
log(f"save_map {MAP_PATH}: {saved}")
|
||||
|
||||
# --- 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")
|
||||
# floor does not. Load it, stage the shot cameras, save. No switch back:
|
||||
# the review map is already saved, and the run ends here.
|
||||
switch_level("/Game/Hollywood/DemoMap")
|
||||
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||
if actor.get_actor_label() == "ShotSequencer":
|
||||
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||
@@ -200,7 +259,7 @@ try:
|
||||
# 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)),
|
||||
("demo-crossing", unreal.Vector(-23750, -1800, 480), unreal.Vector(-24291, -1400, 300)),
|
||||
# the two north-side background buildings (TCL, Tussaud's) seen from
|
||||
# the south sidewalk, across Hollywood Blvd: their frontage and mass.
|
||||
("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)),
|
||||
@@ -214,36 +273,14 @@ try:
|
||||
# quit after the demo shots so a dedicated DemoMap capture run
|
||||
# terminates cleanly (render_screenshots.sh runs both maps).
|
||||
seq.set_editor_property("quit_when_done", True)
|
||||
ensure_writable("/Game/Hollywood/DemoMap")
|
||||
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])
|
||||
shots.set_editor_property(
|
||||
"shot_rotations",
|
||||
[unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams])
|
||||
shots.set_editor_property("shot_names", [c[0] for c in cams])
|
||||
shots.set_editor_property("quit_when_done", True)
|
||||
log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}")
|
||||
except Exception as exc: # noqa
|
||||
log(f"staging failed: {exc!r}")
|
||||
|
||||
# --- save --------------------------------------------------------------------
|
||||
|
||||
try:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(world, MAP_PATH)
|
||||
log(f"save_map {MAP_PATH}: {saved}")
|
||||
except Exception as exc: # noqa
|
||||
log(f"save_map failed: {exc!r}")
|
||||
|
||||
with open(RESULT, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
+18
-4
@@ -27,7 +27,10 @@ def log(msg):
|
||||
unreal.log(str(msg))
|
||||
|
||||
|
||||
def import_fbx(path, name, collision):
|
||||
def import_fbx(path, name, collision="complex"):
|
||||
# 5.8 note: there is no FBX import 'no collision' switch exposed to Python;
|
||||
# a default import simply generates no collision primitives, so collision="none"
|
||||
# means: skip the trace-flag step below and leave the body setup empty.
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = path
|
||||
task.destination_path = DEST
|
||||
@@ -58,10 +61,16 @@ def import_fbx(path, name, collision):
|
||||
# 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
|
||||
# UE 5.8 exposes this only through the body setup's trace flag (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.
|
||||
# the import path sets the body setup's trace flag, then verifies it by reading
|
||||
# it back.
|
||||
if collision == "none":
|
||||
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=none")
|
||||
return
|
||||
trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE
|
||||
if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX)
|
||||
try:
|
||||
@@ -83,6 +92,11 @@ def main():
|
||||
return
|
||||
for path in files:
|
||||
stem = os.path.splitext(os.path.basename(path))[0]
|
||||
# Crossing markings are paint on the road surface — no collision at all,
|
||||
# so the player never stands on a 1 cm lip at the band edges.
|
||||
if stem == "Crossing_Markings":
|
||||
import_fbx(path, stem, "none")
|
||||
continue
|
||||
collision = "complex" if stem in ("Terrain_DEM", "Road_Hollywood",
|
||||
"Sidewalk_South", "Sidewalk_North") else "simple"
|
||||
import_fbx(path, stem, collision)
|
||||
|
||||
Reference in new issue
Block a user