Quest task design

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

Are you doing a similar thing on the client side as well, for applying animations and effects to entities based on their object types?

I’m not sure what you mean.

Like the opening and closing of a chest would be triggered by their open/close scripts.

Consider a client/server setup

For example when you pick up a sword, in the server you will call the “Pickup” script on it which may put it in inventory or complete a quest or… all done on the server-side.

now say you also want to play a specific animation, a particle, or a specific sound when that sword is picked up so do you have a “Pickup” script registered for that sword on the client-side that will be triggered when the sword is picked up which will plays those effects?

I never got that far.

But you if you are familiar with my bullet character demo, you can see that as much as possible, I would try to be context-reactive. I haven’t thought in detail about what that means to pick up an object.

…but even within that I guess my thinking was to have some kind of ‘animation state’ (not an app state) that a client system could interpret how it liked. Hmmm… I want to say that I’ve coded something like this somewhere for some demo/prototype. But it’s all fuzzy.

Sorry I can’t be more specific at the moment.

Hmmm… I guess it was the unreleased Spacebugs prototype I was working on with nehon before he split.

/**
 *  Indicates the type of action that a parent entity is engaged in.
 *  This is generally always combined with a Parent component that denotes
 *  the entity to which this mobility state applies.  A parent can have
 *  multiple action states at a time but generally there will only be one
 *  that is animated.
 *
 *  @author    Paul Speed
 */
public class CharacterAction implements EntityComponent {

    private int id;

    protected CharacterAction() {
    }
    
    public CharacterAction( int id ) {
        this.id = id;
    }
    
    public int getCharacterActionId() {
        return id; 
    }
 
    public String getCharacterActionName( EntityData ed ) {
        return ed.getStrings().getString(id);
    }
    
    public static CharacterAction create( String name, EntityData ed ) {
        return new CharacterAction(ed.getStrings().getStringId(name, true));
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + id + "]";
    } 
}

And here is the crappily coded client side system that goes with that (among other things):

public class MobAnimationState extends BaseAppState {

    static Logger log = LoggerFactory.getLogger(MobAnimationState.class);

    private EntityData ed;
    private ModelViewState models;
    
    // Keep track of the mob animation objects we've already created 
    private Map<EntityId, MobAnimation> mobIndex = new HashMap<>();
    
    // Keep a list of the mob animation objects for convenient per-frame updates.
    private SafeArrayList<MobAnimation> mobs = new SafeArrayList<>(MobAnimation.class);
    
    private MobilityContainer mobStates;
    private ActionContainer mobActions;

    public MobAnimationState() {        
    }
    
    @Override
    protected void initialize( Application app ) {
        this.ed = getState(ConnectionState.class).getEntityData();
        this.models = getState(ModelViewState.class);
        
        this.mobStates = new MobilityContainer(ed);
        this.mobActions = new ActionContainer(ed);        
    }
    
    @Override
    protected void cleanup( Application app ) {
    }
    
    @Override
    protected void onEnable() {
        mobStates.start();
        mobActions.start();
    }
    
    @Override 
    public void update( float tpf ) {
        mobStates.update();
        mobActions.update();
        
        for( MobAnimation mob : mobs.getArray() ) {
            mob.update(tpf);
        }
    }
    
    @Override
    protected void onDisable() {
        mobActions.stop();
        mobStates.stop();
    }
 
    protected MobAnimation getMobAnimation( EntityId id ) {
        return getMobAnimation(id, true);
    }
    
    protected MobAnimation getMobAnimation( EntityId id, boolean create ) {
        MobAnimation result = mobIndex.get(id);
        if( result != null || !create ) {
            return result;
        }
        result = new MobAnimation(id);
        mobIndex.put(id, result);
        mobs.add(result);        
        return result;
    }
 
    protected void removeMobAnimation( EntityId id ) {
        MobAnimation mob = mobIndex.remove(id);
        mobs.remove(mob);
    }
 
    protected void addMobility( EntityId parent, Mobility mobility ) {
        getMobAnimation(parent).addMobility(mobility);
    }
    
    protected void removeMobility( EntityId parent, Mobility mobility ) {
        getMobAnimation(parent).removeMobility(mobility);
    } 

    protected void addAction( EntityId parent, CharacterAction action ) {
        getMobAnimation(parent).addAction(action.getCharacterActionName(ed));
    }
    
    protected void removeAction( EntityId parent, CharacterAction action ) {
        getMobAnimation(parent).removeAction(action.getCharacterActionName(ed));
    } 
 
    /**
     *  Manages the animation state for a specific MOB.
     */
    private class MobAnimation {
        private EntityId id;
        private Set<Mobility> mobility = new HashSet<>();
        private Mobility primary;
        private String currentBase;
        private String action; // only one at a time right now
 
        private Spatial model;       
        private Spatial animRoot;
        private AnimComposer animComposer;
        private Action animAction;
        private double lastSpeed;
        
        private Vector3f lastLocation = new Vector3f();
        private Quaternion lastRotation = new Quaternion();
        private Vector3f velocity = new Vector3f();
         
        
        public MobAnimation( EntityId id ) {
            this.id = id;
        }

        protected Spatial getAnimRoot() {
            if( animRoot == null ) {
                this.model = models.getModel(id);
                if( model == null ) {
                    return null;  // have to wait until later I guess.
                }
                
                // Find spatial with the composer
                // For the moment, we'll guess
                animRoot = ((Node)model).getChild("Root");
                
                lastLocation.set(model.getWorldTranslation());
                lastRotation.set(model.getWorldRotation());
            }
            return animRoot;
        }

        protected AnimComposer getAnimComposer() {
            if( animComposer == null ) {
                Spatial s = getAnimRoot();
                animComposer = s == null ? null : s.getControl(AnimComposer.class); 
            }
            return animComposer;
        }
        
        public void addMobility( Mobility m ) {
            mobility.add(m);
            
            // Just override whatever is there for now... I don't remember why I allow
            // multiple mobilities but when we manage how we pick which one is active
            // or layered at any given time then we can also use its base speed, etc.
            primary = m;
        }
        
        public void removeMobility( Mobility m ) {
            mobility.remove(m);
            if( mobility.isEmpty() ) {
                // Remove ourselves from being managed... there is no mobility component set anymore
                removeMobAnimation(id);
            }
        }
        
        public void addAction( String a ) {
            if( Objects.equals(a, this.action) ) {
                return;
            }
            this.action = a;
            
            // Set the animation on the character
        }
        
        public void removeAction( String a ) {
            if( !Objects.equals(a, this.action) ) {
                return;
            }
            this.action = null;
            
            // Stop the animation on the character
        }
 
        protected void setBaseAnimation( String a, double speed ) {
            AnimComposer ac = getAnimComposer();
            if( ac == null ) {
                return;
            }
            if( Objects.equals(a, currentBase) ) {
                if( animAction != null ) {
speed = Math.round(speed * 10) / 10.0;                
                    if( speed != lastSpeed ) {
//System.out.println("   reset anim speed:" + speed + "   lastSpeed:" + lastSpeed);
//System.out.println("   reset anim speed:" + speed);
                        animAction.setSpeed(speed);
                        lastSpeed = speed;
                    }
                }
                return;
            }
            this.currentBase = a;
//System.out.println("*********  Starting animation:" + a + "  at:" + speed);            
            this.animAction = ac.setCurrentAction(currentBase);
            animAction.setSpeed(speed);
        }
 
        /*private int filterSize = 60; //15; // 1/4 of a second       
        private double[] speedFilter = new double[filterSize]; 
        private int filterIndex = 0;
        private double filterTotal = 0;*/
 
        private Filterd lowPass = new SimpleMovingMean(10); // 1/6th second of data        
        
        public void update( float tpf ) {
            //System.out.println("mobility:" + mobility + "  action:" + action);
 
            Spatial s = getAnimRoot();
            if( s == null ) {
                return; // nothing to do
            }
            
            // Right now since we can't layer... an action will override
            // any mobility
////System.out.println("action:" + action);            
            if( action != null ) {
                setBaseAnimation(action, 1);
                return;
            }
                       
            // See what kind of movement is happening
////System.out.println("current:" + s.getWorldTranslation() + "  last:" + lastLocation);            
            velocity.set(model.getWorldTranslation()).subtractLocal(lastLocation);
            
            // We don't account for up/down right now
            velocity.y = 0;
//System.out.println("-- Velocity:" + velocity + "  old pos:" + lastLocation + "  new pos:" + model.getWorldTranslation());            
            // Track the current values for next time
            lastLocation.set(model.getWorldTranslation());
            lastRotation.set(model.getWorldRotation());

////System.out.println("Facing:" + lastRotation.mult(Vector3f.UNIT_Z));             
 
            float rawSpeed = velocity.length();
//System.out.println("Raw speed:" + rawSpeed);
            if( rawSpeed > 0.001 ) {
                float speed = tpf > 0 ? velocity.length() / tpf : 0;
//System.out.println("Speed:" + speed + "   tpf:" + tpf);
                // Just a simple heuristic for now
                if( speed > 0.01 ) {            
                    float forward = lastRotation.mult(Vector3f.UNIT_Z).dot(velocity);
//System.out.println("raw fwd:" + forward + "  tpf:" + tpf + "   adjusted:" + (tpf > 0 ? forward / tpf : 0));                    
                    forward = tpf > 0 ? forward / tpf : 0;
                    float left = lastRotation.mult(Vector3f.UNIT_X).dot(velocity);
                    left = tpf > 0 ? left / tpf : 0;
                    
                    //double normalWalkSpeed = 0.65; // trial and error, for the puppet
                    //double normalWalkSpeed = 0.5; // trial and error, for the  bug
                    double normalWalkSpeed = primary.getBaseSpeed();
//System.out.println("normalWalkSpeed:" + normalWalkSpeed);                    
    
                    double animSpeed = forward / normalWalkSpeed;
                    
//if( animSpeed > 1.1 ) {
//    System.out.println("*****************************");
//}                     
//System.out.println("Speed:" + speed + "  Walk fwd:" + forward + "   left:" + left + "  animSpeed:" + animSpeed);
                    //averageAnimSpeed = (averageAnimSpeed * 15 + animSpeed) / 16;
                    //animSpeed = averageAnimSpeed;
                    
                    // Pass the anim speed through a low-pass filter using
                    // a moving average
                    lowPass.addValue(animSpeed);
                    animSpeed = lowPass.getFilteredValue();
                    /*filterTotal -= speedFilter[filterIndex]; 
                    speedFilter[filterIndex] = animSpeed;
                    filterTotal += animSpeed; 
                    filterIndex++;
                    if( filterIndex >= filterSize ) {
                        filterIndex = 0;
                    }
                    animSpeed = filterTotal / filterSize;*/
                     
                    if( Math.abs(animSpeed) > 0.01 ) {
                        setBaseAnimation("Walk", animSpeed);
                        return;
                    }
                } 
            }
            
            // If nothing else set an animation then go back to idle
            setBaseAnimation("Idle", 1);
        }
    }
    
    /**
     *  Just need to keep track of the parent and value relationship
     *  so that we can properly remove the value when the tagging entity
     *  goes away.  Generally it's fields have been cleared so we don't
     *  have them anymore.
     */   
    private class ParentedComponent<T> {
        EntityId parentId;
        T value;
        
        public ParentedComponent(EntityId parentId, T value ) {
            this.parentId = parentId;
            this.value = value;
        }
    }
    
    @SuppressWarnings("unchecked")
    private class MobilityContainer extends EntityContainer<ParentedComponent<Mobility>> {
        public MobilityContainer( EntityData ed ) {
            super(ed, Mobility.class, Parent.class);
        }
 
        @Override
        protected ParentedComponent<Mobility> addObject( Entity e ) {
            Parent p = e.get(Parent.class);
            Mobility m = e.get(Mobility.class);
            addMobility(p.getParentId(), m);
            return new ParentedComponent<>(p.getParentId(), m);
        }

        @Override
        protected void updateObject( ParentedComponent<Mobility> object, Entity e ) {
        }
        
        @Override
        protected void removeObject( ParentedComponent<Mobility> object, Entity e ) {
            removeMobility(object.parentId, object.value);
        }
    }
    
    @SuppressWarnings("unchecked")
    private class ActionContainer extends EntityContainer<ParentedComponent<CharacterAction>> {
        public ActionContainer( EntityData ed ) {
            super(ed, CharacterAction.class, Parent.class);
        }
 
        @Override
        protected ParentedComponent<CharacterAction> addObject( Entity e ) {
            Parent p = e.get(Parent.class);
            CharacterAction a = e.get(CharacterAction.class);
            addAction(p.getParentId(), a);
            return new ParentedComponent<>(p.getParentId(), a);
        }

        @Override
        protected void updateObject( ParentedComponent<CharacterAction> object, Entity e ) {
        }
        
        @Override
        protected void removeObject( ParentedComponent<CharacterAction> object, Entity e ) {
            removeAction(object.parentId, object.value);
        }
    }
}

Edit: note because this is all part of an unfinished game… I can’t say that it’s “right”. It’s just where I was headed when development stopped. A mob could run/walk/shoot/wave, etc…

Hmm, yeah, thanks for reminding.

In your opinion does it sound right to make CharacterAction, Mobility, and Parent components a PersistentComponent?