Fixed stuck animations.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", ®istryId)) {
|
||||
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
|
||||
Reference in New Issue
Block a user