Crouching for CharacterControl

Hi guys,



I’m working on a 1st person character control. I used the Q3 and walking character examples to get started but I’m stuck at trying to make a crouch behavior for the character. I created 2 capsule collision shapes for my character, one when he stands up and one when he crouches (so he can sneak under obstacles for example). When he crouches or stands up, I swap the collision shape attached to the character control. So far it works (see attached screenshots to see the collisionshape change). By the way, I offset the camera to get a clear view of the collision shape, there is no character yet, the collision shape is around a node.



first screenshot (standing): http://imgur.com/WtNwY

second screenshot (crouching): http://imgur.com/5IpzT



As you can see, when I swap the shape to the crouching one, the shape “floats”. It doesn’t fall to the ground as it should. If I Console.out its onGround property, it is always true, but it is clearly floating above the ground.



I tried disabling the character control, then swapping the collisionshape, then enabling the character (so it gets removed then added to physics space), but it doesn’t change anything.

I also tried manually removing and adding character control from physic space, but it won’t work either.



Is it the right way to achieve crouching? Is there someway to tell the character control to “fall to the ground”? Any ideas?



Thanks! :slight_smile:

can you show the code that you are using/a testcase

Sure, I took some time to make a test case based on Q3test.



The controls in the example are very clunky, but it’s enough to see the issue. I removed the jump functionality and replaced it by crouch. Move the camera close to the CollisionShape by pressing “w”. When you get near, press on “space” to go to crouch and release “space” to go back to standing. You should see the shape change, but the shapes doesn’t fall to the ground.



Here is the code:



[java]

package testClasses;



import com.jme3.app.SimpleApplication;

import com.jme3.asset.plugins.HttpZipLocator;

import com.jme3.asset.plugins.ZipLocator;

import com.jme3.bullet.BulletAppState;

import com.jme3.bullet.PhysicsSpace;

import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;

import com.jme3.bullet.control.CharacterControl;

import com.jme3.bullet.control.RigidBodyControl;

import com.jme3.input.KeyInput;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.light.AmbientLight;

import com.jme3.light.DirectionalLight;

import com.jme3.material.MaterialList;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector3f;

import com.jme3.scene.Node;

import com.jme3.scene.plugins.ogre.OgreMeshKey;

import java.io.File;



public class TestCrouch extends SimpleApplication implements ActionListener {



private BulletAppState bulletAppState;

private Node gameLevel;

private boolean crouching = false;

//private PhysicsCharacter player;

private CharacterControl player;

private Vector3f walkDirection = new Vector3f();

private static boolean useHttp = false;

private boolean left=false,right=false,up=false,down=false;



private CapsuleCollisionShape normalColShape;

private CapsuleCollisionShape crouchColShape;



public static void main(String[] args) {

File file = new File(“quake3level.zip”);

if (!file.exists()) {

useHttp = true;

}

TestCrouch app = new TestCrouch();

app.start();

}



public void simpleInitApp() {

bulletAppState = new BulletAppState();

stateManager.attach(bulletAppState);

flyCam.setMoveSpeed(100);

setupKeys();



this.cam.setFrustumFar(2000);



DirectionalLight dl = new DirectionalLight();

dl.setColor(ColorRGBA.White.clone().multLocal(2));

dl.setDirection(new Vector3f(-1, -1, -1).normalize());

rootNode.addLight(dl);



AmbientLight am = new AmbientLight();

am.setColor(ColorRGBA.White.mult(2));

rootNode.addLight(am);



// load the level from zip or http zip

if (useHttp) {

assetManager.registerLocator(“http://jmonkeyengine.googlecode.com/files/quake3level.zip”, HttpZipLocator.class);

} else {

assetManager.registerLocator(“quake3level.zip”, ZipLocator.class);

}



// create the geometry and attach it

MaterialList matList = (MaterialList) assetManager.loadAsset(“Scene.material”);

OgreMeshKey key = new OgreMeshKey(“main.meshxml”, matList);

gameLevel = (Node) assetManager.loadAsset(key);

gameLevel.setLocalScale(0.1f);



// add a physics control, it will generate a MeshCollisionShape based on the gameLevel

gameLevel.addControl(new RigidBodyControl(0));



/player = new PhysicsCharacter(new SphereCollisionShape(5), .01f);

player.setJumpSpeed(20);

player.setFallSpeed(30);

player.setGravity(30);
/



normalColShape = new CapsuleCollisionShape(0.98f, 1.80f);

crouchColShape = new CapsuleCollisionShape(0.98f, 0.00f);

Node playerNode = new Node(“playerNode”);

rootNode.attachChild(playerNode);

player = new CharacterControl(normalColShape, .01f);

playerNode.addControl(player);

player.setFallSpeed(30);

player.setJumpSpeed(20);

player.setGravity(30);

player.setPhysicsLocation(new Vector3f(60, 10, -60));







rootNode.attachChild(gameLevel);



getPhysicsSpace().addAll(gameLevel);

getPhysicsSpace().add(player);

getPhysicsSpace().enableDebug(assetManager);

//cam.setLocation(new Vector3f(player.getPhysicsLocation().x-10, player.getPhysicsLocation().y, player.getPhysicsLocation().z));

}



private PhysicsSpace getPhysicsSpace(){

return bulletAppState.getPhysicsSpace();

}



@Override

public void simpleUpdate(float tpf) {

Vector3f camDir = cam.getDirection().clone().multLocal(0.6f);

Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f);

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());

player.setWalkDirection(walkDirection);

//cam.setLocation(new Vector3f(player.getPhysicsLocation().x, player.getPhysicsLocation().y, player.getPhysicsLocation().z));

cam.lookAt(player.getPhysicsLocation(), Vector3f.UNIT_Y);

}



private void setupKeys() {

inputManager.addMapping(“Lefts”, new KeyTrigger(KeyInput.KEY_A));

inputManager.addMapping(“Rights”, new KeyTrigger(KeyInput.KEY_D));

inputManager.addMapping(“Ups”, new KeyTrigger(KeyInput.KEY_W));

inputManager.addMapping(“Downs”, new KeyTrigger(KeyInput.KEY_S));

inputManager.addMapping(“Space”, new KeyTrigger(KeyInput.KEY_SPACE));

inputManager.addListener(this,“Lefts”);

inputManager.addListener(this,“Rights”);

inputManager.addListener(this,“Ups”);

inputManager.addListener(this,“Downs”);

inputManager.addListener(this,“Space”);

}



public void setCrouching(boolean crouching) {

if(this.crouching != crouching) {

this.crouching = crouching;

if(crouching) {

player.setCollisionShape(crouchColShape);

}

else {

player.setCollisionShape(normalColShape);

}

}

}



public void onAction(String binding, boolean value, float tpf) {



if (binding.equals(“Lefts”)) {

if(value)

left=true;

else

left=false;

} else if (binding.equals(“Rights”)) {

if(value)

right=true;

else

right=false;

} else if (binding.equals(“Ups”)) {

if(value)

up=true;

else

up=false;

} else if (binding.equals(“Downs”)) {

if(value)

down=true;

else

down=false;

} else if (binding.equals(“Space”)) {

setCrouching(value);

}

}

}

[/java]



Any ideas?

thanks for taking the time to make a testcase! so I took the time to go through it, and I have managed to make it work, by creating 2 charactercontrols at startup and then switching between them when pressing space. The code can be tidied up somewhat, but there you are:



[java]

package testClasses;



import com.jme3.app.SimpleApplication;

import com.jme3.asset.plugins.HttpZipLocator;

import com.jme3.asset.plugins.ZipLocator;

import com.jme3.bullet.BulletAppState;

import com.jme3.bullet.PhysicsSpace;

import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;

import com.jme3.bullet.control.CharacterControl;

import com.jme3.bullet.control.RigidBodyControl;

import com.jme3.input.KeyInput;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.light.AmbientLight;

import com.jme3.light.DirectionalLight;

import com.jme3.material.MaterialList;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector3f;

import com.jme3.scene.Node;

import com.jme3.scene.plugins.ogre.OgreMeshKey;

import java.io.File;



public class TestCrouch extends SimpleApplication implements ActionListener {



private BulletAppState bulletAppState;

private Node gameLevel;

private boolean crouching = false;

//private PhysicsCharacter player;

private CharacterControl player;

private CharacterControl player1;

private Vector3f walkDirection = new Vector3f();

private static boolean useHttp = false;

private boolean left = false, right = false, up = false, down = false;

private CapsuleCollisionShape normalColShape;

private CapsuleCollisionShape crouchColShape;

private Node playerNode;



public static void main(String[] args) {

File file = new File("quake3level.zip");

if (!file.exists()) {

useHttp = true;

}

TestCrouch app = new TestCrouch();

app.start();

}



public void simpleInitApp() {

bulletAppState = new BulletAppState();

stateManager.attach(bulletAppState);

flyCam.setMoveSpeed(100);

setupKeys();



this.cam.setFrustumFar(2000);



DirectionalLight dl = new DirectionalLight();

dl.setColor(ColorRGBA.White.clone().multLocal(2));

dl.setDirection(new Vector3f(-1, -1, -1).normalize());

rootNode.addLight(dl);



AmbientLight am = new AmbientLight();

am.setColor(ColorRGBA.White.mult(2));

rootNode.addLight(am);



// load the level from zip or http zip

if (useHttp) {

assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/quake3level.zip", HttpZipLocator.class);

} else {

assetManager.registerLocator("quake3level.zip", ZipLocator.class);

}



// create the geometry and attach it

MaterialList matList = (MaterialList) assetManager.loadAsset("Scene.material");

OgreMeshKey key = new OgreMeshKey("main.meshxml", matList);

gameLevel = (Node) assetManager.loadAsset(key);

gameLevel.setLocalScale(0.1f);



// add a physics control, it will generate a MeshCollisionShape based on the gameLevel

gameLevel.addControl(new RigidBodyControl(0));



/player = new PhysicsCharacter(new SphereCollisionShape(5), .01f);

player.setJumpSpeed(20);

player.setFallSpeed(30);

player.setGravity(30);
/



normalColShape = new CapsuleCollisionShape(0.98f, 1.80f);

crouchColShape = new CapsuleCollisionShape(0.98f, 0f);

playerNode = new Node("playerNode");

rootNode.attachChild(playerNode);

player = new CharacterControl(normalColShape, .1f);

playerNode.addControl(player);

player.setFallSpeed(30);

player.setJumpSpeed(20);

player.setGravity(30);

player.setPhysicsLocation(new Vector3f(60, 10, -60));



player1 = new CharacterControl(crouchColShape, .1f);

player1.setFallSpeed(30);

player1.setJumpSpeed(20);

player1.setGravity(30);



rootNode.attachChild(gameLevel);



getPhysicsSpace().addAll(gameLevel);

getPhysicsSpace().add(player);

getPhysicsSpace().enableDebug(assetManager);

//cam.setLocation(new Vector3f(player.getPhysicsLocation().x-10, player.getPhysicsLocation().y, player.getPhysicsLocation().z));

}



private PhysicsSpace getPhysicsSpace() {

return bulletAppState.getPhysicsSpace();

}



@Override

public void simpleUpdate(float tpf) {

Vector3f camDir = cam.getDirection().clone().multLocal(0.6f);

Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f);

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());

}

player.setWalkDirection(walkDirection);

//cam.setLocation(new Vector3f(player.getPhysicsLocation().x, player.getPhysicsLocation().y, player.getPhysicsLocation().z));

cam.lookAt(player.getPhysicsLocation(), Vector3f.UNIT_Y);

}



private void setupKeys() {

inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A));

inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));

inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W));

inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S));

inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));

inputManager.addListener(this, "Lefts");

inputManager.addListener(this, "Rights");

inputManager.addListener(this, "Ups");

inputManager.addListener(this, "Downs");

inputManager.addListener(this, "Space");

}



public void setCrouching(boolean crouching) {

if (this.crouching != crouching) {

this.crouching = crouching;



if (crouching) {



Vector3f location = player.getPhysicsLocation().clone();



getPhysicsSpace().remove(player);

playerNode.removeControl(player);



playerNode.addControl(player1);

getPhysicsSpace().add(player1);

player1.setPhysicsLocation(location);



} else {

Vector3f location = player1.getPhysicsLocation().clone();



getPhysicsSpace().remove(player1);

playerNode.removeControl(player1);



playerNode.addControl(player);

getPhysicsSpace().add(player);

player.setPhysicsLocation(location.addLocal(0, 1, 0));

}

}

}



public void onAction(String binding, boolean value, float tpf) {



if (binding.equals("Lefts")) {

if (value) {

left = true;

} else {

left = false;

}

} else if (binding.equals("Rights")) {

if (value) {

right = true;

} else {

right = false;

}

} else if (binding.equals("Ups")) {

if (value) {

up = true;

} else {

up = false;

}

} else if (binding.equals("Downs")) {

if (value) {

down = true;

} else {

down = false;

}

} else if (binding.equals("Space")) {

setCrouching(value);

}

}

}[/java]

1 Like

Hi wezrule!



I’m surprised to see there is no way to make it work with a single character control. I was hoping to keep this as light as possible. But whatever, I guess storing two character controls vs storing 2 collision shapes is not too different anyway. :slight_smile:



Thanks a lot for your help, I really appreciate it. :slight_smile:

no worries, yeh CharacterControl is somewhat of an exception some times. I also modified the setCrouching method a bit, to make it more clean.



[java] public void setCrouching(boolean crouching) {

if (this.crouching != crouching) {

this.crouching = crouching;



CharacterControl currentControl;

CharacterControl newControl;



if (crouching) {

currentControl = player;

newControl = player1;

} else {

currentControl = player1;

newControl = player;

}



Vector3f location = currentControl.getPhysicsLocation().clone();



getPhysicsSpace().remove(currentControl);

playerNode.removeControl(currentControl);



playerNode.addControl(newControl);

getPhysicsSpace().add(newControl);



if(currentControl == player) {

newControl.setPhysicsLocation(location);

} else {

newControl.setPhysicsLocation(location.addLocal(0, 1, 0));

}

}

}[/java]

Great! Thanks again. :slight_smile:

On stable beta I was able to reproduce this, is anyone running Nightly? Maybe this is a bug?

Do your own character if what the bullet character does isn’t enough for you, its definitely not made for replacing its collision shape in the middle of the simulation.