CollisionChaseCamera + Player Delayed Face Towards Direction

Camera :

  1. player (camera view target) will never be blocked by another spatial (creature/building).

    This is done by raycasting. When another spatial blocks the view, the camera zooms in,

    until player is visible.
  2. When the player moves away from the blocking spatial, the camera will try to zoom out until

    it reaches its “preferred” zoom distance.



    Controls:
  3. player look direction can be different from camera direction. I tried to make it like “kingdom of amalur” camera.



    Performance cost: Without: 750 fps. With: 700 fps.



    Zoomed out at distance 50

    Behind building Zoom in



    Applet For those lazy to test it: http://rpggame.ucoz.com/test/CollisionCamera/index.html



    to run : TestCollisionCamera example you need : town.zip from

    https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:beginner:hello_collision





    [java]

    import com.jme3.collision.CollisionResult;

    import com.jme3.collision.CollisionResults;

    import com.jme3.input.ChaseCamera;

    import com.jme3.math.Ray;

    import com.jme3.math.Vector3f;

    import com.jme3.renderer.Camera;

    import com.jme3.scene.Node;



    public class CollisionChaseCamera extends ChaseCamera

    {

    protected Node collidableScene;

    protected float zoomAtPreferedDistanceSpeed = 20f; //every sec it will try to zoom out until pref dist.



    private float preferredDistance = distance;

    private float maxDistanceWithoutCollision;

    private float previousDistance;



    public CollisionChaseCamera(Camera cam, Node target, Node collidableScene)

    {

    super(cam, target);

    this.collidableScene = collidableScene;

    }



    public boolean checkCameraForCollisions(Vector3f rayStart)

    {

    CollisionResults results = new CollisionResults();

    Ray ray = new Ray(rayStart, cam.getDirection());

    collidableScene.collideWith(ray, results);

    CollisionResult result = results.getClosestCollision(); //just sort.



    for(int i=0; i < results.size(); i++)

    {

    result = results.getCollisionDirect(i);

    if (result.getGeometry().hasAncestor((Node) target))//when you reach targetGeometry

    {

    if (i==0) //if first in order, no collisions happened. maxDistanceWithoutCollision = result.getDistance();

    {

    maxDistanceWithoutCollision = result.getDistance();

    return false;

    }



    maxDistanceWithoutCollision = result.getDistance() - results.getCollisionDirect(i - 1).getDistance();

    return true;

    }

    }

    return false;

    }



    @Override protected void updateCamera(float tpf)

    {

    if (enabled)

    {

    float zoomAtPreferredDistanceAmount = zoomAtPreferedDistanceSpeed * tpf;

    targetLocation.set(target.getWorldTranslation()).addLocal(lookAtOffset);

    vRotation = targetVRotation;

    rotation = targetRotation;

    distance = targetDistance;



    Vector3f camPos ;

    boolean zoomFailed ;

    if (targetDistance < preferredDistance) //attempt to zoom out camera to reach prefered zoom.

    {

    setDistance(targetDistance + zoomAtPreferredDistanceAmount);

    computePosition();

    camPos = pos.addLocal(lookAtOffset); //camPos = cam.getLocation zoomed out by 0.01 (zoomAtPreferedDistanceSpeed)

    zoomFailed = true;

    }

    else

    {

    camPos = cam.getLocation();

    zoomFailed = false;

    }



    boolean collisionHappened = checkCameraForCollisions(camPos);

    if (collisionHappened)

    {

    if (!Float.isInfinite(maxDistanceWithoutCollision) && !Float.isNaN(maxDistanceWithoutCollision))

    {

    if (Math.abs(maxDistanceWithoutCollision - previousDistance) > 1.0f)

    {

    setDistance(maxDistanceWithoutCollision);

    zoomFailed = false;

    }

    }

    if (zoomFailed) //else failed zoom restore change.

    {

    setDistance(targetDistance - zoomAtPreferredDistanceAmount);

    }

    }

    //else if (!collisionHappened) leave distance unchanged since it is fine.



    previousDistance = distance;

    computePosition();

    cam.setLocation(pos.addLocal(lookAtOffset));



    prevPos.set(targetLocation); //keeping track on the previous position of the target

    cam.lookAt(targetLocation, initialUpVec); //the cam looks at the target

    }

    }



    protected void setDistance(float value)

    {

    if (value < minDistance)

    {

    value = minDistance;

    if (veryCloseRotation && (targetVRotation < minVerticalRotation))

    {

    targetVRotation = minVerticalRotation;

    }

    }

    distance = value;

    targetDistance = value;

    }



    @Override protected void zoomCamera(float value)

    {

    super.zoomCamera(value);

    preferredDistance = targetDistance;

    }



    @Override public void setDefaultDistance(float defaultDistance)

    {

    super.setDefaultDistance(defaultDistance);

    preferredDistance = defaultDistance;

    }



    public Node getCollidableScene()

    {

    return collidableScene;

    }



    public void setCollidableScene(Node collidableScene)

    {

    this.collidableScene = collidableScene;

    }



    public float getZoomAtPreferedDistanceSpeed()

    {

    return zoomAtPreferedDistanceSpeed;

    }



    public void setZoomAtPreferedDistanceSpeed(float zoomAtPreferedDistanceSpeed)

    {

    this.zoomAtPreferedDistanceSpeed = zoomAtPreferedDistanceSpeed;

    }



    public void setRotationSpeed(float speed)

    {

    rotationSpeed = speed;

    }

    }

    [/java]



    [java]

    import com.jme3.app.SimpleApplication;

    import com.jme3.asset.plugins.ZipLocator;

    import com.jme3.bounding.BoundingBox;

    import com.jme3.bullet.BulletAppState;

    import com.jme3.bullet.collision.shapes.BoxCollisionShape;

    import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;

    import com.jme3.bullet.collision.shapes.CollisionShape;

    import com.jme3.bullet.control.CharacterControl;

    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.bullet.util.CollisionShapeFactory;

    import com.jme3.input.ChaseCamera;

    import com.jme3.input.KeyInput;

    import com.jme3.input.controls.ActionListener;

    import com.jme3.input.controls.KeyTrigger;

    import com.jme3.light.AmbientLight;

    import com.jme3.light.DirectionalLight;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.FastMath;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial;



    import java.util.logging.Level;

    import java.util.logging.Logger;



    public class TestCollisionCamera extends SimpleApplication implements ActionListener

    {

    protected Spatial stageModel;

    protected RigidBodyControl stagePhysics;



    protected ChaseCamera chaseCam;

    protected Node playerModel;

    protected CharacterControl playerPhysics;

    protected float playerMovementSpeed = 0.125f;

    protected Vector3f walkDirection = new Vector3f(0, 0, 1);

    protected boolean left = false, right = false, up = false, down = false;

    protected BulletAppState bulletAppState = new BulletAppState();



    public static final boolean CAPTULE_CHARACTER_SHAPE = true;

    public static final boolean USE_COLLISION_CHASE_CAMERA = true;



    public static void main(String[] args)

    {

    Logger.getLogger("").setLevel(Level.SEVERE);

    TestCollisionCamera app = new TestCollisionCamera();

    app.start();

    }



    @Override

    public void simpleInitApp()

    {

    setupOptions();

    setupKeys();

    setupLight();

    setupStage();

    setupCharacter(new Vector3f(-0.5f, 0, -0.5f));

    setupCamera(playerModel);

    }



    public void setupOptions()

    {

    bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);

    stateManager.attach(bulletAppState);

    viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));

    flyCam.setMoveSpeed(30);

    }



    private void setupCamera(Node player)

    {

    flyCam.setEnabled(false);

    if (USE_COLLISION_CHASE_CAMERA)

    {

    chaseCam = new CollisionChaseCamera(cam, player, rootNode);

    chaseCam.setMinVerticalRotation(-0.6f);

    chaseCam.setMinDistance(3.0f); //recommented value: based on char size so that camera doesnt get inside it.

    }

    else

    {

    chaseCam = new ChaseCamera(cam, player);

    }



    chaseCam.registerWithInput(inputManager);

    chaseCam.setMaxDistance(30f);

    chaseCam.setDefaultDistance(12f);

    chaseCam.setDragToRotate(false);

    chaseCam.setInvertVerticalAxis(true);

    chaseCam.setDefaultHorizontalRotation(-FastMath.HALF_PI);

    }



    private void setupStage()

    {

    assetManager.registerLocator(“town.zip”, ZipLocator.class.getName());

    stageModel = assetManager.loadModel(“main.scene”);

    stageModel.setLocalScale(0.4f);

    rootNode.attachChild(stageModel);



    CollisionShape sceneShape = CollisionShapeFactory.createMeshShape(stageModel);

    stagePhysics = new RigidBodyControl(sceneShape, 0);

    stageModel.addControl(stagePhysics);

    bulletAppState.getPhysicsSpace().add(stagePhysics);

    }



    public static BoundingBox getBoundingBox(Spatial node)

    {

    return (BoundingBox)node.getWorldBound();

    }



    private void setupCharacter(Vector3f position)

    {

    playerModel = (Node)assetManager.loadModel(“Models/Sinbad/Sinbad.mesh.xml”);

    playerModel.scale(0.25f);

    rootNode.attachChild(playerModel);



    BoundingBox playerBox = getBoundingBox(playerModel);

    CollisionShape playerCapsule;

    if (CAPTULE_CHARACTER_SHAPE)

    {

    playerCapsule = new CapsuleCollisionShape(playerBox.getXExtent(), playerBox.getYExtent() / 2f, 1);

    }

    else

    {

    playerCapsule = new BoxCollisionShape(playerBox.getExtent(null));

    }



    playerPhysics = new CharacterControl(playerCapsule, 0.10f);

    playerModel.addControl(playerPhysics);

    playerPhysics.setPhysicsLocation(new Vector3f(position.x, playerBox.getYExtent() + position.y, position.z));

    bulletAppState.getPhysicsSpace().add(playerPhysics);

    playerPhysics.setJumpSpeed(20);

    playerPhysics.setFallSpeed(30);

    playerPhysics.setGravity(30);

    }



    private void setupLight()

    {

    AmbientLight al = new AmbientLight();

    al.setColor(ColorRGBA.White.mult(1.3f));

    rootNode.addLight(al);



    DirectionalLight dl = new DirectionalLight();

    dl.setColor(ColorRGBA.White);

    dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal());

    rootNode.addLight(dl);

    }



    private void setupKeys()

    {

    inputManager.addMapping(“Left”, new KeyTrigger(KeyInput.KEY_A));

    inputManager.addMapping(“Right”, new KeyTrigger(KeyInput.KEY_D));

    inputManager.addMapping(“Up”, new KeyTrigger(KeyInput.KEY_W));

    inputManager.addMapping(“Down”, new KeyTrigger(KeyInput.KEY_S));

    inputManager.addMapping(“Jump”, new KeyTrigger(KeyInput.KEY_SPACE));

    inputManager.addListener(this, “Left”);

    inputManager.addListener(this, “Right”);

    inputManager.addListener(this, “Up”);

    inputManager.addListener(this, “Down”);

    inputManager.addListener(this, “Jump”);

    }



    //@toDo: non instant transition of rotations. 0.3 sec to do a full rotation.

    public void onAction(String binding, boolean value, float tpf)

    {

    if (binding.equals(“Left”))

    {

    left = value;

    }

    else if (binding.equals(“Right”))

    {

    right = value;

    }

    else if (binding.equals(“Up”))

    {

    up = value;

    }

    else if (binding.equals(“Down”))

    {

    down = value;

    }

    else if (binding.equals(“Jump”))

    {

    playerPhysics.jump();

    }

    }



    //@toDo: rotate player model.

    @Override

    public void simpleUpdate(float tpf)

    {

    super.simpleUpdate(tpf);

    getInputManager().setCursorVisible(false);//@toDo: put it in simpleInit after jme bug fixed.

    boolean moved = left || right || up || down;

    if (!moved)

    {

    playerPhysics.setWalkDirection(Vector3f.ZERO);

    return;

    }

    float rotx = -(chaseCam.getHorizontalRotation() + FastMath.HALF_PI);

    float roty = (chaseCam.getVerticalRotation());



    walkDirection = localToWorldCoordinatesIgnoreHeight((left ? 1 : 0) + (right ? -1 : 0), 0, (up ? 1 : 0) + (down ? -1 : 0), roty, rotx);

    walkDirection.normalizeLocal();

    playerPhysics.setWalkDirection(walkDirection.mult(playerMovementSpeed));

    playerPhysics.setViewDirection(walkDirection);

    }



    /** Converts local cordinates “(dx,dy,dz)” to world coordinates based on Vector3D “rotation”.
  • <a href="3D and Audio APIs and Softwares - Jérôme JOUVIE - http:/jerome.jouvie.free.fr/"> More info : </a>

    *
  • This kind of deplacement generaly used for player movement (in shooting game …).
  • The x rotation is ‘ignored’ for the calculation of the deplacement.
  • This result that if you look upward and you want to move forward,
  • the deplacement is calculated like if your were parallely to the ground.

    */

    public static Vector3f localToWorldCoordinatesIgnoreHeight(float dx, float dy, float dz, float rotationX, float rotationY)

    {

    //Don’t calculate for nothing …

    if (dx == 0.0f & dy == 0.0f && dz == 0.0f)

    return new Vector3f();



    double xRot = -rotationX;

    double yRot = -rotationY;



    //Calculate the formula

    float x = (float) (dx * Math.cos(yRot) + 0 - dz * Math.sin(yRot));

    float y = (float) (0 + dy * Math.cos(xRot) + 0);

    float z = (float) (dx * Math.sin(yRot) + 0 + dz * Math.cos(yRot));



    //Return the vector expressed in the global axis system

    return new Vector3f(x, y, z);

    }//localToWorldCoordinatesIgnoreHeight

    }

    [/java]
3 Likes

The previous code had playerPhysics.setViewDirection(walkDirection) applied instantly.



In the following example i will show you how each player can have a rotation speed,

that way facing towards a direction will take time.



Since my game will be skill based going behind a mage will allow you to dodge all spells (he doesn’t face you to attack) = immune to all dmg.



[java]

import com.jme3.math.FastMath;

import com.jme3.math.Quaternion;

import com.jme3.math.Vector3f;

import java.util.logging.Level;

import java.util.logging.Logger;



public class TestFacing extends TestCollisionCamera

{

protected Vector3f playerDirection = Vector3f.UNIT_Z.clone();

protected Vector3f playerUpDirection = Vector3f.UNIT_Y.clone();

protected float playerRotateSpeed = FastMath.DEG_TO_RAD * 360 * 0.75f;//mages:* 0.75f warriors: 1.5f

protected Quaternion playerRotate = new Quaternion();



public static final boolean DONT_MOVE_DURING_ROTATION = true;



/
* returns true if still rotating this frame.*/

public boolean playerFaceTowards(Vector3f nextDirection, float tpf)

{

float rotateAmount = tpf * playerRotateSpeed;

float playerRotationDistanceFromDestination = nextDirection.angleBetween(playerDirection);

rotateAmount = Math.min(playerRotationDistanceFromDestination, rotateAmount);

if (playerDirection.cross(nextDirection).y < 0) rotateAmount = -rotateAmount;



playerRotate = playerRotate.fromAngleAxis(rotateAmount, playerUpDirection);

playerDirection = playerRotate.mult(playerDirection);

playerPhysics.setViewDirection(playerDirection);

if (Math.abs(playerRotationDistanceFromDestination) < 0.1f) return false;

else return true;

}



@Override

public void simpleUpdate(float tpf)

{

getInputManager().setCursorVisible(false);//@toDo: put it in simpleInit after jme bug fixed.

boolean stillRotating = false;

if (walkDirection.length() > 0.01f) stillRotating = playerFaceTowards(walkDirection, tpf); //ignore zero direction

boolean moved = left || right || up || down;

if (!moved || DONT_MOVE_DURING_ROTATION && stillRotating)

{

playerPhysics.setWalkDirection(Vector3f.ZERO);

return;

}



float rotx = -(chaseCam.getHorizontalRotation() + FastMath.HALF_PI);

float roty = (chaseCam.getVerticalRotation());



walkDirection = localToWorldCoordinatesIgnoreHeight((left ? 1 : 0) + (right ? -1 : 0), 0, (up ? 1 : 0) + (down ? -1 : 0), roty, rotx);

walkDirection.normalizeLocal();

playerPhysics.setWalkDirection(walkDirection.mult(playerMovementSpeed));

}



public static void main(String[] args)

{

Logger.getLogger("").setLevel(Level.SEVERE);

TestFacing app = new TestFacing();

app.start();

}

}

[/java]