Compare commits

...

5 Commits

Author SHA1 Message Date
69035351a6 Action nodes handling 2026-02-01 23:25:38 +03:00
e6efd89bb0 Action nodes work better now 2026-02-01 00:13:33 +03:00
201aa92fe7 Helper script for action node editing 2026-01-31 04:37:38 +03:00
c860152f9b Using threads more 2026-01-30 01:04:23 +03:00
da4c1fee0e Threads and tasks 2026-01-29 15:28:50 +03:00
27 changed files with 2376 additions and 676 deletions

View File

@@ -453,7 +453,9 @@ public:
mDbgDraw->setDebugMode(mDbgDraw->getDebugMode() |
btIDebugDraw::DBG_DrawContactPoints);
#endif
}
Ogre::LogManager::getSingleton().setMinLogLevel(
Ogre::LML_CRITICAL);
}
Ogre::SceneManager *getSceneManager()
{
return mScnMgr;

View File

@@ -0,0 +1,362 @@
import bpy
import mathutils
ACTION_PROPS = ["name", "action", "action_text", "height", "radius", "tags"]
FURNITURE_PROPS = ["name", "furniture_tags"]
test_tracking = {"lib_name": None}
class TEST_OT_LinkAndPlay(bpy.types.Operator):
bl_idname = "test.link_and_play"
bl_label = "Link and Play Sitting"
filepath: bpy.props.StringProperty()
rig_name: bpy.props.StringProperty()
action_name: bpy.props.StringProperty()
def execute(self, context):
target = context.active_object
path = bpy.path.abspath(self.filepath)
mesh_name = "Body"
with bpy.data.libraries.load(path, link=True) as (data_from, data_to):
# We request all three components
data_to.objects = [n for n in [self.rig_name, mesh_name] if n in data_from.objects]
data_to.actions = [self.action_name] if self.action_name in data_from.actions else []
# 2. Add to scene and create Override
rig_obj = None
for obj in data_to.objects:
offset = mathutils.Matrix.Translation((0, -0.1, 0))
bpy.context.collection.objects.link(obj)
obj.matrix_world = target.matrix_world @ offset
if obj.type == 'ARMATURE':
rig_obj = obj
if obj.library:
test_tracking["lib_name"] = obj.library.name
if rig_obj:
if not rig_obj.animation_data:
rig_obj.animation_data_create()
if data_to.actions:
rig_obj.animation_data.action = data_to.actions[0]
bpy.ops.screen.animation_play()
return {'FINISHED'}
class TEST_OT_Cleanup(bpy.types.Operator):
bl_idname = "test.cleanup"
bl_label = "Stop & Cleanup"
def execute(self, context):
if context.screen.is_animation_playing:
bpy.ops.screen.animation_cancel(restore_frame=True)
lib_name = test_tracking.get("lib_name")
if lib_name and lib_name in bpy.data.libraries:
lib = bpy.data.libraries[lib_name]
bpy.data.batch_remove(ids=(lib,))
test_tracking["lib_name"] = None
# Deep purge orphans (meshes, armatures, etc)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
return {'FINISHED'}
class TEST_PT_ObjectPanel(bpy.types.Panel):
"""Only appears in Object Properties when a 'position-' Empty is selected"""
bl_label = "Character Placement Test"
bl_idname = "TEST_PT_object_panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == 'EMPTY' and obj.name.startswith("position-")
def draw(self, context):
layout = self.layout
layout.label(text="Test Controls:", icon='OUTLINER_OB_EMPTY')
# Link button with preset arguments
row = layout.row(align=True)
op = row.operator("test.link_and_play", text="Link & Sit", icon='PLAY')
op.filepath = "//../../edited-normal-male.blend" # SET YOUR FILEPATH
op.rig_name = "male" # SET YOUR RIG NAME
op.action_name = "sitting-chair" # SET YOUR ACTION NAME
# Cleanup button
layout.operator("test.cleanup", text="Stop & Clear", icon='TRASH')
class OBJECT_OT_CreateActionProps(bpy.types.Operator):
"""Initializes custom properties on the selected action- empty"""
bl_idname = "object.create_action_props"
bl_label = "Create Action"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
obj = context.object
for p in ACTION_PROPS:
if p not in obj:
if p in ["height"]:
obj[p] = 1.0
elif p in ["radius"]:
obj[p] = 0.5
elif p == "name":
obj[p] = obj.name.replace("action-","")
elif p == "action_text":
if "action" in obj:
obj[p] = obj["action"].capitalize()
else:
obj[p] = ""
else:
obj[p] = "" # Initialize as empty text string
return {'FINISHED'}
class OBJECT_OT_CreateFurnitureProps(bpy.types.Operator):
"""Initializes custom properties on the selected furniture- empty"""
bl_idname = "object.create_furniture_props"
bl_label = "Create Furniture"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
obj = context.object
for p in FURNITURE_PROPS:
if p not in obj:
if p == "name":
obj[p] = obj.name.replace("furniture-","")
elif p == "furniture_tags":
obj[p] = ""
otags = []
if obj.name.find("toilet") >= 0:
otags.append("wc")
otags.append("bathroom")
otags.append("toilet")
elif obj.name.find("bed") >= 0:
otags.append("bed")
otags.append("bedroom")
obj[p] = ", ".join(otags)
else:
obj[p] = "" # Initialize as empty text string
return {'FINISHED'}
class OBJECT_OT_CreateChildEmpties(bpy.types.Operator):
"""Creates two child empties"""
bl_idname = "object.create_child_empties"
bl_label = "Create Child Nodes"
bl_options = {'REGISTER', 'UNDO'}
target_val: bpy.props.StringProperty()
def execute(self, context):
parent = context.object
# Define offsets and their corresponding labels for the name
configs = []
configs_enter = [((0, 0.5, -parent.location.z), "enter")]
configs_exit = [((0, -0.5, -parent.location.z), "exit")]
if self.target_val == "enter":
configs = configs_enter
elif self.target_val == "exit":
configs = configs_exit
elif self.target_val == "all":
configs = configs_enter + configs_exit
for offset, label in configs:
# Create new Empty with the "position-" prefix
child_name = f"position-{label}.000"
child = bpy.data.objects.new(child_name, None)
# Link to the current collection
context.collection.objects.link(child)
# Set parent and local offset
child.parent = parent
child.location = offset
child["name"] = label
return {'FINISHED'}
class OBJECT_OT_CreateChildActionNode(bpy.types.Operator):
"""Creates child action node"""
bl_idname = "object.create_child_action_node"
bl_label = "Create Child Action Node"
bl_options = {'REGISTER', 'UNDO'}
target_val: bpy.props.StringProperty()
def execute(self, context):
parent = context.object
# Define offsets and their corresponding labels for the name
configs = [((0, 0.1, -parent.location.z + 0.5), self.target_val)]
for offset, label in configs:
# Create new Empty with the "position-" prefix
child_name = f"action-{label}.000"
child = bpy.data.objects.new(child_name, None)
# Link to the current collection
context.collection.objects.link(child)
# Set parent and local offset
child.parent = parent
child.location = offset
child["name"] = label
child["action"] = label
for p in ACTION_PROPS:
if p not in child:
if p in ["height"]:
child[p] = 1.0
elif p in ["radius"]:
child[p] = 0.5
elif p == "name":
child[p] = child.name.replace("action-","")
elif p == "action_text":
if "action" in child:
child[p] = child["action"].capitalize()
else:
child[p] = ""
else:
child[p] = "" # Initialize as empty text string
return {'FINISHED'}
class OBJECT_OT_DestroyChildByProp(bpy.types.Operator):
"""Deletes children starting with 'position-' where custom prop 'name' matches value"""
bl_idname = "object.destroy_child_by_prop"
bl_label = "Destroy Child"
bl_options = {'REGISTER', 'UNDO'}
target_val: bpy.props.StringProperty()
def execute(self, context):
parent = context.object
# Collect children to delete to avoid modifying list during iteration
to_delete = [
child for child in parent.children
if child.name.startswith("position-") and child.get("name") == self.target_val
]
for child in to_delete:
bpy.data.objects.remove(child, do_unlink=True)
return {'FINISHED'}
class OBJECT_PT_ActionEmptyPanel(bpy.types.Panel):
"""Custom GUI panel for 'action-' prefixed Empties"""
bl_label = "Action Controls"
bl_idname = "OBJECT_PT_action_empty_panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
@classmethod
def poll(cls, context):
obj = context.object
return (obj is not None and
obj.type == 'EMPTY' and
obj.name.startswith("action-"))
def draw(self, context):
layout = self.layout
obj = context.object
# Check if all required properties already exist
props_exist = all(p in obj for p in ACTION_PROPS)
if not props_exist:
# Show ONLY the button if properties are missing
layout.operator("object.create_action_props", icon='PLUS')
else:
# Show the text fields if properties exist
box = layout.box()
box.label(text="Action Settings", icon='PROPERTIES')
for p in ACTION_PROPS:
box.prop(obj, f'["{p}"]', text=p.replace("_", " ").title())
layout.separator()
have_enter = False
have_exit = False
for c in obj.children:
if c.name.startswith("position-") and "name" in c and c["name"] == "enter":
have_enter = True
break
for c in obj.children:
if c.name.startswith("position-") and "name" in c and c["name"] == "exit":
have_exit = True
break
if not have_enter:
op_enter = layout.operator("object.create_child_empties", icon='EMPTY_DATA', text="Add Enter Position")
op_enter.target_val = "enter"
else:
op_enter = layout.operator("object.destroy_child_by_prop", text="Destroy 'Enter' Node", icon='X')
op_enter.target_val = "enter"
if not have_exit:
op_exit = layout.operator("object.create_child_empties", icon='EMPTY_DATA', text="Add Exit Position")
op_exit.target_val = "exit"
else:
op_exit = layout.operator("object.destroy_child_by_prop", text="Destroy 'Exit' Node", icon='X')
op_exit.target_val = "exit"
class OBJECT_PT_FurnitureEmptyPanel(bpy.types.Panel):
"""Custom GUI panel for 'furniture-' prefixed Empties"""
bl_label = "Furniture Controls"
bl_idname = "OBJECT_PT_action_furniture_panel"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
@classmethod
def poll(cls, context):
obj = context.object
return (obj is not None and
obj.type == 'EMPTY' and
obj.name.startswith("furniture-"))
def draw(self, context):
layout = self.layout
obj = context.object
# Check if all required properties already exist
props_exist = all(p in obj for p in FURNITURE_PROPS)
if not props_exist:
# Show ONLY the button if properties are missing
layout.operator("object.create_furniture_props", icon='PLUS')
else:
# Show the text fields if properties exist
box = layout.box()
box.label(text="Furnitore Settings", icon='PROPERTIES')
for p in FURNITURE_PROPS:
box.prop(obj, f'["{p}"]', text=p.replace("_", " ").title())
op_use = layout.operator("object.create_child_action_node", icon='EMPTY_DATA', text="Add Use Action Node")
op_use.target_val = "use"
op_use = layout.operator("object.create_child_action_node", icon='EMPTY_DATA', text="Add Sit Action Node")
op_use.target_val = "sit"
def register():
bpy.utils.register_class(OBJECT_OT_CreateActionProps)
bpy.utils.register_class(OBJECT_OT_CreateFurnitureProps)
bpy.utils.register_class(OBJECT_PT_ActionEmptyPanel)
bpy.utils.register_class(OBJECT_OT_CreateChildEmpties)
bpy.utils.register_class(OBJECT_OT_DestroyChildByProp)
bpy.utils.register_class(OBJECT_PT_FurnitureEmptyPanel)
bpy.utils.register_class(OBJECT_OT_CreateChildActionNode)
bpy.utils.register_class(TEST_OT_LinkAndPlay)
bpy.utils.register_class(TEST_OT_Cleanup)
bpy.utils.register_class(TEST_PT_ObjectPanel)
def unregister():
bpy.utils.register_class(OBJECT_OT_CreateActionProps)
bpy.utils.register_class(OBJECT_OT_CreateFurnitureProps)
bpy.utils.unregister_class(OBJECT_PT_ActionEmptyPanel)
bpy.utils.unregister_class(OBJECT_OT_CreateChildEmpties)
bpy.utils.unregister_class(OBJECT_OT_DestroyChildByProp)
bpy.utils.unregister_class(OBJECT_PT_FurnitureEmptyPanel)
bpy.utils.unregister_class(OBJECT_OT_CreateChildActionNode)
bpy.utils.unregister_class(TEST_OT_LinkAndPlay)
bpy.utils.unregister_class(TEST_OT_Cleanup)
bpy.utils.unregister_class(TEST_PT_ObjectPanel)
if __name__ == "__main__":
register()

View File

@@ -84,6 +84,20 @@ def export_root_objects_to_gltf(output_dir):
action["name"] = child["name"]
else:
action["name"] = desc["name"] + "_" + str(len(desc["actions"]))
if "tags" in child:
atags = child["tags"]
if "," in atags:
atags = [m.strip() for m in atags.split(",")]
action["tags"] = atags
else:
action["tags"] = [atags]
else:
action["tags"] = []
for k, v in child.items():
if k.startswith("goap_prereq_"):
action[k] = v
elif k.startswith("goap_effect_"):
action[k] = v
action["furniture"] = {}
action["furniture"]["name"] = desc["name"]
action["furniture"]["tags"] = desc["tags"]
@@ -102,8 +116,8 @@ def export_root_objects_to_gltf(output_dir):
if "type" in schild:
position["type"] = schild["type"]
position["position_x"] = local_pos[0]
position["position_y"] = local_pos[2]
position["position_z"] = local_pos[1]
position["position_y"] = local_pos[1]
position["position_z"] = local_pos[2]
position["rotation_w"] = local_rot[0]
position["rotation_x"] = local_rot[1]
position["rotation_y"] = local_rot[2]
@@ -123,8 +137,8 @@ def export_root_objects_to_gltf(output_dir):
if "type" in child:
position["type"] = child["type"]
position["position_x"] = local_pos[0]
position["position_y"] = local_pos[2]
position["position_z"] = local_pos[1]
position["position_y"] = local_pos[1]
position["position_z"] = local_pos[2]
position["rotation_w"] = local_rot[0]
position["rotation_x"] = local_rot[1]
position["rotation_y"] = local_rot[2]

Binary file not shown.

View File

@@ -661,16 +661,29 @@ setup_action_handler("talk", {
activate = function(this)
local book = narrator.parse_file('stories.talk')
this.story = narrator.init_story(book)
local crash_bind = function()
crash()
local get_crash_bind = function(this)
local crash_bind = function()
print("variables")
print(dump(this.story.variables))
crash()
end
return crash_bind
end
this.story:bind('crash', crash_bind)
this.story:begin()
this:narration_update()
this.story:bind('crash', get_crash_bind(this))
local props = this._get_properties()
print(props)
local goals = this._get_goals()
print("node activated")
-- print(dump(goals))
local json_data = json.decode(props)
print(dump(json_data))
for i, v in ipairs(goals) do
print("Goal: ", i)
local goal_data = json.decode(v)
print(dump(goal_data))
end
this.story:begin()
this:narration_update()
get_crash_bind(this)()
crash()
end,
event = function(this, event)
@@ -744,6 +757,28 @@ setup_action_handler("talk", {
print("tag: " .. tag)
end,
})
setup_action_handler("sit", {
activate = function(this)
print('sit activate')
crash()
end,
event = function(this, event)
print('event', event)
end,
finish = function(this)
end,
})
setup_action_handler("use", {
activate = function(this)
print('sit activate')
crash()
end,
event = function(this, event)
print('event', event)
end,
finish = function(this)
end,
})
--[[
active_dialogues = {}
setup_action_handler("talk", function(town, index, word)

View File

@@ -432,7 +432,9 @@ public:
createContent();
std::cout << "Setup done"
<< "\n";
}
Ogre::LogManager::getSingleton().setMinLogLevel(
Ogre::LML_TRIVIAL);
}
Ogre::Timer mTerrainUpd;
// TODO: implement rough water level calculation
float getWaterLevel(const Ogre::Vector3 &position)

View File

@@ -9,7 +9,7 @@ add_library(GameData STATIC GameData.cpp CharacterModule.cpp WaterModule.cpp Sun
GUIModule.cpp EditorGUIModule.cpp LuaData.cpp WorldMapModule.cpp BoatModule.cpp EventTriggerModule.cpp
CharacterAnimationModule.cpp PhysicsModule.cpp EventModule.cpp CharacterManagerModule.cpp
VehicleManagerModule.cpp AppModule.cpp StaticGeometryModule.cpp SmartObject.cpp SlotsModule.cpp
PlayerActionModule.cpp goap.cpp)
PlayerActionModule.cpp CharacterAIModule.cpp goap.cpp)
target_link_libraries(GameData PUBLIC
lua
flecs::flecs_static
@@ -18,5 +18,5 @@ target_link_libraries(GameData PUBLIC
OgreBites
OgrePaging OgreTerrain OgreOverlay OgreProcedural::OgreProcedural items
PRIVATE sceneloader world-build physics editor)
target_include_directories(GameData PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${BULLET_INCLUDE_DIR} ../luaaa)
target_include_directories(GameData PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${BULLET_INCLUDE_DIR} ../luaaa ../aitoolkit/include)
target_compile_definitions(GameData PRIVATE FLECS_CPP_NO_AUTO_REGISTRATION)

View File

@@ -0,0 +1,895 @@
#include <iostream>
#include <nlohmann/json.hpp>
#include <Ogre.h>
#include "goap.h"
#include "CharacterManagerModule.h"
#include "PlayerActionModule.h"
#include "CharacterModule.h"
#include "items.h"
#include "CharacterAIModule.h"
namespace ECS
{
class ActionNodeActions {
struct WalkToAction : public goap::BaseAction<Blackboard> {
int node;
WalkToAction(int node, int cost)
: goap::BaseAction<Blackboard>(
"WalkTo(" +
Ogre::StringConverter::toString(
node) +
")",
{ { { "at_object", 0 } } },
{ { { "at_object", 1 } } }, cost)
, node(node)
{
}
bool can_run(const Blackboard &state,
bool debug = false) override
{
return (state.distance_to(prereq) == 0);
}
void _plan_effects(Blackboard &state) override
{
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
state.apply(effects);
// node position can change in case of character
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
state.setPosition(nodePosition);
}
int get_cost(const Blackboard &bb) const override
{
int ret = m_cost;
if (!bb.town.is_valid()) {
ret += 1000000;
goto out;
}
{
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
// const TownNPCs &npcs = bb.town.get<TownNPCs>();
// const TownAI &ai = bb.town.get<TownAI>();
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
// flecs::entity e = npcs.npcs.at(bb.index).e;
// bool validActive = e.is_valid() &&
// e.has<CharacterBase>();
const Ogre::Vector3 &npcPosition =
bb.getPosition();
float dist = npcPosition.squaredDistance(
nodePosition);
ret += (int)Ogre::Math::Ceil(dist);
}
out:
return ret;
}
};
struct RunActionNode : public goap::BaseAction<Blackboard> {
int node;
float radius;
Ogre::String action;
RunActionNode(int node, const Ogre::String &action,
const Blackboard &prereq,
const Blackboard &effects, int cost)
: goap::BaseAction<Blackboard>(
"Use(" + action + "," +
Ogre::StringConverter::toString(
node) +
")",
prereq, effects, cost)
, node(node)
, action(action)
{
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
radius = alist.dynamicNodes[node].radius;
}
bool can_run(const Blackboard &state,
bool debug = false) override
{
bool pre = (state.distance_to(prereq) == 0);
if (!pre)
return pre;
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
const Ogre::Vector3 &npcPosition = state.getPosition();
return (npcPosition.squaredDistance(nodePosition) <
radius * radius);
}
};
std::vector<goap::BaseAction<Blackboard> *> m_actions;
public:
ActionNodeActions(int node, const Blackboard &prereq, int cost)
{
OgreAssert(
node < ECS::get<ActionNodeList>().dynamicNodes.size(),
"bad node " + Ogre::StringConverter::toString(node));
m_actions.push_back(OGRE_NEW WalkToAction(node, 10000));
nlohmann::json jactionPrereq = nlohmann::json::object();
nlohmann::json jactionEffect = nlohmann::json::object();
jactionPrereq["at_object"] = 1;
const nlohmann::json props =
ECS::get<ActionNodeList>().dynamicNodes[node].props;
OgreAssert(!props.is_null(),
"bad node " + Ogre::StringConverter::toString(node));
Ogre::String prefix = "goap_prereq_";
for (auto it = props.begin(); it != props.end(); it++) {
if (it.key().substr(0, prefix.length()) == prefix) {
Ogre::String key =
it.key().substr(prefix.length());
jactionPrereq[key] = it.value();
}
}
Ogre::String prefix2 = "goap_effect_";
for (auto it = props.begin(); it != props.end(); it++) {
if (it.key().substr(0, prefix2.length()) == prefix2) {
Ogre::String key =
it.key().substr(prefix2.length());
jactionPrereq[key] = it.value();
}
}
OgreAssert(props.find("action") != props.end(),
"bad action" + props.dump(4));
const Ogre::String &action =
props["action"].get<Ogre::String>();
Ogre::String effectName = "";
#if 0
if (action == "sit") {
const Ogre::String &nodeName =
props["name"].get<Ogre::String>();
effectName = "is_" + nodeName + "_seated";
} else if (action == "use") {
const Ogre::String &nodeName =
props["name"].get<Ogre::String>();
effectName = "is_" + nodeName + "_used";
}
#endif
// const Ogre::String &nodeName =
// props["name"].get<Ogre::String>();
Ogre::String nodeID = Ogre::StringConverter::toString(node);
effectName = "is_used";
if (effectName.length() > 0) {
jactionPrereq[effectName] = 0;
jactionEffect[effectName] = 1;
}
// FIXME: add this to Blender goap_prereq_ and goap_effect_ variables
if (action == "sit") {
jactionPrereq["is_seated"] = 0;
jactionEffect["is_seated"] = 1;
}
Blackboard actionPrereq({ jactionPrereq });
Blackboard actionEffect({ jactionEffect });
if (!prereq.stats.is_null())
actionPrereq.apply(prereq);
m_actions.push_back(OGRE_NEW RunActionNode(
node, action, actionPrereq, actionEffect, cost));
if (effectName == "") {
std::cout << props.dump(4) << std::endl;
std::cout << "Prereq" << std::endl;
std::cout << actionPrereq.stats.dump(4) << std::endl;
std::cout << "Effect" << std::endl;
std::cout << actionEffect.stats.dump(4) << std::endl;
OgreAssert(false, "action");
}
}
std::vector<goap::BaseAction<Blackboard> *> getActions() const
{
return m_actions;
}
};
CharacterAIModule::CharacterAIModule(flecs::world &ecs)
{
static std::mutex ecs_mutex;
ecs.module<CharacterAIModule>();
ecs.import <CharacterManagerModule>();
ecs.component<Blackboard>();
ecs.component<TownAI>().on_add([](flecs::entity e, TownAI &ai) {
std::lock_guard<std::mutex> lock(ecs_mutex);
ai.mutex = std::make_shared<std::mutex>();
std::lock_guard<std::mutex> lock2(*ai.mutex);
ai.goals.push_back(
{ "HealthGoal",
{ nlohmann::json::object({ { "healthy", 1 } }) } });
ai.goals.push_back(
{ "NotHungryGoal", { { { "hungry", 0 } } } });
ai.goals.push_back(
{ "NotThirstyGoal", { { { "thirsty", 0 } } } });
ai.goals.push_back(
{ "SatisfyToiletNeedGoal", { { { "toilet", 0 } } } });
struct ActionData {
Ogre::String name;
Blackboard prereq;
Blackboard effects;
int cost;
};
struct ActionData actionData[] = {
#if 0
{ "WalkTo",
{ { { "at_object", 0 } } },
{ { { "at_object", 1 } } },
1000 },
{ "Sit#",
{ { { "at_object", 1 }, { "is_seated", 0 } } },
{ { { "is_seated", 1 } } },
10 },
#endif
{ "EatFoodSeated",
{ { { "have_food", 1 },
{ "is_seated", 1 },
{ "hungry", 1 } } },
{ { { "healthy", 1 }, { "hungry", 0 } } },
10 },
{ "EatFood",
{ { { "have_food", 1 }, { "hungry", 1 } } },
{ { { "healthy", 1 }, { "hungry", 0 } } },
2000 },
{ "DrinkWaterSeated",
{ { { "have_water", 1 },
{ "is_seated", 1 },
{ "thirsty", 1 } } },
{ { { "thirsty", 0 } } },
10 },
#if 0
{ "DrinkWater",
{ { { "have_water", 1 }, { "thirsty", 1 } } },
{ { { "thirsty", 0 } } },
2000 },
#endif
{ "EatMedicine",
{ { { "have_medicine", 1 }, { "healty", 0 } } },
{ { { "healthy", 1 } } },
100 },
{ "UseToilet",
{ { { "toilet", 1 } } },
{ { { "toilet", 0 } } },
100 },
{ "GetFood",
{ { { "have_food", 0 } } },
{ { { "have_food", 1 } } },
1000 },
{ "GetWater",
{ { { "have_water", 0 } } },
{ { { "have_water", 1 } } },
1000 },
{ "GetMedicine",
{ { { "have_medicine", 0 } } },
{ { { "have_medicine", 1 } } },
1000 },
};
for (const auto &adata : actionData)
ai.actions.push_back(
OGRE_NEW goap::BaseAction<Blackboard>(
adata.name, adata.prereq, adata.effects,
adata.cost));
ai.planner = std::make_shared<goap::BasePlanner<
Blackboard, goap::BaseAction<Blackboard> > >();
});
ecs.system<TownAI, TownNPCs>("CreateBlackboards")
.kind(flecs::OnUpdate)
.each([this](flecs::entity town, TownAI &ai,
const TownNPCs &npcs) {
Ogre::Root::getSingleton().getWorkQueue()->addTask(
[this, town, npcs, &ai]() {
Ogre::Root::getSingleton()
.getWorkQueue()
->addMainThreadTask([this, town,
npcs,
&ai]() {
std::lock_guard<
std::mutex>
lock(ecs_mutex);
createBlackboards(
town, npcs, ai);
});
});
});
ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateDynamicActions")
.kind(flecs::OnUpdate)
.each([](flecs::entity e, ActionNodeList &alist, TownAI &ai,
TownNPCs &npcs) {
std::lock_guard<std::mutex> lock(ecs_mutex);
if (ai.nodeActions.size() > 0)
return;
if (alist.dynamicNodes.size() == 0)
ECS::get_mut<ActionNodeList>()
.updateDynamicNodes();
OgreAssert(alist.dynamicNodes.size() > 0,
"bad dynamic nodes");
int nodeIndex;
for (nodeIndex = 0;
nodeIndex < alist.dynamicNodes.size();
nodeIndex++) {
ActionNodeActions aactions(
nodeIndex,
Blackboard(
{ nlohmann::json::object() }),
10);
ai.nodeActions[nodeIndex] =
aactions.getActions();
OgreAssert(ai.nodeActions[nodeIndex].size() > 0,
"bad action count");
}
OgreAssert(ai.nodeActions.size() > 0,
"no dynamic actions?");
});
ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateDynamicNodes")
.kind(flecs::OnUpdate)
.interval(0.1f)
.each([this](flecs::entity town, ActionNodeList &alist,
TownAI &ai, TownNPCs &npcs) {
std::lock_guard<std::mutex> lock(ecs_mutex);
ECS::get_mut<ActionNodeList>().updateDynamicNodes();
});
ecs.system<TownNPCs>("UpdateNPCPositions")
.kind(flecs::OnUpdate)
.each([](flecs::entity e, TownNPCs &npcs) {
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end();
it++) {
auto &npc = npcs.npcs.at(it->first);
if (npc.e.is_valid() &&
npc.e.has<CharacterBase>())
npc.position =
npc.e.get<CharacterBase>()
.mBodyNode
->_getDerivedPosition();
}
});
ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateBlackboards")
.kind(flecs::OnUpdate)
.interval(0.1f)
.each([this](flecs::entity town, ActionNodeList &alist,
TownAI &ai, const TownNPCs &npcs) {
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end();
it++) {
const auto &npc = npcs.npcs.at(it->first);
if (ai.blackboards.find(it->first) ==
ai.blackboards.end())
continue;
ai.blackboards.at(it->first).setPosition(
npc.position);
}
Ogre::Root::getSingleton().getWorkQueue()->addTask(
[this, town, &alist, npcs, &ai]() {
{
std::lock_guard<std::mutex> lock(
ecs_mutex);
alist.build();
updateBlackboardsBits(
town, alist, npcs, ai);
updateBlackboards(town, alist,
npcs, ai);
}
Ogre::Root::getSingleton()
.getWorkQueue()
->addMainThreadTask([this, town,
&alist]() {
town.modified<TownAI>();
ECS::modified<
ActionNodeList>();
});
});
});
#if 1
ecs.system<TownAI, TownNPCs>("PlanAI")
.kind(flecs::OnUpdate)
.interval(0.5f)
.each([&](flecs::entity town, TownAI &ai,
const TownNPCs &npcs) {
Ogre::Root::getSingleton().getWorkQueue()->addTask(
[this, town, npcs, &ai]() {
std::lock_guard<std::mutex> lock(
ecs_mutex);
buildPlans(town, npcs, ai);
Ogre::Root::getSingleton()
.getWorkQueue()
->addMainThreadTask([this,
town]() {
town.modified<TownAI>();
});
});
});
#endif
}
void CharacterAIModule::createAI(flecs::entity town)
{
town.add<TownAI>();
}
void CharacterAIModule::buildPlans(flecs::entity town, const TownNPCs &npcs,
TownAI &ai)
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &bb = ai.blackboards.at(it->first);
/* if there are plans, skip until these get discarded */
if (ai.plans.find(it->first) != ai.plans.end() &&
ai.plans.at(it->first).size() > 0)
continue;
const auto &npc = npcs.npcs.at(it->first);
int index = it->first;
ai.plans[index] = {};
for (const auto &goal : ai.goals) {
if (goal.is_reached(bb))
continue;
struct TownAI::Plan plan;
plan.goal = &goal;
#if 0
std::cout << "blackboard: "
<< bb.stats.dump(4)
<< std::endl;
std::cout << "goal: "
<< goal.goal.stats.dump(4)
<< std::endl;
#endif
std::vector<goap::BaseAction<Blackboard> *> path;
int actionCount =
ai.blackboards.at(it->first).getActionsCount();
auto actionsData =
ai.blackboards.at(it->first).getActionsData();
path.resize(actionCount * actionCount);
int path_length = ai.planner->plan(
bb, goal, actionsData, actionCount, path.data(),
path.size());
#if 0
std::cout << "Actions: " << std::endl;
for (auto &action : actions) {
std::cout << "name: "
<< action->get_name()
<< std::endl;
std::cout
<< "\tprereq:\n"
<< action->prereq.stats
.dump(4)
<< std::endl;
std::cout
<< "\teffects:\n"
<< action->effects.stats
.dump(4)
<< std::endl;
}
#endif
#if 1
std::cout << bb.index << " ";
std::cout << "Goal: " << goal.get_name();
std::cout << std::endl;
std::cout << "Path: ";
int count = 0;
if (path_length < 0) {
std::cout << "Bad plan " << path_length
<< std::endl;
}
for (auto &action : path) {
if (count >= path_length)
break;
OgreAssert(action, "No action");
std::cout << action->get_name();
if (count < path_length - 1)
std::cout << ", ";
count++;
}
std::cout << std::endl;
std::cout << path_length << std::endl;
// OgreAssert(path_length == 0,
// "planning");
#endif
if (path_length > 0) {
plan.goal = &goal;
plan.plan.insert(plan.plan.end(), path.begin(),
path.begin() + path_length);
ai.plans[it->first].push_back(plan);
break;
}
if (path_length > 0)
OgreAssert(false, "plan");
}
}
}
void CharacterAIModule::createBlackboards(flecs::entity town,
const TownNPCs &npcs, TownAI &ai)
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end()) {
int strength = 10;
int dexterity = 10;
int health = 100;
int stamina = 100;
int sex = it->second.props["sex"].get<int>();
if (sex == 0) { // male
strength += 10;
dexterity += 10;
}
nlohmann::json bb;
bb["strength"] = strength;
bb["dexterity"] = dexterity;
bb["health"] = health;
bb["stamina"] = stamina;
bb["needs_hunger"] = 0;
bb["needs_thirst"] = 0;
bb["needs_toilet"] = 0;
bb["have_water"] = 0;
bb["have_food"] = 0;
bb["have_medicine"] = 0;
bb["at_object"] = 0;
ai.blackboards[it->first] = Blackboard(bb);
ai.blackboards[it->first].index = it->first;
ai.blackboards.at(it->first).town = town;
// FIXME: do this once
ai.blackboards.at(it->first).actionRefResize(0);
ai.blackboards.at(it->first).actionRefAddActions(
ai.actions);
}
}
}
void CharacterAIModule::updateBlackboardsBits(flecs::entity town,
ActionNodeList &alist,
const TownNPCs &npcs, TownAI &ai)
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
struct UpdateBit {
Ogre::String checkValue;
int recover;
int minValue;
int maxValue;
int threshold;
Ogre::String writeValue;
};
struct UpdateBit updateBits[] = {
{ "health", 0, 0, 100, 20, "healthy" },
{ "needs_hunger", 1, 0, 10000, 2000, "hungry" },
{ "needs_thirst", 1, 0, 10000, 1000, "thirsty" },
{ "needs_toilet", 1, 0, 10000, 1500, "toilet" }
};
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &stats = ai.blackboards.at(it->first).stats;
auto &object = ai.blackboards.at(it->first).object;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
for (const auto &bits : updateBits) {
int value = stats[bits.checkValue].get<int>();
int maxValue = bits.maxValue;
int minValue = bits.minValue;
int threshold = bits.threshold;
if (it->second.props.find(bits.checkValue + "_max") !=
it->second.props.end())
maxValue =
it->second
.props[bits.checkValue + "_max"]
.get<int>();
if (it->second.props.find(bits.checkValue +
"_threshold") !=
it->second.props.end())
threshold = it->second
.props[bits.checkValue +
"_threshold"]
.get<int>();
value += bits.recover;
if (value > maxValue)
value = maxValue;
if (value >= threshold)
stats[bits.writeValue] = 1;
else
stats[bits.writeValue] = 0;
if (value < bits.minValue)
value = bits.minValue;
stats[bits.checkValue] = value;
}
}
}
void CharacterAIModule::updateBlackboards(flecs::entity town,
const ActionNodeList &alist,
const TownNPCs &npcs, TownAI &ai)
{
std::lock_guard<std::mutex> lock(*ai.mutex);
OgreAssert(town.is_valid(), "Bad town entity");
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &stats = ai.blackboards.at(it->first).stats;
auto &object = ai.blackboards.at(it->first).object;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
auto &bb = ai.blackboards.at(it->first);
bb.query_ai();
#if 0
OgreAssert(nodeActionCount > 0 ||
points.size() == 0,
"no node actions and no points");
if (nodeActionCount == 0) {
std::cout << "nodes:"
<< alist.nodes.size() << " "
<< alist.dynamicNodes.size()
<< std::endl;
std::cout << "points: " << points.size()
<< std::endl;
std::cout << "position: " << position
<< std::endl;
}
OgreAssert(nodeActionCount > 0,
"no node actions");
#endif
bb.fixupBooleanKeys();
}
}
void Blackboard::_actionRefResize(int count)
{
if (count >= actionRef.size()) {
int allocate = count;
if (allocate < 1000)
allocate = 1000;
actionRef.resize(allocate);
}
OgreAssert(count < actionRef.size(), "out of memory");
actionRefCount = count;
actionRefPtr = count;
}
Blackboard::Blackboard()
: stats(nlohmann::json::object())
, object(-1)
, index(-1)
, actionRefCount(0)
, mutex(std::make_shared<std::mutex>())
{
}
Blackboard::Blackboard(const nlohmann::json &stats)
: Blackboard()
{
this->stats = stats;
}
bool Blackboard::operator==(const Blackboard &other) const
{
return is_satisfied_by(this->stats, other.stats);
}
bool Blackboard::operator!=(const Blackboard &other) const
{
return !(*this == other);
}
void Blackboard::apply(const Blackboard &other)
{
std::lock_guard<std::mutex> lock(*mutex);
stats.update(other.stats);
}
Ogre::String Blackboard::dumpActions()
{
std::lock_guard<std::mutex> lock(*mutex);
Ogre::String ret;
ret += "Actions:\n";
int count = 0;
for (count = 0; count < actionRefCount; count++) {
auto &action = actionRef[count];
ret += "name: " + action->get_name() + "\n";
ret += "\tprereq:\n" + action->prereq.stats.dump(4) + "\n";
ret += "\teffects:\n" + action->effects.stats.dump(4) + "\n";
}
return ret;
}
void Blackboard::printActions()
{
std::cout << dumpActions() << std::endl;
}
void Blackboard::fixupBooleanKeys()
{
std::lock_guard<std::mutex> lock(*mutex);
int count;
for (count = 0; count < actionRefCount; count++) {
auto &action = actionRef[count];
const nlohmann::json &prereq = action->prereq.stats;
const nlohmann::json &effects = action->effects.stats;
for (auto it = prereq.begin(); it != prereq.end(); it++)
if (stats.find(it.key()) == stats.end())
stats[it.key()] = 0;
for (auto it = effects.begin(); it != effects.end(); it++)
if (stats.find(it.key()) == stats.end())
stats[it.key()] = 0;
}
}
void Blackboard::actionRefResize(int count)
{
std::lock_guard<std::mutex> lock(*mutex);
_actionRefResize(count);
}
void Blackboard::actionRefAddAction(goap::BaseAction<Blackboard> *action)
{
std::lock_guard<std::mutex> lock(*mutex);
if (actionRef.size() <= actionRefCount + 16)
actionRef.resize(actionRefCount + 32);
OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
OgreAssert(action, "bad action");
actionRef[actionRefPtr++] = action;
actionRefCount++;
}
void Blackboard::actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &actions)
{
std::lock_guard<std::mutex> lock(*mutex);
_actionRefAddActions(actions);
}
void Blackboard::_actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &actions)
{
if (actionRef.size() <= actionRefCount + actions.size())
actionRef.resize(actionRefCount + actions.size() * 2);
for (const auto &action : actions) {
OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
OgreAssert(action, "bad action");
actionRef[actionRefPtr++] = action;
actionRefCount++;
}
}
void Blackboard::actionRefAddActions(goap::BaseAction<Blackboard> **actions,
int count)
{
int i;
std::lock_guard<std::mutex> lock(*mutex);
if (actionRef.size() <= actionRefCount + count)
actionRef.resize(actionRefCount + count * 2);
for (i = 0; i < count; i++) {
OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
OgreAssert(actions[i], "bad action");
actionRef[actionRefPtr++] = actions[i];
actionRefCount++;
}
}
struct ComparePair {
const nlohmann::json &current;
const nlohmann::json &target;
};
bool Blackboard::is_satisfied_by(const nlohmann::json &current,
const nlohmann::json &target, float epsilon)
{
std::deque<ComparePair> queue;
queue.push_back({ current, target });
while (!queue.empty()) {
ComparePair pair = queue.front();
queue.pop_front();
const nlohmann::json &curr = pair.current;
const nlohmann::json &tgt = pair.target;
if (curr.type() != tgt.type() &&
!(curr.is_number() && tgt.is_number()))
return false;
if (tgt.is_object())
for (auto it = tgt.begin(); it != tgt.end(); ++it) {
auto found = curr.find(it.key());
if (found == curr.end())
return false;
queue.push_back({ *found, it.value() });
}
else if (tgt.is_array()) {
if (curr.size() != tgt.size())
return false;
for (int i = 0; i < tgt.size(); ++i)
queue.push_back({ curr[i], tgt[i] });
} else if (tgt.is_number_float() || curr.is_number_float()) {
if (std::abs(curr.get<float>() - tgt.get<float>()) >=
epsilon)
return false;
} else if (curr != tgt)
return false;
}
return true;
}
int Blackboard::distance_to(const Blackboard &goal) const
{
int distance = 0;
OgreAssert(goal.stats.is_object(),
"Not an object:\n" + goal.stats.dump(4));
for (auto it = goal.stats.begin(); it != goal.stats.end(); ++it) {
const std::string &key = it.key();
const auto &goalVal = it.value();
// If current state doesn't have the key, treat it as a maximum difference
if (stats.find(key) == stats.end()) {
distance += 100; // Example: High cost for missing state
continue;
}
const auto &currentVal = stats[key];
if (goalVal.is_number() && currentVal.is_number()) {
// Add numerical difference
distance += std::abs(goalVal.get<float>() -
currentVal.get<float>());
} else {
// Check non-numeric equality
if (goalVal != currentVal) {
distance += 1; // Penalty for mismatch
}
}
}
return distance;
}
void Blackboard::setPosition(const Ogre::Vector3 &position)
{
std::lock_guard<std::mutex> lock(*mutex);
this->position = position;
}
void Blackboard::query_ai()
{
std::lock_guard<std::mutex> lock(*mutex);
TownAI &ai = town.get_mut<TownAI>();
const TownNPCs &npcs = town.get<TownNPCs>();
const float distance = 10000.0f;
Ogre::Vector3 position(0, 0, 0);
if (npcs.npcs.at(index).e.is_valid())
position = npcs.npcs.at(index)
.e.get<CharacterBase>()
.mBodyNode->_getDerivedPosition();
else
from_json(npcs.npcs.at(index).props["position"], position);
this->position = position;
ActionNodeList &alist = ECS::get_mut<ActionNodeList>();
alist.query_ai(position, distance, points, distances);
_actionRefResize(ai.actions.size());
int actionRefIndex = ai.actions.size();
int nodeActionCount = 0;
for (size_t point : points) {
Ogre::Vector3 &p = alist.dynamicNodes[point].position;
float radius = alist.dynamicNodes[point].radius;
float distance = p.squaredDistance(position);
if (object >= 0 && (size_t)object == point &&
distance > radius * radius) {
object = -1;
stats["at_object"] = 0;
} else if (object >= 0 && (size_t)object == point &&
distance <= radius * radius)
stats["at_object"] = 1;
/* some nodes do not have usable actions */
if (ai.nodeActions[point].size() > 0) {
OgreAssert(ai.nodeActions[point].size() > 0,
"bad node actions count " +
alist.dynamicNodes[point].props.dump(
4));
_actionRefAddActions(ai.nodeActions[point]);
nodeActionCount += ai.nodeActions[point].size();
}
nlohmann::json nodes = nlohmann::json::array();
nlohmann::json j = alist.dynamicNodes[point].props;
j["global_position_x"] = p.x;
j["global_position_y"] = p.y;
j["global_position_z"] = p.z;
nodes.push_back(j);
stats["nodes"] = nodes;
}
}
}

View File

@@ -0,0 +1,101 @@
#ifndef CHARACTERAIMODULE_H
#define CHARACTERAIMODULE_H
#include <vector>
#include <flecs.h>
#include <nlohmann/json.hpp>
#include "goap.h"
namespace ECS
{
struct Blackboard {
nlohmann::json stats;
int object;
int index;
flecs::entity town;
std::shared_ptr<std::mutex> mutex;
private:
std::vector<goap::BaseAction<Blackboard> *> actionRef;
int actionRefCount;
int actionRefPtr;
Ogre::Vector3 position;
std::vector<size_t> points;
std::vector<float> distances;
void _actionRefResize(int count);
void _actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &actions);
public:
Blackboard();
Blackboard(const nlohmann::json &stats);
bool operator==(const Blackboard &other) const;
bool operator!=(const Blackboard &other) const;
void apply(const Blackboard &other);
Ogre::String dumpActions();
void printActions();
void fixupBooleanKeys();
void actionRefResize(int count);
void actionRefAddAction(goap::BaseAction<Blackboard> *action);
void actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &actions);
void actionRefAddActions(goap::BaseAction<Blackboard> **actions,
int count);
goap::BaseAction<Blackboard> **getActionsData()
{
return actionRef.data();
}
int getActionsCount()
{
return actionRefCount;
}
private:
static bool is_satisfied_by(const nlohmann::json &current,
const nlohmann::json &target,
float epsilon = 1e-4);
public:
int distance_to(const Blackboard &goal) const;
void setPosition(const Ogre::Vector3 &position);
const Ogre::Vector3 &getPosition() const
{
return position;
}
void query_ai();
};
struct TownNPCs;
struct ActionNodeList;
struct TownAI {
std::shared_ptr<std::mutex> mutex;
std::vector<goap::BasePlanner<Blackboard,
goap::BaseAction<Blackboard> >::BaseGoal>
goals;
std::vector<goap::BaseAction<Blackboard> *> actions;
std::shared_ptr<
goap::BasePlanner<Blackboard, goap::BaseAction<Blackboard> > >
planner;
std::unordered_map<int, Blackboard> blackboards;
struct Plan {
const goap::BasePlanner<Blackboard,
goap::BaseAction<Blackboard> >::BaseGoal *goal;
std::vector<goap::BaseAction<Blackboard> *> plan;
};
std::unordered_map<int, std::vector<struct Plan> > plans;
std::unordered_map<int, std::vector<goap::BaseAction<Blackboard> *> > nodeActions;
};
struct CharacterAIModule {
CharacterAIModule(flecs::world &ecs);
void createAI(flecs::entity town);
void buildPlans(flecs::entity town, const TownNPCs &npcs, TownAI &ai);
void createBlackboards(flecs::entity town, const TownNPCs &npcs, TownAI &ai);
void updateBlackboardsBits(flecs::entity town, ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai);
void updateBlackboards(flecs::entity town, const ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai);
};
}
#endif // CHARACTERAIMODULE_H

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

@@ -12,18 +12,19 @@
namespace ECS
{
void createNPCActionNodes(flecs::entity town, flecs::entity e, int index)
void createNPCActionNodes(flecs::entity town, int index)
{
NPCActionNodes &anodes = e.get_mut<NPCActionNodes>();
const TownNPCs &npcs = town.get<TownNPCs>();
nlohmann::json npcprops = npcs.npcs.at(index).props;
TownNPCs &npcs = town.get_mut<TownNPCs>();
TownNPCs::NPCData &npc = npcs.npcs.at(index);
flecs::entity e = npc.e;
nlohmann::json npcprops = npc.props;
const CharacterBase &ch = e.get<CharacterBase>();
Ogre::Vector3 characterPos = ch.mBodyNode->_getDerivedPosition();
Ogre::Quaternion characterRot = ch.mBodyNode->_getDerivedOrientation();
if (anodes.anodes.size() > 0) {
if (npc.actionNodes.size() > 0) {
int i;
for (i = 0; i < anodes.anodes.size(); i++) {
auto &anode = anodes.anodes[i];
for (i = 0; i < npc.actionNodes.size(); i++) {
auto &anode = npc.actionNodes[i];
Ogre::Vector3 offset = Ogre::Vector3::UNIT_Z * 0.3f +
Ogre::Vector3::UNIT_Y;
if (i == 1)
@@ -34,7 +35,6 @@ void createNPCActionNodes(flecs::entity town, flecs::entity e, int index)
to_json(anode.props["position"], anode.position);
to_json(anode.props["rotation"], anode.rotation);
}
e.modified<NPCActionNodes>();
return;
}
{
@@ -57,7 +57,7 @@ void createNPCActionNodes(flecs::entity town, flecs::entity e, int index)
anode.props["town"] = town.id();
anode.props["index"] = index;
anode.props["npc"] = npcprops;
anodes.anodes.push_back(anode);
npc.actionNodes.push_back(anode);
}
{
ActionNodeList::ActionNode anode;
@@ -78,9 +78,8 @@ void createNPCActionNodes(flecs::entity town, flecs::entity e, int index)
anode.props["town"] = town.id();
anode.props["index"] = index;
anode.props["npc"] = npcprops;
anodes.anodes.push_back(anode);
npc.actionNodes.push_back(anode);
}
e.modified<NPCActionNodes>();
}
CharacterManagerModule::CharacterManagerModule(flecs::world &ecs)
{
@@ -92,16 +91,10 @@ CharacterManagerModule::CharacterManagerModule(flecs::world &ecs)
ecs.component<TownCharacterHolder>();
ecs.component<TownNPCs>();
ecs.component<LivesIn>();
ecs.component<NPCActionNodes>().on_add(
[](flecs::entity e, NPCActionNodes &anodes) {
anodes.anodes.clear();
});
ecs.system<TerrainItem, TownNPCs>("UpdateCharacters")
.immediate()
.kind(flecs::OnUpdate)
.interval(1.0f)
.write<CharacterBase>()
.write<NPCActionNodes>()
.write<CharacterLocation>()
.write<CharacterConf>()
.write<Character>()
@@ -112,73 +105,72 @@ CharacterManagerModule::CharacterManagerModule(flecs::world &ecs)
return;
if (!player.has<CharacterBase>())
return;
ECS::get().defer_suspend();
Ogre::Vector3 cameraPos =
player.get<CharacterBase>()
.mBodyNode->_getDerivedPosition();
for (auto &npc : npcs.npcs) {
int index = npc.first;
TownNPCs::NPCData &data = npc.second;
Ogre::Vector3 npcPosition = data.position;
Ogre::Quaternion npcOrientation =
data.orientation;
if (cameraPos.squaredDistance(npcPosition) <
10000.0f) {
if (!data.e.is_valid()) {
data.e = createCharacterData(
data.model,
data.position,
data.orientation);
data.e.add<LivesIn>(town);
break;
Ogre::Root::getSingleton().getWorkQueue()->addMainThreadTask([this,
town]() {
flecs::entity player =
ECS::get<CharacterManagerModule>()
.getPlayer();
if (!player.is_valid())
return;
if (!player.has<CharacterBase>())
return;
TownNPCs &npcs = town.get_mut<TownNPCs>();
Ogre::Vector3 cameraPos =
player.get<CharacterBase>()
.mBodyNode
->_getDerivedPosition();
for (auto &npc : npcs.npcs) {
int index = npc.first;
TownNPCs::NPCData &data = npc.second;
Ogre::Vector3 npcPosition =
data.position;
Ogre::Quaternion npcOrientation =
data.orientation;
if (cameraPos.squaredDistance(
npcPosition) < 10000.0f) {
if (!data.e.is_valid()) {
data.e = createCharacterData(
data.model,
data.position,
data.orientation);
data.e.add<LivesIn>(
town);
break;
}
}
if (cameraPos.squaredDistance(
npcPosition) > 22500.0f) {
if (data.e.is_valid()) {
data.e.destruct();
data.e =
flecs::entity();
break;
}
}
}
if (cameraPos.squaredDistance(npcPosition) >
22500.0f) {
if (data.e.is_valid()) {
data.e.destruct();
data.e = flecs::entity();
break;
for (auto &npc : npcs.npcs) {
int index = npc.first;
TownNPCs::NPCData &data = npc.second;
Ogre::Vector3 npcPosition =
data.position;
Ogre::Quaternion npcOrientation =
data.orientation;
if (cameraPos.squaredDistance(
npcPosition) < 10000.0f) {
if (data.e.is_valid()) {
if (data.e.has<
CharacterBase>() &&
data.e.has<LivesIn>(
town))
createNPCActionNodes(
town,
index);
}
}
}
}
ECS::get().defer_resume();
});
ecs.system<TerrainItem, TownNPCs>("UpdateCharacters2")
.immediate()
.kind(flecs::OnUpdate)
.write<CharacterBase>()
.write<NPCActionNodes>()
.write<CharacterLocation>()
.write<CharacterConf>()
.write<Character>()
.write<LivesIn>()
.each([this](flecs::entity town, TerrainItem &item,
TownNPCs &npcs) {
if (!player.is_valid())
return;
if (!player.has<CharacterBase>())
return;
Ogre::Vector3 cameraPos =
player.get<CharacterBase>()
.mBodyNode->_getDerivedPosition();
for (auto &npc : npcs.npcs) {
int index = npc.first;
TownNPCs::NPCData &data = npc.second;
Ogre::Vector3 npcPosition = data.position;
Ogre::Quaternion npcOrientation =
data.orientation;
if (cameraPos.squaredDistance(npcPosition) <
10000.0f) {
if (data.e.is_valid()) {
if (data.e.has<CharacterBase>() &&
data.e.has<LivesIn>(town))
createNPCActionNodes(
town, data.e,
index);
}
}
}
town.modified<TownNPCs>();
});
});
}
flecs::entity
@@ -191,11 +183,10 @@ CharacterManagerModule::createPlayer(const Ogre::Vector3 &position,
player = ECS::get().entity("player");
OgreAssert(player.is_valid(), "Can't create player");
std::cout << "Begin player create" << std::endl;
player.set<CharacterLocation>({ rotation, position })
.set<CharacterConf>({ "normal-male.glb" })
.add<Character>()
// .add<CharacterDisablePhysics>()
.add<Player>();
player.add<Player>();
ECS::get_mut<CharacterModule>().createCharacter(
player, position, rotation, "normal-male.glb");
ECS::modified<CharacterModule>();
std::cout << "End player create" << std::endl;
count++;
return player;
@@ -205,13 +196,10 @@ CharacterManagerModule::createCharacterData(const Ogre::String model,
const Ogre::Vector3 &position,
const Ogre::Quaternion &rotation)
{
flecs::entity e =
ECS::get()
.entity()
.set<CharacterLocation>({ rotation, position })
.set<CharacterConf>({ model })
.add<Character>()
.add<NPCActionNodes>();
flecs::entity e = ECS::get().entity();
ECS::get_mut<CharacterModule>().createCharacter(e, position, rotation,
model);
ECS::modified<CharacterModule>();
return e;
}

View File

@@ -2,6 +2,7 @@
#define _CHARACTER_MANAGER_MODULE_
#include <flecs.h>
#include <nlohmann/json.hpp>
#include "PlayerActionModule.h"
namespace ECS
{
struct TownCharacterHolder{int index;};
@@ -12,6 +13,7 @@ struct TownNPCs {
Ogre::Vector3 position;
Ogre::Quaternion orientation;
Ogre::String model;
std::vector<ActionNodeList::ActionNode> actionNodes;
};
std::map<int, NPCData> npcs;

View File

@@ -9,7 +9,6 @@
#include "CharacterAnimationModule.h"
#include "CharacterManagerModule.h"
#include "CharacterModule.h"
#include "goap.h"
namespace ECS
{
CharacterModule::CharacterModule(flecs::world &ecs)
@@ -26,12 +25,9 @@ CharacterModule::CharacterModule(flecs::world &ecs)
ecs.component<CharacterDisablePhysics>();
ecs.component<CharacterUpdatePhysicsState>();
ecs.component<CharacterInActuator>();
ecs.component<Blackboard>();
ecs.component<ActionTarget>();
ecs.component<Plan>();
ecs.component<Male>();
ecs.component<Female>();
ecs.component<Planner>().add(flecs::Singleton);
ecs.component<CharacterControlDisable>();
ecs.import <CharacterAnimationModule>();
ecs.import <TerrainModule>();
ecs.import <WaterModule>();
@@ -136,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();
@@ -293,29 +228,6 @@ CharacterModule::CharacterModule(flecs::world &ecs)
anim.configured = false;
});
ecs.observer<const CharacterLocation, const CharacterConf>(
"SetupCharacterObs")
.event(flecs::OnSet)
.with<Character>()
.without<CharacterBase>()
.without<AnimationControl>()
.write<CharacterBase>()
.write<AnimationControl>()
.each([&](flecs::entity e, const CharacterLocation &loc,
const CharacterConf &conf) {
std::cout << "OBSERVER!!!"
<< " " << e.id() << std::endl;
if (e.has<CharacterBase>() || e.has<AnimationControl>())
return;
e.world().defer_begin();
e.add<CharacterBase>();
e.add<AnimationControl>();
e.set<CharacterVelocity>(
{ { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } });
e.add<CharacterGravity>();
e.add<CharacterBuoyancy>();
e.world().defer_end();
});
#if 0
ecs.system<const EngineData, const CharacterLocation,
const CharacterConf, Body2Entity>("SetupCharacter")
@@ -631,182 +543,108 @@ void CharacterModule::updateCameraGoal(Camera &camera, Ogre::Real deltaYaw,
}
}
}
CharacterAIModule::CharacterAIModule(flecs::world &ecs)
{
ecs.module<CharacterAIModule>();
ecs.system<Blackboard>("UpdateCharacters")
.kind(flecs::OnUpdate)
.each([&](flecs::entity e, Blackboard &bb) {
bb.flags &=
~(Blackboard::LOW_HEALTH |
Blackboard::FULL_HEALTH |
Blackboard::LOW_STAMINA |
Blackboard::FULL_STAMINA |
Blackboard::LOW_LUST | Blackboard::HIGH_LUST |
Blackboard::FULL_LUST);
if (bb.health < 5)
bb.flags |= Blackboard::LOW_HEALTH;
else if (bb.health >= 100)
bb.flags |= Blackboard::FULL_HEALTH;
if (bb.stamina < 5)
bb.flags |= Blackboard::LOW_STAMINA;
if (bb.stamina >= 100)
bb.flags |= Blackboard::FULL_STAMINA;
if (bb.lust >= 100)
bb.flags |= Blackboard::FULL_LUST;
if (bb.lust > 10)
bb.flags |= Blackboard::HIGH_LUST;
if (bb.lust < 5)
bb.flags |= Blackboard::LOW_LUST;
if (bb.stamina < 0)
bb.stamina = 0;
else if (bb.stamina > 100)
bb.stamina = 100;
if (bb.lust < 0)
bb.lust = 0;
else if (bb.lust > 100)
bb.lust = 100;
});
ecs.system<Blackboard, Plan>("UpdateCharactersPlan2")
.kind(flecs::OnUpdate)
.each([&](flecs::entity e, Blackboard &bb, Plan &p) {
int i;
bool ok_plan = true;
for (i = p.position; i < p.length; i++) {
if (!p.actions[i]->can_run(bb, true)) {
ok_plan = false;
break;
}
int ret = p.actions[i]->execute(bb);
p.position = i;
if (ret == BaseAction::BUSY)
break;
else if (ret == BaseAction::ABORT) {
ok_plan = false;
break;
}
if (ret == BaseAction::OK && i == p.length - 1)
ok_plan = false;
// stop_this = true;
}
if (!ok_plan) {
std::cout << e.name() << ": invalidated plan"
<< " step: " << i << std::endl;
for (i = 0; i < p.length; i++)
p.actions[i]->stop(bb);
e.remove<Plan>();
}
});
ecs.system<Blackboard, Planner>("UpdateCharacters2")
.kind(flecs::OnUpdate)
.without<Plan>()
.each([&](flecs::entity e, Blackboard &bb, Planner &planner) {
int i;
std::vector<BaseAction *> actions;
actions.insert(actions.end(), planner.actions.begin(),
planner.actions.end());
e.world()
.query_builder<const ActionTarget>()
.build()
.each([&](flecs::entity me,
const ActionTarget &t) {
actions.insert(actions.end(),
t.actions.begin(),
t.actions.end());
#if 0
auto it = t.actions.begin();
while (it != t.actions.end()) {
if (me != bb.me)
actions.push_back(*it);
// std::cout << (*it)->get_name()
// << std::endl;
it++;
}
#endif
});
#if 0
for (i = 0; i < actions.size(); i++)
std::cout << "action: " << i << " "
<< actions[i]->get_name()
<< std::endl;
#endif
if (actions.size() == 0)
return;
int len = -1;
OgreAssert(
bb.stamina < 100 ||
bb.stamina >= 100 &&
!bb.get_flag(
Blackboard::LOW_STAMINA),
"bad thing");
for (i = 0; i < planner.goals.size(); i++) {
if (planner.goals[i]->distance_to(bb) > 1000000)
continue;
len = planner.planner.plan(
bb, *planner.goals[i], actions.data(),
actions.size(), planner.path.data(),
100);
std::cout << bb.me.name() << ": goal: " << len
<< " " << planner.goals[i]->get_name()
<< std::endl;
if (len > 0)
break;
}
// std::cout << "plan length: " << len << std::endl;
#if 0
if (len > 0)
stop_this = true;
if (len < 0)
stop_this = true;
#endif
if (len > 0) {
Plan &p = e.ensure<Plan>();
p.actions = planner.path;
p.position = 0;
p.length = len;
for (i = 0; i < len; i++)
std::cout
<< i << " "
<< planner.path[i]->get_name()
<< " "
<< planner.path[i]->get_cost(bb)
<< std::endl;
bool ok_plan = true;
for (i = 0; i < len; i++) {
if (!planner.path[i]->can_run(bb,
true)) {
ok_plan = false;
break;
}
int ret = planner.path[i]->execute(bb);
p.position = i;
std::cout << "exec: " << i << " "
<< planner.path[i]->get_name()
<< std::endl;
if (ret == BaseAction::BUSY)
break;
else if (ret == BaseAction::ABORT) {
ok_plan = false;
} else if (ret == BaseAction::OK)
std::cout
<< "exec: complete "
<< i << " "
<< planner.path[i]
->get_name()
<< std::endl;
}
e.modified<Plan>();
if (!ok_plan) {
std::cout << e.name()
<< ": invalidate plan"
<< " step: " << i
<< std::endl;
for (i = 0; i < len; i++)
planner.path[i]->stop(bb);
e.remove<Plan>();
}
}
});
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,
const Ogre::String model)
{
if (e.has<CharacterBase>() || e.has<AnimationControl>())
return;
e.set<CharacterLocation>({ rotation, position });
e.set<CharacterConf>({ model });
e.add<CharacterBase>();
e.add<AnimationControl>();
e.set<CharacterVelocity>(
{ { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } });
e.add<CharacterGravity>();
e.add<CharacterBuoyancy>();
e.add<Character>();
}
}

View File

@@ -2,7 +2,7 @@
#define CHARACTER_MODULE_H_
#include <flecs.h>
#include <Ogre.h>
#include "goap.h"
#include "Components.h"
namespace ECS
{
struct Camera;
@@ -35,30 +35,16 @@ struct CharacterConf {
};
struct CharacterInActuator {
Ogre::String animationState;
Vector3 prevMotion;
Vector3 prevMotion;
};
struct ActionTarget {
std::vector<BaseAction *> actions;
};
struct Plan {
std::vector<BaseAction *> actions;
int position;
int length;
};
struct Planner {
BasePlanner<ECS::Blackboard, BaseAction> planner;
std::vector<BaseAction *> path;
std::vector<BasePlanner<ECS::Blackboard, BaseAction>::BaseGoal *> goals;
std::vector<BaseAction *> actions;
};
struct CharacterControlDisable {};
struct CharacterModule {
CharacterModule(flecs::world &ecs);
void updateCameraGoal(Camera &camera, Ogre::Real deltaYaw,
Ogre::Real deltaPitch, Ogre::Real deltaZoom);
};
struct CharacterAIModule {
CharacterAIModule(flecs::world &ecs);
void createCharacter(flecs::entity e, const Ogre::Vector3 &position,
const Ogre::Quaternion &rotation,
const Ogre::String model);
};
}
#endif

View File

@@ -89,7 +89,8 @@ struct GUIListener : public Ogre::RenderTargetListener {
}
void buttons_panel()
{
ImVec2 size = ImGui::GetMainViewport()->Size;
bool enableDebugRender = ECS::get<EngineData>().enableDbgDraw;
ImVec2 size = ImGui::GetMainViewport()->Size;
float window_width = size.x * 0.2f;
if (window_width > panel_width)
window_width = panel_width;
@@ -115,7 +116,14 @@ struct GUIListener : public Ogre::RenderTargetListener {
if (enableMapEditor)
enableEditor = false;
}
ImGui::Text("Text message...");
if (ImGui::Checkbox("Enable physics debug",
&enableDebugRender)) {
ECS::get_mut<EngineData>().enableDbgDraw =
enableDebugRender;
ECS::modified<EngineData>();
PhysicsModule::setDebugDraw(enableDebugRender);
}
ImGui::Text("Text message...");
ImGui::End();
}
void create_entity_node(const Ogre::String &name, int key)
@@ -543,11 +551,14 @@ 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 = list.points;
std::vector<float> distances = list.distances;
std::vector<size_t> points =
list.getUIData().points;
std::vector<float> distances =
list.getUIData().distances;
Ogre::SceneNode *cameraNode =
ECS::get<Camera>().mCameraNode;
@@ -555,7 +566,7 @@ struct GUIListener : public Ogre::RenderTargetListener {
cameraNode->_getDerivedPosition();
float minDistance = 25.0f;
int i;
list.selected = -1;
list.setUISelected(-1);
for (i = 0; i < points.size(); i++) {
size_t p = points[i];
float distance = distances[i];
@@ -589,9 +600,9 @@ struct GUIListener : public Ogre::RenderTargetListener {
if (hit)
continue;
if (list.selected == -1) {
if (list.getUIData().selected == -1) {
if (distance < actDistance)
list.selected = p;
list.setUISelected(p);
}
ImDrawList *drawList =
ImGui::GetBackgroundDrawList();
@@ -600,7 +611,7 @@ struct GUIListener : public Ogre::RenderTargetListener {
float circleRadius = 8.0f;
ImColor circleColor(
ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
if (p == list.selected) {
if (p == list.getUIData().selected) {
circleRadius = 16.0f;
circleColor = ImColor(
ImVec4(0, 0, 1, 1));
@@ -684,6 +695,7 @@ GUIModule::GUIModule(flecs::world &ecs)
gui.enabled = false;
gui.grab = false;
gui.grabChanged = false;
gui.enableActions = true;
})
.add(flecs::Singleton);
ecs.component<GUIData>()
@@ -693,7 +705,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

@@ -24,6 +24,7 @@
#include "VehicleManagerModule.h"
#include "PlayerActionModule.h"
#include "AppModule.h"
#include "CharacterAIModule.h"
#include "world-build.h"
namespace ECS
@@ -62,6 +63,7 @@ void setupExteriorScene(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode,
ecs.import <WorldMapModule>();
ecs.import <CharacterAnimationModule>();
ecs.import <PlayerActionModule>();
ecs.import <CharacterAIModule>();
ecs.add<ActionNodeList>();
ecs.system<EngineData>("UpdateDelta")
@@ -242,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

@@ -656,6 +656,8 @@ PhysicsModule::PhysicsModule(flecs::world &ecs)
}
// gr.velocity.y = 0.0f;
// v.y = 0.0f;
OgreAssert(v.squaredLength() < 1000.0f,
"shitty velocity setting");
ch->SetLinearVelocity(
JoltPhysics::convert<JPH::Vec3>(v));
gr.velocity = Ogre::Vector3::ZERO;

View File

@@ -3,6 +3,7 @@
#include <iostream>
#include <flecs.h>
#include <nlohmann/json.hpp>
#include <lua.hpp>
#include "Components.h"
#include "GameData.h"
#include "CharacterManagerModule.h"
@@ -11,6 +12,7 @@
#include "GUIModule.h"
#include "GUIModuleCommon.h"
#include "LuaData.h"
#include "PhysicsModule.h"
#include "PlayerActionModule.h"
namespace ECS
@@ -181,6 +183,33 @@ struct LuaNarrationHandler : GUI::NarrationHandler {
},
1);
lua_setfield(L, -2, "_get_properties");
lua_pushlightuserdata(L, this);
lua_pushcclosure(
L,
[](lua_State *L) {
LuaNarrationHandler *handler =
static_cast<LuaNarrationHandler *>(
lua_touserdata(
L,
lua_upvalueindex(1)));
lua_pushstring(
L, handler->getProperties().c_str());
const std::vector<ActionNodeList::ActionNode>
&nodes = ECS::get<ActionNodeList>()
.dynamicNodes;
lua_newtable(L);
int i;
for (i = 0; i < nodes.size(); i++) {
lua_pushinteger(L, i + 1);
lua_pushstring(
L,
nodes[i].props.dump().c_str());
lua_settable(L, -3);
}
return 1;
},
1);
lua_setfield(L, -2, "_get_goals");
lua_pop(L, 1);
}
void finish() override
@@ -224,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 : e.get<NPCActionNodes>().anodes) {
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>();
}
};
@@ -254,17 +284,18 @@ PlayerActionModule::PlayerActionModule(flecs::world &ecs)
ecs.import <CharacterManagerModule>();
ecs.component<ActionNodeList>()
.on_add([](flecs::entity e, ActionNodeList &alist) {
alist.dirty = true;
alist.nodeMutex = std::make_shared<std::mutex>();
alist.setDirty();
alist.nodes.reserve(1000);
alist.dynamicNodes.reserve(1000);
alist.selected = -1;
alist.busy = false;
alist.setUISelected(-1);
alist.setReady();
})
.add(flecs::Singleton);
ecs.system<ActionNodeList>("updateNodeList")
.kind(flecs::OnUpdate)
.each([](ActionNodeList &list) {
if (list.busy)
if (list.isBusy())
return;
if (list.nodes.size() > 0) {
Ogre::SceneNode *cameraNode =
@@ -279,49 +310,70 @@ PlayerActionModule::PlayerActionModule(flecs::world &ecs)
player.get<CharacterBase>()
.mBodyNode
->_getDerivedPosition();
list.query(playerPos, list.points,
list.distances);
list.UIquery(playerPos);
} else {
list.query(cameraPos, list.points,
list.distances);
list.UIquery(cameraPos);
}
}
});
ecs.system<ActionNodeList, const Input>("ActivateActionNode")
.kind(flecs::OnUpdate)
.each([this](ActionNodeList &list, const Input &input) {
std::lock_guard<std::mutex> lock(*list.nodeMutex);
if (input.control & 32)
std::cout << "act pressed" << std::endl;
if (list.busy)
if (list.isBusy())
return;
if (input.act_pressed && list.selected >= 0) {
std::cout << list.dynamicNodes[list.selected]
.props.dump(4)
<< std::endl;
if (input.act_pressed &&
list.getUIData().selected >= 0) {
std::cout
<< list.dynamicNodes[list.getUIData()
.selected]
.props.dump(4)
<< std::endl;
flecs::entity_t townid =
list.dynamicNodes[list.selected]
list.dynamicNodes[list.getUIData()
.selected]
.props["town"]
.get<flecs::entity_t>();
flecs::entity town = ECS::get().entity(townid);
int index = list.dynamicNodes[list.selected]
int index = list.dynamicNodes[list.getUIData()
.selected]
.props["index"]
.get<int>();
for (auto it = actionWords.begin();
it != actionWords.end(); it++) {
if (it->first ==
list.dynamicNodes[list.selected]
list.dynamicNodes[list.getUIData()
.selected]
.action) {
(*it->second)(
town, index,
-1, town, index,
list.dynamicNodes
[list.selected]
.action);
list.busy = true;
[list.getUIData()
.selected]
.action,
list.getUIData()
.selected);
list.setBusy();
}
}
}
if (!ECS::get<GUI>().enabled)
list.busy = false;
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++;
}
});
}
@@ -342,11 +394,220 @@ 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;
std::cout << "local offset: " << placeLocalOffset[place]
<< std::endl;
std::cout << "parent offset: " << anode.position << std::endl;
Ogre::Quaternion newRotation =
anode.rotation * placeLocalRotation[place];
Ogre::Vector3 newPosition =
anode.position +
anode.rotation * 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) {
delay = 0.0f;
state = 10;
// Yay!!!
std::cout << "Node data: " << std::endl;
std::cout << anode.props.dump(4) << std::endl;
} else if (ECS::get<Input>().act == 0 &&
delay <= 0.2f) {
delay = 0.0f;
state = 100;
}
break;
case 10:
if (ch.is_valid()) {
PhysicsModule::controlPhysics(ch, false);
// no control by player or ai
ch.add<CharacterControlDisable>();
// else
// ch.set<CharacterInActuator>(
// { "idle", { 0, 0, 0 } });
}
delay = 0.0f;
state++;
break;
case 11:
teleport("enter");
delay = 0.0f;
state++;
break;
case 12:
if (ch.is_valid()) {
if (word == "sit")
ch.set<CharacterInActuator>(
{ "sitting-chair",
{ 0, 0, 0 } });
}
teleport("enter");
delay = 0.0f;
state++;
break;
case 13:
teleport("enter");
delay = 0.0f;
state = 50;
break;
case 50:
// teleport again to handle possible problems caused by root motion
delay = 0.0f;
teleport("enter");
state++;
break;
case 51:
// 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 52:
// 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 53:
delay = 0.0;
state = 100;
break;
case 100:
delay = 0.0;
state++;
break;
case 101:
// 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 102:
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) {
@@ -362,26 +623,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 :
e.get<NPCActionNodes>().anodes) {
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>();
}
}
};
@@ -426,27 +700,61 @@ 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);
if (dynamicNodes.size() > nodes.size())
dynamicNodes.resize(nodes.size());
else {
dynamicNodes.clear();
dynamicNodes.insert(dynamicNodes.end(), nodes.begin(),
nodes.end());
}
ECS::get().query_builder<const TownNPCs>().each(
[this](flecs::entity town, const TownNPCs &npcs) {
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end();
it++) {
dynamicNodes.insert(
dynamicNodes.end(),
it->second.actionNodes.begin(),
it->second.actionNodes.end());
}
});
dirty = true;
}
void ActionNodeList::build()
{
dynamicNodes.clear();
dynamicNodes.insert(dynamicNodes.end(), nodes.begin(), nodes.end());
ECS::get().query_builder<const NPCActionNodes>().each(
[&](flecs::entity e, const NPCActionNodes &anodes) {
dynamicNodes.insert(dynamicNodes.end(),
anodes.anodes.begin(),
anodes.anodes.end());
});
std::lock_guard<std::mutex> lock(*nodeMutex);
indexObj = std::make_shared<ActionNodeList::indexObject>(dynamicNodes);
indexObj->index.buildIndex();
dirty = false;
}
bool ActionNodeList::query(const Ogre::Vector3 &position,
std::vector<size_t> &points,
std::vector<float> &distances)
bool ActionNodeList::_query(const Ogre::Vector3 &position,
std::vector<size_t> &points,
std::vector<float> &distances)
{
build();
std::vector<size_t> tmppoints;
std::vector<float> tmpdistances;
points.clear();
@@ -455,6 +763,10 @@ bool ActionNodeList::query(const Ogre::Vector3 &position,
distances.reserve(4);
tmppoints.resize(4);
tmpdistances.resize(4);
if (!indexObj) {
dirty = true;
return false;
}
nanoflann::KNNResultSet<float> resultSet(4);
resultSet.init(tmppoints.data(), tmpdistances.data());
bool ret = indexObj->index.findNeighbors(resultSet, &position.x,
@@ -467,4 +779,108 @@ bool ActionNodeList::query(const Ogre::Vector3 &position,
}
return ret;
}
bool ActionNodeList::query_ai(const Ogre::Vector3 &position, float distance,
std::vector<size_t> &points,
std::vector<float> &distances)
{
std::lock_guard<std::mutex> lock(*nodeMutex);
std::vector<size_t> tmppoints;
std::vector<float> tmpdistances;
points.clear();
points.reserve(100);
distances.clear();
distances.reserve(100);
tmppoints.resize(100);
tmpdistances.resize(100);
if (!indexObj) {
dirty = true;
return false;
}
nanoflann::KNNResultSet<float> resultSet(100);
resultSet.init(tmppoints.data(), tmpdistances.data());
bool ret = indexObj->index.findNeighbors(resultSet, &position.x,
nanoflann::SearchParameters());
int i;
for (i = 0; i < resultSet.size(); i++)
if (tmpdistances[i] < distance) {
points.push_back(tmppoints[i]);
distances.push_back(tmpdistances[i]);
}
return ret;
}
int ActionNodeList::addNode(ActionNode &node)
{
std::lock_guard<std::mutex> lock(*nodeMutex);
int index = nodes.size();
nodes.push_back(node);
dirty = true;
return index;
}
void ActionNodeList::removeNode(int index)
{
std::lock_guard<std::mutex> lock(*nodeMutex);
nodes.erase(nodes.begin() + index);
dirty = true;
}
const ActionNodeList::UIData &ActionNodeList::getUIData()
{
std::lock_guard<std::mutex> lock(*uidata.mutex);
return uidata;
}
void ActionNodeList::setUISelected(int selected)
{
std::lock_guard<std::mutex> lock(*uidata.mutex);
uidata.selected = selected;
}
void ActionNodeList::setUIPoints(const std::vector<size_t> &points,
const std::vector<float> &distances)
{
std::lock_guard<std::mutex> lock(*uidata.mutex);
uidata.points = points;
uidata.distances = distances;
}
void ActionNodeList::UIquery(const Ogre::Vector3 &position)
{
bool needBuild = false;
{
std::lock_guard<std::mutex> lock(*nodeMutex);
if (dirty || !indexObj)
needBuild = true;
}
if (needBuild)
build();
{
std::lock_guard<std::mutex> lock(*uidata.mutex);
_query(position, uidata.points, uidata.distances);
}
}
void ActionNodeList::setDirty()
{
dirty = true;
}
void ActionNodeList::setReady()
{
busy = false;
}
void ActionNodeList::setBusy()
{
busy = true;
}
bool ActionNodeList::isBusy()
{
return busy;
}
}

View File

@@ -18,38 +18,81 @@ struct ActionNodeList {
float radius;
nlohmann::json props;
};
std::vector<ActionNode> nodes, dynamicNodes;
std::shared_ptr<indexObject> indexObj;
std::vector<size_t> points;
std::vector<float> distances;
int selected;
struct UIData {
std::shared_ptr<std::mutex> mutex;
int selected;
std::vector<size_t> points;
std::vector<float> distances;
UIData()
: mutex(std::make_shared<std::mutex>())
, selected(-1)
{
}
};
private:
bool dirty;
bool busy;
std::shared_ptr<indexObject> indexObj;
struct UIData uidata;
bool _query(const Ogre::Vector3 &position, std::vector<size_t> &points,
std::vector<float> &distances);
public:
std::shared_ptr<std::mutex> nodeMutex;
std::vector<ActionNode> nodes, dynamicNodes;
void updateDynamicNodes();
void build();
bool query(const Ogre::Vector3 &position, std::vector<size_t> &points, std::vector<float> &distances);
int addNode(struct ActionNodeList::ActionNode &node)
{
int index = nodes.size();
nodes.push_back(node);
dirty = true;
return index;
}
void removeNode(int index)
{
nodes.erase(nodes.begin() + index);
}
};
struct NPCActionNodes {
std::vector<ActionNodeList::ActionNode> anodes;
bool query_ai(const Ogre::Vector3 &position, float distance,
std::vector<size_t> &points,
std::vector<float> &distances);
int addNode(struct ActionNodeList::ActionNode &node);
void removeNode(int index);
const UIData &getUIData();
void setUISelected(int selected);
void setUIPoints(const std::vector<size_t> &points,
const std::vector<float> &distances);
void UIquery(const Ogre::Vector3 &position);
void setDirty(); // node was added or removed
void setReady();
void setBusy();
bool isBusy();
};
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);
@@ -57,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

@@ -522,9 +522,6 @@ TerrainModule::TerrainModule(flecs::world &ecs)
OgreAssert(terrain.mTerrainGlobals,
"Failed to allocate global options");
Ogre::LogManager::getSingleton().setMinLogLevel(
Ogre::LML_TRIVIAL);
terrain.mTerrainGroup =
OGRE_NEW Ogre::TerrainGroup(
eng.mScnMgr,

View File

@@ -1,38 +1,5 @@
#include <set>
#include "goap.h"
bool BaseAction::can_run(const ECS::Blackboard &state, bool debug)
namespace goap
{
return m_exec->_can_run(state, debug);
}
void BaseAction::_plan_effects(ECS::Blackboard &state)
{
state.clear_flag(m_exec->m_clear_bits);
state.set_flag(m_exec->m_set_bits);
}
bool BaseAction::is_active(const ECS::Blackboard &state)
{
return m_exec->is_active(state);
}
bool BaseAction::stop(ECS::Blackboard &state)
{
if (!is_active(state))
return false;
m_exec->_exit(state);
state._active.erase(m_exec.get());
return true;
}
int BaseAction::execute(ECS::Blackboard &state)
{
if (!is_active(state)) {
state._active.insert(m_exec.get());
m_exec->_enter(state);
}
int ret = m_exec->_execute(state);
if (ret == OK)
stop(state);
return ret;
}
int BaseAction::get_cost(const ECS::Blackboard &state) const
{
return m_exec->_get_cost(state);
}

View File

@@ -1,77 +1,35 @@
#ifndef H_GOAP_H_
#define H_GOAP_H_
#undef NDEBUG
#include <string>
#include <memory>
#include <set>
#include <flecs.h>
namespace ECS
#include <Ogre.h>
namespace goap
{
struct Blackboard;
}
struct BaseAction;
struct BaseActionExec;
namespace ECS
{
struct Blackboard {
enum {
HIGH_LUST = (1 << 0),
LOW_LUST = (1 << 1),
FULL_LUST = (1 << 2),
LOW_HEALTH = (1 << 3),
FULL_HEALTH = (1 << 4),
HAS_TARGET = (1 << 5),
LOW_STAMINA = (1 << 6),
FULL_STAMINA = (1 << 7),
REACHED_TARGET = (1 << 8),
};
flecs::entity me;
int health;
int stamina;
int lust;
uint32_t flags;
std::set<BaseActionExec *> _active;
bool operator==(const Blackboard &other)
{
return flags == other.flags;
}
void set_flag(int flag)
{
flags |= flag;
}
void clear_flag(int flag)
{
flags &= ~flag;
}
bool get_flag(int flag) const
{
return flags & flag;
}
bool check_flag(int flag) const
{
return (flags & flag) == flag;
}
};
}
struct BaseAction {
template <typename State> struct BaseAction {
std::string m_name;
State prereq;
State effects;
int m_cost;
public:
enum { OK = 0, BUSY, ABORT };
private:
std::unique_ptr<BaseActionExec> m_exec;
public:
BaseAction(const std::string &name, BaseActionExec *exec)
: m_name(name)
, m_exec(exec)
BaseAction(const std::string &name, const State &prereq, const State &effects, int cost)
: m_name(name)
, prereq(prereq)
, effects(effects)
, m_cost(cost)
{
}
const std::string &get_name() const
virtual const std::string &get_name() const
{
return m_name;
}
void plan_effects(ECS::Blackboard &state)
void plan_effects(State &state)
{
// std::cout << m_name << " pre: " << &state << " " << state.flags
// << std::endl;
@@ -79,42 +37,20 @@ public:
// std::cout << m_name << " post: " << &state << " " << state.flags
// << std::endl;
}
virtual bool can_run(const ECS::Blackboard &state, bool debug = false);
virtual void _plan_effects(ECS::Blackboard &state);
bool is_active(const ECS::Blackboard &state);
bool stop(ECS::Blackboard &state);
int execute(ECS::Blackboard &state);
virtual int get_cost(const ECS::Blackboard &state) const;
virtual bool can_run(const State &state, bool debug = false)
{
return state.distance_to(prereq) == 0;
}
virtual void _plan_effects(State &state)
{
state.apply(effects);
}
// constant cost
virtual int get_cost(const State &state) const
{
return m_cost;
}
};
struct BaseActionExec {
enum {
OK = BaseAction::OK,
BUSY = BaseAction::BUSY,
ABORT = BaseAction::ABORT
};
BaseActionExec(int set_bits, int clear_bits)
: m_set_bits(set_bits)
, m_clear_bits(clear_bits)
{
}
bool is_active(const ECS::Blackboard &state)
{
return state._active.find(this) != state._active.end();
}
virtual int _execute(ECS::Blackboard &state) = 0;
virtual void _enter(ECS::Blackboard &state) = 0;
virtual void _exit(ECS::Blackboard &state) = 0;
virtual int _get_cost(const ECS::Blackboard &state) const
{
return 1;
}
virtual bool _can_run(const ECS::Blackboard &state, bool debug = false)
{
return true;
}
int m_set_bits, m_clear_bits;
};
template <typename State, typename Act, int N = 100> class BasePlanner {
struct VisitedState {
int priority;
@@ -190,20 +126,24 @@ template <typename State, typename Act, int N = 100> class BasePlanner {
public:
struct BaseGoal {
std::string m_name;
State goal;
public:
BaseGoal(const std::string &name)
: m_name(name)
BaseGoal(const std::string &name, const State &goal)
: m_name(name), goal(goal)
{
}
/** Checks if the goal is reached for the given state. */
virtual bool is_reached(const State &state) const
{
return distance_to(state) == 0;
return distance_to(state) == 0;
}
/** Computes the distance from state to goal. */
virtual int distance_to(const State &state) const = 0;
virtual int distance_to(const State &state) const
{
return state.distance_to(goal);
}
virtual ~BaseGoal() = default;
const std::string &get_name() const
@@ -216,7 +156,7 @@ public:
*
* If path is given, then the found path is stored there.
*/
int plan(const State &state, BaseGoal &goal, Act *actions[],
int plan(const State &state, const BaseGoal &goal, Act *actions[],
unsigned action_count, Act **path = nullptr, int path_len = 10)
{
visited_states_array_to_list(nodes, N);
@@ -244,7 +184,8 @@ public:
}
if (len > path_len) {
return -1;
OgreAssert(len <= path_len, "Out of plan length");
return -3;
}
if (path) {
@@ -255,6 +196,7 @@ public:
}
}
OgreAssert(len >= 0, "Bad plan length");
return len;
}
@@ -274,6 +216,7 @@ public:
}
if (!gc) {
OgreAssert(gc, "Out of memory");
return -2 /* OOM */;
}
@@ -337,14 +280,16 @@ public:
return -1 /* No path */;
}
};
template <class RunnerType> struct DeclareAction : public BaseAction {
template <class RunnerType, class State>
struct DeclareAction : public BaseAction<State> {
RunnerType runner;
template <class... Args>
DeclareAction(const std::string &name, Args... args)
: runner(args...)
, BaseAction(name, &runner)
, BaseAction<State>(name, &runner)
{
}
};
}
#endif

View File

@@ -2,6 +2,8 @@
#define __ITEMS_H__
#include <OgreMeshLodGenerator.h>
#include <flecs.h>
#include "Components.h"
#include "GameData.h"
namespace ECS
{
namespace Items

View File

@@ -19,6 +19,7 @@
#include "LuaData.h"
#include "PlayerActionModule.h"
#include "CharacterManagerModule.h"
#include "CharacterAIModule.h"
#include "items.h"
#include "town.h"
@@ -2095,7 +2096,69 @@ bool editNPCs(nlohmann::json &npcs)
ImGui::Text("NPC");
int id = 0;
for (auto &npc : npcs) {
ImGui::Text("%s", npc["lastName"].get<Ogre::String>().c_str());
static char firstName[64] = { 0 };
static char lastName[64] = { 0 };
static char nickName[64] = { 0 };
static char tags[256] = { 0 };
if (npc.find("firstName") == npc.end())
npc["firstName"] = "";
if (npc.find("lastName") == npc.end())
npc["lastName"] = "";
if (npc.find("nickName") == npc.end())
npc["nickName"] = "";
if (npc.find("tags") == npc.end())
npc["tags"] = "";
if (npc.find("sex") == npc.end())
npc["sex"] = 1;
strncpy(firstName, npc["firstName"].get<Ogre::String>().c_str(),
sizeof(firstName));
strncpy(lastName, npc["lastName"].get<Ogre::String>().c_str(),
sizeof(lastName));
strncpy(nickName, npc["nickName"].get<Ogre::String>().c_str(),
sizeof(lastName));
strncpy(tags, npc["tags"].get<Ogre::String>().c_str(),
sizeof(tags));
ImGui::InputText(
("Last name##" + Ogre::StringConverter::toString(id))
.c_str(),
lastName, sizeof(lastName));
if (ImGui::IsItemDeactivatedAfterEdit()) {
npc["lastName"] = Ogre::String(lastName);
changed = true;
}
ImGui::InputText(
("First name##" + Ogre::StringConverter::toString(id))
.c_str(),
firstName, sizeof(firstName));
if (ImGui::IsItemDeactivatedAfterEdit()) {
npc["firstName"] = Ogre::String(firstName);
changed = true;
}
ImGui::InputText(
("Nickname##" + Ogre::StringConverter::toString(id))
.c_str(),
nickName, sizeof(nickName));
if (ImGui::IsItemDeactivatedAfterEdit()) {
npc["nickName"] = Ogre::String(nickName);
changed = true;
}
ImGui::InputText(
("Tags##" + Ogre::StringConverter::toString(id)).c_str(),
tags, sizeof(tags));
if (ImGui::IsItemDeactivatedAfterEdit()) {
npc["tags"] = Ogre::String(tags);
changed = true;
}
int selection = npc["sex"].get<int>();
const char *items[] = { "Male", "Female" };
if (ImGui::Combo(("Sex##" + Ogre::StringConverter::toString(id))
.c_str(),
&selection, items, 2))
npc["sex"] = selection;
if (ImGui::SmallButton(
("Spawn##" + Ogre::StringConverter::toString(id))
.c_str())) {
@@ -2121,33 +2184,36 @@ bool editNPCs(nlohmann::json &npcs)
}
id++;
}
ImGui::Text("New NPC");
static char lastName[64] = { 0 };
ImGui::InputText("Last name", lastName, sizeof(lastName));
static char firstName[64] = { 0 };
ImGui::InputText("First name", firstName, sizeof(firstName));
static char tags[256] = { 0 };
ImGui::InputText("Tags", tags, sizeof(firstName));
static int selection = 0;
const char *items[] = { "Male", "Female" };
ImGui::Combo("Sex", &selection, items, 2);
if (ImGui::SmallButton("Add NPC")) {
nlohmann::json npc;
npc["lastName"] = Ogre::String(lastName);
npc["firstName"] = Ogre::String(firstName);
npc["tags"] = Ogre::String(tags);
Ogre::Vector3 npcPosition =
ECS::get<EditorGizmo>().sceneNode->_getDerivedPosition();
Ogre::Quaternion npcOrientation =
ECS::get<EditorGizmo>()
.sceneNode->_getDerivedOrientation();
to_json(npc["position"], npcPosition);
to_json(npc["orientation"], npcOrientation);
npc["sex"] = selection;
npc["health"] = 100;
npc["stamina"] = 100;
npcs.push_back(npc);
changed = true;
{
ImGui::Text("New NPC");
static char lastName[64] = { 0 };
ImGui::InputText("Last name", lastName, sizeof(lastName));
static char firstName[64] = { 0 };
ImGui::InputText("First name", firstName, sizeof(firstName));
static char tags[256] = { 0 };
ImGui::InputText("Tags", tags, sizeof(tags));
static int selection = 0;
const char *items[] = { "Male", "Female" };
ImGui::Combo("Sex", &selection, items, 2);
if (ImGui::SmallButton("Add NPC")) {
nlohmann::json npc;
npc["lastName"] = Ogre::String(lastName);
npc["firstName"] = Ogre::String(firstName);
npc["tags"] = Ogre::String(tags);
Ogre::Vector3 npcPosition =
ECS::get<EditorGizmo>()
.sceneNode->_getDerivedPosition();
Ogre::Quaternion npcOrientation =
ECS::get<EditorGizmo>()
.sceneNode->_getDerivedOrientation();
to_json(npc["position"], npcPosition);
to_json(npc["orientation"], npcOrientation);
npc["sex"] = selection;
npc["health"] = 100;
npc["stamina"] = 100;
npcs.push_back(npc);
changed = true;
}
}
ImGui::Separator();
return changed;
@@ -5509,6 +5575,8 @@ struct TownDecorateFurniture : TownTask {
Ogre::Vector3::
UNIT_Y));
node->attachObject(ent);
ent->setRenderingDistance(
60.0f);
addStaticBodyMesh(
e, mesh,
worldCenterPosition +
@@ -5739,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 =
@@ -5857,6 +5931,11 @@ void createTown(flecs::entity e, Ogre::SceneNode *sceneNode,
});
});
registerTownNPCs(e);
if (ECS::get().entity<CharacterAIModule>().is_valid())
if (ECS::get().has<CharacterAIModule>()) {
ECS::get_mut<CharacterAIModule>().createAI(e);
ECS::modified<CharacterAIModule>();
}
}
}
}

View File

@@ -9,13 +9,14 @@ add_library(world-build STATIC world-build.cpp)
target_link_libraries(world-build PRIVATE GameData)
target_include_directories(world-build PUBLIC .)
add_executable(test test.cpp)
target_link_libraries(test PRIVATE action world-build lua GameData OgreMain)
#add_executable(test test.cpp)
#target_link_libraries(test PRIVATE action world-build lua GameData OgreMain)
add_executable(test2 test2.cpp)
target_link_libraries(test2 PRIVATE action world-build lua GameData OgreMain)
#add_executable(test2 test2.cpp)
#target_link_libraries(test2 PRIVATE action world-build lua GameData OgreMain)
add_executable(mark_harbors mark_harbors.cpp)
target_link_libraries(mark_harbors PRIVATE lua OgreMain OgreRTShaderSystem)
#add_executable(mark_harbors mark_harbors.cpp)
#target_link_libraries(mark_harbors PRIVATE lua OgreMain OgreRTShaderSystem)
#add_custom_target(world ALL DEPENDS test test2)
add_custom_target(world ALL DEPENDS test test2)