To start off the chapter, we will create a class that we can use for various character-controlled purposes. The example describes an FPS character, but the method is the same for any player-controlled character.
The Control
class we'll build will be based on BetterCharacterControl
. It might be a good idea to have a look at the class or the TestBetterCharacter
example from the jMonkeyEngine test package if you want to find out how this works. Another good starting point would be the input examples from the same package.
The BetterCharacterControl
class is based on physics and requires a BulletAppState
class to be set up in the application. The steps required to do this are described in the The ImageGenerator class section in Appendix, Information Fragments. To find out more about bullet and physics, refer to Chapter 8, Physics with Bullet.
Perform the following set of steps to create a reusable character control:
GameCharacterControl
, which extends BetterCharacterControl
. This class also needs to implement ActionListener
and AnalogListener
. The idea here is to feed this class with actions that it can handle. To control the movement of a character, use a series of Booleans as follows:boolean forward, backward, leftRotate, rightRotate, leftStrafe, rightStrafe;
moveSpeed
, which will help you control how much the character will move in each update.The control Booleans you added are set in the implemented onAction
method. Note that a key will always trigger !isPressed when released (note that a key always triggers isPressed == false
when released):
public void onAction(String action, boolean isPressed, float tpf) { if (action.equals("StrafeLeft")) { leftStrafe = isPressed; } else if (action.equals("StrafeRight")) { rightStrafe = isPressed; } else if (action.equals("MoveForward")) { forward = isPressed; } else if (action.equals("MoveBackward")) { backward = isPressed; } else if (action.equals("Jump")) { jump(); } else if (action.equals("Duck")) { setDucked(isPressed); } }
update
method. You might recognize the code if you've looked at TestBetterCharacter
. The first thing it does is get the current direction the spatial
object is facing in order to move forward and backwards. It also checks which direction is left for strafing, as follows:public void update(float tpf) { super.update(tpf); Vector3f modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z); Vector3f modelLeftDir = spatial.getWorldRotation().mult(Vector3f.UNIT_X); walkDirection.set(0, 0, 0);
walkDirection
. Normally, you would multiply the result by tpf
as well, but this is already handled in the BetterCharacterControl
class as follows:if (forward) { walkDirection.addLocal(modelForwardDir.mult(moveSpeed)); } else if (backward) { walkDirection.addLocal(modelForwardDir.negate().multLocal(moveSpeed)); } if (leftStrafe) { walkDirection.addLocal(modelLeftDir.mult(moveSpeed)); } else if (rightStrafe) { walkDirection.addLocal(modelLeftDir.negate().multLocal(moveSpeed)); }
setWalkDirection
method, apply walkDirection
as follows:BetterCharacterControl.setWalkDirection(walkDirection);
onAnalog
method. From here, we take the name of the input and apply its value to two new methods, rotate
and lookUpDown
, as follows:public void onAnalog(String name, float value, float tpf) { if (name.equals("RotateLeft")) { rotate(tpf * value * sensitivity); } else if (name.equals("RotateRight")) { rotate(-tpf * value * sensitivity); } else if(name.equals("LookUp")){ lookUpDown(value * tpf * sensitivity); } else if (name.equals("LookDown")){ lookUpDown(-value * tpf * sensitivity); } }
BetterCharacterControl
class already has nice support for turning the character (which, in this case, is the same thing as looking left or right), and you can access its viewDirection
field directly. You should only modify the y axis, which is the axis that goes from head to toe, by a small amount as follows:private void rotate(float value){ Quaternion rotate = new Quaternion().fromAngleAxis(FastMath.PI * value, Vector3f.UNIT_Y); rotate.multLocal(viewDirection); setViewDirection(viewDirection); }
spatial
object handle this. For this, you need to step back to the top of the class and add two more fields: a Node
field called head
and a float field called yaw
. The yaw
field will be the value with which you will control the rotation of the head up and down.head
node. The location is relative to the spatial
object to an appropriate amount. In a normally scaled world, 1.8f
would correspond to 1.8
m (or about 6 feet):head.setLocalTranslation(0, 1.8f, 0);
head
node to spatial
. You can do this in the setSpatial
method. When a spatial
is supplied, first check whether it is a Node
(or you wouldn't be able to add the head). If it is, attach the head as follows:public void setSpatial(Spatial spatial) { super.setSpatial(spatial); if(spatial instanceof Node){ ((Node)spatial).attachChild(head); } }
yaw
field with the supplied value. Then, clamp it so that it can't be rotated more than 90 degrees up or down. Not doing this might lead to weird results. Then, set the rotation for the head around the x axis (think ear-to-ear) as follows:private void lookUpDown(float value){ yaw += value; yaw = FastMath.clamp(yaw, -FastMath.HALF_PI, FastMath.HALF_PI); head.setLocalRotation(new Quaternion().fromAngles(yaw, 0, 0)); }
CameraNode
class and hijack the application's camera. CameraNode
gives you the ability to control the camera as if it were a node. With setControlDir
, we instruct it to use the location and rotation of spatial
as follows:public void setCamera(Camera cam){ CameraNode camNode = new CameraNode("CamNode", cam); camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera); head.attachChild(camNode); }
GameCharacterControl
in an application, add the following lines of code in the simpleInit
method of an application. Instantiate a new (invisible) Node
instance that you can add to the GameCharacterControl
class. Set the application's camera to be used as a character, and add it to physicsSpace
as follows:Node playerNode = new Node("Player"); GameCharacterControl charControl = new GameCharacterControl(0.5f, 2.5f, 8f); charControl.setCamera(cam); playerNode.addControl(charControl); charControl.setGravity(normalGravity); bulletAppState.getPhysicsSpace().add(charControl);
The BetterCharacterControl
class of jMonkeyEngine already has a lot of the functionalities to handle the movement of a character. By extending it, we get access to it and we can implement the additional functionality on top of it.
The reason we use Booleans to control movement is that the events in onAction
and onAnalog
are not fired continuously; they are fired only when they're changed. So, pressing a key wouldn't generate more than two actions, one on pressing it and one on releasing it. With the Boolean, we ensure that the action will keep getting performed until the player releases the key.
This method waits for an action to happen, and depending on the binding parameter, it will set or unset one of our Booleans. By listening for actions rather than inputs (the actual key strokes), we can reuse this class for non-player characters (NPCs).
We can't handle looking up and down in the same way as we perform sideways rotations. The reason is that the latter changes the actual direction of the movement. When looking up or down, we just want the camera to look that way. The character is usually locked to the ground (it would be different in a flight simulator, though!).
As we can see, the BetterCharacterControl
class already has ways to handle jumping and ducking. Nice!
Let's say we would rather have a third-person game. How difficult would it be to modify this class to support that? In a later recipe, we will look at jMonkeyEngine's ChaseCamera
class, but by inserting the following two lines of code at the end of our setCamera
method, we will get a basic camera that follows the character:
camNode.setLocalTranslation(new Vector3f(0, 5, -5)); camNode.lookAt(head.getLocalTranslation(), Vector3f.UNIT_Y);
It's all handled by CamNode
, which offsets the camera's location in relation to its own (which follows the head
node). After moving CamNode
, we make sure that the camera also looks at the head (rather than the default forward).
3.138.120.187