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…