/** * 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.ai.agents.behaviors.npc.steering.SteeringExceptions.WallApproachWithoutWallException; import com.jme3.collision.CollisionResult; import com.jme3.collision.CollisionResults; import com.jme3.math.Ray; import com.jme3.math.Vector3f; import com.jme3.scene.Node; import com.jme3.scene.Spatial; /** * "Approach a 'wall' (or other surface or path) and then maintain a certain * offset from it" <br><br> * * Keep in mind that this relates to wall approach not necessarily to collision * detection. * * @author Jesús Martín Berlanga * @version 1.1 */ public class WallApproachBehavior extends AbstractStrengthSteeringBehavior { //14 tests in total private static enum RayTests { RAY_TEST_X(Vector3f.UNIT_X), RAY_TEST_Y(Vector3f.UNIT_Y), RAY_TEST_Z(Vector3f.UNIT_Z), RAY_TEST_NEGX(Vector3f.UNIT_X.negate()), RAY_TEST_NEGY(Vector3f.UNIT_Y.negate()), RAY_TEST_NEGZ(Vector3f.UNIT_Z.negate()), RAY_TEST_XZ_UP(new Vector3f(1, 1, 1)), RAY_TEST_NEGX_Z_UP(new Vector3f(-1, 1, 1)), RAY_TEST_X_NEGZ_UP(new Vector3f(1, 1, -1)), RAY_TEST_NEGXZ_UP(new Vector3f(-1, 1, -1)), RAY_TEST_XZ_DOWN(new Vector3f(1, -1, 1)), RAY_TEST_NEGX_Z_DOWN(new Vector3f(-1, -1, 1)), RAY_TEST_X_NEGZ_DOWN(new Vector3f(1, -1, -1)), RAY_TEST_NEGXZ_DOWN(new Vector3f(-1, -1, -1)); private Vector3f direction; private RayTests(Vector3f direction) { this.direction = direction; } private Vector3f getDirection() { return this.direction; } } private Node wall; private float offsetToMaintain; private float rayTestOffset; private static final float MIN_RAY_TEST_OFFSET = 0.001f; /** * @throws SteeringExceptions.NegativeValueException If offsetToMaintain is * negative */ public void setOffsetToMaintain(float offsetToMaintain) { WallApproachBehavior.validateOffsetToMaintain(offsetToMaintain); this.offsetToMaintain = offsetToMaintain; } /** * @param wall Surface or path where the agent will maintain a certain * offset * @paaram offsetToMaintain Offset from the surface that the agent will have * to maintain * * @throws WallApproachWithoutWallException If the wall is a null pointer * @throws SteeringExceptions.NegativeValueException If offsetToMaintain is * negative * * @see * AbstractStrengthSteeringBehavior#AbstractStrengthSteeringBehavior(com.jme3.ai.agents.Agent) */ public WallApproachBehavior(Agent agent, Node wall, float offsetToMaintain) { super(agent); WallApproachBehavior.validateConstruction(wall, offsetToMaintain); this.wall = wall; this.offsetToMaintain = offsetToMaintain; if (offsetToMaintain != 0) { this.rayTestOffset = (offsetToMaintain + agent.getRadius()) * 4; } else { this.rayTestOffset = WallApproachBehavior.MIN_RAY_TEST_OFFSET; } } /** * @see WallApproach#WallApproach(com.jme3.ai.agents.Agent, * com.jme3.scene.Node, float) * @see * AbstractStrengthSteeringBehavior#AbstractStrengthSteeringBehavior(com.jme3.ai.agents.Agent, * com.jme3.scene.Spatial) */ public WallApproachBehavior(Agent agent, Node wall, float offsetToMaintain, Spatial spatial) { super(agent, spatial); WallApproachBehavior.validateConstruction(wall, offsetToMaintain); this.wall = wall; this.offsetToMaintain = offsetToMaintain; if (offsetToMaintain != 0) { this.rayTestOffset = (offsetToMaintain + agent.getRadius()) * 4; } else { this.rayTestOffset = WallApproachBehavior.MIN_RAY_TEST_OFFSET; } } private static void validateConstruction(Node wall, float offsetToMaintain) { if (wall == null) { throw new WallApproachWithoutWallException("You can not instantiate a new wall approach behaviour without a wall."); } else { WallApproachBehavior.validateOffsetToMaintain(offsetToMaintain); } } private static void validateOffsetToMaintain(float offsetToMaintain) { if (offsetToMaintain < 0) { throw new SteeringExceptions.NegativeValueException("The superficial offset to maintain cannot be negative.", offsetToMaintain); } } /** * @see AbstractStrengthSteeringBehavior#calculateRawSteering() */ @Override protected Vector3f calculateRawSteering() { Vector3f steer = new Vector3f(); Vector3f aproximatedSurfaceLocationDir = this.approximateSurfaceLocation(); if (aproximatedSurfaceLocationDir != null) { Vector3f surfaceLocation = this.surfaceLocation(aproximatedSurfaceLocationDir); if (surfaceLocation != null) { Vector3f extraOffset = this.agent.offset(surfaceLocation).negate().normalize().mult(this.offsetToMaintain); SeekBehavior seek = new SeekBehavior(this.agent, surfaceLocation.add(extraOffset)); steer = seek.calculateRawSteering(); } } return steer; } /** * Check for intersections with the wall - Ray test */ private Vector3f approximateSurfaceLocation() { class LowerDistances { private Vector3f[] lowerDistances = new Vector3f[]{ Vector3f.POSITIVE_INFINITY, Vector3f.POSITIVE_INFINITY, Vector3f.POSITIVE_INFINITY,}; private void addDistance(Vector3f distance) { float distanceLength = distance.length(); int lowerPos = -1; float currentLength = Float.MAX_VALUE; for (int i = 0; i < lowerDistances.length; i++) { float length = lowerDistances[i].length(); if (length > currentLength) { currentLength = length; lowerPos = i; } } if (lowerPos != -1 && lowerDistances[lowerPos].length() > distanceLength) { lowerDistances[lowerPos] = distance; } } private Vector3f distanceSum() { Vector3f sum = new Vector3f(); boolean almostOne = false; for (int i = 0; i < lowerDistances.length; i++) { if (lowerDistances[i] != null && lowerDistances[i].length() < rayTestOffset) { sum = sum.add(lowerDistances[i]); almostOne = true; } } if (almostOne) { return sum; } else { return null; } } } LowerDistances distances = new LowerDistances(); for (RayTests rayTest : WallApproachBehavior.RayTests.values()) { Vector3f rayTestSurfaceLocation = this.surfaceLocation(rayTest.getDirection()); if (rayTestSurfaceLocation != null) { distances.addDistance(agent.offset(rayTestSurfaceLocation)); } } return distances.distanceSum(); } private Vector3f surfaceLocation(Vector3f direction) { Vector3f surfaceLocation = null; CollisionResults results = new CollisionResults(); Ray ray = new Ray(this.agent.getLocalTranslation(), direction); this.wall.collideWith(ray, results); CollisionResult collisionResult = results.getClosestCollision(); if (collisionResult != null && !Float.isNaN(collisionResult.getDistance()) && !Float.isInfinite(collisionResult.getDistance())) { surfaceLocation = results.getClosestCollision().getContactPoint(); } return surfaceLocation; } }