= User Example: Third Person Camera
:author:
:revnumber:
:revdate: 2016/03/17 20:48
:relfileprefix: ../../
:imagesdir: ../..
ifdef::env-github,env-browser[:outfilesuffix: .adoc]


image:jme3/user_examples/thirdpersoncamera/3p.png[3p.png,width="",height=""]

(by Berzee) (see a video demonstration here: link:https://www.youtube.com/watch?v=phu52hmIoYU[https://www.youtube.com/watch?v=phu52hmIoYU])

Here is a small example of a self-contained Third Person Character (with camera). It uses the familiar Green Ninja model to demonstrate the following controls and animations:

*  “WASD movement
*  “SPACE to jump
*  Horizontal mouse movement to turn left and right
*  Vertical mouselook (camera pivots around character)
*  Left Mouse Button to attack

To try this example for yourself, you can copy the classes from this page. Then you will be able to create a Third-Person Character in your apps like so:

[source,java]
----

player = new ThirdPersonPlayerNode(playerModel, inputManager, cam);
rootNode.attachChild(player);
bulletAppState.getPhysicsSpace().add(player);

//make sure to call "player.update()" in the app's update method, too

----


== How the mouselook works

Most of the code in the example files should be relatively self-explanatory, but mouselook deserves a lovely pictograph of its own. It's easy to make <<jme3/advanced/making_the_camera_follow_a_character#,a camera that follows an object and always looks at the back of it>>, and thus it's easy to account for horizontal mouse movement by simple rotating the entire Player character left or right.

But what if we want to be able to move the camera angle up or down, while the Player stays firmly upright? We can accomplish this with minimal Horrible Maths by adding an invisible “pivot node to the Player, and instructing the camera to follow that “pivot instead. Then we can independently rotate the *pivot* instead of the Player, and do things like this:

image:jme3/user_examples/thirdpersoncamera/pivot2.png[pivot2.png,width="",height=""]

Now you know the mystical secrets, and are ready to see the code itself.


== Source Code


=== ThirdPersonPlayerNode.java

ThirdPersonPlayerNode.java contains the bulk of the logic for keyboard, mouse, physics, and animation. Most of this is lifted straight from the Tutorials for Beginners and can safely be ignored. However, pay particular attention to the “onAnalog() method – that's where all of the mouse movement is processed to turn the player left and right, and the camera up and down.

[source,java]
----

package mygame;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

/**
 *
 * @author Berzee
 */
public class ThirdPersonPlayerNode extends Node implements ActionListener, AnalogListener, AnimEventListener
{
    //ThirdPersonCameraNode automatically sets itself up to follow a target
    //object. Check the "onAnalog" function here to see how we do mouselook.
    private ThirdPersonCamera camera;
    private Camera cam;

    private Spatial model;
    private CharacterControl characterControl;
    private AnimChannel animChannel;
    private AnimControl animControl;
    private InputManager inputManager;
    private Vector3f walkDirection = new Vector3f();

    private boolean left = false;
    private boolean right = false;
    private boolean up = false;
    private boolean down = false;
    private boolean attack;
    private boolean attacking;

    //These can all be changed according to your whims.
    private float walkSpeed = .15f;
    private float mouselookSpeed = FastMath.PI;
    private float jumpSpeed = 15;
    private float fallSpeed = 20;
    private float gravity = 25;
    private float stepSize = .05f;

    //Animation names. These are currently set up for the Ninja.mesh.xml from
    //jme3-test-data.jar (to add that jar to your project, right-click on
    //Libraries in the SDK project explorer, click "Add Library" and choose
    //jme3-test-data.jar from the menu).
    //
    //Alternatively, use any model you like and change these animation names
    //as needed.
    private String idleAnim = "Idle1";
    private String walkAnim = "Walk";
    private String attackAnim = "Attack3";
    private String jumpAnim = "Climb"; //hilarious

    public ThirdPersonPlayerNode(Spatial model, InputManager inputManager, Camera cam)
    {
	super();
	this.cam = cam;
	camera = new ThirdPersonCamera("CamNode", cam, this);

        this.model = model;
	this.model.scale(.01f); //Ninja.mesh.xml-specific scale stuff
	this.model.setLocalTranslation(0f, -1f, 0f); //Ninja-specific
	this.attachChild(this.model);

	CapsuleCollisionShape playerShape = new CapsuleCollisionShape(.5f,1f); //Ninja-specific
	characterControl = new CharacterControl(playerShape, stepSize);
	characterControl.setJumpSpeed(jumpSpeed);
	characterControl.setFallSpeed(fallSpeed);
	characterControl.setGravity(gravity);
	this.addControl(characterControl);

	animControl = model.getControl(AnimControl.class);
	animControl.addListener(this);
	animChannel = animControl.createChannel();
	animChannel.setAnim(idleAnim);

	this.inputManager = inputManager;
	setUpKeys();
    }

    //Make sure to call this from the main simpleUpdate() loop
    public void update()
    {
	Vector3f camDir = cam.getDirection().clone();
	camDir.y = 0;
	Vector3f camLeft = cam.getLeft().clone();
	camLeft.y = 0;
	walkDirection.set(0, 0, 0);

	if (left)  { walkDirection.addLocal(camLeft); }
	if (right) { walkDirection.addLocal(camLeft.negate()); }
	if (up)    { walkDirection.addLocal(camDir); }
	if (down)  { walkDirection.addLocal(camDir.negate()); }

	characterControl.setWalkDirection(walkDirection.normalize().multLocal(walkSpeed));

	handleAnimations();
    }

    private void handleAnimations()
    {
	if(attacking)
	{
	    //waiting for attack animation to finish
	}
	else if(attack)
	{

	    animChannel.setAnim(attackAnim,.3f);
	    animChannel.setLoopMode(LoopMode.DontLoop);
	    attack = false;
	    attacking = true;
	}
	else if(characterControl.onGround())
	{
	    if(left || right || up || down)
	    {
		if(!animChannel.getAnimationName().equals(walkAnim))
		{
		    animChannel.setAnim(walkAnim,.3f);
		    animChannel.setLoopMode(LoopMode.Loop);
		}
	    }
	    else
	    {
		if(!animChannel.getAnimationName().equals(idleAnim))
		{
		    animChannel.setAnim(idleAnim,.3f);
		    animChannel.setLoopMode(LoopMode.Cycle);
		}
	    }
	}
    }

    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.addMapping("Attack", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
	inputManager.addMapping("TurnLeft", new MouseAxisTrigger(MouseInput.AXIS_X,true));
	inputManager.addMapping("TurnRight", new MouseAxisTrigger(MouseInput.AXIS_X,false));
	inputManager.addMapping("MouselookDown", new MouseAxisTrigger(MouseInput.AXIS_Y,true));
	inputManager.addMapping("MouselookUp", new MouseAxisTrigger(MouseInput.AXIS_Y,false));
	inputManager.addListener(this, "Left");
	inputManager.addListener(this, "Right");
	inputManager.addListener(this, "Up");
	inputManager.addListener(this, "Down");
	inputManager.addListener(this, "Jump");
	inputManager.addListener(this, "Attack");
	inputManager.addListener(this, "TurnLeft");
	inputManager.addListener(this, "TurnRight");
	inputManager.addListener(this, "MouselookDown");
	inputManager.addListener(this, "MouselookUp");
    }

    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"))
	{
	    if(characterControl.onGround())
	    {
		characterControl.jump();
		if(!attacking)
		{
		    animChannel.setAnim(jumpAnim,.3f);
		    animChannel.setLoopMode(LoopMode.Loop);
		}
	    }
	}
	else if (binding.equals("Attack"))
	{
	    attack = value;
	}
    }

    //Analog handler for mouse movement events.
    //It is assumed that we want horizontal movements to turn the character,
    //while vertical movements only make the camera rotate up or down.
    public void onAnalog(String binding, float value, float tpf)
    {
	if (binding.equals("TurnLeft"))
	{
	    Quaternion turn = new Quaternion();
	    turn.fromAngleAxis(mouselookSpeed*value, Vector3f.UNIT_Y);
	    characterControl.setViewDirection(turn.mult(characterControl.getViewDirection()));
	}
	else if (binding.equals("TurnRight"))
	{
	    Quaternion turn = new Quaternion();
	    turn.fromAngleAxis(-mouselookSpeed*value, Vector3f.UNIT_Y);
	    characterControl.setViewDirection(turn.mult(characterControl.getViewDirection()));
	}
	else if (binding.equals("MouselookDown"))
	{
	    camera.verticalRotate(mouselookSpeed*value);
	}
	else if (binding.equals("MouselookUp"))
	{
	    camera.verticalRotate(-mouselookSpeed*value);
	}
    }

    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName)
    {
	if(channel == animChannel && attacking && animName.equals(attackAnim))
	{
	    attacking = false;
	}
    }

    public void onAnimChange(AnimControl control, AnimChannel channel, String animName)
    {
    }

    public CharacterControl getCharacterControl()
    {
	return characterControl;
    }

    public ThirdPersonCamera getCameraNode()
    {
	return camera;
    }
}

----


=== ThirdPersonCamera.java

ThirdPersonCamera.java sets up a CameraNode to follow behind the player. It uses the same “ControlDirection.SpatialToCamera approach as <<jme3/advanced/making_the_camera_follow_a_character#,the advanced tutorial about 3rd-person cameras>>, but also adds a pivot node for easy mouselook. (See the scientific diagram above, and the comments in the class, for more explanation).

[source,java]
----

package mygame;

import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl;

/**
 *
 * @author Berzee
 */
public class ThirdPersonCamera
{
    //The "pivot" Node allows for easy third-person mouselook! It's actually
    //just an empty Node that gets attached to the center of the Player.
    //
    //The CameraNode is set up to always position itself behind the *pivot*
    //instead of behind the Player. So when we want to mouselook around the
    //Player, we simply need to spin the pivot! The camera will orbit behind it
    //while the Player object remains still.
    //
    //NOTE: Currently only vertical mouselook (around the X axis) is working.
    //The other two axes could be added fairly easily, once you have an idea
    //for how they should actually behave (min and max angles, et cetera).
    private Node pivot;
    private CameraNode cameraNode;

    //Change these as you desire. Lower verticalAngle values will put the camera
    //closer to the ground.
    public float followDistance = 7;
    public float verticalAngle = 30 * FastMath.DEG_TO_RAD;

    //These bounds keep the camera from spinning too far and clipping through
    //the floor or turning upside-down. You can change them as needed but it is
    //recommended to keep the values in the (-90,90) range.
    public float maxVerticalAngle = 85 * FastMath.DEG_TO_RAD;
    public float minVerticalAngle = 5 * FastMath.DEG_TO_RAD;

    public ThirdPersonCamera(String name, Camera cam, Node player)
    {
	pivot = new Node("CamTrack");
	player.attachChild(pivot);

	cameraNode = new CameraNode(name, cam);
        cameraNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera);
	pivot.attachChild(cameraNode);
	cameraNode.setLocalTranslation(new Vector3f(0, 0, followDistance));
	cameraNode.lookAt(pivot.getLocalTranslation(), Vector3f.UNIT_Y);

	pivot.getLocalRotation().fromAngleAxis(-verticalAngle, Vector3f.UNIT_X);
    }

    public void verticalRotate(float angle)
    {
	verticalAngle += angle;

	if(verticalAngle > maxVerticalAngle)
	{
	    verticalAngle = maxVerticalAngle;
	}
	else if(verticalAngle < minVerticalAngle)
	{
	    verticalAngle = minVerticalAngle;
	}

	pivot.getLocalRotation().fromAngleAxis(-verticalAngle, Vector3f.UNIT_X);
    }

    public CameraNode getCameraNode()
    {
	return cameraNode;
    }

    public Node getCameraTrack()
    {
	return pivot;
    }
}

----


=== Main.java

Here is an example Main.java project that you can use to see the ThirdPersonPlayerNode in action. Just make sure to add the jme3-test-data.jar library to your project so it can access the Ninja model and ManyLights scene (In the SDK: Project Explorer → Right Click on “Libraries → Add Library → jme3-test-data.jar).

[source,java]
----

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.light.DirectionalLight;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Spatial;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;

/**
 * @author Berzee
 */
public class Main extends SimpleApplication
{
    private Spatial sceneModel;
    private RigidBodyControl scene;
    private BulletAppState bulletAppState;
    private ThirdPersonPlayerNode player;

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        mouseInput.setCursorVisible(false);
	flyCam.setEnabled(false);

	bulletAppState = new BulletAppState();
	stateManager.attach(bulletAppState);
	//bulletAppState.getPhysicsSpace().enableDebug(assetManager);

        sceneModel = assetManager.loadModel("Scenes/ManyLights/Main.scene");
	sceneModel.scale(1f,.5f,1f); //Make scenery short enough to jump on. =P
	CollisionShape sceneShape = CollisionShapeFactory.createMeshShape((Node) sceneModel);
	scene = new RigidBodyControl(sceneShape, 0);
	sceneModel.addControl(scene);
        rootNode.attachChild(sceneModel);
	bulletAppState.getPhysicsSpace().add(scene);

	Spatial playerModel = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
	player = new ThirdPersonPlayerNode(playerModel, inputManager, cam);
	player.getCharacterControl().setPhysicsLocation(new Vector3f(-5f,2f,5f));
	rootNode.attachChild(player);
	bulletAppState.getPhysicsSpace().add(player);

        // You must add a light to make the model visible
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-.1f, -.7f, -1f));
        rootNode.addLight(sun);
    }

    @Override
    public void simpleUpdate(float tpf)
    {
	player.update();
    }

    @Override
    public void simpleRender(RenderManager rm)
    {
        //TODO: add render code
    }
}

----
