Anyone have a good third person camera?

I’m curious if anyone has any third person camera code they are willing to share. I tried to the Chase Camera but it tries to be good at so many things that it just ends up being dysfunctional bloat ware.

I have a nifty unity script but I can’t get it converted over because I suck at math :stuck_out_tongue: Maybe if someone could help me with that.

Thanks,
Jojoofu

Heheh… read: “Does anyone have a third person camera specific to YOUR game? The one that tries to be general doesn’t work with my game but I’m sure the one specific to your game will.” Just funny.

Your best bets are:
-start from chase camera (forked) and fix it for your needs with help from the forum
-post code that is almost working and get help from the forum.

Else, you will get a lot of chase cameras specific to other games that are not useful to you. I mean, I could post mine but I doubt the code that intersects with my block world is very useful to you… and the rest is only like 4 lines of code.

Found this out of my bookmarks.

My third person camera borrows the basic from chase camera. I just really suck at the math. I have it where it sorta does what I want.

To help with the math… we’d of course have to see the math and know what you don’t like about it.

You asked for it :stuck_out_tongue: This is a stripped down version of the chase cam. It just lock the camera behind the target between an angle and rotates it behind if it goes outside the angle. My big issue is when I do the collision test and set the targetDistance it keeps jumping in and out wildly and causes the camera to shake. If I commit out the collisionAdjustment in the cameraUpdate. It works better but still gets a little bit of camera shaking.

package com.jpony.camera;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.collision.CollisionResults;
import com.jme3.input.InputManager;
import com.jme3.input.controls.AnalogListener;
import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import static com.jpony.camera.CameraConstants.*;
import com.jpony.humanoid.Humanoid;
import com.jpony.input.PonyInput;


/**
 * @author Jojoofu
 */
public class ThirdPersonCamera extends AbstractAppState implements AnalogListener{
    
    private PonyInput input;
    public Node target;
    public Camera cam;
    
    private Node collisionNode;
    private CollisionResults results;
    private Ray ray;
    private float collisionOffset = 0.2f;
    
    protected float minVerticalRotation = 0.00f;
    protected float maxVerticalRotation = FastMath.PI / 2;
    
    protected float minDistance = 1.0f;
    protected float maxDistance = 40.0f;
   
    protected float rotationSpeed = 2.0f;

    protected float zoomSensitivity = 2f;

    protected float targetRotation = 0;
    protected InputManager inputManager;
    protected Vector3f initialUpVec;
    protected float targetVRotation = FastMath.PI / 6;

    protected float targetDistance = 15;
    private float lastGoodDistance = targetDistance;
    private boolean collision = false;
    
    protected final Vector3f pos = new Vector3f();
    protected Vector3f targetLocation = new Vector3f(0, 0, 0);
    protected Vector3f lookAtOffset = new Vector3f(0, 0, 0);

    private boolean invertYaxis = false;
    private boolean invertXaxis = false;

    protected boolean zoomin;
    protected boolean hideCursorOnRotate = true;
    
    // Get camera left of target
    private Vector3f left = new Vector3f();
    private Vector3f relative = new Vector3f();
    private float dot = 0;
    private boolean leftOfTarget;
    
    private Humanoid humanoid;
    
    private float cameraRotationSpeed = 0.5f;
    private float tempCamRotSpeed = cameraRotationSpeed;
    
    private float cameraSensitivity = 0.5f;
    private float tempCamSensitivity = cameraSensitivity;
    
    private float lockAngle = 0.1f;
    
    public ThirdPersonCamera(SimpleApplication app , PonyInput input , Node target , Humanoid humanoid){
       this.cam = app.getCamera();
       this.input = input;
       this.target = target;
       this.inputManager = app.getInputManager();
       this.humanoid = humanoid;
    }
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        //TODO: initialize your AppState, e.g. attach spatials to rootNode
        //this is called on the OpenGL thread after the AppState has been attached
        input.key(AXISXPOSITIVE).addListener(this);
        input.key(AXISXNEGATIVE).addListener(this);
        input.key(AXISYPOSITIVE).addListener(this);
        input.key(AXISYNEGATIVE).addListener(this);
        input.key(WHEELPOSITIVE).addListener(this);
        input.key(WHEELNEGATIVE).addListener(this);
        input.key(CAMERAACTIONKEY).addListener(this);
        
        initialUpVec = cam.getUp().clone();
        
    }
    
    @Override
    public void update(float tpf) {
        //TODO: implement behavior during runtime
        updateCamera(tpf);
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        //TODO: clean up what you initialized in the initialize method,
        //e.g. remove all spatials from rootNode
        //this is called on the OpenGL thread after the AppState has been detached
        input.key(AXISXPOSITIVE).removeListener(this);
        input.key(AXISXNEGATIVE).removeListener(this);
        input.key(AXISYPOSITIVE).removeListener(this);
        input.key(AXISYNEGATIVE).removeListener(this);
        input.key(WHEELPOSITIVE).removeListener(this);
        input.key(WHEELNEGATIVE).removeListener(this);
        input.key(CAMERAACTIONKEY).removeListener(this);
    }

    public void onAnalog(String name, float value, float tpf) {
        
        if (name.equals(WHEELPOSITIVE)) {
            zoomCamera(-value);
        } else if (name.equals(WHEELNEGATIVE)) {
            zoomCamera(value);
        }
        
        if (input.key(CAMERAACTIONKEY).up){
            return;
        }
        
        if (name.equals(AXISXPOSITIVE)) {
            rotateCamera(invertXaxis ? -value:value);
        } else if (name.equals(AXISXNEGATIVE)) {
            rotateCamera(invertXaxis ? value:-value);
        } else if (name.equals(AXISYPOSITIVE)) {
            vRotateCamera(invertYaxis ? -value:value);
        } else if (name.equals(AXISYNEGATIVE)) {
            vRotateCamera(invertYaxis ? value:-value);
        }
    }

    protected void computePosition() {
        float hDistance = (targetDistance) * FastMath.sin((FastMath.PI / 2) - targetVRotation);
        pos.set(hDistance * FastMath.cos(targetRotation), (targetDistance) * FastMath.sin(targetVRotation), hDistance * FastMath.sin(targetRotation));
        pos.addLocal(target.getWorldTranslation());
    }

    //rotate the camera around the target on the horizontal plane
    protected void rotateCamera(float value) {

        targetRotation += value * rotationSpeed;

    }

    //move the camera toward or away the target
    protected void zoomCamera(float value) {

        targetDistance += value * zoomSensitivity;
        if (targetDistance > maxDistance) {
            targetDistance = maxDistance;
        }
        if (targetDistance < minDistance) {
            targetDistance = minDistance;
        }

    }

    //rotate the camera around the target on the vertical plane
    protected void vRotateCamera(float value) {
 
        float lastGoodRot = targetVRotation;
        targetVRotation += value * rotationSpeed;
        if (targetVRotation > maxVerticalRotation) {
            targetVRotation = lastGoodRot;
        }
        if ((targetVRotation < minVerticalRotation)) {
            targetVRotation = lastGoodRot;
        }
        
    }

    /**
     * Updates the camera, should only be called internally
     */
    protected void updateCamera(float tpf) {
        
        if (input.key(CAMERAACTIONKEY).down){
            inputManager.setCursorVisible(false);
        } else {
            inputManager.setCursorVisible(true);
            // Check if camera is to the left of the target.
            leftOfTarget();
            
            /**
             * If camera is to far to the left or right of the target we up the rotation speed
             * gradually until it within the desired rotation range behind the target.
             */
            
            if (humanoid.physicsControl.isTurning() && !inBetweenValue(dot,-cameraSensitivity,cameraSensitivity)){
                tempCamRotSpeed = humanoid.turnSpeed;
            } else {
                tempCamRotSpeed = cameraRotationSpeed;
            }
            
            // Lock the camera behind the target inbetween the specified angle
            if (!inBetweenValue(dot,-.1f,.1f)){
                if (leftOfTarget){
                    rotateCamera(-tempCamRotSpeed * tpf);
                } else {
                    rotateCamera(tempCamRotSpeed * tpf);
                }
            } else {
                //To do: set camera directly behind target.
            }
            
        }
        
        targetLocation.set(target.getWorldTranslation()).addLocal(lookAtOffset);
        
        computePosition();
        cam.setLocation(pos.addLocal(lookAtOffset));
        
        //the cam looks at the target
        cam.lookAt(targetLocation, initialUpVec);
        
        adjustForCollision();
        
    }

    private void adjustForCollision(){
        if (collisionNode == null){
            return;
        }
        // 1. Reset results list.
        results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        ray = new Ray(targetLocation, cam.getDirection().negate());
        // 3. Collect intersections between Ray and Shootables in results list.
        collisionNode.collideWith(ray, results);
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0) {
          if (!collision){
            lastGoodDistance = targetDistance;
            collision = true;
          }
          
          targetDistance = results.getClosestCollision().getDistance() - collisionOffset;
        } else {
          if (collision){
             targetDistance = lastGoodDistance;
             collision = false;
          }
          
        }
        
    }
    
    private void leftOfTarget(){
        left = target.getLocalRotation().mult(Vector3f.UNIT_X);
        relative = cam.getLocation().subtract(target.getWorldTranslation());
        dot = left.dot(relative.normalize());
        leftOfTarget = dot > 0;
    }
    
    private boolean inBetweenValue(float value,float lesser,float greater){
        return value > lesser && value < greater;
    }
    
    /**
     * Returns the max zoom distance of the camera (default is 40)
     * @return maxDistance
     */
    public float getMaxDistance() {
        return maxDistance;
    }

    /**
     * Sets the max zoom distance of the camera (default is 40)
     * @param maxDistance
     */
    public void setMaxDistance(float maxDistance) {
        this.maxDistance = maxDistance;

    }

    /**
     * Returns the min zoom distance of the camera (default is 1)
     * @return minDistance
     */
    public float getMinDistance() {
        return minDistance;
    }

    /**
     * Sets the min zoom distance of the camera (default is 1)
     */
    public void setMinDistance(float minDistance) {
        this.minDistance = minDistance;
 
    }
 
    /**
     * Sets the spacial for the camera control, should only be used internally
     * @param spatial
     */
    public void setTarget(Node node) {
        target = node;
        if (node == null) {
            return;
        }
        computePosition();
        cam.setLocation(pos);
    }

    /**
     * @return The maximal vertical rotation angle in radian of the camera around the target
     */
    public float getMaxVerticalRotation() {
        return maxVerticalRotation;
    }

    /**
     * Sets the maximal vertical rotation angle in radian of the camera around the target. Default is Pi/2;
     * @param maxVerticalRotation
     */
    public void setMaxVerticalRotation(float maxVerticalRotation) {
        this.maxVerticalRotation = maxVerticalRotation;
    }

    /**
     *
     * @return The minimal vertical rotation angle in radian of the camera around the target
     */
    public float getMinVerticalRotation() {
        return minVerticalRotation;
    }

    /**
     * Sets the minimal vertical rotation angle in radian of the camera around the target default is 0;
     * @param minHeight
     */
    public void setMinVerticalRotation(float minHeight) {
        this.minVerticalRotation = minHeight;
    }

    /**
     * returns the zoom sensitivity
     * @return
     */
    public float getZoomSensitivity() {
        return zoomSensitivity;
    }

    /**
     * Sets the zoom sensitivity, the lower the value, the slower the camera will zoom in and out.
     * default is 2.
     * @param zoomSensitivity
     */
    public void setZoomSensitivity(float zoomSensitivity) {
        this.zoomSensitivity = zoomSensitivity;
    }
    
    /**
     * Returns the rotation speed when the mouse is moved.
     *
     * @return the rotation speed when the mouse is moved.
     */
    public float getRotationSpeed() {
        return rotationSpeed;
    }

    /**
     * Sets the rotate amount when user moves his mouse, the lower the value,
     * the slower the camera will rotate. default is 1.
     *
     * @param rotationSpeed Rotation speed on mouse movement, default is 1.
     */
    public void setRotationSpeed(float rotationSpeed) {
        this.rotationSpeed = rotationSpeed;
    }

    /**
     * returns the offset from the target's position where the camera looks at
     * @return
     */
    public Vector3f getLookAtOffset() {
        return lookAtOffset;
    }

    /**
     * Sets the offset from the target's position where the camera looks at
     * @param lookAtOffset
     */
    public void setLookAtOffset(Vector3f lookAtOffset) {
        this.lookAtOffset = lookAtOffset;
    }

    /**
     * Sets the up vector of the camera used for the lookAt on the target
     * @param up
     */
    public void setUpVector(Vector3f up) {
        initialUpVec = up;
    }

    /**
     * Returns the up vector of the camera used for the lookAt on the target
     * @return
     */
    public Vector3f getUpVector() {
        return initialUpVec;
    }

    public boolean isHideCursorOnRotate() {
        return hideCursorOnRotate;
    }

    public void setHideCursorOnRotate(boolean hideCursorOnRotate) {
        this.hideCursorOnRotate = hideCursorOnRotate;
    }

    /**
     * @return the cameraRotationSpeed
     */
    public float getCameraRotationSpeed() {
        return cameraRotationSpeed;
    }

    /**
     * @param cameraRotationSpeed the cameraRotationSpeed to set
     */
    public void setCameraRotationSpeed(float cameraRotationSpeed) {
        if (cameraRotationSpeed > humanoid.turnSpeed){
            this.cameraRotationSpeed = humanoid.turnSpeed;
        } else {
            this.cameraRotationSpeed = cameraRotationSpeed;
        }
        
    }

    /**
     * @return the invertYaxis
     */
    public boolean isInvertYaxis() {
        return invertYaxis;
    }

    /**
     * @param invertYaxis the invertYaxis to set
     */
    public void setInvertYaxis(boolean invertYaxis) {
        this.invertYaxis = invertYaxis;
    }

    /**
     * @return the invertXaxis
     */
    public boolean isInvertXaxis() {
        return invertXaxis;
    }

    /**
     * @param invertXaxis the invertXaxis to set
     */
    public void setInvertXaxis(boolean invertXaxis) {
        this.invertXaxis = invertXaxis;
    }

    /**
     * @return the cameraSensitivity
     */
    public float getCameraSensitivity() {
        return cameraSensitivity;
    }

    /**
     * A value from 0 to 1. This regulates the reaction time between
     * the camera and the player moving. Higher the value the quicker
     * the reaction time and the faster the camera will catch up to the
     * player.
     * @param cameraSensitivity the cameraSensitivity to set
     */
    public void setCameraSensitivity(float cameraSensitivity) {
        this.cameraSensitivity = FastMath.clamp(cameraSensitivity, 0, 1);
    }

    /**
     * @return the collisionNode
     */
    public Node getCollisionNode() {
        return collisionNode;
    }

    /**
     * @param collisionNode the collisionNode to set
     */
    public void setCollisionNode(Node collisionNode) {
        this.collisionNode = collisionNode;
    }

    /**
     * @return the collisionOffset
     */
    public float getCollisionOffset() {
        return collisionOffset;
    }

    /**
     * @param collisionOffset the collisionOffset to set
     */
    public void setCollisionOffset(float collisionOffset) {
        this.collisionOffset = collisionOffset;
    }

    /**
     * @return the lockAngle
     */
    public float getLockAngle() {
        return lockAngle;
    }

    /**
     * How far the camera can rotate away from the target to left and right.
     * The value is from 0 to 1. Where 0 to 1 represents a a half turn.
     * @param lockAngle the lockAngle to set
     */
    public void setLockAngle(float lockAngle) {
        this.lockAngle = FastMath.clamp(lockAngle, 0, 1);
    }

}

I swear I thought I put it in code blocks :wink:

Here is the relevant parts.The collision method seems to be doing strange things to my targetDistance variable. At first I thought maybe I had the ray going the wrong direction and I was hitting stuff I shouldn’t but I tested and not collisions happened during the jerking movements.

 protected void computePosition() {
        float hDistance = (targetDistance) * FastMath.sin((FastMath.PI / 2) - targetVRotation);
        pos.set(hDistance * FastMath.cos(targetRotation), (targetDistance) * FastMath.sin(targetVRotation), hDistance * FastMath.sin(targetRotation));
        pos.addLocal(target.getWorldTranslation());
    }

 private void adjustForCollision(){
        if (collisionNode == null){
            return;
        }
        // 1. Reset results list.
        results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        ray = new Ray(targetLocation, cam.getDirection().negate());
        // 3. Collect intersections between Ray and Shootables in results list.
        collisionNode.collideWith(ray, results);
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0) {
          if (!collision){
            lastGoodDistance = targetDistance;
            collision = true;
          }
          
          targetDistance = results.getClosestCollision().getDistance() - collisionOffset;
        } else {
          if (collision){
             targetDistance = lastGoodDistance;
             collision = false;
          }
          
        }
        
    }

you should use the ChaseCameraAppState as a basis…

  1. it’s an app state, the chase camera should have been one.
  2. it does almost the same with almost no math at all.
  3. ChaseCamera is definitely a bloat ware.
1 Like

Funny you should mention that because last night as I was digging through the libraries I noticed there where two chase cams. For now I scrapped the third person camera but in the future I think I may do just that add the chase cam app then just throw a collision check in the update loop.

This was going to be part of Jpony but I just left it out as anyone can simply do what I said above.

A video about 3rd person cams in Java with maths from ThinMatrix.

I figured out the problem with the distance resetting. Doing my raycast I failed to remember a line goes on infinitely into space. I didn’t check if the collision distance was greater then my cameras max distance.

1 Like

…set the max length of the Ray.

Wait wut ?! you can do that :stuck_out_tongue: I learned something new about JME today

I know the super secrets because I have access to the super-secret javadoc and read it quietly in my little hiding corner so no one will see me. :wink:

http://javadoc.jmonkeyengine.org/com/jme3/math/Ray.html

“limit” is a field right there on the first page. One need only scroll down a bit to find setLimit().

http://javadoc.jmonkeyengine.org/com/jme3/math/Ray.html#setLimit-float-

setLimit isn’t working unless their is some other super secret parameter I need to set.

Yes, it’s working. Your test is flawed.

No matter what I set the limit too it shoots off into the great beyond. Target distance is just how far the camera is away from the player.

private void adjustForCollision(){
        if (collisionNode == null){
            return;
        }
        // Clear the result list.
        results.clear();
        // Aim ray from target towards camera.
        ray.setOrigin(targetLocation);
        ray.setDirection(cam.getDirection().negate());
        ray.setLimit(targetDistance);
        // Get the results.
        collisionNode.collideWith(ray, results);
        // Check if we have any hits.
        if (results.size() > 0) {
            tempDistance = results.getClosestCollision().getDistance();
            if (tempDistance <= targetDistance){
                cam.setLocation(results.getClosestCollision().getContactPoint());
                cam.setLocation(cam.getLocation().add(cam.getDirection().mult(collisionOffset)));
            }  
        } 
        
    }

It works. I use it all the time.

I don’t have time to write a simple test case to prove that it works so maybe you can write one to prove me wrong so we can fix the issue.