alexshafranov / derplanner

Hierarchical-Task-Network planner in C++
zlib License
42 stars 11 forks source link

How to use? #1

Open JimmyJames707 opened 10 years ago

JimmyJames707 commented 10 years ago

Hello, This looks very interesting, but I don't know how to use it.

I was able to compile block.txt and travel.txt, but don't know what to do after that. I assume you add block and travel's C++ source files to the derplanner-runtime section, but after that I have no idea.

A sample main.cpp file that calls them and gets the steps of the plan would be very helpful.

alexshafranov commented 10 years ago

Hi,

Thanks for opening the issue and sorry for the confusion. The project is work in progress and it definitely lacks documentation :)

travel.main.cpp & blocks.main.cpp were in the scratch folder and weren't part of the build. I moved *.main.cpp files to the examples folder (auto generated travel.cpp and blocks.cpp are also under source control in the examples folder). I also added examples as subprojects to premake build.

The runtime part is the first attempt and I'll be changing it soon. Feel free to comment!

Cheers, Alexander.

JimmyJames707 commented 10 years ago

Thanks for answering so quickly. I look forward to trying it out. :)

JimmyJames707 commented 10 years ago

Hi, I just compiled the new version in VS2013 and it does not like this macro:

derplanner-master\examples\blocks.cpp(78): error C2051: case expression not constant

PLNNR_COROUTINE_YIELD(state);

Microsoft's Edit and Continue is the cause. The way to fix it:

http://stackoverflow.com/questions/11461915/visual-studio-9-0-error-c2051-case-expression-not-constant

alexshafranov commented 10 years ago

Oops, it took me to do the full rebuild in vs2013 to notice the problem! With incremental build it was fine.

Now it should be fixed - those fake coroutine macro are just the way to make generated code a bit nicer, so no real need for LINE macro there, it is now replaced with explicit numbers generated by compiler.

Cheers!

JimmyJames707 commented 10 years ago

It works perfectly. Thank you very much.

JimmyJames707 commented 10 years ago

Hi Alex, Is there a way to debug output the methods, that were used to create the task list? It would helpful to see how find_plan_step finds a solution.

I tried using this:

plnnr::method_instance* method = plnnr::bottomplnnr::method_instance(pstate.methods); plnnr::method_printf method_printer; plnnr::walk_stack_downbot::task_type(method, method_printer);

But it only outputs the root method.

alexshafranov commented 10 years ago

Hi Jimmy,

find_plan works something like this: initialize: push root method on stack. while stack is not empty:

So, to see that in action, you can use walk_stack_up after each planing step. walk_stack_down gives you only the root method, because the root method is at the bottom.

BTW, I'm going to get rid of template based "reflection", and will have virtual interface instead. That will make generated code shorter, and hopefully the API will be easier.

Cheers!

JimmyJames707 commented 10 years ago

"So, to see that in action, you can use walk_stack_up after each planing step."

Unfortunately that doesn't compile with what I'm using. I get the error: 'next' : is not a member of 'plnnr::method_instance'

alexshafranov commented 10 years ago

Ooops, sorry for the misleading comment, method_instance doesn't include next offset as there's no need for it in runtime. But planner_state keeps track of the top method on the methods stack, so could you try this: walk_stack_down(pstate.top_method)

JimmyJames707 commented 10 years ago

Thank you, that worked.

"BTW, I'm going to get rid of template based "reflection", and will have virtual interface instead. That will make generated code shorter, and hopefully the API will be easier."

I haven't had much difficulty understanding how to use it so far, but I did have the advantage of watching your video at aigamedev a few times. Your capture the flag example is very helpful.

JimmyJames707 commented 10 years ago

Hi Alex, Not sure if I am doing something wrong, but I think I'm getting a junk value for top_task, when I do the following:

plnnr::task_instance *top_task = plnnr::top<plnnr::task_instance>(pstate.tasks);
plnnr::task_printf task_printer;
plnnr::walk_stack_down<bot::task_type>(top_task, task_printer);

This code works fine:

plnnr::task_instance *bottom_task = plnnr::bottom<plnnr::task_instance>(pstate.tasks);
plnnr::task_printf task_printer;
plnnr::walk_stack_up<bot::task_type>(bottom_task, task_printer);

What I would like to do is walk the tasks in reverse, if that is possible.

alexshafranov commented 10 years ago

Hi Jimmy,

Sorry for the confusion, the plnnr::top would only work for the stacks where each element is of the same size.

While in the tasks stack we have task_instance struct followed by some auto-generated struct of the unknown size (i.e. arguments of that task instance).

As with method instances, the pointer to the topmost task_instance (which is less than the actual stack top) is stored in planner state:

    task_printf task_printer;
    printf("down:\n");
    plnnr::walk_stack_down<travel::Task_Type>(pstate.top_task, task_printer);

Cheers, Alexander.

JimmyJames707 commented 10 years ago

It works perfectly. Thank you!

JimmyJames707 commented 9 years ago

Hi Alex, I hope you are well. :)

I've been thinking it would be very helpful, to be able to see the structure of a script after it is compiled. I thought of making derplanner output a text file that Graphviz can read. Is something like that possible?

alexshafranov commented 9 years ago

Hi Jimmy,

Thanks for the idea! That shouldn't be too hard to generate Graphviz script from the domain script. Code-wise it could be done as an alternative mode in derplannerc which would walk the built ast::Tree to generate Graphiz code.

I'm thinking though to start with dropping the LISP-like syntax and replacing it with something more structured, like this:

; domain consists of composite tasks descriptions (methods):
; each task contains one or more cases (branches)
; each case specifies first-order logic formula to be satisfied as a precondition
; and a list of tasks to expand to.

domain bot
{
    task behave()
    {
        case(target(t) & visible(t)) -> [attack(t)]
    }

    task attack(target)
    {
        <...>
    }
}

What do you think about this?

JimmyJames707 commented 9 years ago

That looks pretty neat. I like it. :)

JimmyJames707 commented 9 years ago

Don't know if you are familiar with Graphviz. If you aren't, this might save you a little time. If you are, well at least I had some fun.

This is how your travel example might look:

digraph {

node [margin=0.1 fontcolor=black width=0 shape=ellipse style=filled fillcolor=yellow] "root()" "travel(x, y)" "!ride_taxi(x, y)" "travel_by_air(x, y)" "travel(x, ax)" "!fly(ax, ay)" "travel(ay, y)"

node [margin=0.1 fontcolor=black width=0.5 shape=rect style=filled fillcolor=white] "Precond: start(s) & finish(f)" "Precond: short_distance(x, y)" "Precond: long_distance(x, y)" "Precond: airport(x, ax) & airport(y, ay)"

"root()" -> "Precond: start(s) & finish(f)"[color=blue] "Precond: start(s) & finish(f)" -> "travel(x, y)"[dir=none]

"travel(x, y)" -> "Precond: short_distance(x, y)"[color=blue] "Precond: short_distance(x, y)" -> "!ride_taxi(x, y)"[dir=none]

"travel(x, y)" -> "Precond: long_distance(x, y)"[color=blue] "Precond: long_distance(x, y)" -> "travel_by_air(x, y)"[dir=none]

"travel_by_air(x, y)" -> "Precond: airport(x, ax) & airport(y, ay)"[color=blue] "Precond: airport(x, ax) & airport(y, ay)" -> "travel(x, ax)"[dir=none] "Precond: airport(x, ax) & airport(y, ay)" -> "!fly(ax, ay)"[dir=none] "Precond: airport(x, ax) & airport(y, ay)" -> "travel(ay, y)"[dir=none]

}

alexshafranov commented 9 years ago

Thanks thats cool. Is that script written manually or generated?

JimmyJames707 commented 9 years ago

That's one I wrote by hand while I was learning it. Wish I had the skills to mod your code though. :)

astrocoder commented 9 years ago

Alex I like the new structured domain format - it's quite easy to ready. I was wondering though if you could provide some more examples of how to define other types of elements in the domain script? In particular I would like to define an entity type to pass to the methods.

alexshafranov commented 9 years ago

Hi @astrocoder ,

The approach in 'master' branch was to just use C++ type description. So e.g. Bot* could be used as a type. In the end I decided that approach to be too restrictive: all reflection of world state needs to be templated, no notion of which types can be casted to others etc.

The current plan (branch 'two') is to have a few basic types: ints, float, maybe vectors Plus some kind of typedefs for better type safety.

In that scheme entities have to be referenced by handles: world { position( uint32, vec3 ) }

Or with typedef:

world { Bot = uint32
position( Bot, vec3 ) // passing uint32 which is not Bot will be disallowed }

And then you somehow map this uint32 to an entity data on the C++ side.

Feel free to share your thoughts as well

JimmyJames707 commented 9 years ago

Hi Alex, Long time no see. :) Since we last spoke I have been forced to change game engines to Unity 5. I found a free HTN planner for it written in C#, but I can't imagine it would be as fast yours. For a short time they had a commercial one written in C# with a visual interface, but the author stopped supporting it. So I was wondering, do you think it would be possible to use your HTN planner with the free version of Unity 5, or would it be too difficult?

alexshafranov commented 9 years ago

Hi @JimmyJames707,

It's probably doable, but not very straight-forward :)

Domain code can now be compiled to DLL and that DLL can be loaded from Unity. It seems that in Unity 5 plugins are available in the free version.

Then runtime needs to be compiled to DLL as well with the interface usable from C#.

So if you're keen to try it out then we could have a chat over skype or what'd be convenient to you.

Cheers!

JimmyJames707 commented 9 years ago

Thanks Alex, but I'm not on Skype due to being a bit shy about talking to cameras. lol What I'll do is try to figure it out and ask questions if I get stuck. ATM I'm still learning Unity and trying out some assets, so there's really no rush. I just wanted to know if it was possible to use it at some point in the future. :)

Vitalfever commented 8 years ago

Hi Alex. I was hoping you could give a quick overview of how you'd make this work with a bot. What functions do you call from the bot? I saw the interview on AIGameDev but I'm still a little unsure on the implementation for that.

Thanks for making this and releasing the source.

alexshafranov commented 8 years ago

Hi @Vitalfever,

For this type of planner the structure is going to be similar to what you'd have with the behavior tree.

Main difference with BT:

As this planning approach doesn't support planning into the future, you'll need to plan at regular intervals and have some kind of 'continue' task, to check validity of the currently executing plan.

Check this presentation for more details: https://www.guerrilla-games.com/read/killzone-2-multiplayer-bots

Besides parametrized tasks, I think this HTN approach is most useful for AI with a wide array of choices to make. Like e.g. a lot of different weapons

In terms of code, here's the outline:

    bot_init_domain_info();

*_init_domain_info and *_get_domain_info are functions from the generated code

    const plnnr::Domain_Info* domain = bot_get_domain_info();
    plnnr::Fact_Database db;
    plnnr::init(&db, &mem, &domain->database_req);

This database is like blackboard, various systems will fill it with information to be used during planning

    plnnr::Planning_State pstate;
    plnnr::init(&pstate, &mem, &config);
    plnnr::bind(&pstate, domain, &db);
Vitalfever commented 8 years ago

Thank you so much for going over that. Exactly what I needed. I really appreciate it.

alexshafranov commented 8 years ago

@Vitalfever No problem, feel free to ask if you have more questions

Vitalfever commented 8 years ago

Hello again. I've got it working in my game thanks to your help. Just wondering how you'd bind gameplay functions to the plans with the new code. I saw in the AIGameDev video you hooked them up but the way you did it there doesn't appear to be relevant anymore.

I'm also having some trouble with passing the database and state from my init function to my tick. They only seem to work as local variables, and will not work passing it from one function to another. It appears this is because of the structs destructors.

If you could be so kind, I'd also like a little help with writing domains. For example, in your readme you've got a sample of compound tasks here. task attack(Bot, Enemy) { case pos(Bot, Src) & pos(Enemy, Dst) & dist(Src, Dst) < Close_Range -> [ approach(Src, Dst), melee!(Bot, Enemy), retreat(Bot) ] } I get that pos is a macro, but I do not understand what "Close_Range" is however, or how it's defined. Is it a previously defined fact? If you had any resources or tips for domains in general that would be fantastic.

alexshafranov commented 8 years ago

Hi @Vitalfever,

So something like this:

class BrainComponent {
 BrainComponent();
  void find_plan();
private:
   plnnr::Fact_Database m_facts;
   plnnr::Planning_State m_planner;
};

What kind of gameplay functions were you wanting to add?

Vitalfever commented 8 years ago

Thanks for that. I understand better now.

That's almost exactly how the class is set up. Turns out the crash is while it's deleting the tables. It gets an invalid pointer trying to delete tables when the AI is destroyed. I'll need to look into why that is.

For the gameplay functions I meant for example, telling my bot in game to do his ShootEnemy (game code, not planner or domain code) function from the plan generating a fire! task. What is the best way to do this?

alexshafranov commented 8 years ago

Tasks in the plan are identified by integers.

The identifier can be used as an index to arrays in plnnr::Task_Info:

Task_Info itself is a field plnnr::Domain_Info::task_info.

Domain_Info is a static data in the generated code, and can be accessed by calling the function: extern "C" PLNNR_DOMAIN_API const plnnr::Domain_Info* <DOMAIN>_get_domain_info();

As part of initialization of the hypothetical BrainComponent you can bind your task code to the tasks defined in a domain by name.

class BrainComponent {
private:
 Task* m_tasks;

 void bind_to_domain(const Domain_Info* domain)
 {
   for each task name we require from this domain {
     task_id = find index of the name in plnnr::Task_Info::names
     m_tasks[task_id] = address of the task function, or a task object, or a new task object, etc.
   }
 }
};

Task can just be a function pointer, or a virtual interface:

class Task {
 virtual void execute() = 0;
};

void execute_plan(plnnr::Task_Frame* tasks, uint32_t num_tasks)
{
 for each task {
   m_tasks[task.task_type]->execute(...);
 }
}

With that scheme you can also implement reloading of a domain code.

Vitalfever commented 8 years ago

Hello again. In an earlier response you said in relation to planning every few frames that:

If there's a current plan and the found plan has no continue task, abort the current one. Start executing the found plan or continue with an old one.

What is a continue task?

Sorry for all the silly questions, and thank you again for all your help. This planner is incredibly fast.

alexshafranov commented 8 years ago

Hi @Vitalfever,

Continue task is a way to indicate you want to keep the current plan as part of the planning process itself.

fact plan( int32 )    // current plan 

const Plan_Defend = 1   // plan names

prim continue!()
prim plan!( int32 ) // primitive task which updates `plan` fact

task defend()
{
   case plan( Plan_Defend ) -> [ continue!() ] // already defending, let's not interrupt

   case <...> -> [ plan!( Plan_Defend ), <...> ] // one of the tasks in the defend plan is to set `plan` fact
}

There're a few shortcomings currently with this:

So as plnnr::find_plan doesn't support built-in continue task yet, you'll need to handle that in your code.

plnnr::find_plan_step is a single iteration of the planning loop (see the source code for plnnr::find_plan):

    find_plan_init(self, domain);

    Find_Plan_Status status = find_plan_step(self, db);
    while (status == Find_Plan_In_Progress)
        status = find_plan_step(self, db);

    return status;

plnnr::find_plan_step returns every time a new compound or primitive task is added to the stack.

You can check if continue was added to the primitive task stack and abort the loop:

<custom_planning_loop> {
    while (true) {
        if (has_continue(self))
           return <escape the loop and keep the current plan>

        if (status != Find_Plan_In_Progress)
            return <escape the loop and start a new plan or handle failure, see Find_Plan_Status>

        status = find_plan_step(self, db);
    }
}

bool BrainComponent::has_continue(const Planning_State* self)
{
  return plnnr::size(self->task_stack) > 0 && is_continue(plnnr::top(self->task_stack));
}

bool BrainComponent::is_continue(const plnnr::Task_Frame* task)
{
  return <check if the task is continue! using task->task_type>
}

Note that there was a bug where plnnr::find_plan_init wasn't resetting the previous loop state. It's fixed in https://github.com/alexshafranov/derplanner/commit/d1042ba74eea856891132e1dc51cbcad6662f8c3

Thank you for your questions, feel free to ask more. It will help me write the more detailed readme :-)

Vitalfever commented 8 years ago

So in the above example,

[ plan!( Plan_Defend ) ]

sets the plan fact to the value of Plan_Defend? Is there a nice way to get the current values in your tables?

alexshafranov commented 8 years ago

Yes, plan! is a task receiving an integer argument that needs to be implemented on the game side.

The API for Fact_Database isn't complete as it needs to be, so at the moment getting entry requires using Fact_Table data directly.

Vitalfever commented 8 years ago

Yes, plan! is a task receiving an integer argument and needs to be implement on the game side.

How can I get the integer argument from the plan! task call into the game code? IE, how will the game code know the planner has called plan! with a value of 1, or 2, etc?

That's fine, it's not so difficult to get the values as it is now.

alexshafranov commented 8 years ago

For the game code plan! task is the same as any other task. So each task in a plan is a Task_Frame object.

Task_Frame has an void* arguments; pointer.

Check print_plan in https://github.com/alexshafranov/derplanner/blob/d1042ba74eea856891132e1dc51cbcad6662f8c3/examples/travel.main.cpp for details on how to read the arguments using that pointer.

Basically it's going to be a plnnr::as_Int32(arguments, layout, 0) call for the task declared as plan!( int32 )

Vitalfever commented 8 years ago

Ah I didn't see that, I should look through all the overloads next time. Very clever, thank you once more, you've been very helpful.

makuto commented 7 years ago

Hey Alex, Your code is great! Thank you for releasing this with a permissive license.

I do have some questions about how derplanner fundamentally works.

Is the state/facts meant to be immutable, and is all mutability meant to be expressed in the task arguments? i.e., if the plan you ended up with was such: find_ingredients!(kitchen) -> cook_ingredients!(eggs, bacon) where find_ingredients!() added eggs and bacon to the facts, would that find_ingredients!() have to be some special case in a planning loop, like continue!(), so that it could modify the state/facts?

Put another way, if you had a plan which was find_item!(myLocation, itemToFind) -> walk_to(myLocation, destination) where find_item!() changes myLocation, how can walk_to know that myLocation has changed?

I'm trying to wrap my head around how derplanner works without postconditions/effects/mutable world state during planning.

alexshafranov commented 7 years ago

Hey @makuto,

You can add a special case in the planning loop or add a task which is modifying fact database. (like for example you can have a task to request a path finding query)

But fundamentally in the current version there's no effects, thus the planning process doesn't modify facts and it's doesn't backtrack over those modifications.

So it won't be able to solve a blocks world problem :) It's a reactive planner similar to parametric behavior tree (I like to think of it as of a family of BTs)

Your cooking example would be:

task cook()
{
  case ingredient(Eggs) & ingredient(Bacon) -> [ cook_egg_n_bacon_pie() ]

  case <something else>
}
makuto commented 7 years ago

Ah, okay. I was looking at it as if it were a STRIPS-esque hierarchical task network, which was what I was looking for.