Fixed stuck animations.

This commit is contained in:
2026-06-24 13:24:56 +03:00
parent d4974f5d90
commit d832127d5a
25 changed files with 1517 additions and 279 deletions
+13
View File
@@ -43,6 +43,7 @@ set(EDITSCENE_SOURCES
systems/SaveLoadDialog.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/CharacterSpawnerSystem.cpp
systems/OgreEntityHack.cpp
systems/CharacterRegistry.cpp
systems/MarkovNameGenerator.cpp
@@ -98,6 +99,7 @@ set(EDITSCENE_SOURCES
ui/PrimitiveEditor.cpp
ui/TriangleBufferEditor.cpp
ui/CharacterSlotsEditor.cpp
ui/CharacterSpawnerEditor.cpp
ui/CharacterIdentityEditor.cpp
ui/AnimationTreeEditor.cpp
ui/AnimationTreeTemplateEditor.cpp
@@ -140,6 +142,7 @@ set(EDITSCENE_SOURCES
components/PrimitiveModule.cpp
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/CharacterSpawnerModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTreeTemplateModule.cpp
components/AnimationTree.cpp
@@ -299,6 +302,7 @@ set(EDITSCENE_HEADERS
ui/PrimitiveEditor.hpp
ui/TriangleBufferEditor.hpp
ui/CharacterSlotsEditor.hpp
ui/CharacterSpawnerEditor.hpp
ui/CharacterIdentityEditor.hpp
ui/AnimationTreeEditor.hpp
ui/AnimationTreeTemplateEditor.hpp
@@ -352,6 +356,15 @@ set(EDITSCENE_HEADERS
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
add_dependencies(editSceneEditor morph)
# Verbose diagnostics for player input, locomotion state changes and animation
# blending. Disabled by default; enable with -DEDITSCENE_VERBOSE_DEBUG=ON.
option(EDITSCENE_VERBOSE_DEBUG
"Enable verbose player controller / animation diagnostics" OFF)
if(EDITSCENE_VERBOSE_DEBUG)
target_compile_definitions(editSceneEditor
PRIVATE EDITSCENE_VERBOSE_DEBUG)
endif()
# Define JPH_DEBUG_RENDERER for physics debug drawing
target_compile_definitions(editSceneEditor PRIVATE JPH_DEBUG_RENDERER)
+128 -99
View File
@@ -16,6 +16,8 @@
#include "systems/ProceduralMaterialSystem.hpp"
#include "systems/ProceduralMeshSystem.hpp"
#include "systems/CharacterSlotSystem.hpp"
#include "systems/CharacterSpawnerSystem.hpp"
#include "components/CharacterSpawner.hpp"
#include "systems/AnimationTreeSystem.hpp"
#include "systems/HairPhysicsSystem.hpp"
#include "systems/BehaviorTreeSystem.hpp"
@@ -99,10 +101,18 @@
#include "components/Item.hpp"
#include "components/Inventory.hpp"
#ifdef EDITSCENE_VERBOSE_DEBUG
#define EDITSCENE_LOG_INPUT(msg) \
Ogre::LogManager::getSingleton().logMessage(msg)
#else
#define EDITSCENE_LOG_INPUT(msg)
#endif
#include <OgreRTShaderSystem.h>
#include <OgreArchiveManager.h>
#include "package/OgrePackageArchive.h"
#include <imgui.h>
#include <SDL2/SDL.h>
#include "lua/LuaEntityApi.hpp"
#include "lua/LuaComponentApi.hpp"
#include "lua/LuaEventApi.hpp"
@@ -252,6 +262,7 @@ EditorApp::~EditorApp()
m_behaviorTreeSystem.reset();
m_animationTreeSystem.reset();
m_characterSlotSystem.reset();
m_characterSpawnerSystem.reset();
m_proceduralMeshSystem.reset();
m_proceduralMaterialSystem.reset();
m_proceduralTextureSystem.reset();
@@ -397,6 +408,13 @@ void EditorApp::setup()
m_uiSystem->getCharacterRegistry().setCharacterSlotSystem(
m_characterSlotSystem.get());
// Setup CharacterSpawner system
m_characterSpawnerSystem =
std::make_unique<CharacterSpawnerSystem>(
m_world, m_sceneMgr, m_cameraSystem.get(),
m_uiSystem.get());
m_characterSpawnerSystem->initialize();
// Setup AnimationTree system
m_animationTreeSystem = std::make_unique<AnimationTreeSystem>(
m_world, m_sceneMgr);
@@ -681,6 +699,11 @@ void EditorApp::setGamePlayState(GamePlayState state)
editScene::GamePlayState::Paused :
editScene::GamePlayState::Menu);
/* Avoid stuck movement keys when leaving the Playing state (e.g. opening
* the inventory/pause menu while Shift+W is held). */
if (state != GamePlayState::Playing)
m_gameInput.clearMovementInput();
// Grab/ungrab mouse based on gameplay state
if (m_gameMode == GameMode::Game) {
if (state == GamePlayState::Playing) {
@@ -726,33 +749,12 @@ void EditorApp::startNewGame(const Ogre::String &scenePath)
Ogre::LogManager::getSingleton().logMessage(
"Game started: loaded scene " + scenePath);
// Set up player character: find the player controller entity
// and resolve the actual target character so the character sheet
// and inventory are on the same entity that ActuatorSystem uses.
flecs::entity playerController;
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e, EntityNameComponent &name) {
if (name.name == "player" && e.is_alive()) {
playerController = e;
}
});
flecs::entity playerCharacter = playerController;
if (playerController.is_alive() &&
playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController
.get<PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name ==
pc.targetCharacterName)
playerCharacter = e;
});
}
}
// Set up player character: resolve the actual target character
// (spawned instance if the controller targets a spawner) so the
// character sheet and inventory are on the same entity that
// ActuatorSystem uses.
flecs::entity playerCharacter = getPlayerCharacterEntity();
if (playerCharacter.is_valid() && playerCharacter.is_alive()) {
// Check if player already has a character identity
if (!playerCharacter.has<CharacterIdentityComponent>()) {
// Create a character record for the player
uint64_t charId =
@@ -869,32 +871,11 @@ void EditorApp::saveGame(const std::string &slotPath,
/* Identify the player character registry ID for load-time setup */
uint64_t playerCharId = 0;
flecs::entity playerController;
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e, EntityNameComponent &name) {
if (name.name == "player")
playerController = e;
});
if (playerController.is_alive()) {
flecs::entity playerChar = playerController;
if (playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController
.get<PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name ==
pc.targetCharacterName)
playerChar = e;
});
}
}
if (playerChar.has<CharacterIdentityComponent>()) {
playerCharId =
playerChar.get<CharacterIdentityComponent>()
.registryId;
}
flecs::entity playerChar = getPlayerCharacterEntity();
if (playerChar.is_alive() &&
playerChar.has<CharacterIdentityComponent>()) {
playerCharId =
playerChar.get<CharacterIdentityComponent>().registryId;
}
saveData["saveGame"]["playerCharacterId"] = playerCharId;
@@ -1157,9 +1138,21 @@ void EditorApp::loadGame(const std::string &slotPath)
auto &pc =
playerController
.get_mut<PlayerControllerComponent>();
if (playerChar.has<EntityNameComponent>()) {
/* If the saved character is currently owned by a spawner,
* keep the controller targeted at the spawner so the
* character respawns correctly. Otherwise target the
* character directly. */
flecs::entity targetEntity = playerChar;
if (m_characterSpawnerSystem) {
flecs::entity spawner =
m_characterSpawnerSystem
->getSpawnerForCharacter(playerChar);
if (spawner.is_alive())
targetEntity = spawner;
}
if (targetEntity.has<EntityNameComponent>()) {
pc.targetCharacterName =
playerChar.get<EntityNameComponent>()
targetEntity.get<EntityNameComponent>()
.name;
}
if (!playerChar.has<InventoryComponent>()) {
@@ -1427,6 +1420,9 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
/* --- Gameplay systems (paused when in Paused state) --- */
if (!paused) {
/* --- Visual mesh setup (must run before animation) --- */
if (m_characterSpawnerSystem) {
m_characterSpawnerSystem->update();
}
if (m_characterSlotSystem) {
m_characterSlotSystem->update();
}
@@ -1581,33 +1577,10 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
flecs::entity::null());
setGamePlayState(GamePlayState::Playing);
} else if (m_gamePlayState == GamePlayState::Playing) {
// Find player controller and resolve target character
// so the sheet shows the same entity ActuatorSystem uses.
flecs::entity playerController;
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &name) {
if (name.name == "player" &&
e.is_alive()) {
playerController = e;
}
});
flecs::entity playerCharacter = playerController;
if (playerController.is_alive() &&
playerController.has<PlayerControllerComponent>()) {
auto &pc = playerController.get<
PlayerControllerComponent>();
if (!pc.targetCharacterName.empty()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e,
EntityNameComponent &en) {
if (en.name ==
pc.targetCharacterName)
playerCharacter =
e;
});
}
}
// Resolve the live player character (spawned instance if
// the controller targets a spawner) so the sheet shows
// the same entity ActuatorSystem uses.
flecs::entity playerCharacter = getPlayerCharacterEntity();
if (playerCharacter.is_valid() &&
playerCharacter.is_alive()) {
m_characterClassSystem->toggleCharacterSheet(
@@ -1723,40 +1696,64 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
setGamePlayState(GamePlayState::Playing);
return true;
}
bool pressed = true;
/* Ignore repeated keydowns for movement keys. Releasing Shift
* while W/A/S/D is held can generate a repeated keydown with a
* different symbol after the physical release, which would
* re-stick the key. Also avoid re-setting a key that is already
* marked pressed so duplicate initial keydowns cannot flip it
* back on after a release. */
bool isRepeat = evt.repeat != 0;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
if (!isRepeat && !m_gameInput.w) {
m_gameInput.w = true;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed W");
}
break;
case 's':
case 'S':
m_gameInput.s = pressed;
if (!isRepeat && !m_gameInput.s) {
m_gameInput.s = true;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed S");
}
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
if (!isRepeat && !m_gameInput.a) {
m_gameInput.a = true;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed A");
}
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
if (!isRepeat && !m_gameInput.d) {
m_gameInput.d = true;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed D");
}
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
case SDLK_RSHIFT:
if (!isRepeat && !m_gameInput.shift) {
m_gameInput.shift = true;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed SHIFT");
}
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
m_gameInput.e = true;
m_gameInput.ePressed = true;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
m_gameInput.f = true;
m_gameInput.fPressed = true;
break;
case 'i':
case 'I':
m_gameInput.i = pressed;
m_gameInput.i = true;
m_gameInput.iPressed = true;
break;
}
@@ -1794,40 +1791,46 @@ bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt)
m_currentModifiers = evt.keysym.mod;
if (m_gameMode == GameMode::Game) {
bool pressed = false;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
m_gameInput.w = false;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyReleased W");
break;
case 's':
case 'S':
m_gameInput.s = pressed;
m_gameInput.s = false;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyReleased S");
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
m_gameInput.a = false;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyReleased A");
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
m_gameInput.d = false;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyReleased D");
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
case SDLK_RSHIFT:
m_gameInput.shift = false;
EDITSCENE_LOG_INPUT("[INPUT_DBG] keyReleased SHIFT");
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
m_gameInput.e = false;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
m_gameInput.f = false;
break;
case 'i':
case 'I':
m_gameInput.i = pressed;
m_gameInput.i = false;
break;
}
return true;
}
@@ -1847,6 +1850,32 @@ flecs::entity EditorApp::getSelectedEntity() const
return flecs::entity::null();
}
flecs::entity EditorApp::getPlayerCharacterEntity()
{
flecs::entity controller = flecs::entity::null();
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e, EntityNameComponent &name) {
if (name.name == "player")
controller = e;
});
if (!controller.is_alive() ||
!controller.has<PlayerControllerComponent>())
return flecs::entity::null();
if (m_playerControllerSystem)
return m_playerControllerSystem->getCurrentTarget(controller);
auto &pc = controller.get<PlayerControllerComponent>();
flecs::entity target = flecs::entity::null();
m_world.query<EntityNameComponent>().each(
[&](flecs::entity e, EntityNameComponent &en) {
if (en.name == pc.targetCharacterName)
target = e;
});
return target;
}
PauseMenuSystem *EditorApp::getPauseMenuSystem() const
{
return &PauseMenuSystem::getInstance();
+25 -2
View File
@@ -23,6 +23,7 @@ class ProceduralTextureSystem;
class ProceduralMaterialSystem;
class ProceduralMeshSystem;
class CharacterSlotSystem;
class CharacterSpawnerSystem;
class AnimationTreeSystem;
class HairPhysicsSystem;
class BehaviorTreeSystem;
@@ -78,6 +79,15 @@ struct GameInputState {
fPressed = false;
iPressed = false;
}
void clearMovementInput()
{
w = false;
a = false;
s = false;
d = false;
shift = false;
}
};
/**
@@ -166,8 +176,7 @@ public:
void clearScene();
// Save / Load
void saveGame(const std::string &slotPath,
const std::string &slotName);
void saveGame(const std::string &slotPath, const std::string &slotName);
void loadGame(const std::string &slotPath);
// Input access
@@ -178,6 +187,15 @@ public:
// Getters
flecs::entity getSelectedEntity() const;
/**
* Return the live player character entity that is currently controlled by
* the "player" controller. If the controller targets a character spawner,
* this returns the spawned character instance, forcing a spawn if necessary.
* Returns null if no player controller or target exists.
*/
flecs::entity getPlayerCharacterEntity();
Ogre::SceneManager *getSceneManager() const
{
return m_sceneMgr;
@@ -198,6 +216,10 @@ public:
{
return m_characterSlotSystem.get();
}
CharacterSpawnerSystem *getCharacterSpawnerSystem() const
{
return m_characterSpawnerSystem.get();
}
StartupMenuSystem *getStartupMenuSystem() const
{
return m_startupMenuSystem.get();
@@ -252,6 +274,7 @@ private:
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
std::unique_ptr<CharacterSpawnerSystem> m_characterSpawnerSystem;
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
std::unique_ptr<HairPhysicsSystem> m_hairPhysicsSystem;
std::unique_ptr<BehaviorTreeSystem> m_behaviorTreeSystem;
@@ -0,0 +1,29 @@
#ifndef EDITSCENE_CHARACTERSPAWNER_HPP
#define EDITSCENE_CHARACTERSPAWNER_HPP
#pragma once
#include <cstdint>
/**
* @brief Distance-based character spawner.
*
* Attaches to an entity with a TransformComponent. When the active camera is
* within spawnDistanceSq of the spawner, the registry character identified by
* registryId is spawned as an independent entity at the spawner's transform.
* When the camera moves beyond despawnDistanceSq, the spawned entity is
* destroyed.
*
* Distances are stored squared for fast comparison.
*/
struct CharacterSpawnerComponent {
/** Registry character ID to spawn. */
uint64_t registryId = 0;
/** Squared distance at which the character should be spawned. */
float spawnDistanceSq = 100.0f * 100.0f;
/** Squared distance at which the character should be despawned. */
float despawnDistanceSq = 200.0f * 200.0f;
};
#endif // EDITSCENE_CHARACTERSPAWNER_HPP
@@ -0,0 +1,26 @@
#include "CharacterSpawner.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CharacterSpawnerEditor.hpp"
REGISTER_COMPONENT_GROUP("Character Spawner", "Character",
CharacterSpawnerComponent, CharacterSpawnerEditor)
{
registry.registerComponent<CharacterSpawnerComponent>(
"Character Spawner",
std::make_unique<CharacterSpawnerEditor>(sceneMgr),
/* Adder */
[sceneMgr](flecs::entity e) {
(void)sceneMgr;
if (!e.has<CharacterSpawnerComponent>()) {
e.set<CharacterSpawnerComponent>(
CharacterSpawnerComponent{});
}
},
/* Remover */
[sceneMgr](flecs::entity e) {
(void)sceneMgr;
if (e.has<CharacterSpawnerComponent>()) {
e.remove<CharacterSpawnerComponent>();
}
});
}
@@ -24,8 +24,8 @@ struct PlayerControllerComponent {
/* Swim animation states (used when character is in water) */
Ogre::String swimIdleState = "swim-idle";
Ogre::String swimState = "swim";
Ogre::String swimFastState = "swim-fast";
Ogre::String swimState = "swimming";
Ogre::String swimFastState = "swimming-fast";
/* Actuator interaction settings */
float actuatorDistance = 25.0f;
@@ -39,4 +39,11 @@ struct PlayerControllerComponent {
bool inputLocked = false;
};
/**
* Tag component marking a character entity as currently under player control.
* AI systems (e.g. SmartObjectSystem) should skip entities with this tag.
*/
struct PlayerControlledComponent {
};
#endif // EDITSCENE_PLAYERCONTROLLER_HPP
@@ -0,0 +1,116 @@
-- =============================================================================
-- CharacterSpawner Component Lua API Example
-- =============================================================================
-- This file demonstrates how to create a character spawner from Lua.
--
-- A CharacterSpawner entity references a character in the CharacterRegistry and
-- automatically spawns/despawns that character based on camera distance. The
-- spawner uses the entity's TransformComponent for the spawn position and
-- rotation.
--
-- Distances are stored squared on the component for efficient comparison, so
-- set spawnDistanceSq and despawnDistanceSq to the square of the desired
-- distance.
-- =============================================================================
-- =============================================================================
-- Creating a Character Spawner
-- =============================================================================
local spawner = ecs.create_entity()
ecs.set_entity_name(spawner, "Village Guard Spawner")
-- The spawner needs a world transform so the system knows where to spawn the
-- character.
ecs.set_component(spawner, "Transform", {
position = { 120.0, 0.0, 240.0 },
rotation = { 1, 0, 0, 0 }, -- quaternion w, x, y, z
scale = { 1, 1, 1 }
})
-- Add the spawner component. registryId is the ID of a character in the
-- CharacterRegistry. spawnDistanceSq / despawnDistanceSq are squared distances
-- in world units.
ecs.set_component(spawner, "CharacterSpawner", {
registryId = 42,
spawnDistanceSq = 100.0 * 100.0, -- spawn when camera is within 100 units
despawnDistanceSq = 200.0 * 200.0 -- despawn when camera is beyond 200 units
})
print("Created character spawner at (120, 0, 240) for registry character 42")
-- =============================================================================
-- Querying and Updating a Spawner
-- =============================================================================
if ecs.has_component(spawner, "CharacterSpawner") then
local s = ecs.get_component(spawner, "CharacterSpawner")
print("Spawner registryId: " .. s.registryId)
print("Spawner spawnDistanceSq: " .. s.spawnDistanceSq)
print("Spawner despawnDistanceSq: " .. s.despawnDistanceSq)
end
-- Change which character is spawned by updating the registry ID:
ecs.set_field(spawner, "CharacterSpawner", "registryId", 7)
-- Adjust distances using squared values:
ecs.set_field(spawner, "CharacterSpawner", "spawnDistanceSq", 150.0 * 150.0)
ecs.set_field(spawner, "CharacterSpawner", "despawnDistanceSq", 300.0 * 300.0)
local updated = ecs.get_component(spawner, "CharacterSpawner")
print("Updated spawner registryId: " .. updated.registryId)
print("Updated spawnDistanceSq: " .. updated.spawnDistanceSq)
print("Updated despawnDistanceSq: " .. updated.despawnDistanceSq)
-- =============================================================================
-- Creating a Dynamic Character and Spawning it Automatically
-- =============================================================================
-- Create a character record at runtime. The returned value is the registry ID
-- that the CharacterSpawner component references.
local dynamicNpc = ecs.character.create("Bree", "Miller", "", true)
print("Created dynamic character (ID: " .. dynamicNpc .. ")")
-- Create another spawner entity that will automatically spawn Bree when the
-- camera gets close and despawn her when the camera moves away.
local dynamicSpawner = ecs.create_entity()
ecs.set_entity_name(dynamicSpawner, "Bree Miller Spawner")
ecs.set_component(dynamicSpawner, "Transform", {
position = { 50.0, 0.0, 80.0 },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(dynamicSpawner, "CharacterSpawner", {
registryId = dynamicNpc,
spawnDistanceSq = 80.0 * 80.0,
despawnDistanceSq = 160.0 * 160.0
})
print("Dynamic spawner set up for character " .. dynamicNpc)
-- You can still query the spawner settings:
local dynamicSettings = ecs.get_component(dynamicSpawner, "CharacterSpawner")
print("Dynamic spawner registryId: " .. dynamicSettings.registryId)
print("Dynamic spawner spawnDistanceSq: " .. dynamicSettings.spawnDistanceSq)
-- The CharacterSpawnerSystem will now manage the spawned entity automatically.
-- The registry record stays even if the spawned instance is despawned, so the
-- character can be respawned again later.
-- =============================================================================
-- Removing a Spawner
-- =============================================================================
-- Removing the component will also despawn any currently spawned character
-- instance managed by this spawner.
ecs.remove_component(spawner, "CharacterSpawner")
ecs.remove_component(dynamicSpawner, "CharacterSpawner")
if not ecs.has_component(spawner, "CharacterSpawner") and
not ecs.has_component(dynamicSpawner, "CharacterSpawner") then
print("CharacterSpawner components removed; any spawned instances were despawned")
end
print("CharacterSpawner example completed successfully!")
@@ -33,6 +33,7 @@
#include "components/Primitive.hpp"
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/CharacterSpawner.hpp"
#include "components/CharacterIdentity.hpp"
#include "systems/CharacterRegistry.hpp"
#include "components/AnimationTree.hpp"
@@ -570,6 +571,27 @@ static void registerAllComponents()
}
});
// --- CharacterSpawner ---
REGISTER_COMPONENT(
CharacterSpawnerComponent, "CharacterSpawner",
lua_pushinteger(L, (lua_Integer)c.registryId);
lua_setfield(L, -2, "registryId");
lua_pushnumber(L, c.spawnDistanceSq);
lua_setfield(L, -2, "spawnDistanceSq");
lua_pushnumber(L, c.despawnDistanceSq);
lua_setfield(L, -2, "despawnDistanceSq");
, if (lua_getfield(L, idx, "registryId"), lua_isnumber(L, -1))
c.registryId = (uint64_t)lua_tointeger(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, idx, "spawnDistanceSq"),
lua_isnumber(L, -1))
c.spawnDistanceSq = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, idx, "despawnDistanceSq"),
lua_isnumber(L, -1))
c.despawnDistanceSq = (float)lua_tonumber(L, -1);
lua_pop(L, 1););
// --- AnimationTree ---
REGISTER_COMPONENT(
AnimationTreeComponent, "AnimationTree",
@@ -272,8 +272,12 @@ void ActuatorSystem::update(float deltaTime)
return;
}
// Find player character
// Find player character (resolve spawned instance if controller
// targets a spawner).
flecs::entity playerCharacter = flecs::entity::null();
if (m_editorApp)
playerCharacter = m_editorApp->getPlayerCharacterEntity();
float actuatorDistance = 25.0f;
float actuatorCooldown = 1.5f;
m_circleColor = ImVec4(0.0f, 0.4f, 1.0f, 1.0f);
@@ -281,26 +285,17 @@ void ActuatorSystem::update(float deltaTime)
m_nearRadius = 14.0f;
m_labelFontSize = 14.0f;
m_world.query<PlayerControllerComponent>().each([&](flecs::entity e,
PlayerControllerComponent
&pc) {
(void)e;
if (!playerCharacter.is_alive()) {
m_world.query<EntityNameComponent>().each(
[&](flecs::entity ec, EntityNameComponent &en) {
if (!playerCharacter.is_alive() &&
en.name == pc.targetCharacterName)
playerCharacter = ec;
});
actuatorDistance = pc.actuatorDistance;
actuatorCooldown = pc.actuatorCooldown;
m_circleColor = ImVec4(pc.actuatorColor.x,
pc.actuatorColor.y,
pc.actuatorColor.z, 1.0f);
m_distantRadius = pc.distantCircleRadius;
m_nearRadius = pc.nearCircleRadius;
m_labelFontSize = pc.actuatorLabelFontSize;
}
m_world.query<PlayerControllerComponent>().each([&](flecs::entity,
PlayerControllerComponent
&pc) {
actuatorDistance = pc.actuatorDistance;
actuatorCooldown = pc.actuatorCooldown;
m_circleColor = ImVec4(pc.actuatorColor.x,
pc.actuatorColor.y,
pc.actuatorColor.z, 1.0f);
m_distantRadius = pc.distantCircleRadius;
m_nearRadius = pc.nearCircleRadius;
m_labelFontSize = pc.actuatorLabelFontSize;
});
if (!playerCharacter.is_alive() ||
@@ -4,13 +4,22 @@
#include "../components/Renderable.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/Character.hpp"
#include "../components/PlayerController.hpp"
#include <OgreEntity.h>
#include <OgreLogManager.h>
#include <OgreSceneNode.h>
#include <cmath>
#include <iostream>
static unsigned short findBoneBlendIndex(Ogre::SkeletonInstance *skel, Ogre::Bone *bone)
#ifdef EDITSCENE_VERBOSE_DEBUG
#define EDITSCENE_LOG_ANIM(msg) \
Ogre::LogManager::getSingleton().logMessage(msg)
#else
#define EDITSCENE_LOG_ANIM(msg)
#endif
static unsigned short findBoneBlendIndex(Ogre::SkeletonInstance *skel,
Ogre::Bone *bone)
{
unsigned short handle = bone->getHandle();
if (handle < skel->getNumBones() && skel->getBone(handle) == bone)
@@ -54,7 +63,8 @@ Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e)
if (!e.is_alive())
return nullptr;
CharacterSlotSystem *slotSystem = CharacterSlotSystem::getSingletonPtr();
CharacterSlotSystem *slotSystem =
CharacterSlotSystem::getSingletonPtr();
if (slotSystem) {
Ogre::Entity *masterEnt = slotSystem->getMasterEntity(e);
if (masterEnt && masterEnt->hasSkeleton())
@@ -106,12 +116,11 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
* setupEntity() does not produce a one-frame zero-delta
* stutter when the mesh is recreated by CharacterSlotSystem.
*/
std::unordered_map<Ogre::String, std::pair<Ogre::Vector3, bool>>
std::unordered_map<Ogre::String, std::pair<Ogre::Vector3, bool> >
prevRootMotion;
for (const auto &pair : state.animations) {
prevRootMotion[pair.first] =
{ pair.second.prevRootPos,
pair.second.hasPrevRootPos };
prevRootMotion[pair.first] = { pair.second.prevRootPos,
pair.second.hasPrevRootPos };
}
state.ogreEntity = ent;
state.ogreEntityName = ent->getName();
@@ -154,13 +163,16 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
if (state.rootBone) {
as->destroyBlendMask();
as->createBlendMask(skel->getNumBones(), 1.0f);
unsigned short blendIdx = findBoneBlendIndex(skel, state.rootBone);
unsigned short blendIdx = findBoneBlendIndex(
skel, state.rootBone);
if (blendIdx != (unsigned short)-1) {
as->setBlendMaskEntry(blendIdx, 0.0f);
} else {
std::cout << "AnimationTreeSystem::setupEntity: WARNING could not find blend mask index for root bone "
std::cout
<< "AnimationTreeSystem::setupEntity: WARNING could not find blend mask index for root bone "
<< state.rootBone->getName()
<< " handle=" << state.rootBone->getHandle()
<< " handle="
<< state.rootBone->getHandle()
<< std::endl;
}
}
@@ -189,17 +201,19 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
* would make loopTranslation slightly
* wrong and cause a backward lurch on
* wrap frames. */
Ogre::TransformKeyFrame tkfBeg(
info.rootTrack, 0.0f);
Ogre::TransformKeyFrame tkfEnd(
info.rootTrack, 0.0f);
Ogre::TransformKeyFrame
tkfBeg(info.rootTrack,
0.0f);
Ogre::TransformKeyFrame
tkfEnd(info.rootTrack,
0.0f);
info.rootTrack
->getInterpolatedKeyFrame(
0.0f, &tkfBeg);
info.rootTrack
->getInterpolatedKeyFrame(
as->getLength(),
&tkfEnd);
0.0f,
&tkfBeg);
info.rootTrack->getInterpolatedKeyFrame(
as->getLength(),
&tkfEnd);
info.loopTranslation =
tkfEnd.getTranslate() -
tkfBeg.getTranslate();
@@ -388,18 +402,23 @@ void AnimationTreeSystem::update(float deltaTime)
if (!state.ogreEntity)
return;
#ifdef EDITSCENE_VERBOSE_DEBUG
/* Diagnostic: check if OGRE moved the root bone during rendering */
if (state.rootBone) {
Ogre::Vector3 currentPos = state.rootBone->getPosition();
Ogre::Vector3 diff = currentPos - state.rootBindingPosition;
Ogre::Vector3 currentPos =
state.rootBone->getPosition();
Ogre::Vector3 diff =
currentPos - state.rootBindingPosition;
if (diff.squaredLength() > 0.0001f) {
std::cout << "[ANIM_DBG] ROOT BONE MOVED! entity=" << e.id()
<< " current=" << currentPos
<< " binding=" << state.rootBindingPosition
<< " diff=" << diff
<< std::endl;
std::cout
<< "[ANIM_DBG] ROOT BONE MOVED! entity="
<< e.id() << " current=" << currentPos
<< " binding="
<< state.rootBindingPosition
<< " diff=" << diff << std::endl;
}
}
#endif
if (!at.enabled) {
disableAllAnimations(state);
@@ -445,13 +464,13 @@ void AnimationTreeSystem::update(float deltaTime)
if (at.useRootMotion && info.rootTrack) {
Ogre::TransformKeyFrame tkf(nullptr,
0.0f);
0.0f);
info.rootTrack->getInterpolatedKeyFrame(
thisTime, &tkf);
Ogre::Vector3 thisPos =
tkf.getTranslate();
/*
/*
* For looping animations, use the average
* cycle velocity (loopTranslation / duration)
* instead of exact frame-to-frame displacement.
@@ -464,45 +483,62 @@ void AnimationTreeSystem::update(float deltaTime)
* displacement so acceleration curves are
* preserved.
*/
float animLen = info.ogreAnimState->getLength();
if (
animLen > 0.0001f &&
info.loopTranslation.squaredLength() > 0.0001f) {
Ogre::Vector3 avgVelLocal =
info.loopTranslation / animLen;
totalRootMotion +=
avgVelLocal *
data.timeDelta *
data.weight;
} else if (info.hasPrevRootPos) {
Ogre::Vector3 displacement =
thisPos -
info.prevRootPos;
if (thisTime < lastTime)
displacement +=
info.loopTranslation;
totalRootMotion +=
displacement *
data.weight;
} else {
/* Fallback for first frame:
float animLen =
info.ogreAnimState->getLength();
if (animLen > 0.0001f &&
info.loopTranslation.squaredLength() >
0.0001f) {
Ogre::Vector3 avgVelLocal =
info.loopTranslation /
animLen;
totalRootMotion +=
avgVelLocal *
data.timeDelta *
data.weight;
} else if (info.hasPrevRootPos) {
Ogre::Vector3 displacement =
thisPos -
info.prevRootPos;
if (thisTime < lastTime)
displacement +=
info.loopTranslation;
totalRootMotion +=
displacement *
data.weight;
} else {
/* Fallback for first frame:
* instantaneous velocity */
float sampleDt = 0.001f;
float nextTime = thisTime + sampleDt;
bool wrapped = false;
if (nextTime > info.ogreAnimState->getLength()) {
nextTime -= info.ogreAnimState->getLength();
wrapped = true;
}
Ogre::TransformKeyFrame tkfNext(nullptr, 0.0f);
info.rootTrack->getInterpolatedKeyFrame(
nextTime, &tkfNext);
Ogre::Vector3 nextPos = tkfNext.getTranslate();
if (wrapped)
nextPos += info.loopTranslation;
Ogre::Vector3 velocityLocal = (nextPos - thisPos) / sampleDt;
totalRootMotion += velocityLocal * data.timeDelta * data.weight;
float sampleDt = 0.001f;
float nextTime =
thisTime + sampleDt;
bool wrapped = false;
if (nextTime >
info.ogreAnimState
->getLength()) {
nextTime -=
info.ogreAnimState
->getLength();
wrapped = true;
}
Ogre::TransformKeyFrame tkfNext(
nullptr, 0.0f);
info.rootTrack
->getInterpolatedKeyFrame(
nextTime,
&tkfNext);
Ogre::Vector3 nextPos =
tkfNext.getTranslate();
if (wrapped)
nextPos +=
info.loopTranslation;
Ogre::Vector3 velocityLocal =
(nextPos - thisPos) /
sampleDt;
totalRootMotion +=
velocityLocal *
data.timeDelta *
data.weight;
}
info.prevRootPos = thisPos;
info.hasPrevRootPos = true;
@@ -510,36 +546,89 @@ void AnimationTreeSystem::update(float deltaTime)
}
}
if (at.useRootMotion && sceneNode) {
/* Smooth root-motion displacement with EMA to
if (at.useRootMotion && sceneNode) {
/* Smooth root-motion displacement with EMA to
* filter high-frequency hip oscillation within
* the walk cycle. Alpha = 0.15 gives strong
* attenuation of ~2 Hz walk-cycle jitter while
* still responding to stops within ~6 frames. */
float alpha = 0.15f;
if (state.hasSmoothedRootMotion) {
totalRootMotion =
state.smoothedRootMotion *
(1.0f - alpha) +
totalRootMotion * alpha;
}
state.smoothedRootMotion = totalRootMotion;
state.hasSmoothedRootMotion = true;
float alpha = 0.15f;
if (state.hasSmoothedRootMotion) {
totalRootMotion = state.smoothedRootMotion *
(1.0f - alpha) +
totalRootMotion * alpha;
}
state.smoothedRootMotion = totalRootMotion;
state.hasSmoothedRootMotion = true;
Ogre::Quaternion worldRot =
sceneNode->_getDerivedOrientation();
Ogre::Vector3 displacementWorld =
worldRot * totalRootMotion;
Ogre::Quaternion worldRot =
sceneNode->_getDerivedOrientation();
Ogre::Vector3 displacementWorld =
worldRot * totalRootMotion;
if (e.has<CharacterComponent>()) {
auto &cc = e.get_mut<CharacterComponent>();
cc.useRootMotion = true;
if (deltaTime > 0.0000001f) {
cc.linearVelocity =
displacementWorld / deltaTime;
}
if (e.has<CharacterComponent>()) {
auto &cc = e.get_mut<CharacterComponent>();
cc.useRootMotion = true;
if (deltaTime > 0.0000001f) {
cc.linearVelocity =
displacementWorld / deltaTime;
}
}
}
#ifdef EDITSCENE_VERBOSE_DEBUG
/* Diagnostic: for player-controlled characters in the idle
* locomotion state, log which Ogre animations are still
* active and the state-machine fade weights. This helps
* catch cases where walk/run keeps playing after idle is
* requested. */
if (e.has<PlayerControlledComponent>()) {
auto itLoco = at.currentStates.find("locomotion");
if (itLoco != at.currentStates.end() &&
itLoco->second == "idle") {
Ogre::String animList;
bool hasNonIdle = false;
int activeCount = 0;
for (const auto &pair : ctx.animData) {
if (pair.second.weight > 0.001f) {
activeCount++;
animList += pair.first + "=" +
Ogre::StringConverter::toString(
pair.second.weight) +
" ";
if (pair.first != "idle")
hasNonIdle = true;
}
}
auto itFade = state.fadeStates.find("locomotion");
Ogre::String fadeList;
bool fading = false;
if (itFade != state.fadeStates.end()) {
for (const auto &w : itFade->second.weights) {
fadeList += w.first + "=" +
Ogre::StringConverter::toString(
w.second) +
" ";
}
fadeList += "fadeIn={";
for (const auto &n : itFade->second.fadeIn) {
fadeList += n + " ";
fading = true;
}
fadeList += "} fadeOut={";
for (const auto &n : itFade->second.fadeOut) {
fadeList += n + " ";
fading = true;
}
fadeList += "}";
}
if (activeCount > 1 || hasNonIdle || fading) {
EDITSCENE_LOG_ANIM("[ANIM_DBG] player idle anims: " +
animList + "| fade: " + fadeList);
}
}
}
#endif
/* Reset root bone to binding pose */
if (state.rootBone) {
state.rootBone->setPosition(state.rootBindingPosition);
@@ -551,7 +640,6 @@ void AnimationTreeSystem::update(float deltaTime)
/* Handle end-of-animation transitions */
checkEndTransitions(e, at, state, ctx);
});
}
void AnimationTreeSystem::evaluateNode(const AnimationTreeNode &node,
@@ -730,9 +818,19 @@ void AnimationTreeSystem::setStateInternal(flecs::entity e,
return;
auto &fade = state.fadeStates[stateMachineName];
fade.fadeOut.insert(itCurrent->second);
const Ogre::String &oldState = itCurrent->second;
/* If the new state is currently fading out, stop fading it out; if the
* old state is still fading in, stop fading it in. Otherwise a state can
* end up in both fadeIn and fadeOut simultaneously and its weight gets
* stuck. */
fade.fadeOut.erase(stateName);
fade.fadeIn.erase(oldState);
fade.fadeOut.insert(oldState);
fade.fadeIn.insert(stateName);
fade.weights[stateName] = 0.0f;
if (fade.weights.find(stateName) == fade.weights.end())
fade.weights[stateName] = 0.0f;
itCurrent->second = stateName;
state.prevStates[stateMachineName] = stateName;
@@ -820,5 +918,3 @@ AnimationTreeSystem::findAnimationNode(const AnimationTreeNode &stateNode) const
}
return nullptr;
}
@@ -713,6 +713,10 @@ flecs::entity CharacterRegistry::findSpawnedEntity(uint64_t id) const
void CharacterRegistry::markCharacterDirty(uint64_t id)
{
CharacterRecord *rec = findCharacter(id);
if (rec)
rec->version++;
flecs::entity e = findSpawnedEntity(id);
if (!e.is_alive())
return;
@@ -720,6 +724,12 @@ void CharacterRegistry::markCharacterDirty(uint64_t id)
m_slotSystem->markEntityDirty(e.id());
}
uint64_t CharacterRegistry::getCharacterVersion(uint64_t id) const
{
const CharacterRecord *rec = findCharacter(id);
return rec ? rec->version : 0;
}
bool CharacterRegistry::despawnCharacter(uint64_t id)
{
if (!m_world)
@@ -99,6 +99,10 @@ public:
std::unordered_map<std::string, int64_t> intColumns;
std::unordered_map<std::string, double> floatColumns;
std::unordered_map<std::string, std::string> stringColumns;
/* Version counter incremented when the record changes in ways that
* require a respawn of distance-spawned characters. Runtime-only. */
uint64_t version = 0;
};
struct GroupRecord {
@@ -332,6 +336,14 @@ public:
* registry field that affects the character's look.
*/
void markCharacterDirty(uint64_t id);
/**
* Return the current version of a character record. The version is
* bumped by markCharacterDirty() and can be used by spawners to detect
* registry changes that require a respawn.
*/
uint64_t getCharacterVersion(uint64_t id) const;
bool despawnCharacter(uint64_t id);
flecs::entity spawnCharacter(uint64_t id);
flecs::entity spawnInlineCharacter(const CharacterRecord &c,
@@ -0,0 +1,344 @@
#include "CharacterSpawnerSystem.hpp"
#include "CharacterRegistry.hpp"
#include "CameraSystem.hpp"
#include "EditorUISystem.hpp"
#include "../components/CharacterSpawner.hpp"
#include "../components/Transform.hpp"
#include "../components/EditorMarker.hpp"
#include "../components/Renderable.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../GameMode.hpp"
#include <OgreCamera.h>
#include <OgreLogManager.h>
CharacterSpawnerSystem::CharacterSpawnerSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorCameraSystem *cameraSystem,
EditorUISystem *uiSystem)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_cameraSystem(cameraSystem)
, m_uiSystem(uiSystem)
, m_query(world.query<CharacterSpawnerComponent, TransformComponent>())
{
m_world.observer<CharacterSpawnerComponent>("CharacterSpawnerCleanup")
.event(flecs::OnRemove)
.each([this](flecs::entity e, CharacterSpawnerComponent &) {
despawn(e);
});
}
CharacterSpawnerSystem::~CharacterSpawnerSystem()
{
std::vector<flecs::entity_t> ids;
ids.reserve(m_spawnedEntities.size());
for (const auto &pair : m_spawnedEntities)
ids.push_back(pair.first);
for (flecs::entity_t id : ids)
despawn(m_world.entity(id));
}
void CharacterSpawnerSystem::initialize()
{
if (m_initialized)
return;
m_initialized = true;
Ogre::LogManager::getSingleton().logMessage(
"CharacterSpawnerSystem initialized");
}
Ogre::Vector3 CharacterSpawnerSystem::getCameraPosition() const
{
if (m_cameraSystem) {
Ogre::Camera *cam = m_cameraSystem->getActiveCamera();
if (cam)
return cam->getDerivedPosition();
}
return Ogre::Vector3::ZERO;
}
void CharacterSpawnerSystem::spawnCharacter(
flecs::entity spawnerEntity, const CharacterSpawnerComponent &spawner)
{
if (spawner.registryId == 0 || !spawnerEntity.is_alive() ||
!spawnerEntity.has<TransformComponent>())
return;
/* Make sure any previous instance is gone first. */
despawn(spawnerEntity);
flecs::entity character =
CharacterRegistry::getSingleton().spawnCharacter(
spawner.registryId);
if (!character.is_alive()) {
const CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton().findCharacter(
spawner.registryId);
if (rec) {
character = CharacterRegistry::getSingleton()
.spawnInlineCharacter(
*rec, Ogre::Vector3::ZERO);
}
}
if (!character.is_alive()) {
Ogre::LogManager::getSingleton().logMessage(
"CharacterSpawnerSystem: failed to spawn character " +
std::to_string(spawner.registryId));
return;
}
/* Apply spawner transform to the spawned character. */
if (character.has<TransformComponent>()) {
const auto &spawnerTransform =
spawnerEntity.get<TransformComponent>();
auto &charTransform = character.get_mut<TransformComponent>();
charTransform.position = spawnerTransform.position;
charTransform.rotation = spawnerTransform.rotation;
charTransform.scale = spawnerTransform.scale;
if (charTransform.node && spawnerTransform.node) {
charTransform.node->setPosition(
spawnerTransform.node->getPosition());
charTransform.node->setOrientation(
spawnerTransform.node->getOrientation());
charTransform.node->setScale(
spawnerTransform.node->getScale());
}
charTransform.applyToNode();
}
/* Hide from editor entity lists and property panels. */
if (character.has<EditorMarkerComponent>())
character.remove<EditorMarkerComponent>();
/* Remove from editor UI cache so the spawned instance does not appear in
* entity lists or property panels. */
if (m_uiSystem)
m_uiSystem->removeEntity(character);
m_spawnedEntities[spawnerEntity.id()] = character;
m_spawnedVersions[spawnerEntity.id()] =
CharacterRegistry::getSingleton().getCharacterVersion(
spawner.registryId);
/* Remember the spawner transform so we only re-apply it when it actually
* changes, leaving the spawned character free to move on its own. */
const auto &spawnerTransform = spawnerEntity.get<TransformComponent>();
m_lastSpawnerTransforms[spawnerEntity.id()] = {
spawnerTransform.position,
spawnerTransform.rotation,
spawnerTransform.scale,
};
Ogre::LogManager::getSingleton().logMessage(
"CharacterSpawnerSystem: spawned character " +
std::to_string(spawner.registryId) + " for spawner " +
std::to_string(spawnerEntity.id()));
}
void CharacterSpawnerSystem::destroySpawnedEntity(flecs::entity character)
{
if (!character.is_alive())
return;
/* Explicitly clean up Ogre resources that flecs component destructors do
* not handle automatically. */
if (character.has<TransformComponent>()) {
auto &transform = character.get_mut<TransformComponent>();
if (transform.node) {
try {
m_sceneMgr->destroySceneNode(transform.node);
} catch (...) {
}
transform.node = nullptr;
}
}
if (character.has<RenderableComponent>()) {
auto &renderable = character.get_mut<RenderableComponent>();
if (renderable.entity) {
try {
m_sceneMgr->destroyEntity(renderable.entity);
} catch (...) {
}
renderable.entity = nullptr;
}
}
/* Remove from editor UI cache before destruction. */
if (m_uiSystem)
m_uiSystem->removeEntity(character);
/* CharacterSlotSystem and CharacterSystem clean up the rest via their
* OnRemove observers. */
character.destruct();
}
void CharacterSpawnerSystem::applySpawnerTransform(
flecs::entity spawnerEntity, flecs::entity characterEntity)
{
if (!spawnerEntity.is_alive() || !characterEntity.is_alive())
return;
if (!spawnerEntity.has<TransformComponent>() ||
!characterEntity.has<TransformComponent>())
return;
const auto &spawnerTransform = spawnerEntity.get<TransformComponent>();
auto &charTransform = characterEntity.get_mut<TransformComponent>();
charTransform.position = spawnerTransform.position;
charTransform.rotation = spawnerTransform.rotation;
charTransform.scale = spawnerTransform.scale;
if (charTransform.node && spawnerTransform.node) {
charTransform.node->setPosition(
spawnerTransform.node->getPosition());
charTransform.node->setOrientation(
spawnerTransform.node->getOrientation());
charTransform.node->setScale(spawnerTransform.node->getScale());
}
charTransform.applyToNode();
}
void CharacterSpawnerSystem::despawn(flecs::entity spawnerEntity)
{
if (!spawnerEntity.is_alive())
return;
auto it = m_spawnedEntities.find(spawnerEntity.id());
if (it == m_spawnedEntities.end())
return;
flecs::entity character = it->second;
m_spawnedEntities.erase(it);
m_spawnedVersions.erase(spawnerEntity.id());
m_lastSpawnerTransforms.erase(spawnerEntity.id());
destroySpawnedEntity(character);
Ogre::LogManager::getSingleton().logMessage(
"CharacterSpawnerSystem: despawned character for spawner " +
std::to_string(spawnerEntity.id()));
}
void CharacterSpawnerSystem::spawn(flecs::entity spawnerEntity)
{
if (!spawnerEntity.is_alive() ||
!spawnerEntity.has<CharacterSpawnerComponent>())
return;
spawnCharacter(spawnerEntity,
spawnerEntity.get<CharacterSpawnerComponent>());
}
void CharacterSpawnerSystem::lockSpawner(flecs::entity spawnerEntity)
{
if (!spawnerEntity.is_alive() ||
!spawnerEntity.has<CharacterSpawnerComponent>())
return;
m_lockedSpawners.insert(spawnerEntity.id());
}
void CharacterSpawnerSystem::unlockSpawner(flecs::entity spawnerEntity)
{
m_lockedSpawners.erase(spawnerEntity.id());
}
flecs::entity
CharacterSpawnerSystem::getSpawnedCharacter(flecs::entity spawnerEntity) const
{
auto it = m_spawnedEntities.find(spawnerEntity.id());
if (it != m_spawnedEntities.end() && it->second.is_alive())
return it->second;
return flecs::entity::null();
}
flecs::entity
CharacterSpawnerSystem::getSpawnerForCharacter(flecs::entity characterEntity) const
{
for (const auto &pair : m_spawnedEntities) {
if (pair.second == characterEntity)
return m_world.entity(pair.first);
}
return flecs::entity::null();
}
bool CharacterSpawnerSystem::isSpawnerLocked(flecs::entity spawnerEntity) const
{
return m_lockedSpawners.find(spawnerEntity.id()) !=
m_lockedSpawners.end();
}
void CharacterSpawnerSystem::update()
{
if (!m_initialized)
return;
const Ogre::Vector3 cameraPos = getCameraPosition();
m_query.each([&](flecs::entity e, CharacterSpawnerComponent &spawner,
TransformComponent &transform) {
if (!transform.node)
return;
const Ogre::Vector3 diff =
transform.node->_getDerivedPosition() - cameraPos;
const float distSq = diff.squaredLength();
auto spawnedIt = m_spawnedEntities.find(e.id());
const bool isSpawned = spawnedIt != m_spawnedEntities.end() &&
spawnedIt->second.is_alive();
const bool locked = isSpawnerLocked(e);
if (locked && !isSpawned) {
spawnCharacter(e, spawner);
} else if (!isSpawned && distSq <= spawner.spawnDistanceSq) {
spawnCharacter(e, spawner);
} else if (isSpawned && !locked &&
distSq > spawner.despawnDistanceSq) {
despawn(e);
} else if (isSpawned) {
const uint64_t currentVersion =
CharacterRegistry::getSingleton()
.getCharacterVersion(
spawner.registryId);
auto verIt = m_spawnedVersions.find(e.id());
if (verIt == m_spawnedVersions.end() ||
verIt->second != currentVersion) {
despawn(e);
spawnCharacter(e, spawner);
} else {
/* Editor placement: if the spawner itself moved/rotated/scaled,
* update the spawned character to match. Otherwise leave the
* character alone so it can move on its own. */
auto lastIt =
m_lastSpawnerTransforms.find(e.id());
const bool hasSnapshot =
lastIt != m_lastSpawnerTransforms.end();
const bool moved =
!hasSnapshot ||
(transform.position -
lastIt->second.position)
.squaredLength() >
1e-6f ||
(transform.scale - lastIt->second.scale)
.squaredLength() >
1e-6f ||
1.0f - Ogre::Math::Abs(
transform.rotation.Dot(
lastIt->second
.rotation)) >
1e-4f;
if (moved) {
applySpawnerTransform(
e, spawnedIt->second);
m_lastSpawnerTransforms[e.id()] = {
transform.position,
transform.rotation,
transform.scale,
};
}
}
}
});
}
@@ -0,0 +1,101 @@
#ifndef EDITSCENE_CHARACTERSPAWNERSYSTEM_HPP
#define EDITSCENE_CHARACTERSPAWNERSYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <unordered_map>
#include <unordered_set>
class EditorCameraSystem;
class EditorUISystem;
/**
* @brief Distance-based character spawning system.
*
* Queries entities with CharacterSpawnerComponent and TransformComponent.
* Spawns the referenced registry character when the active camera is within
* spawnDistanceSq, and despawns it when the camera moves beyond
* despawnDistanceSq. Registry changes that bump the character version also
* trigger a respawn.
*/
class CharacterSpawnerSystem {
public:
CharacterSpawnerSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorCameraSystem *cameraSystem,
EditorUISystem *uiSystem = nullptr);
~CharacterSpawnerSystem();
void initialize();
void update();
/**
* Manually despawn the character associated with a spawner, if any.
*/
void despawn(flecs::entity spawnerEntity);
/**
* Force an immediate spawn for the given spawner, regardless of camera
* distance. Does nothing if the spawner is missing or invalid.
*/
void spawn(flecs::entity spawnerEntity);
/**
* Lock a spawner so that its spawned character is kept alive even when
* the camera moves out of range. Multiple locks are idempotent.
*/
void lockSpawner(flecs::entity spawnerEntity);
/**
* Remove a lock previously added with lockSpawner().
*/
void unlockSpawner(flecs::entity spawnerEntity);
/**
* Return the character entity currently spawned by a spawner, or null if
* none is alive.
*/
flecs::entity getSpawnedCharacter(flecs::entity spawnerEntity) const;
/**
* Return the spawner entity that currently owns the given character, or null
* if the character is not spawned by any known spawner.
*/
flecs::entity getSpawnerForCharacter(flecs::entity characterEntity) const;
/**
* Return true if the spawner is currently locked.
*/
bool isSpawnerLocked(flecs::entity spawnerEntity) const;
private:
void spawnCharacter(flecs::entity spawnerEntity,
const struct CharacterSpawnerComponent &spawner);
void destroySpawnedEntity(flecs::entity character);
void applySpawnerTransform(flecs::entity spawnerEntity,
flecs::entity characterEntity);
Ogre::Vector3 getCameraPosition() const;
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
EditorCameraSystem *m_cameraSystem;
EditorUISystem *m_uiSystem;
flecs::query<struct CharacterSpawnerComponent, struct TransformComponent>
m_query;
bool m_initialized = false;
struct SpawnerTransformSnapshot {
Ogre::Vector3 position;
Ogre::Quaternion rotation;
Ogre::Vector3 scale;
};
std::unordered_map<flecs::entity_t, flecs::entity> m_spawnedEntities;
std::unordered_map<flecs::entity_t, uint64_t> m_spawnedVersions;
std::unordered_map<flecs::entity_t, SpawnerTransformSnapshot>
m_lastSpawnerTransforms;
std::unordered_set<flecs::entity_t> m_lockedSpawners;
};
#endif // EDITSCENE_CHARACTERSPAWNERSYSTEM_HPP
@@ -27,6 +27,7 @@
#include "../components/LodSettings.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../components/CharacterSpawner.hpp"
#include "../components/Character.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
@@ -1082,6 +1083,14 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// Render CharacterSpawner if present
if (entity.has<CharacterSpawnerComponent>()) {
auto &spawner = entity.get_mut<CharacterSpawnerComponent>();
m_componentRegistry.render<CharacterSpawnerComponent>(entity,
spawner);
componentCount++;
}
// Render AnimationTree if present
if (entity.has<AnimationTreeComponent>()) {
auto &at = entity.get_mut<AnimationTreeComponent>();
@@ -3,6 +3,7 @@
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
@@ -63,6 +64,20 @@ public:
m_allEntities.push_back(entity);
}
/**
* Remove an entity from the cached list.
*
* Used for runtime entities that should not be tracked by the editor UI
* (e.g. characters spawned by CharacterSpawnerSystem).
*/
void removeEntity(flecs::entity entity)
{
auto it = std::find(m_allEntities.begin(), m_allEntities.end(),
entity);
if (it != m_allEntities.end())
m_allEntities.erase(it);
}
/**
* Clear entity cache and deselect
*/
@@ -5,8 +5,12 @@
#include "../components/Character.hpp"
#include "../components/Transform.hpp"
#include "../components/EntityName.hpp"
#include "../components/CharacterSpawner.hpp"
#include "../components/PathFollowing.hpp"
#include "../components/BehaviorTree.hpp"
#include "AnimationTreeSystem.hpp"
#include "CharacterSlotSystem.hpp"
#include "CharacterSpawnerSystem.hpp"
#include "../camera/EditorCamera.hpp"
#include <OgreEntity.h>
#include <OgreCamera.h>
@@ -15,12 +19,21 @@
#include <OgreSkeletonInstance.h>
#include <cmath>
#ifdef EDITSCENE_VERBOSE_DEBUG
#define EDITSCENE_LOG_PC(msg) \
Ogre::LogManager::getSingleton().logMessage(msg)
#else
#define EDITSCENE_LOG_PC(msg)
#endif
PlayerControllerSystem::PlayerControllerSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
EditorApp *editorApp)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_editorApp(editorApp)
, m_spawnerSystem(editorApp ? editorApp->getCharacterSpawnerSystem() :
nullptr)
{
}
@@ -34,6 +47,10 @@ PlayerControllerSystem::~PlayerControllerSystem()
void PlayerControllerSystem::shutdownController(ControllerState &state)
{
if (state.targetEntity.is_alive())
state.targetEntity.remove<PlayerControlledComponent>();
state.targetEntity = flecs::entity::null();
if (state.pivotNode) {
m_sceneMgr->destroySceneNode(state.pivotNode);
state.pivotNode = nullptr;
@@ -42,6 +59,10 @@ void PlayerControllerSystem::shutdownController(ControllerState &state)
m_sceneMgr->destroySceneNode(state.goalNode);
state.goalNode = nullptr;
}
if (state.spawnerEntity.is_alive() && m_spawnerSystem) {
m_spawnerSystem->unlockSpawner(state.spawnerEntity);
state.spawnerEntity = flecs::entity::null();
}
state.initialized = false;
}
@@ -66,10 +87,42 @@ void PlayerControllerSystem::initController(flecs::entity controllerEntity,
ControllerState &state)
{
(void)controllerEntity;
state.targetEntity = findTargetEntity(pc.targetCharacterName);
/* Release any previous spawner lock before re-resolving the target. */
if (state.spawnerEntity.is_alive() && m_spawnerSystem) {
m_spawnerSystem->unlockSpawner(state.spawnerEntity);
}
state.spawnerEntity = flecs::entity::null();
flecs::entity namedEntity = findTargetEntity(pc.targetCharacterName);
flecs::entity target = namedEntity;
if (namedEntity.is_alive() &&
namedEntity.has<CharacterSpawnerComponent>()) {
state.spawnerEntity = namedEntity;
if (m_spawnerSystem) {
m_spawnerSystem->lockSpawner(namedEntity);
target = m_spawnerSystem->getSpawnedCharacter(
namedEntity);
if (!target.is_alive()) {
m_spawnerSystem->spawn(namedEntity);
target = m_spawnerSystem->getSpawnedCharacter(
namedEntity);
}
} else {
target = flecs::entity::null();
}
}
if (state.targetEntity.is_alive() && state.targetEntity != target)
state.targetEntity.remove<PlayerControlledComponent>();
state.targetEntity = target;
if (!state.targetEntity.is_alive())
return;
state.targetEntity.add<PlayerControlledComponent>();
if (!state.pivotNode) {
state.pivotNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
@@ -92,48 +145,54 @@ void PlayerControllerSystem::update(float deltaTime)
if (!m_editorApp ||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
m_editorApp->getGamePlayState() !=
EditorApp::GamePlayState::Playing)
EditorApp::GamePlayState::Playing) {
/* Clean up any active controller state (and the PlayerControlled
* tag) when leaving game mode / play state so editor-mode
* ActionDebug tests can run on the same character. */
for (auto &pair : m_states)
shutdownController(pair.second);
m_states.clear();
return;
}
m_world.query<PlayerControllerComponent>().each(
[&](flecs::entity e, PlayerControllerComponent &pc) {
auto it = m_states.find(e.id());
if (it == m_states.end()) {
ControllerState newState;
initController(e, pc, newState);
it = m_states.insert({ e.id(), newState }).first;
}
m_world.query<PlayerControllerComponent>().each([&](flecs::entity e,
PlayerControllerComponent
&pc) {
auto it = m_states.find(e.id());
if (it == m_states.end()) {
ControllerState newState;
initController(e, pc, newState);
it = m_states.insert({ e.id(), newState }).first;
}
ControllerState &state = it->second;
ControllerState &state = it->second;
// Re-resolve target if name changed or entity invalid
if (!state.targetEntity.is_alive()) {
initController(e, pc, state);
}
if (!state.targetEntity.is_alive())
return;
// Re-resolve target if name changed or entity invalid
if (!state.targetEntity.is_alive()) {
initController(e, pc, state);
}
if (!state.targetEntity.is_alive())
return;
if (!state.targetEntity.has<TransformComponent>())
return;
if (!state.targetEntity.has<TransformComponent>())
return;
auto &transform =
state.targetEntity.get<TransformComponent>();
if (!transform.node)
return;
auto &transform = state.targetEntity.get<TransformComponent>();
if (!transform.node)
return;
Ogre::Vector3 charPos =
transform.node->_getDerivedPosition();
Ogre::Vector3 charPos = transform.node->_getDerivedPosition();
// Update camera
if (pc.cameraMode == PlayerControllerComponent::TPS) {
updateTPSCamera(pc, state, charPos, deltaTime);
} else {
updateFPSCamera(pc, state, charPos);
}
// Update camera
if (pc.cameraMode == PlayerControllerComponent::TPS) {
updateTPSCamera(pc, state, charPos, deltaTime);
} else {
updateFPSCamera(pc, state, charPos);
}
// Update locomotion
updateLocomotion(pc, state, deltaTime);
});
// Update locomotion
updateLocomotion(pc, state, deltaTime);
});
}
void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc,
@@ -380,8 +439,113 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
}
}
Ogre::String prevState = ats->getCurrentState(
state.targetEntity, pc.locomotionStateMachine);
if (!animState.empty()) {
ats->setState(state.targetEntity, pc.locomotionStateMachine,
animState);
}
}
/* Log every locomotion transition so we can verify state names and
* see whether another system is overriding the state. */
if (prevState != animState) {
EDITSCENE_LOG_PC("[PC_DBG] locomotion transition: requested=" +
animState + " prev=" + prevState + " input w=" +
Ogre::StringConverter::toString(input.w) +
" shift=" +
Ogre::StringConverter::toString(input.shift));
}
/* Detect the stuck slow-walk condition: we are setting the walk
* animation but no movement keys are pressed, or another system
* has already forced the walk state before we could set idle. */
bool anyMovement = input.w || input.a || input.s || input.d;
bool hasPathFollowing =
state.targetEntity.has<PathFollowingComponent>();
bool hasBehaviorTree = state.targetEntity.has<BehaviorTreeComponent>();
if ((animState == pc.walkState && !anyMovement) ||
(animState == pc.idleState && prevState == pc.walkState &&
!anyMovement)) {
m_stuckWalkFrames++;
if (m_stuckWalkFrames >= 60 && !m_stuckWalkLogged) {
EDITSCENE_LOG_PC("[PC_DBG] STUCK WALK DETECTED: frames=" +
Ogre::StringConverter::toString(
m_stuckWalkFrames) +
" w=" +
Ogre::StringConverter::toString(input.w) +
" a=" +
Ogre::StringConverter::toString(input.a) +
" s=" +
Ogre::StringConverter::toString(input.s) +
" d=" +
Ogre::StringConverter::toString(input.d) +
" shift=" +
Ogre::StringConverter::toString(input.shift) +
" requested=" + animState +
" prev=" + prevState + " isMoving=" +
Ogre::StringConverter::toString(isMoving) +
" inputLocked=" +
Ogre::StringConverter::toString(
pc.inputLocked) +
" targetAlive=" +
Ogre::StringConverter::toString(
state.targetEntity.is_alive()) +
" pathFollowing=" +
Ogre::StringConverter::toString(
hasPathFollowing) +
" behaviorTree=" +
Ogre::StringConverter::toString(
hasBehaviorTree));
m_stuckWalkLogged = true;
}
} else {
m_stuckWalkFrames = 0;
m_stuckWalkLogged = false;
}
}
flecs::entity
PlayerControllerSystem::getCurrentTarget(flecs::entity controllerEntity)
{
auto it = m_states.find(controllerEntity.id());
if (it != m_states.end() && it->second.targetEntity.is_alive())
return it->second.targetEntity;
if (!controllerEntity.is_alive() ||
!controllerEntity.has<PlayerControllerComponent>())
return flecs::entity::null();
auto &pc = controllerEntity.get<PlayerControllerComponent>();
return resolveTargetEntity(pc.targetCharacterName, true);
}
flecs::entity
PlayerControllerSystem::resolveTargetEntity(const Ogre::String &targetName,
bool forceSpawn)
{
flecs::entity namedEntity = findTargetEntity(targetName);
if (!namedEntity.is_alive())
return flecs::entity::null();
if (!namedEntity.has<CharacterSpawnerComponent>())
return namedEntity;
if (!m_spawnerSystem)
return flecs::entity::null();
if (forceSpawn) {
m_spawnerSystem->lockSpawner(namedEntity);
flecs::entity spawned =
m_spawnerSystem->getSpawnedCharacter(namedEntity);
if (!spawned.is_alive()) {
m_spawnerSystem->spawn(namedEntity);
spawned = m_spawnerSystem->getSpawnedCharacter(
namedEntity);
}
return spawned;
}
return m_spawnerSystem->getSpawnedCharacter(namedEntity);
}
@@ -11,6 +11,7 @@
class EditorApp;
class AnimationTreeSystem;
class CharacterSlotSystem;
class CharacterSpawnerSystem;
/**
* System that handles player input, camera control (FPS/TPS),
@@ -26,9 +27,26 @@ public:
void update(float deltaTime);
/**
* Return the live target entity for a controller. If the controller has
* already been initialized this frame, returns its cached target. Otherwise
* falls back to resolving the controller's targetCharacterName, spawning the
* character through a spawner if necessary.
*/
flecs::entity getCurrentTarget(flecs::entity controllerEntity);
/**
* Resolve a target name to a live entity. If the named entity is a character
* spawner and forceSpawn is true, the spawner is locked and spawned and the
* spawned character entity is returned. Direct characters are returned as-is.
*/
flecs::entity resolveTargetEntity(const Ogre::String &targetName,
bool forceSpawn = false);
private:
struct ControllerState {
flecs::entity targetEntity = flecs::entity::null();
flecs::entity spawnerEntity = flecs::entity::null();
float yaw = 0.0f;
float pitch = 0.0f;
Ogre::SceneNode *pivotNode = nullptr;
@@ -54,8 +72,13 @@ private:
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
EditorApp *m_editorApp;
CharacterSpawnerSystem *m_spawnerSystem;
std::unordered_map<flecs::entity_t, ControllerState> m_states;
// Diagnostics for the stuck slow-walk bug
int m_stuckWalkFrames = 0;
bool m_stuckWalkLogged = false;
};
#endif // EDITSCENE_PLAYERCONTROLLERSYSTEM_HPP
@@ -19,6 +19,7 @@
#include "../components/Primitive.hpp"
#include "../components/TriangleBuffer.hpp"
#include "../components/Character.hpp"
#include "../components/CharacterSpawner.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/CharacterIdentity.hpp"
#include "CharacterRegistry.hpp"
@@ -158,7 +159,12 @@ bool SceneSerializer::loadFromFile(const std::string &filepath,
}
}
// Second pass: resolve material entity references
// Second pass: resolve procedural texture references and material
// entity references.
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolving procedural texture references (second pass)");
resolveProceduralTextureReferences();
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolving material references (second pass)");
resolveMaterialReferences();
@@ -249,6 +255,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["characterIdentity"] = serializeCharacterIdentity(entity);
}
if (entity.has<CharacterSpawnerComponent>()) {
json["characterSpawner"] = serializeCharacterSpawner(entity);
}
if (entity.has<AnimationTreeComponent>()) {
json["animationTree"] = serializeAnimationTree(entity);
}
@@ -477,6 +487,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
deserializeCharacterSlots(entity, json["characterSlots"]);
}
if (json.contains("characterSpawner")) {
deserializeCharacterSpawner(entity, json["characterSpawner"]);
}
if (json.contains("animationTree")) {
deserializeAnimationTree(entity, json["animationTree"]);
}
@@ -712,6 +726,10 @@ void SceneSerializer::deserializeEntityComponents(
deserializeCharacterSlots(entity, json["characterSlots"]);
}
if (json.contains("characterSpawner")) {
deserializeCharacterSpawner(entity, json["characterSpawner"]);
}
if (json.contains("animationTree")) {
deserializeAnimationTree(entity, json["animationTree"]);
}
@@ -1038,6 +1056,37 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
}
}
void SceneSerializer::resolveProceduralTextureReferences()
{
for (auto &pending : m_pendingProceduralTextureResolutions) {
flecs::entity entity = pending.first;
const std::string &textureId = pending.second;
if (!entity.is_alive() || textureId.empty() ||
!entity.has<ProceduralMaterialComponent>())
continue;
auto &material = entity.get_mut<ProceduralMaterialComponent>();
flecs::entity resolved = flecs::entity::null();
m_world.query<ProceduralTextureComponent>().each(
[&](flecs::entity textureEntity,
ProceduralTextureComponent &texture) {
if (texture.textureId == textureId) {
resolved = textureEntity;
}
});
if (resolved.is_alive()) {
material.diffuseTextureEntity = resolved;
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolved ProceduralMaterial " +
std::to_string(entity.id()) +
" diffuse texture " + textureId);
}
}
m_pendingProceduralTextureResolutions.clear();
}
void SceneSerializer::resolveMaterialReferences()
{
// Resolve Town material references
@@ -1059,6 +1108,8 @@ void SceneSerializer::resolveMaterialReferences()
ProceduralMaterialComponent>()) {
town.proceduralMaterialEntity =
matEntity;
town.proceduralMaterialEntityId =
std::to_string(matEntity.id());
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolved Town material entity " +
Ogre::StringConverter::
@@ -1090,6 +1141,8 @@ void SceneSerializer::resolveMaterialReferences()
ProceduralMaterialComponent>()) {
district.proceduralMaterialEntity =
matEntity;
district.proceduralMaterialEntityId =
std::to_string(matEntity.id());
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolved District material entity " +
Ogre::StringConverter::
@@ -1121,6 +1174,8 @@ void SceneSerializer::resolveMaterialReferences()
ProceduralMaterialComponent>()) {
lot.proceduralMaterialEntity =
matEntity;
lot.proceduralMaterialEntityId =
std::to_string(matEntity.id());
Ogre::LogManager::getSingleton().logMessage(
"SceneSerializer: Resolved Lot material entity " +
Ogre::StringConverter::
@@ -1983,16 +2038,12 @@ void SceneSerializer::deserializeProceduralMaterial(flecs::entity entity,
material.materialId = generatePersistentId();
}
// Resolve texture reference by ID (like LOD system does)
// Texture reference is resolved in a second pass after all entities
// are loaded, so the referenced ProceduralTexture entity exists even if
// it appears later in the JSON file.
if (!material.diffuseTextureId.empty()) {
m_world.query<ProceduralTextureComponent>().each(
[&](flecs::entity e,
ProceduralTextureComponent &texture) {
if (texture.textureId ==
material.diffuseTextureId) {
material.diffuseTextureEntity = e;
}
});
m_pendingProceduralTextureResolutions.push_back(
{ entity, material.diffuseTextureId });
}
if (json.contains("ambient")) {
@@ -2221,6 +2272,28 @@ void SceneSerializer::deserializeCharacterIdentity(flecs::entity entity,
entity.set<CharacterIdentityComponent>(ci);
}
nlohmann::json SceneSerializer::serializeCharacterSpawner(flecs::entity entity)
{
auto &spawner = entity.get<CharacterSpawnerComponent>();
nlohmann::json json;
json["registryId"] = spawner.registryId;
json["spawnDistance"] = std::sqrt(spawner.spawnDistanceSq);
json["despawnDistance"] = std::sqrt(spawner.despawnDistanceSq);
return json;
}
void SceneSerializer::deserializeCharacterSpawner(flecs::entity entity,
const nlohmann::json &json)
{
CharacterSpawnerComponent spawner;
spawner.registryId = json.value("registryId", 0);
float spawnDist = json.value("spawnDistance", 100.0f);
float despawnDist = json.value("despawnDistance", 200.0f);
spawner.spawnDistanceSq = spawnDist * spawnDist;
spawner.despawnDistanceSq = despawnDist * despawnDist;
entity.set<CharacterSpawnerComponent>(spawner);
}
/* Forward declarations for AnimationTreeNode serialization */
static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node);
static void deserializeAnimationTreeNode(AnimationTreeNode &node,
@@ -2695,6 +2768,8 @@ void SceneSerializer::deserializeTown(flecs::entity entity,
ProceduralMaterialComponent>()) {
town.proceduralMaterialEntity =
matEntity;
town.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
} else {
// Entity not found in map (might not exist or not loaded yet)
@@ -2705,6 +2780,8 @@ void SceneSerializer::deserializeTown(flecs::entity entity,
ProceduralMaterialComponent>()) {
town.proceduralMaterialEntity =
matEntity;
town.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
}
} catch (...) {
@@ -2764,6 +2841,8 @@ void SceneSerializer::deserializeDistrict(flecs::entity entity,
ProceduralMaterialComponent>()) {
district.proceduralMaterialEntity =
matEntity;
district.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
} else {
// Entity not found in map (might not exist or not loaded yet)
@@ -2774,6 +2853,8 @@ void SceneSerializer::deserializeDistrict(flecs::entity entity,
ProceduralMaterialComponent>()) {
district.proceduralMaterialEntity =
matEntity;
district.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
}
} catch (...) {
@@ -2825,6 +2906,8 @@ void SceneSerializer::deserializeLot(flecs::entity entity,
ProceduralMaterialComponent>()) {
lot.proceduralMaterialEntity =
matEntity;
lot.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
} else {
// Entity not found in map (might not exist or not loaded yet)
@@ -2835,6 +2918,8 @@ void SceneSerializer::deserializeLot(flecs::entity entity,
ProceduralMaterialComponent>()) {
lot.proceduralMaterialEntity =
matEntity;
lot.proceduralMaterialEntityId =
std::to_string(matEntity.id());
}
}
} catch (...) {
@@ -84,6 +84,10 @@ private:
flecs::entity parent,
EditorUISystem *uiSystem);
// Resolve procedural texture references for ProceduralMaterial components
// (must run after all entities are loaded).
void resolveProceduralTextureReferences();
// Second-pass: resolve material entity references for Town, District, Lot
void resolveMaterialReferences();
@@ -105,6 +109,7 @@ private:
nlohmann::json serializeTriangleBuffer(flecs::entity entity);
nlohmann::json serializeCharacter(flecs::entity entity);
nlohmann::json serializeCharacterIdentity(flecs::entity entity);
nlohmann::json serializeCharacterSpawner(flecs::entity entity);
nlohmann::json serializeCharacterSlots(flecs::entity entity);
nlohmann::json serializeCharacterShapeKeys(flecs::entity entity);
nlohmann::json serializeAnimationTree(flecs::entity entity);
@@ -158,6 +163,8 @@ private:
const nlohmann::json &json);
void deserializeCharacterIdentity(flecs::entity entity,
const nlohmann::json &json);
void deserializeCharacterSpawner(flecs::entity entity,
const nlohmann::json &json);
void deserializeCharacterSlots(flecs::entity entity,
const nlohmann::json &json);
void deserializeCharacterShapeKeys(flecs::entity entity,
@@ -305,6 +312,10 @@ private:
std::vector<PendingResolution> m_pendingTownResolutions;
std::vector<PendingResolution> m_pendingDistrictResolutions;
std::vector<PendingResolution> m_pendingLotResolutions;
// Pending ProceduralMaterial -> ProceduralTexture resolutions
std::vector<std::pair<flecs::entity, std::string>>
m_pendingProceduralTextureResolutions;
};
#endif // EDITSCENE_SCENESERIALIZER_HPP
@@ -36,6 +36,11 @@ static bool hasRootMotion(flecs::entity e)
return e.get<AnimationTreeComponent>().useRootMotion;
}
static bool isPlayerControlled(flecs::entity e)
{
return e.has<PlayerControlledComponent>();
}
SmartObjectSystem *SmartObjectSystem::s_instance = nullptr;
SmartObjectSystem::SmartObjectSystem(flecs::world &world,
@@ -253,7 +258,8 @@ void SmartObjectSystem::update(float deltaTime)
// Skip player-controlled characters - they are managed
// by PlayerControllerSystem, not by SmartObjectSystem AI.
if (playerCharacterIds.find(e.id()) !=
playerCharacterIds.end())
playerCharacterIds.end() ||
isPlayerControlled(e))
return;
auto &state = m_states[e.id()];
@@ -671,7 +677,8 @@ void SmartObjectSystem::update(float deltaTime)
// Skip player-controlled characters - they are managed
// by PlayerControllerSystem, not by SmartObjectSystem AI.
if (playerCharacterIds.find(e.id()) != playerCharacterIds.end())
if (playerCharacterIds.find(e.id()) != playerCharacterIds.end() ||
isPlayerControlled(e))
return;
auto &state = m_states[e.id()];
Binary file not shown.
@@ -6,14 +6,11 @@
* add_component, remove_component, has_component, get_component,
* set_component, get_field, set_field.
*
* Build with:
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
* component_lua_test.cpp \
* ../lua/LuaComponentApi.cpp \
* ../../lua/lua-5.4.8/src/liblua.a \
* -o component_lua_test -lm
* Build with CMake (recommended):
* cmake --build <build-dir> --target component_lua_test
*
* Or via CMake (see CMakeLists.txt in this directory).
* Run:
* <build-dir>/src/features/editScene/component_lua_test
*/
#include <cstdio>
@@ -1386,22 +1383,26 @@ static int testTriangleBufferComponent(lua_State *L)
}
// ---------------------------------------------------------------------------
// Test 46: CharacterSlots component
// Test 46: CharacterSpawner component
// ---------------------------------------------------------------------------
static int testCharacterSlotsComponent(lua_State *L)
static int testCharacterSpawnerComponent(lua_State *L)
{
TEST("CharacterSlots component");
TEST("CharacterSpawner component");
bool ok = runLua(L, "local id = ecs.create_entity();"
"ecs.set_component(id, 'CharacterSlots', {"
" slotCount = 8"
"ecs.set_component(id, 'CharacterSpawner', {"
" registryId = 42,"
" spawnDistanceSq = 10000,"
" despawnDistanceSq = 40000"
"});"
"local c = ecs.get_component(id, 'CharacterSlots');"
"assert(c ~= nil, 'CharacterSlots should exist');"
"assert(c.slotCount == 8, 'wrong slotCount')");
"local c = ecs.get_component(id, 'CharacterSpawner');"
"assert(c ~= nil, 'CharacterSpawner should exist');"
"assert(c.registryId == 42, 'wrong registryId');"
"assert(c.spawnDistanceSq == 10000, 'wrong spawnDistanceSq');"
"assert(c.despawnDistanceSq == 40000, 'wrong despawnDistanceSq')");
if (!ok)
FAIL("CharacterSlots component assertion failed");
FAIL("CharacterSpawner component assertion failed");
PASS();
return 0;
@@ -1878,7 +1879,7 @@ int main()
failures += testProceduralMaterialComponent(L);
failures += testPrimitiveComponent(L);
failures += testTriangleBufferComponent(L);
failures += testCharacterSlotsComponent(L);
failures += testCharacterSpawnerComponent(L);
failures += testAnimationTreeComponent(L);
failures += testAnimationTreeTemplateComponent(L);
failures += testBehaviorTreeComponent(L);
@@ -0,0 +1,70 @@
#include "CharacterSpawnerEditor.hpp"
#include "../systems/CharacterRegistry.hpp"
#include <imgui.h>
CharacterSpawnerEditor::CharacterSpawnerEditor(Ogre::SceneManager *sceneMgr)
: m_sceneMgr(sceneMgr)
{
(void)m_sceneMgr;
}
bool CharacterSpawnerEditor::renderComponent(
flecs::entity entity, CharacterSpawnerComponent &spawner)
{
(void)entity;
bool modified = false;
if (ImGui::CollapsingHeader("Character Spawner",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
int registryId = static_cast<int>(spawner.registryId);
if (ImGui::InputInt("Registry ID", &registryId)) {
if (registryId >= 0) {
spawner.registryId =
static_cast<uint64_t>(registryId);
modified = true;
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Character registry ID of the character to spawn");
}
const CharacterRegistry::CharacterRecord *rec =
CharacterRegistry::getSingleton().findCharacter(
spawner.registryId);
if (rec) {
ImGui::TextDisabled("Character: %s %s",
rec->firstName.c_str(),
rec->lastName.c_str());
} else if (spawner.registryId != 0) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
"No character with this registry ID");
}
float spawnDist = std::sqrt(spawner.spawnDistanceSq);
if (ImGui::DragFloat("Spawn Distance", &spawnDist, 1.0f,
0.0f, 10000.0f)) {
spawner.spawnDistanceSq = spawnDist * spawnDist;
modified = true;
}
float despawnDist = std::sqrt(spawner.despawnDistanceSq);
if (ImGui::DragFloat("Despawn Distance", &despawnDist, 1.0f,
0.0f, 10000.0f)) {
spawner.despawnDistanceSq = despawnDist * despawnDist;
modified = true;
}
if (spawner.spawnDistanceSq > spawner.despawnDistanceSq) {
ImGui::TextColored(
ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
"Spawn distance is greater than despawn distance");
}
ImGui::Unindent();
}
return modified;
}
@@ -0,0 +1,30 @@
#ifndef EDITSCENE_CHARACTERSPAWNEREDITOR_HPP
#define EDITSCENE_CHARACTERSPAWNEREDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/CharacterSpawner.hpp"
#include <OgreSceneManager.h>
/**
* @brief Editor panel for CharacterSpawnerComponent.
*/
class CharacterSpawnerEditor
: public ComponentEditor<CharacterSpawnerComponent> {
public:
explicit CharacterSpawnerEditor(Ogre::SceneManager *sceneMgr);
const char *getName() const override
{
return "Character Spawner";
}
protected:
bool renderComponent(flecs::entity entity,
CharacterSpawnerComponent &spawner) override;
private:
Ogre::SceneManager *m_sceneMgr;
};
#endif // EDITSCENE_CHARACTERSPAWNEREDITOR_HPP