Labels and actuators work perfectly!

This commit is contained in:
2026-04-27 09:03:47 +03:00
parent fa49bb5005
commit b9cce0248a
13 changed files with 369 additions and 150 deletions

View File

@@ -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 &&

View File

@@ -186,6 +186,10 @@ public:
{
return m_startupMenuSystem.get();
}
ActuatorSystem *getActuatorSystem() const
{
return m_actuatorSystem.get();
}
Ogre::ImGuiOverlay *getImGuiOverlay() const
{
return m_imguiOverlay;

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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>();

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)) {