Quest task design

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?

I think that depends on the game. I think if you are storing state then you’d probably want them stored. I could also imagine AI implementations that would want to reassess where they are on reload and would maybe get confused by having the state already set… but I’d also say they were broken. :slight_smile: