Alright, here are the most relevant files.
First is my player character control:
package com.mygame;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import java.util.EnumMap;
/**
* Class that encapsulates the movement functions of the player character on the overworld.
* @author cameron
*/
public class OverworldCharacter extends CharacterControl implements ActionListener{
private EnumMap<Control, Boolean> controls = new EnumMap<>(Control.class);
private Vector3f walkDirection = Vector3f.ZERO.clone();
private Vector3f lastWalkDirection;
private final float ACCEL = 0.273f; //arbitrary but it works
private Camera cam;
//movement values are arbitrary, can be adjusted
private final float walkSpeed = 10.0f;
private final float runMultiplier = 2.5f;
//private final Vector3f forecast = Vector3f.UNIT_X.clone(); //will be used to check for interactable objects
/**
* Basic constructor, sets the player model
* @param playerModel
*/
public OverworldCharacter(Spatial playerModel){
super (new CapsuleCollisionShape(1.0f, 4.5f), 5);
this.setSpatial(playerModel);
}
/**
* Adds listeners for the controls that matter to the OverworldCharacter controller
* (i.e. movement and interacting with objects (not yet implemented))
* @param im
*/
public void addInputListeners(InputManager im){
im.addListener(this, Control.FORWARD.name());
im.addListener(this, Control.BACKWARD.name());
im.addListener(this, Control.STRAFE_L.name());
im.addListener(this, Control.STRAFE_R.name());
im.addListener(this, Control.INTERACT.name());
im.addListener(this, Control.RUN.name());
controls.put(Control.FORWARD, false);
controls.put(Control.BACKWARD, false);
controls.put(Control.STRAFE_L, false);
controls.put(Control.STRAFE_R, false);
controls.put(Control.INTERACT, false);
controls.put(Control.RUN, false);
}
/**
* Marks that a control was pressed or released.
* @param name - name of control, corresponds with "Control" enum (see addInputListeners for checked values)
* @param pressed - true if pressed, false otherwise
* @param tpf - unused but needed to match method signature
*/
@Override
public void onAction(String name, boolean pressed, float tpf) {
controls.put(Control.valueOf(name), pressed);
System.out.println("Control " + name + "set to " + pressed);
}
/**
* Sets the camera for easy access to calculate movement direction
* @param cam
*/
public void setCamera(Camera cam){
this.cam = cam;
}
/**
* LERP interpolation for turning towards the new movement angle;
* uses a tertiary point if the player is turning ~180 degrees
* @param newFaceAngle
*/
private void turnToward(Vector3f newFaceAngle){
//normalize the view directors
Vector3f faceAngle = this.getViewDirection(null).normalize();
Vector3f newFace = newFaceAngle.normalize();
if(faceAngle.negate().subtractLocal(newFace).length() < 0.1){ //estimate 180 degree turnaround
//Tertiary point necessary, use 2d normal of the current facing vector
Vector3f normal = new Vector3f(-1 * faceAngle.z, 0, faceAngle.x);
newFace = FastMath.interpolateLinear(0.125f, FastMath.interpolateLinear(0.125f, faceAngle, normal), newFace);
}
else //no tertiary point needed, simply interpolate
newFace = FastMath.interpolateLinear(0.25f, faceAngle, newFace);
//Set view direction
setViewDirection(newFace);
}
/**
* Update method that does the movement relative to the camera.
* /Should/ be called every frame, but is not unless manually called from OverworldAppState
* @param tpf - time per frame
*/
@Override
public void update(float tpf){
//System.out.println("OverworldCharacter updated");
lastWalkDirection = walkDirection.clone();
//I use a member vector to avoid creating new vectors every frame, but I'm not sure if that 'optimization' is worth the effort
walkDirection.set(0,0,0);
//Default vectors for camera adjustment, if camera is not set then player will move based on initial rotation
Vector3f camAdjust_F = Vector3f.UNIT_Z;
Vector3f camAdjust_L = Vector3f.UNIT_X;
//If camera is set, get the camera's view vectors to adjust the player movement
if (cam != null){
Quaternion cameraRot = cam.getRotation();
camAdjust_F = cameraRot.mult(Vector3f.UNIT_Z).multLocal(1,0,1);
camAdjust_L = cameraRot.mult(Vector3f.UNIT_X);
}
//Not super proud of this, but it works. No else because if player presses forward and backward at once then they will negate and do nothing rather than one having precedence.
if(controls.get(Control.FORWARD))
walkDirection.addLocal(camAdjust_F);
if(controls.get(Control.BACKWARD))
walkDirection.addLocal(camAdjust_F.negateLocal());
if(controls.get(Control.STRAFE_L))
walkDirection.addLocal(camAdjust_L);
if(controls.get(Control.STRAFE_R))
walkDirection.addLocal(camAdjust_L.negate());
//Set y value of walk direction to 0 in case of rounding errors from above then normalize.
walkDirection.setY(0.0f);
walkDirection.normalizeLocal(); // to prevent the "fast diagonal" problem
//Direction has been calculated and normalized.
//If walking, turn toward the direction of the walk.
if(! walkDirection.equals(Vector3f.ZERO)){
turnToward(walkDirection);
}
//multiply the walking speed, and multiply the running multiplier if run is held
walkDirection.multLocal(walkSpeed);
if(controls.get(Control.RUN))
walkDirection.multLocal(runMultiplier);
//TODO animation here
//LERP to get a more smooth transition for walking speed
walkDirection = FastMath.interpolateLinear(ACCEL, lastWalkDirection, walkDirection);
this.setWalkDirection(walkDirection);
//Finally, call the superclass update to actually implement the walk, do physics, etc.
super.update(tpf);
}
}
Here’s the appstate for the overworld, as well:
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.input.ChaseCamera;
import com.jme3.input.InputManager;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
/**
* @author cameron
*/
public class OverworldAppState extends BaseAppState {
private final OverworldMap map;
private volatile SimpleApplication sapp;
private volatile BulletAppState bas;
private Node rootNode;
private ChaseCamera chaseCam;
DirectionalLight sun;
private Node player;
private OverworldCharacter playerCharacter;
private Spatial characterModel;
/**
* Simple constructor, instatiates map object which contains data incl spatial and encounter tables
* @param map
*/
public OverworldAppState(OverworldMap map){
this.map = map;
}
/**
* AppState initialize function; grabs map and player data and constructs player movement system, camera,
* @param app
*/
@Override
protected void initialize(Application app) {
this.sapp = (SimpleApplication)app;
rootNode = this.sapp.getRootNode();
bas = sapp.getStateManager().getState(BulletAppState.class);
PhysicsSpace ps = bas.getPhysicsSpace();
rootNode.attachChild(map.getMapScene()); //map.getMapScene() returns a spatial node tree with attached Minie rigidBodyControls
//ps.addAll(map.getMapScene()); //FIXME this is broken for now
//construct player spatial and offset vertically
player = new Node(); //created to offset the collision from the model
characterModel = sapp.getAssetManager().loadModel("Models/robody.j3o");
player.attachChild(characterModel);
characterModel.setLocalTranslation(0, -4.0f, 0); //FIXME make values non-arbitrary
//create control and attach to player, add input, attach construct to root node
playerCharacter = new OverworldCharacter((Spatial)player);
playerCharacter.setPhysicsLocation(map.getSpawnPoint());
playerCharacter.setPhysicsSpace(ps);
playerCharacter.addInputListeners(sapp.getInputManager());
rootNode.attachChild(player);
//boilerplate grabbed from previous project - will probably replace
chaseCam = new ChaseCamera(sapp.getCamera(), characterModel, sapp.getInputManager());
chaseCam.setMinDistance(20.0f);
chaseCam.setMaxDistance(40.0f);
chaseCam.setDefaultDistance(30.0f);
chaseCam.setDefaultVerticalRotation(FastMath.PI / 6);
chaseCam.setMinVerticalRotation(FastMath.PI / 24);
chaseCam.setDownRotateOnCloseViewOnly(false);
chaseCam.setMaxVerticalRotation((2 * FastMath.PI) / 6);
chaseCam.setLookAtOffset(new Vector3f(0, 4, 0));
chaseCam.setDragToRotate(false);
chaseCam.setDefaultHorizontalRotation(FastMath.PI / 2 * 3); //start behind the player instead of in front
playerCharacter.setCamera(sapp.getCamera());
//palette sun code so we can actually see what we're doing
/* A white, directional light source */
sun = new DirectionalLight();
sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
sun.setColor(ColorRGBA.White);
rootNode.addLight(sun);
}
/**
* AppState cleanup method, detaches map and player and removes input listeners
* @param app
*/
@Override
protected void cleanup(Application app) {
InputManager im = sapp.getInputManager();
//remove spatials
rootNode.detachChild(map.getMapScene());
rootNode.detachChild(player);
//remove input listening
im.removeListener(playerCharacter);
chaseCam.cleanupWithInput(im); //feel like i'm missing something with the camera
//remove sun
rootNode.removeLight(sun);
}
//onEnable()/onDisable() can be used for managing things that should
//only exist while the state is enabled. Prime examples would be scene
//graph attachment or input listener attachment.
@Override
protected void onEnable() {
//As of yet unused, add functions for pausing here later
}
@Override
protected void onDisable() {
//Also as of yet unused, add functions for unpausing here later
}
@Override
public void update(float tpf) {
// uncomment to enable the player character's movement...?
//playerCharacter.update(tpf);
}
}
Finally, here’s my Main.java at the moment:
package com.mygame;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AppStateManager;
import com.jme3.bullet.BulletAppState;
import com.jme3.input.InputManager;
import com.jme3.input.Joystick;
import com.jme3.input.JoystickConnectionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.renderer.RenderManager;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.GuiGlobals;
/**
* Main class
* @author normenhansen <- shout outs for the gradle template
*/
public class Main extends SimpleApplication {
private AppStateManager asm;
private OverworldAppState overworldState;
private static AppSettings configureSettings(){
AppSettings settings = new AppSettings(true);
settings.put("Width", 1280);
settings.put("Height", 720);
settings.put("Title", "Untitled RPG");
settings.setVSync(true);
settings.setFrequency(60);
return settings;
}
private void configureInput(){
InputManager im = this.getInputManager();
im.addJoystickConnectionListener(new JoystickConnectionListener(){
@Override
public void onConnected(Joystick arg0) {
System.out.println("Joystick connected: " + arg0.getName());
}
@Override
public void onDisconnected(Joystick arg0) {
System.out.println("Joystick disconnected: " + arg0.getName());
}
});
for(Control c : Control.values()){
im.addMapping(c.name(), new KeyTrigger(c.getDefaultKey()));
}
//flyCam.setEnabled(false);
}
public static void main(String[] args) {
Main app = new Main();
app.setSettings(configureSettings());
app.showSettings = false;
app.start();
}
public Main(){
asm = this.getStateManager();
}
/**
* Encapsulate loading all of the game data (stored in .json files).
*/
private void initializeData(){
OverworldMap.loadMapData(this.getAssetManager());
}
/**
* Initialize the gui state; currently unused.
*/
private void initLemurGui(){
GuiGlobals.initialize(this);
//FIXME determine if a different gui style should be used or created
GuiGlobals.getInstance().getStyles().setDefaultStyle("glass");
}
@Override
public void simpleInitApp() {
initializeData();
configureInput();
//initLemurGui();
BulletAppState bas = new BulletAppState();
bas.setDebugEnabled(true);
stateManager.attach(bas);
OverworldMap mapdata = new OverworldMap("testisle", this.getAssetManager());
overworldState = new OverworldAppState(mapdata);
asm.attach(overworldState);
overworldState.setEnabled(true);
}
@Override
public void simpleUpdate(float tpf) {
//TODO: add update code
}
@Override
public void simpleRender(RenderManager rm) {
//TODO: add render code
}
}
It’s a lot of code to dump at once imo, but I tried to comment the important bits for clarity. A few changes might need to be made to directly load models rather than pull them from JSON files as I have, but I didn’t want to just post my entire project source code here at the moment. I can make those edits if someone requests that I do so.