Skip to content
This repository has been archived by the owner on May 4, 2018. It is now read-only.

Latest commit

 

History

History
464 lines (424 loc) · 45.4 KB

walking_character.md

File metadata and controls

464 lines (424 loc) · 45.4 KB
title
Walking Character

Walking Character

In the Hello Collision tutorial and the TestQ3.java code sample you have seen how to create collidable landscapes and walk around in a first-person perspective. The first-person camera is enclosed by a collision shape and is steered by the BetterCharacterControl.

Other games however require a third-person perspective of the character: In these cases you use a CharacterControl on a Spatial. This example also shows how to set up custom navigation controls, so you can press WASD to make the third-person character walk; and how to implement dragging the mouse to rotate.

Some details on this page still need to be updated from old CharacterControl API to BetterCharacterControl API.

Sample Code

Several related code samples can be found here:

The code in this tutorial is a combination of these samples.

BetterCharacterControl

Motivation: When you load a character model, give it a RigidBodyControl, and use forces to push it around, you do not get the desired behaviour: RigidBodyControl'ed objects (such as cubes and spheres) roll or tip over when pushed by physical forces. This is not the behaviour that you expect of a walking character. JME3's BulletPhysics integration offers a BetterCharacterControl with a setWalkDirection() method. You use it to create simple characters that treat floors and walls as solid, and always stays locked upright while moving.

This code sample creates a simple upright Character:

// Load any model
Node myCharacter = (Node) assetManager.loadModel("Models/myCharacterModel.mesh.xml");
rootNode.attachChild(myCharacter);
// Create a appropriate physical shape for it
CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(1.5f, 6f, 1);
CharacterControl myCharacter_phys = new CharacterControl(capsuleShape, 0.01f);
// Attach physical properties to model and PhysicsSpace
myCharacter.addControl(myCharacter_phys);
bulletAppState.getPhysicsSpace().add(myCharacter_phys);

To use the BetterCharacterControl, you may load your character like this:

// Load any model
Spatial playerSpatial = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
player =  (Node)playerSpatial; // You can set the model directly to the player. (We just wanted to explicitly show that it's a spatial.)
Node playerNode = new Node(); // You can create a new node to wrap your player to adjust the location. (This allows you to solve issues with character sinking into floor, etc.)
playerNode.attachChild(player); // add it to the wrapper
player.move(0,3.5f,0); // adjust position to ensure collisions occur correctly.
player.setLocalScale(0.5f); // optionally adjust scale of model
// setup animation:
        control = player.getControl(AnimControl.class);
        control.addListener(this);
        channel = control.createChannel();
        channel.setAnim("stand");
playerControl = new BetterCharacterControl(1.5f, 6f, 1f); // construct character. (If your character bounces, try increasing height and weight.)
playerNode.addControl(playerControl); // attach to wrapper
// set basic physical properties:
        playerControl.setJumpForce(new Vector3f(0,5f,0)); 
        playerControl.setGravity(new Vector3f(0,1f,0));
playerControl.warp(new Vector3f(0,10,10)); // warp character into landscape at particular location
// add to physics state
        bulletAppState.getPhysicsSpace().add(playerControl); 
        bulletAppState.getPhysicsSpace().addAll(playerNode); 
rootNode.attachChild(playerNode); // add wrapper to root

The BulletPhysics CharacterControl only collides with “real” PhysicsControls (RigidBody). It does not detect collisions with other CharacterControls! If you need additional collision checks, add GhostControls to your characters and create a custom collision listener to respond. (The JME3 team may implement a better CharacterControl one day.)

A CharacterControl is a special kinematic object with restricted movement. CharacterControls have a fixed “upward” axis, this means they do not topple over when walking over an obstacle (as opposed to RigidBodyControls), which simulates a being's ability to balance upright. A CharacterControl can jump and fall along its upward axis, and it can scale steps of a certain height/steepness.

CharacterControl Method Property
setUpAxis(1) Fixed upward axis. Values: 0 = X axis , 1 = Y axis , 2 = Z axis.
Default: 1, because for characters and vehicles, up is typically along the Y axis.
setJumpSpeed(10f) Jump speed (movement along upward-axis)
setFallSpeed(20f) Fall speed (movement opposite to upward-axis)
setMaxSlope(1.5f) How steep the slopes and steps are that the character can climb without considering them an obstacle. Higher obstacles need to be jumped. Vertical height in world units.
setGravity(1f) The intensity of gravity for this CharacterControl'ed entity. Tip: To change the direction of gravity for a character, modify the up axis.
setWalkDirection(new Vector3f(0f,0f,0.1f)) (CharacterControl only) Make a physical character walk continuously while checking for floors and walls as solid obstacles. This should probably be called “setPositionIncrementPerSimulatorStep”. This argument is neither a direction nor a velocity, but the amount to increment the position each physics tick: vector length = accuracy*speed in m/s.
Use setWalkDirection(Vector3f.ZERO) to stop a directional motion.

For best practices on how to use setWalkDirection(), see the Navigation Inputs example below.

Walking Character Demo

Code Skeleton

public class WalkingCharacterDemo extends SimpleApplication
        implements ActionListener, AnimEventListener {
 
  public static void main(String[] args) {
    WalkingCharacterDemo app = new WalkingCharacterDemo();
    app.start();
  }
 
  public void simpleInitApp() { }
 
  public void simpleUpdate(float tpf) { }
 
  public void onAction(String name, boolean isPressed, float tpf) { }
 
  public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { }
 
  public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { }

Overview

To create a walking character:

  1. (Unless you already have it) Activate physics in the scene by adding a BulletAppState.
  2. Init the scene by loading the game level model (terrain or floor/buildings), and giving the scene a MeshCollisionShape.
  3. Create the animated character:
    1. Load an animated character model.
    2. Add a CharacterControl to the model.
  4. Set up animation channel and controllers.
  5. Add a ChaseCam or CameraNode.
  6. Handle navigational inputs.

Activate Physics

private BulletAppState bulletAppState;
...
public void simpleInitApp() {
    bulletAppState = new BulletAppState();
    //bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
    stateManager.attach(bulletAppState);
    ...
}

Initialize the Scene

In the simpleInitApp() method you initialize the scene and give it a MeshCollisionShape. The sample in the jme3 sources uses a custom helper class that simply creates a flat floor and drops some cubes and spheres on it:

public void simpleInitApp() {
  ...
  PhysicsTestHelper.createPhysicsTestWorld(rootNode,
      assetManager, bulletAppState.getPhysicsSpace());
  ...

In a real game, you would load a scene model here instead of a test world. You can load a model from a local or remote zip file, and scale and position it:

private Node gameLevel;
..
public void simpleInitApp() {
  ...
  //assetManager.registerLocator("quake3level.zip", ZipLocator.class);
  assetManager.registerLocator(
  "http://jmonkeyengine.googlecode.com/files/quake3level.zip",
    HttpZipLocator.class);
  MaterialList matList = (MaterialList) assetManager.loadAsset("Scene.material");
  OgreMeshKey key = new OgreMeshKey("main.meshxml", matList);
  gameLevel = (Node) assetManager.loadAsset(key);
  gameLevel.setLocalTranslation(-20, -16, 20);
  gameLevel.setLocalScale(0.10f);
  gameLevel.addControl(new RigidBodyControl(0));
  rootNode.attachChild(gameLevel);
  bulletAppState.getPhysicsSpace().addAll(gameLevel);
  ...

Also, add a light source to be able to see the scene.

  AmbientLight light = new AmbientLight();
  light.setColor(ColorRGBA.White.mult(2));
  rootNode.addLight(light);

Create the Animated Character

You create an animated model, such as Oto.mesh.xml.

  1. Place the “Oto” model into the assets/Models/Oto/ directory of your project.
  2. Create the CollisionShape and adjust the capsule radius and height to fit your character model.
  3. Create the CharacterControl and adjust the stepheight (here 0.05f) to the height that the character can climb up without jumping.
  4. Load the visible model. Make sure its start position does not overlap with scene objects.
  5. Add the CharacterControl to the model and register it to the physicsSpace.
  6. Attach the visible model to the rootNode.
private CharacterControl character;
private Node model;
...
public void simpleInitApp() {
  ...
  CapsuleCollisionShape capsule = new CapsuleCollisionShape(3f, 4f);
  character = new CharacterControl(capsule, 0.05f);
  character.setJumpSpeed(20f);
  model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");
  model.addControl(character);
  bulletAppState.getPhysicsSpace().add(character);
  rootNode.attachChild(model);
  ...

Did you know? A CapsuleCollisionShape is a cylinder with rounded top and bottom. A capsule rotated upright is a good collision shape for a humanoid character since its roundedness reduces the risk of getting stuck on obstacles.

Set Up AnimControl and AnimChannels

Create several AnimChannels, one for each animation that can happen simultaneously. In this example, you create one channel for walking and one for attacking. (Because the character can attack with its arms and walk with the rest of the body at the same time.)

private AnimChannel animationChannel;
private AnimChannel attackChannel;
private AnimControl animationControl;
...
public void simpleInitApp() {
  ...
  animationControl = model.getControl(AnimControl.class);
  animationControl.addListener(this);
  animationChannel = animationControl.createChannel();
  attackChannel = animationControl.createChannel();
  attackChannel.addBone(animationControl.getSkeleton().getBone("uparm.right"));
  attackChannel.addBone(animationControl.getSkeleton().getBone("arm.right"));
  attackChannel.addBone(animationControl.getSkeleton().getBone("hand.right"));
  ...

The attackChannel only controls one arm, while the walking channels controls the whole character.

Add ChaseCam / CameraNode

private ChaseCamera chaseCam;
 
...
 
public void simpleInitApp() {
  ...
  flyCam.setEnabled(false);
  chaseCam = new ChaseCamera(cam, model, inputManager);
  ...

Handle Navigation

Configure custom key bindings for WASD keys that you will use to make the character walk. Then calculate the vector where the user wants the character to move. Note the use of the special setWalkDirection() method below.

// track directional input, so we can walk left-forward etc
private boolean left = false, right = false, up = false, down = false;
...
 
public void simpleInitApp() {
  ...
  // configure mappings, e.g. the WASD keys
  inputManager.addMapping("CharLeft", new KeyTrigger(KeyInput.KEY_A));
  inputManager.addMapping("CharRight", new KeyTrigger(KeyInput.KEY_D));
  inputManager.addMapping("CharForward", new KeyTrigger(KeyInput.KEY_W));
  inputManager.addMapping("CharBackward", new KeyTrigger(KeyInput.KEY_S));
  inputManager.addMapping("CharJump", new KeyTrigger(KeyInput.KEY_RETURN));
  inputManager.addMapping("CharAttack", new KeyTrigger(KeyInput.KEY_SPACE));
  inputManager.addListener(this, "CharLeft", "CharRight");
  inputManager.addListener(this, "CharForward", "CharBackward");
  inputManager.addListener(this, "CharJump", "CharAttack");
  ...
}

Respond to the key bindings by setting variables that track in which direction you will go. This allows us to steer the character forwards and to the left at the same time. Note that no actual walking happens here yet! We just track the input.

@Override
public void onAction(String binding, boolean value, float tpf) {
  if (binding.equals("CharLeft")) {
      if (value) left = true;
      else left = false;
  } else if (binding.equals("CharRight")) {
      if (value) right = true;
      else right = false;
  } else if (binding.equals("CharForward")) {
      if (value) up = true;
      else up = false;
  } else if (binding.equals("CharBackward")) {
      if (value) down = true;
      else down = false;
  } else if (binding.equals("CharJump"))
      character.jump();
  if (binding.equals("CharAttack"))
    attack();
}

The player can attack and walk at the same time. Attack() is a custom method that triggers an attack animation in the arms. Here you should also add custom code to play an effect and sound, and to determine whether the hit was successful.

private void attack() {
    attackChannel.setAnim("Dodge", 0.1f);
    attackChannel.setLoopMode(LoopMode.DontLoop);
}

Finally, the update loop looks at the directional variables and moves the character accordingly. Since this is a special kinematic CharacterControl, we use the setWalkDirection() method.

The variable airTime tracks how long the character is off the ground (e.g. when jumping or falling) and adjusts the walk and stand animations acccordingly.

private Vector3f walkDirection = new Vector3f(0,0,0); // stop
 
private float airTime = 0;
 
public void simpleUpdate(float tpf) {
  Vector3f camDir = cam.getDirection().clone();
  Vector3f camLeft = cam.getLeft().clone();
  camDir.y = 0;
  camLeft.y = 0;
  camDir.normalizeLocal();
  camLeft.normalizeLocal();
  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());
 
  if (!character.onGround()) { // use !character.isOnGround() if the character is a BetterCharacterControl type.
      airTime += tpf;
  } else {
      airTime = 0;
  }
 
  if (walkDirection.lengthSquared() == 0) { //Use lengthSquared() (No need for an extra sqrt())
      if (!"stand".equals(animationChannel.getAnimationName())) {
        animationChannel.setAnim("stand", 1f);
      }
  } else {
      character.setViewDirection(walkDirection);
      if (airTime > .3f) {
        if (!"stand".equals(animationChannel.getAnimationName())) {
          animationChannel.setAnim("stand");
        }
      } else if (!"Walk".equals(animationChannel.getAnimationName())) {
        animationChannel.setAnim("Walk", 0.7f);
      }
    }
 
  walkDirection.multLocal(25f).multLocal(tpf);// The use of the first multLocal here is to control the rate of movement multiplier for character walk speed. The second one is to make sure the character walks the same speed no matter what the frame rate is.
  character.setWalkDirection(walkDirection); // THIS IS WHERE THE WALKING HAPPENS
}

This method resets the walk animation.

public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
        if (channel == attackChannel) channel.setAnim("stand");
}
 
public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { }

See also