Labels and actuators work perfectly!
This commit is contained in:
@@ -112,6 +112,14 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
m_uiSystem->update(m_deltaTime);
|
||||
}
|
||||
|
||||
// Render actuator markers in game mode (after NewFrame so draw
|
||||
// commands survive)
|
||||
if (m_editorApp) {
|
||||
ActuatorSystem *actuatorSys = m_editorApp->getActuatorSystem();
|
||||
if (actuatorSys)
|
||||
actuatorSys->render();
|
||||
}
|
||||
|
||||
// Render startup menu in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
|
||||
@@ -186,6 +186,10 @@ public:
|
||||
{
|
||||
return m_startupMenuSystem.get();
|
||||
}
|
||||
ActuatorSystem *getActuatorSystem() const
|
||||
{
|
||||
return m_actuatorSystem.get();
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
|
||||
@@ -31,6 +31,12 @@ struct PlayerControllerComponent {
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f);
|
||||
float distantCircleRadius = 8.0f;
|
||||
float nearCircleRadius = 14.0f;
|
||||
float actuatorLabelFontSize = 14.0f;
|
||||
|
||||
/* Runtime: set by ActuatorSystem while executing an action */
|
||||
bool inputLocked = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PLAYERCONTROLLER_HPP
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../camera/EditorCamera.hpp"
|
||||
#include <OgreCamera.h>
|
||||
@@ -16,9 +15,9 @@
|
||||
#include <cmath>
|
||||
|
||||
ActuatorSystem::ActuatorSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
@@ -75,6 +74,14 @@ bool ActuatorSystem::isInRange(const Ogre::Vector3 &charPos,
|
||||
return true;
|
||||
}
|
||||
|
||||
void ActuatorSystem::setPlayerInputLocked(bool locked)
|
||||
{
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity, PlayerControllerComponent &pc) {
|
||||
pc.inputLocked = locked;
|
||||
});
|
||||
}
|
||||
|
||||
void ActuatorSystem::executeAction(flecs::entity character,
|
||||
flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName)
|
||||
@@ -90,87 +97,46 @@ void ActuatorSystem::executeAction(flecs::entity character,
|
||||
|
||||
m_executingActuatorId = actuatorEntity.id();
|
||||
m_executingCharacterId = character.id();
|
||||
m_executingActionName = actionName;
|
||||
m_actionFirstFrame = true;
|
||||
|
||||
// Set up ActionDebug on the character to run the behavior tree
|
||||
if (!character.has<ActionDebug>()) {
|
||||
character.set<ActionDebug>({});
|
||||
}
|
||||
auto &debug = character.get_mut<ActionDebug>();
|
||||
debug.isRunning = true;
|
||||
debug.runTimer = 0.0f;
|
||||
debug.currentActionName = actionName;
|
||||
debug.selectedActionName = actionName;
|
||||
// Lock player input while action executes
|
||||
setPlayerInputLocked(true);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[ActuatorSystem] Executing action: " + actionName);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character)
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character,
|
||||
float deltaTime)
|
||||
{
|
||||
if (!m_btSystem || !character.is_alive())
|
||||
return true;
|
||||
|
||||
if (!character.has<ActionDebug>())
|
||||
if (m_executingActionName.empty())
|
||||
return true;
|
||||
|
||||
auto &debug = character.get<ActionDebug>();
|
||||
if (!debug.isRunning)
|
||||
return true;
|
||||
|
||||
auto &btState = m_btSystem->getActionDebugState(character.id());
|
||||
return btState.treeResult != BehaviorTreeSystem::Status::running;
|
||||
}
|
||||
|
||||
void ActuatorSystem::drawActuatorMarkers(
|
||||
const std::vector<ScreenActuator> &markers,
|
||||
const ScreenActuator *target, const Ogre::String &labelText)
|
||||
{
|
||||
ImDrawList *drawList = ImGui::GetBackgroundDrawList();
|
||||
if (!drawList)
|
||||
return;
|
||||
|
||||
// Default circle color from player controller
|
||||
ImVec4 defaultColorVec(0.0f, 0.4f, 1.0f, 1.0f);
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity, PlayerControllerComponent &pc) {
|
||||
defaultColorVec = ImVec4(pc.actuatorColor.x,
|
||||
pc.actuatorColor.y,
|
||||
pc.actuatorColor.z, 1.0f);
|
||||
// Look up the action in the database to get its behavior tree
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
ImColor defaultColor(defaultColorVec);
|
||||
// Target is brighter
|
||||
ImColor targetColor(
|
||||
ImVec4(std::min(defaultColorVec.x + 0.2f, 1.0f),
|
||||
std::min(defaultColorVec.y + 0.3f, 1.0f),
|
||||
std::min(defaultColorVec.z + 0.0f, 1.0f), 1.0f));
|
||||
if (!db)
|
||||
return true;
|
||||
|
||||
for (const auto &marker : markers) {
|
||||
bool isTarget = target && target->entity.id() == marker.entity.id();
|
||||
float circleRadius = isTarget ? 12.0f : 8.0f;
|
||||
ImColor circleCol = isTarget ? targetColor : defaultColor;
|
||||
const GoapAction *action = db->findAction(m_executingActionName);
|
||||
if (!action)
|
||||
return true;
|
||||
|
||||
ImVec2 center(marker.screenPos.x, marker.screenPos.y);
|
||||
drawList->AddCircleFilled(center, circleRadius, circleCol);
|
||||
drawList->AddCircle(center, circleRadius,
|
||||
IM_COL32(255, 255, 255, 180), 0, 2.0f);
|
||||
}
|
||||
// Evaluate the behavior tree directly (no ActionDebug)
|
||||
auto status = m_btSystem->evaluatePlayerAction(
|
||||
character.id(), action->behaviorTree, deltaTime,
|
||||
m_actionFirstFrame);
|
||||
m_actionFirstFrame = false;
|
||||
|
||||
// Draw label for target
|
||||
if (target && !labelText.empty()) {
|
||||
ImVec2 center(target->screenPos.x, target->screenPos.y);
|
||||
float circleRadius = 12.0f;
|
||||
ImVec2 textSize = ImGui::CalcTextSize(labelText.c_str());
|
||||
ImVec2 textPos(center.x - (textSize.x * 0.5f),
|
||||
center.y + circleRadius + 6.0f);
|
||||
|
||||
// Shadow
|
||||
drawList->AddText(
|
||||
ImVec2(textPos.x + 1, textPos.y + 1),
|
||||
IM_COL32(0, 0, 0, 200), labelText.c_str());
|
||||
// Text
|
||||
drawList->AddText(textPos, IM_COL32(255, 255, 255, 255),
|
||||
labelText.c_str());
|
||||
}
|
||||
return status != BehaviorTreeSystem::Status::running;
|
||||
}
|
||||
|
||||
void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
@@ -209,6 +175,7 @@ void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
@@ -226,9 +193,6 @@ void ActuatorSystem::update(float deltaTime)
|
||||
if (actuator.cooldownTimer < 0.0f)
|
||||
actuator.cooldownTimer = 0.0f;
|
||||
}
|
||||
if (actuator.cooldownTimer <= 0.0f && !actuator.isExecuting) {
|
||||
// Fully reset
|
||||
}
|
||||
});
|
||||
|
||||
// Only run in game mode while playing
|
||||
@@ -241,13 +205,12 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Check if an action is currently executing
|
||||
if (m_executingActuatorId != 0) {
|
||||
flecs::entity character = m_world.entity(m_executingCharacterId);
|
||||
if (isActionComplete(character)) {
|
||||
if (isActionComplete(character, deltaTime)) {
|
||||
// Action finished - start cooldown
|
||||
flecs::entity actuator = m_world.entity(m_executingActuatorId);
|
||||
if (actuator.is_alive() && actuator.has<ActuatorComponent>()) {
|
||||
auto &ac = actuator.get_mut<ActuatorComponent>();
|
||||
ac.isExecuting = false;
|
||||
// Get cooldown from player controller config
|
||||
float cooldown = 1.5f;
|
||||
m_world
|
||||
.query<PlayerControllerComponent>()
|
||||
@@ -259,29 +222,31 @@ void ActuatorSystem::update(float deltaTime)
|
||||
}
|
||||
m_executingActuatorId = 0;
|
||||
m_executingCharacterId = 0;
|
||||
m_executingActionName.clear();
|
||||
|
||||
// Clear ActionDebug on character
|
||||
if (character.is_alive() && character.has<ActionDebug>()) {
|
||||
auto &debug = character.get_mut<ActionDebug>();
|
||||
debug.isRunning = false;
|
||||
debug.currentActionName.clear();
|
||||
}
|
||||
} else {
|
||||
// Action still running - don't draw any markers
|
||||
return;
|
||||
// Unlock player input
|
||||
setPlayerInputLocked(false);
|
||||
}
|
||||
// Don't collect or draw anything while executing
|
||||
m_visibleActuators.clear();
|
||||
m_targetIndex = -1;
|
||||
m_labelText.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find player character
|
||||
flecs::entity playerCharacter = flecs::entity::null();
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
m_circleColor = ImVec4(0.0f, 0.4f, 1.0f, 1.0f);
|
||||
m_distantRadius = 8.0f;
|
||||
m_nearRadius = 14.0f;
|
||||
m_labelFontSize = 14.0f;
|
||||
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity e, PlayerControllerComponent &pc) {
|
||||
(void)e;
|
||||
if (!playerCharacter.is_alive()) {
|
||||
// Find character by name
|
||||
m_world.query<EntityNameComponent>()
|
||||
.each([&](flecs::entity ec,
|
||||
EntityNameComponent &en) {
|
||||
@@ -291,6 +256,13 @@ void ActuatorSystem::update(float deltaTime)
|
||||
});
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -303,7 +275,7 @@ void ActuatorSystem::update(float deltaTime)
|
||||
->_getDerivedPosition();
|
||||
|
||||
// Collect actuators within distance
|
||||
std::vector<ScreenActuator> visibleActuators;
|
||||
m_visibleActuators.clear();
|
||||
std::vector<ScreenActuator> inRangeActuators;
|
||||
|
||||
m_world.query<ActuatorComponent, TransformComponent>()
|
||||
@@ -329,12 +301,11 @@ void ActuatorSystem::update(float deltaTime)
|
||||
sa.entity = e;
|
||||
sa.screenPos = screenPos;
|
||||
sa.distance = dist;
|
||||
// Distance to screen center (X only for horizontal preference)
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
sa.distToScreenCenter =
|
||||
std::abs(screenPos.x - vpSize.x * 0.5f);
|
||||
|
||||
visibleActuators.push_back(sa);
|
||||
m_visibleActuators.push_back(sa);
|
||||
|
||||
if (isInRange(charPos, objPos, actuator.radius,
|
||||
actuator.height)) {
|
||||
@@ -343,9 +314,8 @@ void ActuatorSystem::update(float deltaTime)
|
||||
});
|
||||
|
||||
// Determine target actuator for interaction
|
||||
const ScreenActuator *target = nullptr;
|
||||
m_targetIndex = -1;
|
||||
if (!inRangeActuators.empty()) {
|
||||
// Pick closest to screen X-center, then bottommost (largest Y)
|
||||
size_t bestIdx = 0;
|
||||
for (size_t i = 1; i < inRangeActuators.size(); i++) {
|
||||
if (inRangeActuators[i].distToScreenCenter <
|
||||
@@ -353,30 +323,35 @@ void ActuatorSystem::update(float deltaTime)
|
||||
bestIdx = i;
|
||||
} else if (inRangeActuators[i].distToScreenCenter ==
|
||||
inRangeActuators[bestIdx].distToScreenCenter) {
|
||||
// Bottommost = larger Y
|
||||
if (inRangeActuators[i].screenPos.y >
|
||||
inRangeActuators[bestIdx].screenPos.y)
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
target = &inRangeActuators[bestIdx];
|
||||
}
|
||||
|
||||
// Build label text
|
||||
Ogre::String labelText;
|
||||
if (target && target->entity.is_alive() &&
|
||||
target->entity.has<ActuatorComponent>()) {
|
||||
auto &actuator = target->entity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
labelText = "E - " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
labelText = "E";
|
||||
// Find the index in m_visibleActuators
|
||||
for (size_t i = 0; i < m_visibleActuators.size(); i++) {
|
||||
if (m_visibleActuators[i].entity.id() ==
|
||||
inRangeActuators[bestIdx].entity.id()) {
|
||||
m_targetIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw markers
|
||||
drawActuatorMarkers(visibleActuators, target, labelText);
|
||||
// Build label text
|
||||
m_labelText.clear();
|
||||
if (m_targetIndex >= 0) {
|
||||
flecs::entity targetEntity = m_visibleActuators[m_targetIndex].entity;
|
||||
if (targetEntity.is_alive() && targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
m_labelText = "E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle input
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
@@ -384,10 +359,9 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Handle menu state first
|
||||
if (m_menuOpen) {
|
||||
flecs::entity menuActuator = m_world.entity(m_menuActuatorId);
|
||||
drawActionMenu(menuActuator);
|
||||
// Menu rendering happens in render()
|
||||
|
||||
if (!m_pendingActionName.empty()) {
|
||||
// Action selected from menu
|
||||
executeAction(playerCharacter, menuActuator,
|
||||
m_pendingActionName);
|
||||
m_pendingActionName.clear();
|
||||
@@ -399,7 +373,6 @@ void ActuatorSystem::update(float deltaTime)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
|
||||
// Also close menu if E is released without selection
|
||||
if (!input.e && m_eWasHeld) {
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
@@ -412,23 +385,23 @@ void ActuatorSystem::update(float deltaTime)
|
||||
}
|
||||
|
||||
// Handle E key
|
||||
if (target && input.e) {
|
||||
auto &actuator = target->entity.get<ActuatorComponent>();
|
||||
if (m_targetIndex >= 0 && input.e) {
|
||||
flecs::entity targetEntity =
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
m_eHoldTime += deltaTime;
|
||||
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
// Single action: execute on press
|
||||
if (input.ePressed) {
|
||||
executeAction(playerCharacter, target->entity,
|
||||
executeAction(playerCharacter, targetEntity,
|
||||
actuator.actionNames[0]);
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
// Multiple actions: hold to open menu
|
||||
if (m_eHoldTime > 0.3f && !m_menuOpen) {
|
||||
m_menuOpen = true;
|
||||
m_menuActuatorId = target->entity.id();
|
||||
m_menuActuatorId = targetEntity.id();
|
||||
m_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(false);
|
||||
@@ -439,3 +412,66 @@ void ActuatorSystem::update(float deltaTime)
|
||||
m_eWasHeld = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ActuatorSystem::render()
|
||||
{
|
||||
// Only run in game mode while playing
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
ImDrawList *drawList = ImGui::GetBackgroundDrawList();
|
||||
if (!drawList)
|
||||
return;
|
||||
|
||||
ImColor defaultColor(m_circleColor);
|
||||
ImColor targetColor(
|
||||
ImVec4(std::min(m_circleColor.x + 0.2f, 1.0f),
|
||||
std::min(m_circleColor.y + 0.3f, 1.0f),
|
||||
std::min(m_circleColor.z + 0.0f, 1.0f),
|
||||
1.0f));
|
||||
|
||||
for (size_t i = 0; i < m_visibleActuators.size(); i++) {
|
||||
bool isTarget = (static_cast<int>(i) == m_targetIndex);
|
||||
float circleRadius = isTarget ? m_nearRadius : m_distantRadius;
|
||||
ImColor circleCol = isTarget ? targetColor : defaultColor;
|
||||
|
||||
ImVec2 center(m_visibleActuators[i].screenPos.x,
|
||||
m_visibleActuators[i].screenPos.y);
|
||||
drawList->AddCircleFilled(center, circleRadius, circleCol);
|
||||
drawList->AddCircle(center, circleRadius,
|
||||
IM_COL32(255, 255, 255, 180), 0, 2.0f);
|
||||
}
|
||||
|
||||
// Draw label for target
|
||||
if (m_targetIndex >= 0 && !m_labelText.empty()) {
|
||||
float circleRadius = m_nearRadius;
|
||||
ImVec2 center(m_visibleActuators[m_targetIndex].screenPos.x,
|
||||
m_visibleActuators[m_targetIndex].screenPos.y);
|
||||
|
||||
ImFont *font = ImGui::GetFont();
|
||||
ImVec2 textSize = font->CalcTextSizeA(
|
||||
m_labelFontSize, FLT_MAX, -1.0f,
|
||||
m_labelText.c_str());
|
||||
|
||||
ImVec2 textPos(center.x - (textSize.x * 0.5f),
|
||||
center.y + circleRadius + 6.0f);
|
||||
|
||||
// Shadow
|
||||
drawList->AddText(font, m_labelFontSize,
|
||||
ImVec2(textPos.x + 1, textPos.y + 1),
|
||||
IM_COL32(0, 0, 0, 200), m_labelText.c_str());
|
||||
// Text
|
||||
drawList->AddText(font, m_labelFontSize, textPos,
|
||||
IM_COL32(255, 255, 255, 255),
|
||||
m_labelText.c_str());
|
||||
}
|
||||
|
||||
// Draw action menu if open
|
||||
if (m_menuOpen) {
|
||||
flecs::entity menuActuator = m_world.entity(m_menuActuatorId);
|
||||
drawActionMenu(menuActuator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
@@ -14,10 +15,13 @@ class BehaviorTreeSystem;
|
||||
* System that handles player interaction with Actuator entities.
|
||||
*
|
||||
* In game mode:
|
||||
* - Draws on-screen circle markers for actuators within range
|
||||
* - update() finds nearby actuators and handles input
|
||||
* - render() draws on-screen circle markers (called in preViewportUpdate after NewFrame)
|
||||
* - Shows "E - ActionName" (or just "E" for multi-action) when in reach
|
||||
* - Handles E press / hold for action activation
|
||||
* - Manages per-actuator cooldowns
|
||||
* - Executes actions via BehaviorTreeSystem directly (no ActionDebug)
|
||||
* - Disables player controls during action execution
|
||||
*/
|
||||
class ActuatorSystem {
|
||||
public:
|
||||
@@ -26,6 +30,7 @@ public:
|
||||
~ActuatorSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
void render();
|
||||
|
||||
private:
|
||||
struct ScreenActuator {
|
||||
@@ -40,17 +45,24 @@ private:
|
||||
const Ogre::Vector3 &objPos, float radius, float height);
|
||||
void executeAction(flecs::entity character, flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName);
|
||||
bool isActionComplete(flecs::entity character);
|
||||
void drawActuatorMarkers(const std::vector<ScreenActuator> &markers,
|
||||
const ScreenActuator *target,
|
||||
const Ogre::String &labelText);
|
||||
bool isActionComplete(flecs::entity character, float deltaTime);
|
||||
void drawActionMenu(flecs::entity actuatorEntity);
|
||||
void setPlayerInputLocked(bool locked);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
|
||||
// Cached data between update() and render()
|
||||
std::vector<ScreenActuator> m_visibleActuators;
|
||||
int m_targetIndex = -1;
|
||||
Ogre::String m_labelText;
|
||||
ImVec4 m_circleColor;
|
||||
float m_distantRadius = 8.0f;
|
||||
float m_nearRadius = 14.0f;
|
||||
float m_labelFontSize = 14.0f;
|
||||
|
||||
// Multi-action menu state
|
||||
bool m_menuOpen = false;
|
||||
flecs::entity_t m_menuActuatorId = 0;
|
||||
@@ -61,6 +73,8 @@ private:
|
||||
// Currently executing action state
|
||||
flecs::entity_t m_executingActuatorId = 0;
|
||||
flecs::entity_t m_executingCharacterId = 0;
|
||||
Ogre::String m_executingActionName;
|
||||
bool m_actionFirstFrame = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/Relationship.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
@@ -675,3 +674,24 @@ void BehaviorTreeSystem::update(float deltaTime)
|
||||
debug.runTimer += deltaTime;
|
||||
});
|
||||
}
|
||||
|
||||
BehaviorTreeSystem::Status
|
||||
BehaviorTreeSystem::evaluatePlayerAction(flecs::entity_t id,
|
||||
const BehaviorTreeNode &root,
|
||||
float deltaTime, bool reset)
|
||||
{
|
||||
auto &state = m_playerActionStates[id];
|
||||
if (reset) {
|
||||
state.lastActiveLeaves.clear();
|
||||
state.firstRun = true;
|
||||
state.nodeTimers.clear();
|
||||
state.treeResult = Status::running;
|
||||
}
|
||||
state.currentActiveLeaves.clear();
|
||||
Status result = evaluateNode(root, m_world.entity(id), state,
|
||||
deltaTime);
|
||||
state.lastActiveLeaves = state.currentActiveLeaves;
|
||||
state.firstRun = false;
|
||||
state.treeResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,14 @@ public:
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
/** Evaluate a behavior tree directly for an entity.
|
||||
* Used by ActuatorSystem for player action execution.
|
||||
* Pass reset=true on the first call for a new action.
|
||||
* Returns running/success/failure. */
|
||||
Status evaluatePlayerAction(flecs::entity_t id,
|
||||
const BehaviorTreeNode &root, float deltaTime,
|
||||
bool reset);
|
||||
|
||||
private:
|
||||
Status evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
RunnerState &state, float deltaTime);
|
||||
@@ -84,6 +92,7 @@ private:
|
||||
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_runnerStates;
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_actionDebugStates;
|
||||
std::unordered_map<flecs::entity_t, RunnerState> m_playerActionStates;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_BEHAVIOR_TREE_SYSTEM_HPP
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/GoapRunner.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
@@ -1063,6 +1064,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Actuator if present
|
||||
if (entity.has<ActuatorComponent>()) {
|
||||
auto &actuator = entity.get_mut<ActuatorComponent>();
|
||||
m_componentRegistry.render<ActuatorComponent>(entity, actuator);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
|
||||
@@ -164,9 +164,9 @@ void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc,
|
||||
state.faceHidden = false;
|
||||
}
|
||||
|
||||
// Read mouse input
|
||||
// Read mouse input (skip if input locked by action)
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
if (input.mouseMoved) {
|
||||
if (!pc.inputLocked && input.mouseMoved) {
|
||||
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
|
||||
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
|
||||
// Clamp pitch
|
||||
@@ -277,9 +277,9 @@ void PlayerControllerSystem::updateFPSCamera(PlayerControllerComponent &pc,
|
||||
|
||||
camNode->setPosition(boneWorldPos + offset);
|
||||
|
||||
// Apply mouse look
|
||||
// Apply mouse look (skip if input locked by action)
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
if (input.mouseMoved) {
|
||||
if (!pc.inputLocked && input.mouseMoved) {
|
||||
state.yaw -= input.mouseDeltaX * pc.mouseSensitivity;
|
||||
state.pitch -= input.mouseDeltaY * pc.mouseSensitivity;
|
||||
if (state.pitch > 89.0f)
|
||||
@@ -302,6 +302,13 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
|
||||
if (!state.targetEntity.has<CharacterComponent>())
|
||||
return;
|
||||
|
||||
// Skip locomotion if input is locked by an executing action
|
||||
if (pc.inputLocked) {
|
||||
auto &cc = state.targetEntity.get_mut<CharacterComponent>();
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
auto &cc = state.targetEntity.get_mut<CharacterComponent>();
|
||||
|
||||
|
||||
@@ -2867,6 +2867,13 @@ nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity)
|
||||
json["swimIdleState"] = pc.swimIdleState;
|
||||
json["swimState"] = pc.swimState;
|
||||
json["swimFastState"] = pc.swimFastState;
|
||||
json["actuatorDistance"] = pc.actuatorDistance;
|
||||
json["actuatorCooldown"] = pc.actuatorCooldown;
|
||||
json["actuatorColor"] = { pc.actuatorColor.x, pc.actuatorColor.y,
|
||||
pc.actuatorColor.z };
|
||||
json["distantCircleRadius"] = pc.distantCircleRadius;
|
||||
json["nearCircleRadius"] = pc.nearCircleRadius;
|
||||
json["actuatorLabelFontSize"] = pc.actuatorLabelFontSize;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -2903,6 +2910,21 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity,
|
||||
pc.swimIdleState = json.value("swimIdleState", pc.swimIdleState);
|
||||
pc.swimState = json.value("swimState", pc.swimState);
|
||||
pc.swimFastState = json.value("swimFastState", pc.swimFastState);
|
||||
pc.actuatorDistance = json.value("actuatorDistance", pc.actuatorDistance);
|
||||
pc.actuatorCooldown = json.value("actuatorCooldown", pc.actuatorCooldown);
|
||||
if (json.contains("actuatorColor") && json["actuatorColor"].is_array() &&
|
||||
json["actuatorColor"].size() >= 3) {
|
||||
pc.actuatorColor = Ogre::Vector3(json["actuatorColor"][0],
|
||||
json["actuatorColor"][1],
|
||||
json["actuatorColor"][2]);
|
||||
}
|
||||
pc.distantCircleRadius =
|
||||
json.value("distantCircleRadius", pc.distantCircleRadius);
|
||||
pc.nearCircleRadius = json.value("nearCircleRadius", pc.nearCircleRadius);
|
||||
pc.actuatorLabelFontSize =
|
||||
json.value("actuatorLabelFontSize", pc.actuatorLabelFontSize);
|
||||
// inputLocked is runtime-only, always reset to false on load
|
||||
pc.inputLocked = false;
|
||||
entity.set<PlayerControllerComponent>(pc);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,113 @@
|
||||
#include "ActuatorEditor.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
ActionDatabase *ActuatorEditor::findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
bool ActuatorEditor::renderComponent(flecs::entity entity,
|
||||
ActuatorComponent &actuator)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
ImGui::PushID("Actuator");
|
||||
|
||||
modified |= ImGui::DragFloat("Radius", &actuator.radius, 0.1f,
|
||||
0.1f, 100.0f);
|
||||
modified |= ImGui::DragFloat("Height", &actuator.height, 0.1f,
|
||||
0.1f, 100.0f);
|
||||
ImGui::Text("Actuator Settings");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::DragFloat("Radius", &actuator.radius, 0.1f, 0.1f, 100.0f,
|
||||
"%.1f")) {
|
||||
if (actuator.radius < 0.1f)
|
||||
actuator.radius = 0.1f;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(XZ interaction distance)");
|
||||
|
||||
if (ImGui::DragFloat("Height", &actuator.height, 0.1f, 0.1f, 100.0f,
|
||||
"%.1f")) {
|
||||
if (actuator.height < 0.1f)
|
||||
actuator.height = 0.1f;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(Y interaction threshold)");
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Actions:");
|
||||
ImGui::Indent();
|
||||
|
||||
int removeIdx = -1;
|
||||
for (int i = 0; i < (int)actuator.actionNames.size(); i++) {
|
||||
ImGui::PushID(i);
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "%s",
|
||||
actuator.actionNames[i].c_str());
|
||||
if (ImGui::InputText("##name", buf, sizeof(buf))) {
|
||||
actuator.actionNames[i] = buf;
|
||||
modified = true;
|
||||
}
|
||||
// List currently selected actions
|
||||
for (size_t i = 0; i < actuator.actionNames.size(); i++) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::Text("%s", actuator.actionNames[i].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X"))
|
||||
removeIdx = i;
|
||||
if (ImGui::SmallButton("X")) {
|
||||
actuator.actionNames.erase(
|
||||
actuator.actionNames.begin() + i);
|
||||
modified = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeIdx >= 0) {
|
||||
actuator.actionNames.erase(
|
||||
actuator.actionNames.begin() + removeIdx);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Action")) {
|
||||
actuator.actionNames.push_back("");
|
||||
modified = true;
|
||||
|
||||
// Add action from database
|
||||
ActionDatabase *db = findDatabase(entity);
|
||||
if (db && !db->actions.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Add action:");
|
||||
static int selectedAction = -1;
|
||||
|
||||
std::vector<const char *> availableNames;
|
||||
std::vector<Ogre::String> availableNamesStorage;
|
||||
for (const auto &action : db->actions) {
|
||||
bool alreadySelected = false;
|
||||
for (const auto &selected : actuator.actionNames) {
|
||||
if (selected == action.name) {
|
||||
alreadySelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadySelected) {
|
||||
availableNamesStorage.push_back(action.name);
|
||||
availableNames.push_back(
|
||||
availableNamesStorage.back().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!availableNames.empty()) {
|
||||
if (selectedAction >= (int)availableNames.size())
|
||||
selectedAction = 0;
|
||||
if (ImGui::Combo("##actionSelect", &selectedAction,
|
||||
availableNames.data(),
|
||||
(int)availableNames.size())) {
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add")) {
|
||||
if (selectedAction >= 0 &&
|
||||
selectedAction < (int)availableNames.size()) {
|
||||
actuator.actionNames.push_back(
|
||||
availableNamesStorage[selectedAction]);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("All actions already selected");
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("No actions in database");
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
|
||||
class ActionDatabase;
|
||||
|
||||
class ActuatorEditor : public ComponentEditor<ActuatorComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
@@ -15,6 +17,9 @@ public:
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
ActuatorComponent &actuator) override;
|
||||
|
||||
private:
|
||||
ActionDatabase *findDatabase(flecs::entity entity);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_EDITOR_HPP
|
||||
|
||||
@@ -116,6 +116,18 @@ bool PlayerControllerEditor::renderComponent(flecs::entity entity,
|
||||
if (ImGui::DragFloat("Actuator Cooldown", &pc.actuatorCooldown,
|
||||
0.1f, 0.0f, 10.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Distant Circle Radius",
|
||||
&pc.distantCircleRadius, 0.5f, 1.0f,
|
||||
64.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Near Circle Radius",
|
||||
&pc.nearCircleRadius, 0.5f, 1.0f,
|
||||
64.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Label Font Size",
|
||||
&pc.actuatorLabelFontSize, 0.5f, 8.0f,
|
||||
32.0f))
|
||||
modified = true;
|
||||
float color[3] = { pc.actuatorColor.x, pc.actuatorColor.y,
|
||||
pc.actuatorColor.z };
|
||||
if (ImGui::ColorEdit3("Actuator Color", color)) {
|
||||
|
||||
Reference in New Issue
Block a user