Compare commits
2 Commits
7aa7682e97
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dd1835b4a3 | |||
| 7a388d2a6f |
@@ -196,6 +196,32 @@ cd demos/demo-interior-exterior-dynamics
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
# Demo: forklift sokoban activity in the open world
|
||||
# (demos/demo-sokoban, target demoSokoban). Starts as an exact copy of
|
||||
# demo-interior-exterior-dynamics (same interior/exterior scenes,
|
||||
# streaming terrain, scene-switch doors, --test-switch contract); a
|
||||
# drivable forklift (Jolt VehicleConstraint), pushable crates, target
|
||||
# pads and a completion HUD are added to demo_scene_exterior.json step
|
||||
# by step - see demos/demo-sokoban/PLAN.md (F10 vehicle, F11 sokoban,
|
||||
# F12 HUD status display).
|
||||
cd demos/demo-sokoban
|
||||
./demoSokoban
|
||||
# ...or headless smoke run (one frame, then exit):
|
||||
./demoSokoban --headless --exit-after-first-frame
|
||||
# ...or headless end-to-end checks (judge by the "[test] PASS" line in
|
||||
# stdout; a known pre-existing teardown segfault in WaterPlane RTT
|
||||
# viewport destruction can mask the exit code with 139):
|
||||
./demoSokoban --headless --test-switch
|
||||
# ...vehicle check on the streaming-terrain exterior scene (spawn,
|
||||
# settle, drive forward, brake to a stop; a grounding watchdog
|
||||
# re-teleports the chassis while streaming page colliders build):
|
||||
./demoSokoban --headless --test-vehicle
|
||||
# ...same check on a minimal flat-floor scene
|
||||
# (demo_scene_vehicletest.json, symlinked like the two main scenes):
|
||||
./demoSokoban --headless --test-vehicle demo_scene_vehicletest.json
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
|
||||
# Road wedge/segment self-intersection regression test (no scene needed,
|
||||
# also registered as CTest roadGeometryOverlapTest)
|
||||
@@ -642,7 +668,15 @@ door builder also adds a black occluder covering the doorway, childed to
|
||||
the grid node (it does NOT swing with the hinge) and tracked via
|
||||
`DoorComponent::occluder`; `DoorSystem` hides it while the door is fully
|
||||
closed so the player never sees the missing room interior through the
|
||||
opened doorway before the switch fires. Every scene-switch door gets the
|
||||
opened doorway before the switch fires. A second flat black **gap shield**
|
||||
quad (`CellGridDoorOccluderShield_*`, tracked via `DoorComponent::gapShield`)
|
||||
sits 1 cm behind the closed leaf's back face on the same void side and is
|
||||
visible exactly while the door is fully closed: spanning the leaf box plus
|
||||
a 6 cm margin, it blacks out the leaf/frame clearance slits (0.02 m per
|
||||
side, 0.05 m at the top) that would otherwise show the ungenerated void
|
||||
(sky/terrain) around the closed door; its margin edges end up inside the
|
||||
jamb boxes and wall thickness, so it never pokes out of the frame. Every
|
||||
scene-switch door gets the
|
||||
same open-front tunnel mesh (`CellGridDoorOccluderTunnel_*`, shared per
|
||||
size and side) on the VOID side of the doorway: door-local +Z points
|
||||
outward from the owning cell, where normal grids hide the missing room,
|
||||
@@ -651,7 +685,9 @@ while exteriorOnly grids hide their missing interior inward, so
|
||||
builder default is +1; standalone doors keep it). The tunnel depth covers
|
||||
the fully-open leaf sweep, so the leaf stays visible whether it swings
|
||||
towards or away from the player, and the side/top/bottom walls flared
|
||||
1 cm into the frame keep the darkness hole-free. The shared
|
||||
6 cm into the frame — past the 0.02 m side / 0.05 m top leaf clearances,
|
||||
so their edges sit inside the jamb boxes and no sight line slips between
|
||||
the frame and the tunnel into the void — keep the darkness hole-free. The shared
|
||||
`CellGridDoorOccluderBlack` material keeps lighting ENABLED with
|
||||
all-black colours (diffuse/ambient/specular/emissive) — with lighting
|
||||
disabled RTSS ignores material colours and renders the pass white.
|
||||
@@ -873,6 +909,47 @@ Test: `testNavMeshDoors` in the `--run-terrain-tests` suite (builds a
|
||||
floor + wall + doorway with a real door entity headlessly; path through
|
||||
the doorway, door-area marking, lock blocks, unlock restores).
|
||||
|
||||
### VehicleSystem (F10)
|
||||
|
||||
Drivable vehicles on top of `JPH::VehicleConstraint` +
|
||||
`WheeledVehicleController` (raycast `VehicleCollisionTester`). An
|
||||
entity with `VehicleComponent` + a dynamic `RigidBodyComponent` (box
|
||||
collider) gets a vehicle constraint on its rigid body;
|
||||
`VehicleSystem::prePhysicsUpdate()` feeds `inputForward` / `inputRight`
|
||||
/ `inputBrake` / `inputHandBrake` into Jolt (with the Jolt sample's
|
||||
brake-before-reverse rule) and `postPhysicsUpdate()` moves wheel visual
|
||||
child nodes (created from `wheelMeshName`) to the constraint's wheel
|
||||
transforms. Both hooks are called from `EditorApp` around the physics
|
||||
step. Any number of vehicles per scene; removal of the component or
|
||||
the body tears the constraint down safely.
|
||||
|
||||
`VehicleComponent` fields (serialized as the `vehicle` scene key,
|
||||
editable via `ui/VehicleEditor`): `maxTorque`, `maxPitchRollAngleDeg`,
|
||||
`seatOffset`, `wheelMeshName`, and a `wheels` list — per wheel
|
||||
`position` (chassis-local), `radius`, `width`, `suspensionMinLength` /
|
||||
`suspensionMaxLength` / `suspensionFrequency` / `suspensionDamping`,
|
||||
`maxSteerAngleDeg` (0 = fixed), `driven`, `maxHandBrakeTorque`. Keep
|
||||
`maxTorque` modest (the demo forklift uses 120; large values through
|
||||
1st gear cause wheelies). The wrapper splits the differential
|
||||
`mEngineTorqueRatio` evenly across driven wheels (Jolt asserts the
|
||||
ratios sum to 1).
|
||||
|
||||
Gotchas found while landing this (demo-sokoban Phase 1):
|
||||
|
||||
- `EditorPhysicsSystem::buildCompoundShape()` places the rigid-body
|
||||
entity's *own* collider at the compound origin (intra-entity offset
|
||||
via `collider.offset` only); child colliders keep their local
|
||||
transforms. It previously offset the own collider by the entity's
|
||||
world position, stranding dynamic-body colliders at 2x their world
|
||||
position (invisible near the origin, fatal far away — e.g. on the
|
||||
streaming terrain).
|
||||
- Headless runs need `EditorApp::setFixedDeltaTime(1/60)` (used by the
|
||||
demo's test modes): sub-millisecond headless frames otherwise starve
|
||||
the fixed-step accumulator and the soft suspension position-solve
|
||||
misbehaves.
|
||||
- `EditorPhysicsSystem::update()` clamps deltaTime to 0.1 s so the
|
||||
post-load hitch cannot trigger huge catch-up steps.
|
||||
|
||||
### SceneScriptComponent & SceneScriptSystem
|
||||
|
||||
Attaches a Lua script to a scene or prefab entity. Fields:
|
||||
|
||||
@@ -20,6 +20,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/EditorUISystem.cpp
|
||||
systems/SceneSerializer.cpp
|
||||
systems/PhysicsSystem.cpp
|
||||
systems/VehicleSystem.cpp
|
||||
systems/BuoyancySystem.cpp
|
||||
systems/EditorSunSystem.cpp
|
||||
systems/EditorSkyboxSystem.cpp
|
||||
@@ -105,6 +106,7 @@ set(EDITSCENE_SOURCES
|
||||
ui/RenderableEditor.cpp
|
||||
ui/PhysicsColliderEditor.cpp
|
||||
ui/RigidBodyEditor.cpp
|
||||
ui/VehicleEditor.cpp
|
||||
ui/LightEditor.cpp
|
||||
ui/CameraEditor.cpp
|
||||
ui/LodEditor.cpp
|
||||
@@ -187,6 +189,7 @@ set(EDITSCENE_SOURCES
|
||||
components/TransformModule.cpp
|
||||
components/RenderableModule.cpp
|
||||
components/RigidBodyModule.cpp
|
||||
components/VehicleModule.cpp
|
||||
components/PhysicsColliderModule.cpp
|
||||
components/PrefabInstanceModule.cpp
|
||||
components/CharacterIdentityModule.cpp
|
||||
@@ -1051,3 +1054,4 @@ add_subdirectory(demos/demo-character-controller)
|
||||
add_subdirectory(demos/demo-scene-switching)
|
||||
add_subdirectory(demos/demo-scene-switching-extra)
|
||||
add_subdirectory(demos/demo-interior-exterior-dynamics)
|
||||
add_subdirectory(demos/demo-sokoban)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "GameMode.hpp"
|
||||
#include <OgreMaterialManager.h>
|
||||
#include "systems/EditorUISystem.hpp"
|
||||
#include "systems/VehicleSystem.hpp"
|
||||
#include "systems/PhysicsSystem.hpp"
|
||||
#include "systems/BuoyancySystem.hpp"
|
||||
#include "systems/EditorSunSystem.hpp"
|
||||
@@ -427,6 +428,8 @@ void EditorApp::destroyEditorSystems()
|
||||
m_skyboxSystem.reset();
|
||||
m_sunSystem.reset();
|
||||
m_buoyancySystem.reset();
|
||||
/* Vehicles hold physics step listeners; tear down before physics. */
|
||||
m_vehicleSystem.reset();
|
||||
m_physicsSystem.reset();
|
||||
|
||||
/* Flush the RTShader generator cache before clearing materials. This
|
||||
@@ -530,6 +533,10 @@ void EditorApp::setup()
|
||||
if (m_uiSystem)
|
||||
m_uiSystem->setPhysicsSystem(m_physicsSystem.get());
|
||||
|
||||
/* F10 vehicles (needs the physics wrapper). */
|
||||
m_vehicleSystem = std::make_unique<VehicleSystem>(
|
||||
m_world, m_physicsSystem->getPhysicsWrapper());
|
||||
|
||||
// Setup buoyancy system (requires physics system)
|
||||
// Get the physics wrapper from the physics system
|
||||
m_buoyancySystem = std::make_unique<BuoyancySystem>(
|
||||
@@ -2109,6 +2116,14 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
{
|
||||
bool paused = (m_gamePlayState == GamePlayState::Paused);
|
||||
|
||||
/* Headless test runs can pin the frame delta so physics and
|
||||
* gameplay systems step deterministically (a headless frame takes
|
||||
* ~1ms of wall time, which would otherwise feed tiny deltas to the
|
||||
* fixed-step physics accumulator). */
|
||||
Ogre::FrameEvent fixedEvt = evt;
|
||||
if (m_fixedDeltaTime > 0.0f)
|
||||
fixedEvt.timeSinceLastFrame = m_fixedDeltaTime;
|
||||
|
||||
/* A queued scene switch runs before anything else so systems
|
||||
* below already see the new scene. */
|
||||
processPendingSceneSwitch();
|
||||
@@ -2162,14 +2177,14 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
if (m_gameMode == GameMode::Editor) {
|
||||
// Update camera
|
||||
if (m_camera) {
|
||||
m_camera->update(evt.timeSinceLastFrame);
|
||||
m_camera->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
} else if (m_gameMode == GameMode::Game) {
|
||||
if (m_gamePlayState == GamePlayState::Playing) {
|
||||
m_playTime += evt.timeSinceLastFrame;
|
||||
m_playTime += fixedEvt.timeSinceLastFrame;
|
||||
if (m_playerControllerSystem) {
|
||||
m_playerControllerSystem->update(
|
||||
evt.timeSinceLastFrame);
|
||||
fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2185,13 +2200,13 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
}
|
||||
/* --- Animation / procedural generation --- */
|
||||
if (m_animationTreeSystem) {
|
||||
m_animationTreeSystem->update(evt.timeSinceLastFrame);
|
||||
m_animationTreeSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
if (m_behaviorTreeSystem)
|
||||
m_behaviorTreeSystem->update(
|
||||
evt.timeSinceLastFrame);
|
||||
fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
if (m_pathFollowingSystem) {
|
||||
m_pathFollowingSystem->update(evt.timeSinceLastFrame);
|
||||
m_pathFollowingSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
if (m_proceduralMeshSystem) {
|
||||
m_proceduralMeshSystem->update();
|
||||
@@ -2199,7 +2214,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
|
||||
/* --- Terrain update (before static world so navmesh can use terrain) --- */
|
||||
if (m_terrainSystem) {
|
||||
m_terrainSystem->update(evt.timeSinceLastFrame);
|
||||
m_terrainSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Terrain prefab spawners (after terrain so spawn Y-snap
|
||||
@@ -2223,47 +2238,47 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
|
||||
/* --- NavMesh builds after static geometry is ready --- */
|
||||
if (m_navMeshSystem) {
|
||||
m_navMeshSystem->update(evt.timeSinceLastFrame);
|
||||
m_navMeshSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Smart Object system (AI navigation to smart objects) --- */
|
||||
if (m_smartObjectSystem) {
|
||||
m_smartObjectSystem->update(evt.timeSinceLastFrame);
|
||||
m_smartObjectSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- GOAP Planner system (plan generation) --- */
|
||||
if (m_goapPlannerSystem) {
|
||||
m_goapPlannerSystem->update(evt.timeSinceLastFrame);
|
||||
m_goapPlannerSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- GOAP Runner system (plan execution) --- */
|
||||
if (m_goapRunnerSystem) {
|
||||
m_goapRunnerSystem->update(evt.timeSinceLastFrame);
|
||||
m_goapRunnerSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Actuator system (player interaction prompts) --- */
|
||||
if (m_actuatorSystem) {
|
||||
m_actuatorSystem->update(evt.timeSinceLastFrame);
|
||||
m_actuatorSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Door system (door swing animation) --- */
|
||||
if (m_doorSystem) {
|
||||
m_doorSystem->update(evt.timeSinceLastFrame);
|
||||
m_doorSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Standalone doors (rebuild dirty door entities) --- */
|
||||
if (m_standaloneDoorSystem) {
|
||||
m_standaloneDoorSystem->update(evt.timeSinceLastFrame);
|
||||
m_standaloneDoorSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Event Handler system (event-driven BTs) --- */
|
||||
if (m_eventHandlerSystem) {
|
||||
m_eventHandlerSystem->update(evt.timeSinceLastFrame);
|
||||
m_eventHandlerSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Dynamic physics (characters after static world) --- */
|
||||
if (m_characterSystem) {
|
||||
m_characterSystem->update(evt.timeSinceLastFrame);
|
||||
m_characterSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Buoyancy system (before physics so impulse is integrated) --- */
|
||||
@@ -2296,7 +2311,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
}
|
||||
m_buoyancySystem->setCameraPosition(cameraPos);
|
||||
}
|
||||
m_buoyancySystem->update(evt.timeSinceLastFrame);
|
||||
m_buoyancySystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Hair physics root sync (before physics step) --- */
|
||||
@@ -2304,9 +2319,20 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_hairPhysicsSystem->prePhysicsUpdate();
|
||||
}
|
||||
|
||||
/* --- F10 vehicle inputs (before the physics step) --- */
|
||||
if (m_vehicleSystem) {
|
||||
m_vehicleSystem->prePhysicsUpdate(
|
||||
fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Main physics step --- */
|
||||
if (m_physicsSystem) {
|
||||
m_physicsSystem->update(evt.timeSinceLastFrame);
|
||||
m_physicsSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- F10 vehicle wheel visuals (after the physics step) --- */
|
||||
if (m_vehicleSystem) {
|
||||
m_vehicleSystem->postPhysicsUpdate();
|
||||
}
|
||||
|
||||
/* --- Hair physics pose read-back (after physics step) --- */
|
||||
@@ -2322,7 +2348,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
cam = m_sceneMgr->getCamera("PlayerCamera");
|
||||
if (!cam && m_camera)
|
||||
cam = m_camera->getCamera();
|
||||
m_sunSystem->update(evt.timeSinceLastFrame, cam);
|
||||
m_sunSystem->update(fixedEvt.timeSinceLastFrame, cam);
|
||||
}
|
||||
if (m_skyboxSystem) {
|
||||
Ogre::Camera *cam = nullptr;
|
||||
@@ -2338,7 +2364,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
cam = m_sceneMgr->getCamera("PlayerCamera");
|
||||
if (!cam && m_camera)
|
||||
cam = m_camera->getCamera();
|
||||
m_waterPlaneSystem->update(evt.timeSinceLastFrame, cam);
|
||||
m_waterPlaneSystem->update(fixedEvt.timeSinceLastFrame, cam);
|
||||
}
|
||||
if (m_lightSystem) {
|
||||
m_lightSystem->update();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
class VehicleSystem;
|
||||
class EditorCamera;
|
||||
class EditorPhysicsSystem;
|
||||
class EditorLightSystem;
|
||||
@@ -170,6 +171,10 @@ public:
|
||||
void shutdownEditor();
|
||||
void closeApp();
|
||||
|
||||
/* Pin the per-frame delta (seconds) for deterministic headless
|
||||
* tests; 0 (default) uses the wall-clock delta. */
|
||||
void setFixedDeltaTime(float dt) { m_fixedDeltaTime = dt; }
|
||||
|
||||
// OgreBites::InputListener overrides
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
|
||||
@@ -319,6 +324,14 @@ public:
|
||||
{
|
||||
return m_playerControllerSystem.get();
|
||||
}
|
||||
VehicleSystem *getVehicleSystem() const
|
||||
{
|
||||
return m_vehicleSystem.get();
|
||||
}
|
||||
EditorPhysicsSystem *getEditorPhysicsSystem() const
|
||||
{
|
||||
return m_physicsSystem.get();
|
||||
}
|
||||
TerrainPrefabSpawnerSystem *getTerrainPrefabSpawnerSystem() const
|
||||
{
|
||||
return m_terrainPrefabSpawnerSystem.get();
|
||||
@@ -425,6 +438,8 @@ private:
|
||||
// Game systems
|
||||
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
/* F10 vehicles: Jolt VehicleConstraint per VehicleComponent. */
|
||||
std::unique_ptr<VehicleSystem> m_vehicleSystem;
|
||||
|
||||
// State
|
||||
uint16_t m_currentModifiers;
|
||||
@@ -465,6 +480,10 @@ private:
|
||||
int m_sceneSwitchCoverFrames = 0;
|
||||
static const int SCENE_SWITCH_COVER_FRAMES = 45;
|
||||
|
||||
/* When > 0, frameRenderingQueued uses this fixed delta instead of
|
||||
* the wall-clock delta (headless test determinism). */
|
||||
float m_fixedDeltaTime = 0.0f;
|
||||
|
||||
void processPendingSceneSwitch();
|
||||
bool performSceneSwitch(const Ogre::String &scenePath,
|
||||
const SceneSwitchOptions &opts);
|
||||
|
||||
@@ -779,8 +779,18 @@ side of the doorway — door-local +Z points outward from the owning cell
|
||||
`occluderSide = -1` for exteriorOnly grids, whose missing interior is
|
||||
inward. The back panel sits behind the fully-open leaf sweep so the
|
||||
swinging leaf stays visible for both swing directions, and
|
||||
side/top/bottom walls flared 1 cm into the frame keep the darkness
|
||||
hole-free; `DoorSystem` hides the occluder while fully closed. The
|
||||
side/top/bottom walls flared 6 cm into the frame — past the 0.02 m side /
|
||||
0.05 m top leaf clearances, so their edges sit inside the jamb boxes and
|
||||
no sight line slips between the frame and the tunnel into the void — keep the darkness
|
||||
hole-free; `DoorSystem` hides the occluder while fully closed. A second
|
||||
flat black gap-shield quad (`CellGridDoorOccluderShield_*`, tracked via
|
||||
`DoorComponent::gapShield`) sits 1 cm behind the closed leaf's back face
|
||||
on the same void side and is visible exactly while the door is fully
|
||||
closed: spanning the leaf box plus a 6 cm margin, it blacks out the
|
||||
leaf/frame clearance slits (0.02 m per side, 0.05 m at the top) that
|
||||
otherwise show the ungenerated void (sky/terrain) around the closed
|
||||
door; its margin edges end up inside the jamb boxes and wall thickness,
|
||||
so it never pokes out of the frame. The
|
||||
occluder material keeps lighting enabled with all-black
|
||||
colours — an unlit pass renders white under RTSS (FFPColour defaults the
|
||||
output to white when no lighting SRS runs). Scene-switch doors never
|
||||
@@ -822,7 +832,11 @@ sub-millisecond while swings are real-time).
|
||||
Its visibility follows the door state: hidden while fully closed, visible
|
||||
while open/swinging (visible flag toggled by `DoorSystem` from
|
||||
`currentAngle`), so the player never sees the missing room interior
|
||||
behind the opened doorway before the switch fires. The material is lit
|
||||
behind the opened doorway before the switch fires. A complementary flat
|
||||
gap-shield quad just behind the closed leaf (leaf box + 6 cm margin,
|
||||
covering the 0.02 m side / 0.05 m top clearances) is visible exactly
|
||||
while the door is fully closed, blacking out the clearance slits. The
|
||||
material is lit
|
||||
with all-black colours (unlit passes render white under RTSS).
|
||||
- The prompt stays "E Open" (never "E Close") for scene-switch doors, as
|
||||
today.
|
||||
|
||||
@@ -78,6 +78,13 @@ struct DoorComponent {
|
||||
// so it does NOT swing with the hinge. Hidden while the door is
|
||||
// fully closed. Runtime only.
|
||||
Ogre::Entity *occluder = nullptr;
|
||||
|
||||
// F1: flat black gap-shield quad just behind the closed leaf of a
|
||||
// scene-switch door (same void side as the tunnel), visible only
|
||||
// while the door is fully closed - it blacks out the leaf/frame
|
||||
// clearance slits (0.02 m sides, 0.05 m top) the tunnel, hidden
|
||||
// while closed, does not cover. Runtime only.
|
||||
Ogre::Entity *gapShield = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_HPP
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef EDITSCENE_VEHICLE_HPP
|
||||
#define EDITSCENE_VEHICLE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <Jolt/Jolt.h>
|
||||
|
||||
namespace JPH
|
||||
{
|
||||
class VehicleConstraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vehicle wheel definition (F10, see demos/demo-sokoban/PLAN.md).
|
||||
*
|
||||
* Wheels come in axle pairs (0/1 = first axle, 2/3 = second, ...);
|
||||
* engine torque is routed through a differential per pair whose wheels
|
||||
* are both marked driven.
|
||||
*/
|
||||
struct VehicleWheel {
|
||||
/* Suspension attachment point in chassis space. */
|
||||
Ogre::Vector3 position = Ogre::Vector3::ZERO;
|
||||
float radius = 0.3f;
|
||||
float width = 0.15f;
|
||||
float suspensionMinLength = 0.1f;
|
||||
float suspensionMaxLength = 0.4f;
|
||||
float suspensionFrequency = 1.5f;
|
||||
float suspensionDamping = 0.7f;
|
||||
/* 0 = not steered (degrees). */
|
||||
float maxSteerAngleDeg = 0.0f;
|
||||
bool driven = false;
|
||||
/* 0 = no handbrake on this wheel. */
|
||||
float maxHandBrakeTorque = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vehicle component.
|
||||
*
|
||||
* Turns an entity with a dynamic RigidBodyComponent (the chassis) into
|
||||
* a drivable vehicle: VehicleSystem attaches a Jolt VehicleConstraint
|
||||
* with a WheeledVehicleController to the chassis body and feeds the
|
||||
* driver input fields below into it.
|
||||
*
|
||||
* Visuals: the chassis mesh comes from the entity's RenderableComponent
|
||||
* (or Primitive); when wheelMeshName is set, VehicleSystem creates one
|
||||
* child node per wheel and syncs it from the constraint state.
|
||||
*/
|
||||
struct VehicleComponent {
|
||||
std::vector<VehicleWheel> wheels;
|
||||
float maxTorque = 400.0f;
|
||||
/* Degrees; 0 = unlimited. */
|
||||
float maxPitchRollAngleDeg = 60.0f;
|
||||
|
||||
/* Driver seat offset in chassis space (used by the future
|
||||
* enter/exit flow to seat the player character). */
|
||||
Ogre::Vector3 seatOffset = Ogre::Vector3(0.0f, 1.0f, 0.0f);
|
||||
|
||||
/* Wheel visual mesh (Y-axis aligned cylinder); empty = no wheel
|
||||
* visuals. */
|
||||
Ogre::String wheelMeshName;
|
||||
|
||||
/* Driver input, written by the controlling system (the future
|
||||
* VehicleControllerSystem or tests), consumed by VehicleSystem:
|
||||
* forward/right -1..1, brake/handbrake 0..1. */
|
||||
float inputForward = 0.0f;
|
||||
float inputRight = 0.0f;
|
||||
float inputBrake = 0.0f;
|
||||
float inputHandBrake = 0.0f;
|
||||
|
||||
/* Runtime: the Jolt vehicle constraint on the chassis body. */
|
||||
JPH::VehicleConstraint *constraint = nullptr;
|
||||
bool constraintCreated = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_VEHICLE_HPP
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "Vehicle.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/VehicleEditor.hpp"
|
||||
|
||||
/* Default 4-wheel layout when the component is added in the editor:
|
||||
* a small forklift-ish chassis (half extents ~0.7 x 0.4 x 1.2) with
|
||||
* front axle driven and rear axle steered (real forklifts steer with
|
||||
* the rear wheels; see demos/demo-sokoban/PLAN.md open questions). */
|
||||
static VehicleComponent makeDefaultVehicle()
|
||||
{
|
||||
VehicleComponent v;
|
||||
v.wheels.resize(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
VehicleWheel &w = v.wheels[i];
|
||||
bool left = (i % 2) == 0;
|
||||
bool front = i < 2;
|
||||
w.position = Ogre::Vector3(left ? 0.65f : -0.65f, -0.2f,
|
||||
front ? 0.85f : -0.85f);
|
||||
w.radius = 0.3f;
|
||||
w.width = 0.2f;
|
||||
w.suspensionMinLength = 0.05f;
|
||||
w.suspensionMaxLength = 0.3f;
|
||||
w.suspensionFrequency = 2.0f;
|
||||
w.suspensionDamping = 0.8f;
|
||||
w.driven = front;
|
||||
w.maxSteerAngleDeg = front ? 0.0f : 35.0f;
|
||||
w.maxHandBrakeTorque = front ? 0.0f : 200.0f;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Register Vehicle component
|
||||
REGISTER_COMPONENT_GROUP("Vehicle", "Physics", VehicleComponent,
|
||||
VehicleEditor)
|
||||
{
|
||||
registry.registerComponent<VehicleComponent>(
|
||||
"Vehicle", "Physics", std::make_unique<VehicleEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<VehicleComponent>())
|
||||
e.set<VehicleComponent>(makeDefaultVehicle());
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<VehicleComponent>())
|
||||
e.remove<VehicleComponent>();
|
||||
});
|
||||
}
|
||||
@@ -484,7 +484,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
}
|
||||
/* F1: every scene-switch door gets the void-side
|
||||
* tunnel occluder (depth covers the leaf sweep,
|
||||
* so both swing directions stay visible). */
|
||||
* so both swing directions stay visible) plus the
|
||||
* gap shield backing the leaf/frame clearance
|
||||
* slits while the door is closed. */
|
||||
{
|
||||
const DoorComponent &d =
|
||||
findDoorById(exitDoorId)
|
||||
@@ -499,6 +501,49 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
"F1 exit door has no tunnel occluder";
|
||||
break;
|
||||
}
|
||||
if (!d.gapShield ||
|
||||
d.gapShield->getMesh()
|
||||
->getName()
|
||||
.find("CellGridDoorOccluderShield") !=
|
||||
0) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F1 exit door has no gap shield";
|
||||
break;
|
||||
}
|
||||
/* The door is still closed here: the shield
|
||||
* backs the slits, the tunnel stays hidden. */
|
||||
if (!d.gapShield->isVisible() ||
|
||||
d.occluder->isVisible()) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F1 closed-door occluder visibility wrong";
|
||||
break;
|
||||
}
|
||||
/* The tunnel walls must reach past the
|
||||
* leaf/frame clearances (0.02 m sides,
|
||||
* 0.05 m top) or the void shows between the
|
||||
* frame and the black corridor while the
|
||||
* door is open; both meshes are hinge-local,
|
||||
* so the tunnel footprint must cover at
|
||||
* least the gap shield's (6 cm margin). */
|
||||
{
|
||||
Ogre::Vector3 th =
|
||||
d.occluder->getMesh()
|
||||
->getBounds()
|
||||
.getHalfSize();
|
||||
Ogre::Vector3 sh =
|
||||
d.gapShield->getMesh()
|
||||
->getBounds()
|
||||
.getHalfSize();
|
||||
if (th.x < sh.x - 0.001f ||
|
||||
th.y < sh.y - 0.001f) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F1 tunnel walls inside the leaf/frame clearances";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!requestDoorSwitch(exitDoorId))
|
||||
break;
|
||||
@@ -730,7 +775,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* Z:0:0:0 is the way back. E on such a door swings the leaf open first;
|
||||
* the scene switch fires only when the leaf reaches the open angle, and
|
||||
* a black occluder box behind the doorway hides the missing half of the
|
||||
* building while it swings. Both scenes carry their own player
|
||||
* building while it swings; while the door is closed a flat black gap
|
||||
* shield behind the leaf blacks out the leaf/frame clearance slits, so
|
||||
* no sky/terrain bleeds through the closed door. Both scenes carry
|
||||
* their own player
|
||||
* controller, so each switch is a clean takeover (see
|
||||
* EditorApp::performSceneSwitch); travel back and forth is endless.
|
||||
*
|
||||
@@ -752,7 +800,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* persisted and restored across the scene switches) and
|
||||
* both F1 scene-switch doors (interior -> exterior and
|
||||
* back each fire only when the leaf is fully open,
|
||||
* black occluder present), verifying the round-trip
|
||||
* black occluder tunnel + closed-door gap shield
|
||||
* present with the right closed-state visibility),
|
||||
* verifying the round-trip
|
||||
* arrival teleports (the exterior arrival check reads
|
||||
* the expected Y from TerrainSystem::getHeightAt and
|
||||
* polls while the streaming window loads and the
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# demo-sokoban — forklift sokoban activity demo (vehicle + quest HUD)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Starts as an exact copy of demos/demo-interior-exterior-dynamics (same
|
||||
# scene-switch-door setup, same streaming-terrain/sky/water exterior at
|
||||
# world center ~(4000, 4000)); the sokoban activity (drivable forklift,
|
||||
# pushable crates, target pads, completion HUD) is added to
|
||||
# demo_scene_exterior.json step by step - see PLAN.md in this directory.
|
||||
#
|
||||
# The executable is self-contained in this build directory: a POST_BUILD
|
||||
# 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 both demo scenes
|
||||
# (demo_scene_interior.json / demo_scene_exterior.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; the terrain heightmap is staged separately
|
||||
# by configure_file (see below) so source changes re-copy it on the next
|
||||
# build. Run it from here:
|
||||
# cd <build>/src/features/editScene/demos/demo-sokoban
|
||||
# ./demoSokoban
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# 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}/")
|
||||
|
||||
# --- Embedded project configuration (F8 release binary) ---------------
|
||||
# Read project.json at configure time and embed its parameters into the
|
||||
# binary via a generated project.h, so the release binary is attached to
|
||||
# its project directory without needing --project. Editing project.json
|
||||
# re-triggers the CMake configure step (CMAKE_CONFIGURE_DEPENDS).
|
||||
set(PROJECT_JSON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/project.json")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${PROJECT_JSON_PATH}")
|
||||
file(READ "${PROJECT_JSON_PATH}" PROJECT_JSON_TEXT)
|
||||
string(JSON PROJECT_APP_NAME GET "${PROJECT_JSON_TEXT}" appName)
|
||||
string(JSON PROJECT_START_SCENE GET "${PROJECT_JSON_TEXT}" startScene)
|
||||
string(JSON PROJECT_GAME_MODE GET "${PROJECT_JSON_TEXT}" gameMode)
|
||||
if(PROJECT_GAME_MODE)
|
||||
set(PROJECT_GAME_MODE_VALUE 1)
|
||||
else()
|
||||
set(PROJECT_GAME_MODE_VALUE 0)
|
||||
endif()
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/project.h.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/project.h" @ONLY)
|
||||
|
||||
# Terrain heightmap for the exterior scene's terrain entity (terrainId
|
||||
# 4242424300000001, heightmapFile heightmap.bin — TerrainSystem resolves it
|
||||
# as heightmaps/<terrainId>/<heightmapFile> relative to the CWD). Generated
|
||||
# in the source tree by gen_heightmap.py. The terrain runs in streaming
|
||||
# mode (base heights come from baseNoise, not the heightmap), but the file
|
||||
# stays staged so flipping streamingEnabled off keeps working. A
|
||||
# configure_file COPYONLY copy is used on purpose: it registers a configure
|
||||
# dependency on the source file, so regenerating heightmap.bin re-copies it
|
||||
# into the staging directory on the next build without manual intervention.
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/heightmap.bin"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/heightmaps/4242424300000001/heightmap.bin"
|
||||
COPYONLY)
|
||||
|
||||
add_executable(demoSokoban
|
||||
demo_main.cpp
|
||||
${DEMO_SOURCES}
|
||||
)
|
||||
|
||||
target_compile_definitions(demoSokoban
|
||||
PRIVATE EDITSCENE_HAS_EMBEDDED_PROJECT)
|
||||
|
||||
add_dependencies(demoSokoban morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing (same as editor)
|
||||
target_compile_definitions(demoSokoban PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(demoSokoban
|
||||
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(demoSokoban PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}/generated
|
||||
${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 demoSokoban 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-sokoban standalone runtime"
|
||||
)
|
||||
@@ -0,0 +1,362 @@
|
||||
# demo-sokoban — Implementation Plan
|
||||
|
||||
Forklift sokoban activity in the open world of `demo_scene_exterior.json`:
|
||||
the player finds a drivable forklift near the building, drives it (Jolt
|
||||
`VehicleConstraint`), pushes crates onto marked target pads in a walled
|
||||
yard, and completing the layout registers a completed "quest" shown in a
|
||||
reusable HUD status display.
|
||||
|
||||
This demo starts as an exact copy of `demo-interior-exterior-dynamics`
|
||||
(scene-switch doors interior <-> exterior, streaming terrain archipelago,
|
||||
sky, water). All sokoban content is added to the exterior scene near the
|
||||
existing content site at world ~(4000, 10, 4000).
|
||||
|
||||
## Design decisions (agreed with the user)
|
||||
|
||||
- **Quest**: no full quest system yet. Completion is recorded in
|
||||
`GlobalStateStore` (per-instance keys `quest.<id>.completed = true`),
|
||||
a `quest_completed` event is sent on the `EventBus`, and a new reusable
|
||||
HUD status display (top-right corner) shows progress and the
|
||||
completion banner. The HUD display is designed to be reused by a
|
||||
future real quest system.
|
||||
- **Vehicle**: real `JPH::VehicleConstraint` with
|
||||
`WheeledVehicleController` — not a kinematic arcade hack.
|
||||
- **Rules**: classic multi-crate sokoban field — several crates, the
|
||||
same number of target pads, walls around the yard, completion when
|
||||
every pad is covered. Crates are free dynamic bodies pushed by the
|
||||
forklift (no grid snapping of motion; the *layout* is grid-aligned).
|
||||
- **Visuals**: new Blender assets (forklift, crate, target pad)
|
||||
exported through the existing asset pipeline — no procedural
|
||||
primitives for the final look.
|
||||
- Should be implemented in a way it is easy to set up in other demo
|
||||
- It should be possible to play multiple instances of Sokoban in the same demo using the same or different forklifts.
|
||||
- Should use Lua APIs where appropriate (for Sokoban logic at least).
|
||||
- Jolt examples can be a reference for vehicle implementation.
|
||||
|
||||
### Consequences of these decisions (architecture constraints)
|
||||
|
||||
- **Engine-level, not demo-level**: all new components/systems/Lua APIs
|
||||
live in `src/features/editScene` (`components/`, `systems/`, `lua/`,
|
||||
`ui/`) so any demo or scene can use them. `demo-sokoban` itself only
|
||||
contributes scene JSON data, assets and headless test listeners in
|
||||
`demo_main.cpp` — no game logic in the demo binary.
|
||||
- **Instance-based sokoban**: one yard = one game instance with its own
|
||||
id, crate list, pad list and `quest.<id>.*` global-state keys. Any
|
||||
number of yards can coexist in a scene; forklifts and crates are not
|
||||
bound to a yard — anything that pushes a crate onto a pad counts.
|
||||
- **Lua runs the rules**: C++ provides per-frame physics-adjacent
|
||||
detection and emits events; the sokoban rules (what counts as
|
||||
completion, HUD text, rewards, reset) live in a shared Lua module.
|
||||
Note the existing Lua integration is execute-once + event-driven
|
||||
(`SceneScriptSystem`, `EventBus`) with *no per-frame Lua hook* — so
|
||||
per-frame detection stays in C++ and Lua reacts to events; no
|
||||
per-frame Lua polling is added.
|
||||
|
||||
## Existing pieces we build on
|
||||
|
||||
- `physics/physics.h` (`JoltPhysicsWrapper`): body creation (incl.
|
||||
world-space `RVec3` overloads), sensors, per-body contact listeners,
|
||||
raycasts, `getPhysicsSystem()` access to the raw `JPH::PhysicsSystem`.
|
||||
- `components/RigidBody.hpp` + `components/PhysicsCollider.hpp` +
|
||||
`systems/PhysicsSystem.cpp`: static/dynamic/kinematic bodies from
|
||||
scene JSON (`rigidBody`, `collider` keys), physics->node sync for
|
||||
dynamic bodies. The crate needs nothing new.
|
||||
- `components/Actuator.hpp` + `systems/ActuatorSystem.cpp`: E-key
|
||||
interaction prompts; actions run behavior trees from the scene's
|
||||
top-level `actionDatabase`, including `luaTask` nodes — enough to
|
||||
emit a `vehicle_enter` event without new BT node types.
|
||||
- `systems/PlayerControllerSystem.cpp` + `GameInputState`
|
||||
(EditorApp.hpp): game-mode input handling and TPS camera boom to
|
||||
model the vehicle controller/camera on.
|
||||
- `systems/GlobalStateStore.*` (F9): persistent key/value store,
|
||||
survives `switchScene()`, C++/Lua API.
|
||||
- `systems/EventBus.*`: `send`/`subscribe` events, used by the door
|
||||
contract and Lua (`ecs.send_event` / `ecs.subscribe_event`).
|
||||
- `components/SceneScript.hpp` + inline scene scripts: per-scene Lua
|
||||
glue.
|
||||
- Component registration pattern: `components/FooModule.cpp` with
|
||||
`REGISTER_COMPONENT_GROUP` + `ui/FooEditor.hpp` (see
|
||||
`components/RigidBodyModule.cpp`), plus serialization in
|
||||
`systems/SceneSerializer.cpp`.
|
||||
- Demo scaffolding: this directory (CMake target, `stage_runtime.cmake`,
|
||||
embedded `project.json`, symlinked scenes, `--headless
|
||||
--exit-after-first-frame` smoke run, `--test-switch` end-to-end
|
||||
listener pattern in `demo_main.cpp`).
|
||||
- Feature doc convention: `GameFeatures202609.md` (F0-F9 used; this
|
||||
work adds F10/F11/F12).
|
||||
- Jolt vehicle reference implementations, checked out locally:
|
||||
`/media/slapin/library/ogre3/jolt/Samples/Tests/Vehicle/`
|
||||
(`VehicleConstraintTest`, `VehicleTest`, `VehicleSixDOFTest`) — the
|
||||
same Jolt version the SDK is built from (`Jolt/Jolt.h` headers in the
|
||||
ogre-sdk include tree).
|
||||
|
||||
## Pieces that do not exist yet (to build)
|
||||
|
||||
1. Vehicle physics: no `VehicleConstraint` use anywhere; the wrapper
|
||||
has no constraint support at all.
|
||||
2. Vehicle concept: no `VehicleComponent`/`VehicleSystem`, no
|
||||
enter/exit flow, no vehicle input/camera.
|
||||
3. Quest system: none in editScene (only legacy `src/gamedata`, off
|
||||
limits) — deliberately out of scope; only the HUD status display
|
||||
+ global-state record + event are built.
|
||||
4. Sokoban logic: no crate/target/game-state tracking. Split into a
|
||||
generic C++ zone-detection piece (reusable by future activities) and
|
||||
the sokoban rules in Lua.
|
||||
5. Assets: no forklift/crate/pad meshes.
|
||||
6. Lua surface: no `ecs.hud.*` bindings; the HUD status display and its
|
||||
Lua API are new.
|
||||
|
||||
## Phases
|
||||
|
||||
Each phase ends in a buildable, runnable demo; verification commands
|
||||
assume the build tree (e.g. `build-vscode`) is configured.
|
||||
|
||||
### Phase 0 — Demo skeleton (this step)
|
||||
|
||||
Copy of `demo-interior-exterior-dynamics` with target `demoSokoban`,
|
||||
`project.json` appName `demo-sokoban`, registered via
|
||||
`add_subdirectory(demos/demo-sokoban)` in
|
||||
`src/features/editScene/CMakeLists.txt`.
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
cmake --build build-vscode --target demoSokoban -j4
|
||||
cd build-vscode/src/features/editScene/demos/demo-sokoban
|
||||
./demoSokoban --headless --exit-after-first-frame
|
||||
./demoSokoban --headless --test-switch # inherited scene contract
|
||||
```
|
||||
|
||||
### Phase 1 — Vehicle physics foundation (F10) — DONE
|
||||
|
||||
Status: complete; `--test-vehicle` passes on both the flat-floor scene
|
||||
(`demo_scene_vehicletest.json`) and the streaming-terrain exterior scene.
|
||||
|
||||
What landed:
|
||||
|
||||
- `physics/physics.h/.cpp`: `JoltPhysicsWrapper::createVehicle` /
|
||||
`destroyVehicle` / `setVehicleInput` / `getVehicleForwardSpeed` /
|
||||
`getWheelWorldTransform` on top of `JPH::VehicleConstraint` +
|
||||
`WheeledVehicleController` + a raycast `VehicleCollisionTester`;
|
||||
the differential `mEngineTorqueRatio` is split evenly (1/N per driven
|
||||
wheel) so 4WD configs satisfy Jolt's sum-of-torque-ratios assert.
|
||||
- `components/Vehicle.hpp` + `components/VehicleModule.cpp` +
|
||||
`ui/VehicleEditor.{hpp,cpp}`: `VehicleComponent` (wheel list with
|
||||
offset/radius/width/suspension/steer/driven/handbrake per wheel,
|
||||
`maxTorque`, `maxPitchRollAngleDeg`, `seatOffset`, `wheelMeshName`),
|
||||
registered, editable in the editor UI and serialized as the `vehicle`
|
||||
scene key (see `SceneSerializer`).
|
||||
- `systems/VehicleSystem.{hpp,cpp}`: creates/destroys the constraint
|
||||
from `VehicleComponent` + the entity's existing `RigidBodyComponent`
|
||||
body, applies driver input (with the Jolt sample's
|
||||
brake-before-reverse rule) in `prePhysicsUpdate`, and moves wheel
|
||||
visual child nodes from constraint state in `postPhysicsUpdate`
|
||||
(both wired into `EditorApp` around the physics step).
|
||||
- Forklift placeholder entity `forklift1` (id 500) at (4012, 10.8, 3960)
|
||||
in `demo_scene_exterior.json` and (0, 1.5, 0) in the new flat-floor
|
||||
regression scene `demo_scene_vehicletest.json` (symlinked into the
|
||||
build dir by `stage_runtime.cmake` like the two main scenes).
|
||||
Tuning: `maxTorque` 120 (500 Nm through 1st gear produced ~15 kN and
|
||||
wheelies/backflips), front wheels driven, rear wheels steer 35 deg +
|
||||
handbrake (rear-steer like a real forklift — resolves open question
|
||||
4). Drives straight and stable: 0 -> ~8 m/s in 7 s on flat ground.
|
||||
- `--test-vehicle` in `demo_main.cpp` (`VehicleTestListener`):
|
||||
waits for constraint creation, waits for the chassis to settle
|
||||
upright on the terrain (a grounding watchdog re-teleports the chassis
|
||||
to its authored pose while the streaming page colliders are still
|
||||
being built — kept deliberately, it guards the collider race), drives
|
||||
forward 180 frames asserting progress and uprightness, then brakes to
|
||||
a full stop; prints `[test] PASS`. Note: the process then dies with
|
||||
a known pre-existing teardown segfault (WaterPlane RTT viewport
|
||||
destruction, also present in the base demo) that masks the exit code
|
||||
with 139 — judge by the `[test] PASS` line in stdout.
|
||||
- Headless determinism: headless frames are ~1.3 ms wall time, which
|
||||
starved the fixed-step accumulator and made the soft suspension
|
||||
position-solve misbehave; `EditorApp::setFixedDeltaTime(float)` (used
|
||||
by all headless test modes in this demo) pins
|
||||
`evt.timeSinceLastFrame` to 1/60 so physics sees real steps.
|
||||
`EditorPhysicsSystem::update()` also clamps deltaTime > 0.1 to 0.1 so
|
||||
the post-load hitch cannot trigger huge catch-up steps.
|
||||
- Bug fix in `systems/PhysicsSystem.cpp` (`buildCompoundShape`): the
|
||||
rigid-body entity's *own* collider shape was wrapped in a
|
||||
`RotatedTranslatedShape` offset by the entity's world position while
|
||||
the body was also created at that position — the collider ended up at
|
||||
2x the world position, so any dynamic body far from the origin fell
|
||||
through the terrain (invisible near the origin, which is why it went
|
||||
unnoticed; wheel raycasts still hit the terrain, which made it look
|
||||
like a convex-vs-mesh narrowphase failure — a standalone Jolt repro
|
||||
proved the page `MeshShape` itself is fine). The own-collider is now
|
||||
placed at the compound origin; child colliders keep their local
|
||||
offsets.
|
||||
- Debug scene variants used during the hunt
|
||||
(`demo_scene_exterior_nowater/_box/_min/_nostream.json`) were
|
||||
deleted; coverage is the two `--test-vehicle` scenes.
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
cmake --build build-vscode --target demoSokoban -j4
|
||||
cd build-vscode/src/features/editScene/demos/demo-sokoban
|
||||
./demoSokoban --headless --test-vehicle # terrain scene
|
||||
./demoSokoban --headless --test-vehicle demo_scene_vehicletest.json # flat floor
|
||||
./demoSokoban --headless --test-switch # inherited contract
|
||||
./demoSokoban --headless --exit-after-first-frame # smoke
|
||||
```
|
||||
|
||||
|
||||
### Phase 2 — Blender assets
|
||||
|
||||
- `assets/blender/vehicles/forklift.blend`: chassis, mast, fork, 4
|
||||
wheels as separately exported meshes (wheel visuals are moved by
|
||||
`VehicleSystem` from constraint state).
|
||||
- `assets/blender/vehicles/crate.blend` (1 m box, or reuse an existing
|
||||
crate/pallet asset if one fits) and a flat target-pad marker mesh.
|
||||
- Extend/reuse the vehicle export script in `assets/blender/scripts/`
|
||||
(check `export_vehicles.py`) and the CMake asset rules so the meshes
|
||||
land in the staged `resources/` (the demo symlinks it).
|
||||
- Swap the Phase 1 placeholder boxes for the real meshes in the scene.
|
||||
|
||||
Verify: run the demo with `--force-pos` + `--screenshot` (see the base
|
||||
demo's debug capture flags) and inspect the render.
|
||||
|
||||
### Phase 3 — Enter/exit and vehicle controller (F10)
|
||||
|
||||
- `systems/VehicleControllerSystem` (or a mode of
|
||||
`PlayerControllerSystem` — decide during implementation): while
|
||||
driving, reads `GameInputState` (W/S throttle, A/D steer, Space
|
||||
handbrake) and feeds the vehicle; TPS camera boom follows the
|
||||
forklift (same collision-clamped boom as the character camera).
|
||||
- Enter: forklift entity carries an `ActuatorComponent` ("Drive"
|
||||
action); the action's behavior tree runs a `luaTask` that sends
|
||||
`vehicle_enter` (no new BT node types). The controller subscribes on
|
||||
the `EventBus`: on enter, the player character is seated
|
||||
(physics capsule disabled, node attached at the seat offset, mesh
|
||||
optionally hidden) and the vehicle gets the `PlayerControlledComponent`
|
||||
tag; on exit (E while driving) the character is restored beside the
|
||||
forklift.
|
||||
- Guardrails: cannot exit into a wall (raycast for a free spot), pause
|
||||
menu and save/load keep working while driving.
|
||||
|
||||
Verify: `--test-vehicle-drive` headless: emit enter, drive, exit,
|
||||
assert character ends up next to the moved forklift.
|
||||
|
||||
### Phase 4 — Crate (F11)
|
||||
|
||||
- `components/Pushable.hpp` + module + serialization: a small generic
|
||||
marker component (`PushableComponent`) for "a dynamic prop that
|
||||
activities care about" — zone detection keys off it, so any crate can
|
||||
be used by any sokoban instance (or a future activity).
|
||||
- Scene JSON per crate: `renderable` (crate mesh), `rigidBody`
|
||||
(dynamic, tuned mass/friction, no restitution), box `collider`,
|
||||
`pushable`. No other new code expected.
|
||||
- Tune so the forklift pushes crates without launching or tipping them
|
||||
(friction, mass ratio, chassis/fork contact height).
|
||||
|
||||
Verify: headless test applies an impulse through the wrapper and
|
||||
asserts the crate slides and settles.
|
||||
|
||||
### Phase 5 — Sokoban yard layout + target zones (F11)
|
||||
|
||||
- Flat driving yard near the building at ~(4000, 10, 4000). Options
|
||||
to evaluate, in order of preference: (a) terrain compliance
|
||||
flattening (the "proper" open-world way, see
|
||||
`TerrainPrefabSpawnerSystem` compliance), (b) a large flat static
|
||||
"concrete pad" platform entity. Decide with a quick experiment.
|
||||
- Walled rectangular yard (static boxes / low fence meshes), a
|
||||
grid-aligned layout of N crates and N target pads (start: 3x3, one
|
||||
solvable classic layout), the forklift parked just outside.
|
||||
- `components/TargetZone.hpp` + module + serialization: generic
|
||||
`TargetZoneComponent` — a named pad/zone entity (zone id, radius /
|
||||
half extents, visual feedback mesh/material). Generic on purpose:
|
||||
any activity can use zones, not just sokoban.
|
||||
- Per yard one controller entity with a scene script that registers
|
||||
the instance in Lua (see Phase 6); nothing about crates/pads/zones is
|
||||
hardcoded per demo.
|
||||
|
||||
Verify: screenshot of the yard from `--force-pos` above the site.
|
||||
|
||||
### Phase 6 — Zone detection (C++) + sokoban rules (Lua) (F11/F12)
|
||||
|
||||
C++ side (per-frame detection, engine-level, multi-instance):
|
||||
|
||||
- `systems/ZoneSystem.{hpp,cpp}`: per-frame XZ-overlap + nearly-at-rest
|
||||
check of `PushableComponent` entities against `TargetZoneComponent`
|
||||
zones; emits `EventBus` events `zone_entered` / `zone_left`
|
||||
(params: zone id, entity name/id); drives the pad visual feedback
|
||||
(covered/uncovered material swap). Zone ids are unique per yard
|
||||
(`<yardId>.pad<n>`), so multiple yards never interfere.
|
||||
|
||||
Lua side (the actual sokoban rules, shared by all demos):
|
||||
|
||||
- `lua-scripts/sokoban.lua` (staged through the `LuaScripts` resource
|
||||
group like the existing scripts): `sokoban.new{ id = "yard1", pads =
|
||||
{ ... }, questName = "Crate Yard", onCompleted = optional fn }`
|
||||
returns an instance handle. The instance subscribes to
|
||||
`zone_entered`/`zone_left`, tracks crates-on-pads n/m, and on full
|
||||
coverage:
|
||||
- `ecs.global_state.set("quest.<id>.completed", true)` and progress
|
||||
keys (survive scene switches, F9);
|
||||
- `ecs.send_event("quest_completed", { quest_id = id, ... })`;
|
||||
- HUD updates through the new `ecs.hud.*` bindings (Phase 7);
|
||||
- `sokoban_reset`-style event re-places crates at their scene-load
|
||||
transforms (positions captured at registration; also exposed as an
|
||||
actuator action on a yard sign).
|
||||
- The yard controller entity's inline scene script is one line:
|
||||
`local game = dofile-ish require("sokoban").new{ ... }` (exact module
|
||||
loading mechanism per the existing `LuaScripts` setup — check how
|
||||
`scriptPath` scripts are resolved before choosing `require` vs an
|
||||
explicit loader).
|
||||
- Decision to make: crate positions are *not* persisted across scene
|
||||
switches/saves (layout resets) unless we decide otherwise.
|
||||
|
||||
Verify: `--test-sokoban` headless: teleport crates onto pads through
|
||||
the wrapper, assert the zone events fire, the Lua instance completes,
|
||||
global state is set and `quest_completed` is received; exit non-zero on
|
||||
failure. `--test-switch` must keep passing. A second yard instance in
|
||||
the test scene verifies multi-instance isolation.
|
||||
|
||||
### Phase 7 — HUD status display + Lua bindings (F12)
|
||||
|
||||
- `systems/GameHudSystem.{hpp,cpp}`: game-mode-only ImGui overlay,
|
||||
top-right corner, no window chrome (like the actuator prompts);
|
||||
reusable API: `setStatus(key, text)` for persistent lines ("Sokoban:
|
||||
crates 2/3") and `pushMessage(text, ttl)` for transient banners
|
||||
("Quest completed: Crate Yard"). A future quest system reuses the
|
||||
same display.
|
||||
- `lua/LuaHudApi.{hpp,cpp}`: `ecs.hud.set_status(key, text)`,
|
||||
`ecs.hud.clear_status(key)`, `ecs.hud.push_message(text [, ttl])` —
|
||||
the Lua sokoban module is the first client.
|
||||
- Wire into `EditorApp` next to `ActuatorSystem::render`.
|
||||
|
||||
Verify: screenshot showing the status line and the completion banner.
|
||||
|
||||
### Phase 8 — Docs and cleanup
|
||||
|
||||
- Update `src/features/editScene/AGENTS.md`: demo entry (build/run/
|
||||
controls/test flags), new systems (Vehicle, VehicleController, Zone,
|
||||
GameHud), components (Vehicle, Pushable, TargetZone) and the
|
||||
`ecs.hud.*` Lua API.
|
||||
- Update root `AGENTS.md`: build target/output for `demoSokoban`.
|
||||
- `GameFeatures202609.md`: F10 vehicle concept + forklift, F11 sokoban
|
||||
activity (zones + Lua rules), F12 HUD status display.
|
||||
- `lua-examples/sokoban_example.lua` showing a minimal yard setup
|
||||
(zones + `sokoban.new`) and a `quest_completed` subscription.
|
||||
|
||||
## Open questions (to resolve as phases start)
|
||||
|
||||
1. Yard ground: terrain compliance flattening vs static concrete pad
|
||||
platform (Phase 5 experiment decides).
|
||||
2. Forklift forks: decorative (push-only, classic sokoban) or
|
||||
functional lifting (adds grab/constraint mechanics — big extra;
|
||||
assume decorative for now).
|
||||
3. Crate/pad count and layout difficulty (start with 3 and one known
|
||||
solvable layout).
|
||||
4. Steering: rear-wheel steering like a real forklift, or front-wheel?
|
||||
(Resolved in Phase 1: rear axle steers 35 deg, front wheels driven.)
|
||||
5. Exit placement rule when the forklift is boxed in.
|
||||
6. Whether crate positions persist across scene switches/saves
|
||||
(default: no — the layout resets).
|
||||
7. Lua module loading for `lua-scripts/sokoban.lua`: how scene scripts
|
||||
pull in a shared module today (resource-group path + `require`
|
||||
shim vs an explicit loader) — decide when Phase 6 starts.
|
||||
8. `ecs.hud.*` naming and whether `GameHudSystem` messages should also
|
||||
be reachable from C++ only (minimal) or fully scriptable (chosen:
|
||||
fully scriptable, per the Lua-first rule).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,906 @@
|
||||
{
|
||||
"actionDatabase": {
|
||||
"actions": [
|
||||
{
|
||||
"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": {
|
||||
"children": [
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
{
|
||||
"index": 1,
|
||||
"name": "hungry"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"name": "thirsty"
|
||||
}
|
||||
],
|
||||
"goals": []
|
||||
},
|
||||
"bookmarks": [],
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 488,
|
||||
"name": {
|
||||
"name": "arrival_b"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.60009765625,
|
||||
"y": 9.990500450134277,
|
||||
"z": 3995.89990234375
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.0,
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"cellGrid": {
|
||||
"ceilingRectName": "",
|
||||
"cellHeight": 4.0,
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 37757955,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 2097667,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 41953283,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 20976643,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 16781315,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 25171971,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
}
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorConfigs": {},
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorSceneSwitchPath": "demo_scene_interior.json",
|
||||
"doorSceneSwitchTarget": "arrival_a",
|
||||
"doorSwingReversed": false,
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
"extWallRectName": "",
|
||||
"extWindowFrameRectName": "",
|
||||
"floorRectName": "",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationMode": "exteriorOnly",
|
||||
"generationScript": "",
|
||||
"glassColor": [
|
||||
0.4000000059604645,
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
0.3499999940395355
|
||||
],
|
||||
"glassMaterialName": "",
|
||||
"glassReflectivity": 0.800000011920929,
|
||||
"gridUid": "77370b1d-e8ab-44a6-865b-e55c15b0fc78",
|
||||
"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": true,
|
||||
"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": 4000.0,
|
||||
"y": 10.014800071716309,
|
||||
"z": 4000.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": 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": 4000.0,
|
||||
"y": 20.0,
|
||||
"z": 4000.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": 494,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 9.903900146484375,
|
||||
"z": 3975.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": 495,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": 496,
|
||||
"name": {
|
||||
"name": "terrain"
|
||||
},
|
||||
"terrain": {
|
||||
"auxMaps": [],
|
||||
"baseNoise": {
|
||||
"amplitude": 15.0,
|
||||
"frequency": 0.0003000000142492354,
|
||||
"lacunarity": 2.0,
|
||||
"octaves": 3,
|
||||
"persistence": 0.5,
|
||||
"seed": 55
|
||||
},
|
||||
"blendMapSize": 1024,
|
||||
"compositeMapDistance": 300.0,
|
||||
"detailNoise": {
|
||||
"amplitude": 10.0,
|
||||
"enabled": false,
|
||||
"frequency": 0.006000000052154064,
|
||||
"lacunarity": 2.0,
|
||||
"octaves": 4,
|
||||
"persistence": 0.5,
|
||||
"seed": 26368
|
||||
},
|
||||
"enabled": true,
|
||||
"farClipDistance": 6000.0,
|
||||
"fogEnabled": true,
|
||||
"fogEnd": 5500.0,
|
||||
"fogStart": 2500.0,
|
||||
"heightmapFile": "heightmap.bin",
|
||||
"heightmapSize": 256,
|
||||
"layers": [
|
||||
{
|
||||
"diffuseTexture": "Ground23_col.jpg",
|
||||
"name": "Base",
|
||||
"normalTexture": "Ground23_normheight.dds",
|
||||
"worldSize": 100.0
|
||||
},
|
||||
{
|
||||
"diffuseTexture": "Ground37_diffspec.dds",
|
||||
"name": "Layer 1",
|
||||
"normalTexture": "Ground37_normheight.dds",
|
||||
"worldSize": 100.0
|
||||
}
|
||||
],
|
||||
"maxBatchSize": 65,
|
||||
"maxPixelError": 1.0,
|
||||
"minBatchSize": 17,
|
||||
"pageHoldRadius": 3,
|
||||
"pageLoadRadius": 2,
|
||||
"roadConfig": {
|
||||
"laneWidth": 3.0,
|
||||
"lanesPerDirection": 1,
|
||||
"prefabDespawnDistance": 250.0,
|
||||
"prefabSpawnDistance": 150.0,
|
||||
"roadLodDistance": 200.0,
|
||||
"roadMaterialName": "RoadMaterial",
|
||||
"roadMeshTemplate": "road_segment.mesh",
|
||||
"roadThickness": 0.30000001192092896,
|
||||
"roadVisibilityDistance": 1000.0,
|
||||
"sidewalkEnabled": false,
|
||||
"sidewalkHeight": 0.15000000596046448,
|
||||
"sidewalkMeshTemplate": "",
|
||||
"sidewalkThickness": 0.30000001192092896,
|
||||
"sidewalkWidth": 1.5
|
||||
},
|
||||
"streamingEnabled": true,
|
||||
"terrainId": 4242424300000001,
|
||||
"terrainSize": 65,
|
||||
"worldSize": 2000.0,
|
||||
"worldSizeUnits": 10000.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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 497,
|
||||
"name": {
|
||||
"name": "sky"
|
||||
},
|
||||
"skybox": {
|
||||
"cloudiness": 0.0,
|
||||
"dayBottomColor": [
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
1.0
|
||||
],
|
||||
"dayTopColor": [
|
||||
0.20000000298023224,
|
||||
0.5,
|
||||
1.0
|
||||
],
|
||||
"enabled": true,
|
||||
"moonSize": 0.029999999329447746,
|
||||
"nightBottomColor": [
|
||||
0.05000000074505806,
|
||||
0.05000000074505806,
|
||||
0.15000000596046448
|
||||
],
|
||||
"nightTopColor": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.05000000074505806
|
||||
],
|
||||
"size": 443.0,
|
||||
"starsEnabled": false,
|
||||
"sunSize": 0.05000000074505806,
|
||||
"sunriseColor": [
|
||||
1.0,
|
||||
0.5,
|
||||
0.20000000298023224
|
||||
],
|
||||
"sunsetColor": [
|
||||
1.0,
|
||||
0.30000001192092896,
|
||||
0.10000000149011612
|
||||
]
|
||||
},
|
||||
"sun": {
|
||||
"ambientDay": [
|
||||
0.30000001192092896,
|
||||
0.30000001192092896,
|
||||
0.30000001192092896
|
||||
],
|
||||
"ambientNight": [
|
||||
0.05000000074505806,
|
||||
0.05000000074505806,
|
||||
0.15000000596046448
|
||||
],
|
||||
"ambientSunrise": [
|
||||
0.30000001192092896,
|
||||
0.20000000298023224,
|
||||
0.15000000596046448
|
||||
],
|
||||
"ambientSunset": [
|
||||
0.25,
|
||||
0.15000000596046448,
|
||||
0.10000000149011612
|
||||
],
|
||||
"castShadows": true,
|
||||
"enabled": true,
|
||||
"intensity": 1.7899999618530273,
|
||||
"moonColor": [
|
||||
0.30000001192092896,
|
||||
0.30000001192092896,
|
||||
0.5
|
||||
],
|
||||
"moonSphereSize": 3.4000000953674316,
|
||||
"orbitTilt": 15.0,
|
||||
"showMoonSphere": true,
|
||||
"showSunSphere": true,
|
||||
"sunColor": [
|
||||
1.0,
|
||||
0.949999988079071,
|
||||
0.800000011920929
|
||||
],
|
||||
"sunSphereSize": 5.0,
|
||||
"timeOfDay": 8.35942554473877,
|
||||
"timeSpeed": 0.12999999523162842
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 10.0,
|
||||
"z": 4000.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": 498,
|
||||
"name": {
|
||||
"name": "water"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4000.0,
|
||||
"y": 0.0,
|
||||
"z": 4000.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"waterPhysics": {
|
||||
"defaultAngularDrag": 0.05000000074505806,
|
||||
"defaultBuoyancy": 1.0,
|
||||
"defaultLinearDrag": 0.25,
|
||||
"defaultSubmergedThreshold": 0.10000000149011612,
|
||||
"enabled": true,
|
||||
"gravity": 9.8100004196167,
|
||||
"waterDensity": 1000.0,
|
||||
"waterSurfaceY": 6.0
|
||||
},
|
||||
"waterPlane": {
|
||||
"autoUpdateFromWaterPhysics": true,
|
||||
"enabled": true,
|
||||
"planeSize": 12000.0,
|
||||
"reflectivity": 0.3799999952316284,
|
||||
"renderTextureSize": 512,
|
||||
"tiling": 0.012000000104308128,
|
||||
"waterColor": [
|
||||
0.0,
|
||||
0.30000001192092896,
|
||||
0.5,
|
||||
0.800000011920929
|
||||
],
|
||||
"waterSurfaceY": 6.0,
|
||||
"waveScale": 0.03099999949336052,
|
||||
"waveSpeed": 0.9800000190734863
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 500,
|
||||
"name": {
|
||||
"name": "forklift1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4012.0,
|
||||
"y": 10.8,
|
||||
"z": 3960.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 0.7,
|
||||
"y": 0.45,
|
||||
"z": 1.1
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"collider": {
|
||||
"shapeType": "box",
|
||||
"parameters": {
|
||||
"x": 0.7,
|
||||
"y": 0.3,
|
||||
"z": 1.2
|
||||
},
|
||||
"radius": 0.5,
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
}
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "dynamic",
|
||||
"mass": 1500.0,
|
||||
"friction": 0.8,
|
||||
"restitution": 0.0,
|
||||
"isSensor": false,
|
||||
"enabled": true
|
||||
},
|
||||
"vehicle": {
|
||||
"maxTorque": 120.0,
|
||||
"maxPitchRollAngleDeg": 60.0,
|
||||
"seatOffset": {
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"wheelMeshName": "",
|
||||
"wheels": [
|
||||
{
|
||||
"position": {
|
||||
"x": 0.65,
|
||||
"y": -0.2,
|
||||
"z": 0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 0.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": -0.65,
|
||||
"y": -0.2,
|
||||
"z": 0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 0.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": 0.65,
|
||||
"y": -0.2,
|
||||
"z": -0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 200.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": -0.65,
|
||||
"y": -0.2,
|
||||
"z": -0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 200.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,460 @@
|
||||
{
|
||||
"actionDatabase": {
|
||||
"actions": [
|
||||
{
|
||||
"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": {
|
||||
"children": [
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
{
|
||||
"index": 1,
|
||||
"name": "hungry"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"name": "thirsty"
|
||||
}
|
||||
],
|
||||
"goals": []
|
||||
},
|
||||
"bookmarks": [],
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 4294967793,
|
||||
"name": {
|
||||
"name": "arrival_a"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 24.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.0,
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 4294967794,
|
||||
"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": 4294967792,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoFloorPlaneInterior",
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
"registryId": 2,
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 4294967790,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.10288965702056885,
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 4294967789,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": 500,
|
||||
"name": {
|
||||
"name": "forklift1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 1.5,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 0.7,
|
||||
"y": 0.45,
|
||||
"z": 1.1
|
||||
}
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "Cube.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"collider": {
|
||||
"shapeType": "box",
|
||||
"parameters": {
|
||||
"x": 0.7,
|
||||
"y": 0.3,
|
||||
"z": 1.2
|
||||
},
|
||||
"radius": 0.5,
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
}
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "dynamic",
|
||||
"mass": 1500.0,
|
||||
"friction": 0.8,
|
||||
"restitution": 0.0,
|
||||
"isSensor": false,
|
||||
"enabled": true
|
||||
},
|
||||
"vehicle": {
|
||||
"maxTorque": 120.0,
|
||||
"maxPitchRollAngleDeg": 60.0,
|
||||
"seatOffset": {
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"wheelMeshName": "",
|
||||
"wheels": [
|
||||
{
|
||||
"position": {
|
||||
"x": 0.65,
|
||||
"y": -0.2,
|
||||
"z": 0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 0.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": -0.65,
|
||||
"y": -0.2,
|
||||
"z": 0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 0.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": 0.65,
|
||||
"y": -0.2,
|
||||
"z": -0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 200.0
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"x": -0.65,
|
||||
"y": -0.2,
|
||||
"z": -0.85
|
||||
},
|
||||
"radius": 0.3,
|
||||
"width": 0.2,
|
||||
"suspensionMinLength": 0.05,
|
||||
"suspensionMaxLength": 0.3,
|
||||
"suspensionFrequency": 2.0,
|
||||
"suspensionDamping": 0.8,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 200.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate heightmap.bin for the demo-interior-exterior-dynamics terrain.
|
||||
|
||||
NOTE: the exterior scene's terrain (terrainId 4242424300000001) runs in
|
||||
streaming mode, where base heights come from the terrain's baseNoise
|
||||
(FastNoiseLite) and this file is NOT sampled — it is only staged next to
|
||||
the binary (heightmaps/4242424300000001/heightmap.bin) so the loader
|
||||
finds it. This script documents the historical non-streaming layout and
|
||||
is kept as the generator of record for that staged file.
|
||||
|
||||
Legacy description (non-streaming mode):
|
||||
|
||||
The exterior scene's terrain entity (terrainId 4242424300000001,
|
||||
non-streaming, worldSize 500 per page) loads its base heights from
|
||||
heightmaps/4242424300000001/heightmap.bin via
|
||||
TerrainSystem::loadSceneHeightmap. File format: uint32 resolution,
|
||||
then res*res little-endian float32 heights.
|
||||
|
||||
Height profile: a flat disc at y=0 around the origin (the playable area
|
||||
with the building shell and the scene-switch arrival point), sloping
|
||||
down to -5 in a ring so the water plane (waterSurfaceY -0.5) is visible
|
||||
as a lake around the centre.
|
||||
|
||||
Sampling math (see TerrainSystem::sampleBaseLocked / fillPageHeightData
|
||||
and the visualToPhysicalX/Z helpers): in non-streaming mode the system
|
||||
loads a fixed 3x3 page grid, so the heightmap covers the physical world
|
||||
rect [-worldSize, 2*worldSize] on both axes and texel (ix, iz) holds
|
||||
the height at physical coordinate (-ws + i * 3*ws / res). Physical
|
||||
space maps to visual (scene) space by a half-page shift on X and a
|
||||
per-page Z mirror; this script inverts that mapping so every texel gets
|
||||
the height of the *visual* position it will be rendered at, then
|
||||
verifies the round trip by re-sampling the generated grid the same
|
||||
bilinear way the engine does.
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
||||
RES = 256 # must match "heightmapSize" in the scene JSON
|
||||
WORLD_SIZE = 500.0 # per-page world size ("worldSize" in the scene JSON)
|
||||
FLAT_RADIUS = 60.0 # flat-0 disc radius around the visual origin
|
||||
EDGE_RADIUS = 160.0 # slope reaches full depth here
|
||||
DEPTH = -5.0 # lake floor height (below waterSurfaceY -0.5)
|
||||
|
||||
|
||||
def profile(r):
|
||||
"""Visual height at distance r from the origin."""
|
||||
if r <= FLAT_RADIUS:
|
||||
return 0.0
|
||||
if r >= EDGE_RADIUS:
|
||||
return DEPTH
|
||||
t = (r - FLAT_RADIUS) / (EDGE_RADIUS - FLAT_RADIUS)
|
||||
t = t * t * (3.0 - 2.0 * t) # smoothstep
|
||||
return DEPTH * t
|
||||
|
||||
|
||||
def physical_to_visual(px, pz):
|
||||
"""Invert TerrainSystem::visualToPhysicalX/Z (terrain origin 0,0,0)."""
|
||||
vx = px - WORLD_SIZE * 0.5
|
||||
page = math.floor(pz / WORLD_SIZE)
|
||||
vz = 2.0 * page * WORLD_SIZE + WORLD_SIZE * 0.5 - pz
|
||||
return vx, vz
|
||||
|
||||
|
||||
def visual_to_physical(vx, vz):
|
||||
"""TerrainSystem::visualToPhysicalX/Z with a zero terrain origin."""
|
||||
px = vx + WORLD_SIZE * 0.5
|
||||
page = math.floor((vz + WORLD_SIZE * 0.5) / WORLD_SIZE)
|
||||
pz = 2.0 * page * WORLD_SIZE + WORLD_SIZE * 0.5 - vz
|
||||
return px, pz
|
||||
|
||||
|
||||
def main():
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else "heightmap.bin"
|
||||
span = 3.0 * WORLD_SIZE
|
||||
origin = -WORLD_SIZE
|
||||
|
||||
grid = []
|
||||
for iz in range(RES):
|
||||
row = []
|
||||
pz = origin + iz * span / RES
|
||||
for ix in range(RES):
|
||||
px = origin + ix * span / RES
|
||||
vx, vz = physical_to_visual(px, pz)
|
||||
row.append(profile(math.hypot(vx, vz)))
|
||||
grid.append(row)
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(struct.pack("<I", RES))
|
||||
for row in grid:
|
||||
f.write(struct.pack("<%df" % RES, *row))
|
||||
|
||||
# Verify: re-sample the grid exactly like TerrainSystem::sampleBaseLocked
|
||||
# (bilinear, physical coords) at a few visual positions.
|
||||
def sample(vx, vz):
|
||||
px, pz = visual_to_physical(vx, vz)
|
||||
fx = (px - origin) / span * RES
|
||||
fz = (pz - origin) / span * RES
|
||||
x0 = max(0, min(int(math.floor(fx)), RES - 1))
|
||||
z0 = max(0, min(int(math.floor(fz)), RES - 1))
|
||||
x1 = max(0, min(x0 + 1, RES - 1))
|
||||
z1 = max(0, min(z0 + 1, RES - 1))
|
||||
tx = fx - int(math.floor(fx))
|
||||
tz = fz - int(math.floor(fz))
|
||||
h00 = grid[z0][x0]
|
||||
h10 = grid[z0][x1]
|
||||
h01 = grid[z1][x0]
|
||||
h11 = grid[z1][x1]
|
||||
return ((1 - tx) * (1 - tz) * h00 + tx * (1 - tz) * h10 +
|
||||
(1 - tx) * tz * h01 + tx * tz * h11)
|
||||
|
||||
checks = [
|
||||
("arrival (0,24)", 0.0, 24.0),
|
||||
("origin (0,0)", 0.0, 0.0),
|
||||
("building corner (4,30)", 4.0, 30.0),
|
||||
("flat edge (55,0)", 55.0, 0.0),
|
||||
("mid slope (110,0)", 110.0, 0.0),
|
||||
("lake (200,0)", 200.0, 0.0),
|
||||
("lake (-200,200)", -200.0, 200.0),
|
||||
]
|
||||
ok = True
|
||||
for label, vx, vz in checks:
|
||||
h = sample(vx, vz)
|
||||
print("%-24s visual (%7.1f,%7.1f) -> height %8.3f"
|
||||
% (label, vx, vz, h))
|
||||
for vx, vz in [(0.0, 24.0), (0.0, 0.0), (4.0, 30.0), (55.0, 0.0)]:
|
||||
if abs(sample(vx, vz)) > 1e-4:
|
||||
print("ERROR: playable area not flat at", vx, vz)
|
||||
ok = False
|
||||
for vx, vz in [(200.0, 0.0), (-200.0, 200.0)]:
|
||||
if sample(vx, vz) > -4.9:
|
||||
print("ERROR: lake area not deep enough at", vx, vz)
|
||||
ok = False
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
print("heightmap written to %s (%dx%d), all checks passed"
|
||||
% (out_path, RES, RES))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Entity Hierarchy]
|
||||
Pos=0,0
|
||||
Size=300,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Property Editor]
|
||||
Pos=1570,0
|
||||
Size=350,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Load Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Save Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][StartupMenu]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Prefab Browser]
|
||||
Pos=300,300
|
||||
Size=250,400
|
||||
|
||||
[Window][Create Prefab]
|
||||
Pos=655,364
|
||||
Size=305,77
|
||||
|
||||
[Window][3D Cursor]
|
||||
Pos=300,100
|
||||
Size=280,350
|
||||
|
||||
[Window][Mesh Browser]
|
||||
Pos=533,240
|
||||
Size=416,406
|
||||
|
||||
[Window][Action Database (Singleton)]
|
||||
Pos=326,33
|
||||
Size=404,694
|
||||
|
||||
[Window][Dialogue Settings]
|
||||
Pos=300,100
|
||||
Size=400,500
|
||||
|
||||
[Window][DialogueBox]
|
||||
Pos=0,681
|
||||
Size=1682,227
|
||||
|
||||
[Window][Character Class Database]
|
||||
Pos=303,109
|
||||
Size=600,600
|
||||
|
||||
[Window][Character Registry]
|
||||
Pos=476,258
|
||||
Size=903,429
|
||||
|
||||
[Window][Delete Prefab]
|
||||
Pos=797,467
|
||||
Size=325,109
|
||||
|
||||
[Window][PauseMenu]
|
||||
Pos=0,0
|
||||
Size=2490,1536
|
||||
LastUsed=20260905
|
||||
|
||||
[Window][Item Registry]
|
||||
Pos=60,60
|
||||
Size=600,500
|
||||
|
||||
[Window][Inventory Dialog Config]
|
||||
Pos=300,100
|
||||
Size=350,200
|
||||
|
||||
[Window][Character Sheet]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Load Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Save Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Animation Tree Registry]
|
||||
Pos=60,60
|
||||
Size=900,600
|
||||
|
||||
[Window][Confirm Blend Map Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Confirm Heightmap Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Road Graph Invalid]
|
||||
Pos=892,217
|
||||
Size=136,127
|
||||
LastUsed=20260731
|
||||
|
||||
[Window][Wedge Geometry Debug]
|
||||
Pos=10,10
|
||||
Size=420,520
|
||||
LastUsed=20260814
|
||||
|
||||
[Window][Road Graph Valid]
|
||||
Pos=882,486
|
||||
Size=156,71
|
||||
LastUsed=20260821
|
||||
|
||||
[Window][Switch Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260906
|
||||
|
||||
[Window][Open Project]
|
||||
Pos=300,100
|
||||
Size=639,377
|
||||
LastUsed=20260908
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
/* Generated by CMake from project.json - do not edit.
|
||||
* Embeds the project parameters into the release binary so it runs its
|
||||
* project with no --project flag (see GameFeatures202609.md, F8). */
|
||||
#define EDITSCENE_PROJECT_APP_NAME "@PROJECT_APP_NAME@"
|
||||
#define EDITSCENE_PROJECT_START_SCENE "@PROJECT_START_SCENE@"
|
||||
#define EDITSCENE_PROJECT_GAME_MODE @PROJECT_GAME_MODE_VALUE@
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"appName": "demo-sokoban",
|
||||
"startScene": "demo_scene_interior.json",
|
||||
"gameMode": true
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Stage everything demoSokoban needs into its own directory so it
|
||||
# runs standalone from
|
||||
# <build>/src/features/editScene/demos/demo-sokoban.
|
||||
#
|
||||
# Expected -D arguments: DEMO_DIR, EDITSCENE_BIN, EDITSCENE_SRC, SRC_DIR.
|
||||
#
|
||||
# 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
|
||||
# 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}/project.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
foreach(f demo_scene_interior.json demo_scene_exterior.json
|
||||
demo_scene_vehicletest.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()
|
||||
|
||||
# Terrain heightmap: staged at configure time by a configure_file COPYONLY
|
||||
# in CMakeLists.txt (to heightmaps/4242424300000001/), which re-copies it
|
||||
# automatically when the source file changes. Not symlinked: the fixup
|
||||
# layer and save path write next to it.
|
||||
|
||||
# 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()
|
||||
@@ -35,6 +35,10 @@
|
||||
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <Jolt/Physics/Body/BodyActivationListener.h>
|
||||
#include <Jolt/Physics/Body/BodyLock.h>
|
||||
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
|
||||
#include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
|
||||
#include <Jolt/Physics/Vehicle/VehicleCollisionTester.h>
|
||||
#include <Jolt/Renderer/DebugRendererSimple.h>
|
||||
|
||||
// STL includes
|
||||
@@ -568,6 +572,12 @@ class Physics {
|
||||
JPH::Vec3 gravity = JPH::Vec3(0.0f, -9.8f, 0.0f);
|
||||
std::unordered_map<uint32_t, JPH::Ref<JPH::GroupFilterTable> > groupFilters;
|
||||
|
||||
/* F10 vehicles: live constraints (for teardown) and one shared
|
||||
* ray-cast collision tester (stateless; MOVING object layer hits
|
||||
* both broadphase layers with our filter tables). */
|
||||
std::set<JPH::VehicleConstraint *> vehicles;
|
||||
JPH::Ref<JPH::VehicleCollisionTester> vehicleTester;
|
||||
|
||||
public:
|
||||
class ActivationListener : public JPH::BodyActivationListener {
|
||||
public:
|
||||
@@ -602,6 +612,164 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* --- F10 vehicles (mirrors the Jolt VehicleConstraintTest
|
||||
* sample) --- */
|
||||
|
||||
JPH::VehicleConstraint *
|
||||
createVehicle(const JPH::BodyID &chassisBodyId,
|
||||
const JoltPhysicsWrapper::VehicleDesc &desc)
|
||||
{
|
||||
JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(),
|
||||
chassisBodyId);
|
||||
if (!lock.Succeeded())
|
||||
return nullptr;
|
||||
JPH::Body &body = lock.GetBody();
|
||||
|
||||
JPH::VehicleConstraintSettings settings;
|
||||
if (desc.maxPitchRollAngleDeg > 0.0f)
|
||||
settings.mMaxPitchRollAngle =
|
||||
JPH::DegreesToRadians(desc.maxPitchRollAngleDeg);
|
||||
settings.mWheels.reserve(desc.wheels.size());
|
||||
for (const JoltPhysicsWrapper::VehicleWheelDesc &wd :
|
||||
desc.wheels) {
|
||||
JPH::WheelSettingsWV *w = new JPH::WheelSettingsWV;
|
||||
/* Chassis-local attachment point (Vec3, not RVec3). */
|
||||
w->mPosition = JPH::Vec3(wd.position.x, wd.position.y,
|
||||
wd.position.z);
|
||||
w->mSuspensionDirection = JPH::Vec3(0, -1, 0);
|
||||
w->mSteeringAxis = JPH::Vec3(0, 1, 0);
|
||||
w->mWheelUp = JPH::Vec3(0, 1, 0);
|
||||
w->mWheelForward = JPH::Vec3(0, 0, 1);
|
||||
w->mSuspensionMinLength = wd.suspensionMinLength;
|
||||
w->mSuspensionMaxLength = wd.suspensionMaxLength;
|
||||
w->mSuspensionSpring.mFrequency =
|
||||
wd.suspensionFrequency;
|
||||
w->mSuspensionSpring.mDamping = wd.suspensionDamping;
|
||||
w->mMaxSteerAngle =
|
||||
JPH::DegreesToRadians(wd.maxSteerAngleDeg);
|
||||
w->mMaxHandBrakeTorque = wd.maxHandBrakeTorque;
|
||||
w->mRadius = wd.radius;
|
||||
w->mWidth = wd.width;
|
||||
settings.mWheels.push_back(w);
|
||||
}
|
||||
|
||||
JPH::WheeledVehicleControllerSettings *controller =
|
||||
new JPH::WheeledVehicleControllerSettings;
|
||||
settings.mController = controller;
|
||||
controller->mEngine.mMaxTorque = desc.maxTorque;
|
||||
|
||||
/* One differential per axle pair (wheels 0/1, 2/3, ...)
|
||||
* where both wheels are driven. Engine torque ratios must
|
||||
* sum to 1 across differentials (Jolt assert), so split
|
||||
* evenly. */
|
||||
for (size_t i = 0; i + 1 < desc.wheels.size(); i += 2) {
|
||||
if (!desc.wheels[i].driven ||
|
||||
!desc.wheels[i + 1].driven)
|
||||
continue;
|
||||
JPH::VehicleDifferentialSettings diff;
|
||||
diff.mLeftWheel = (int)i;
|
||||
diff.mRightWheel = (int)i + 1;
|
||||
controller->mDifferentials.push_back(diff);
|
||||
}
|
||||
if (!controller->mDifferentials.empty()) {
|
||||
float ratio =
|
||||
1.0f / (float)controller->mDifferentials.size();
|
||||
for (JPH::VehicleDifferentialSettings &d :
|
||||
controller->mDifferentials)
|
||||
d.mEngineTorqueRatio = ratio;
|
||||
}
|
||||
|
||||
JPH::VehicleConstraint *constraint =
|
||||
new JPH::VehicleConstraint(body, settings);
|
||||
|
||||
/* Same tyre-impulse compensation as the Jolt sample: the
|
||||
* sample settings were tuned with N-velocity-steps-times
|
||||
* more longitudinal impulse than intended, so the max
|
||||
* impulse is scaled to keep the tuned feel. */
|
||||
static_cast<JPH::WheeledVehicleController *>(
|
||||
constraint->GetController())
|
||||
->SetTireMaxImpulseCallback(
|
||||
[](JPH::uint, float &outLongitudinalImpulse,
|
||||
float &outLateralImpulse,
|
||||
float inSuspensionImpulse,
|
||||
float inLongitudinalFriction,
|
||||
float inLateralFriction, float, float,
|
||||
float) {
|
||||
outLongitudinalImpulse =
|
||||
10.0f * inLongitudinalFriction *
|
||||
inSuspensionImpulse;
|
||||
outLateralImpulse = inLateralFriction *
|
||||
inSuspensionImpulse;
|
||||
});
|
||||
|
||||
if (!vehicleTester)
|
||||
vehicleTester =
|
||||
new JPH::VehicleCollisionTesterRay(
|
||||
Layers::MOVING);
|
||||
constraint->SetVehicleCollisionTester(vehicleTester);
|
||||
|
||||
physics_system.AddConstraint(constraint);
|
||||
physics_system.AddStepListener(constraint);
|
||||
vehicles.insert(constraint);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
void destroyVehicle(JPH::VehicleConstraint *vehicle)
|
||||
{
|
||||
if (!vehicle)
|
||||
return;
|
||||
auto it = vehicles.find(vehicle);
|
||||
if (it == vehicles.end())
|
||||
return;
|
||||
physics_system.RemoveStepListener(vehicle);
|
||||
physics_system.RemoveConstraint(vehicle);
|
||||
vehicles.erase(it);
|
||||
}
|
||||
|
||||
void setVehicleInput(JPH::VehicleConstraint *vehicle, float forward,
|
||||
float right, float brake, float handbrake)
|
||||
{
|
||||
if (!vehicle || vehicles.find(vehicle) == vehicles.end())
|
||||
return;
|
||||
if (forward != 0.0f || right != 0.0f || brake != 0.0f ||
|
||||
handbrake != 0.0f)
|
||||
physics_system.GetBodyInterface().ActivateBody(
|
||||
vehicle->GetVehicleBody()->GetID());
|
||||
static_cast<JPH::WheeledVehicleController *>(
|
||||
vehicle->GetController())
|
||||
->SetDriverInput(forward, right, brake, handbrake);
|
||||
}
|
||||
|
||||
float getVehicleForwardSpeed(JPH::VehicleConstraint *vehicle) const
|
||||
{
|
||||
const JPH::Body *body = vehicle->GetVehicleBody();
|
||||
return (body->GetRotation().Conjugated() *
|
||||
body->GetLinearVelocity())
|
||||
.GetZ();
|
||||
}
|
||||
|
||||
void getWheelWorldTransform(JPH::VehicleConstraint *vehicle,
|
||||
int wheelIndex, Ogre::Vector3 &position,
|
||||
Ogre::Quaternion &orientation) const
|
||||
{
|
||||
/* The wheel visual (a cylinder mesh) is Y-axis aligned, so
|
||||
* Y is the rotational axis and X the "up" reference. */
|
||||
JPH::RMat44 t = vehicle->GetWheelWorldTransform(
|
||||
(JPH::uint)wheelIndex, JPH::Vec3::sAxisY(),
|
||||
JPH::Vec3::sAxisX());
|
||||
position = JoltPhysics::convert(t.GetTranslation());
|
||||
orientation = JoltPhysics::convert(t.GetQuaternion());
|
||||
}
|
||||
|
||||
void destroyAllVehicles()
|
||||
{
|
||||
for (JPH::VehicleConstraint *v : vehicles) {
|
||||
physics_system.RemoveStepListener(v);
|
||||
physics_system.RemoveConstraint(v);
|
||||
}
|
||||
vehicles.clear();
|
||||
}
|
||||
|
||||
void setBodyDrawFilter(JPH::BodyDrawFilter *filter)
|
||||
{
|
||||
mBodyDrawFilter = filter;
|
||||
@@ -762,6 +930,10 @@ public:
|
||||
}
|
||||
~Physics()
|
||||
{
|
||||
/* Vehicles hold step-listener registrations and body
|
||||
* references; remove them before the system goes down. */
|
||||
destroyAllVehicles();
|
||||
|
||||
// Unregisters all types with the factory and cleans up the default material
|
||||
JPH::UnregisterTypes();
|
||||
|
||||
@@ -2126,5 +2298,44 @@ JPH::GroupFilterTable *JoltPhysicsWrapper::getGroupFilter(uint32_t groupId) cons
|
||||
return phys->getGroupFilter(groupId);
|
||||
}
|
||||
|
||||
JPH::VehicleConstraint *
|
||||
JoltPhysicsWrapper::createVehicle(const JPH::BodyID &chassisBody,
|
||||
const VehicleDesc &desc)
|
||||
{
|
||||
return phys->createVehicle(chassisBody, desc);
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::destroyVehicle(JPH::VehicleConstraint *vehicle)
|
||||
{
|
||||
phys->destroyVehicle(vehicle);
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::setVehicleInput(JPH::VehicleConstraint *vehicle,
|
||||
float forward, float right,
|
||||
float brake, float handbrake)
|
||||
{
|
||||
phys->setVehicleInput(vehicle, forward, right, brake, handbrake);
|
||||
}
|
||||
|
||||
float JoltPhysicsWrapper::getVehicleForwardSpeed(
|
||||
JPH::VehicleConstraint *vehicle) const
|
||||
{
|
||||
return phys->getVehicleForwardSpeed(vehicle);
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::getWheelWorldTransform(
|
||||
JPH::VehicleConstraint *vehicle, int wheelIndex,
|
||||
Ogre::Vector3 &position, Ogre::Quaternion &orientation) const
|
||||
{
|
||||
phys->getWheelWorldTransform(vehicle, wheelIndex, position,
|
||||
orientation);
|
||||
}
|
||||
|
||||
int JoltPhysicsWrapper::getVehicleWheelCount(
|
||||
JPH::VehicleConstraint *vehicle) const
|
||||
{
|
||||
return (int)vehicle->GetWheels().size();
|
||||
}
|
||||
|
||||
template <>
|
||||
JoltPhysicsWrapper *Ogre::Singleton<JoltPhysicsWrapper>::msSingleton = 0;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <Jolt/Physics/EActivation.h>
|
||||
#include <Jolt/Physics/Collision/GroupFilterTable.h>
|
||||
#include <vector>
|
||||
void physics();
|
||||
namespace JPH
|
||||
{
|
||||
@@ -21,6 +22,7 @@ class ContactManifold;
|
||||
class ContactSettings;
|
||||
class SubShapeIDPair;
|
||||
class PhysicsSystem;
|
||||
class VehicleConstraint;
|
||||
}
|
||||
// Layer that objects can be in, determines which other objects it can collide with
|
||||
// Typically you at least want to have 1 layer for moving bodies and 1 layer for static bodies, but you can have more
|
||||
@@ -271,5 +273,57 @@ public:
|
||||
uint32_t groupId,
|
||||
uint32_t numSubGroups = Layers::MAX_CHARACTER_SUBGROUPS);
|
||||
JPH::GroupFilterTable *getGroupFilter(uint32_t groupId) const;
|
||||
|
||||
/* --- F10 vehicle support (Jolt VehicleConstraint) ---------------
|
||||
* See demos/demo-sokoban/PLAN.md. Modelled on the Jolt samples
|
||||
* (Samples/Tests/Vehicle/VehicleConstraintTest.cpp): the chassis is
|
||||
* a regular dynamic body created through the usual RigidBody path;
|
||||
* createVehicle() attaches a VehicleConstraint with a
|
||||
* WheeledVehicleController to it and registers the constraint as a
|
||||
* physics step listener so suspension raycasts run inside the
|
||||
* physics step. */
|
||||
struct VehicleWheelDesc {
|
||||
/* Suspension attachment point in chassis space. */
|
||||
Ogre::Vector3 position = Ogre::Vector3::ZERO;
|
||||
float radius = 0.3f;
|
||||
float width = 0.15f;
|
||||
float suspensionMinLength = 0.05f;
|
||||
float suspensionMaxLength = 0.3f;
|
||||
float suspensionFrequency = 2.0f;
|
||||
float suspensionDamping = 0.8f;
|
||||
/* 0 = not steered. */
|
||||
float maxSteerAngleDeg = 0.0f;
|
||||
/* Engine torque is routed through axle differentials pairing
|
||||
* wheels (0,1), (2,3), ...; only pairs with both wheels
|
||||
* driven get a differential. */
|
||||
bool driven = false;
|
||||
/* 0 = no handbrake on this wheel. */
|
||||
float maxHandBrakeTorque = 0.0f;
|
||||
};
|
||||
struct VehicleDesc {
|
||||
std::vector<VehicleWheelDesc> wheels;
|
||||
float maxTorque = 400.0f;
|
||||
/* Degrees; 0 = unlimited. */
|
||||
float maxPitchRollAngleDeg = 60.0f;
|
||||
};
|
||||
|
||||
/* Create/destroy a vehicle on an existing (already added) dynamic
|
||||
* chassis body. Returns nullptr on failure. */
|
||||
JPH::VehicleConstraint *createVehicle(const JPH::BodyID &chassisBody,
|
||||
const VehicleDesc &desc);
|
||||
void destroyVehicle(JPH::VehicleConstraint *vehicle);
|
||||
/* Driver input: forward -1..1, right -1..1, brake 0..1,
|
||||
* handbrake 0..1. Activates the chassis body on non-zero input. */
|
||||
void setVehicleInput(JPH::VehicleConstraint *vehicle, float forward,
|
||||
float right, float brake, float handbrake);
|
||||
/* Chassis forward speed in m/s (local +Z), for brake/reverse
|
||||
* logic in the caller. */
|
||||
float getVehicleForwardSpeed(JPH::VehicleConstraint *vehicle) const;
|
||||
/* Wheel world transform for the visual wheel node; the wheel mesh
|
||||
* is assumed Y-axis aligned (OGRE cylinder convention). */
|
||||
void getWheelWorldTransform(JPH::VehicleConstraint *vehicle,
|
||||
int wheelIndex, Ogre::Vector3 &position,
|
||||
Ogre::Quaternion &orientation) const;
|
||||
int getVehicleWheelCount(JPH::VehicleConstraint *vehicle) const;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -64,9 +64,16 @@ occluderTunnelMesh(const Ogre::Vector3 ¢er,
|
||||
const float y0 = center.y - hy, y1 = center.y + hy;
|
||||
const float z0 = center.z + side * (hz + 0.02f);
|
||||
const float z1 = z0 + side * (x1 + hz * 2.0f + 0.05f);
|
||||
/* Flare the walls 1 cm INTO the frame/jamb: no gap at the
|
||||
* opening edges and no z-fighting with the jamb faces. */
|
||||
const float f = 0.01f;
|
||||
/* Flare the walls INTO the frame/jamb past the leaf/frame
|
||||
* clearances (0.02 m per side, 0.05 m at the top): with a smaller
|
||||
* flare, sight lines through the open doorway slip over the top
|
||||
* wall / past the side walls into the void between the jamb inner
|
||||
* face and the wall, showing sky/terrain around the black
|
||||
* corridor. 6 cm puts the wall edges inside the jamb boxes (0.1 m
|
||||
* thick, inner faces at the clearances), so any ray past a wall
|
||||
* hits the frame instead, with no z-fighting against the jamb
|
||||
* faces. */
|
||||
const float f = 0.06f;
|
||||
|
||||
char name[160];
|
||||
snprintf(name, sizeof(name),
|
||||
@@ -123,6 +130,67 @@ occluderTunnelMesh(const Ogre::Vector3 ¢er,
|
||||
return name;
|
||||
}
|
||||
|
||||
/* F1: flat black gap shield for scene-switch doors, shown while the
|
||||
* door is fully closed (the tunnel covers the swinging/open states):
|
||||
* the leaf is sized with clearances against the frame opening (0.02 m
|
||||
* per side, 0.05 m at the top), and without a backing those slits show
|
||||
* the ungenerated void (sky/terrain) around the closed door. The
|
||||
* shield is a single quad 1 cm behind the closed leaf's back face on
|
||||
* the void side (`side` * Z), spanning the leaf box plus a margin that
|
||||
* exceeds the clearances; the margin edges end up inside the jamb
|
||||
* boxes and the wall thickness, so the quad never pokes out of the
|
||||
* frame. Like the tunnel, the coordinates are baked into the mesh in
|
||||
* hinge-local space and meshes are shared between doors of equal size
|
||||
* and side. */
|
||||
static Ogre::String
|
||||
occluderShieldMesh(const Ogre::Vector3 ¢er,
|
||||
const Ogre::Vector3 &halfExtents, float side,
|
||||
const Ogre::String &group)
|
||||
{
|
||||
const float hx = halfExtents.x;
|
||||
const float hy = halfExtents.y;
|
||||
const float hz = halfExtents.z;
|
||||
const float x0 = center.x - hx, x1 = center.x + hx;
|
||||
const float y0 = center.y - hy, y1 = center.y + hy;
|
||||
const float z = center.z + side * (hz + 0.01f);
|
||||
/* Margin beyond the leaf box: must exceed the leaf/frame
|
||||
* clearances (0.02 m sides, 0.05 m top). */
|
||||
const float m = 0.06f;
|
||||
|
||||
char name[160];
|
||||
snprintf(name, sizeof(name),
|
||||
"CellGridDoorOccluderShield_%d_%d_%d_%d_%d_%d_%d",
|
||||
(int)lroundf(center.x * 1000.0f),
|
||||
(int)lroundf(center.y * 1000.0f),
|
||||
(int)lroundf(center.z * 1000.0f),
|
||||
(int)lroundf(hx * 1000.0f), (int)lroundf(hy * 1000.0f),
|
||||
(int)lroundf(hz * 1000.0f), (int)side);
|
||||
if (!Ogre::MeshManager::getSingleton().getByName(name, group).isNull())
|
||||
return name;
|
||||
|
||||
const float x0m = x0 - m, x1m = x1 + m;
|
||||
const float y0m = y0 - m, y1m = y1 + m;
|
||||
|
||||
Ogre::ManualObject mo(name);
|
||||
mo.begin("CellGridDoorOccluderBlack",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
Ogre::Vector3 normal(0, 0, -side);
|
||||
Ogre::uint32 base = mo.getCurrentVertexCount();
|
||||
mo.position(x0m, y0m, z);
|
||||
mo.normal(normal);
|
||||
mo.position(x1m, y0m, z);
|
||||
mo.normal(normal);
|
||||
mo.position(x1m, y1m, z);
|
||||
mo.normal(normal);
|
||||
mo.position(x0m, y1m, z);
|
||||
mo.normal(normal);
|
||||
mo.triangle(base, base + 1, base + 2);
|
||||
mo.triangle(base, base + 2, base + 3);
|
||||
mo.end();
|
||||
mo.convertToMesh(name, group);
|
||||
return name;
|
||||
}
|
||||
|
||||
flecs::entity DoorBuilder::build(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
flecs::entity parentEntity,
|
||||
@@ -286,7 +354,10 @@ flecs::entity DoorBuilder::build(flecs::world &world,
|
||||
* the frame. The geometry is baked into the mesh in hinge-local
|
||||
* space and the node hangs off the PARENT node (not the hinge),
|
||||
* so it does not swing with the leaf; DoorSystem toggles its
|
||||
* visibility from the swing angle. */
|
||||
* visibility from the swing angle. A second flat gap-shield quad
|
||||
* sits just behind the closed leaf and is visible only while the
|
||||
* door is fully closed, blacking out the leaf/frame clearance
|
||||
* slits the tunnel (hidden while closed) does not cover. */
|
||||
if (!door.sceneSwitchPath.empty()) {
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
@@ -328,6 +399,42 @@ flecs::entity DoorBuilder::build(flecs::world &world,
|
||||
|
||||
doorEntity.get_mut<DoorComponent>().occluder = occEnt;
|
||||
}
|
||||
|
||||
Ogre::String shieldMeshName = occluderShieldMesh(
|
||||
colliderCenter, colliderHalfExtents,
|
||||
params.occluderSide, group);
|
||||
Ogre::Entity *shieldEnt = nullptr;
|
||||
try {
|
||||
shieldEnt = sceneMgr->createEntity(shieldMeshName);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DoorBuilder: Error creating door gap shield: " +
|
||||
std::string(e.what()));
|
||||
}
|
||||
if (shieldEnt) {
|
||||
shieldEnt->setMaterialName("CellGridDoorOccluderBlack");
|
||||
shieldEnt->setVisible(!wasOpen);
|
||||
|
||||
Ogre::SceneNode *shieldNode =
|
||||
parentNode->createChildSceneNode();
|
||||
shieldNode->setPosition(occPos);
|
||||
shieldNode->setOrientation(doorwayRot);
|
||||
shieldNode->setScale(occScale);
|
||||
shieldNode->attachObject(shieldEnt);
|
||||
|
||||
flecs::entity shieldEntity = world.entity();
|
||||
shieldEntity.child_of(doorEntity);
|
||||
shieldEntity.set<TransformComponent>(
|
||||
{shieldNode, occPos, doorwayRot, occScale});
|
||||
RenderableComponent shieldRenderable;
|
||||
shieldRenderable.entity = shieldEnt;
|
||||
shieldRenderable.meshName = shieldMeshName;
|
||||
shieldEntity.set<RenderableComponent>(
|
||||
shieldRenderable);
|
||||
|
||||
doorEntity.get_mut<DoorComponent>().gapShield =
|
||||
shieldEnt;
|
||||
}
|
||||
}
|
||||
|
||||
return doorEntity;
|
||||
|
||||
@@ -18,11 +18,15 @@
|
||||
* leaf, DoorComponent, RigidBodyComponent static, ActuatorComponent)
|
||||
* ├── collider child (PhysicsColliderComponent box of the closed
|
||||
* │ leaf; disabled by DoorSystem while not fully closed)
|
||||
* └── occluder child (F1, scene-switch doors only: open-front
|
||||
* black tunnel on the void side of the doorway - door-local
|
||||
* +Z normally, -Z for exteriorOnly grids -, node on the
|
||||
* PARENT node so it does not swing; DoorSystem toggles its
|
||||
* visibility)
|
||||
* ├── occluder child (F1, scene-switch doors only: open-front
|
||||
* │ black tunnel on the void side of the doorway - door-local
|
||||
* │ +Z normally, -Z for exteriorOnly grids -, node on the
|
||||
* │ PARENT node so it does not swing; DoorSystem toggles its
|
||||
* │ visibility, hidden while the door is fully closed)
|
||||
* └── gap-shield child (F1, scene-switch doors only: flat black
|
||||
* quad just behind the closed leaf, visible only while the
|
||||
* door is fully closed; blacks out the leaf/frame clearance
|
||||
* slits the hidden tunnel does not cover)
|
||||
*
|
||||
* The builder also declares the F6 store defaults for persistent doors
|
||||
* (DoorSystem::declareDoorDefaults) and snaps a door that was left open
|
||||
|
||||
@@ -175,10 +175,13 @@ void DoorSystem::update(float deltaTime)
|
||||
}
|
||||
}
|
||||
|
||||
// F1: the black occluder is visible unless the door is
|
||||
// fully closed
|
||||
// F1: the black occluder tunnel is visible unless the door
|
||||
// is fully closed; the gap shield backs the leaf/frame
|
||||
// clearance slits exactly while it is
|
||||
if (door.occluder)
|
||||
door.occluder->setVisible(door.currentAngle != 0.0f);
|
||||
if (door.gapShield)
|
||||
door.gapShield->setVisible(door.currentAngle == 0.0f);
|
||||
});
|
||||
|
||||
// Refresh the per-door "door_unlock_<doorId>" subscriptions
|
||||
|
||||
@@ -35,6 +35,14 @@ void EditorPhysicsSystem::update(float deltaTime)
|
||||
if (!m_initialized || !m_physics)
|
||||
return;
|
||||
|
||||
/* Clamp the step delta: the first frame after a scene load carries
|
||||
* the whole load time (seconds), and without a clamp the wrapper
|
||||
* would run hundreds of catch-up steps in one frame - dynamic bodies
|
||||
* free-fall through not-yet-streamed terrain colliders or get
|
||||
* launched sky-high by buoyancy impulses at depth. */
|
||||
if (deltaTime > 0.1f)
|
||||
deltaTime = 0.1f;
|
||||
|
||||
// Sync bodies before simulation
|
||||
syncBodies();
|
||||
|
||||
@@ -230,12 +238,18 @@ EditorPhysicsSystem::buildCompoundShape(flecs::entity rigidBodyEntity)
|
||||
rigidBodyEntity.has<TransformComponent>()) {
|
||||
auto &collider =
|
||||
rigidBodyEntity.get_mut<PhysicsColliderComponent>();
|
||||
auto &transform = rigidBodyEntity.get<TransformComponent>();
|
||||
JPH::ShapeRefC shape = createShape(collider);
|
||||
if (shape) {
|
||||
/* The body is created at this entity's world
|
||||
* transform, so its own collider sits at the
|
||||
* origin of the compound (intra-entity offset is
|
||||
* handled by collider.offset in createShape).
|
||||
* Using transform.position here would double-apply
|
||||
* the entity position and strand the collider at
|
||||
* 2x the world position. */
|
||||
shapes.push_back(shape);
|
||||
positions.push_back(transform.position);
|
||||
rotations.push_back(transform.rotation);
|
||||
positions.push_back(Ogre::Vector3::ZERO);
|
||||
rotations.push_back(Ogre::Quaternion::IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/EditorMarker.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/Vehicle.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/Light.hpp"
|
||||
#include "../components/Camera.hpp"
|
||||
@@ -265,6 +266,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
json["rigidBody"] = serializeRigidBody(entity);
|
||||
}
|
||||
|
||||
if (entity.has<VehicleComponent>()) {
|
||||
json["vehicle"] = serializeVehicle(entity);
|
||||
}
|
||||
|
||||
if (entity.has<PhysicsColliderComponent>()) {
|
||||
json["collider"] = serializeCollider(entity);
|
||||
}
|
||||
@@ -499,6 +504,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeRigidBody(entity, json["rigidBody"]);
|
||||
}
|
||||
|
||||
if (json.contains("vehicle")) {
|
||||
deserializeVehicle(entity, json["vehicle"]);
|
||||
}
|
||||
|
||||
if (json.contains("collider")) {
|
||||
deserializeCollider(entity, json["collider"]);
|
||||
}
|
||||
@@ -753,6 +762,10 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeRigidBody(entity, json["rigidBody"]);
|
||||
}
|
||||
|
||||
if (json.contains("vehicle")) {
|
||||
deserializeVehicle(entity, json["vehicle"]);
|
||||
}
|
||||
|
||||
if (json.contains("collider")) {
|
||||
deserializeCollider(entity, json["collider"]);
|
||||
}
|
||||
@@ -1629,6 +1642,86 @@ void SceneSerializer::deserializeRigidBody(flecs::entity entity,
|
||||
entity.set<RigidBodyComponent>(rb);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeVehicle(flecs::entity entity)
|
||||
{
|
||||
auto &vehicle = entity.get<VehicleComponent>();
|
||||
nlohmann::json json;
|
||||
|
||||
json["maxTorque"] = vehicle.maxTorque;
|
||||
json["maxPitchRollAngleDeg"] = vehicle.maxPitchRollAngleDeg;
|
||||
json["seatOffset"] = { { "x", vehicle.seatOffset.x },
|
||||
{ "y", vehicle.seatOffset.y },
|
||||
{ "z", vehicle.seatOffset.z } };
|
||||
json["wheelMeshName"] = vehicle.wheelMeshName;
|
||||
|
||||
nlohmann::json wheels = nlohmann::json::array();
|
||||
for (const VehicleWheel &w : vehicle.wheels) {
|
||||
nlohmann::json wj;
|
||||
wj["position"] = { { "x", w.position.x },
|
||||
{ "y", w.position.y },
|
||||
{ "z", w.position.z } };
|
||||
wj["radius"] = w.radius;
|
||||
wj["width"] = w.width;
|
||||
wj["suspensionMinLength"] = w.suspensionMinLength;
|
||||
wj["suspensionMaxLength"] = w.suspensionMaxLength;
|
||||
wj["suspensionFrequency"] = w.suspensionFrequency;
|
||||
wj["suspensionDamping"] = w.suspensionDamping;
|
||||
wj["maxSteerAngleDeg"] = w.maxSteerAngleDeg;
|
||||
wj["driven"] = w.driven;
|
||||
wj["maxHandBrakeTorque"] = w.maxHandBrakeTorque;
|
||||
wheels.push_back(wj);
|
||||
}
|
||||
json["wheels"] = wheels;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeVehicle(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
VehicleComponent vehicle;
|
||||
|
||||
vehicle.maxTorque = json.value("maxTorque", 400.0f);
|
||||
vehicle.maxPitchRollAngleDeg =
|
||||
json.value("maxPitchRollAngleDeg", 60.0f);
|
||||
if (json.contains("seatOffset")) {
|
||||
auto &so = json["seatOffset"];
|
||||
vehicle.seatOffset = Ogre::Vector3(so.value("x", 0.0f),
|
||||
so.value("y", 1.0f),
|
||||
so.value("z", 0.0f));
|
||||
}
|
||||
vehicle.wheelMeshName = json.value("wheelMeshName", "");
|
||||
|
||||
if (json.contains("wheels")) {
|
||||
for (const auto &wj : json["wheels"]) {
|
||||
VehicleWheel w;
|
||||
if (wj.contains("position")) {
|
||||
auto &p = wj["position"];
|
||||
w.position = Ogre::Vector3(p.value("x", 0.0f),
|
||||
p.value("y", 0.0f),
|
||||
p.value("z", 0.0f));
|
||||
}
|
||||
w.radius = wj.value("radius", 0.3f);
|
||||
w.width = wj.value("width", 0.15f);
|
||||
w.suspensionMinLength =
|
||||
wj.value("suspensionMinLength", 0.1f);
|
||||
w.suspensionMaxLength =
|
||||
wj.value("suspensionMaxLength", 0.4f);
|
||||
w.suspensionFrequency =
|
||||
wj.value("suspensionFrequency", 1.5f);
|
||||
w.suspensionDamping =
|
||||
wj.value("suspensionDamping", 0.7f);
|
||||
w.maxSteerAngleDeg = wj.value("maxSteerAngleDeg", 0.0f);
|
||||
w.driven = wj.value("driven", false);
|
||||
w.maxHandBrakeTorque =
|
||||
wj.value("maxHandBrakeTorque", 0.0f);
|
||||
vehicle.wheels.push_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
entity.set<VehicleComponent>(vehicle);
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeCollider(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
|
||||
@@ -142,6 +142,7 @@ private:
|
||||
nlohmann::json serializeAnimationTreeTemplate(flecs::entity entity);
|
||||
nlohmann::json serializeStartupMenu(flecs::entity entity);
|
||||
nlohmann::json serializePlayerController(flecs::entity entity);
|
||||
nlohmann::json serializeVehicle(flecs::entity entity);
|
||||
|
||||
// CellGrid/Town component serialization
|
||||
nlohmann::json serializeCellGrid(flecs::entity entity);
|
||||
@@ -203,6 +204,8 @@ private:
|
||||
const nlohmann::json &json);
|
||||
void deserializePlayerController(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeVehicle(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// CellGrid/Town component deserialization
|
||||
void deserializeCellGrid(flecs::entity entity,
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "VehicleSystem.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreEntity.h>
|
||||
|
||||
VehicleSystem::VehicleSystem(flecs::world &world, JoltPhysicsWrapper *physics)
|
||||
: m_world(world)
|
||||
, m_physics(physics)
|
||||
, m_query(world
|
||||
.query<VehicleComponent, RigidBodyComponent,
|
||||
TransformComponent>())
|
||||
{
|
||||
m_removeObserver =
|
||||
m_world.observer<VehicleComponent>("VehicleCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, VehicleComponent &v) {
|
||||
destroyConstraint(v);
|
||||
destroyWheelVisuals(e.id());
|
||||
});
|
||||
}
|
||||
|
||||
VehicleSystem::~VehicleSystem()
|
||||
{
|
||||
if (m_removeObserver.is_alive())
|
||||
m_removeObserver.destruct();
|
||||
/* Constraints referencing scene bodies must not outlive the
|
||||
* system; entities are expected to be cleared first (clearScene),
|
||||
* but remove whatever is left. */
|
||||
m_query.each(
|
||||
[this](flecs::entity e, VehicleComponent &v,
|
||||
RigidBodyComponent &, TransformComponent &) {
|
||||
destroyConstraint(v);
|
||||
destroyWheelVisuals(e.id());
|
||||
});
|
||||
}
|
||||
|
||||
void VehicleSystem::createConstraint(flecs::entity entity,
|
||||
VehicleComponent &vehicle,
|
||||
RigidBodyComponent &rigidBody)
|
||||
{
|
||||
if (rigidBody.bodyType != RigidBodyComponent::BodyType::Dynamic) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"VehicleSystem: entity " + Ogre::StringConverter::toString(
|
||||
(unsigned long long)entity.id()) +
|
||||
" has a VehicleComponent but its RigidBody is not "
|
||||
"dynamic - constraint not created");
|
||||
return;
|
||||
}
|
||||
if (vehicle.wheels.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"VehicleSystem: VehicleComponent has no wheels - "
|
||||
"constraint not created");
|
||||
return;
|
||||
}
|
||||
|
||||
JoltPhysicsWrapper::VehicleDesc desc;
|
||||
desc.maxTorque = vehicle.maxTorque;
|
||||
desc.maxPitchRollAngleDeg = vehicle.maxPitchRollAngleDeg;
|
||||
desc.wheels.reserve(vehicle.wheels.size());
|
||||
for (const VehicleWheel &vw : vehicle.wheels) {
|
||||
JoltPhysicsWrapper::VehicleWheelDesc wd;
|
||||
wd.position = vw.position;
|
||||
wd.radius = vw.radius;
|
||||
wd.width = vw.width;
|
||||
wd.suspensionMinLength = vw.suspensionMinLength;
|
||||
wd.suspensionMaxLength = vw.suspensionMaxLength;
|
||||
wd.suspensionFrequency = vw.suspensionFrequency;
|
||||
wd.suspensionDamping = vw.suspensionDamping;
|
||||
wd.maxSteerAngleDeg = vw.maxSteerAngleDeg;
|
||||
wd.driven = vw.driven;
|
||||
wd.maxHandBrakeTorque = vw.maxHandBrakeTorque;
|
||||
desc.wheels.push_back(wd);
|
||||
}
|
||||
|
||||
vehicle.constraint =
|
||||
m_physics->createVehicle(rigidBody.bodyID, desc);
|
||||
vehicle.constraintCreated = vehicle.constraint != nullptr;
|
||||
if (vehicle.constraintCreated) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"VehicleSystem: vehicle constraint created (entity " +
|
||||
Ogre::StringConverter::toString(
|
||||
(unsigned long long)entity.id()) +
|
||||
", " +
|
||||
Ogre::StringConverter::toString(
|
||||
(int)vehicle.wheels.size()) +
|
||||
" wheels)");
|
||||
createWheelVisuals(entity, vehicle);
|
||||
}
|
||||
}
|
||||
|
||||
void VehicleSystem::destroyConstraint(VehicleComponent &vehicle)
|
||||
{
|
||||
if (!vehicle.constraintCreated)
|
||||
return;
|
||||
m_physics->destroyVehicle(vehicle.constraint);
|
||||
vehicle.constraint = nullptr;
|
||||
vehicle.constraintCreated = false;
|
||||
}
|
||||
|
||||
void VehicleSystem::createWheelVisuals(flecs::entity entity,
|
||||
VehicleComponent &vehicle)
|
||||
{
|
||||
if (vehicle.wheelMeshName.empty())
|
||||
return;
|
||||
if (!entity.has<TransformComponent>())
|
||||
return;
|
||||
const TransformComponent &t = entity.get<TransformComponent>();
|
||||
if (!t.node)
|
||||
return;
|
||||
|
||||
destroyWheelVisuals(entity.id());
|
||||
WheelVisual visual;
|
||||
try {
|
||||
for (size_t i = 0; i < vehicle.wheels.size(); i++) {
|
||||
Ogre::SceneNode *node = t.node->createChildSceneNode();
|
||||
Ogre::Entity *ent = t.node->getCreator()->createEntity(
|
||||
vehicle.wheelMeshName);
|
||||
node->attachObject(ent);
|
||||
visual.nodes.push_back(node);
|
||||
visual.entities.push_back(ent);
|
||||
}
|
||||
} catch (const Ogre::Exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"VehicleSystem: cannot create wheel visuals '" +
|
||||
vehicle.wheelMeshName + "': " + e.getDescription());
|
||||
destroyWheelVisuals(entity.id());
|
||||
return;
|
||||
}
|
||||
m_wheelVisuals[entity.id()] = visual;
|
||||
}
|
||||
|
||||
void VehicleSystem::destroyWheelVisuals(flecs::entity_t id)
|
||||
{
|
||||
/* The wheel nodes are children of the chassis node and die with
|
||||
* it; just drop the bookkeeping. */
|
||||
m_wheelVisuals.erase(id);
|
||||
}
|
||||
|
||||
void VehicleSystem::prePhysicsUpdate(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
if (!m_physics)
|
||||
return;
|
||||
|
||||
m_query.each([&](flecs::entity entity, VehicleComponent &vehicle,
|
||||
RigidBodyComponent &rigidBody,
|
||||
TransformComponent &) {
|
||||
/* The chassis body went away (disabled, rebuilt, scene
|
||||
* teardown): the constraint references the old body, drop
|
||||
* it. */
|
||||
if (vehicle.constraintCreated &&
|
||||
(!rigidBody.bodyCreated ||
|
||||
vehicle.constraint->GetVehicleBody()->GetID() !=
|
||||
rigidBody.bodyID)) {
|
||||
destroyConstraint(vehicle);
|
||||
}
|
||||
|
||||
if (!vehicle.constraintCreated) {
|
||||
if (rigidBody.bodyCreated && rigidBody.enabled)
|
||||
createConstraint(entity, vehicle,
|
||||
rigidBody);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Driver input with the sample's brake-vs-reverse
|
||||
* logic: a direction change request while still rolling
|
||||
* the other way becomes a brake input until nearly
|
||||
* stopped. */
|
||||
float forward = vehicle.inputForward;
|
||||
float brake = vehicle.inputBrake;
|
||||
float handbrake = vehicle.inputHandBrake;
|
||||
float &prevForward = m_prevForward[entity.id()];
|
||||
|
||||
if (prevForward * forward < 0.0f) {
|
||||
float velocity = m_physics->getVehicleForwardSpeed(
|
||||
vehicle.constraint);
|
||||
if ((forward > 0.0f && velocity < -0.1f) ||
|
||||
(forward < 0.0f && velocity > 0.1f)) {
|
||||
forward = 0.0f;
|
||||
brake = 1.0f;
|
||||
} else {
|
||||
prevForward = forward;
|
||||
}
|
||||
}
|
||||
if (handbrake != 0.0f)
|
||||
forward = 0.0f;
|
||||
|
||||
m_physics->setVehicleInput(vehicle.constraint, forward,
|
||||
vehicle.inputRight, brake,
|
||||
handbrake);
|
||||
});
|
||||
}
|
||||
|
||||
void VehicleSystem::postPhysicsUpdate()
|
||||
{
|
||||
if (!m_physics)
|
||||
return;
|
||||
|
||||
m_query.each([&](flecs::entity entity, VehicleComponent &vehicle,
|
||||
RigidBodyComponent &, TransformComponent &transform) {
|
||||
if (!vehicle.constraintCreated || !transform.node)
|
||||
return;
|
||||
auto it = m_wheelVisuals.find(entity.id());
|
||||
if (it == m_wheelVisuals.end())
|
||||
return;
|
||||
|
||||
Ogre::Quaternion invChassis =
|
||||
transform.node->_getDerivedOrientation().Inverse();
|
||||
for (size_t i = 0; i < it->second.nodes.size(); i++) {
|
||||
Ogre::Vector3 pos;
|
||||
Ogre::Quaternion ori;
|
||||
m_physics->getWheelWorldTransform(
|
||||
vehicle.constraint, (int)i, pos, ori);
|
||||
Ogre::SceneNode *node = it->second.nodes[i];
|
||||
node->setPosition(transform.node->convertWorldToLocalPosition(
|
||||
pos));
|
||||
node->setOrientation(invChassis * ori);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef EDITSCENE_VEHICLESYSTEM_HPP
|
||||
#define EDITSCENE_VEHICLESYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "../physics/physics.h"
|
||||
#include "../components/Vehicle.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
|
||||
/**
|
||||
* Vehicle system (F10, see demos/demo-sokoban/PLAN.md).
|
||||
*
|
||||
* Owns the Jolt VehicleConstraint of every entity with a
|
||||
* VehicleComponent + dynamic RigidBodyComponent (the chassis):
|
||||
*
|
||||
* - prePhysicsUpdate() (call BEFORE EditorPhysicsSystem::update):
|
||||
* attaches the constraint once the chassis body exists, re-attaches
|
||||
* it when the body is rebuilt, and feeds the component's driver
|
||||
* input fields into the WheeledVehicleController;
|
||||
* - postPhysicsUpdate() (call AFTER the physics step): syncs the
|
||||
* per-wheel visual child nodes from the constraint state (when
|
||||
* wheelMeshName is set).
|
||||
*
|
||||
* The chassis transform itself is synced by EditorPhysicsSystem like
|
||||
* any other dynamic body.
|
||||
*/
|
||||
class VehicleSystem {
|
||||
public:
|
||||
VehicleSystem(flecs::world &world, JoltPhysicsWrapper *physics);
|
||||
~VehicleSystem();
|
||||
|
||||
void prePhysicsUpdate(float deltaTime);
|
||||
void postPhysicsUpdate();
|
||||
|
||||
private:
|
||||
struct WheelVisual {
|
||||
std::vector<Ogre::SceneNode *> nodes;
|
||||
std::vector<Ogre::Entity *> entities;
|
||||
};
|
||||
|
||||
void createConstraint(flecs::entity entity, VehicleComponent &vehicle,
|
||||
RigidBodyComponent &rigidBody);
|
||||
void destroyConstraint(VehicleComponent &vehicle);
|
||||
void createWheelVisuals(flecs::entity entity,
|
||||
VehicleComponent &vehicle);
|
||||
void destroyWheelVisuals(flecs::entity_t id);
|
||||
|
||||
flecs::world &m_world;
|
||||
JoltPhysicsWrapper *m_physics;
|
||||
flecs::query<VehicleComponent, RigidBodyComponent,
|
||||
TransformComponent> m_query;
|
||||
flecs::entity m_removeObserver;
|
||||
|
||||
std::map<flecs::entity_t, WheelVisual> m_wheelVisuals;
|
||||
/* Per-vehicle previous forward input for brake/reverse logic. */
|
||||
std::map<flecs::entity_t, float> m_prevForward;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_VEHICLESYSTEM_HPP
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "VehicleEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool VehicleEditor::renderComponent(flecs::entity entity,
|
||||
VehicleComponent &vehicle)
|
||||
{
|
||||
bool modified = false;
|
||||
|
||||
if (ImGui::CollapsingHeader("Vehicle",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
|
||||
if (ImGui::DragFloat("Max Torque", &vehicle.maxTorque, 1.0f,
|
||||
0.0f, 10000.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Max Pitch/Roll (deg)",
|
||||
&vehicle.maxPitchRollAngleDeg, 0.5f,
|
||||
0.0f, 90.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat3("Seat Offset",
|
||||
&vehicle.seatOffset.x, 0.01f))
|
||||
modified = true;
|
||||
|
||||
char wheelMeshBuf[256];
|
||||
snprintf(wheelMeshBuf, sizeof(wheelMeshBuf), "%s",
|
||||
vehicle.wheelMeshName.c_str());
|
||||
if (ImGui::InputText("Wheel Mesh", wheelMeshBuf,
|
||||
sizeof(wheelMeshBuf))) {
|
||||
vehicle.wheelMeshName = wheelMeshBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Wheels (%d)", (int)vehicle.wheels.size());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Add")) {
|
||||
vehicle.wheels.push_back(VehicleWheel());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
int removeWheel = -1;
|
||||
for (size_t i = 0; i < vehicle.wheels.size(); i++) {
|
||||
VehicleWheel &w = vehicle.wheels[i];
|
||||
ImGui::PushID((int)i);
|
||||
Ogre::String label =
|
||||
"Wheel " + std::to_string(i);
|
||||
if (ImGui::TreeNode(label.c_str())) {
|
||||
if (ImGui::DragFloat3("Position",
|
||||
&w.position.x, 0.01f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Radius", &w.radius,
|
||||
0.005f, 0.05f, 2.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Width", &w.width, 0.005f,
|
||||
0.02f, 1.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Susp. Min",
|
||||
&w.suspensionMinLength,
|
||||
0.005f, 0.0f, 3.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Susp. Max",
|
||||
&w.suspensionMaxLength,
|
||||
0.005f, 0.0f, 3.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Susp. Freq",
|
||||
&w.suspensionFrequency,
|
||||
0.01f, 0.1f, 10.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Susp. Damping",
|
||||
&w.suspensionDamping,
|
||||
0.01f, 0.0f, 2.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Max Steer (deg)",
|
||||
&w.maxSteerAngleDeg, 0.5f,
|
||||
0.0f, 90.0f))
|
||||
modified = true;
|
||||
if (ImGui::Checkbox("Driven", &w.driven))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Handbrake Torque",
|
||||
&w.maxHandBrakeTorque,
|
||||
1.0f, 0.0f, 10000.0f))
|
||||
modified = true;
|
||||
if (ImGui::SmallButton("Remove"))
|
||||
removeWheel = (int)i;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeWheel >= 0) {
|
||||
vehicle.wheels.erase(vehicle.wheels.begin() +
|
||||
removeWheel);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Status: %s",
|
||||
vehicle.constraintCreated ?
|
||||
"constraint active" :
|
||||
"no constraint");
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef EDITSCENE_VEHICLEEDITOR_HPP
|
||||
#define EDITSCENE_VEHICLEEDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Vehicle.hpp"
|
||||
|
||||
/**
|
||||
* Editor for VehicleComponent (F10)
|
||||
*/
|
||||
class VehicleEditor : public ComponentEditor<VehicleComponent> {
|
||||
public:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
VehicleComponent &vehicle) override;
|
||||
const char *getName() const override { return "Vehicle"; }
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_VEHICLEEDITOR_HPP
|
||||
Reference in New Issue
Block a user