Path following works great
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user