ThirdPersonHandler

Upon request I’ve created a test showing how to work with the classes I posted.



I’ve also placed the camera system in a jar, which can be downloaded here.


/*
 * Copyright (c) 2003-2004, jMonkeyEngine - Mojo Monkey Coding 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 the Mojo Monkey Coding, jME,
 * jMonkey Engine, 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 OWNER 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 test;

import java.util.ArrayList;

import com.jme.app.SimpleGame;
import com.jme.math.FastMath;
import com.jme.math.Matrix3f;
import com.jme.math.Vector3f;
import com.jme.scene.BillboardNode;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Quad;
import com.jme.scene.shape.Sphere;

import camerasys.CameraHandler;

/**
 * @author Per Thulin
 */
public class TestCameraSystem extends SimpleGame {
   // The sphere moving back and forth.
   private Sphere player;
   
   // The box that makes out the ground.
   private Box ground;
   
   // Shows that Nodes also can be passed as targets.
   private Node complex;
   
   // The same applies to BillboardNodes.
   private BillboardNode billboard;
   
   // Used in <code>ThirdPersonHandler</code> to swap between targets.
   private ArrayList targets;
   
   // Makes the camera follow after and face our target.
   private CameraHandler cameraHandler;
   
   protected void simpleInitGame() {
      display.setTitle("Test Camera System | Wheel = zoom | S = swap target | + & - = vertical displacement");
      
      targets = new ArrayList();
      
      initPlayer();
      initComplexTarget();
      initBillboardTarget();
      initGround();
      initCamera();
      initInput();
   }
   
   private void initPlayer() {
      player = new Sphere("Player", 40, 40, 3);
      rootNode.attachChild(player);
      rootNode.addController(new PlayerMover());
      
      targets.add(player);
   }
   
   private void initComplexTarget() {
      complex = new Node("Complex Target");
      complex.setLocalTranslation(new Vector3f(-170, 20, 0));
      complex.addController(new ComplexMover());
      
      Box b1 = new Box("Box 1", new Vector3f(), 10, 10, 10);
      b1.setLocalTranslation(new Vector3f(0, 0, 0));
      complex.attachChild(b1);
      
      Box b2 = new Box("Box 2", new Vector3f(), 10, 1, 5);
      b2.setLocalTranslation(new Vector3f(0, 30f, 0));
      complex.attachChild(b2);
      
      Sphere s1 = new Sphere("Sphere 1", 10, 10, 10);
      s1.setLocalTranslation(new Vector3f(0, 15f, 0));
      complex.attachChild(s1);
      
      rootNode.attachChild(complex);
      
      targets.add(complex);
   }
   
   private void initBillboardTarget() {
      billboard = new BillboardNode("Billboard Target");
      billboard.setLocalTranslation(new Vector3f(150, 30, 0));
      
      billboard.attachChild(new Quad("Quad", 10, 10));
      
      rootNode.attachChild(billboard);
      
      targets.add(billboard);
   }
   
   private void initGround() {
      ground = new Box("Ground", new Vector3f(), 150, 1, 150);
      ground.setLocalTranslation(new Vector3f(0, -5f, 0));
      rootNode.attachChild(ground);
      
      targets.add(ground);
   }
   
   private void initCamera() {
      // Theta and phi is the start angles. If theta = 180 degrees the
      // billboard node gets screwed up. Why? I don't know :P
      float theta = 175 * FastMath.DEG_TO_RAD;
      float phi = 50 * FastMath.DEG_TO_RAD;
      
      // 200 = distance to the target.
      cameraHandler = new CameraHandler(theta, phi, 200);
      cameraHandler.setTarget(player);
   }
   
   private void initInput() {
      input = new ThirdPersonHandler(cameraHandler, targets);
   }
   
   public static void main(String[] args) {
      TestCameraSystem app = new TestCameraSystem();
      app.setDialogBehaviour(TestCameraSystem.ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }
   
   /**
    * A controller that makes the sphere move back and forth.
    *
    * @author Per Thulin
    */
   private class PlayerMover extends Controller {
      private float counter;
      
      public void update(float tpf) {
         counter += tpf;
         float z = FastMath.sin(counter) * 100f;
         player.getLocalTranslation().z = z;
      }      
   }
   
   /**
    * A controller that makes the complex target rotate and move up and down.
    *
    * @author Per Thulin
    */
   private class ComplexMover extends Controller {
      // temporary variables for rotation
      private final Vector3f tempVa = new Vector3f();
      private final Matrix3f incr = new Matrix3f();
      private final Matrix3f tempMa = new Matrix3f();
      private final Matrix3f tempMb = new Matrix3f();
      private float counter;
      
      public void update(float tpf) {
         counter += tpf;
         float y = 50 + FastMath.sin(counter) * 100f;
         complex.getLocalTranslation().y = y;
         incr.loadIdentity();
         incr.fromAxisAngle(complex.getLocalRotation().getRotationColumn(1,
               tempVa), 5 * tpf);
         complex.getLocalRotation().fromRotationMatrix(
               incr.mult(complex.getLocalRotation().toRotationMatrix(tempMa),
                     tempMb));
         complex.getLocalRotation().normalize();
      }      
   }
}



/*
 * Copyright (c) 2003-2004, jMonkeyEngine - Mojo Monkey Coding 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 the Mojo Monkey Coding, jME,
 * jMonkey Engine, 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 OWNER 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 test;

import java.util.ArrayList;

import com.jme.input.InputHandler;
import com.jme.input.InputSystem;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.input.RelativeMouse;
import com.jme.scene.Spatial;

import camerasys.CameraHandler;
import camerasys.SphericalMouseLook;

/**
 * @author Per Thulin
 */
public class ThirdPersonHandler extends InputHandler {
   private SphericalMouseLook mouseLook;   
   private CameraHandler cameraHandler;
   private ArrayList targets;
   private int targetNumber;
   
   public ThirdPersonHandler(CameraHandler cameraHandler, ArrayList targets) {
      this.cameraHandler = cameraHandler;
      this.targets = targets;
      targetNumber = 0;
      initKeyboard();
      initMouse();
   }
   
   private void initKeyboard() {
      keyboard = KeyBindingManager.getKeyBindingManager();
      keyboard.setKeyInput(InputSystem.getKeyInput());
      
      keyboard.set("increase_y_displacement", KeyInput.KEY_ADD);
      keyboard.set("decrease_y_displacement", KeyInput.KEY_SUBTRACT);
      keyboard.set("swap_target", KeyInput.KEY_S);
      
      setKeyBindingManager(keyboard);
   }
   
   private void initMouse() {
      mouse = new RelativeMouse("Mouse Input");
      mouse.setMouseInput(InputSystem.getMouseInput());
      
      setMouse(mouse);
      
      mouseLook = new SphericalMouseLook(mouse, cameraHandler);
      
      addAction(mouseLook);
   }
   
   public void update(float time) {
      super.update(time);
      
      if(keyboard.isValidCommand("increase_y_displacement")) {
         cameraHandler.getDisplacement().y += time * 100;
      }
      
      if(keyboard.isValidCommand("decrease_y_displacement")) {
         cameraHandler.getDisplacement().y -= time * 100;
      }
      
      if(keyboard.isValidCommand("swap_target", false)) {
         targetNumber++;
         if (targetNumber >= targets.size()) targetNumber = 0;
         cameraHandler.setTarget((Spatial) targets.get(targetNumber));
      }
      
      cameraHandler.update(time);
   }
}

I updated the test to show off vertical displacement.

Updated the test again. Now you can swap between four types of targets :slight_smile:

Good job, works well ! It’s almost there… I’m new to jME so I might be wrong, but I couldn’t find a way to get to the camera translation. Why? So that the player moves as the mouse does aka many 3rd person games. Therefore I’ve made a few tweaks so the player becomes controllable. Use the Up and Down arrow keys to move the player on the ground, moving the mouse rotates the player left/ right thus changing player’s direction.



Known issues:

i. The player will only rotate through 90 degrees at which point rotation stops even if you still use the mouse. I’m sure there’s an obvious reason but I couldn’t see it…



Add to end of CameraHandler so we can get to the camera translation

   public Camera getRenderCamera() {
      return this.cam;
   }



All other changes within TestCameraSystem
1. Change player to a box - spheres look the same if you rotate them! :D

   private void initPlayer() {
      player = new Box("Player Box", new Vector3f(), 20, 20, 20);
      rootNode.attachChild(player);
      rootNode.addController(new PlayerMover());
       
      targets.add(player);
   }



2. Change initInput to add key handling


   private void initInput() {
      input = new ThirdPersonHandler(cameraHandler, targets);
      KeyBindingManager keyboard = input.getKeyBindingManager();
      // add commands to move the player       
      keyboard.set("forward", KeyInput.KEY_UP);
      keyboard.set("backward", KeyInput.KEY_DOWN);
   }



3. Change the player controller routine PlayerMover

   private class PlayerMover extends Controller {
       // Nb - no reason for this to be a controller object now [todo]
       // temporary variables for rotation
       private final Vector3f tempVa = new Vector3f();
       private final Matrix3f incr = new Matrix3f();
       private final Matrix3f tempMa = new Matrix3f();
       private final Matrix3f tempMb = new Matrix3f();

       private float oldCamDirx = (float)0f;
       
      public void update(float tpf) {
         float speed = tpf*100; // not too fast !
         Vector3f camDir = cameraHandler.getRenderCamera().getDirection();
         float camDiffx = camDir.x-oldCamDirx;
         oldCamDirx=camDir.x;

           incr.loadIdentity();
           incr.fromAxisAngle(complex.getLocalRotation().getRotationColumn(1,
                    tempVa), camDiffx);
           player.getLocalRotation().fromRotationMatrix(
                   incr.mult(player.getLocalRotation().toRotationMatrix(tempMa),
                           tempMb));
           player.getLocalRotation().normalize();

         // Note, make keyboard global to optimize
         KeyBindingManager keyboard = input.getKeyBindingManager();

         if(keyboard.isValidCommand("forward")) {
             player.getLocalTranslation().x+=camDir.x*speed;
             player.getLocalTranslation().z+=camDir.z*speed;

         }
         if(keyboard.isValidCommand("backward")) {
             player.getLocalTranslation().x-=camDir.x*speed;
             player.getLocalTranslation().z-=camDir.z*speed;
         }
      }       
   }



4. Add a new import line for TestCameraSystem to access the keys and stuff

import com.jme.input.*;

Cool :slight_smile:



but I couldn't find a way to get to the camera translation. Why?

You can get its world translation through Camera.getLocation().

That is looking pretty good. I am going to play with it a bit more and probably go ahead and check it in. I’ll let you know.

Okey… I just made a small update:


  • Some optimizations to flatline memory usage.
  • Added setter in SphericalMouseLook to inverse vertical movement.
  • CameraManager now have displacements in a Vector3f, like in DP’s version. Note: this can break some code (updated the test to work with it).



    Source and jar can be downloaded zipped from here.



    Also, what do you feel about having two different targets? One which the camera follows and one which the camera looks at? It would solve Boiler98’s problem.

Also, maybe the name CameraHandler should be something more in line with DP’s version (SphericalCameraManager). ThirdPersonCameraManager? A bit long though :?



Also, place it in whatever package you’d like… com.jme.input & com.jme.input.action?

Also, what do you feel about having two different targets? One which the camera follows and one which the camera looks at? It would solve Boiler98's problem.


Sounds like a good idea, as long as it's optional and in the base case you only have to set a single target.

Ok, got that included now (updated the posted classes). I had to cut loose CameraHandler.getTarget(), but otherwise no code should get broken.



A little test to demo it (I know it’s not very pretty, but it serves its purpose. Maybe I’ll make a cooler one later on, with the camera gliding through a terrain, looking at something neat etc. :)):



/*
 * Copyright (c) 2003-2004, jMonkeyEngine - Mojo Monkey Coding 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 the Mojo Monkey Coding, jME,
 * jMonkey Engine, 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 OWNER 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 test;

import camerasys.CameraHandler;

import com.jme.app.SimpleGame;
import com.jme.input.KeyBindingManager;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Box;

/**
 * The <code>CameraHandler</code> contains two targets: followTarget and lookAtTarget.
 * In <code>TestCameraSystem</code> we used CameraHandler.setTarget(Spatial) to tell
 * the <code>CameraHandler</code> what to follow and look at.
 *
 * In this test we will let the camera follow one target while facing another.
 *
 * Enjoy! ;)
 *
 * @author Per Thulin
 */
public class TestTargetTypes extends SimpleGame {
   
   // The target we want our camera to follow.
   private Spatial followTarget;
   
   // The target we want our camera to look at.
   private Spatial lookAtTarget;
   
   // The manager that will take care of this.
   private CameraHandler cameraHandler;
   
   protected void simpleInitGame() {
      initFollowTarget();
      initLookAtTarget();
      initCamera();
      initInput();
   }
   
   /**
    * Here we create the target we want our camera to follow. We make it an
    * invisible Node (a lour). We also add a controller to it which makes it
    * move back and forth.
    */
   private void initFollowTarget() {
      followTarget = new Node("Lour");
      followTarget.setLocalTranslation(new Vector3f(200, 0, 0));
      followTarget.addController(new TargetMover());
      
      rootNode.attachChild(followTarget);
   }
   
   /**
    * Here we create a static (non moving) target which we want our camera to look
    * at.
    */
   private void initLookAtTarget() {
      lookAtTarget = new Box("Target", new Vector3f(), 10, 10, 10);
      
      rootNode.attachChild(lookAtTarget);
   }
   
   private void initCamera() {
      // A distance of 0 will make us sit within the target. Sort of like a
      // first person camera :)
      cameraHandler = new CameraHandler(0, 0, 0);
      cameraHandler.setMaxDistance(0);
      cameraHandler.setMinDistance(0);
      cameraHandler.setFollowTarget(followTarget);
      cameraHandler.setLookAtTarget(lookAtTarget);
   }
   
   /**
    * We can reuse the input handler used in the other test, as long as we remove
    * the "swap target" command (we have no targets to swap).
    */
   private void initInput() {
      input = new ThirdPersonHandler(cameraHandler, null);
      KeyBindingManager.getKeyBindingManager().remove("swap_target");
   }
   
   public static void main(String[] args) {
      TestTargetTypes app = new TestTargetTypes();
      app.setDialogBehaviour(TestTargetTypes.ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }
   
   /**
    * A controller that makes followTarget move back and forth.
    *
    * @author Per Thulin
    */
   private class TargetMover extends Controller {
      private float counter;
      
      public void update(float tpf) {
         counter += tpf;
         float z = FastMath.sin(counter) * 500;
         followTarget.getLocalTranslation().z = z;
      }      
   }
}



Source and jar can be downloaded here.

Ok, people are complaining about Marble Fun showing up weird when having low fps.



I’m very tired (it’s late here) but I think I know what the problem is. At line 208 in CameraHandler we have this:


speed = tpf * movementSpeed;


When having low fps (large tpf) this value can get very large, and if it exceeds 1 it will screw up the interpolation.

So I think a fix is changing it to:


speed = tpf * movementSpeed;
if (speed > 1) speed = 1;



Anyway, added that to my earlier post etc.

FYI I am currently working on this, got it up and working, now I’m just reorganizing some classes and methods to better fit into the input system. I’m going to bed now though and will get back to it tomorrow after work.

Cool :slight_smile:



now I'm just reorganizing some classes and methods to better fit into the input system

FWIW, I think DP got those KeyInputActions working in his version.

Hope you get this working, as I'm going to be away for a few days.

for your information, controlling the 3rdperson player with KeyNodeRotateLeftAction | KeyNodeForwardAction | KeyNodeLookUpAction works fine.



maybe a KeyNodeRotateUpAction with an axis lock could be usefull too ?



ps : this is great, many simple and enjoying games could be done in 3rd person (aircrafts,stunt cars,super frogs …), I think we will see more and more mini-games here !

FWIW, I think DP got those KeyInputActions working in his version.


I got your version in and then started working from his version to update some of the things he did, so it's a mixture of the two. I should be done by this evening if time permits me to work on it enough.

Hi,







How I can make my character rotate along "Y" and make the camera follow the movement during the rotation…Like the game: Vampire - BloodLines or Tomb Rider…



Ps: I