Demos for scee script and player controller

This commit is contained in:
2026-09-05 13:32:15 +03:00
parent 4670aa7af9
commit 58deefa0a0
15 changed files with 1124 additions and 21 deletions
+47 -1
View File
@@ -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
+4
View File
@@ -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"
)
@@ -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()
+41 -14
View File
@@ -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);
+22 -2
View File
@@ -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) {