- jMonkeyEngine 3.0 Cookbook
- Rickard Edén
- 1451字
- 2021-09-03 10:00:48
Creating a reusable character control
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.
Getting ready
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.
How to do it...
Perform the following set of steps to create a reusable character control:
- Start by creating a new class called
GameCharacterControl
, which extendsBetterCharacterControl
. This class also needs to implementActionListener
andAnalogListener
. 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;
- Also, define a float field called
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 triggersisPressed == 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); } }
- Now that you have handled the key input, put the control Booleans to be used in the
update
method. You might recognize the code if you've looked atTestBetterCharacter
. The first thing it does is get the current direction thespatial
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);
- Depending on your Booleans, the following code modifies
walkDirection
. Normally, you would multiply the result bytpf
as well, but this is already handled in theBetterCharacterControl
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)); }
- Finally, in the
setWalkDirection
method, applywalkDirection
as follows:BetterCharacterControl.setWalkDirection(walkDirection);
- The preceding code handles moving forward, backward, and to the side. The turning and looking up and down actions of a character is normally handled by moving the mouse (or game controller), which is instead an analog input. This is handled by the
onAnalog
method. From here, we take the name of the input and apply its value to two new methods,rotate
andlookUpDown
, 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); } }
- Now, start by handling the process of turning the character left and right. The
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 itsviewDirection
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); }
- In order to handle looking up and down, you have to do some more work. The idea is to let the
spatial
object handle this. For this, you need to step back to the top of the class and add two more fields: aNode
field calledhead
and a float field calledyaw
. Theyaw
field will be the value with which you will control the rotation of the head up and down. - In the constructor, set the location of the
head
node. The location is relative to thespatial
object to an appropriate amount. In a normally scaled world,1.8f
would correspond to1.8
m (or about 6 feet):head.setLocalTranslation(0, 1.8f, 0);
- Next, you need to attach the
head
node tospatial
. You can do this in thesetSpatial
method. When aspatial
is supplied, first check whether it is aNode
(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); } }
- Now that you have a head that can rotate freely, you can implement the method that handles looking up and down. Modify the
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)); }
- Now, we have a character that can move and rotate like a standard FPS character. It still doesn't have a camera tied to it. To solve this, we're going to use the
CameraNode
class and hijack the application's camera.CameraNode
gives you the ability to control the camera as if it were a node. WithsetControlDir
, we instruct it to use the location and rotation ofspatial
as follows:public void setCamera(Camera cam){ CameraNode camNode = new CameraNode("CamNode", cam); camNode.setControlDir(CameraControl.ControlDirection.SpatialToCamera); head.attachChild(camNode); }
Note
Cameras are logical objects and are not part of the scene graph. The
CameraNode
keeps an instance of Camera. It is aNode
and propagates its own location to the Camera. It can also do the opposite and apply the Camera's location toCameraNode
(and thus, any otherspatial
object attached to it). - To use
GameCharacterControl
in an application, add the following lines of code in thesimpleInit
method of an application. Instantiate a new (invisible)Node
instance that you can add to theGameCharacterControl
class. Set the application's camera to be used as a character, and add it tophysicsSpace
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);
How it works...
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!
There's more...
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).
- Python編程自學手冊
- What's New in TensorFlow 2.0
- Visual Basic 6.0程序設計計算機組裝與維修
- Effective C#:改善C#代碼的50個有效方法(原書第3版)
- jQuery EasyUI網站開發實戰
- INSTANT Weka How-to
- 深入淺出Android Jetpack
- 程序員修煉之道:通向務實的最高境界(第2版)
- C語言程序設計學習指導與習題解答
- SAP BusinessObjects Dashboards 4.1 Cookbook
- 深入理解Elasticsearch(原書第3版)
- Tableau 10 Bootcamp
- NetBeans IDE 8 Cookbook
- Instant Lucene.NET
- Visual Basic程序設計上機實驗教程