Quest task design

Hi

I’m interested to know how you would design quest tasks.

I want to define each quest and its tasks in a groovy script. A quest can have multiple tasks that player must fulfill them. Tasks can be of type Kill, Defend, Escort, Delivery, Collect, Activate, Carry, Construct, Escape, Retrieve, Sell,…

How would you handle the task? Would you define each task as a component and a relevant system to manage and keep track of task states? or would do all stuff inside the quest script?

Regards

I’m still in a bit of earlier stage of development with my RPG game so I haven’t thought about this yet.
But I think it alll boils down to one question.

Are you able to handle all task types just by listening to events fired by other parts of the game? For example when a monster is killed I fire an appropriate event. In your case you would use a system to watch for a component marking that event or something. But the idea is the same.

If you can do that I think you don’t need scripts. If you can’t you need scripts. And when I say scripts I mean specific logic for specific tasks not the script with list of tasks.

1 Like

Yes, using Entity Component System (with Zay-ES), I am able to handle that, either by using a EntitySet to batch listen to all events or using a WatchedEntity to listen to an specific entity.

I still need a script to do the logic. For example, what to do when a specific task completed or failed, I might need to spawn a bunch of entities or remove a bunch of other entities or activates another task,…

Yes for that you need a script, but that script is not in any way related to determining if the task is complete or not. It is executed when some other system decided the task is complete.

At least that’s what I thought you were asking about. Watching for the completion.

1 Like

And you can always create a “Custom task” which would contain a script with the completion logic. This script would have to be run every frame and should be reserved for a very specific things that cannot be handled by watching for some standard event.

At least these are my thoughts. Thanks for bringing this topic up. I was trying to push myself to look into it for some time already :smiley:

1 Like

I think that you want a way to design the game with high level logic, which require an abstraction layer. I don’t think you can get it easily “for free”.

This is not a big problem as I will handle it in the quest script.

Edit:

My concern is about how to implement the task itself.

For example, let’s consider the Kill task.

In the kill task, I am checking if the target is dead then task is over.

I can think of two different ways to implement it:

1- With a component/system approach

Kill task is a component and keeps id of the target entity that must be killed.

And I will need a system that has two entity sets, one for kill tasks (all entities that have Kill component on them) and one set for dying entities (decayed entities).

When an entity gets decayed I will check if there is a kill task listening for that and if so I mark the task as done.

2- Implement the task as a self-contained script

A script with a run() method that is being updated every frame and it uses a WatchedEntity that listens for a decay component on the target entity that must die and after it is dead the task is done.

I am not sure which way I should use :thinking:

Well, I do not use Zay-ES because I am using my Outside Engine, but it is setup to create quests with groove scripts. For a kill based quest, the quest will register a listener on the npc, and onDeath, the quest will check what killed the npc, and if it was the player, then the quest can move to the next step, or complete. It is important in Outside to check what killed the npc as it is a mmo engine. In the Outside Engine almost everything has the ability for listeners, and all quests are just steps triggered by listeners, but in completely self contained scripts for maintainability.

2 Likes

@tlf30 thanks for sharing your approach on this. It is helpful. :slightly_smiling_face:

Now I can see a bunch of ways I can take:

1- Using a regular listener (similar to what @tlf30 does): A task adds a listener to a system to get notified. (for example when an entity dies).

2- Using EventBus (similar to what @raistm does): A system will fire an event using EventBus and from the task, I register a listener to EventBus to get notified

3- Using ES based events: A system creates an event entity and adds a component to it, and inside the task script, I create an EntitySet and listen to the event, but does this infer a quest task should be a system? so I am back to this question

@pspeed may I have your help as well? :slightly_smiling_face:

To be honest, I haven’t given this much deep thought yet but there seems to be two kinds of quest state:
!. Has some condition been satisfied when you need to do something. (ie: do you have the special key for the door, have you talked to NPC 1 before NPC 2, etc.)
2. Some condition triggers some other things to happen

I think about things from the perspective of “giant infinite worlds” and so I would try to put as many things in category (1) as possible. There is no overhead in that case. The player does something that requires the check and then something else happens. New dialog option, door opens, whatever.

Even some things you’d think would be in (2) can sometimes be moved to (1). “Retrieving the gem at the end of the dungeon spawns some critters at the beginning of the dungeon.” sounds like it would be (2) but if the game has ‘spawn areas’ or ‘spawn triggers’ then that could be conditional based on the gem being gone or whatever. So entering the encounter trigger would check for the other conditions.

…and that’s perhaps a poor example anyway because in Mythruna I could drop custom scripts on item events. So retrieving the gem means I “picked up” the gem which was an item event that could have had a custom script on it.

So a lot of it is “it depends”.

Listeners are usually the devil, though… especially if you add them ad hoc. You have to remember to clean them up again, they spawn a little garbage every time they are run, etc… In an ES, at the game level, it’s rare that I would even consider listeners.

…but I guess in the end the line between a script handler on an item and a listener is a fine one.

Another example while I’m here. “When all 10 critters X die, spawn in giant critter Y” There are multiple ways to attack that, too… either the spawn trigger manages its children (part of a system that maintains spawn trigger conditions… after all some spawns may require constant amounts of some monster). In which case the ‘closure’ of one spawn trigger is the condition that the other spawn trigger is waiting for. Alternately, sometimes the AI is managing ‘groups’ of mobs whether to slot them to avoid having all 10 attack the player at once or because there is some level of AI coordination. In which case, as each one dies they hand some state off to the next one. The last mob who dies has all 10 of the states and so his loot drop script would also spawn the boss.

There are a variety of ways to look at a problem but if you can structure them all in some player triggered or item triggered way (which can’t happen anyway except for some player interaction) then it means your world has no overhead where the player isn’t. And for an infinite world, that’s pretty crucial… which is why I think that way.

1 Like

Thanks for detailed explanation. It’s much appreciated. :heart:

Is item event fired from ES (represented by an entity)? …or from an EventBus?

It’s not an “event” in the “listener” sense. It’s an event in the human sense… something happened.

Game objects have a list of ‘methods’ that are dynamically defined in groovy scripts. For example, a chest may have an “open” method and a “close” method that does some action. A log may have a “light” method while a lit log would have an “extinguish” method.

The game calls certain “methods” when objects are interacted with.

So when a player tries to pick up an object, the “take” method’s script is run (I don’t remember if ‘take’ is the actual name I used or not… it’s been a while). The default ‘take’ script for all objects transfers the entity from the world to the player’s inventory. But the script can be overridden by specific object types to do other things… electrocute the player, spawn monsters, or simply not allow itself to be picked up.

This started out as wanting to have dynamic popup menus for when the player right clicked on objects and just kind of extended from there. In the unreleased version 2 engine, even the tools/wands you held in your hand were activated this way… by calling the ‘activate’ method.

I’m not sure how dominant this system will be in the new engine yet but it will definitely be there.

1 Like

I see now, that’s cool :slightly_smiling_face:

Thanks so much for helping me out.

Would you handle the kill event the same way? For example, using an “OnDie” method on object script. (Let’s say I want to automatically mark a quest task completed when an specific object dies.)

Let’s say I have a CombatSystem that deals damages and I have a HealthSystem that applies damages to entity’s health, so the HealthSystem would know when an entity dies then it will look into the object “methods” list and if that object has an “OnDie” method, then runs it. Right?

Yep… the default behavior for “OnDeath” would be to drop loot.

1 Like

Are these “methods” registered for a specific object type as a Function/Closure when loaded from groovy scripts?

For example something like a registry class?

public interface ObjectActions {

     public void register(ObjectType objectType, String actionName, Function<EntityId, Void> action);

     public Function get(ObjectType objectType, String actionName);

     public void run(String actionName, EntityId eId, EntityData ed);

}

It’s something like that. I’d have to look at the code (on another computer) to be more specific.

1 Like

I created a registry of object actions

/**
 * A registry of object actions that can be looked up by object type and action name.
 *
 * @author Ali-RS
 */
public interface ObjectActions {

    /**
     * Registers an action to specific object type.
     */
    public void register(String objectType, ObjectAction action);

    /**
     * Returns the action registered to specified object type, or defaultAction if it contains no mapping.
     */
    public ObjectAction getOrDefault(String objectType, String actionName, ObjectAction defaultAction);

}

and an interface for object action

public interface ObjectAction {

    public String getName();

    public void run(EntityId eId, Object... args);

}

And in groovy scripts, I will implement actions and register them to ObjectActions registry which is provided to them via binding.

Please let me know if there is something I am missing :slightly_smiling_face:

I think ‘action’ is what I called it, too.

If I get time, I will try to look at the code from the mark 2 engine and see if there is anything useful that I can post. I haven’t gotten this far on the MOSS stuff yet.

2 Likes

Thanks