How to rotate BetterChararacterControl

Hi all. My name is PEBKAC and I have a problem :wink:

It’s just that I can’t seem to figure out how to rotate a BetterChararacterControl. I’m relatively new to JME. I’ve done all the basic tutorials, googled extensively on this problem and tried for example this (source https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:advanced:walking_character):


  Vector3f camDir = cam.getDirection().clone();
  Vector3f camLeft = cam.getLeft().clone();

What this does for me when I rotates the camera (I use a Camera Node), my character walk in the general direction I’m looking at, but doesn’t rotate. It doesn’t turn in the cam’s direction and walk straight in that direction, but makes a sideways shuffle…

I bet there’s a physics-based function to rotate (like warp() instead of move() for translations) but I really can’t find how…

It might help if you showed us the actual code.

I get that. However, I’m looking for a more general-purpose answer. I expect the answer to be: Check out this method… IMO, that should not be dependant on my code.

I don’t mind posting my code, just not yet.

@pebkac said: I get that. However, I'm looking for a more general-purpose answer. I expect the answer to be: Check out this method... IMO, that should not be dependant on my code.

I don’t mind posting my code, just not yet.

Ok, so that means now we play the game where I point you to something and then you say “I’ve already tried that”… because otherwise, you are simply using me to lookup javadoc:
http://hub.jmonkeyengine.org/javadoc/com/jme3/bullet/control/BetterCharacterControl.html#setViewDirection(com.jme3.math.Vector3f)

And I will offer this advice for all Java coders new and otherwise: if the javadoc is not at most one click away then you are “doing it wrong” and you might as well quit and find something else to do with your time.

Of course I’ve looked at the Javadocs. And it’s all the elaborate examples and clarifications that have even gotten me this far…

Here’s my code:


package nl.pebkac.inthegame;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.control.CameraControl.ControlDirection;
import com.jme3.scene.plugins.blender.BlenderModelLoader;
import com.jme3.scene.shape.Box;
import nl.pebkac.inthegame.terrain.SimpleTerrainGenerator;

/**
 * test
 * @author normenhansen
 */
public class MainHub extends SimpleApplication 
{
    public static void main(String[] args) 
	{		
        MainHub app = new MainHub();
        app.start();
    }
	
	// app vars
	private BulletAppState bas;
	private Node terrain = new Node("Terrain");
	private CameraNode camNode;	
	private Person person;	
	
	private boolean escapePressed = false;
	
    @Override
    public void simpleInitApp() 
	{	
		// Setting up defaults
		assetManager.registerLoader(BlenderModelLoader.class, ".blend");				
		
		// Setting up appstate / physics
		bas = new BulletAppState();		
		stateManager.attach(bas);		
		// Loading terrain
		SimpleTerrainGenerator.generateTerrain(assetManager, bas, terrain);
		DirectionalLight dl = new DirectionalLight();
		dl.setColor(ColorRGBA.White);
		dl.setDirection(Vector3f.UNIT_XYZ);
		rootNode.addLight(dl);
				
		// init person
		camNode = new CameraNode("Camera", cam);
		person = new Person(assetManager, bas, cam);		
		person.warp(new Vector3f(0, 10f, 0));
		
		// Bringing it all together
		rootNode.attachChild(terrain);
		rootNode.attachChild(person);
		
		//setting up camera		
		flyCam.setEnabled(false);
		//create the camera Node			
		camNode.setControlDir(ControlDirection.SpatialToCamera);		
		person.getPlayerNode().attachChild(camNode);
		camNode.setLocalTranslation(new Vector3f(-0.2f, 2.7f, -6));		
		camNode.lookAt(person.getLookAtOffset(), Vector3f.UNIT_Y);		
				
		this.bindKeysForState_INGAME();		
    }

    @Override
    public void simpleUpdate(float tpf) 
	{
		super.simpleUpdate(tpf);		
        person.update(tpf);		
		camNode.lookAt(person.getBirdieNode().getLocalTranslation(), Vector3f.UNIT_Y);
		
		if(escapePressed)	{	System.exit(0);	}
    }
		
	// input handling...
	private final String MV_LEFT = "move_left";
	private final String MV_RIGHT = "move_right";
	private final String MV_FORWARD = "move_forward";
	private final String MV_BACK = "move_back";	

	private final String RT_CAM_XAXIS = "rotate_x_axis";
	private final String RT_CAM_YAXIS = "rotate_y_axis";
	
	private final String RT_CAM_XAXIS_NEG = "rotate_x_axis_neg";
	private final String RT_CAM_YAXIS_NEG = "rotate_y_axis_neg";
	
	private ActionListener actionListener = new ActionListener() {
		public void onAction(String name, boolean isPressed, float tpf)
		{				
			if(name.equals("ESCAPE"))	{escapePressed = true;	}			
			else if(MV_BACK.equals(name))		{person.back = isPressed; }
			else if(MV_FORWARD.equals(name))	{person.forward = isPressed;}
			else if(MV_RIGHT.equals(name))		{person.right = isPressed;}
			else if(MV_LEFT.equals(name))		{person.left = isPressed;}						
		}
	};
		
	private AnalogListener analogListener = new AnalogListener() 
	{
		public void onAnalog(String name, float value, float tpf)
		{
			if(RT_CAM_XAXIS.equals(name))			{person.rotateLookat("X", value * tpf); }
			else if(RT_CAM_XAXIS_NEG.equals(name))	{person.rotateLookat("X", -value * tpf); }
			else if(RT_CAM_YAXIS.equals(name))		{person.rotateLookat("Y", value * tpf); }
			else if(RT_CAM_YAXIS_NEG.equals(name))	{person.rotateLookat("Y", -value * tpf); }
		}
	};
	
	private void bindKeysForState_INGAME()
	{
		inputManager.clearMappings();
		inputManager.addMapping(MV_LEFT, new KeyTrigger(KeyInput.KEY_LEFT));
		inputManager.addMapping(MV_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT));
		inputManager.addMapping(MV_FORWARD, new KeyTrigger(KeyInput.KEY_UP));
		inputManager.addMapping(MV_BACK, new KeyTrigger(KeyInput.KEY_DOWN));		
		inputManager.addMapping("ESCAPE", new KeyTrigger(KeyInput.KEY_ESCAPE));
		
		inputManager.addListener(this.actionListener, MV_LEFT, MV_RIGHT, MV_FORWARD, MV_BACK);		
		inputManager.addListener(this.actionListener, "ESCAPE");
		
		inputManager.addMapping(RT_CAM_XAXIS, new MouseAxisTrigger(MouseInput.AXIS_X, false));
		inputManager.addMapping(RT_CAM_XAXIS_NEG, new MouseAxisTrigger(MouseInput.AXIS_X, true));
		inputManager.addMapping(RT_CAM_YAXIS, new MouseAxisTrigger(MouseInput.AXIS_Y, false));
		inputManager.addMapping(RT_CAM_YAXIS_NEG, new MouseAxisTrigger(MouseInput.AXIS_Y, true));
		inputManager.addListener(this.analogListener, RT_CAM_XAXIS, RT_CAM_YAXIS, RT_CAM_XAXIS_NEG, RT_CAM_YAXIS_NEG);
	}
}

class Person extends Node
{
	private final String ANIM_WALK = "Walk";
	private final String ANIM_IDLE = "Idle";
	
	public enum Actions
	{
		NULL(-1)
		, IDLE(0)
		, WALKING(1)
		, RUNNING(2)
		;		
		private final int order;
		private Actions(int order)	{this.order = order;}		
		public int getOrder()		{ return this.order; }
	}
	
	private Node playerNode;
	private Node camBirdie = new Node("LookAtTheBirdie!");
	private float camBirdieRadius = 50f;
	private float camBirdieAngleHor = -90f;
	private float camBirdieAngleVer = 0.0f;
	private Actions actionLower = Actions.NULL;
	private Actions actionUpper = Actions.NULL;
	
	// movement helpers
	private Vector3f direction = new Vector3f();
	private Vector3f dir_forward = new Vector3f();
	private Vector3f dir_left = new Vector3f();
	private Camera cam;
	
	// mobvement controllers
	public boolean back = false;
	public boolean forward = false;
	public boolean right = false;
	public boolean left = false;
	
	// Santanimations
	private AnimChannel channel_top;
	private AnimChannel channel_bottom;
	private AnimControl animControl;
	private AnimEventListener animListener;
	
	private BetterCharacterControl playerControl;
	
	private float walkSpeed = 5f;
	public float getWalkSpeed()			{ return this.walkSpeed;	}
	public void setWalkSpeed(float v)	{ this.walkSpeed = v;		}
	
	private float turnSpeed = 180f * 1000f;	// degrees / second
	public float getTurnSpeed()			{ return this.turnSpeed;	}
	public void setTurnSpeed(float v)	{ this.turnSpeed = v;		}
	
	public Person(AssetManager assetManager, BulletAppState bas, Camera cam)
	{	
		// Creating a node and loading model
		this.cam = cam;
		playerNode = (Node) assetManager.loadModel("Models/kerstman/Cube.001.mesh.xml");		
		Material mat_default = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat_default.setTexture("ColorMap", assetManager.loadTexture("Textures/kerstman.png"));
		playerNode.setMaterial(mat_default);						

		// enabling physics
		playerControl = new BetterCharacterControl(1f, 2.1f, 80f);
		playerControl.setGravity(new Vector3f(0f, -10f, 0f));		
		playerNode.addControl(playerControl);
		bas.getPhysicsSpace().add(playerControl);
		
		// Setting up animations
		setupAnimChannels();		
		channel_top.setAnim(ANIM_IDLE);
		channel_top.setLoopMode(LoopMode.Loop);

		channel_bottom.setAnim(ANIM_IDLE);
		channel_bottom.setLoopMode(LoopMode.Loop);
		
		setBirdie();
		this.attachChild(playerNode);
		this.attachChild(camBirdie);		
		debugBirdie(assetManager);		
	}
	
	public Node getNode()			{		return this;	}	
	public Node getPlayerNode()		{		return this.playerNode;	}	
	public Node getBirdieNode()		{		return this.camBirdie;	}
	
	public Vector3f getLookAtOffset()
	{	
		return this.camBirdie.getLocalTranslation().subtract( this.playerNode.getLocalTranslation());
	}
	
	public void update(float milli)	
	{
		setBirdie();
		direction.set(0, 0, 0);
		
		Vector3f camDir = cam.getDirection().clone();
		Vector3f camLeft = cam.getLeft().clone();		
		dir_forward = camDir.normalize();
		dir_left = camLeft.normalize();
		
		if(back)		{ direction.addLocal(dir_forward.negate());}
		if(forward)		{ direction.addLocal(dir_forward);}		
		if(right)		{ direction.addLocal(dir_left.negate());}		
		if(left)		{ direction.addLocal(dir_left);}

		direction = direction.multLocal(5f);		
		playerControl.setWalkDirection(direction);		
	}
	
	// initialisers
	private void setupAnimChannels()
	{
		// inner helper listener for anim events
		this.animListener = new AnimEventListener() 
		{
			public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName)
			{
				if(channel == channel_top)
				{
					if(channel.getLoopMode() != LoopMode.Loop)
						switchAction(actionLower);
				}				
				if(channel == channel_bottom)
				{

					if(channel.getLoopMode() != LoopMode.Loop)
						switchAction(Actions.IDLE);
				}
			}

			public void onAnimChange(AnimControl control, AnimChannel channel, String animName)	{}
		};
		
		animControl = playerNode.getControl(AnimControl.class);
		animControl.addListener(this.animListener);		
		
		channel_top = animControl.createChannel();
		channel_top.addBone("neck");
		channel_top.addBone("chest");
		channel_top.addBone("shoulder.r");
		channel_top.addBone("shoulder.l");
		channel_top.addBone("arm.upper.r");
		channel_top.addBone("arm.upper.l");
		channel_top.addBone("arm.lower.r");
		channel_top.addBone("arm.lower.l");
		channel_top.addBone("abdomen");
		 		
		channel_bottom = animControl.createChannel();
		channel_bottom.addBone("root");		
		channel_bottom.addBone("leg.right");		
		channel_bottom.addBone("leg.left");
	}

	public void warp(Vector3f vector3f)	{	playerControl.warp(new Vector3f(0, 15f, 0));	}
	
	public void rotateLookat(String axis, float amtPerSec)
	{
		if(axis.equals("X"))	
		{
			camBirdieAngleHor += amtPerSec * turnSpeed; 		
			if(camBirdieAngleHor < 0f)	{ camBirdieAngleHor += 360f; }		
			if(camBirdieAngleHor > 360f){ camBirdieAngleHor -= 360f; }		
		}
		
		if(axis.equals("Y"))	
		{
			camBirdieAngleVer += amtPerSec * turnSpeed; 
			if(camBirdieAngleVer < 0f)	{ camBirdieAngleVer += 360f; }		
			if(camBirdieAngleVer > 360f){ camBirdieAngleVer -= 360f; }			
		}
		setBirdie();
	}
	
	public void switchAction(Actions newAction)
	{
		switchAction(newAction, false);
	}
	
	public void switchAction(Actions newAction, boolean override)
	{
		boolean upchanged = false;
		boolean lowchanged = false;
		if(actionUpper.getOrder() < newAction.getOrder() || override)
		{
			actionUpper = newAction;
			upchanged = true;
		}
		if(actionLower.getOrder() < newAction.getOrder() || override)
		{
			actionLower = newAction;
			lowchanged = true;
		}
		
		if(!upchanged && !lowchanged)
			return;
		
		if(upchanged)
		{
			switch(actionUpper)
			{
				default:
					channel_top.setAnim(ANIM_IDLE);
					channel_top.setLoopMode(LoopMode.Loop);
					break;
			}
		}
		
		if(lowchanged)
		{
			switch(actionLower)
			{
				default:
					channel_bottom.setAnim(ANIM_IDLE);
					channel_bottom.setLoopMode(LoopMode.Loop);
					break;
			}
		}
	}
	
	private void setBirdie()
	{		
		float my_x = playerNode.getLocalTranslation().x;
		float my_y = playerNode.getLocalTranslation().y;
		float my_z = playerNode.getLocalTranslation().z;
		
		float bird_x_a = (float) (my_x + (camBirdieRadius * Math.sin(FastMath.DEG_TO_RAD * camBirdieAngleHor)));
		float bird_x_b = (float) (my_x + (camBirdieRadius * Math.sin(FastMath.DEG_TO_RAD * camBirdieAngleVer)));
		float bird_y = (float) (my_y + (camBirdieRadius * Math.sin(FastMath.DEG_TO_RAD * camBirdieAngleVer)));
		float bird_z = (float) (my_z + (camBirdieRadius * Math.cos(FastMath.DEG_TO_RAD * camBirdieAngleHor)));

		Vector3f r = new Vector3f(bird_x_a, 0, bird_z).addLocal(0, bird_y, 0);
		camBirdie.setLocalTranslation(r);
		playerControl.setWalkDirection(camBirdie.getLocalTranslation());
	}
	
	private void debugBirdie(AssetManager assetManager)
	{
		Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Red);
        geom.setMaterial(mat);
        camBirdie.attachChild(geom);
	}
}

You can set the view direction of the character control with the “setViewDirection()” method just set the view direction to whatever you want to face.

Curiously enough, that seems to work now. In my Person.update() method, after getting the cam direction, I now use SetViewDirection and it works (somewhat…).

Thanks.

offtopic:

why do you using sines and cosines? you can avoid that

what the hell is this? :slight_smile: Vector3f r = new Vector3f(bird_x_a, 0, bird_z).addLocal(0, bird_y, 0);

what is that birdie stuff?

  • spatials and quaternions have lookAt method.
  • and for vector, you can easily create direction vector by subtracting one from another and then normalizing it