Actuators
This commit is contained in:
@@ -55,6 +55,9 @@ set(EDITSCENE_SOURCES
|
||||
ui/GoapRunnerEditor.cpp
|
||||
ui/PathFollowingEditor.cpp
|
||||
ui/GoapPlannerEditor.cpp
|
||||
systems/ActuatorSystem.cpp
|
||||
ui/ActuatorEditor.cpp
|
||||
components/ActuatorModule.cpp
|
||||
systems/PrefabSystem.cpp
|
||||
ui/PrefabInstanceEditor.cpp
|
||||
|
||||
@@ -192,6 +195,8 @@ set(EDITSCENE_HEADERS
|
||||
systems/GoapRunnerSystem.hpp
|
||||
systems/PathFollowingSystem.hpp
|
||||
systems/GoapPlannerSystem.hpp
|
||||
components/Actuator.hpp
|
||||
ui/ActuatorEditor.hpp
|
||||
components/PrefabInstance.hpp
|
||||
ui/PrefabInstanceEditor.hpp
|
||||
|
||||
|
||||
@@ -69,9 +69,11 @@
|
||||
#include "systems/PrefabSystem.hpp"
|
||||
#include "components/NavMesh.hpp"
|
||||
#include "components/SmartObject.hpp"
|
||||
#include "components/Actuator.hpp"
|
||||
#include "components/GoapPlanner.hpp"
|
||||
#include "components/GoapRunner.hpp"
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
@@ -342,6 +344,10 @@ void EditorApp::setup()
|
||||
// Wire up EditorApp for game mode detection
|
||||
m_smartObjectSystem->setEditorApp(this);
|
||||
|
||||
// Setup Actuator system
|
||||
m_actuatorSystem = std::make_unique<ActuatorSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup GOAP Runner system
|
||||
m_goapRunnerSystem = std::make_unique<GoapRunnerSystem>(
|
||||
m_world, m_sceneMgr, m_smartObjectSystem.get(),
|
||||
@@ -601,6 +607,9 @@ void EditorApp::setupECS()
|
||||
// Register Smart Object component
|
||||
m_world.component<SmartObjectComponent>();
|
||||
|
||||
// Register Actuator component
|
||||
m_world.component<ActuatorComponent>();
|
||||
|
||||
// Register GOAP Planner component
|
||||
m_world.component<GoapPlannerComponent>();
|
||||
|
||||
@@ -808,6 +817,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_goapRunnerSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Actuator system (player interaction prompts) --- */
|
||||
if (m_actuatorSystem) {
|
||||
m_actuatorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Dynamic physics (characters after static world) --- */
|
||||
|
||||
if (m_characterSystem) {
|
||||
|
||||
@@ -39,6 +39,7 @@ class SmartObjectSystem;
|
||||
class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -228,6 +229,7 @@ private:
|
||||
std::unique_ptr<GoapRunnerSystem> m_goapRunnerSystem;
|
||||
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
|
||||
// Game systems
|
||||
|
||||
|
||||
44
src/features/editScene/components/Actuator.hpp
Normal file
44
src/features/editScene/components/Actuator.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_HPP
|
||||
#define EDITSCENE_ACTUATOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Actuator component.
|
||||
*
|
||||
* An interactive object visible only to the player character.
|
||||
* When the player is within radius + height range, an on-screen
|
||||
* prompt appears and the action can be triggered with the action key.
|
||||
*
|
||||
* Unlike SmartObject, Actuators do not use pathfinding or path
|
||||
* following — they are instantaneous interactions.
|
||||
*/
|
||||
struct ActuatorComponent {
|
||||
// Interaction radius in XZ plane
|
||||
float radius = 1.5f;
|
||||
|
||||
// Maximum height difference for interaction
|
||||
float height = 1.8f;
|
||||
|
||||
// Names of GOAP actions (from ActionDatabase) that this actuator provides
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Runtime: cooldown timer (seconds remaining)
|
||||
float cooldownTimer = 0.0f;
|
||||
|
||||
// Runtime: currently executing an action
|
||||
bool isExecuting = false;
|
||||
|
||||
ActuatorComponent() = default;
|
||||
|
||||
explicit ActuatorComponent(float radius_, float height_)
|
||||
: radius(radius_)
|
||||
, height(height_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_HPP
|
||||
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Actuator.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActuatorEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
|
||||
{
|
||||
registry.registerComponent<ActuatorComponent>(
|
||||
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActuatorComponent>())
|
||||
e.set<ActuatorComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActuatorComponent>())
|
||||
e.remove<ActuatorComponent>();
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,11 @@ struct PlayerControllerComponent {
|
||||
Ogre::String swimIdleState = "swim-idle";
|
||||
Ogre::String swimState = "swim";
|
||||
Ogre::String swimFastState = "swim-fast";
|
||||
|
||||
/* Actuator interaction settings */
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PLAYERCONTROLLER_HPP
|
||||
|
||||
441
src/features/editScene/systems/ActuatorSystem.cpp
Normal file
441
src/features/editScene/systems/ActuatorSystem.cpp
Normal file
@@ -0,0 +1,441 @@
|
||||
#include "ActuatorSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
#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>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
|
||||
ActuatorSystem::ActuatorSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
, m_btSystem(btSystem)
|
||||
{
|
||||
}
|
||||
|
||||
ActuatorSystem::~ActuatorSystem() = default;
|
||||
|
||||
Ogre::Vector2 ActuatorSystem::projectToScreen(const Ogre::Vector3 &worldPoint)
|
||||
{
|
||||
if (!m_editorApp)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
EditorCamera *editorCam = m_editorApp->getEditorCamera();
|
||||
if (!editorCam)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
Ogre::Camera *camera = editorCam->getCamera();
|
||||
if (!camera)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
float width = vpSize.x;
|
||||
float height = vpSize.y;
|
||||
|
||||
// Convert to camera space
|
||||
Ogre::Vector3 eyeSpacePoint = camera->getViewMatrix() * worldPoint;
|
||||
|
||||
// Project to clip space
|
||||
Ogre::Vector3 clipSpacePoint =
|
||||
camera->getProjectionMatrix() * eyeSpacePoint;
|
||||
if (clipSpacePoint.z < 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// Convert from clip space (-1 to 1) to screen space (0 to 1)
|
||||
float screenX = (clipSpacePoint.x / 2.0f) + 0.5f;
|
||||
float screenY = 1.0f - ((clipSpacePoint.y / 2.0f) + 0.5f);
|
||||
|
||||
// Map to actual pixel dimensions
|
||||
return Ogre::Vector2(screenX * width, screenY * height);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius,
|
||||
float height)
|
||||
{
|
||||
Ogre::Vector3 diff = charPos - objPos;
|
||||
float xzDist = std::sqrt(diff.x * diff.x + diff.z * diff.z);
|
||||
if (xzDist > radius)
|
||||
return false;
|
||||
float yDiff = std::abs(diff.y);
|
||||
if (yDiff > height)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ActuatorSystem::executeAction(flecs::entity character,
|
||||
flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName)
|
||||
{
|
||||
if (!character.is_alive() || !actuatorEntity.is_alive())
|
||||
return;
|
||||
|
||||
if (!actuatorEntity.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
auto &actuator = actuatorEntity.get_mut<ActuatorComponent>();
|
||||
actuator.isExecuting = true;
|
||||
|
||||
m_executingActuatorId = actuatorEntity.id();
|
||||
m_executingCharacterId = character.id();
|
||||
|
||||
// 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;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[ActuatorSystem] Executing action: " + actionName);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character)
|
||||
{
|
||||
if (!m_btSystem || !character.is_alive())
|
||||
return true;
|
||||
|
||||
if (!character.has<ActionDebug>())
|
||||
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);
|
||||
});
|
||||
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));
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
{
|
||||
if (!actuatorEntity.is_alive() ||
|
||||
!actuatorEntity.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
auto &actuator = actuatorEntity.get<ActuatorComponent>();
|
||||
|
||||
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing,
|
||||
ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Appearing);
|
||||
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
if (ImGui::Begin("Select Action", nullptr, flags)) {
|
||||
ImGui::Text("Select an action:");
|
||||
ImGui::Separator();
|
||||
|
||||
for (const auto &name : actuator.actionNames) {
|
||||
if (name.empty())
|
||||
continue;
|
||||
if (ImGui::Button(name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ActuatorSystem::update(float deltaTime)
|
||||
{
|
||||
// Update cooldown timers for all actuators
|
||||
m_world.query<ActuatorComponent>().each(
|
||||
[&](flecs::entity, ActuatorComponent &actuator) {
|
||||
if (actuator.cooldownTimer > 0.0f) {
|
||||
actuator.cooldownTimer -= 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
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
// Check if an action is currently executing
|
||||
if (m_executingActuatorId != 0) {
|
||||
flecs::entity character = m_world.entity(m_executingCharacterId);
|
||||
if (isActionComplete(character)) {
|
||||
// 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>()
|
||||
.each([&](flecs::entity,
|
||||
PlayerControllerComponent &pc) {
|
||||
cooldown = pc.actuatorCooldown;
|
||||
});
|
||||
ac.cooldownTimer = cooldown;
|
||||
}
|
||||
m_executingActuatorId = 0;
|
||||
m_executingCharacterId = 0;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Find player character
|
||||
flecs::entity playerCharacter = flecs::entity::null();
|
||||
float actuatorDistance = 25.0f;
|
||||
float actuatorCooldown = 1.5f;
|
||||
|
||||
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) {
|
||||
if (!playerCharacter.is_alive() &&
|
||||
en.name == pc.targetCharacterName)
|
||||
playerCharacter = ec;
|
||||
});
|
||||
actuatorDistance = pc.actuatorDistance;
|
||||
actuatorCooldown = pc.actuatorCooldown;
|
||||
}
|
||||
});
|
||||
|
||||
if (!playerCharacter.is_alive() ||
|
||||
!playerCharacter.has<TransformComponent>())
|
||||
return;
|
||||
|
||||
Ogre::Vector3 charPos =
|
||||
playerCharacter.get<TransformComponent>().node
|
||||
->_getDerivedPosition();
|
||||
|
||||
// Collect actuators within distance
|
||||
std::vector<ScreenActuator> visibleActuators;
|
||||
std::vector<ScreenActuator> inRangeActuators;
|
||||
|
||||
m_world.query<ActuatorComponent, TransformComponent>()
|
||||
.each([&](flecs::entity e, ActuatorComponent &actuator,
|
||||
TransformComponent &trans) {
|
||||
// Skip if on cooldown or executing
|
||||
if (actuator.cooldownTimer > 0.0f || actuator.isExecuting)
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 objPos = trans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
|
||||
Ogre::Vector2 screenPos = projectToScreen(objPos);
|
||||
if (screenPos.x < 0)
|
||||
return;
|
||||
|
||||
ScreenActuator sa;
|
||||
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);
|
||||
|
||||
if (isInRange(charPos, objPos, actuator.radius,
|
||||
actuator.height)) {
|
||||
inRangeActuators.push_back(sa);
|
||||
}
|
||||
});
|
||||
|
||||
// Determine target actuator for interaction
|
||||
const ScreenActuator *target = nullptr;
|
||||
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 <
|
||||
inRangeActuators[bestIdx].distToScreenCenter) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// Draw markers
|
||||
drawActuatorMarkers(visibleActuators, target, labelText);
|
||||
|
||||
// Handle input
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
|
||||
// Handle menu state first
|
||||
if (m_menuOpen) {
|
||||
flecs::entity menuActuator = m_world.entity(m_menuActuatorId);
|
||||
drawActionMenu(menuActuator);
|
||||
|
||||
if (!m_pendingActionName.empty()) {
|
||||
// Action selected from menu
|
||||
executeAction(playerCharacter, menuActuator,
|
||||
m_pendingActionName);
|
||||
m_pendingActionName.clear();
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
|
||||
// Also close menu if E is released without selection
|
||||
if (!input.e && m_eWasHeld) {
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle E key
|
||||
if (target && input.e) {
|
||||
auto &actuator = target->entity.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,
|
||||
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_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_eHoldTime = 0.0f;
|
||||
m_eWasHeld = false;
|
||||
}
|
||||
}
|
||||
66
src/features/editScene/systems/ActuatorSystem.hpp
Normal file
66
src/features/editScene/systems/ActuatorSystem.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
#define EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
class EditorApp;
|
||||
class BehaviorTreeSystem;
|
||||
|
||||
/**
|
||||
* System that handles player interaction with Actuator entities.
|
||||
*
|
||||
* In game mode:
|
||||
* - Draws on-screen circle markers for actuators within range
|
||||
* - Shows "E - ActionName" (or just "E" for multi-action) when in reach
|
||||
* - Handles E press / hold for action activation
|
||||
* - Manages per-actuator cooldowns
|
||||
*/
|
||||
class ActuatorSystem {
|
||||
public:
|
||||
ActuatorSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem);
|
||||
~ActuatorSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
struct ScreenActuator {
|
||||
flecs::entity entity;
|
||||
Ogre::Vector2 screenPos;
|
||||
float distance;
|
||||
float distToScreenCenter;
|
||||
};
|
||||
|
||||
Ogre::Vector2 projectToScreen(const Ogre::Vector3 &worldPoint);
|
||||
bool isInRange(const Ogre::Vector3 &charPos,
|
||||
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);
|
||||
void drawActionMenu(flecs::entity actuatorEntity);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
|
||||
// Multi-action menu state
|
||||
bool m_menuOpen = false;
|
||||
flecs::entity_t m_menuActuatorId = 0;
|
||||
float m_eHoldTime = 0.0f;
|
||||
bool m_eWasHeld = false;
|
||||
Ogre::String m_pendingActionName;
|
||||
|
||||
// Currently executing action state
|
||||
flecs::entity_t m_executingActuatorId = 0;
|
||||
flecs::entity_t m_executingCharacterId = 0;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_SYSTEM_HPP
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
@@ -303,6 +304,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<SmartObjectComponent>()) {
|
||||
json["smartObject"] = serializeSmartObject(entity);
|
||||
}
|
||||
if (entity.has<ActuatorComponent>()) {
|
||||
json["actuator"] = serializeActuator(entity);
|
||||
}
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
json["goapPlanner"] = serializeGoapPlanner(entity);
|
||||
}
|
||||
@@ -509,6 +513,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("actuator")) {
|
||||
deserializeActuator(entity, json["actuator"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
@@ -791,6 +798,9 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("actuator")) {
|
||||
deserializeActuator(entity, json["actuator"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
@@ -3641,3 +3651,30 @@ void SceneSerializer::deserializeNavMeshGeometrySource(
|
||||
src.include = json.value("include", true);
|
||||
entity.set<NavMeshGeometrySource>(src);
|
||||
}
|
||||
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActuator(flecs::entity entity)
|
||||
{
|
||||
const ActuatorComponent &actuator = entity.get<ActuatorComponent>();
|
||||
nlohmann::json json;
|
||||
json["radius"] = actuator.radius;
|
||||
json["height"] = actuator.height;
|
||||
json["actionNames"] = actuator.actionNames;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeActuator(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ActuatorComponent actuator;
|
||||
actuator.radius = json.value("radius", 1.5f);
|
||||
actuator.height = json.value("height", 1.8f);
|
||||
if (json.contains("actionNames") && json["actionNames"].is_array()) {
|
||||
actuator.actionNames.clear();
|
||||
for (const auto &name : json["actionNames"]) {
|
||||
if (name.is_string())
|
||||
actuator.actionNames.push_back(name);
|
||||
}
|
||||
}
|
||||
entity.set<ActuatorComponent>(actuator);
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ private:
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
nlohmann::json serializePathFollowing(flecs::entity entity);
|
||||
nlohmann::json serializeSmartObject(flecs::entity entity);
|
||||
nlohmann::json serializeActuator(flecs::entity entity);
|
||||
nlohmann::json serializeGoapPlanner(flecs::entity entity);
|
||||
nlohmann::json serializeBehaviorTree(flecs::entity entity);
|
||||
void deserializeActionDatabase(flecs::entity entity,
|
||||
@@ -220,6 +221,8 @@ private:
|
||||
const nlohmann::json &json);
|
||||
void deserializeSmartObject(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeActuator(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeGoapPlanner(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeBehaviorTree(flecs::entity entity,
|
||||
|
||||
45
src/features/editScene/ui/ActuatorEditor.cpp
Normal file
45
src/features/editScene/ui/ActuatorEditor.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "ActuatorEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool ActuatorEditor::renderComponent(flecs::entity entity,
|
||||
ActuatorComponent &actuator)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
|
||||
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::Separator();
|
||||
ImGui::Text("Actions:");
|
||||
|
||||
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;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X"))
|
||||
removeIdx = i;
|
||||
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;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
20
src/features/editScene/ui/ActuatorEditor.hpp
Normal file
20
src/features/editScene/ui/ActuatorEditor.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_EDITOR_HPP
|
||||
#define EDITSCENE_ACTUATOR_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
|
||||
class ActuatorEditor : public ComponentEditor<ActuatorComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Actuator";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
ActuatorComponent &actuator) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_EDITOR_HPP
|
||||
@@ -107,6 +107,23 @@ bool PlayerControllerEditor::renderComponent(flecs::entity entity,
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Actuator Interaction");
|
||||
|
||||
if (ImGui::DragFloat("Actuator Distance", &pc.actuatorDistance,
|
||||
0.5f, 1.0f, 200.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Actuator Cooldown", &pc.actuatorCooldown,
|
||||
0.1f, 0.0f, 10.0f))
|
||||
modified = true;
|
||||
float color[3] = { pc.actuatorColor.x, pc.actuatorColor.y,
|
||||
pc.actuatorColor.z };
|
||||
if (ImGui::ColorEdit3("Actuator Color", color)) {
|
||||
pc.actuatorColor = Ogre::Vector3(color[0], color[1],
|
||||
color[2]);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user