RTS Camera control

Update:
For latest version please look at
https://subversion.assembla.com/svn/vmat/vmat/trunk/src/main/java/net/virtualmat/jme3/RtsCam.java
and/or posts at the end of this thread.


This is a class which allows you a rts-like camera control with keyboard. It assumes flat terrain, with up being y positive direction. Camera is looking at certain point from above. You can rotate camera around that point (Q and E keys), change tilt between directly from top to almost parallel to ground (R and F keys), zoom in and out (Z and X keys) and move the camera around changing the place you are looking at (WASD controls).

setCenter method allows you to set starting point. You may want to put y coordinate slightly above the ground level to avoid too close zoom.

You can control minimum/maximum tilt/zoom with setMinMaxValues method (don’t allow tilt to go to PI/2, as it reverses the camera at the very end). You can play with max speed/acceleration with setMaxSpeed methods. Certain degree of inertia is implemented, so you are not stopping/reversing immediately. Feel bit better that way, but can be annoying if you set acceleration time to be too long. In such case, it will probably make sense to deccelerate faster - should be simple extension.

You can add it to your scenegraph with code like

[java]
final RtsCam rtsCam = new RtsCam(cam, rootNode);
rtsCam.registerWithInput(inputManager);
rtsCam.setCenter(new Vector3f(20,0.5f,20));
[/java]

possibly detaching default fly cam with inputManager.removeListener(flyCam);

[java]
import java.io.IOException;

import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;

public class RtsCam implements Control, ActionListener {

public enum Degree {
SIDE,
FWD,
ROTATE,
TILT,
DISTANCE
}

private InputManager inputManager;
private final Camera cam;

private int[] direction = new int[5];
private float[] accelPeriod = new float[5];

private float[] maxSpeed = new float[5];
private float[] maxAccelPeriod = new float[5];
private float[] minValue = new float[5];
private float[] maxValue = new float[5];

private Vector3f position = new Vector3f();

private Vector3f center = new Vector3f();
private float tilt = (float)(Math.PI / 4);
private float rot = 0;
private float distance = 15;

private static final int SIDE = Degree.SIDE.ordinal();
private static final int FWD = Degree.FWD.ordinal();
private static final int ROTATE = Degree.ROTATE.ordinal();
private static final int TILT = Degree.TILT.ordinal();
private static final int DISTANCE = Degree.DISTANCE.ordinal();

public RtsCam(Camera cam, Spatial target) {
this.cam = cam;

setMinMaxValues(Degree.SIDE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
setMinMaxValues(Degree.FWD, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
setMinMaxValues(Degree.ROTATE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
setMinMaxValues(Degree.TILT, 0.2f, (float)(Math.PI / 2) - 0.001f);
setMinMaxValues(Degree.DISTANCE, 2, Float.POSITIVE_INFINITY);

setMaxSpeed(Degree.SIDE,10f,0.4f);
setMaxSpeed(Degree.FWD,10f,0.4f);
setMaxSpeed(Degree.ROTATE,2f,0.4f);
setMaxSpeed(Degree.TILT,1f,0.4f);
setMaxSpeed(Degree.DISTANCE,15f,0.4f);
target.addControl(this);
}

public void setMaxSpeed(Degree deg, float maxSpd, float accelTime) {
maxSpeed[deg.ordinal()] = maxSpd/accelTime;
maxAccelPeriod[deg.ordinal()] = accelTime;
}

public void registerWithInput(InputManager inputManager) {
this.inputManager = inputManager;

String[] mappings = new String[] { "+SIDE", "+FWD", "+ROTATE", "+TILT", "+DISTANCE",
"-SIDE", "-FWD", "-ROTATE", "-TILT", "-DISTANCE", };

inputManager.addMapping("-SIDE", new KeyTrigger(KeyInput.KEY_A));
inputManager.addMapping("+SIDE", new KeyTrigger(KeyInput.KEY_D));
inputManager.addMapping("+FWD", new KeyTrigger(KeyInput.KEY_S));
inputManager.addMapping("-FWD", new KeyTrigger(KeyInput.KEY_W));
inputManager.addMapping("+ROTATE", new KeyTrigger(KeyInput.KEY_Q));
inputManager.addMapping("-ROTATE", new KeyTrigger(KeyInput.KEY_E));
inputManager.addMapping("+TILT", new KeyTrigger(KeyInput.KEY_R));
inputManager.addMapping("-TILT", new KeyTrigger(KeyInput.KEY_F));
inputManager.addMapping("-DISTANCE", new KeyTrigger(KeyInput.KEY_Z));
inputManager.addMapping("+DISTANCE", new KeyTrigger(KeyInput.KEY_X));

inputManager.addListener(this, mappings);
inputManager.setCursorVisible(true);
}

public void write(JmeExporter ex) throws IOException {

}

public void read(JmeImporter im) throws IOException {

}

public Control cloneForSpatial(Spatial spatial) {
RtsCam other = new RtsCam(cam, spatial);
other.registerWithInput(inputManager);
return other;
}

public void setSpatial(Spatial spatial) {

}

public void setEnabled(boolean enabled) {

}

public boolean isEnabled() {

return true;
}

public void update(final float tpf) {

for (int i = 0; i < direction.length; i++) {
int dir = direction<i>;
switch (dir) {
case -1:
accelPeriod<i> = clamp(-maxAccelPeriod<i>,accelPeriod<i>-tpf,accelPeriod<i>);
break;
case 0:
if (accelPeriod<i> != 0) {
double oldSpeed = accelPeriod<i>;
if (accelPeriod<i> > 0) {
accelPeriod<i> -= tpf;
} else {
accelPeriod<i> += tpf;
}
if (oldSpeed * accelPeriod<i> < 0) {
accelPeriod<i> = 0;
}
}
break;
case 1:
accelPeriod<i> = clamp(accelPeriod<i>,accelPeriod<i>+tpf,maxAccelPeriod<i>);
break;
}

}

distance += maxSpeed[DISTANCE] * accelPeriod[DISTANCE] * tpf;
tilt += maxSpeed[TILT] * accelPeriod[TILT] * tpf;
rot += maxSpeed[ROTATE] * accelPeriod[ROTATE] * tpf;

distance = clamp(minValue[DISTANCE],distance,maxValue[DISTANCE]);
rot = clamp(minValue[ROTATE],rot,maxValue[ROTATE]);
tilt = clamp(minValue[TILT],tilt,maxValue[TILT]);

double offX = maxSpeed[SIDE] * accelPeriod[SIDE] * tpf;
double offZ = maxSpeed[FWD] * accelPeriod[FWD] * tpf;

center.x += offX * Math.cos(-rot) + offZ * Math.sin(rot);
center.z += offX * Math.sin(-rot) + offZ * Math.cos(rot);

position.x = center.x + (float)(distance * Math.cos(tilt) * Math.sin(rot));
position.y = center.y + (float)(distance * Math.sin(tilt));
position.z = center.z + (float)(distance * Math.cos(tilt) * Math.cos(rot));

cam.setLocation(position);
cam.lookAt(center, new Vector3f(0,1,0));

}

private static float clamp(float min, float value, float max) {
if ( value < min ) {
return min;
} else if ( value > max ) {
return max;
} else {
return value;
}
}

public float getMaxSpeed(Degree dg) {
return maxSpeed[dg.ordinal()];
}

public float getMinValue(Degree dg) {
return minValue[dg.ordinal()];
}

public float getMaxValue(Degree dg) {
return maxValue[dg.ordinal()];
}

// SIDE and FWD min/max values are ignored
public void setMinMaxValues(Degree dg, float min, float max) {
minValue[dg.ordinal()] = min;
maxValue[dg.ordinal()] = max;
}

public Vector3f getPosition() {
return position;
}

public void setCenter(Vector3f center) {
this.center.set(center);
}

public void render(RenderManager rm, ViewPort vp) {

}

public void onAction(String name, boolean isPressed, float tpf) {
int press = isPressed ? 1 : 0;

char sign = name.charAt(0);
if ( sign == ‘-’) {
press = -press;
} else if (sign != ‘+’) {
return;
}

Degree deg = Degree.valueOf(name.substring(1));
direction[deg.ordinal()] = press;
}
}

[/java]</i></i></i></i></i></i></i></i></i></i></i></i></i></i></i></i>

19 Likes

I just tried this and it works great. Thanks!


  • Damyon

thanks for contribute your code! :smiley:

Can I use this in my game?

Sure, I guess thats why abies posted it here :wink:

Can anyone put a simple example using this code and the deafult models?

Plz.

oO its shown the first lines of the post:

[java]final RtsCam rtsCam = new RtsCam(cam, rootNode);

rtsCam.registerWithInput(inputManager);

rtsCam.setCenter(new Vector3f(20,0.5f,20));[/java]

@normen I was asking about something like that, but I already did by myself.

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    /

    package mygame;



    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.input.KeyInput;

    import com.jme3.input.controls.ActionListener;

    import com.jme3.input.controls.KeyTrigger;

    import com.jme3.light.DirectionalLight;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Node;



    /
    * Sample 7 - how to load an OgreXML model and play an animation,
  • using channels, a controller, and an AnimEventListener. /

    public class RtsTest extends SimpleApplication

    implements AnimEventListener {

    private AnimChannel channel;

    private AnimControl control;

    Node player;

    public static void main(String[] args) {

    RtsTest app = new RtsTest();

    app.start();

    }



    @Override

    public void simpleInitApp() {

    viewPort.setBackgroundColor(ColorRGBA.LightGray);

    initKeys();

    DirectionalLight dl = new DirectionalLight();

    dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());

    rootNode.addLight(dl);

    player = (Node) assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");

    player.setLocalScale(new Vector3f(.05f,.05f,.05f));

    rootNode.attachChild(player);

    control = player.getControl(AnimControl.class);

    control.addListener(this);

    channel = control.createChannel();

    channel.setAnim("Stealth");



    final RtsCam rtsCam = new RtsCam(cam, rootNode);

    rtsCam.registerWithInput(inputManager);

    rtsCam.setCenter(new Vector3f(20,20,20));



    }



    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {

    if (animName.equals("Walk")) {

    channel.setAnim("Stealth", 0.50f);

    channel.setLoopMode(LoopMode.DontLoop);

    channel.setSpeed(1f);

    }

    }



    public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {

    // unused

    }



    /
    * Custom Keybinding: Map named actions to inputs. */

    private void initKeys() {

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

    inputManager.addListener(actionListener, "Walk");

    }

    private ActionListener actionListener = new ActionListener() {

    public void onAction(String name, boolean keyPressed, float tpf) {

    if (name.equals("Walk") && !keyPressed) {

    if (!channel.getAnimationName().equals("Walk")) {

    channel.setAnim("Walk", 0.50f);

    channel.setLoopMode(LoopMode.Loop);

    }

    }

    }

    };

    }[/java]

I almost forgot thanks a lot @abies you’re amazing and your code too!

Any way to make the camera follow a terrain ? i.e always 10 units above the ground, but still able to move around using wasd and tilting etc.

@asser-fahrenholz said:
Any way to make the camera follow a terrain ? i.e always 10 units above the ground, but still able to move around using wasd and tilting etc.

do a raycast to check where the terrain is and set the location
@normen said:
do a raycast to check where the terrain is and set the location


I did a check on the height of the terrain at the given point, adding it to Y of the position-vector, it ended up screwing the camera control entirely.
@asser-fahrenholz said:
I did a check on the height of the terrain at the given point, adding it to Y of the position-vector, it ended up screwing the camera control entirely.

Right, anyway that would be the way. I also suggest doing it in your own CamControl.

Thanks :slight_smile: got it working now. Thanks to the Abies as well.

@asser-fahrenholz said:
Thanks :) got it working now. Thanks to the Abies as well.


would you mind sharing the code how you managed to make the camera follow the terrain? I'm learning JME and trying to do that, but it may be too soon for me to figure it out on my own, as I'm struggling a bit..

You can either do a physics ray cast or a normal ray collision.



You find where it intersects the terrain, and then get the distance from that first collision and then move the camera accordingly.



Ray collisions can be found here:

https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:beginner:hello_picking?s[]=collidewith

hey there, I stumbled upon this and had to test it right away - great piece of work! :slight_smile:



However I tried to change the key bindings of (+/-)DISTANCE to MouseWheel.

Now what happens is when I scroll either up or down it does “move” in the right direction, however it doesn’t stop (until max/min is met) even though I only scrolled a little bit. However I would like to have steps / free choice of when to stop (by not scrolling anymore). Any idea what’s causing it? How to “fix” it?



Cheers,



netsky

This is because there is no “stopped scrolling” event passed to the inputManager (i think). So what you can do if after they are called in the update loop, you can set them to 0. Try this (new code at the bottom):



[java] public void update(final float tpf) {



for (int i = 0; i < direction.length; i++) {

int dir = direction;

switch (dir) {

case -1:

accelPeriod = clamp(-maxAccelPeriod, accelPeriod - tpf, accelPeriod);

break;

case 0:

if (accelPeriod != 0) {

double oldSpeed = accelPeriod;

if (accelPeriod > 0) {

accelPeriod -= tpf;

} else {

accelPeriod += tpf;

}

if (oldSpeed * accelPeriod < 0) {

accelPeriod = 0;

}

}

break;

case 1:

accelPeriod = clamp(accelPeriod, accelPeriod + tpf, maxAccelPeriod);

break;

}



// New code

if (i == DISTANCE) {

direction[DISTANCE] = 0;

}



}[/java]

2 Likes

indeed! works perfectly, now all I need to do is adjust distance “float maxSpd, float accelTime”



thanks a lot!

<cite>@teosk said:</cite> <br /> <br /> would you mind sharing the code how you managed to make the camera follow the terrain? I'm learning JME and trying to do that, but it may be too soon for me to figure it out on my own, as I'm struggling a bit..

Sorry I never got back to you. I’ll see if I can find the code - otherwise i’ll have to re-create it and then i’ll share it.