Action nodes work better now

This commit is contained in:
2026-02-01 00:13:33 +03:00
parent 201aa92fe7
commit e6efd89bb0
14 changed files with 548 additions and 126 deletions

View File

@@ -115,7 +115,10 @@ CharacterAnimationModule::CharacterAnimationModule(flecs::world &ecs)
->state("sitting-ground")
->animation("sitting-ground")
->end()
->transition_end("swimming-edge-climb", "idle")
->state("sitting-chair")
->animation("sitting-chair")
->end()
->transition_end("swimming-edge-climb", "idle")
->transition_end("hanging-climb", "idle")
->transition_end("pass-character", "idle")
->end()
@@ -375,7 +378,8 @@ CharacterAnimationModule::CharacterAnimationModule(flecs::world &ecs)
.kind(flecs::OnUpdate)
.with<Character>()
.without<CharacterInActuator>()
.each([](flecs::entity e, const CharacterBase &ch,
.without<CharacterControlDisable>()
.each([](flecs::entity e, const CharacterBase &ch,
AnimationControl &anim) {
if (!anim.configured)
return;
@@ -395,6 +399,7 @@ CharacterAnimationModule::CharacterAnimationModule(flecs::world &ecs)
.with<Character>()
.with<Player>()
.without<CharacterInActuator>()
.without<CharacterControlDisable>()
.each([](flecs::entity e, const Input &input,
const CharacterBase &ch, AnimationControl &anim) {
if (!anim.configured)
@@ -474,6 +479,7 @@ CharacterAnimationModule::CharacterAnimationModule(flecs::world &ecs)
.kind(flecs::OnUpdate)
.with<Character>()
.with<Player>()
.without<CharacterControlDisable>()
.each([](flecs::entity e, const Input &input,
const CharacterBase &ch, AnimationControl &anim,
CharacterInActuator &act) {

View File

@@ -27,6 +27,7 @@ CharacterModule::CharacterModule(flecs::world &ecs)
ecs.component<CharacterInActuator>();
ecs.component<Male>();
ecs.component<Female>();
ecs.component<CharacterControlDisable>();
ecs.import <CharacterAnimationModule>();
ecs.import <TerrainModule>();
ecs.import <WaterModule>();
@@ -131,75 +132,14 @@ CharacterModule::CharacterModule(flecs::world &ecs)
else if (current_subm < 0.8f)
ch.is_submerged = false;
});
#if 0
ecs.system<const EngineData, const CharacterBase, CharacterVelocity>(
"HandleGravityBouyanceWater")
.kind(flecs::OnUpdate)
.with<TerrainReady>()
.with<WaterReady>()
.with<InWater>()
.without<CharacterDisablePhysics>()
.without<CharacterUpdatePhysicsState>()
.each([this](flecs::entity e, const EngineData &eng,
const CharacterBase &ch, CharacterVelocity &gr) {
Ogre::Vector3 gravity(0, -9.8f, 0);
Ogre::Vector3 pos = ch.mBodyNode->getPosition();
Ogre::Vector3 v(0, 0, 0);
if (e.has<CharacterGravity>())
v += gravity;
if (e.has<CharacterBuoyancy>()) {
float volume = 2.0f * 0.5f * 0.5f;
float density = 900.0f;
float full_subm = 2.0f;
float mass = 80.0f;
float multiplier = 0.25f;
float current_subm = -Ogre::Math::Clamp(
pos.y + Ogre::Math::Sin(ch.mTimer *
0.13f +
130.0f) *
0.07f,
-full_subm, 0.0f);
Ogre::Vector3 b = -gravity * density * volume *
multiplier * current_subm /
full_subm / mass;
v += b;
}
gr.gvelocity += v * eng.delta;
gr.gvelocity.y =
Ogre::Math::Clamp(gr.gvelocity.y, -2.5f, 1.5f);
gr.gvelocity *= (1.0 - eng.delta);
gr.velocity.y *= (1.0 - eng.delta);
});
#endif
#if 0
ecs.system<const EngineData, const CharacterBase, CharacterVelocity>(
"HandleGravityNoWater")
.kind(flecs::OnUpdate)
.with<TerrainReady>()
.with<WaterReady>()
.without<InWater>()
.with<CharacterGravity>()
.without<CharacterDisablePhysics>()
.each([this](flecs::entity e, const EngineData &eng,
const CharacterBase &ch, CharacterVelocity &gr) {
Ogre::Vector3 gravity(0, -9.8f, 0);
Ogre::Vector3 pos = ch.mBodyNode->getPosition();
gr.gvelocity += gravity * eng.delta;
if (pos.y < -1.2) {
gr.gvelocity.y = 0.0f;
}
gr.gvelocity *= (1.0 - eng.delta);
gr.velocity.y *= (1.0 - eng.delta);
});
#endif
#define TURN_SPEED 500.0f // character turning in degrees per second
ecs.system<const Input, const Camera, CharacterBase>("UpdateBody")
.kind(flecs::OnUpdate)
.with<Character>()
.with<Player>()
.without<CharacterInActuator>()
.each([](flecs::entity e, const Input &input,
.without<CharacterControlDisable>()
.each([](flecs::entity e, const Input &input,
const Camera &camera, CharacterBase &ch) {
ch.mGoalDirection = Ogre::Vector3::ZERO;
float delta = e.world().delta_time();
@@ -604,6 +544,92 @@ void CharacterModule::updateCameraGoal(Camera &camera, Ogre::Real deltaYaw,
}
}
void applyWeightBasedScale(Ogre::Entity *ent,
const Ogre::String &targetBoneName,
const Ogre::Vector3 &scale)
{
Ogre::MeshPtr mesh = ent->getMesh();
Ogre::SkeletonInstance *skel = ent->getSkeleton();
Ogre::Bone *targetBone = skel->getBone(targetBoneName);
// Create a non-uniform scale matrix relative to the bone's local space
Ogre::Matrix4 scaleMatrix = Ogre::Matrix4::IDENTITY;
scaleMatrix.setScale(scale);
for (unsigned short i = 0; i < mesh->getNumSubMeshes(); ++i) {
Ogre::SubMesh *submesh = mesh->getSubMesh(i);
Ogre::VertexData *vertexData = submesh->useSharedVertices ?
mesh->sharedVertexData :
submesh->vertexData;
// 1. Get Position Element
const Ogre::VertexElement *posElem =
vertexData->vertexDeclaration->findElementBySemantic(
Ogre::VES_POSITION);
Ogre::HardwareVertexBufferSharedPtr vbuf =
vertexData->vertexBufferBinding->getBuffer(
posElem->getSource());
// 2. Access Bone Assignments
// This map tells us which bones influence which vertex and by how much
const Ogre::Mesh::VertexBoneAssignmentList &vbal =
submesh->getBoneAssignments();
// Lock buffer for reading and writing
float *pVertices = static_cast<float *>(
vbuf->lock(Ogre::HardwareBuffer::HBL_NORMAL));
for (size_t vIdx = 0; vIdx < vertexData->vertexCount; ++vIdx) {
float totalWeight = 0.0f;
// Find assignments for this specific vertex
for (auto const &[index, assignment] : vbal) {
if (assignment.vertexIndex == vIdx &&
assignment.boneIndex ==
targetBone->getHandle()) {
totalWeight = assignment.weight;
break;
}
}
if (totalWeight > 0.0f) {
float *pPos;
posElem->baseVertexPointerToElement(
reinterpret_cast<unsigned char *>(
pVertices) +
vIdx * vbuf->getVertexSize(),
&pPos);
Ogre::Vector3 vertexPos(pPos[0], pPos[1],
pPos[2]);
// Transform vertex to bone local space, scale it, then move back
// This ensures scaling happens along the bone's axis, not the world axis
Ogre::Vector3 localPos =
targetBone->_getDerivedOrientation()
.Inverse() *
(vertexPos -
targetBone->_getDerivedPosition());
Ogre::Vector3 scaledLocalPos =
scaleMatrix * localPos;
Ogre::Vector3 worldPos =
(targetBone->_getDerivedOrientation() *
scaledLocalPos) +
targetBone->_getDerivedPosition();
// Interpolate based on weight (Lerp) to handle vertices shared with other bones
Ogre::Vector3 finalPos = Ogre::Math::lerp(
vertexPos, worldPos, totalWeight);
pPos[0] = finalPos.x;
pPos[1] = finalPos.y;
pPos[2] = finalPos.z;
}
}
vbuf->unlock();
}
}
void CharacterModule::createCharacter(flecs::entity e,
const Ogre::Vector3 &position,
const Ogre::Quaternion &rotation,

View File

@@ -37,6 +37,7 @@ struct CharacterInActuator {
Ogre::String animationState;
Vector3 prevMotion;
};
struct CharacterControlDisable {};
struct CharacterModule {
CharacterModule(flecs::world &ecs);
void updateCameraGoal(Camera &camera, Ogre::Real deltaYaw,

View File

@@ -543,7 +543,8 @@ struct GUIListener : public Ogre::RenderTargetListener {
} else {
ECS::ActionNodeList &list =
ECS::get_mut<ECS::ActionNodeList>();
if (list.dynamicNodes.size() > 0) {
if (list.dynamicNodes.size() > 0 &&
ECS::get<GUI>().enableActions) {
int j;
Ogre::Vector3 queryPos;
std::vector<size_t> points =
@@ -686,6 +687,7 @@ GUIModule::GUIModule(flecs::world &ecs)
gui.enabled = false;
gui.grab = false;
gui.grabChanged = false;
gui.enableActions = true;
})
.add(flecs::Singleton);
ecs.component<GUIData>()
@@ -695,7 +697,7 @@ GUIModule::GUIModule(flecs::world &ecs)
priv.mGuiOverlay = nullptr;
})
.add(flecs::Singleton);
ecs.set<GUI>({ false, true, false, false, false, "", {}, -1 });
ecs.set<GUI>({ false, true, false, false, false, true, "", {}, -1 });
ecs.set<GUIData>({ nullptr, {}, nullptr });
ui_wait =
ecs.system<const RenderWindow, App, GUIData>("SetupGUI")

View File

@@ -14,6 +14,7 @@ struct GUI {
bool grabChanged;
bool narrationBox;
bool mainMenu;
bool enableActions;
Ogre::String narrationText;
std::vector<Ogre::String> choices;
int narration_answer;

View File

@@ -244,7 +244,7 @@ void setupEditor(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode,
nullptr,
false,
{ 0, 0, 0 } });
ecs.set<GUI>({ true, true, true, false, false, "", {}, -1 });
ecs.set<GUI>({ true, true, true, false, false, true, "", {}, -1 });
ecs.get_mut<GUI>().enabled = true;
ecs.get_mut<GUI>().setWindowGrab(false);
ecs.modified<GUI>();

View File

@@ -12,6 +12,7 @@
#include "GUIModule.h"
#include "GUIModuleCommon.h"
#include "LuaData.h"
#include "PhysicsModule.h"
#include "PlayerActionModule.h"
namespace ECS
@@ -252,27 +253,28 @@ struct LuaNarrationHandler : GUI::NarrationHandler {
};
struct SimpleWordHandler : PlayerActionModule::ActionWordHandler {
void operator()(flecs::entity town, int index,
const Ogre::String &word) override
void operator()(int actor, flecs::entity town, int index,
const Ogre::String &word, int actionNode) override
{
TestNarrativeHandler *handle = OGRE_NEW TestNarrativeHandler();
const TownNPCs::NPCData &npc =
town.get<TownNPCs>().npcs.at(index);
flecs::entity e = npc.e;
for (const auto &anode : npc.actionNodes) {
if (anode.action == word) {
nlohmann::json props = anode.props;
props["initiator"] =
ECS::get<CharacterManagerModule>()
.getPlayer()
.id();
props["recipient"] = e.id();
handle->setProperties(props.dump());
break;
if (index >= 0) {
TestNarrativeHandler *handle =
OGRE_NEW TestNarrativeHandler();
/* this is for NPCs only, right? */
const TownNPCs::NPCData &npc =
town.get<TownNPCs>().npcs.at(index);
flecs::entity e = npc.e;
for (const auto &anode : npc.actionNodes) {
if (anode.action == word) {
nlohmann::json props = anode.props;
props["initiator"] = actor;
props["recipient"] = index;
handle->setProperties(props.dump());
break;
}
}
ECS::get_mut<GUI>().addNarrationHandler(handle);
ECS::modified<GUI>();
}
ECS::get_mut<GUI>().addNarrationHandler(handle);
ECS::modified<GUI>();
}
};
@@ -346,11 +348,13 @@ PlayerActionModule::PlayerActionModule(flecs::world &ecs)
.selected]
.action) {
(*it->second)(
town, index,
-1, town, index,
list.dynamicNodes
[list.getUIData()
.selected]
.action);
.action,
list.getUIData()
.selected);
list.setBusy();
}
}
@@ -358,6 +362,19 @@ PlayerActionModule::PlayerActionModule(flecs::world &ecs)
if (!ECS::get<GUI>().enabled)
list.setReady();
});
ecs.system<const EngineData>("UpdateActivatedWords")
.kind(flecs::OnUpdate)
.each([this](const EngineData &eng) {
for (auto it = activatedWords.begin();
it != activatedWords.end();) {
int ret = it->second->_update(eng.delta);
if (ret != ActivatedWordHandler::BUSY) {
delete it->second;
it = activatedWords.erase(it);
} else
it++;
}
});
}
void PlayerActionModule::addWordHandler(const Ogre::String &word,
@@ -377,11 +394,197 @@ void PlayerActionModule::removeWordHandler(const Ogre::String &word,
}
}
static std::set<int> activeActors;
struct TestActivatedWordHandler : PlayerActionModule::ActivatedWordHandler {
int actor;
flecs::entity town;
int index;
Ogre::String word;
int actionNode;
ActionNodeList::ActionNode anode;
flecs::entity ch;
int state;
float delay;
std::unordered_map<Ogre::String, Ogre::Vector3> placeLocalOffset;
std::unordered_map<Ogre::String, Ogre::Quaternion> placeLocalRotation;
TestActivatedWordHandler(int actor, flecs::entity town, int index,
const Ogre::String &word, int actionNode)
: PlayerActionModule::ActivatedWordHandler()
, actor(actor)
, town(town)
, index(index)
, word(word)
, actionNode(actionNode)
, state(0)
, delay(0)
{
activeActors.insert(actor);
// dynamic nodes can disappear on us so to avoid that use a copy
anode = ECS::get<ActionNodeList>().dynamicNodes[actionNode];
if (actor == -1)
ch = ECS::get<CharacterManagerModule>().getPlayer();
else if (actor >= 0) {
const TownNPCs &npcs = town.get<TownNPCs>();
ch = npcs.npcs.at(actor).e;
}
if (anode.props.find("positions") == anode.props.end())
goto out;
for (const auto &position : anode.props["positions"]) {
if (position.find("name") == position.end())
continue;
Ogre::Vector3 localPosition;
Ogre::Quaternion localRotation;
localPosition.x = position["position_x"].get<float>();
localPosition.y = position["position_y"].get<float>();
localPosition.z = position["position_z"].get<float>();
localRotation.w = position["rotation_w"].get<float>();
localRotation.x = position["rotation_x"].get<float>();
localRotation.y = position["rotation_y"].get<float>();
localRotation.z = position["rotation_z"].get<float>();
placeLocalOffset[position["name"].get<Ogre::String>()] =
localPosition;
placeLocalRotation[position["name"].get<Ogre::String>()] =
localRotation;
}
out:;
}
void teleport(const Ogre::String &place)
{
if (placeLocalOffset.find(place) == placeLocalOffset.end())
return;
Ogre::Quaternion newRotation =
anode.rotation * placeLocalRotation[place];
Ogre::Vector3 newPosition =
anode.position + newRotation * placeLocalOffset[place];
if (ch.is_valid() && ch.has<CharacterBase>()) {
ch.get<CharacterBase>()
.mBodyNode->_setDerivedOrientation(newRotation);
ch.get<CharacterBase>().mBodyNode->_setDerivedPosition(
newPosition);
}
if (actor >= 0) {
town.get_mut<TownNPCs>().npcs[actor].position =
newPosition;
town.get_mut<TownNPCs>().npcs[actor].orientation =
newRotation;
town.modified<TownNPCs>();
}
}
int update(float delta)
{
switch (state) {
case 0:
if (ECS::get<Input>().act)
delay += delta;
// activate anly after delay
if (ECS::get<Input>().act == 0 && delay > 0.2f) {
// Yay!!!
std::cout << "Node data: " << std::endl;
std::cout << anode.props.dump(4) << std::endl;
if (ch.is_valid()) {
PhysicsModule::controlPhysics(ch,
false);
// no control by player or ai
ch.add<CharacterControlDisable>();
if (word == "sit")
ch.set<CharacterInActuator>(
{ "sitting-chair",
{ 0, 0, 0 } });
// else
// ch.set<CharacterInActuator>(
// { "idle", { 0, 0, 0 } });
}
teleport("enter");
delay = 0.0f;
state = 5;
} else if (ECS::get<Input>().act == 0 &&
delay <= 0.2f) {
delay = 0.0f;
state = 10;
}
break;
case 5:
// teleport again to handle possible problems caused by root motion
delay = 0.0f;
teleport("enter");
state++;
break;
case 6:
// do not move anywhere until we depress key and wait a bit
if (ECS::get<Input>().act == 0)
delay += delta;
if (delay > 1.0f) {
delay = 0.0f;
state++;
}
break;
case 7:
// if the key is pressed for a second move to next state
if (ECS::get<Input>().act)
delay += delta;
if (delay > 1.0f) {
delay = 0.0f;
state++;
}
break;
case 8:
delay = 0.0;
state = 10;
break;
case 10:
delay = 0.0;
state++;
break;
case 11:
// wait until key is depressed for a second
if (ECS::get<Input>().act == 0)
delay += delta;
if (delay > 1.0f) {
delay = 0.0;
state++;
}
break;
case 12:
delay = 0.0f;
state = 0;
return OK;
break;
}
std::cout << this << " delay: " << delay << " state: " << state
<< std::endl;
return BUSY;
}
void enter()
{
delay = 0.0f;
state = 0;
ECS::get_mut<GUI>().enableActions = false;
ECS::modified<GUI>();
std::cout << "enter" << std::endl;
}
void exit(int result)
{
ch.remove<CharacterInActuator>();
ch.remove<CharacterControlDisable>();
PhysicsModule::controlPhysics(ch, true);
// OgreAssert(false, "exit");
delay = 0.0f;
state = 0;
ECS::get_mut<GUI>().enableActions = true;
ECS::modified<GUI>();
std::cout << "exit" << std::endl;
}
virtual ~TestActivatedWordHandler()
{
activeActors.erase(actor);
}
};
struct LuaWordHandler : PlayerActionModule::ActionWordHandler {
lua_State *L;
int ref;
void operator()(flecs::entity town, int index,
const Ogre::String &word) override
void operator()(int actor, flecs::entity town, int index,
const Ogre::String &word, int actionNode) override
{
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
if (lua_type(L, -1) == LUA_TFUNCTION) {
@@ -397,25 +600,39 @@ struct LuaWordHandler : PlayerActionModule::ActionWordHandler {
} else if (lua_type(L, -1) == LUA_TTABLE) {
luaL_checktype(L, -1, LUA_TTABLE);
lua_pop(L, 1);
LuaNarrationHandler *handle =
OGRE_NEW LuaNarrationHandler(L, ref);
const TownNPCs::NPCData &npc =
town.get<TownNPCs>().npcs.at(index);
flecs::entity e = npc.e;
for (const auto &anode : npc.actionNodes) {
if (anode.action == word) {
nlohmann::json props = anode.props;
props["initiator"] =
ECS::get<CharacterManagerModule>()
.getPlayer()
.id();
props["recipient"] = e.id();
handle->setProperties(props.dump());
break;
/* trying to talk to NPC activates narration mode */
if (index >= 0 && word == "talk") {
LuaNarrationHandler *handle =
OGRE_NEW LuaNarrationHandler(L, ref);
const TownNPCs::NPCData &npc =
town.get<TownNPCs>().npcs.at(index);
flecs::entity e = npc.e;
for (const auto &anode : npc.actionNodes) {
if (anode.action == word) {
nlohmann::json props =
anode.props;
props["initiator"] = actor;
props["recipient"] = index;
handle->setProperties(
props.dump());
break;
}
}
ECS::get_mut<GUI>().addNarrationHandler(handle);
ECS::modified<GUI>();
} else {
if (activeActors.find(actor) ==
activeActors.end()) {
TestActivatedWordHandler *handler =
OGRE_NEW TestActivatedWordHandler(
actor, town, index,
word, actionNode);
ECS::get_mut<PlayerActionModule>()
.addActivatedWordHandler(
word, handler);
ECS::modified<PlayerActionModule>();
}
}
ECS::get_mut<GUI>().addNarrationHandler(handle);
ECS::modified<GUI>();
}
}
};
@@ -460,6 +677,26 @@ int PlayerActionModule::setupLuaActionHandler(lua_State *L)
return 0;
}
void PlayerActionModule::addActivatedWordHandler(const Ogre::String &word,
ActivatedWordHandler *handler)
{
for (auto it = activatedWords.begin(); it != activatedWords.end();
it++) {
}
activatedWords.insert({ word, handler });
}
void PlayerActionModule::removeActivatedWordHandler(
const Ogre::String &word, ActivatedWordHandler *handler)
{
for (auto it = activatedWords.begin(); it != activatedWords.end();) {
if (it->first == word && it->second == handler)
it = activatedWords.erase(it);
else
it++;
}
}
void ActionNodeList::updateDynamicNodes()
{
std::lock_guard<std::mutex> lock(*nodeMutex);

View File

@@ -60,12 +60,39 @@ public:
};
struct PlayerActionModule {
struct ActionWordHandler {
virtual void operator()(flecs::entity town, int index,
const Ogre::String &word) = 0;
/** actor is -1 for player >=0 for NPCs */
virtual void operator()(int actor, flecs::entity town, int index,
const Ogre::String &word, int actionNode) = 0;
};
struct ActivatedWordHandler {
enum {OK = 0, BUSY, ERROR};
private:
bool active;
public:
ActivatedWordHandler(): active(false) {}
int _update(float delta)
{
if (!active) {
enter();
active = true;
}
int ret = update(delta);
if (ret != BUSY) {
exit(ret);
active = false;
}
return ret;
}
virtual ~ActivatedWordHandler() {}
bool _is_active() {return active;}
private:
virtual void enter() = 0;
virtual void exit(int result) = 0;
virtual int update(float delta) = 0;
};
std::multimap<Ogre::String, ActionWordHandler *>
actionWords;
std::multimap<Ogre::String, ActivatedWordHandler *> activatedWords;
PlayerActionModule(flecs::world &ecs);
void addWordHandler(const Ogre::String &word, ActionWordHandler *handler);
@@ -73,6 +100,8 @@ struct PlayerActionModule {
void addLuaWordHandler(const Ogre::String &word, lua_State *L, int ref);
void removeLuaWordHandler(const Ogre::String &word, lua_State *L, int ref);
int setupLuaActionHandler(lua_State *L);
void addActivatedWordHandler(const Ogre::String &word, ActivatedWordHandler *handler);
void removeActivatedWordHandler(const Ogre::String &word, ActivatedWordHandler *handler);
};
}

View File

@@ -5807,6 +5807,12 @@ struct TownDecorateFurniture : TownTask {
.get<float>();
anode.props =
action;
anode.props
["town"] =
e.id();
anode.props
["index"] =
-1;
anode.position =
worldSensorPosition;
anode.rotation =