A list of commands for units in 2D RPG, a way to handle it.

Started by
5 comments, last by BaneTrapper 9 years, 3 months ago
Hello.
So i am up to this seemingly tough problem, i would like some opinions, and thoughts on how to solve it, or deal with it in a good(Efficient) way.
I have 2D Rpg, where AI can do specific tasks, but i cannot tell it to do a list of them.
For example: I want to do this:
Give unit a order: Go talk to quest_giver_person, receive quest, and finish it.
That would be something like this code vise:

//Unit* unit;
//TheList->
0: unit->moveToTargetUntilInRadius(quest_giver_person.position, glob::COMMUNICATION_RADIUS);//To be very simple, this gets you to target until you are in radius given
1: unit->getQuestFrom(quest_giver_person);//AI Logic handles this, and gets quest
Now we let this execute, we start from 0: this would make the unit generate a path to the given target so the list of commands would look like
We also, regenerate the path every so often, so we actually get to the target if he repositioned.

//Unit* unit;
//TheList->
0: unit->moveUntilPathFinished();//Follows generated path, handles exceptions like collision that shouldn't occur, but gets you to target location in the end
1: unit->moveToTargetUntilInRadius(quest_giver_person.position, glob::COMMUNICATION_RADIUS);//To be very simple, this gets you to target until you are in radius given
2: unit->getQuestFrom(quest_giver_person);//AI Logic handles this, and gets quest
Now 0 command finished, it gets erased and we continue

//Unit* unit;
//TheList->
0: unit->moveToTargetUntilInRadius(quest_giver_person.position, glob::COMMUNICATION_RADIUS);//To be very simple, this gets you to target until you are in radius given
1: unit->getQuestFrom(quest_giver_person);//AI Logic handles this, and gets quest
Now we are in quest_giver_person radius so 0 also gets errased so we get left with

//Unit* unit;
//TheList->
1: unit->getQuestFrom(quest_giver_person);//AI Logic handles this, and gets quest
This would generate a new list of stuff for the AI to do.
I hope this explains the principle of what i am trying to achieve. I will be active if you have questions, or if i missed some crucial data, i will supply it asap!
How the unit is structured, and some functions that would be used in this example:


int main()
{

}
int a = 1;
double d = 2;//REGISTER THIS AS C++ SNYTAX PLS!
//Unit.h
namespace uni
{
enum MovementType
{
MTNone = 0,
MTPath = 2,
MTKeysButton = 3,
MTPatrol = 4
};
};
 
class UnitMovement
{
public:
uni::MovementType moveType;
glob::Direction movDirectionX;
glob::Direction movDirectionY;
bool isMoving;
double moveSpeed;
std::vector<sf::Vector2f> listPath;
int moveTargetVecID;//Target vecID to follow
glob::Direction facingDirection;
int patrolVecID;//This is ID, that indicates to which note in listPath to move to
int patrolIncrementValue;//This is to know if moving forward on path or backwards
unsigned int patrolAmountOfTimes;
 
UnitMovement(double MoveSpeed);
UnitMovement(double MoveSpeed, const std::vector<sf::Vector2f> & ListPath);
void setMoveToLocation(Object & me, const sf::Vector2f & position);
void setMoveToLocationSmart(Object & me, const sf::Vector2f & position);//Uses AStar
void setAllMovementToHalt();
void setPatrolToLocation(Object & me, const sf::Vector2f & position, unsigned int TimesToPatrol = glob::MaxUI);
void setPatrolPath(Object & me, std::vector<sf::Vector2f> & listPath, unsigned int TimesToPatrol = glob::MaxUI);
void AssignFollowTarget(Object & obj);
void SetFacingDirection(glob::Direction dir, Object & me);
};
class Unit : public UnitMovement, public UnitBattle ...
{
public:
    LoopUnit();//Loops unit specific stuff like movement, following paths, and executes it on the unit effectively causing un
//it to reposition, and calls LoopUnitAI()
    LoopUnitAI();//Loops unit AI specific stuff if unit controller is AI
};
 
//Object.h
class Object
{
public:
    std::unique_ptr<Unit> unit;
};
class ObjectHolder
{
public:
    std::vector<Object> listObject;
    LoopObjects();//Handles objects logic, and calls LoopUnit()
};

I gave it a though with teammate, and the result is pretty poor, it will work but i don't like that:
How it would work is:
Have a function that would take action, and be able to tell unit what to do with the action.
For example:


 
int a = 0;//C++ syntax PLS!
enum Command
{
    none = 0,
    moveToTargetUntilInRadius = 1,
    getQuestFrom = 2
    //... 
};
 
class Action
{
    Command commandType;
    //Whatever std::bind requires to be stored
    Action(Command CommandType, whatever_std::bind_requires);
}
 
//Will probably use std::list for the final product, but i haven't used list in ages so i'l use vector in this example
std::vector<Action> listActions;//This would be the list that unit has
 
//This is how it would add stuff to the list
listActions.push_back(Action(Command::moveToTargetUntilInRadius, std::bind(target_unit->objVectorID)));

And this should work, i need to prototype it first, but its idea we came up with.

Advertisement

Hello BaneTapper,

am I understanding that you need suggestions on how to design your class?

Maybe showing the whole implementation here is overkill.

Can you describe in words what you want the classes to do, how you tried to

implement it and why that is not elegant or efficient enough in you opinion?

Regards

Henry

I smell a lot of extremely bad design in just the snippets you've posted, but it's hard to know how deep to dive... I suspect that getting a clean architecture might entail rewriting an awful lot of your code. Not to say it isn't worth it - but you have to decide for yourself how important it is to you to get a clean design versus finishing soon.


For the actual game logic/scripting/high level rules system, consider something like this:

struct ActionInterface {
    enum ActionStatus {
        ACTION_STATUS_DONE,
        ACTION_STATUS_CONTINUE,
    };

    virtual ActionStatus Execute (Unit* boundUnit) = 0;
};

struct ActionMove : ActionInterface {
    Position TargetPosition;

    ActionStatus Execute (Unit* boundUnit) override {
        if (Distance(TargetPosition, boundUnit->GetPosition()) > SOME_THRESHOLD) {
            boundUnit->AdvanceToward(TargetPosition);

            return ACTION_STATUS_CONTINUE;
        }

        return ACTION_STATUS_DONE;
    }
};

struct ActionGetQuest : ActionInterface {
    ActionStatus Execute (Unit* boundUnit) override {
        QuestGiver* qg = QuestManagement::GetNearestQuestGiver(boundUnit->GetPosition());
        if (qg)
            qg->GetQuestForUnit(boundUnit);

        return ACTION_STATUS_DONE;
    }
};
Your "update unit" code basically does exactly what you described: if an action is in ACTION_STATUS_DONE, it gets popped from the order queue, and the next time the code runs we start the subsequent order. If an action returns ACTION_STATUS_CONTINUE, we assume it isn't ready to move on yet, and simply proceed to updating the next unit.

It is important to note that (without building a coroutine engine) you need to separate your unit updates into two phases: planning and execution. In the former, we call a sequence of functions that puts orders into the order queue. In the latter, we look at the orders in the queue and attempt to follow the current "top" order.


Final thought - having nested order queues is very powerful. For example, if I want to treat "go to the nearest quest giver and get a quest and then try to beat the quest" as a single atomic order, I could do that in two ways. Either I build a really complex ActionDoTonsOfComplicatedStuff object, or I write a single ActionDoMultipleActions. ActionDoMultipleActions is just a copy of the code that invokes ActionInterface::Execute(). Using this nested structure I can create arbitrarily rich flows between orders (oops, I can't do X! Better pop out a level and do Y instead!) and so on. You can even build more complex logic in if you want, so that orders can cancel each other, suggest other courses of action, etc.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


glportal, on 17 Jan 2015 - 11:38 PM, said:
Hello BaneTapper,
...
Hello to you too Henry.
What i am doing is AI for units, at time of postings units knew how to do tasks(move to location, attack, idle, check if enemy radius...)
The engine handles units data, and reacts accordingly, if unit flag for movement is set to move on given path, it will read from units path list, and lead unit on the path until the path list is done.
What i wanted for units to have is memory of their actions, here is a example why:
Unit is assigned a path, and is walking on it. If unit spots a enemy in sight radius it will decide, should it run, or fight the spotted enemy, now i can set flags for movement to walk to enemy, or run away from the enemy, but in process i lost the path unit was walking before.
That was my problem i was trying to solve, i decided unit needs a action list. With action list, if unit was walking a path, and spotted a enemy. It would push a new action on top of the stack, thus remembering its previous actions, while allowing new ones to execute.
Now in the first post i really did try hard to simplify it, i am doing it right now, when unit spots a enemy unit allot of actions are set, but i was very simple(stating it would only push one action) for sake of understanding.

ApochPiQ, on 18 Jan 2015 - 12:18 AM, said:
I sm...
I started this project with little design, and on top of all i am actually self learned, and read no books on programming or design. I get stuff to work, and that is what i want to achieve. But i do watch lots of educational videos on programming in general, and reading lots of topics to keep up to date with the programming world.
The way you designed code, i attempted it when i was starting project, but it just didn't fit, the way i am doing it right now is easier for me, and also is less coding from what i notice, that is my opinion.
I suffer from lack of knowledge to explain, and additionally inexperience.
How i handle the "go to the nearest quest giver and get a quest and then try to beat the quest" is:
I first call a function getPathToUnit(position_start, unit_ID); From unit id i track position all time because the unit will reposition in most cases.
The function handles the path generation. Lets start_pos unit is in tavern in town One, and unit_ID unit position is in its House in town Two. The action list ends up looking something like this:

setMoveToEntranceOfBuilding()
setMoveToEntranceOfTown()
setMoveTravelToTown_name(Town_Name)
setMoveToTarget(Unit_ID)
initateDialogWith(Unit_ID)
getQuestFrom(Unit_ID)
But if unit was next to the target the list would look approximately to

setMoveToTarget_Until_In_Radius(Unit_ID, Dialog_Radius)
initateDialogWith(Unit_ID)
getQuestFrom(Unit_ID)
So that is not a one call, because that would be a real nightmare to make. Possible, but unnecessary work, because that big function can be cut up intro many small ones, and they can be reused.
I have made a prototype, and i am happy enough to go with it, i want to finish the game. Or at least make the AI be smarter then a empty box.
I will be posting full code when i'm done with prototype, to show how iv'e done it.
Why did you ask for opinions and suggestions if you don't want to listen to anyone's opinions or suggestions?

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


Final thought - having nested order queues is very powerful. For example, if I want to treat "go to the nearest quest giver and get a quest and then try to beat the quest" as a single atomic order, I could do that in two ways. Either I build a really complex ActionDoTonsOfComplicatedStuff object, or I write a single ActionDoMultipleActions. ActionDoMultipleActions is just a copy of the code that invokes ActionInterface::Execute(). Using this nested structure I can create arbitrarily rich flows between orders (oops, I can't do X! Better pop out a level and do Y instead!) and so on. You can even build more complex logic in if you want, so that orders can cancel each other, suggest other courses of action, etc.

Yep. One way of implementing such a "nested order queue" (in principle only, because it were not really a queue) with break points and alternatives and prioritization of what to do and ... already build-in is the behavior tree (just to throw in this term ;) )


I have made a prototype, and i am happy enough to go with it, i want to finish the game. Or at least make the AI be smarter then a empty box.

It is okay if this is your goal, and/or if the game is a, say, personal study on the way to evolve. But in the end I'm seconding ApochePiQ, in both roles as a developer as well as a player. An NPC that is not able to react on world state changes is what is looking really stupid. If a NPC follows a once planned action list stubbornly without the ability to break and restart elsewhere, then noticeable chances are that the NPC gets stuck somewhere and try forever, and a player observing the NPC simply shakes the head. You have to answer the question how probable that is and how much the game will suffer if it occurs. Worst case, of course, is that the game will be unplayable. With this in mind, having a game finished is not to be judged on the criterion of superficially seen code completeness, but also on its usability as a game.

I accidentally clicked away pressed backspace and lost about 30minutes of text(20 lines) :| so here we go again... For some reason the written text was not saved when i got back in wacko.png I will shorten it up.

Why did you ask for opinions and suggestions if you don't want to listen to anyone's opinions or suggestions?

I have thoroughly read your opinions, and i have even added a check for actions as you have, i stated i would be posting my code later on smile.png. At the time i was making first post i was going with templates, and variadic template for the list. It wasn't working at all. As a matter of fact, the way i am doing it is very similar to yours ApochPiQ, i would go as far to say the idea is exactly the same, i just implemented it in a different way, its implemented in a way to suit my code design that is already in place so i don't rework it all.


Final thought - having nested order queues is very powerful. For example, if I want to treat "go to the nearest quest giver and get a quest and then try to beat the quest" as a single atomic order, I could do that in two ways. Either I build a really complex ActionDoTonsOfComplicatedStuff object, or I write a single ActionDoMultipleActions. ActionDoMultipleActions is just a copy of the code that invokes ActionInterface::Execute(). Using this nested structure I can create arbitrarily rich flows between orders (oops, I can't do X! Better pop out a level and do Y instead!) and so on. You can even build more complex logic in if you want, so that orders can cancel each other, suggest other courses of action, etc.

Yep. One way of implementing such a "nested order queue" (in principle only, because it were not really a queue) with break points and alternatives and prioritization of what to do and ... already build-in is the behavior tree (just to throw in this term ;) )


I have made a prototype, and i am happy enough to go with it, i want to finish the game. Or at least make the AI be smarter then a empty box.

It is okay if this is your goal, and/or if the game is a, say, personal study on the way to evolve. But in the end I'm seconding ApochePiQ, in both roles as a developer as well as a player. An NPC that is not able to react on world state changes is what is looking really stupid. If a NPC follows a once planned action list stubbornly without the ability to break and restart elsewhere, then noticeable chances are that the NPC gets stuck somewhere and try forever, and a player observing the NPC simply shakes the head. You have to answer the question how probable that is and how much the game will suffer if it occurs. Worst case, of course, is that the game will be unplayable. With this in mind, having a game finished is not to be judged on the criterion of superficially seen code completeness, but also on its usability as a game.

I have added a check system for actions, it will handle those cases if unit got stuck intro a corner of house and surrounded by objects, by asshole player. It will get it self out. If it has to destroy the objects player placed, then it can. Its handling cases like those when unit is pathing, and there is no path to the end position. It can modify the world in order to get there. If it means start hacking away at a big mountain so be it! although to prevent stupid cases like that i got a road system, and units while traveling from location to location will stick to the road.
If even the road got blocked by a troll played that put allot of boxes on the road to block it. Unit would try to find path using pathfinding from last path node to next one, and last node to the final one. If it still failed, well then its the mountain hacking scenario.

But for a person doing AI first time, its a hard task getting AI to even work, making it smart is very complex, but doable.

This topic is closed to new replies.

Advertisement