Path following works great

This commit is contained in:
2026-04-25 01:17:31 +03:00
parent 5ed7552164
commit 2e358275f0
5 changed files with 239 additions and 103 deletions

View File

@@ -5,30 +5,30 @@
#include "GoapBlackboard.hpp"
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* Configuration for one animation state machine / state pair.
* Used to map logical animation names (e.g. "idle", "walk", "run")
* to the actual state machine and state in the animation tree.
* Configuration for one path following animation state.
*
* Each state (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast")
* consists of unlimited name-value pairs where:
* - name = state machine name (e.g. "main", "locomotion")
* - value = state name within that machine (e.g. "Idle", "Walking")
*
* When a path following state is activated, ALL its name-value pairs
* are applied via AnimationTreeSystem::setState().
*/
struct AnimationStateConfig {
/** Logical name used by code (e.g. "idle", "walk", "run") */
struct PathFollowingState {
/** Logical name (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") */
Ogre::String name;
/** Name of the state machine in the animation tree */
Ogre::String stateMachine = "Locomotion";
/** State machine name -> state name pairs */
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
/** Name of the state within the state machine */
Ogre::String stateName = "Idle";
PathFollowingState() = default;
AnimationStateConfig() = default;
AnimationStateConfig(const Ogre::String &name_,
const Ogre::String &stateMachine_,
const Ogre::String &stateName_)
PathFollowingState(const Ogre::String &name_)
: name(name_)
, stateMachine(stateMachine_)
, stateName(stateName_)
{
}
};
@@ -57,13 +57,13 @@ struct ActionDebug {
// Debug output
Ogre::String lastResult;
// --- Animation state machine configuration ---
// List of animation state configs (name/stateMachine/stateName triples)
// --- Path following animation states ---
// Each state has unlimited state machine name -> state name pairs.
// Default entries: idle, walk, run
std::vector<AnimationStateConfig> animStates = {
{ "idle", "Locomotion", "Idle" },
{ "walk", "Locomotion", "Walk" },
{ "run", "Locomotion", "Run" },
std::vector<PathFollowingState> pathFollowingStates = {
{ "idle" },
{ "walk" },
{ "run" },
};
// Walk speed (m/s) used for root motion scaling
@@ -78,21 +78,17 @@ struct ActionDebug {
ActionDebug() = default;
/**
* Get the state machine and state name for a given logical animation name.
* Returns true if found, false if not (caller should use defaults).
* Get the state machine/state pairs for a given path following state name.
* Returns nullptr if not found.
*/
bool getAnimState(const Ogre::String &animName,
Ogre::String &outStateMachine,
Ogre::String &outStateName) const
const PathFollowingState *
findPathState(const Ogre::String &stateName) const
{
for (const auto &cfg : animStates) {
if (cfg.name == animName) {
outStateMachine = cfg.stateMachine;
outStateName = cfg.stateName;
return true;
}
for (const auto &state : pathFollowingStates) {
if (state.name == stateName)
return &state;
}
return false;
return nullptr;
}
};

View File

@@ -1936,6 +1936,9 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
for (const auto &pair : cs.slots)
json["slots"][pair.first] = pair.second;
// Serialize front axis
json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z };
return json;
}
@@ -1949,6 +1952,16 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
for (auto &[slot, mesh] : json["slots"].items())
cs.slots[slot] = mesh.get<std::string>();
}
// Deserialize front axis
if (json.contains("frontAxis") && json["frontAxis"].is_array() &&
json["frontAxis"].size() >= 3) {
cs.frontAxis = Ogre::Vector3(json["frontAxis"][0].get<float>(),
json["frontAxis"][1].get<float>(),
json["frontAxis"][2].get<float>());
if (cs.frontAxis.squaredLength() > 0.0001f)
cs.frontAxis.normalise();
}
cs.dirty = true;
entity.set<CharacterSlotsComponent>(cs);
}
@@ -3215,14 +3228,19 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
if (!debug.selectedGoalName.empty())
json["selectedGoalName"] = debug.selectedGoalName;
// Serialize animation state configs
json["animStates"] = nlohmann::json::array();
for (const auto &cfg : debug.animStates) {
// Serialize path following animation states
json["pathFollowingStates"] = nlohmann::json::array();
for (const auto &pfState : debug.pathFollowingStates) {
nlohmann::json entry;
entry["name"] = cfg.name;
entry["stateMachine"] = cfg.stateMachine;
entry["stateName"] = cfg.stateName;
json["animStates"].push_back(entry);
entry["name"] = pfState.name;
entry["stateMachineStates"] = nlohmann::json::array();
for (const auto &pair : pfState.stateMachineStates) {
nlohmann::json pairEntry;
pairEntry["stateMachine"] = pair.first;
pairEntry["stateName"] = pair.second;
entry["stateMachineStates"].push_back(pairEntry);
}
json["pathFollowingStates"].push_back(entry);
}
json["walkSpeed"] = debug.walkSpeed;
@@ -3240,28 +3258,71 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
debug.selectedActionName = json.value("selectedActionName", "");
debug.selectedGoalName = json.value("selectedGoalName", "");
// Deserialize animation state configs (new format)
if (json.contains("animStates") && json["animStates"].is_array()) {
debug.animStates.clear();
// Deserialize path following animation states
if (json.contains("pathFollowingStates") &&
json["pathFollowingStates"].is_array()) {
debug.pathFollowingStates.clear();
for (const auto &entry : json["pathFollowingStates"]) {
PathFollowingState pfState;
pfState.name = entry.value("name", "");
if (entry.contains("stateMachineStates") &&
entry["stateMachineStates"].is_array()) {
for (const auto &pairEntry :
entry["stateMachineStates"]) {
Ogre::String sm = pairEntry.value(
"stateMachine", "main");
Ogre::String state = pairEntry.value(
"stateName", "Idle");
pfState.stateMachineStates.push_back(
{ sm, state });
}
}
debug.pathFollowingStates.push_back(pfState);
}
} else if (json.contains("animStates") &&
json["animStates"].is_array()) {
// Backward compatibility: old animStates format
debug.pathFollowingStates.clear();
for (const auto &entry : json["animStates"]) {
AnimationStateConfig cfg;
cfg.name = entry.value("name", "");
cfg.stateMachine =
entry.value("stateMachine", "Locomotion");
cfg.stateName = entry.value("stateName", "Idle");
debug.animStates.push_back(cfg);
PathFollowingState pfState;
pfState.name = entry.value("name", "");
Ogre::String mainSM =
entry.value("mainStateMachine", "main");
Ogre::String subSM =
entry.value("subStateMachine", "locomotion");
Ogre::String stateName =
entry.value("stateName", "Idle");
pfState.stateMachineStates.push_back({ mainSM, subSM });
pfState.stateMachineStates.push_back(
{ subSM, stateName });
debug.pathFollowingStates.push_back(pfState);
}
} else {
// Backward compatibility: old format with individual fields
debug.animStates.clear();
// Backward compatibility: old individual fields format
debug.pathFollowingStates.clear();
Ogre::String sm =
json.value("locomotionStateMachine", "Locomotion");
debug.animStates.push_back(
{ "idle", sm, json.value("idleStateName", "Idle") });
debug.animStates.push_back(
{ "walk", sm, json.value("walkStateName", "Walk") });
debug.animStates.push_back(
{ "run", sm, json.value("runStateName", "Run") });
{
PathFollowingState idle("idle");
idle.stateMachineStates.push_back({ "main", sm });
idle.stateMachineStates.push_back(
{ sm, json.value("idleStateName", "Idle") });
debug.pathFollowingStates.push_back(idle);
}
{
PathFollowingState walk("walk");
walk.stateMachineStates.push_back({ "main", sm });
walk.stateMachineStates.push_back(
{ sm, json.value("walkStateName", "Walk") });
debug.pathFollowingStates.push_back(walk);
}
{
PathFollowingState run("run");
run.stateMachineStates.push_back({ "main", sm });
run.stateMachineStates.push_back(
{ sm, json.value("runStateName", "Run") });
debug.pathFollowingStates.push_back(run);
}
}
debug.walkSpeed = json.value("walkSpeed", 2.5f);

View File

@@ -111,20 +111,24 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e,
if (!e.has<AnimationTreeComponent>())
return;
Ogre::String smName = "Locomotion";
Ogre::String stateName = animName;
// Try to get the state machine/state from ActionDebug's animStates list
// Look up the path following state in ActionDebug
if (e.has<ActionDebug>()) {
auto &debug = e.get<ActionDebug>();
Ogre::String foundSM, foundState;
if (debug.getAnimState(animName, foundSM, foundState)) {
smName = foundSM;
stateName = foundState;
const PathFollowingState *pfState =
debug.findPathState(animName);
if (pfState) {
// Apply ALL state machine name -> state name pairs
for (const auto &pair : pfState->stateMachineStates) {
m_animTreeSystem->setState(e, pair.first,
pair.second, false);
}
return;
}
}
m_animTreeSystem->setState(e, smName, stateName, false);
// Fallback: if no config found, try to set the state directly
// using the animName as both state machine and state name
m_animTreeSystem->setState(e, "main", animName, false);
}
bool SmartObjectSystem::testSmartObjectAction(flecs::entity character,

View File

@@ -153,47 +153,88 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity,
void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug)
{
ImGui::Text("Animation State Configs:");
ImGui::Text("Path Following Animation States:");
ImGui::TextDisabled(
"Map logical names (idle/walk/run) to state machine/state pairs");
"Each state (idle/walk/run/swim-idle/swim/swim-fast) has unlimited\n"
"state machine name -> state name pairs applied when activated.");
ImGui::Separator();
// Render each animation state config
int removeIdx = -1;
for (int i = 0; i < (int)debug.animStates.size(); i++) {
auto &cfg = debug.animStates[i];
// Render each path following state
int removeStateIdx = -1;
for (int i = 0; i < (int)debug.pathFollowingStates.size(); i++) {
auto &pfState = debug.pathFollowingStates[i];
ImGui::PushID(i);
char buf[256];
ImGui::Text("Entry %d:", i);
ImGui::Text("State %d:", i);
ImGui::Indent();
// Logical name (e.g. "idle", "walk", "run")
strncpy(buf, cfg.name.c_str(), sizeof(buf) - 1);
strncpy(buf, pfState.name.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("Name", buf, sizeof(buf))) {
cfg.name = buf;
pfState.name = buf;
}
// State machine name
strncpy(buf, cfg.stateMachine.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("State Machine", buf, sizeof(buf))) {
cfg.stateMachine = buf;
ImGui::Text("State Machine -> State pairs:");
ImGui::Indent();
// Render each name-value pair
int removePairIdx = -1;
for (int j = 0; j < (int)pfState.stateMachineStates.size();
j++) {
auto &pair = pfState.stateMachineStates[j];
ImGui::PushID(j);
char smBuf[128];
char stateBuf[128];
strncpy(smBuf, pair.first.c_str(), sizeof(smBuf) - 1);
smBuf[sizeof(smBuf) - 1] = '\0';
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) {
pair.first = smBuf;
}
ImGui::SameLine();
ImGui::Text("->");
ImGui::SameLine();
strncpy(stateBuf, pair.second.c_str(),
sizeof(stateBuf) - 1);
stateBuf[sizeof(stateBuf) - 1] = '\0';
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputText("##state", stateBuf,
sizeof(stateBuf))) {
pair.second = stateBuf;
}
ImGui::SameLine();
if (ImGui::SmallButton("X")) {
removePairIdx = j;
}
ImGui::PopID();
}
// State name within the state machine
strncpy(buf, cfg.stateName.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("State Name", buf, sizeof(buf))) {
cfg.stateName = buf;
if (removePairIdx >= 0) {
pfState.stateMachineStates.erase(
pfState.stateMachineStates.begin() +
removePairIdx);
}
// Remove button (keep at least 1 entry)
if (debug.animStates.size() > 1) {
if (ImGui::SmallButton("Remove")) {
removeIdx = i;
if (ImGui::SmallButton("+ Add Pair")) {
pfState.stateMachineStates.push_back(
{ "main", "Idle" });
}
ImGui::Unindent();
// Remove state button
if (debug.pathFollowingStates.size() > 1) {
if (ImGui::SmallButton("Remove State")) {
removeStateIdx = i;
}
}
@@ -202,13 +243,15 @@ void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug)
ImGui::PopID();
}
if (removeIdx >= 0) {
debug.animStates.erase(debug.animStates.begin() + removeIdx);
if (removeStateIdx >= 0) {
debug.pathFollowingStates.erase(
debug.pathFollowingStates.begin() + removeStateIdx);
}
// Add new entry button
if (ImGui::Button("Add Animation State")) {
debug.animStates.push_back(AnimationStateConfig());
// Add new state button
if (ImGui::Button("Add Path Following State")) {
debug.pathFollowingStates.push_back(
PathFollowingState("new_state"));
}
ImGui::Separator();

View File

@@ -21,13 +21,13 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
CharacterSlotSystem::loadCatalog();
/* Age selector */
std::vector<Ogre::String> ages =
CharacterSlotSystem::getAges();
std::vector<Ogre::String> ages = CharacterSlotSystem::getAges();
Ogre::String currentAge = cs.age;
if (ImGui::BeginCombo("Age", currentAge.c_str())) {
for (const auto &age : ages) {
bool isSelected = (currentAge == age);
if (ImGui::Selectable(age.c_str(), isSelected)) {
if (ImGui::Selectable(age.c_str(),
isSelected)) {
cs.age = age;
modified = true;
cs.dirty = true;
@@ -45,7 +45,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
if (ImGui::BeginCombo("Sex", currentSex.c_str())) {
for (const auto &sex : sexes) {
bool isSelected = (currentSex == sex);
if (ImGui::Selectable(sex.c_str(), isSelected)) {
if (ImGui::Selectable(sex.c_str(),
isSelected)) {
cs.sex = sex;
modified = true;
cs.dirty = true;
@@ -58,11 +59,43 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
ImGui::Separator();
/* Front-facing axis */
{
Ogre::Vector3 axis = cs.frontAxis;
float axisVals[3] = { axis.x, axis.y, axis.z };
ImGui::Text("Front Axis:");
ImGui::SameLine();
ImGui::TextDisabled(
"(direction character faces, used by path following)");
if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f,
-1.0f, 1.0f)) {
cs.frontAxis = Ogre::Vector3(
axisVals[0], axisVals[1], axisVals[2]);
if (cs.frontAxis.squaredLength() > 0.0001f)
cs.frontAxis.normalise();
modified = true;
}
/* Quick presets */
ImGui::SameLine();
if (ImGui::SmallButton("-Z")) {
cs.frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
modified = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("+Z")) {
cs.frontAxis = Ogre::Vector3::UNIT_Z;
modified = true;
}
}
ImGui::Separator();
/* Collect available and configured slots */
std::vector<Ogre::String> availableSlots =
CharacterSlotSystem::getSlots(cs.age, cs.sex);
for (const auto &pair : cs.slots) {
if (std::find(availableSlots.begin(), availableSlots.end(),
if (std::find(availableSlots.begin(),
availableSlots.end(),
pair.first) == availableSlots.end())
availableSlots.push_back(pair.first);
}
@@ -80,9 +113,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
slot);
Ogre::String label = slot;
Ogre::String preview = currentMesh.empty() ?
"(none)" :
currentMesh;
Ogre::String preview =
currentMesh.empty() ? "(none)" : currentMesh;
if (ImGui::BeginCombo(label.c_str(), preview.c_str())) {
bool noneSelected = currentMesh.empty();