Doors implementation
This commit is contained in:
@@ -170,12 +170,13 @@ to `EditorApp`.
|
||||
13. `GoapPlannerSystem`
|
||||
14. `GoapRunnerSystem`
|
||||
15. `ActuatorSystem`
|
||||
16. `EventHandlerSystem`
|
||||
17. `CharacterSystem`
|
||||
18. `BuoyancySystem`
|
||||
19. `PhysicsSystem`
|
||||
20. `HairPhysicsSystem` pose read-back
|
||||
21. Rendering support systems (sun, skybox, water, light, LOD, etc.)
|
||||
16. `DoorSystem`
|
||||
17. `EventHandlerSystem`
|
||||
18. `CharacterSystem`
|
||||
19. `BuoyancySystem`
|
||||
20. `PhysicsSystem`
|
||||
21. `HairPhysicsSystem` pose read-back
|
||||
22. Rendering support systems (sun, skybox, water, light, LOD, etc.)
|
||||
|
||||
Systems that write animation state or velocity should respect systems that run
|
||||
before them. In particular, `PlayerControllerSystem` runs first and is the only
|
||||
@@ -420,6 +421,50 @@ screen-space via ImGui, so `update()` and `render()` bail out early when there
|
||||
is no ImGui context (headless mode); cooldown timers and the executing-action
|
||||
block still run.
|
||||
|
||||
### DoorSystem & CellGrid doors
|
||||
|
||||
`CellGridSystem::buildDoorEntities()` spawns one runtime door entity per
|
||||
unique doorway when `CellGridComponent::doorsEnabled` is set (default true).
|
||||
Doors are placed on the same condition as door frames (any of the 8 door cell
|
||||
flags, internal and external); because the two cells sharing a door edge both
|
||||
carry the flag, placement is deduplicated on a canonical edge key so each room
|
||||
connection or exit gets exactly one door.
|
||||
|
||||
Each door entity (child of the grid, no `EditorMarkerComponent`, so it is not
|
||||
serialized or editable) carries:
|
||||
|
||||
- `TransformComponent` whose node is the hinge node at the doorway
|
||||
- `RenderableComponent` with the leaf mesh
|
||||
- `DoorComponent` (runtime only, not serialized) with the swing state
|
||||
- `RigidBodyComponent` (static) plus a child entity with a box
|
||||
`PhysicsColliderComponent` covering the closed leaf
|
||||
- `ActuatorComponent` so the player gets an "E Open"/"E Close" prompt
|
||||
|
||||
The leaf mesh is procedural by default (a box sized to fit the door frame
|
||||
opening, hinge edge at the origin, UV-mapped through
|
||||
`CellGridComponent::doorRectName` like the other grid parts) and uses the
|
||||
grid's procedural material. Setting `CellGridComponent::doorMeshName` loads a
|
||||
custom mesh instead (it must be modeled with the hinge edge at the origin,
|
||||
spanning +X/+Y); it still uses the procedural material unless
|
||||
`doorUseMeshMaterial` is set.
|
||||
|
||||
Interaction: `ActuatorSystem` sets `DoorComponent::toggleRequested` on E and,
|
||||
when `CellGridComponent::doorActionName` is set, also runs that action.
|
||||
`DoorSystem` consumes the request, swings the hinge node around local Y to
|
||||
`doorOpenAngle` at `doorOpenSpeed`, disables the door's rigid body while the
|
||||
door is not fully closed, and re-enables it once closed again.
|
||||
|
||||
Scene switching doors: when `CellGridComponent::doorSceneSwitchPath` is set,
|
||||
activation instead queues `EditorApp::switchScene()` directly (no action or
|
||||
behavior tree involved) with `doorSceneSwitchTarget` as the teleport target
|
||||
entity name in the new scene (same mechanism as the "switchScene" BT node's
|
||||
"@name" param). The leaf does not swing and the prompt always reads "E Open".
|
||||
|
||||
New `CellGridComponent` fields (serialized in the scene JSON and exposed to
|
||||
Lua): `doorsEnabled`, `doorRectName`, `doorMeshName`, `doorUseMeshMaterial`,
|
||||
`doorOpenAngle`, `doorOpenSpeed`, `doorActionName`, `doorSceneSwitchPath`,
|
||||
`doorSceneSwitchTarget`.
|
||||
|
||||
### SceneScriptComponent & SceneScriptSystem
|
||||
|
||||
Attaches a Lua script to a scene or prefab entity. Fields:
|
||||
|
||||
@@ -36,6 +36,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/ProceduralMaterialSystem.cpp
|
||||
systems/ProceduralMeshSystem.cpp
|
||||
systems/CellGridSystem.cpp
|
||||
systems/DoorSystem.cpp
|
||||
systems/NormalDebugSystem.cpp
|
||||
systems/RoomLayoutSystem.cpp
|
||||
systems/FurnitureLibrary.cpp
|
||||
@@ -254,6 +255,7 @@ set(EDITSCENE_HEADERS
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
systems/CellGridSystem.hpp
|
||||
systems/DoorSystem.hpp
|
||||
systems/NormalDebugSystem.hpp
|
||||
systems/RoomLayoutSystem.hpp
|
||||
systems/FurnitureLibrary.hpp
|
||||
@@ -284,6 +286,7 @@ set(EDITSCENE_HEADERS
|
||||
systems/PathFollowingSystem.hpp
|
||||
systems/GoapPlannerSystem.hpp
|
||||
components/Actuator.hpp
|
||||
components/Door.hpp
|
||||
ui/ActuatorEditor.hpp
|
||||
systems/EventBus.hpp
|
||||
components/EventHandler.hpp
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
#include "components/GoapRunner.hpp"
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
#include "systems/DoorSystem.hpp"
|
||||
#include "systems/EventHandlerSystem.hpp"
|
||||
#include "systems/EventBus.hpp"
|
||||
#include "systems/SceneScriptSystem.hpp"
|
||||
@@ -391,6 +392,7 @@ void EditorApp::destroyEditorSystems()
|
||||
m_itemSystem.reset();
|
||||
m_eventHandlerSystem.reset();
|
||||
m_actuatorSystem.reset();
|
||||
m_doorSystem.reset();
|
||||
m_goapPlannerSystem.reset();
|
||||
m_pathFollowingSystem.reset();
|
||||
m_goapRunnerSystem.reset();
|
||||
@@ -702,8 +704,12 @@ void EditorApp::setup()
|
||||
// Setup CellGrid system
|
||||
m_cellGridSystem =
|
||||
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
|
||||
m_cellGridSystem->setPhysicsSystem(m_physicsSystem.get());
|
||||
m_cellGridSystem->initialize();
|
||||
|
||||
// Setup Door system (swing animation for cell grid doors)
|
||||
m_doorSystem = std::make_unique<DoorSystem>(m_world);
|
||||
|
||||
// Wire CellGridSystem into NavMeshSystem so it can collect
|
||||
// batched frame/furniture geometry from StaticGeometry.
|
||||
m_navMeshSystem->setCellGridSystem(m_cellGridSystem.get());
|
||||
@@ -2149,6 +2155,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_actuatorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Door system (door swing animation) --- */
|
||||
if (m_doorSystem) {
|
||||
m_doorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Event Handler system (event-driven BTs) --- */
|
||||
if (m_eventHandlerSystem) {
|
||||
m_eventHandlerSystem->update(evt.timeSinceLastFrame);
|
||||
|
||||
@@ -48,6 +48,7 @@ class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class DoorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class CharacterClassSystem;
|
||||
@@ -378,6 +379,7 @@ private:
|
||||
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<DoorSystem> m_doorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
|
||||
|
||||
@@ -145,6 +145,22 @@ struct CellGridComponent {
|
||||
std::string roofTopRectName;
|
||||
std::string roofSideRectName;
|
||||
|
||||
// Door settings - one door entity is spawned per unique doorway
|
||||
// (deduplicated across the two cells sharing a door edge) on the same
|
||||
// condition as door frames (any of the 8 door cell flags).
|
||||
bool doorsEnabled = true; // Spawn openable door entities
|
||||
std::string doorRectName; // Named rect for procedural door leaf UV
|
||||
std::string doorMeshName; // Custom door leaf mesh (empty = procedural)
|
||||
bool doorUseMeshMaterial = false; // Custom mesh keeps its own material
|
||||
float doorOpenAngle = 100.0f; // Swing angle when open (degrees)
|
||||
float doorOpenSpeed = 180.0f; // Swing speed (degrees per second)
|
||||
std::string doorActionName; // Optional action run on activation
|
||||
// Scene switching doors (e.g. interior <-> exterior): when
|
||||
// doorSceneSwitchPath is set, activating a door queues an
|
||||
// EditorApp::switchScene() instead of swinging the leaf
|
||||
std::string doorSceneSwitchPath; // Target scene path (empty = disabled)
|
||||
std::string doorSceneSwitchTarget; // Teleport target entity name (optional)
|
||||
|
||||
// Physics properties for generated colliders
|
||||
float friction = 0.5f;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef EDITSCENE_DOOR_HPP
|
||||
#define EDITSCENE_DOOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Door component (runtime only - not serialized).
|
||||
*
|
||||
* Attached to door entities spawned by CellGridSystem for each unique
|
||||
* doorway (door cell flags, deduplicated across the two cells sharing a
|
||||
* door edge). The entity's TransformComponent node is the hinge node:
|
||||
* DoorSystem rotates it around local Y to swing the door.
|
||||
*
|
||||
* Interaction works through the ActuatorComponent on the same entity:
|
||||
* ActuatorSystem sets toggleRequested on activation; DoorSystem consumes
|
||||
* it, swings the door and disables/enables the door's RigidBodyComponent
|
||||
* (collider disabled while the door is not fully closed).
|
||||
*/
|
||||
struct DoorComponent {
|
||||
// Current state
|
||||
bool isOpen = false;
|
||||
|
||||
// Set by ActuatorSystem (or scripts) to request an open/close toggle
|
||||
bool toggleRequested = false;
|
||||
|
||||
// Swing configuration (copied from CellGridComponent at build time)
|
||||
float openAngle = 100.0f; // Target angle when open (degrees)
|
||||
float openSpeed = 180.0f; // Swing speed (degrees per second)
|
||||
|
||||
// Runtime: current swing angle (0 = closed)
|
||||
float currentAngle = 0.0f;
|
||||
|
||||
// Local orientation of the hinge node in the closed pose
|
||||
Ogre::Quaternion closedOrientation = Ogre::Quaternion::IDENTITY;
|
||||
|
||||
// Door-local offset from the hinge to the leaf center; used by
|
||||
// ActuatorSystem to place the interaction prompt/circle (the hinge
|
||||
// node sits on the side edge of the doorway)
|
||||
Ogre::Vector3 centerOffset = Ogre::Vector3::ZERO;
|
||||
|
||||
// Scene switching (copied from CellGridComponent at build time):
|
||||
// when sceneSwitchPath is set, activation switches scenes instead
|
||||
// of swinging the leaf
|
||||
std::string sceneSwitchPath; // Target scene path (empty = disabled)
|
||||
std::string sceneSwitchTarget; // Teleport target entity name
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_HPP
|
||||
@@ -10,9 +10,11 @@
|
||||
# without a Lot/District/Town parent.
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# step (stage_runtime.cmake) copies resources.cfg, both demo scenes, the
|
||||
# step (stage_runtime.cmake) copies resources.cfg, 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
|
||||
# run) and any runtime config JSONs here, and symlinks both demo scenes
|
||||
# (demo_scene_a.json / demo_scene_b.json, so scene edits in the source tree
|
||||
# are visible without a rebuild) plus 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-scene-switching-extra
|
||||
|
||||
@@ -359,9 +359,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* Escape toggles the pause menu, which frees the cursor.
|
||||
*
|
||||
* The binary is self-contained in its build directory: run it from there
|
||||
* (resources.cfg, both scene JSONs, resources/, characters/, lua-scripts/,
|
||||
* prefabs/ and the runtime config JSONs are staged next to it by the
|
||||
* build).
|
||||
* (resources.cfg, resources/, characters/, lua-scripts/, prefabs/ and the
|
||||
* runtime config JSONs are staged next to it by the build; both scene
|
||||
* JSONs are symlinks into the source tree, so scene edits take effect
|
||||
* without a rebuild).
|
||||
*
|
||||
* Extra flags:
|
||||
* --test-switch headless-friendly end-to-end check: executes both
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 4294967791,
|
||||
"id": 488,
|
||||
"name": {
|
||||
"name": "arrival_a"
|
||||
},
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 133143986672,
|
||||
"id": 489,
|
||||
"light": {
|
||||
"castShadows": false,
|
||||
"constantAttenuation": 1.0,
|
||||
@@ -231,7 +231,7 @@
|
||||
},
|
||||
"shapeType": "box"
|
||||
},
|
||||
"id": 77309411831,
|
||||
"id": 490,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
@@ -275,7 +275,7 @@
|
||||
"radius": 1.5
|
||||
},
|
||||
"children": [],
|
||||
"id": 77309411832,
|
||||
"id": 491,
|
||||
"name": {
|
||||
"name": "portal_a"
|
||||
},
|
||||
@@ -309,7 +309,7 @@
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 77309411833,
|
||||
"id": 492,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
@@ -334,7 +334,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 77309411834,
|
||||
"id": 493,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
@@ -386,7 +386,7 @@
|
||||
"cellGrid": {
|
||||
"ceilingRectName": "ceiling",
|
||||
"cellHeight": 4.0,
|
||||
"cellSize": 4.0,
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 147495,
|
||||
@@ -551,25 +551,158 @@
|
||||
"z": 6
|
||||
},
|
||||
{
|
||||
"flags": 81943,
|
||||
"flags": 81927,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 7
|
||||
},
|
||||
{
|
||||
"flags": 1048835,
|
||||
"flags": 1048579,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 7
|
||||
},
|
||||
{
|
||||
"flags": 98331,
|
||||
"flags": 98315,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 7
|
||||
},
|
||||
{
|
||||
"flags": 147463,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 8
|
||||
},
|
||||
{
|
||||
"flags": 2097155,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 8
|
||||
},
|
||||
{
|
||||
"flags": 163851,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 8
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 9
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 9
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 9
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 11
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 11
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 11
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 12
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 12
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 12
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 13
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 13
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 13
|
||||
},
|
||||
{
|
||||
"flags": 81943,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 14
|
||||
},
|
||||
{
|
||||
"flags": 1048835,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 14
|
||||
},
|
||||
{
|
||||
"flags": 98331,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 14
|
||||
}
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
"extWallRectName": "",
|
||||
"extWindowFrameRectName": "",
|
||||
@@ -595,12 +728,12 @@
|
||||
"clearRooms": false,
|
||||
"maxX": 8,
|
||||
"maxY": 1,
|
||||
"maxZ": 8,
|
||||
"maxZ": 20,
|
||||
"minX": -8,
|
||||
"minY": 0,
|
||||
"minZ": -8
|
||||
},
|
||||
"id": 103079215604,
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "r1"
|
||||
},
|
||||
@@ -625,12 +758,14 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 103079215605,
|
||||
"id": 496,
|
||||
"name": {
|
||||
"name": "r2"
|
||||
},
|
||||
"room": {
|
||||
"connectedRoomIds": [],
|
||||
"connectedRoomIds": [
|
||||
"room_1788724060923073709_509"
|
||||
],
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
@@ -672,9 +807,61 @@
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 509,
|
||||
"name": {
|
||||
"name": "r3"
|
||||
},
|
||||
"room": {
|
||||
"connectedRoomIds": [
|
||||
"room_1788648326350139622_498"
|
||||
],
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": false,
|
||||
"exits": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"fillRoomWithFurniture": false,
|
||||
"furnitureSeed": 42,
|
||||
"furnitureYOffset": 0.05000000074505806,
|
||||
"maxX": 2,
|
||||
"maxY": 1,
|
||||
"maxZ": 15,
|
||||
"minX": -1,
|
||||
"minY": 0,
|
||||
"minZ": 8,
|
||||
"persistentId": "room_1788724060923073709_509",
|
||||
"roomType": "",
|
||||
"tags": []
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 103079215603,
|
||||
"id": 494,
|
||||
"name": {
|
||||
"name": "interrior"
|
||||
},
|
||||
@@ -1327,7 +1514,7 @@
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.15325629711151123,
|
||||
"y": 0.013780713081359863,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
|
||||
@@ -1,178 +1,136 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"actionDatabase": {
|
||||
"actions": [
|
||||
{
|
||||
"name": "goto_scene_a",
|
||||
"cost": 1,
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=Welcome to the game!",
|
||||
"type": "luaTask"
|
||||
},
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "9.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "lua_hello_action",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "debugPrint",
|
||||
"name": "[demo] portal B: switching to scene A"
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"type": "switchScene",
|
||||
"name": "demo_scene_a.json",
|
||||
"params": "@arrival_a"
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "6.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=\"hello, world!\"",
|
||||
"type": "luaTask"
|
||||
}
|
||||
]
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "testAction",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "[demo] portal B: switching to scene A",
|
||||
"type": "debugPrint"
|
||||
},
|
||||
{
|
||||
"name": "demo_scene_a.json",
|
||||
"params": "@arrival_a",
|
||||
"type": "switchScene"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "goto_scene_a",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"bitNames": [
|
||||
{
|
||||
"index": 1,
|
||||
"name": "hungry"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"name": "thirsty"
|
||||
}
|
||||
],
|
||||
"goals": []
|
||||
},
|
||||
"bookmarks": [],
|
||||
"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": "DemoFloorPlaneB",
|
||||
"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": "portal_b"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.8,
|
||||
"z": 28.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": "DemoActuatorMarkerB",
|
||||
"visible": true
|
||||
},
|
||||
"actuator": {
|
||||
"radius": 1.5,
|
||||
"height": 1.8,
|
||||
"actionNames": ["goto_scene_a"]
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"children": [],
|
||||
"id": 488,
|
||||
"name": {
|
||||
"name": "arrival_b"
|
||||
},
|
||||
@@ -193,11 +151,405 @@
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"children": [
|
||||
{
|
||||
"cellGrid": {
|
||||
"ceilingRectName": "",
|
||||
"cellHeight": 4.0,
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 147495,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 2097667,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 163883,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 81943,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 65555,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 98331,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
}
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
"extWallRectName": "",
|
||||
"extWindowFrameRectName": "",
|
||||
"floorRectName": "",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationScript": "",
|
||||
"height": 1,
|
||||
"intDoorFrameRectName": "",
|
||||
"intWallRectName": "",
|
||||
"intWindowFrameRectName": "",
|
||||
"roofSideRectName": "",
|
||||
"roofTopRectName": "",
|
||||
"width": 10
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"clearArea": {
|
||||
"clearCells": true,
|
||||
"clearFurniture": true,
|
||||
"clearRoofs": false,
|
||||
"clearRooms": false,
|
||||
"maxX": 10,
|
||||
"maxY": 1,
|
||||
"maxZ": 10,
|
||||
"minX": -10,
|
||||
"minY": 0,
|
||||
"minZ": -10
|
||||
},
|
||||
"id": 491,
|
||||
"name": {
|
||||
"name": "c1"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 492,
|
||||
"name": {
|
||||
"name": "r1"
|
||||
},
|
||||
"room": {
|
||||
"connectedRoomIds": [],
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": false,
|
||||
"exits": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"fillRoomWithFurniture": false,
|
||||
"furnitureSeed": 42,
|
||||
"furnitureYOffset": 0.05000000074505806,
|
||||
"maxX": 2,
|
||||
"maxY": 1,
|
||||
"maxZ": 4,
|
||||
"minX": -1,
|
||||
"minY": 0,
|
||||
"minZ": 0,
|
||||
"persistentId": "room_1788689581735260047_499",
|
||||
"roomType": "",
|
||||
"tags": []
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 490,
|
||||
"name": {
|
||||
"name": "w1"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 489,
|
||||
"name": {
|
||||
"name": "x1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.9691458940505981,
|
||||
"y": 0.0,
|
||||
"z": 32.09623718261719
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 493,
|
||||
"light": {
|
||||
"castShadows": false,
|
||||
"constantAttenuation": 1.0,
|
||||
"diffuseColor": {
|
||||
"a": 1.0,
|
||||
"b": 1.0,
|
||||
"g": 1.0,
|
||||
"r": 1.0
|
||||
},
|
||||
"direction": {
|
||||
"x": 0.30000001192092896,
|
||||
"y": -1.0,
|
||||
"z": 0.20000000298023224
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"lightType": "directional",
|
||||
"linearAttenuation": 0.0,
|
||||
"quadraticAttenuation": 0.0,
|
||||
"range": 100.0,
|
||||
"specularColor": {
|
||||
"a": 1.0,
|
||||
"b": 0.5,
|
||||
"g": 0.5,
|
||||
"r": 0.5
|
||||
},
|
||||
"spotlightFalloff": 1.0,
|
||||
"spotlightInnerAngle": 30.0,
|
||||
"spotlightOuterAngle": 45.0
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"collider": {
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": -0.10000000149011612,
|
||||
"z": 0.0
|
||||
},
|
||||
"parameters": {
|
||||
"x": 30.0,
|
||||
"y": 0.10000000149011612,
|
||||
"z": 30.0
|
||||
},
|
||||
"radius": 0.5,
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"shapeType": "box"
|
||||
},
|
||||
"id": 494,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoFloorPlaneB",
|
||||
"visible": true
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "static",
|
||||
"enabled": true,
|
||||
"friction": 0.800000011920929,
|
||||
"isSensor": false,
|
||||
"mass": 1.0,
|
||||
"restitution": 0.0
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"goto_scene_a"
|
||||
],
|
||||
"height": 1.7999999523162842,
|
||||
"radius": 1.5
|
||||
},
|
||||
"children": [],
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "portal_b"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoActuatorMarkerB",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.800000011920929,
|
||||
"z": 28.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": 496,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
@@ -218,37 +570,14 @@
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
"registryId": 2,
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"children": [],
|
||||
"id": 497,
|
||||
"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,
|
||||
@@ -274,7 +603,25 @@
|
||||
"tpsHeight": 2.0,
|
||||
"walkState": "walking"
|
||||
},
|
||||
"children": []
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# Small demo-owned files are COPIED; the big pre-staged runtime directories
|
||||
# Small demo-owned files are COPIED, except the demo scenes, which are
|
||||
# SYMLINKED to the source tree so scene edits are visible to the demo
|
||||
# without a rebuild (the demo never saves scenes, so nothing writes through
|
||||
# the links); 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
|
||||
@@ -12,8 +15,14 @@
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_scene_a.json" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_scene_b.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
foreach(f demo_scene_a.json demo_scene_b.json)
|
||||
set(link "${DEMO_DIR}/${f}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${SRC_DIR}/${f}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Character prefab for the spawner's registry entry (registryId 2). It is
|
||||
# written by a previous editor/game run (CharacterRegistry::savePrefab...),
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
# demo_main.cpp).
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# step (stage_runtime.cmake) copies resources.cfg, both demo scenes, the
|
||||
# step (stage_runtime.cmake) copies resources.cfg, 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
|
||||
# run) and any runtime config JSONs here, and symlinks both demo scenes
|
||||
# (demo_scene_a.json / demo_scene_b.json, so scene edits in the source tree
|
||||
# are visible without a rebuild) plus 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-scene-switching
|
||||
|
||||
@@ -354,9 +354,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* Escape toggles the pause menu, which frees the cursor.
|
||||
*
|
||||
* The binary is self-contained in its build directory: run it from there
|
||||
* (resources.cfg, both scene JSONs, resources/, characters/, lua-scripts/,
|
||||
* prefabs/ and the runtime config JSONs are staged next to it by the
|
||||
* build).
|
||||
* (resources.cfg, resources/, characters/, lua-scripts/, prefabs/ and the
|
||||
* runtime config JSONs are staged next to it by the build; both scene
|
||||
* JSONs are symlinks into the source tree, so scene edits take effect
|
||||
* without a rebuild).
|
||||
*
|
||||
* Extra flags:
|
||||
* --test-switch headless-friendly end-to-end check: executes both
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# Small demo-owned files are COPIED; the big pre-staged runtime directories
|
||||
# Small demo-owned files are COPIED, except the demo scenes, which are
|
||||
# SYMLINKED to the source tree so scene edits are visible to the demo
|
||||
# without a rebuild (the demo never saves scenes, so nothing writes through
|
||||
# the links); 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
|
||||
@@ -12,8 +15,14 @@
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_scene_a.json" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/demo_scene_b.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
foreach(f demo_scene_a.json demo_scene_b.json)
|
||||
set(link "${DEMO_DIR}/${f}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK "${SRC_DIR}/${f}" "${link}" SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Character prefab for the spawner's registry entry (registryId 2). It is
|
||||
# written by a previous editor/game run (CharacterRegistry::savePrefab...),
|
||||
|
||||
@@ -1410,6 +1410,24 @@ static void registerAllComponents()
|
||||
lua_setfield(L, -2, "cellSize");
|
||||
lua_pushnumber(L, c.cellHeight);
|
||||
lua_setfield(L, -2, "cellHeight");
|
||||
lua_pushboolean(L, c.doorsEnabled ? 1 : 0);
|
||||
lua_setfield(L, -2, "doorsEnabled");
|
||||
lua_pushstring(L, c.doorRectName.c_str());
|
||||
lua_setfield(L, -2, "doorRectName");
|
||||
lua_pushstring(L, c.doorMeshName.c_str());
|
||||
lua_setfield(L, -2, "doorMeshName");
|
||||
lua_pushboolean(L, c.doorUseMeshMaterial ? 1 : 0);
|
||||
lua_setfield(L, -2, "doorUseMeshMaterial");
|
||||
lua_pushnumber(L, c.doorOpenAngle);
|
||||
lua_setfield(L, -2, "doorOpenAngle");
|
||||
lua_pushnumber(L, c.doorOpenSpeed);
|
||||
lua_setfield(L, -2, "doorOpenSpeed");
|
||||
lua_pushstring(L, c.doorActionName.c_str());
|
||||
lua_setfield(L, -2, "doorActionName");
|
||||
lua_pushstring(L, c.doorSceneSwitchPath.c_str());
|
||||
lua_setfield(L, -2, "doorSceneSwitchPath");
|
||||
lua_pushstring(L, c.doorSceneSwitchTarget.c_str());
|
||||
lua_setfield(L, -2, "doorSceneSwitchTarget");
|
||||
, if (lua_getfield(L, idx, "width"), lua_isnumber(L, -1))
|
||||
c.width = (int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -1424,6 +1442,36 @@ static void registerAllComponents()
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "cellHeight"), lua_isnumber(L, -1))
|
||||
c.cellHeight = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorsEnabled"), lua_isboolean(L, -1))
|
||||
c.doorsEnabled = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorRectName"), lua_isstring(L, -1))
|
||||
c.doorRectName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorMeshName"), lua_isstring(L, -1))
|
||||
c.doorMeshName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorUseMeshMaterial"),
|
||||
lua_isboolean(L, -1))
|
||||
c.doorUseMeshMaterial = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorOpenAngle"), lua_isnumber(L, -1))
|
||||
c.doorOpenAngle = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorOpenSpeed"), lua_isnumber(L, -1))
|
||||
c.doorOpenSpeed = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorActionName"), lua_isstring(L, -1))
|
||||
c.doorActionName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorSceneSwitchPath"),
|
||||
lua_isstring(L, -1))
|
||||
c.doorSceneSwitchPath = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorSceneSwitchTarget"),
|
||||
lua_isstring(L, -1))
|
||||
c.doorSceneSwitchTarget = lua_tostring(L, -1);
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- Room ---
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "ItemRegistry.hpp"
|
||||
#include "ItemStateRegistry.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
@@ -330,6 +331,13 @@ void ActuatorSystem::update(float deltaTime)
|
||||
|
||||
Ogre::Vector3 objPos =
|
||||
trans.node->_getDerivedPosition();
|
||||
// Doors: the node is the hinge on the side edge of the
|
||||
// doorway; prompt at the leaf center instead
|
||||
if (e.has<DoorComponent>()) {
|
||||
const auto &door = e.get<DoorComponent>();
|
||||
objPos += trans.node->_getDerivedOrientation() *
|
||||
door.centerOffset;
|
||||
}
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
@@ -426,14 +434,26 @@ void ActuatorSystem::update(float deltaTime)
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
if (targetEntity.is_alive()) {
|
||||
if (targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator =
|
||||
targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
if (targetEntity.has<DoorComponent>()) {
|
||||
// Doors toggle open/close on E
|
||||
const auto &door =
|
||||
targetEntity.get<DoorComponent>();
|
||||
// Scene switching doors never toggle
|
||||
m_labelText =
|
||||
"E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
(door.isOpen &&
|
||||
door.sceneSwitchPath.empty()) ?
|
||||
"E Close" :
|
||||
"E Open";
|
||||
} else {
|
||||
auto &actuator =
|
||||
targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
m_labelText =
|
||||
"E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
}
|
||||
}
|
||||
} else if (targetEntity.has<ItemComponent>()) {
|
||||
// Show "E - Pick up [ItemName]" for items
|
||||
@@ -487,7 +507,36 @@ void ActuatorSystem::update(float deltaTime)
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
m_eHoldTime += deltaTime;
|
||||
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
if (targetEntity.has<DoorComponent>()) {
|
||||
// Doors toggle open/close on E and optionally
|
||||
// run the configured action; scene switching
|
||||
// doors queue a scene switch instead
|
||||
if (input.ePressed) {
|
||||
auto &door =
|
||||
targetEntity.get_mut<DoorComponent>();
|
||||
if (!door.sceneSwitchPath.empty()) {
|
||||
SceneSwitchOptions opts;
|
||||
opts.targetEntityName =
|
||||
door.sceneSwitchTarget;
|
||||
m_editorApp->switchScene(
|
||||
door.sceneSwitchPath,
|
||||
opts);
|
||||
} else {
|
||||
door.toggleRequested = true;
|
||||
if (!actuator.actionNames
|
||||
.empty() &&
|
||||
!actuator.actionNames[0]
|
||||
.empty()) {
|
||||
executeAction(
|
||||
playerCharacter,
|
||||
targetEntity,
|
||||
actuator.actionNames
|
||||
[0]);
|
||||
}
|
||||
}
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
if (input.ePressed) {
|
||||
executeAction(playerCharacter,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "CellGridSystem.hpp"
|
||||
#include "FurnitureLibrary.hpp"
|
||||
#include "PhysicsSystem.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
@@ -7,6 +8,9 @@
|
||||
#include "../components/StaticGeometryMember.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include <OgreStaticGeometry.h>
|
||||
#include "../components/ProceduralMaterial.hpp"
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
@@ -29,6 +33,7 @@
|
||||
#include <ProceduralConeGenerator.h>
|
||||
#include <ProceduralLathe.h>
|
||||
#include <ProceduralShape.h>
|
||||
#include <set>
|
||||
|
||||
CellGridSystem::CellGridSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
@@ -583,6 +588,18 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
|
||||
"CellGrid: Unknown error building furniture");
|
||||
}
|
||||
|
||||
// Build door leaf entities (one per unique doorway)
|
||||
try {
|
||||
buildDoorEntities(entity, grid, materialName, materialEntity);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Error building doors: " +
|
||||
std::string(e.what()));
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Unknown error building doors");
|
||||
}
|
||||
|
||||
// Build combined static geometry (frames + furniture + roofs)
|
||||
auto sgIt = m_entityMeshes.find(entity.id());
|
||||
if (sgIt != m_entityMeshes.end() && sgIt->second.frameStaticGeometry) {
|
||||
@@ -2681,6 +2698,9 @@ void CellGridSystem::destroyCellGridMeshes(flecs::entity entity)
|
||||
// Destroy frames
|
||||
destroyFrames(entity);
|
||||
|
||||
// Destroy door entities
|
||||
destroyDoorEntities(entity);
|
||||
|
||||
// Destroy physics colliders
|
||||
destroyPhysicsColliders(entity);
|
||||
|
||||
@@ -4560,3 +4580,399 @@ void CellGridSystem::placeDoorFramesInStaticGeometry(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Door leaf entities (one per unique doorway, swing open/closed)
|
||||
// ============================================================================
|
||||
|
||||
void CellGridSystem::createDoorLeafMesh(const CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
const std::string &meshPrefix,
|
||||
flecs::entity materialEntity)
|
||||
{
|
||||
std::string leafMeshName = meshPrefix + "door_leaf";
|
||||
|
||||
auto &meshMgr = Ogre::MeshManager::getSingleton();
|
||||
if (meshMgr.resourceExists(leafMeshName)) {
|
||||
meshMgr.remove(leafMeshName);
|
||||
}
|
||||
|
||||
// Leaf dimensions: fit inside the door frame opening
|
||||
// (door frame: width 2.8 * cellSize/4, height 3.0 * cellHeight/4,
|
||||
// frame post thickness 0.1)
|
||||
const float doorWidth = 2.8f * (grid.cellSize / 4.0f);
|
||||
const float doorHeight = 3.0f * (grid.cellHeight / 4.0f);
|
||||
float leafWidth = doorWidth - 2.0f * 0.1f - 0.04f;
|
||||
float leafHeight = doorHeight - 0.05f;
|
||||
float leafThickness = 0.08f;
|
||||
|
||||
// Origin at the hinge edge: the leaf spans x in [0, leafWidth],
|
||||
// y in [0, leafHeight]
|
||||
Procedural::TriangleBuffer leafTb;
|
||||
Procedural::BoxGenerator()
|
||||
.setSizeX(leafWidth)
|
||||
.setSizeY(leafHeight)
|
||||
.setSizeZ(leafThickness)
|
||||
.setNumSegX(1)
|
||||
.setNumSegY(2)
|
||||
.setNumSegZ(1)
|
||||
.setPosition(
|
||||
Ogre::Vector3(leafWidth / 2.0f, leafHeight / 2.0f, 0))
|
||||
.setEnableNormals(true)
|
||||
.addToTriangleBuffer(leafTb);
|
||||
|
||||
applyUVMappingToBuffer(leafTb, grid.doorRectName, materialEntity);
|
||||
Ogre::MeshPtr leafMesh = leafTb.transformToMesh(leafMeshName);
|
||||
if (!materialName.empty() && leafMesh->getNumSubMeshes() > 0) {
|
||||
leafMesh->getSubMesh(0)->setMaterialName(materialName);
|
||||
}
|
||||
generateLodForMesh(leafMesh);
|
||||
}
|
||||
|
||||
void CellGridSystem::buildDoorEntities(flecs::entity entity,
|
||||
const CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
flecs::entity materialEntity)
|
||||
{
|
||||
destroyDoorEntities(entity);
|
||||
|
||||
if (!grid.doorsEnabled)
|
||||
return;
|
||||
|
||||
if (!entity.has<TransformComponent>())
|
||||
return;
|
||||
auto &transform = entity.get<TransformComponent>();
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
auto &meshData = m_entityMeshes[entity.id()];
|
||||
|
||||
// Resolve the door leaf mesh: custom mesh or procedural leaf.
|
||||
// Custom door meshes must be modeled with the hinge edge at the
|
||||
// origin, the leaf spanning +X and +Y.
|
||||
std::string meshPrefix =
|
||||
"CellGrid_" + std::to_string(entity.id()) + "_";
|
||||
std::string leafMeshName;
|
||||
bool usingCustomMesh = false;
|
||||
bool useMeshMaterial = false;
|
||||
Ogre::AxisAlignedBox customBounds;
|
||||
if (!grid.doorMeshName.empty()) {
|
||||
auto &meshMgr = Ogre::MeshManager::getSingleton();
|
||||
Ogre::MeshPtr customMesh =
|
||||
meshMgr.getByName(grid.doorMeshName);
|
||||
if (!customMesh) {
|
||||
Ogre::String group =
|
||||
Ogre::ResourceGroupManager::getSingleton()
|
||||
.findGroupContainingResource(
|
||||
grid.doorMeshName);
|
||||
if (!group.empty()) {
|
||||
customMesh = meshMgr.load(grid.doorMeshName,
|
||||
group);
|
||||
}
|
||||
}
|
||||
if (customMesh) {
|
||||
leafMeshName = grid.doorMeshName;
|
||||
usingCustomMesh = true;
|
||||
useMeshMaterial = grid.doorUseMeshMaterial;
|
||||
customBounds = customMesh->getBounds();
|
||||
generateLodForMesh(customMesh);
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Could not load door mesh '" +
|
||||
grid.doorMeshName +
|
||||
"', falling back to procedural door");
|
||||
}
|
||||
}
|
||||
if (leafMeshName.empty()) {
|
||||
createDoorLeafMesh(grid, materialName, meshPrefix,
|
||||
materialEntity);
|
||||
leafMeshName = meshPrefix + "door_leaf";
|
||||
meshData.doorLeafMesh = leafMeshName;
|
||||
}
|
||||
|
||||
// Leaf dimensions (procedural leaf - must match createDoorLeafMesh)
|
||||
const float doorWidth = 2.8f * (grid.cellSize / 4.0f);
|
||||
const float doorHeight = 3.0f * (grid.cellHeight / 4.0f);
|
||||
float leafWidth = doorWidth - 2.0f * 0.1f - 0.04f;
|
||||
float leafHeight = doorHeight - 0.05f;
|
||||
float leafThickness = 0.08f;
|
||||
|
||||
const float halfCell = grid.cellSize / 2.0f;
|
||||
const float frameOffset = 0.1f;
|
||||
|
||||
// One door per unique doorway: the two cells sharing a door edge
|
||||
// both carry a door flag, so dedupe on a canonical edge key.
|
||||
std::set<std::string> placedDoors;
|
||||
|
||||
// side: 0 = Z-, 1 = Z+, 2 = X-, 3 = X+
|
||||
auto tryPlaceDoor = [&](const Cell &cell, int side, bool internal) {
|
||||
// Canonical key of the shared cell edge
|
||||
std::string key;
|
||||
if (side == 2)
|
||||
key = "X:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else if (side == 3)
|
||||
key = "X:" + std::to_string(cell.x + 1) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else if (side == 0)
|
||||
key = "Z:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else
|
||||
key = "Z:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z + 1);
|
||||
if (!placedDoors.insert(key).second)
|
||||
return; // doorway already has a door
|
||||
|
||||
// Same placement as the door frame for this side
|
||||
// (see placeDoorFramesInStaticGeometry)
|
||||
Ogre::Vector3 origin =
|
||||
grid.cellToWorld(cell.x, cell.y, cell.z);
|
||||
float yBase = internal ? 0.1f : 0.0f;
|
||||
Ogre::Vector3 pos;
|
||||
Ogre::Quaternion rot;
|
||||
switch (side) {
|
||||
case 2: // X-
|
||||
pos = origin +
|
||||
Ogre::Vector3(internal ? -halfCell + frameOffset :
|
||||
-halfCell - frameOffset,
|
||||
yBase, 0);
|
||||
rot = Ogre::Quaternion(Ogre::Degree(-90),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
break;
|
||||
case 3: // X+
|
||||
pos = origin +
|
||||
Ogre::Vector3(internal ? halfCell - frameOffset :
|
||||
halfCell + frameOffset,
|
||||
yBase, 0);
|
||||
rot = Ogre::Quaternion(Ogre::Degree(90),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
break;
|
||||
case 0: // Z-
|
||||
pos = origin +
|
||||
Ogre::Vector3(0, yBase,
|
||||
internal ? -halfCell + frameOffset :
|
||||
-halfCell - frameOffset);
|
||||
rot = Ogre::Quaternion(Ogre::Degree(180),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
break;
|
||||
default: // Z+
|
||||
pos = origin +
|
||||
Ogre::Vector3(0, yBase,
|
||||
internal ? halfCell - frameOffset :
|
||||
halfCell + frameOffset);
|
||||
rot = Ogre::Quaternion(Ogre::Degree(0),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
break;
|
||||
}
|
||||
|
||||
Ogre::Entity *leafEnt = nullptr;
|
||||
try {
|
||||
leafEnt = m_sceneMgr->createEntity(leafMeshName);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Error creating door entity: " +
|
||||
std::string(e.what()));
|
||||
return;
|
||||
}
|
||||
if (!leafEnt)
|
||||
return;
|
||||
if (!useMeshMaterial && !materialName.empty())
|
||||
leafEnt->setMaterialName(materialName);
|
||||
|
||||
// The hinge node is the rotation pivot (DoorSystem rotates it
|
||||
// around local Y), so it sits on the side edge of the doorway
|
||||
// with the leaf extending +X from it in door-local space, not
|
||||
// in the doorway center. Its height is also used for the
|
||||
// actuator prompt position.
|
||||
Ogre::Vector3 hingePos;
|
||||
Ogre::Vector3 colliderCenter;
|
||||
Ogre::Vector3 colliderHalfExtents;
|
||||
if (usingCustomMesh) {
|
||||
// Custom mesh: hinge at the mesh origin (mesh spans
|
||||
// +X/+Y from it); shift the hinge so the leaf is
|
||||
// centered in the doorway when closed
|
||||
Ogre::Vector3 center = Ogre::Vector3::ZERO;
|
||||
if (!customBounds.isNull() &&
|
||||
customBounds.isFinite()) {
|
||||
center = customBounds.getCenter();
|
||||
colliderHalfExtents =
|
||||
customBounds.getHalfSize();
|
||||
} else {
|
||||
colliderHalfExtents =
|
||||
Ogre::Vector3(0.5f, 1.5f, 0.05f);
|
||||
}
|
||||
hingePos = pos - rot * Ogre::Vector3(center.x, 0, 0);
|
||||
hingePos.y = pos.y;
|
||||
colliderCenter = center;
|
||||
} else {
|
||||
// Procedural leaf: mesh spans [0, leafWidth] x
|
||||
// [0, leafHeight] from the hinge edge; the child node
|
||||
// centers it vertically on the hinge
|
||||
hingePos = pos -
|
||||
rot * Ogre::Vector3(leafWidth / 2.0f, 0, 0);
|
||||
hingePos.y = pos.y + leafHeight / 2.0f;
|
||||
colliderHalfExtents =
|
||||
Ogre::Vector3(leafWidth / 2.0f,
|
||||
leafHeight / 2.0f,
|
||||
leafThickness / 2.0f);
|
||||
colliderCenter =
|
||||
Ogre::Vector3(leafWidth / 2.0f, 0, 0);
|
||||
}
|
||||
|
||||
Ogre::SceneNode *hingeNode =
|
||||
transform.node->createChildSceneNode();
|
||||
hingeNode->setPosition(hingePos);
|
||||
hingeNode->setOrientation(rot);
|
||||
|
||||
if (usingCustomMesh) {
|
||||
hingeNode->attachObject(leafEnt);
|
||||
} else {
|
||||
Ogre::SceneNode *leafNode =
|
||||
hingeNode->createChildSceneNode();
|
||||
leafNode->setPosition(
|
||||
Ogre::Vector3(0, -leafHeight / 2.0f, 0));
|
||||
leafNode->attachObject(leafEnt);
|
||||
}
|
||||
|
||||
// Door entity (runtime - no EditorMarkerComponent, so it is
|
||||
// neither serialized nor editable; it dies with the grid
|
||||
// through the ChildOf cascade)
|
||||
flecs::entity doorEntity = m_world.entity();
|
||||
doorEntity.child_of(entity);
|
||||
doorEntity.set<TransformComponent>(
|
||||
{hingeNode, hingePos, rot, Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
RenderableComponent renderable;
|
||||
renderable.entity = leafEnt;
|
||||
renderable.meshName = leafMeshName;
|
||||
doorEntity.set<RenderableComponent>(renderable);
|
||||
|
||||
DoorComponent door;
|
||||
door.openAngle = grid.doorOpenAngle;
|
||||
door.openSpeed = grid.doorOpenSpeed;
|
||||
door.closedOrientation = rot;
|
||||
door.centerOffset = colliderCenter;
|
||||
door.sceneSwitchPath = grid.doorSceneSwitchPath;
|
||||
door.sceneSwitchTarget = grid.doorSceneSwitchTarget;
|
||||
doorEntity.set<DoorComponent>(door);
|
||||
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
rb.friction = grid.friction;
|
||||
doorEntity.set<RigidBodyComponent>(rb);
|
||||
|
||||
ActuatorComponent actuator;
|
||||
actuator.radius = 1.5f;
|
||||
actuator.height = 1.8f;
|
||||
if (!grid.doorActionName.empty())
|
||||
actuator.actionNames.push_back(grid.doorActionName);
|
||||
doorEntity.set<ActuatorComponent>(actuator);
|
||||
|
||||
// Box collider for the closed door (child of the door
|
||||
// entity with zero local offset, shape offset to the leaf
|
||||
// center); DoorSystem disables the body while the door is
|
||||
// not fully closed
|
||||
flecs::entity colliderEntity = m_world.entity();
|
||||
colliderEntity.child_of(doorEntity);
|
||||
colliderEntity.set<TransformComponent>(
|
||||
{nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
PhysicsColliderComponent collider;
|
||||
collider.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
collider.parameters = colliderHalfExtents;
|
||||
collider.offset = colliderCenter;
|
||||
colliderEntity.set<PhysicsColliderComponent>(collider);
|
||||
|
||||
meshData.doorEntities.push_back(doorEntity);
|
||||
};
|
||||
|
||||
for (const auto &cell : grid.cells) {
|
||||
if (cell.hasFlag(CellFlags::DoorXNeg))
|
||||
tryPlaceDoor(cell, 2, false);
|
||||
if (cell.hasFlag(CellFlags::DoorXPos))
|
||||
tryPlaceDoor(cell, 3, false);
|
||||
if (cell.hasFlag(CellFlags::DoorZNeg))
|
||||
tryPlaceDoor(cell, 0, false);
|
||||
if (cell.hasFlag(CellFlags::DoorZPos))
|
||||
tryPlaceDoor(cell, 1, false);
|
||||
if (cell.hasFlag(CellFlags::IntDoorXNeg))
|
||||
tryPlaceDoor(cell, 2, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorXPos))
|
||||
tryPlaceDoor(cell, 3, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorZNeg))
|
||||
tryPlaceDoor(cell, 0, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorZPos))
|
||||
tryPlaceDoor(cell, 1, true);
|
||||
}
|
||||
|
||||
if (!meshData.doorEntities.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Created " +
|
||||
std::to_string(meshData.doorEntities.size()) +
|
||||
" door entities for entity " +
|
||||
std::to_string(entity.id()));
|
||||
}
|
||||
}
|
||||
|
||||
void CellGridSystem::destroyDoorEntities(flecs::entity entity)
|
||||
{
|
||||
auto it = m_entityMeshes.find(entity.id());
|
||||
if (it == m_entityMeshes.end())
|
||||
return;
|
||||
|
||||
for (auto doorEntity : it->second.doorEntities) {
|
||||
if (!doorEntity.is_valid() || !doorEntity.is_alive())
|
||||
continue;
|
||||
|
||||
// Remove the physics body before the entity dies
|
||||
if (doorEntity.has<RigidBodyComponent>() &&
|
||||
m_physicsSystem) {
|
||||
auto &rb = doorEntity.get_mut<RigidBodyComponent>();
|
||||
if (rb.bodyCreated)
|
||||
m_physicsSystem->removeRigidBody(rb);
|
||||
}
|
||||
|
||||
// Destroy the leaf renderable and the hinge node (the leaf
|
||||
// child node dies with it)
|
||||
if (doorEntity.has<RenderableComponent>()) {
|
||||
auto &renderable =
|
||||
doorEntity.get_mut<RenderableComponent>();
|
||||
if (renderable.entity) {
|
||||
try {
|
||||
m_sceneMgr->destroyEntity(
|
||||
renderable.entity);
|
||||
} catch (...) {
|
||||
}
|
||||
renderable.entity = nullptr;
|
||||
}
|
||||
}
|
||||
if (doorEntity.has<TransformComponent>()) {
|
||||
auto &t = doorEntity.get_mut<TransformComponent>();
|
||||
if (t.node) {
|
||||
try {
|
||||
m_sceneMgr->destroySceneNode(t.node);
|
||||
} catch (...) {
|
||||
}
|
||||
t.node = nullptr;
|
||||
}
|
||||
}
|
||||
doorEntity.destruct();
|
||||
}
|
||||
it->second.doorEntities.clear();
|
||||
|
||||
// Remove the procedural leaf mesh (custom meshes are shared
|
||||
// resources and are not removed)
|
||||
if (!it->second.doorLeafMesh.empty()) {
|
||||
try {
|
||||
Ogre::MeshManager::getSingleton().remove(
|
||||
it->second.doorLeafMesh);
|
||||
} catch (...) {
|
||||
}
|
||||
it->second.doorLeafMesh.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ public:
|
||||
// Force rebuild of a specific cell grid
|
||||
void rebuildCellGrid(flecs::entity entity);
|
||||
|
||||
// Wire the physics system so door rigid bodies can be removed
|
||||
// properly when doors are rebuilt (EditorApp calls this once)
|
||||
void setPhysicsSystem(class EditorPhysicsSystem *physics)
|
||||
{
|
||||
m_physicsSystem = physics;
|
||||
}
|
||||
|
||||
// Create/Update town material
|
||||
void updateTownMaterial(flecs::entity townEntity);
|
||||
|
||||
@@ -75,6 +82,7 @@ public:
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
class EditorPhysicsSystem *m_physicsSystem = nullptr;
|
||||
|
||||
flecs::query<struct CellGridComponent> m_cellGridQuery;
|
||||
flecs::query<struct TownComponent> m_townQuery;
|
||||
@@ -163,6 +171,17 @@ private:
|
||||
const Ogre::Vector3 &parentScale,
|
||||
Ogre::StaticGeometry *staticGeom, int &frameCount);
|
||||
|
||||
// Door leaf entities (one per unique doorway, swing open/closed)
|
||||
void buildDoorEntities(flecs::entity entity,
|
||||
const struct CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
flecs::entity materialEntity);
|
||||
void createDoorLeafMesh(const struct CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
const std::string &meshPrefix,
|
||||
flecs::entity materialEntity);
|
||||
void destroyDoorEntities(flecs::entity entity);
|
||||
|
||||
// Convert triangle buffer to mesh
|
||||
Ogre::MeshPtr convertToMesh(const std::string &name,
|
||||
Procedural::TriangleBuffer &tb,
|
||||
@@ -241,6 +260,10 @@ private:
|
||||
std::vector<ColliderPlacement> furniturePlacements;
|
||||
std::vector<flecs::entity> isolatedFurnitureEntities;
|
||||
flecs::entity physicsParentEntity = flecs::entity::null();
|
||||
|
||||
// Door leaf entities (runtime, one per unique doorway)
|
||||
std::vector<flecs::entity> doorEntities;
|
||||
std::string doorLeafMesh; // procedural leaf mesh (custom mesh not owned)
|
||||
};
|
||||
std::unordered_map<uint64_t, MeshData> m_entityMeshes;
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "DoorSystem.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include <OgreSceneNode.h>
|
||||
#include <algorithm>
|
||||
|
||||
DoorSystem::DoorSystem(flecs::world &world)
|
||||
: m_world(world)
|
||||
, m_doorQuery(world.query<DoorComponent, TransformComponent>())
|
||||
{
|
||||
}
|
||||
|
||||
DoorSystem::~DoorSystem() = default;
|
||||
|
||||
void DoorSystem::update(float deltaTime)
|
||||
{
|
||||
m_doorQuery.each([&](flecs::entity entity, DoorComponent &door,
|
||||
TransformComponent &transform) {
|
||||
// Consume toggle requests (from ActuatorSystem or scripts)
|
||||
if (door.toggleRequested) {
|
||||
door.toggleRequested = false;
|
||||
door.isOpen = !door.isOpen;
|
||||
// Opening: remove the collider immediately
|
||||
if (door.isOpen && entity.has<RigidBodyComponent>()) {
|
||||
auto &rb = entity.get_mut<RigidBodyComponent>();
|
||||
rb.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
float targetAngle = door.isOpen ? door.openAngle : 0.0f;
|
||||
if (door.currentAngle == targetAngle)
|
||||
return;
|
||||
|
||||
// Swing towards the target angle
|
||||
float step = door.openSpeed * deltaTime;
|
||||
if (door.currentAngle < targetAngle) {
|
||||
door.currentAngle =
|
||||
std::min(door.currentAngle + step, targetAngle);
|
||||
} else {
|
||||
door.currentAngle =
|
||||
std::max(door.currentAngle - step, targetAngle);
|
||||
}
|
||||
|
||||
if (transform.node) {
|
||||
transform.node->setOrientation(
|
||||
door.closedOrientation *
|
||||
Ogre::Quaternion(
|
||||
Ogre::Degree(door.currentAngle),
|
||||
Ogre::Vector3::UNIT_Y));
|
||||
}
|
||||
|
||||
// Fully closed again: restore the collider in the closed pose
|
||||
if (!door.isOpen && door.currentAngle == 0.0f &&
|
||||
entity.has<RigidBodyComponent>()) {
|
||||
auto &rb = entity.get_mut<RigidBodyComponent>();
|
||||
rb.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef EDITSCENE_DOOR_SYSTEM_HPP
|
||||
#define EDITSCENE_DOOR_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* Door system - animates CellGrid door entities.
|
||||
*
|
||||
* Doors are runtime entities spawned by CellGridSystem (one per unique
|
||||
* doorway). This system consumes DoorComponent::toggleRequested (set by
|
||||
* ActuatorSystem on player interaction), swings the hinge node around
|
||||
* local Y at the configured speed, and disables the door's
|
||||
* RigidBodyComponent while the door is not fully closed (the collider
|
||||
* only exists in the closed pose).
|
||||
*/
|
||||
class DoorSystem {
|
||||
public:
|
||||
explicit DoorSystem(flecs::world &world);
|
||||
~DoorSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
flecs::query<struct DoorComponent, struct TransformComponent>
|
||||
m_doorQuery;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_SYSTEM_HPP
|
||||
@@ -2594,6 +2594,17 @@ nlohmann::json SceneSerializer::serializeCellGrid(flecs::entity entity)
|
||||
json["roofSideRectName"] = grid.roofSideRectName;
|
||||
json["friction"] = grid.friction;
|
||||
|
||||
// Serialize door settings
|
||||
json["doorsEnabled"] = grid.doorsEnabled;
|
||||
json["doorRectName"] = grid.doorRectName;
|
||||
json["doorMeshName"] = grid.doorMeshName;
|
||||
json["doorUseMeshMaterial"] = grid.doorUseMeshMaterial;
|
||||
json["doorOpenAngle"] = grid.doorOpenAngle;
|
||||
json["doorOpenSpeed"] = grid.doorOpenSpeed;
|
||||
json["doorActionName"] = grid.doorActionName;
|
||||
json["doorSceneSwitchPath"] = grid.doorSceneSwitchPath;
|
||||
json["doorSceneSwitchTarget"] = grid.doorSceneSwitchTarget;
|
||||
|
||||
// Serialize cells
|
||||
nlohmann::json cellsJson = nlohmann::json::array();
|
||||
for (const auto &cell : grid.cells) {
|
||||
@@ -2796,6 +2807,17 @@ void SceneSerializer::deserializeCellGrid(flecs::entity entity,
|
||||
grid.roofSideRectName = json.value("roofSideRectName", "");
|
||||
grid.friction = json.value("friction", 0.5f);
|
||||
|
||||
// Deserialize door settings
|
||||
grid.doorsEnabled = json.value("doorsEnabled", true);
|
||||
grid.doorRectName = json.value("doorRectName", "");
|
||||
grid.doorMeshName = json.value("doorMeshName", "");
|
||||
grid.doorUseMeshMaterial = json.value("doorUseMeshMaterial", false);
|
||||
grid.doorOpenAngle = json.value("doorOpenAngle", 100.0f);
|
||||
grid.doorOpenSpeed = json.value("doorOpenSpeed", 180.0f);
|
||||
grid.doorActionName = json.value("doorActionName", "");
|
||||
grid.doorSceneSwitchPath = json.value("doorSceneSwitchPath", "");
|
||||
grid.doorSceneSwitchTarget = json.value("doorSceneSwitchTarget", "");
|
||||
|
||||
// Deserialize cells
|
||||
if (json.contains("cells") && json["cells"].is_array()) {
|
||||
for (const auto &cellJson : json["cells"]) {
|
||||
|
||||
@@ -1058,7 +1058,16 @@ static int testCellGridComponent(lua_State *L)
|
||||
" height = 5,"
|
||||
" depth = 10,"
|
||||
" cellSize = 1.0,"
|
||||
" cellHeight = 0.5"
|
||||
" cellHeight = 0.5,"
|
||||
" doorsEnabled = false,"
|
||||
" doorRectName = 'door',"
|
||||
" doorMeshName = 'door.mesh',"
|
||||
" doorUseMeshMaterial = true,"
|
||||
" doorOpenAngle = 90.0,"
|
||||
" doorOpenSpeed = 120.0,"
|
||||
" doorActionName = 'knock',"
|
||||
" doorSceneSwitchPath = 'outside.json',"
|
||||
" doorSceneSwitchTarget = 'arrival'"
|
||||
"});"
|
||||
"local c = ecs.get_component(id, 'CellGrid');"
|
||||
"assert(c ~= nil, 'CellGrid should exist');"
|
||||
@@ -1066,7 +1075,23 @@ static int testCellGridComponent(lua_State *L)
|
||||
"assert(c.height == 5, 'wrong height');"
|
||||
"assert(c.depth == 10, 'wrong depth');"
|
||||
"assert(c.cellSize == 1.0, 'wrong cellSize');"
|
||||
"assert(c.cellHeight == 0.5, 'wrong cellHeight')");
|
||||
"assert(c.cellHeight == 0.5, 'wrong cellHeight');"
|
||||
"assert(c.doorsEnabled == false, 'wrong doorsEnabled');"
|
||||
"assert(c.doorRectName == 'door', 'wrong doorRectName');"
|
||||
"assert(c.doorMeshName == 'door.mesh', "
|
||||
"'wrong doorMeshName');"
|
||||
"assert(c.doorUseMeshMaterial == true, "
|
||||
"'wrong doorUseMeshMaterial');"
|
||||
"assert(c.doorOpenAngle == 90.0, "
|
||||
"'wrong doorOpenAngle');"
|
||||
"assert(c.doorOpenSpeed == 120.0, "
|
||||
"'wrong doorOpenSpeed');"
|
||||
"assert(c.doorActionName == 'knock', "
|
||||
"'wrong doorActionName');"
|
||||
"assert(c.doorSceneSwitchPath == 'outside.json', "
|
||||
"'wrong doorSceneSwitchPath');"
|
||||
"assert(c.doorSceneSwitchTarget == 'arrival', "
|
||||
"'wrong doorSceneSwitchTarget')");
|
||||
if (!ok)
|
||||
FAIL("CellGrid component assertion failed");
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
#include "../systems/FurnitureLibrary.hpp"
|
||||
#include <OgreStringConverter.h>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& grid)
|
||||
@@ -55,6 +56,61 @@ bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& gr
|
||||
renderTextureRectEditor(entity, grid);
|
||||
}
|
||||
|
||||
// Door editor
|
||||
if (ImGui::CollapsingHeader("Doors")) {
|
||||
if (ImGui::Checkbox("Enabled", &grid.doorsEnabled)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Angle", &grid.doorOpenAngle,
|
||||
1.0f, 0.0f, 180.0f, "%.0f deg")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Speed", &grid.doorOpenSpeed,
|
||||
1.0f, 1.0f, 720.0f, "%.0f deg/s")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char meshBuffer[256];
|
||||
strncpy(meshBuffer, grid.doorMeshName.c_str(),
|
||||
sizeof(meshBuffer) - 1);
|
||||
meshBuffer[sizeof(meshBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Custom Mesh (optional)", meshBuffer,
|
||||
sizeof(meshBuffer))) {
|
||||
grid.doorMeshName = meshBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Use Mesh Material",
|
||||
&grid.doorUseMeshMaterial)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char actionBuffer[256];
|
||||
strncpy(actionBuffer, grid.doorActionName.c_str(),
|
||||
sizeof(actionBuffer) - 1);
|
||||
actionBuffer[sizeof(actionBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Action on Toggle (optional)",
|
||||
actionBuffer, sizeof(actionBuffer))) {
|
||||
grid.doorActionName = actionBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char sceneBuffer[512];
|
||||
strncpy(sceneBuffer, grid.doorSceneSwitchPath.c_str(),
|
||||
sizeof(sceneBuffer) - 1);
|
||||
sceneBuffer[sizeof(sceneBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Scene Switch Path (optional)",
|
||||
sceneBuffer, sizeof(sceneBuffer))) {
|
||||
grid.doorSceneSwitchPath = sceneBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char targetBuffer[256];
|
||||
strncpy(targetBuffer, grid.doorSceneSwitchTarget.c_str(),
|
||||
sizeof(targetBuffer) - 1);
|
||||
targetBuffer[sizeof(targetBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Teleport Target (optional)",
|
||||
targetBuffer, sizeof(targetBuffer))) {
|
||||
grid.doorSceneSwitchTarget = targetBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Script editor
|
||||
if (ImGui::CollapsingHeader("Generation Script")) {
|
||||
renderScriptEditor(grid);
|
||||
@@ -422,6 +478,9 @@ void CellGridEditor::renderTextureRectEditor(flecs::entity entity, CellGridCompo
|
||||
if (renderRectCombo("Roof Side", grid.roofSideRectName)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (renderRectCombo("Door Leaf", grid.doorRectName)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user