Browse Source

Groundwork for Hierarchical Task Networks; still very broken

combatComponentRefactor
Macoy Madson 6 years ago
parent
commit
4026f364e4
  1. 15
      .gitignore
  2. 8
      Jamrules
  3. 34
      assets/designDocs/technical/behaviorTrees.txt
  4. 82
      assets/designDocs/technical/hierarchicalTaskNetworks.txt
  5. 2
      src/GalavantMain.cpp
  6. 1
      src/Jamfile
  7. 6
      src/ai/Jamfile
  8. 307
      src/ai/htn/HTNPlanner.cpp
  9. 42
      src/ai/htn/HTNTasks.cpp
  10. 65
      src/ai/htn/HTNTasks.hpp
  11. 49
      src/ai/htn/HTNTypes.hpp
  12. BIN
      src/experiments/flatbuffers/SavedHello.bin
  13. BIN
      src/experiments/flatbuffers/testFlatbuffers_write
  14. 5
      src/project/galavantSublime/galavant.sublime-project
  15. 70
      src/unitTesting/HTN_test.cpp
  16. 9
      src/unitTesting/Jamfile

15
.gitignore

@ -27,13 +27,11 @@
*.out
*.app
# Codeblocks folder ignores
# Build folder ignores
bin/
obj/
lib/
# Codeblocks file ignores
*.layout
# Ignore documentation (can be autogenerated via doxygen)
docs/
@ -41,8 +39,17 @@ docs/
# Ignore anything with the LOCAL prefix
LOCAL*
#
# My Workflow Ignores
#
# Codeblocks file ignores
*.layout
# Ignore sublime workspace
*.sublime-workspace
CppCheckResults.xml
ValgrindOut.xml
ValgrindOut.xml
*.ycm_extra_conf.py

8
Jamrules

@ -1,11 +1,15 @@
CC = gcc ;
C++ = g++ ;
LINK = g++ ;
#C++ = clang++ ;
#LINK = clang++ ;
# Galavant requires C++11
# fPIC = position independent code. This is so the lib can be linked in other libs/executables
C++FLAGS = -std=c++11 -fPIC -g ;
OBJECTC++FLAGS = -std=c++11 ;
# g = debug symbols
# lstdc++ = standard library
C++FLAGS = -std=c++11 -fPIC -g -Wall -Wextra ;
OBJECTC++FLAGS = -std=c++11 -lstdc++ ;
HDRS = thirdParty/flatbuffers/include ;

34
assets/designDocs/technical/behaviorTrees.txt

@ -0,0 +1,34 @@
Behavior Trees
====================
- Leaf: Cannot have children
- Conditional: returns success or failure immediately
- Action: can return running
- Composite: Has one or more children
- Parallel: runs multiple children at once
- Sequence: runs children in order, aborts on child failure
- Selector: runs children in order, continues on child failure
- Random: runs one of its children at random
- Decorator: Has exactly one child
- Inverter
- Succeeder: always return success
- Failer: always return failure
- Repeat X times
- Repeat until failure
- Repeat until success
Behavior Tree
ReceiveStatuses(Node, (TreeInstances, Statuses))
RunChildren(TreeInstances, ChildNodes)
Update
loopCounter
do
update trees which have nodes which aren't running
while there are trees which aren't running and not at max number loops
tick running nodes

82
assets/designDocs/technical/hierarchicalTaskNetworks.txt

@ -0,0 +1,82 @@
Tasks
Something to be done which can be broken down into a method to accomplish (task sort of equals goal)
Methods
If the agent's state matches a set of preconditions, return a list of methods and primitives which complete the task
Primitives/Operators
If the agent's state matches a set of preconditions, return that the primitive can be performed
Perform the actual change in the world
Change the world state (this can be both during planning and after actually performing the primitive task) (should
these be separated?)
agent = entityID
agentState = struct
{
isUndead = false
hasAmmo = false
target = entityID (should this in agent state or parameters?)
}
worldState = struct
{
towns = {townA: [x, y, z, properties], townB: [x, y, z, properties]}
canUndeadDoShit = true
}
parameters = vector of param
param = struct
{
enum type
union
{
entityID entity
coordinate coord
scalar val
vector vec
float floatVal
}
}
MethodA = 0
MethodB = 1
PrimitiveA = 0
PrimitiveB = 1
MethodA = struct
{
preconditionsSatisfied(agent, agentState, worldState, parameters)
{
return worldState.useMethodA
}
newTasks = [(1, method, [param1, param2]), (0, primitive, [])];
}
PrimitiveA = module
{
arePreconditionsSatisfied(agent, agentState, worldState, parameters)
{
return worldState.useMethodA
}
applyEffects(agent, agentState, worldState, parameters)
{
worldState.hasDonePrimitiveA = true
}
execute(agent, agentState, worldState, parameters)
{
if preconditionsSatisfied(agent, agentState, worldState, parameters)
primitiveAComponentManager.subscribeTaskAgent(agent, this) // pass self in for callback (instead: event listener?)
return true
return false
}
PrimitiveADoneCallback(agent, bool succeeded)
{
// plan follow code will call applyEffects ?
notifyPlanFollowCode(agent, succeeded)
}
}

2
src/GalavantMain.cpp

@ -4,5 +4,5 @@
void gv::GalavantMain::Update(float frameTime)
{
std::cout << "Updated Galavant\n";
std::cout << "Updated Galavant frameTime: " << frameTime << "\n";
}

1
src/Jamfile

@ -8,6 +8,7 @@ MakeLocate libGalavant.a : lib ;
SubInclude . src thirdPartyWrapper ;
SubInclude . src entityComponentSystem ;
SubInclude . src ai ;
# Experiments and Testing (feel free to remove these if you don't want them built)
SubInclude . src experiments ;

6
src/ai/Jamfile

@ -0,0 +1,6 @@
SubDir . src ai ;
Library libGalaAi : htn/HTNTasks.cpp ;
#Library libGalaAi : htn/HTNTasks.cpp htn/HTNPlanner.cpp ;
MakeLocate libGalaAi.a : lib ;

307
src/ai/htn/HTNPlanner.cpp

@ -0,0 +1,307 @@
#include "HTNTypes.hpp"
#include "HTNTasks.hpp"
namespace Htn
{
bool DecomposeGoalTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments)
{
GoalTask* goalTask = static_cast<GoalTask*>(task);
return false; // TODO
}
bool DecomposeCompoundTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments)
{
CompoundTask* compoundTask = static_cast<CompoundTask*>(task);
if (compoundTask->StateMeetsPreconditions(taskArguments))
// Decomposition is determined by the task itself
return compoundTask->Decompose(taskList, taskArguments);
return false;
}
struct DecomposeTaskState
{
Task* DecomposedTask;
ParameterList Parameters;
};
typedef std::vector<DecomposeTaskState> DecomposedTaskList;
struct TaskDecomposition
{
Task* DecomposedTask;
unsigned int DecompositionIndex;
DecomposedTaskList* Decomposition;
};
typedef std::vector<TaskDecomposition> TaskDecompositionStack;
enum class PlanStepStatus
{
Failed = 0, // generic failure
FailedDecomposition,
SuccessfulDecomposition,
FailedToMeetPrecondtions,
StateMeetsPreconditions,
NoTasks,
PlanComplete
};
/*
Goal Task // Set Agent StateA and StateB = true
Method1 = Compound1(AgentState and Params from initial call)
Method2 = Compound2
Method3 = Compound3
Compound1
StateMeetsPrecondtion(AgentState, Params from Goal) then
return {primitiveA(paramA, paramB), primitiveB(ParamA, ParamB, ParamC)}
primitiveA
StateMeetsPrecondition(AgentState, paramA, paramB)
then AgentState.StateA = true
primitiveB
StateMeetsPrecondition(AgentState, paramA, paramB, paramC)
then AgentState.StateB = true
PlanStep(stack, taskList)
if not stack
// startup
stack.push(decompose(taskList[0]))
else
decomposition = stack.pop()
allPrimitives = true
for task in decomposition
if task == primitive
task.StateMeetsPrecondition then
modify currentState
// don't break here
else
goto fail
if task == compound
allPrimitives = false
task.StateMeetsPrecondition then
stack.push(decompose(task))
break
else
goto fail
if task == goal
allPrimitives = false
methodIndex = 0
decomposedTasks = task.GetMethodAtIndex(0), currentState
stack.push(decomposition)
break
fail:
*/
PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
if (!stack.empty())
{
// Check to see if the current stack item is all primitive tasks with met preconditions. If
// so, we can add them to the tasklist and walk all the way back up
// TODO: naming
TaskDecomposition currentDecomposition = stack.end();
bool onlyPrimitiveTasksRemain = true;
bool failedToDecompose = false;
for (DecomposeTaskState* currentTaskState : currentDecomposition.Decomposition)
{
if (!currentTaskState || !currentTaskState->DecomposeTask)
continue;
Task* currentTask = currentTaskState->DecomposeTask;
TaskType currentTaskType = currentTask->GetType();
TaskArguments& currentTaskArguments = currentDecomposition->Arguments;
if (currentTaskType == TaskType::Primitive)
{
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(currentTask);
if (!primitiveTask->StateMeetsPreconditions(currentTaskArguments))
{
failedToDecompose = true;
break;
}
// Modify state
}
else
{
onlyPrimitiveTasksRemain = false;
// TODO
//failedToDecompose = DecomposeTask()
}
}
// TODO: Goal tasks trying new decompositions
if (failedToDecompose)
{
stack.pop_back();
delete currentDecomposition;
return PlanStepStatus::FailedDecomposition;
}
}
else if (!taskList.empty())
{
// No stack; we must be just starting
Task* currentTask = *taskList.data();
if (!currentTask)
return PlanStepStatus::Failed;
TaskType currentTaskType = currentTask->GetType();
// What if taskList just has primitives? Multiple goals?
if (currentTaskType == TaskType::Goal)
{
GoalTask* goalTask = static_cast<GoalTask*>(currentTask);
int numMethods = goalTask->GetNumMethods();
TaskDecomposition newDecomposition;
newDecomposition.DecomposedTask = currentTask;
newDecomposition.DecompositionIndex = 0;
newDecomposition.Decomposition = nullptr;
// Goal tasks are decomposed via methods. Methods are Tasks which can be used to achieve
// the goal. Goal tasks do not have conditonals - they just blindly pick methods in
// order until they find one which works
for (int methodIndex = newDecomposition.DecompositionIndex;
!newDecomposition.Decomposition && methodIndex < numMethods; methodIndex++)
{
Task* method = goalTask->GetMethodAtIndex(newDecomposition.DecompositionIndex);
if (!method)
continue;
TaskType methodType = method->GetType();
if (methodType == TaskType::GoalTask)
{
newDecomposition.newDecomposition = new TaskList(1);
newDecomposition.Decomposition->push_back(method);
newDecomposition.DecompositionIndex = methodIndex;
}
else if (methodType == TaskType::Primitive)
{
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(method);
if (primitiveTask->StateMeetsPreconditions(taskArguments))
{
newDecomposition.newDecomposition = new TaskList(1);
newDecomposition.Decomposition->push_back(method);
newDecomposition.DecompositionIndex = methodIndex;
}
}
else if (methodType == TaskType::CompoundTask)
{
CompoundTask* compoundTask = static_cast<CompoundTask*>(method);
if (compoundTask->StateMeetsPreconditions(taskArguments))
{
newDecomposition.newDecomposition = new TaskList();
compoundTask->Decompose(&newDecomposition.newDecomposition, taskArguments);
newDecomposition.DecompositionIndex = methodIndex;
}
}
else
// We got something we don't know how to handle
return PlanStepStatus::Failed;
}
stack.push_back(newDecomposition);
return PlanStepStatus::SuccessfulDecomposition;
}
}
else
return PlanStepStatus::NoTasks;
}
/*PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
bool planComplete = true;
// Check to see if there are tasks which need to be decomposed
for (Task* currentTask : taskList)
{
if (!currentTask)
continue;
TaskType currentTaskType = currentTask->GetType();
// The plan is complete once all Goal and Compound Tasks have been decomposed into Primitive
// Tasks, which we can actually execute
if (currentTaskType != TaskType::Primitive)
{
planComplete = false;
break;
}
}
if (!planComplete)
{
Task* currentTask = *taskList.data();
if (!currentTask)
return PlanStepStatus::Failed;
TaskType currentTaskType = currentTask->GetType();
if (currentTaskType == TaskType::Goal)
{
GoalTask* goalTask = static_cast<GoalTask*>(currentTask);
}
}
else
return PlanStepStatus::PlanComplete;
}*/
/*// Take the first task in the list and decompose it or make sure it's a valid part of the plan
PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
const TaskArguments& taskArguments)
{
if (taskList.size())
{
Task* currentTask = *taskList.data();
TaskType taskType = currentTask->GetType();
// If it's a goal, we want to find a compound task that will accomplish it
if (taskType == TaskType::Goal)
{
if (DecomposeGoalTask(taskList, currentTask, taskArguments))
return PlanStepStatus::SuccessfulDecomposition;
else
return PlanStepStatus::FailedDecomposition;
}
// If it's a compound task, we want to decompose it
if (taskType == TaskType::Compound)
{
if (DecomposeCompoundTask(taskList, currentTask, taskArguments))
return PlanStepStatus::SuccessfulDecomposition;
else
return PlanStepStatus::FailedDecomposition;
}
// If it's a primitive task, we want to make sure we can actually execute it with our given
// state
if (taskType == TaskType::Primitive)
{
// TODO
}
return PlanStepStatus::Failed;
}
return PlanStepStatus::NoTasks;
}*/
}

42
src/ai/htn/HTNTasks.cpp

@ -0,0 +1,42 @@
#include "HTNTasks.hpp"
namespace Htn
{
Task::Task(void)
{
Type = TaskType::None;
}
TaskType Task::GetType(void)
{
return Type;
}
int GoalTask::GetNumMethods(void)
{
return Methods ? Methods->size() : 0;
}
Task* GoalTask::GetMethodAtIndex(int index)
{
if (index < 0 || index >= GetNumMethods() || !Methods)
return nullptr;
return (*Methods)[index];
}
void GoalTask::SetMethods(TaskList* newMethods)
{
Methods = newMethods;
}
CompoundTask::CompoundTask(void)
{
Type = TaskType::Compound;
}
PrimitiveTask::PrimitiveTask(void)
{
Type = TaskType::Primitive;
}
}

65
src/ai/htn/HTNTasks.hpp

@ -0,0 +1,65 @@
#pragma once
#include "HTNTypes.hpp"
namespace Htn
{
enum class TaskType
{
None = 0,
Goal,
Compound,
Primitive
};
// Task only exists so that we can store all our Task types in a flat list
class Task
{
public:
Task(void);
// Type is used to know what to cast this Task to
TaskType GetType(void);
protected:
// This is set by the constructors of each different Task
TaskType Type;
};
typedef std::vector<Task*> TaskList;
//
// Different types of tasks
//
class GoalTask : public Task
{
private:
TaskList* Methods;
public:
GoalTask(void);
int GetNumMethods(void);
Task* GetMethodAtIndex(int index);
void SetMethods(TaskList* newMethods);
};
class CompoundTask : public Task
{
public:
CompoundTask(void);
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
virtual bool Decompose(TaskList& taskList, const TaskArguments& arguments) = 0;
};
class PrimitiveTask : public Task
{
public:
PrimitiveTask(void);
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0;
// Returns whether or not starting the task was successful (NOT whether the task completed)
virtual bool Execute(const TaskArguments& arguments) = 0;
};
}

49
src/ai/htn/HTNTypes.hpp

@ -0,0 +1,49 @@
#pragma once
#include "../entityComponentSystem/EntityTypes.hpp"
namespace Htn
{
// Htn::Parameter is used as a way to send arbitrary arguments between different types of Tasks
// TODO: Add getters/setters so this is safe to use
struct Parameter
{
enum class ParamType
{
None = 0,
// Primitives
Int,
Float,
Bool,
// Game-specific
Entity
};
ParamType Type;
union
{
int IntValue;
float FloatValue;
bool BoolValue;
Entity EntityValue;
};
};
typedef std::vector<Parameter> ParameterList;
typedef std::vector<Parameter>::iterator ParameterListIterator;
typedef std::vector<Parameter>::const_iterator ParameterListConstIterator;
typedef std::vector<Parameter>::reverse_iterator ParameterListReverseIterator;
// The arguments passed to most all Task functions
struct TaskArguments
{
Entity Agent;
// AgentState* AgentState;
// WorldState* WorldState;
ParameterList Parameters;
};
}

BIN
src/experiments/flatbuffers/SavedHello.bin

Binary file not shown.

BIN
src/experiments/flatbuffers/testFlatbuffers_write

Binary file not shown.

5
src/project/galavantSublime/galavant.sublime-project

@ -42,6 +42,11 @@
"shell_cmd": "make",
"working_dir": "../../../src/experiments"
},
{
"name": "Jam",
"shell_cmd": "jam"
},
// Assume GalavantUnreal is cloned as a submodule or linked (I can't remember if I actually made it a submodule)
{
"name": "Jam",
"shell_cmd": "jam"

70
src/unitTesting/HTN_test.cpp

@ -0,0 +1,70 @@
#include <iostream>
#include "../ai/htn/HTNTypes.hpp"
#include "../ai/htn/HTNTasks.hpp"
class TestCompoundTaskA : public Htn::CompoundTask
{
public:
TestCompoundTaskA(void)
{
//Type = Htn::TaskType::Primitive;
}
virtual ~TestCompoundTaskA(void)
{
}
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
{
return true;
}
virtual bool Decompose(Htn::TaskList& taskList, const Htn::TaskArguments& args)
{
std::cout << "Decompose TestCompoundTaskA: " << args.Parameters[0].IntValue << "\n";
return true;
}
};
class TestPrimitiveTask : public Htn::PrimitiveTask
{
public:
TestPrimitiveTask(void)
{
//Type = Htn::TaskType::Primitive;
}
virtual ~TestPrimitiveTask(void)
{
}
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args)
{
return true;
}
virtual bool Execute(const Htn::TaskArguments& args)
{
std::cout << "execute TestPrimitiveTask: " << args.Parameters[0].IntValue << "\n";
return true;
}
};
int main()
{
// Test parameters
Htn::Parameter testParam = {Htn::Parameter::ParamType::Int, 123};
std::cout << testParam.FloatValue << "\n";
// Test Task types
TestPrimitiveTask testPrimitive;
std::cout << (int)testPrimitive.GetType() << "\n";
// Goal task setup
TestCompoundTaskA testCompoundTaskAA;
TestCompoundTaskA testCompoundTaskAB;
const Htn::TaskList goalTaskMethods = {&testCompoundTaskAA, &testCompoundTaskAB};
return 0;
}

9
src/unitTesting/Jamfile

@ -3,11 +3,14 @@ SubDir . src unitTesting ;
Main objectComponentTest : ObjectComponent_test.cpp ../objectComponent/Component.cpp ../objectComponent/ComponentManager.cpp ../objectComponent/Object.cpp ../objectComponent/ObjectComponentManager.cpp ../objectComponent/ObjectID.cpp ../objectComponent/ObjectType.cpp ;
Main entityComponentTest : EntityComponentSystem_test.cpp ;
LinkLibraries entityComponentTest : libGalaEntityComponent ;
Main objectPoolTest : ObjectPoolTest.cpp ;
LinkLibraries entityComponentTest : libGalaEntityComponent ;
Main htnTest : HTN_test.cpp ;
LinkLibraries htnTest : libGalaAi ;
MakeLocate objectComponentTest entityComponentTest objectPoolTest : bin ;
MakeLocate objectComponentTest entityComponentTest objectPoolTest htnTest : bin ;
SubInclude . src entityComponentSystem ;
SubInclude . src entityComponentSystem ;
SubInclude . src ai ;
Loading…
Cancel
Save