/** * Copyright (c) 2014, jMonkeyEngine All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of 'jMonkeyEngine' nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.ai.agents.behaviors.npc.steering; import com.jme3.ai.agents.Agent; import com.jme3.bounding.BoundingSphere; import com.jme3.collision.CollisionResult; import com.jme3.collision.CollisionResults; import com.jme3.math.Ray; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.scene.Spatial; import java.util.Random; /** * Wander is a type of random steering. This idea can be implemented several * ways, but one that has produced good results is to constrain the steering * force to the surface of a sphere located slightly ahead of the character. To * produce the steering force for the next frame: a random displacement is added * to the previous value, and the sum is constrained again to the sphere's * surface. The sphere's radius determines the maximum wandering strength and * the magnitude of the random displacement determines the wander 'rate'. * <br><br> * * The steer force is contained in the XY plane. * * @author Jesús Martín Berlanga * @version 1.0 */ public class SphereWanderBehavior extends AbstractStrengthSteeringBehavior { private static final float OFFSET_DISTANCE = 0.01f; private static final float RANDOM_OFFSET = 0.01f; private static final float SIDE_REFERENCE_OFFSET = 0.0001f; private float sphereRadius = 0.75f; /** * Position of target. */ protected Vector3f targetPosition; /** * Time interval durring which target position doesn't change. */ protected float timeInterval; /** * Current time. */ protected float time; private float randomFactor; private Vector2f randomDirection; private float maxRandom; private float rotationFactor; private BoundingSphere wanderSphere; /** * Constructor for wander behavior. * * @param agent to whom behavior belongs * @param timeInterval Sets the time interval for changing target position. * @param randomFactor Defines the maximum random value * @param rotationFactor Defines the maximum random variaton for each * iteration. * * @throws SteeringExceptions.NegativeValueException If timeInterval is * lower or equals to 0 * @throws SteeringExceptions.IllegalIntervalException If randomFactor is * not contained in the [0,1] interval or if rotationFactor is not contained * in the [0,1] interval */ public SphereWanderBehavior(Agent agent, float timeInterval, float randomFactor, float rotationFactor) { super(agent); this.construct(timeInterval, randomFactor, rotationFactor); } /** * @see SphereWanderBehavior#SphereWanderBehavior(com.jme3.ai.agents.Agent, * float, float, float) * @see * AbstractSteeringBehavior#AbstractSteeringBehavior(com.jme3.ai.agents.Agent, * com.jme3.scene.Spatial) */ public SphereWanderBehavior(Agent agent, float timeInterval, float randomFactor, float rotationFactor, Spatial spatial) { super(agent, spatial); this.construct(timeInterval, randomFactor, rotationFactor); } private void construct(float timeInterval, float randomFactor, float rotationFactor) { if (timeInterval <= 0) { throw new SteeringExceptions.NegativeValueException("The time interval must be possitive." + timeInterval); } else if (randomFactor < 0 || randomFactor > 1) { throw new SteeringExceptions.IllegalIntervalException("random", randomFactor); } else if (rotationFactor < 0 || rotationFactor > 1) { throw new SteeringExceptions.IllegalIntervalException("rotation", rotationFactor); } this.timeInterval = timeInterval; this.time = this.timeInterval; this.randomFactor = randomFactor; this.wanderSphere = new BoundingSphere(this.sphereRadius, Vector3f.ZERO); this.targetPosition = this.wanderSphere.getCenter(); this.randomDirection = new Vector2f(); this.maxRandom = this.sphereRadius - SphereWanderBehavior.RANDOM_OFFSET; this.rotationFactor = rotationFactor; this.maxRandom *= this.rotationFactor; } /** * Calculate steering vector. * * @return steering vector */ @Override protected Vector3f calculateRawSteering() { changeTargetPosition(timePerFrame); return this.agent.offset(this.targetPosition).mult((0.5f / this.sphereRadius) * this.agent.getMoveSpeed()); } /** * Metod for changing target position. * * @param tpf time per frame */ protected void changeTargetPosition(float tpf) { time -= tpf; Vector3f forward; if (this.agent.getVelocity() != null) { forward = this.agent.getVelocity().normalize(); } else { forward = this.agent.fordwardVector(); } if (forward.equals(Vector3f.UNIT_Y)) { forward = forward.add(new Vector3f(0, 0, SphereWanderBehavior.SIDE_REFERENCE_OFFSET)); } //Update sphere position this.wanderSphere.setCenter(this.agent.getLocalTranslation().add(forward.mult(SphereWanderBehavior.OFFSET_DISTANCE + this.agent.getRadius() + this.sphereRadius))); if (time <= 0) { this.calculateNewRandomDir(); time = timeInterval; } Vector3f sideVector = forward.cross(Vector3f.UNIT_Y).normalize(); Vector3f rayDir = (this.agent.offset(wanderSphere.getCenter())).add(sideVector.mult(this.randomDirection.x));//.add(Vector3f.UNIT_Y.mult(this.randomDirection.y)); Ray ray = new Ray(this.agent.getLocalTranslation(), rayDir); CollisionResults results = new CollisionResults(); this.wanderSphere.collideWith(ray, results); CollisionResult collisionResult = results.getCollision(1); //The collision with the second hemisphere this.targetPosition = collisionResult.getContactPoint(); } protected void calculateNewRandomDir() { Random rand = new Random(); float extraRandomSide = (rand.nextFloat() / 2) * this.sphereRadius * this.rotationFactor * this.randomFactor; //float extraRandomZ = (rand.nextFloat() / 2) * this.sphereRadius * this.rotationFactor * this.randomFactor ; if (rand.nextBoolean()) { extraRandomSide *= -1; } //if(rand.nextBoolean()) extraRandomZ *= -1; float exceededSideOffset; //float exceededZOffset; float predictecRandomSide = this.randomDirection.x + extraRandomSide; if (predictecRandomSide > this.maxRandom) { exceededSideOffset = this.maxRandom - predictecRandomSide; } else if (predictecRandomSide < -this.maxRandom) { exceededSideOffset = -predictecRandomSide + this.maxRandom; } else { exceededSideOffset = 0; } /* float predictedRandomUp = this.randomDirection.y + extraRandomZ; if(predictedRandomUp > this.maxRandom) exceededZOffset = this.maxRandom - predictedRandomUp; else if(predictedRandomUp < -this.maxRandom) exceededZOffset = -predictedRandomUp + this.maxRandom; else exceededZOffset = 0; */ this.randomDirection = this.randomDirection.add(new Vector2f( extraRandomSide + exceededSideOffset, 0 //extraRandomZ + exceededZOffset )); } /** * Get time interval for changing target position. * * @return */ public float getTimeInterval() { return timeInterval; } /** * Setting time interval for changing target position. * * @param timeInterval */ public void setTimeInterval(float timeInterval) { this.timeInterval = timeInterval; } /** * The sphere radius is 0.75 by default. Note that low radius values can * cause unexpected errors. * * @throws SteeringExceptions.NegativeValueException If sphereRadius is * lower or equals to 0 */ public void setSphereRadius(float sphereRadius) { if (sphereRadius <= 0) { throw new SteeringExceptions.NegativeValueException("The sphere radius must be possitive.", sphereRadius); } this.sphereRadius = sphereRadius; } }