Demos for scee script and player controller
This commit is contained in:
@@ -44,6 +44,37 @@ cd build-vscode/src/features/editScene
|
||||
# Run only matching test steps (substring match on step name)
|
||||
TERRAIN_TEST_FILTER=terrainCompliance ./editSceneEditor --headless --run-terrain-tests=1
|
||||
|
||||
# Demo: game-mode SceneScriptComponent inline-Lua event handling
|
||||
# (scene_loaded / prefab_loaded / custom "demo_pulse"; watch stdout for
|
||||
# [demo] lines and the cubes moving/scaling). Lives in
|
||||
# demos/demo-lua-scene-script and is self-contained in its own build
|
||||
# directory (resources.cfg copied; resources/, characters/, lua-scripts/
|
||||
# symlinked from the editScene binary dir; prefabs/demo_pulse_cube.json
|
||||
# copied). Requires the editSceneEditor staging to have run once.
|
||||
cd demos/demo-lua-scene-script
|
||||
./demoLuaSceneScript
|
||||
# ...or headless smoke run (one frame, then exit):
|
||||
./demoLuaSceneScript --headless --exit-after-first-frame
|
||||
# Controls (no player controller in the demo scene; cursor is not grabbed):
|
||||
# RMB drag = look, W/A/S/D = move, Q/E = down/up, Shift = boost,
|
||||
# Escape = built-in game-mode pause menu toggle.
|
||||
|
||||
# Demo: game-mode PlayerControllerSystem on a flat physical floor
|
||||
# (demos/demo-character-controller, target demoCharacterController). The
|
||||
# scene (demo_character_controller.json) has a flat colored floor plane
|
||||
# (created programmatically as "DemoFloorPlane" in demo_main.cpp) with a
|
||||
# static box collider, a character spawner ("s1", character registry ID 2,
|
||||
# same player character setup as town8.json, prefab prefabs/char_2.json)
|
||||
# and a PlayerControllerComponent targeting it. The initial window is
|
||||
# 1920x1080 and the mouse stays grabbed while Playing.
|
||||
cd demos/demo-character-controller
|
||||
./demoCharacterController
|
||||
# ...or headless smoke run (one frame, then exit):
|
||||
./demoCharacterController --headless --exit-after-first-frame
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
|
||||
# Road wedge/segment self-intersection regression test (no scene needed,
|
||||
# also registered as CTest roadGeometryOverlapTest)
|
||||
./road_geometry_overlap_test
|
||||
@@ -364,7 +395,9 @@ Attaches a Lua script to a scene or prefab entity. Fields:
|
||||
- `sendSceneLoaded(scenePath)` sends `"scene_loaded"` with param `scenePath`;
|
||||
`sendPrefabLoaded(prefabPath, entityId)` sends `"prefab_loaded"` with
|
||||
params `prefabPath` and `entityId` (both via `EventBus` +
|
||||
`editScene::EventParams`).
|
||||
`editScene::EventParams`). `entityId` (and any `ENTITY_ID` event param)
|
||||
is delivered to Lua handlers mapped into the `ecs.*` entity ID space, so
|
||||
handlers can pass it straight to `ecs.get_component` etc.
|
||||
|
||||
Wiring call sites (each calls `loadPendingScripts` then sends the event):
|
||||
|
||||
@@ -380,6 +413,19 @@ Wiring call sites (each calls `loadPendingScripts` then sends the event):
|
||||
|
||||
Lua binding: `ecs.get_component`/`set_component` with `"SceneScript"`
|
||||
(`scriptPath`, `inlineScript`). Example: `lua-examples/scene_script_example.lua`.
|
||||
A runnable game-mode demo with inline scripts lives in
|
||||
`demos/demo-lua-scene-script` (target `demoLuaSceneScript`): the scene-level
|
||||
script catches `scene_loaded` and a custom `demo_pulse` event, while the demo
|
||||
prefab's own inline script also catches `prefab_loaded`. Note that
|
||||
scene-level scripts execute lazily at the first `loadPendingScripts()` call
|
||||
inside `PrefabSystem::resolveInstances()`, so they DO see the `prefab_loaded`
|
||||
events of prefabs instantiated during the same `startNewGame()` load; the
|
||||
known exception is `startup_menu.json`, whose scripts only run later in
|
||||
`EditorApp::setup()` (after Lua init), missing the menu's prefab events.
|
||||
|
||||
Note: setting the `Transform` component from Lua (`ecs.set_component` /
|
||||
`ecs.set_field`) applies the new values to the scene node and bumps the
|
||||
change-tracking version, so script-side moves/scales are visible immediately.
|
||||
|
||||
### Save/Load
|
||||
|
||||
|
||||
@@ -887,3 +887,7 @@ add_custom_command(TARGET editSceneEditor POST_BUILD
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/prefabs"
|
||||
COMMENT "Copying resources to editSceneEditor build directory"
|
||||
)
|
||||
|
||||
# Demos (separate executables reusing the editScene sources)
|
||||
add_subdirectory(demos/demo-lua-scene-script)
|
||||
add_subdirectory(demos/demo-character-controller)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# demo-character-controller — game-mode demo for PlayerControllerSystem
|
||||
# ---------------------------------------------------------------------------
|
||||
# Separate executable built from the same sources as editSceneEditor (minus
|
||||
# main.cpp). It runs in game mode and loads demo_character_controller.json,
|
||||
# which contains a flat colored floor plane (procedurally created in
|
||||
# demo_main.cpp as "DemoFloorPlane") with a static box collider, a character
|
||||
# spawner ("s1", character registry ID 2, same player character setup as
|
||||
# town8.json) and a PlayerControllerComponent targeting it. The mouse is
|
||||
# grabbed while Playing (built-in game-mode behaviour) and the initial
|
||||
# window is larger than the default (1920x1080, see DemoApp in
|
||||
# demo_main.cpp).
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# step (stage_runtime.cmake) copies resources.cfg, the demo scene, the
|
||||
# character prefab (prefabs/char_2.json, written by a previous editor/game
|
||||
# run) and any runtime config JSONs here, and symlinks the big pre-staged
|
||||
# runtime directories (resources/, characters/, lua-scripts/) from the
|
||||
# editScene binary directory. Run it from here:
|
||||
# cd <build>/src/features/editScene/demos/demo-character-controller
|
||||
# ./demoCharacterController
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
get_filename_component(EDITSCENE_SOURCE_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
||||
get_filename_component(EDITSCENE_BINARY_DIR
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/../.." ABSOLUTE)
|
||||
|
||||
# Reuse the editScene sources, swapping main.cpp for demo_main.cpp.
|
||||
set(DEMO_SOURCES ${EDITSCENE_SOURCES})
|
||||
list(REMOVE_ITEM DEMO_SOURCES main.cpp)
|
||||
list(TRANSFORM DEMO_SOURCES PREPEND "${EDITSCENE_SOURCE_DIR}/")
|
||||
|
||||
add_executable(demoCharacterController
|
||||
demo_main.cpp
|
||||
${DEMO_SOURCES}
|
||||
)
|
||||
|
||||
add_dependencies(demoCharacterController morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing (same as editor)
|
||||
target_compile_definitions(demoCharacterController PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(demoCharacterController
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
OgreProcedural::OgreProcedural
|
||||
RecastNavigation::Recast
|
||||
RecastNavigation::Detour
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
target_include_directories(demoCharacterController PRIVATE
|
||||
${EDITSCENE_SOURCE_DIR}
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/FastNoiseLite
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# Stage the standalone runtime next to the executable (see header comment).
|
||||
add_custom_command(TARGET demoCharacterController POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DDEMO_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-DEDITSCENE_BIN=${EDITSCENE_BINARY_DIR}
|
||||
-DEDITSCENE_SRC=${EDITSCENE_SOURCE_DIR}
|
||||
-DSRC_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/stage_runtime.cmake"
|
||||
COMMENT "Staging demo-character-controller standalone runtime"
|
||||
)
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"entities": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"name": "demo_light"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 10.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"lightType": "directional",
|
||||
"diffuseColor": {
|
||||
"r": 1.0,
|
||||
"g": 1.0,
|
||||
"b": 1.0,
|
||||
"a": 1.0
|
||||
},
|
||||
"specularColor": {
|
||||
"r": 0.5,
|
||||
"g": 0.5,
|
||||
"b": 0.5,
|
||||
"a": 1.0
|
||||
},
|
||||
"direction": {
|
||||
"x": 0.3,
|
||||
"y": -1.0,
|
||||
"z": 0.2
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"castShadows": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoFloorPlane",
|
||||
"visible": true
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "static",
|
||||
"mass": 1.0,
|
||||
"friction": 0.8,
|
||||
"restitution": 0.0,
|
||||
"isSensor": false,
|
||||
"enabled": true
|
||||
},
|
||||
"collider": {
|
||||
"shapeType": "box",
|
||||
"parameters": {
|
||||
"x": 30.0,
|
||||
"y": 0.1,
|
||||
"z": 30.0
|
||||
},
|
||||
"radius": 0.5,
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": -0.1,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -3.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
"registryId": 2,
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"playerController": {
|
||||
"actuatorColor": [
|
||||
0.0,
|
||||
0.4000000059604645,
|
||||
1.0
|
||||
],
|
||||
"actuatorCooldown": 1.5,
|
||||
"actuatorDistance": 25.0,
|
||||
"actuatorLabelFontSize": 12.0,
|
||||
"cameraMode": 0,
|
||||
"distantCircleRadius": 8.0,
|
||||
"fpsBoneName": "Head",
|
||||
"idleState": "idle",
|
||||
"locomotionStateMachine": "locomotion",
|
||||
"mouseSensitivity": 0.20000000298023224,
|
||||
"nearCircleRadius": 14.0,
|
||||
"runState": "running",
|
||||
"swimFastState": "swimming-fast",
|
||||
"swimIdleState": "swim-idle",
|
||||
"swimState": "swimming",
|
||||
"targetCharacterName": "s1",
|
||||
"tpsDistance": 3.0,
|
||||
"tpsHeight": 2.0,
|
||||
"walkState": "walking"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#include <iostream>
|
||||
#include "EditorApp.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <OgreRoot.h>
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
bool triggered = false;
|
||||
ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
root->queueEndRendering();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Same application as the editor/game, but with a larger initial window. */
|
||||
class DemoApp : public EditorApp {
|
||||
public:
|
||||
OgreBites::NativeWindowPair
|
||||
createWindow(const Ogre::String &name, uint32_t w, uint32_t h,
|
||||
Ogre::NameValuePairList miscParams) override
|
||||
{
|
||||
(void)w;
|
||||
(void)h;
|
||||
return EditorApp::createWindow(name, 1920, 1080, miscParams);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The demo floor mesh and its flat colored material are created
|
||||
* programmatically because RenderableComponent only references a mesh by
|
||||
* name and carries no material/color of its own. The demo scene JSON
|
||||
* references the "DemoFloorPlane" mesh on the "demo_floor" entity.
|
||||
*/
|
||||
static void createFloorResources()
|
||||
{
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
|
||||
if (!Ogre::MeshManager::getSingleton()
|
||||
.getByName("DemoFloorPlane", group)
|
||||
.isNull())
|
||||
return;
|
||||
|
||||
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(
|
||||
"DemoFloorMaterial", group);
|
||||
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
|
||||
pass->setDiffuse(0.35f, 0.5f, 0.35f, 1.0f);
|
||||
pass->setAmbient(0.15f, 0.2f, 0.15f);
|
||||
pass->setSpecular(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
/* 60 x 60 units, matching the 30 x 0.1 x 30 static box collider
|
||||
* (top surface at y = 0) on the "demo_floor" entity in the scene. */
|
||||
Ogre::Plane groundPlane(Ogre::Vector3::UNIT_Y, 0.0f);
|
||||
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createPlane(
|
||||
"DemoFloorPlane", group, groundPlane, 60.0f, 60.0f, 1, 1,
|
||||
true, 1, 1.0f, 1.0f, Ogre::Vector3::UNIT_Z);
|
||||
mesh->getSubMesh(0)->setMaterialName("DemoFloorMaterial");
|
||||
}
|
||||
|
||||
/*
|
||||
* demo-character-controller: runs the editScene game mode with a small
|
||||
* hand-authored scene (demo_character_controller.json) containing a flat
|
||||
* colored floor plane with a static physics collider, a character spawner
|
||||
* ("s1", character registry ID 2, same character setup as town8.json) and
|
||||
* a PlayerControllerComponent targeting it. The character walks on the
|
||||
* physical floor exactly as in game mode.
|
||||
*
|
||||
* The mouse is grabbed while Playing (built-in game-mode behaviour);
|
||||
* Escape toggles the pause menu, which frees the cursor.
|
||||
*
|
||||
* The binary is self-contained in its build directory: run it from there
|
||||
* (resources.cfg, resources/, characters/, lua-scripts/, prefabs/ and the
|
||||
* runtime config JSONs are staged next to it by the build).
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
try {
|
||||
DemoApp app;
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
|
||||
bool headless = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
Ogre::String sceneFile = "demo_character_controller.json";
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--headless") {
|
||||
headless = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
exitAfterFirstFrame = true;
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
}
|
||||
}
|
||||
app.setHeadless(headless);
|
||||
|
||||
app.initApp();
|
||||
|
||||
if (headless) {
|
||||
/* Headless mode never creates EditorUISystem, which
|
||||
* owns the CharacterRegistry singleton; the demo scene
|
||||
* has a character spawner, so provide a bare registry
|
||||
* to keep spawner resolution from asserting. The
|
||||
* character then falls back to an inline spawn without
|
||||
* a physics capsule (fine for a smoke run). */
|
||||
static CharacterRegistry s_characterRegistry;
|
||||
s_characterRegistry.setWorld(app.getWorld());
|
||||
s_characterRegistry.setSceneManager(app.getSceneManager());
|
||||
s_characterRegistry.initialize();
|
||||
}
|
||||
|
||||
/* Mesh + material referenced by the floor entity. */
|
||||
createFloorResources();
|
||||
|
||||
std::cout << "[demo] starting new game with scene: "
|
||||
<< sceneFile << std::endl;
|
||||
app.startNewGame(sceneFile);
|
||||
std::cout << "[demo] controls: mouse = look, W/A/S/D = move, "
|
||||
"Shift = run, Escape = pause menu" << std::endl;
|
||||
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
|
||||
app.getRoot()->startRendering();
|
||||
/* Destroy scene entities while the systems are still alive:
|
||||
* CharacterSpawnerSystem registers an OnRemove observer on
|
||||
* CharacterSpawnerComponent that dereferences the system, and
|
||||
* the flecs world only dies after destroyEditorSystems() in
|
||||
* ~EditorApp, so letting the spawner entity live that long
|
||||
* would call back into a destroyed system. */
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Stage everything demoCharacterController needs into its own directory so
|
||||
# it runs standalone from
|
||||
# <build>/src/features/editScene/demos/demo-character-controller.
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# Small demo-owned files are COPIED; the big pre-staged runtime directories
|
||||
# are SYMLINKED from the editScene binary directory (Ogre FileSystem
|
||||
# locations follow symlinks, and copying would duplicate hundreds of MB on
|
||||
# every build). The symlink targets are populated by the editSceneEditor
|
||||
# staging, so editSceneEditor must have been built (and run its POST_BUILD
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_character_controller.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
# Character prefab for the spawner's registry entry (registryId 2). It is
|
||||
# written by a previous editor/game run (CharacterRegistry::savePrefab...),
|
||||
# not part of the source tree, so copy it from the editScene binary
|
||||
# directory when present.
|
||||
file(MAKE_DIRECTORY "${DEMO_DIR}/prefabs")
|
||||
foreach(f char_2.json)
|
||||
if(EXISTS "${EDITSCENE_BIN}/prefabs/${f}")
|
||||
file(COPY "${EDITSCENE_BIN}/prefabs/${f}"
|
||||
DESTINATION "${DEMO_DIR}/prefabs")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(dir resources characters lua-scripts)
|
||||
set(link "${DEMO_DIR}/${dir}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE_RECURSE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${EDITSCENE_BIN}/${dir}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Runtime config JSONs loaded at startup relative to the CWD (game mode
|
||||
# reads startup_menu.json, the registries read the rest — the demo needs
|
||||
# character_registry.json for the spawner's registryId 2 and
|
||||
# animation_tree.json for the character's "male1_6" animation tree). They
|
||||
# only exist after the editor/game has run once; copy them when present so
|
||||
# the demo behaves the same as when run from the editor binary directory.
|
||||
foreach(f startup_menu.json character_registry.json character_class.json
|
||||
items.json item_state.json inventory_config.json
|
||||
animation_tree.json)
|
||||
if(EXISTS "${EDITSCENE_BIN}/${f}")
|
||||
file(COPY "${EDITSCENE_BIN}/${f}" DESTINATION "${DEMO_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
@@ -0,0 +1,86 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# demo-lua-scene-script — game-mode demo for SceneScriptComponent
|
||||
# ---------------------------------------------------------------------------
|
||||
# Separate executable built from the same sources as editSceneEditor (minus
|
||||
# main.cpp). It runs in game mode and loads demo_lua_scene_script.json,
|
||||
# whose "demo_events" entity carries an inline SceneScriptComponent Lua
|
||||
# script demonstrating scene_loaded / prefab_loaded / custom "demo_pulse"
|
||||
# event handling. The demo prefab (demo_pulse_cube.json) carries its own
|
||||
# inline script covering the prefab-local variant: prefab scripts run inside
|
||||
# instantiatePrefab() right before the instance's prefab_loaded is sent, and
|
||||
# work even when no scene script exists yet (e.g. in the startup menu scene).
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# step (stage_runtime.cmake) copies resources.cfg, the demo scene/prefab and
|
||||
# any runtime config JSONs here, and symlinks the big pre-staged runtime
|
||||
# directories (resources/, characters/, lua-scripts/) from the editScene
|
||||
# binary directory. Run it from here:
|
||||
# cd <build>/src/features/editScene/demos/demo-lua-scene-script
|
||||
# ./demoLuaSceneScript
|
||||
# Controls: RMB drag = look, W/A/S/D = move, Q/E = down/up, Shift = boost,
|
||||
# Escape = pause menu.
|
||||
|
||||
get_filename_component(EDITSCENE_SOURCE_DIR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
||||
get_filename_component(EDITSCENE_BINARY_DIR
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/../.." ABSOLUTE)
|
||||
|
||||
# Reuse the editScene sources, swapping main.cpp for demo_main.cpp.
|
||||
set(DEMO_SOURCES ${EDITSCENE_SOURCES})
|
||||
list(REMOVE_ITEM DEMO_SOURCES main.cpp)
|
||||
list(TRANSFORM DEMO_SOURCES PREPEND "${EDITSCENE_SOURCE_DIR}/")
|
||||
|
||||
add_executable(demoLuaSceneScript
|
||||
demo_main.cpp
|
||||
${DEMO_SOURCES}
|
||||
)
|
||||
|
||||
add_dependencies(demoLuaSceneScript morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing (same as editor)
|
||||
target_compile_definitions(demoLuaSceneScript PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(demoLuaSceneScript
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
OgreProcedural::OgreProcedural
|
||||
RecastNavigation::Recast
|
||||
RecastNavigation::Detour
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
target_include_directories(demoLuaSceneScript PRIVATE
|
||||
${EDITSCENE_SOURCE_DIR}
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/FastNoiseLite
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# Stage the standalone runtime next to the executable (see header comment).
|
||||
add_custom_command(TARGET demoLuaSceneScript POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DDEMO_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-DEDITSCENE_BIN=${EDITSCENE_BINARY_DIR}
|
||||
-DEDITSCENE_SRC=${EDITSCENE_SOURCE_DIR}
|
||||
-DSRC_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/stage_runtime.cmake"
|
||||
COMMENT "Staging demo-lua-scene-script standalone runtime"
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"entities": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"name": "demo_light"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 10.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"lightType": "directional",
|
||||
"diffuseColor": {
|
||||
"r": 1.0,
|
||||
"g": 1.0,
|
||||
"b": 1.0,
|
||||
"a": 1.0
|
||||
},
|
||||
"specularColor": {
|
||||
"r": 0.5,
|
||||
"g": 0.5,
|
||||
"b": 0.5,
|
||||
"a": 1.0
|
||||
},
|
||||
"direction": {
|
||||
"x": 0.3,
|
||||
"y": -1.0,
|
||||
"z": 0.2
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"castShadows": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": {
|
||||
"name": "demo_ground"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": -0.2,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 30.0,
|
||||
"y": 0.2,
|
||||
"z": 30.0
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": {
|
||||
"name": "demo_cube"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -2.0,
|
||||
"y": 0.5,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": {
|
||||
"name": "demo_pulse_target"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.5,
|
||||
"z": -2.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": {
|
||||
"name": "demo_prefab_instance"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 2.0,
|
||||
"y": 0.5,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"prefabInstance": {
|
||||
"prefabPath": "prefabs/demo_pulse_cube.json"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": {
|
||||
"name": "demo_events"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"sceneScript": {
|
||||
"scriptPath": "",
|
||||
"inlineScript": "-- demo-lua-scene-script: inline scene script.\n-- Executed once when the scene finishes loading; subscribes to the\n-- engine load events and to a custom \"demo_pulse\" event. Every handler\n-- prints a [demo] line to stdout AND visibly mutates an entity.\n\nprint(\"[demo] scene script executed: subscribing to events\")\n\n-- scene_loaded: sent by EditorApp::startNewGame() right after this script\n-- runs, so the subscription below always catches it.\necs.subscribe_event(\"scene_loaded\", function(event, params)\n print(\"[demo] scene_loaded: \" .. tostring(params and params.scenePath))\n -- Visibly mutate: lift the cube named \"demo_cube\" up by 2 units.\n local cube = ecs.get_entity_by_name(\"demo_cube\")\n if cube and ecs.has_component(cube, \"Transform\") then\n local t = ecs.get_component(cube, \"Transform\")\n ecs.set_component(cube, \"Transform\", {\n position = { t.position[1], t.position[2] + 2.0, t.position[3] },\n rotation = t.rotation,\n scale = t.scale,\n })\n print(\"[demo] scene_loaded: demo_cube moved up by +2.0 in Y\")\n end\n -- Fire the custom event so its handler runs as part of the load.\n ecs.send_event(\"demo_pulse\", { tick = 1 })\n print(\"[demo] scene_loaded: demo_pulse sent\")\nend)\n\n-- prefab_loaded: scene scripts run lazily at the first loadPendingScripts\n-- call, which happens inside PrefabSystem::resolveInstances() right before\n-- the instance's prefab_loaded is sent, so this handler fires even for\n-- prefabs instantiated during the same scene load. The inline script inside\n-- prefabs/demo_pulse_cube.json demonstrates the prefab-local variant, which\n-- also works when a prefab is instantiated before any scene script exists\n-- (e.g. in the startup menu scene).\necs.subscribe_event(\"prefab_loaded\", function(event, params)\n print(\"[demo] scene script saw prefab_loaded: \" ..\n tostring(params and params.prefabPath) ..\n \" entity=\" .. tostring(params and params.entityId))\nend)\n\n-- Custom event: scales the cube named \"demo_pulse_target\" by 2x.\necs.subscribe_event(\"demo_pulse\", function(event, params)\n print(\"[demo] demo_pulse received, tick=\" ..\n tostring(params and params.tick))\n local cube = ecs.get_entity_by_name(\"demo_pulse_target\")\n if cube and ecs.has_component(cube, \"Transform\") then\n local t = ecs.get_component(cube, \"Transform\")\n ecs.set_component(cube, \"Transform\", {\n position = t.position,\n rotation = t.rotation,\n scale = { t.scale[1] * 2.0, t.scale[2] * 2.0, t.scale[3] * 2.0 },\n })\n print(\"[demo] demo_pulse: demo_pulse_target scaled x2\")\n end\nend)\n"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#include <iostream>
|
||||
#include "EditorApp.hpp"
|
||||
#include "GameMode.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <OgreRoot.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
bool triggered = false;
|
||||
ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
root->queueEndRendering();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Demo fly camera. The demo scene has no player controller, so in game
|
||||
* mode nothing drives the camera and EditorApp would only grab the mouse
|
||||
* (setGamePlayState(Playing) -> setWindowGrab(true)). EditorApp consumes
|
||||
* all SDL input events in game mode before chained listeners see them, so
|
||||
* the controls are polled per frame via SDL_GetKeyboardState() /
|
||||
* SDL_GetRelativeMouseState() — the same approach GameInputState uses.
|
||||
*
|
||||
* Controls: RMB drag = look, W/A/S/D = move, Q/E = down/up, Shift = boost.
|
||||
* Escape keeps the built-in game-mode behaviour (pause menu toggle).
|
||||
*/
|
||||
class DemoFlyCamera : public Ogre::FrameListener {
|
||||
public:
|
||||
DemoFlyCamera(EditorApp &app)
|
||||
: m_app(app)
|
||||
{
|
||||
}
|
||||
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override
|
||||
{
|
||||
/* Keep the cursor free: game mode re-grabs the mouse whenever
|
||||
* the Playing state is (re-)entered (also when leaving the
|
||||
* pause menu), so ungrab on every transition into Playing. */
|
||||
bool playing = editScene::isGamePlaying();
|
||||
if (playing && !m_wasPlaying)
|
||||
m_app.setWindowGrab(false);
|
||||
m_wasPlaying = playing;
|
||||
|
||||
Ogre::Camera *cam = m_app.getEditorCamera()->getCamera();
|
||||
if (!cam || !cam->getParentSceneNode())
|
||||
return true;
|
||||
Ogre::SceneNode *node = cam->getParentSceneNode();
|
||||
|
||||
/* Mouse look while the right button is held; the relative
|
||||
* state is polled every frame so deltas stay current. */
|
||||
int dx, dy;
|
||||
uint32_t buttons = SDL_GetRelativeMouseState(&dx, &dy);
|
||||
if (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
|
||||
m_yaw -= dx * 0.003f;
|
||||
m_pitch -= dy * 0.003f;
|
||||
m_pitch = Ogre::Math::Clamp(m_pitch, -1.5f, 1.5f);
|
||||
}
|
||||
node->setOrientation(
|
||||
Ogre::Quaternion(Ogre::Radian(m_yaw),
|
||||
Ogre::Vector3::UNIT_Y) *
|
||||
Ogre::Quaternion(Ogre::Radian(m_pitch),
|
||||
Ogre::Vector3::UNIT_X));
|
||||
|
||||
/* WASD + QE fly movement, Shift boosts x10. */
|
||||
const uint8_t *keys = SDL_GetKeyboardState(nullptr);
|
||||
Ogre::Vector3 move = Ogre::Vector3::ZERO;
|
||||
if (keys[SDL_SCANCODE_W])
|
||||
move.z -= 1.0f;
|
||||
if (keys[SDL_SCANCODE_S])
|
||||
move.z += 1.0f;
|
||||
if (keys[SDL_SCANCODE_A])
|
||||
move.x -= 1.0f;
|
||||
if (keys[SDL_SCANCODE_D])
|
||||
move.x += 1.0f;
|
||||
float vertical = 0.0f;
|
||||
if (keys[SDL_SCANCODE_E])
|
||||
vertical += 1.0f;
|
||||
if (keys[SDL_SCANCODE_Q])
|
||||
vertical -= 1.0f;
|
||||
float speed = 10.0f;
|
||||
if (keys[SDL_SCANCODE_LSHIFT] || keys[SDL_SCANCODE_RSHIFT])
|
||||
speed *= 10.0f;
|
||||
if (!move.isZeroLength() || vertical != 0.0f) {
|
||||
Ogre::Vector3 delta =
|
||||
node->getOrientation() * move +
|
||||
Ogre::Vector3(0.0f, vertical, 0.0f);
|
||||
delta.normalise();
|
||||
node->translate(delta * speed * evt.timeSinceLastFrame);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
EditorApp &m_app;
|
||||
bool m_wasPlaying = false;
|
||||
float m_yaw = 0.0f;
|
||||
float m_pitch = -0.3f;
|
||||
};
|
||||
|
||||
/*
|
||||
* demo-lua-scene-script: runs the editScene game mode with a hand-authored
|
||||
* scene whose SceneScriptComponent carries an inline Lua script
|
||||
* demonstrating event handling (scene_loaded, prefab_loaded and a custom
|
||||
* "demo_pulse" event). See demo_lua_scene_script.json.
|
||||
*
|
||||
* The binary is self-contained in its build directory: run it from there
|
||||
* (resources.cfg, resources/, characters/, lua-scripts/ and prefabs/ are
|
||||
* staged next to it by the build).
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
try {
|
||||
EditorApp app;
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
|
||||
bool headless = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
Ogre::String sceneFile = "demo_lua_scene_script.json";
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--headless") {
|
||||
headless = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
exitAfterFirstFrame = true;
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
}
|
||||
}
|
||||
app.setHeadless(headless);
|
||||
|
||||
app.initApp();
|
||||
|
||||
std::cout << "[demo] starting new game with scene: "
|
||||
<< sceneFile << std::endl;
|
||||
app.startNewGame(sceneFile);
|
||||
/* Game mode grabbed the mouse on entering Playing; the demo
|
||||
* has no player controller, so free the cursor again (the fly
|
||||
* camera below re-frees it whenever Playing is re-entered). */
|
||||
app.setWindowGrab(false);
|
||||
std::cout << "[demo] controls: RMB drag = look, "
|
||||
"W/A/S/D = move, Q/E = down/up, Shift = boost, "
|
||||
"Escape = pause menu" << std::endl;
|
||||
|
||||
DemoFlyCamera flyCamera(app);
|
||||
if (!headless)
|
||||
app.getRoot()->addFrameListener(&flyCamera);
|
||||
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
|
||||
app.getRoot()->startRendering();
|
||||
app.closeApp();
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "DemoPulseCube",
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.5,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"sceneScript": {
|
||||
"scriptPath": "",
|
||||
"inlineScript": "-- demo-lua-scene-script: inline prefab script.\n-- Runs inside PrefabSystem/SceneSerializer::instantiatePrefab() right\n-- before the \"prefab_loaded\" event for the instance is sent, so the\n-- subscription below always catches the instance's own load event. (Scene\n-- scripts do see it too \u2014 they run lazily at this same loadPendingScripts\n-- call \u2014 but prefab-local scripts also work when no scene script exists,\n-- e.g. for prefabs instantiated while the startup menu scene loads.)\n\nprint(\"[demo] prefab script executed: subscribing to prefab_loaded\")\n\necs.subscribe_event(\"prefab_loaded\", function(event, params)\n if not params then\n return\n end\n print(\"[demo] prefab_loaded: \" .. tostring(params.prefabPath) ..\n \" entity=\" .. tostring(params.entityId))\n -- Visibly mutate: lift the freshly instantiated cube by 1.5 units.\n if params.entityId and ecs.has_component(params.entityId, \"Transform\") then\n local t = ecs.get_component(params.entityId, \"Transform\")\n ecs.set_component(params.entityId, \"Transform\", {\n position = { t.position[1], t.position[2] + 1.5, t.position[3] },\n rotation = t.rotation,\n scale = t.scale,\n })\n print(\"[demo] prefab_loaded: instance lifted by +1.5 in Y\")\n end\nend)\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Stage everything demoLuaSceneScript needs into its own directory so it
|
||||
# runs standalone from
|
||||
# <build>/src/features/editScene/demos/demo-lua-scene-script.
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# Small demo-owned files are COPIED; the big pre-staged runtime directories
|
||||
# are SYMLINKED from the editScene binary directory (Ogre FileSystem
|
||||
# locations follow symlinks, and copying would duplicate hundreds of MB on
|
||||
# every build). The symlink targets are populated by the editSceneEditor
|
||||
# staging, so editSceneEditor must have been built (and run its POST_BUILD
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_lua_scene_script.json" DESTINATION "${DEMO_DIR}")
|
||||
file(MAKE_DIRECTORY "${DEMO_DIR}/prefabs")
|
||||
file(COPY "${SRC_DIR}/demo_pulse_cube.json" DESTINATION "${DEMO_DIR}/prefabs")
|
||||
|
||||
foreach(dir resources characters lua-scripts)
|
||||
set(link "${DEMO_DIR}/${dir}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE_RECURSE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${EDITSCENE_BIN}/${dir}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Runtime config JSONs loaded at startup relative to the CWD (game mode
|
||||
# reads startup_menu.json, the registries read the rest). They only exist
|
||||
# after the editor/game has run once; copy them when present so the demo
|
||||
# behaves the same as when run from the editor binary directory.
|
||||
foreach(f startup_menu.json character_registry.json character_class.json
|
||||
items.json item_state.json inventory_config.json
|
||||
animation_tree.json)
|
||||
if(EXISTS "${EDITSCENE_BIN}/${f}")
|
||||
file(COPY "${EDITSCENE_BIN}/${f}" DESTINATION "${DEMO_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
@@ -275,20 +275,47 @@ static void registerAllComponents()
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- Transform ---
|
||||
REGISTER_COMPONENT(
|
||||
TransformComponent, "Transform", pushVector3(L, c.position);
|
||||
lua_setfield(L, -2, "position"); pushQuaternion(L, c.rotation);
|
||||
lua_setfield(L, -2, "rotation"); pushVector3(L, c.scale);
|
||||
lua_setfield(L, -2, "scale");
|
||||
, if (lua_getfield(L, idx, "position"), lua_istable(L, -1))
|
||||
c.position = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "rotation"), lua_istable(L, -1))
|
||||
c.rotation = readQuaternion(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "scale"), lua_istable(L, -1))
|
||||
c.scale = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1););
|
||||
/* Hand-written (not REGISTER_COMPONENT) so that a script-side
|
||||
* set_component/set_field also applies the new values to the scene
|
||||
* node and bumps the change-tracking version; the generic writer only
|
||||
* updates the component data, leaving the visual node unmoved. */
|
||||
s_components["Transform"] = {
|
||||
"Transform",
|
||||
[](lua_State *L, flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
lua_pushnil(L);
|
||||
return;
|
||||
}
|
||||
const auto &c = e.get<TransformComponent>();
|
||||
lua_newtable(L);
|
||||
pushVector3(L, c.position);
|
||||
lua_setfield(L, -2, "position");
|
||||
pushQuaternion(L, c.rotation);
|
||||
lua_setfield(L, -2, "rotation");
|
||||
pushVector3(L, c.scale);
|
||||
lua_setfield(L, -2, "scale");
|
||||
},
|
||||
[](lua_State *L, flecs::entity e, int idx) {
|
||||
TransformComponent c;
|
||||
if (e.has<TransformComponent>())
|
||||
c = e.get<TransformComponent>();
|
||||
if (lua_getfield(L, idx, "position"),
|
||||
lua_istable(L, -1))
|
||||
c.position = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "rotation"),
|
||||
lua_istable(L, -1))
|
||||
c.rotation = readQuaternion(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "scale"), lua_istable(L, -1))
|
||||
c.scale = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
e.set<TransformComponent>(c);
|
||||
auto &stored = e.get_mut<TransformComponent>();
|
||||
stored.applyToNode();
|
||||
stored.markChanged();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Renderable ---
|
||||
REGISTER_COMPONENT(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "components/EditorMarker.hpp"
|
||||
#include "components/EntityName.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
|
||||
@@ -101,6 +102,8 @@ static int luaGetPlayerEntity(lua_State *L)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
// Matches the Flecs entity name first, then falls back to
|
||||
// EntityNameComponent (the name scene entities get on load).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityByName(lua_State *L)
|
||||
@@ -109,6 +112,16 @@ static int luaGetEntityByName(lua_State *L)
|
||||
const char *name = lua_tostring(L, 1);
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup(name);
|
||||
if (!e.is_valid()) {
|
||||
/* Fall back to EntityNameComponent (the scene/editor name);
|
||||
* world.lookup only covers Flecs entity names, which
|
||||
* scene-loaded entities do not have. */
|
||||
world.query<const EntityNameComponent>().each(
|
||||
[&](flecs::entity cand, const EntityNameComponent &n) {
|
||||
if (!e.is_valid() && n.name == name)
|
||||
e = cand;
|
||||
});
|
||||
}
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
|
||||
@@ -21,6 +21,26 @@ namespace editScene
|
||||
static std::unordered_map<int, EventBus::ListenerId> s_luaSubscriptions;
|
||||
static int s_nextLuaSubId = 1;
|
||||
|
||||
/* Same registry lookup as LuaEntityApi.cpp (kept file-local). */
|
||||
static flecs::world getWorld(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
|
||||
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
|
||||
flecs::world *world =
|
||||
static_cast<flecs::world *>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *world;
|
||||
}
|
||||
|
||||
/* Map a raw Flecs entity ID from an EventParams payload into the
|
||||
* Lua-facing ID space (g_luaEntityIdMap) so handlers can pass it back to
|
||||
* the ecs.* entity/component functions. Returns -1 for dead entities. */
|
||||
static lua_Integer pushableEntityId(lua_State *L, uint64_t rawId)
|
||||
{
|
||||
flecs::entity e = getWorld(L).get_alive((flecs::entity_t)rawId);
|
||||
return (lua_Integer)luaEntityToId(e);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push an EventValue as a Lua value
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -38,7 +58,7 @@ static void pushEventValue(lua_State *L, const EventValue &val)
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
case EventValue::ENTITY_ID:
|
||||
lua_pushinteger(L, (lua_Integer)val.getEntityId());
|
||||
lua_pushinteger(L, pushableEntityId(L, val.getEntityId()));
|
||||
break;
|
||||
case EventValue::INT:
|
||||
lua_pushinteger(L, (lua_Integer)val.getInt());
|
||||
@@ -56,7 +76,7 @@ static void pushEventValue(lua_State *L, const EventValue &val)
|
||||
lua_newtable(L);
|
||||
const auto &arr = val.getEntityIdArray();
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
lua_pushinteger(L, (lua_Integer)arr[i]);
|
||||
lua_pushinteger(L, pushableEntityId(L, arr[i]));
|
||||
lua_rawseti(L, -2, (int)(i + 1));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1103,11 +1103,17 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
|
||||
EditorUISystem *uiSystem)
|
||||
{
|
||||
try {
|
||||
/* Copy the path: callers commonly pass a reference straight
|
||||
* into component storage (PrefabInstanceComponent::prefabPath)
|
||||
* which can be relocated when instantiation adds components
|
||||
* to the instance entity. */
|
||||
const std::string path = filepath;
|
||||
|
||||
/* Cached parse (M4.4): the same prefab may be instantiated
|
||||
* by hundreds of streamed spawners. */
|
||||
const nlohmann::json *cached = loadPrefabJsonCached(filepath);
|
||||
const nlohmann::json *cached = loadPrefabJsonCached(path);
|
||||
if (!cached) {
|
||||
m_lastError = "Failed to open prefab: " + filepath;
|
||||
m_lastError = "Failed to open prefab: " + path;
|
||||
return false;
|
||||
}
|
||||
const nlohmann::json &prefabJson = *cached;
|
||||
@@ -1136,8 +1142,7 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
|
||||
* (pending-only, so repeat instantiations are cheap), then
|
||||
* notify listeners that this instance finished loading. */
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
SceneScriptSystem::sendPrefabLoaded(filepath,
|
||||
instanceEntity.id());
|
||||
SceneScriptSystem::sendPrefabLoaded(path, instanceEntity.id());
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
Reference in New Issue
Block a user