JME Steering and ES design question (Zay-ES)

Hi

I am separating JME Steering behavior library which is integrated with MonkeyBrain library to rewrite it with ES structure.

Each steer behavior is a jme control which I am going to change it to AppState (Actually AbstractGameSystem because it is in server side)

The output of each steer behavior is a force. And an entity can have one or MANY steer behavior (ex : PursuitBehavior + ObstacleAvoidanceBehavior) . Each steer behavior system can run in separate thread.

So my question is what is the best way to gather these output forces, in my Physic system to calculate velocity for that entity ?

0- Each steering behavior system can add a force component to entity (agent) ? cons : breaking this ES rule : “Two systems should not be producing the same component type for the same entities.” so we are not considering this as an option :yum:

1- Each steering behavior system should generate an entity with a Force component which has a link to agent entityId ? Possible cons: Physic system needs to wait

2-Make Each steer behavior system to access to Body (and ControlDriver -> (BetterCharacterControl)) object from Physic System and apply velocity directly ? Possible cons : Steering systems and Physic System have tight coupling, also there might need for thread synchronizing.

3- You suggest please :slight_smile:

First… that seems pretty crazy.

Steering isn’t really something that needs to be in a separate thread.

I also don’t know that I’d do this in the ES either but I don’t know why it’s not just like player movement in the end.

Also, even presuming that the steering is done through the ES, it’s just one system. A system per steering behavior would be like having a separate system per AI behavior. It doesn’t really make sense. There may be any number of complex bits of logic to synthesize multiple steering outputs into one final steering value… and it’s not always going to be “just add them up”.

In the end, you have something that’s very similar to the way that the player controls their own character.

And I don’t see any reason at all to make it multiple threads. It’s just way overcomplicating it. You’d have to synchronize access to the world state and then synchronize again the steering output. And while the ES may do the low level threading bit for you, it doesn’t change the fact that your different steering would all be operating on different views of the world.

I was wanting to put them in other thread because they are all math calculation and all of them generate just a force as output which need to be added with outputs from other steers. (and this deceived me to do them in other thread)

see this example, i just need the output from calculateRawSteering()

/**
 * Returns a steering force to avoid a given obstacle. The purely lateral
 * steering force will turn our vehicle towards a silhouette edge of the
 * obstacle. Avoidance is required when (1) the obstacle intersects the
 * vehicle's current path, (2) it is in front of the vehicle, and (3) is within
 * minTimeToCollision seconds of travel at the vehicle's current velocity.
 * Returns a zero vector value when no avoidance is required. <br> <br>
 *
 * The implementation of obstacle avoidance behavior here will make a
 * simplifying assumption that both the character and obstacle can be reasonably
 * approximated as spheres. <br> <br>
 *
 * Keep in mind that this relates to obstacle avoidance not necessarily to
 * collision detection. <br> <br>
 *
 * The goal of the behavior is to keep an imaginary cylinder of free space in
 * front of the character. The cylinder lies along the character's forward axis,
 * has a diameter equal to the character's bounding sphere, and extends from the
 * character's center for a distance based on the character's velocity. <br>
 * <br>
 *
 * It is needed that the obstacles (Agents) have the "radius" atribute correctly
 * setted up.
 *
 * @see AIControl#setRadius(float)
 *
 * @author JesĂşs MartĂ­n Berlanga
 * @version 1.1.1
 */
public class ObstacleAvoidanceBehavior extends AbstractStrengthSteeringBehavior {

    private float minDistance;
    private float minTimeToCollision;
    private List<AIControl> obstacles;

    /**
     * @param obstacles A list with the obstacles (Agents)
     * @param minTimeToCollision When the time to collision is lower than this
     * value the steer force will appear. Time is measured in seconds.
     *
     * @throws SteeringExceptions.NegativeValueException If minTimeToCollision
     * is lower or equals to 0
     *
     * @see
     * AbstractSteeringBehavior#AbstractSteeringBehavior()
     */
    public ObstacleAvoidanceBehavior(List<AIControl> obstacles, float minTimeToCollision) {
        this(obstacles, minTimeToCollision, Float.POSITIVE_INFINITY);
    }

    /**
     * @param obstacles A list with the obstacles (Agents)
     * @param minTimeToCollision When the time to collision is lower than this
     * @param minDistance Min. distance from center to center to consider an
     * obstacle
     *
     * @throws SteeringExceptions.NegativeValueException If minTimeToCollision
     * is lower than 0
     *
     * @see
     * ObstacleAvoidanceBehavior#ObstacleAvoidanceBehavior(java.util.List, float)
     */
    public ObstacleAvoidanceBehavior(List<AIControl> obstacles, float minTimeToCollision, float minDistance) {
        super();
        this.validateMinTimeToCollision(minTimeToCollision);
        this.validateMinDistance(minDistance);
        this.minTimeToCollision = minTimeToCollision;
        this.obstacles = obstacles;
        this.minDistance = minDistance;
    }

    private void validateMinDistance(float minDistance) {
        if (minDistance < 0) {
            throw new SteeringExceptions.NegativeValueException("The min distance from an obstacle can not be negative.", minDistance);
        }
    }

    private void validateMinTimeToCollision(float minTimeToCollision) {
        if (minTimeToCollision <= 0) {
            throw new SteeringExceptions.NegativeValueException("The min time to collision must be postitive.", minTimeToCollision);
        }
    }

    /**
     * @see AbstractSteeringBehavior#calculateSteering()
     */
    @Override
    protected Vector3f calculateRawSteering() {
        Vector3f nearestObstacleSteerForce = new Vector3f();

        if (this.agent.getVelocity() != null) {
            float agentVel = this.agent.getVelocity().length();
            float minDistanceToCollision = agentVel * timePerFrame * this.minTimeToCollision;

            // test all obstacles for intersection with my forward axis,
            // select the one whose intersection is nearest
            for (AIControl obstacle : this.obstacles) {
                float distanceFromCenterToCenter = this.agent.distanceTo(obstacle);
                if (distanceFromCenterToCenter > this.minDistance) {
                    break;
                }

                float distanceFromCenterToObstacleSuperf = distanceFromCenterToCenter - obstacle.getRadius();
                float distance = distanceFromCenterToObstacleSuperf - this.agent.getRadius();

                if (distanceFromCenterToObstacleSuperf < 0) {
                    distanceFromCenterToObstacleSuperf = 0;
                }

                if (distance < 0) {
                    distance = 0;
                }

                // if it is at least in the radius of the collision cylinder and we are facing the obstacle
                if (this.agent.forwardness(obstacle) > 0
                        && //Are we facing the obstacle ?
                        distance * distance
                        < ((minDistanceToCollision * minDistanceToCollision)
                        + (this.agent.getRadius() * this.agent.getRadius())) //Pythagoras Theorem
                        ) {
                    Vector3f velocityNormalized = this.agent.getVelocity().normalize();
                    Vector3f distanceVec = this.agent.vectorTo(obstacle).normalize().mult(distanceFromCenterToObstacleSuperf);
                    Vector3f projectedVector = velocityNormalized.mult(velocityNormalized.dot(distanceVec));

                    Vector3f collisionDistanceOffset = projectedVector.subtract(distanceVec);

                    if (collisionDistanceOffset.length() < this.agent.getRadius()) {
                        Vector3f collisionDistanceDirection;

                        if (!collisionDistanceOffset.equals(Vector3f.ZERO)) {
                            collisionDistanceDirection = collisionDistanceOffset.normalize();
                        } else {
                            collisionDistanceDirection = randomVectInPlane(this.agent.getVelocity(), this.agent.getWorldTranslation()).normalize();
                        }

                        Vector3f steerForce = collisionDistanceDirection.mult((this.agent.getRadius() - collisionDistanceOffset.length())
                                / this.agent.getRadius());

                        if (steerForce.length() > nearestObstacleSteerForce.length()) {
                            nearestObstacleSteerForce = steerForce;
                        }
                    }
                }
            }
        }
        return nearestObstacleSteerForce;
    }

    /**
     * Generates a random vector inside a plane defined by a normal vector and a
     * point
     */
    protected Vector3f randomVectInPlane(Vector3f planeNormalV, Vector3f planePoint) {
        Random rand = FastMath.rand;

        /* Plane ecuation: Ax + By + Cz + D = 0 
         *  => z = -(Ax + By + D) / C
         *  => x = -(By + Cz + D) / A
         *  => y = -(Ax + Cz + D) / B
         */
        float a = planeNormalV.x;
        float b = planeNormalV.y;
        float c = planeNormalV.z;
        float d = -((a * planePoint.x)
                + (b * planePoint.y)
                + (c * planePoint.z));

        float x, y, z;

        if (c != 0) {
            x = rand.nextFloat();
            y = rand.nextFloat();
            z = -((a * x) + (b * y) + d) / c;
        } else if (a != 0) {
            y = rand.nextFloat();
            z = rand.nextFloat();
            x = -((b * y) + (c * z) + d) / a;
        } else if (b != 0) {
            x = rand.nextFloat();
            z = rand.nextFloat();
            y = -((a * x) + (c * z) + d) / b;
        } else {
            x = rand.nextFloat();
            y = rand.nextFloat();
            z = rand.nextFloat();
        }

        Vector3f randPoint = new Vector3f(x, y, z);

        return randPoint.subtract(planePoint);
    }

    protected List<AIControl> getObstacles() {
        return this.obstacles;
    }

    public void setObstacles(List<AIControl> obstacles) {
        this.obstacles = obstacles;
    }

    protected float getMinTimeToCollision() {
        return this.minTimeToCollision;
    }

    protected float getMinDistance() {
        return this.minDistance;
    }
} 

In the end, you have something that’s very similar to the way that the player controls their own character.

Okay, so this one

2-Make Each steer behavior system to access to Body (and ControlDriver)

No, it is different from AI tasks, it is like SightingState (Abilities which have more abstract logics than tasks)

/**
 * Provides sighting ability to entities so ai entities can see the environment.
 *
 * @author Ali-RS <ali_codmw@yahoo.com>
 */
public class SightingState extends AbstractGameSystem {

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

    private EntityData ed;
    private EntitySet watchers;
    private EntitySet watchables;

    @Override
    protected void initialize() {
        log.info("initialize()");
        this.ed = getSystem(EntityData.class);
        if (ed == null) {
            throw new RuntimeException("SightingState requires an EntityData object.");
        }
    }

    @Override
    public void start() {
        log.info("start()");
        this.watchers = ed.getEntities(BodyPosition.class, Sighting.class);
        this.watchables = ed.getEntities(BodyPosition.class);
    }

    @Override
    public void update(SimTime time) {
        watchers.applyChanges();
        watchables.applyChanges();
        for (Entity watcher : watchers) {
            Sighting sighting = watcher.get(Sighting.class);
            List<EntityId> entitiesInSight = new ArrayList<>();
            for (Entity other : watchables) {
                if (watcher.getId() != other.getId() //Do not need to see itself
                        && isInSight(watcher, other, time)) {
                    entitiesInSight.add(other.getId());
                }
            }
            ed.setComponent(watcher.getId(), new Sighting(sighting.range, sighting.angle, entitiesInSight));
        }
    }

    private boolean isInSight(Entity watcher, Entity other, SimTime time) {
        Sighting sighting = watcher.get(Sighting.class);
        BodyPosition watcherBP = watcher.get(BodyPosition.class);
        PositionTransition watcherTrans = watcherBP.getBuffer().getTransition(time.getTime());
        Vector3f watcherPos = watcherTrans.getPosition(time.getTime(), true);
        Quaternion watcherRot = watcherTrans.getRotation(time.getTime(), true);

        BodyPosition otherBP = other.get(BodyPosition.class);
        PositionTransition otherTrans = otherBP.getBuffer().getTransition(time.getTime());
        Vector3f otherPos = otherTrans.getPosition(time.getTime(), true);
        
        Vector3f sub = otherPos.subtract(watcherPos);
        
        if (sub.lengthSquared() < (sighting.range * sighting.range) ) {
            Vector3f watcherDir = new Vector3f(Vector3f.UNIT_Z);
      
            watcherRot.mult(watcherDir);
            sub.normalizeLocal();
            if (FastMath.acos(watcherDir.dot(sub)) < sighting.angle / 2) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void stop() {
        this.watchers.clear();
        this.watchers = null;

        this.watchables.clear();
        this.watchables = null;
    }

    @Override
    protected void terminate() {

    }

}

By changing behaviors from AbstractControl to BaseAppState i can behave with these steerings just like my Sighting state.

By looking trough the library all CompoundSteeringBehavior are calculated by just adding the force output.

see thiscalculateRawSteering() from CompoundSteeringBehavior class :
Only need to return totalForce;

 /**
     * Calculates the composed steering force. The composed force is the
     * summatory of the behaviors steering forces.
     *
     * @return The composed steering force.
     */
    @Override
    protected Vector3f calculateRawSteering() {

        Vector3f totalForce = new Vector3f();
        float totalBraking = 1;

        this.behaviors.moveAtBeginning();

        if (!this.behaviors.nullPointer()) {
            int currentLayer = this.behaviors.getLayer();
            int inLayerCounter = 0;
            int validCounter = 0;

            while (!this.behaviors.nullPointer()) {
                if (this.behaviors.getLayer() != currentLayer) {
                    //We have finished the last layer, check If it was a valid layer
                    if (inLayerCounter == validCounter) {
                        //If we have a valid layer, return the force
                        break;
                    } else {
                        //If not, reset the total force
                        totalForce = new Vector3f();
                        //and reset braking
                        totalBraking = 1;
                    }

                    currentLayer = this.behaviors.getLayer();
                    inLayerCounter = 0;
                    validCounter = 0;
                }

                Vector3f force = this.calculatePartialForce(this.behaviors.getBehavior());
                if (force.length() > this.behaviors.getMinLengthToInvalidSteer()) {
                    validCounter++;
                }
                totalForce = totalForce.add(force);
                totalBraking *= this.behaviors.getBehavior().getBrakingFactor();

                inLayerCounter++;
                this.behaviors.moveNext();
            }
        }

        this.setBrakingFactor(totalBraking);
        return totalForce;
    }

    /**
     * Calculates the steering force of a single behavior
     *
     * @param behavior The behavior.
     * @return The steering force of that behavior
     */
    protected Vector3f calculatePartialForce(AbstractSteeringBehavior behavior) {
        return behavior.calculateSteering();
    }

Yes maybe running each steer behavior in separate thread is not that sensible, so i may put all of the steerings states run in one thread.

I haven’t read through all of the code but I recommend you to just take my fork/version/w.e, because:

  1. I fixed some crucial bugs which lead to wrong behavior
  2. You can use it multithreaded (actually you can run them in a separate thread only or like 4 threads (number of cores, more threads don’t make sense)).
  3. I decoupled it from MonkeyBrains again
  4. I made it Spatial independent, so you just need to hook into the callback where the new position is calculated. Or do it like [here] (https://github.com/MeFisto94/MonkeyBrains/blob/9498c6578102e31027296b2a2eef7cbe0067b3d7/src/com/jme3/ai/agents/debugtools/ShowVelocityControl.java#L51)
  5. I changed it to use the World Rotation, because the thing would steer wrong if a parent node rotates everything around 90°.

Also see Agent#setApplicationType, AIControl#setIgnoreSceneGraphUpdates (You can decide whether you manually control the updates or the scene graph), AiControl#getPredictedRotation (This you could use along with the velocity pendant).

AND the MultiThreaded Example here

The Example looks clunky but is the most important one because when we are multithreaded we run agent calculations without scene graph in seperate threads and then in the simpleUpdate, we gather the calculated results which you could then plug into your ES.

Have fun :slight_smile:

1 Like

I agtee with @pspeed I would as well only have one system the behavior system and inside that you can have it as complex as you want.

1 Like

@Darkchaos thanks :slight_smile:

Just cloned your fork. I see behaviors are not jme Control anymore.

Agent class which is a JME Control is extra stuff for my case because it is equivalent to Body class (from sim-eth-es) in my case. So i need to pass the physic Body to behaviors with ApplyType = BetterCharacterControlNoY.

I will investigate more on your fork.