Hi
As requested in
Sharing the simple virtual joystick I have created to control player movent.
package game;
import app.view.PlayerMovementState;
import com.dalwiestudio.jmeutils.gui.ScreenResizeListener;
import com.dalwiestudio.jmeutils.gui.WindowManagerState;
import com.dalwiestudio.rpg.scene.view.SceneViewState;
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Texture;
import com.rvandoosselaer.jmeutils.ApplicationGlobals;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.component.QuadBackgroundComponent;
import com.simsilica.lemur.core.GuiMaterial;
import com.simsilica.lemur.event.*;
/**
* @author Ali-RS
*/
public class VirtualJoystickState extends BaseAppState {
private Node panel;
private Node pointer;
private Geometry blocker;
private TerrainQuad terrain;
private PlayerMovementState movementState;
private final ColorRGBA defaultBackgroundColor = new ColorRGBA(0, 0, 0, 0);
private final DragListener dragListener = new DragListener();
private final ResizeListener resizeListener = new ResizeListener();
private final Vector3f vTemp = new Vector3f();
public VirtualJoystickState() {
super("VirtualJoystickState");
}
/**
* Calculates that maximum Z value given the current contents of
* the GUI node.
*/
protected float getMaxGuiZ() {
BoundingVolume bv = ApplicationGlobals.getInstance().getGuiNode().getWorldBound();
return getMaxZ(bv);
}
protected float getMaxZ( BoundingVolume bv ) {
if( bv instanceof BoundingBox) {
BoundingBox bb = (BoundingBox)bv;
return bb.getCenter().z + bb.getZExtent();
} else if( bv instanceof BoundingSphere) {
BoundingSphere bs = (BoundingSphere)bv;
return bs.getCenter().z + bs.getRadius();
} else if( bv == null ) {
// Apparently this can happen for empty nodes...
return 0;
}
Vector3f offset = bv.getCenter().add(0, 0, 1000);
return offset.z - bv.distanceTo(offset);
}
protected Geometry createBlocker( float z, ColorRGBA backgroundColor ) {
Camera cam = getApplication().getCamera();
Node guiNode = ApplicationGlobals.getInstance().getGuiNode();
// Get the inverse scale of whatever the current guiNode is so that
// we can find a proper screen size
float width = cam.getWidth() / guiNode.getLocalScale().x;
float height = cam.getHeight() / guiNode.getLocalScale().y;
Quad quad = new Quad(width, height);
Geometry result = new Geometry("blocker", quad);
GuiMaterial guiMat = createBlockerMaterial(backgroundColor);
result.setMaterial(guiMat.getMaterial());
//result.setQueueBucket(Bucket.Transparent); // no, it goes in the gui bucket.
result.setLocalTranslation(0, 0, z);
return result;
}
protected GuiMaterial createBlockerMaterial( ColorRGBA color ) {
GuiMaterial result = GuiGlobals.getInstance().createMaterial(color, false);
Material mat = result.getMaterial();
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
return result;
}
@Override
protected void initialize(Application app) {
terrain = getState(SceneViewState.class, true).getTerrain();
movementState = getState(PlayerMovementState.class, true);
panel = new Node();
Label joystickLabel = new Label("");
Texture texture = GuiGlobals.getInstance().loadTexture("Interface/Icons/Gui/play_postion_bg.png", false, false);
joystickLabel.setBackground(new QuadBackgroundComponent(texture, 60, 50));
panel.attachChild(joystickLabel);
Vector3f halfSize = joystickLabel.getPreferredSize().mult(0.5f);
joystickLabel.move(-halfSize.x, halfSize.y, 0);
pointer = new Node();
Label pointerLabel = new Label("");
texture = GuiGlobals.getInstance().loadTexture("Interface/Icons/Gui/play_postion_point.png", false, false);
pointerLabel.setBackground(new QuadBackgroundComponent(texture, 25, 15));
pointer.attachChild(pointerLabel);
halfSize = pointerLabel.getPreferredSize().mult(0.5f);
pointerLabel.move(-halfSize.x, halfSize.y, 0);
panel.attachChild(pointer);
float zBase = getMaxGuiZ() + 100;
this.blocker = createBlocker(zBase, defaultBackgroundColor);
MouseEventControl.addListenersToSpatial(blocker, new BlockerListener());
getState(WindowManagerState.class).addScreenResizeListener(resizeListener);
}
@Override
protected void cleanup(Application app) {
getState(WindowManagerState.class).removeScreenResizeListener(resizeListener);
terrain = null;
movementState = null;
panel = null;
pointer = null;
}
@Override
protected void onEnable() {
MouseEventControl.addListenersToSpatial(terrain, dragListener);
}
@Override
protected void onDisable() {
if (isAttached()) {
detach();
}
MouseEventControl.removeListenersFromSpatial(terrain, dragListener);
}
@Override
public void update(float tpf) {
if (isAttached()) {
Vector2f cursorPosition = getApplication().getInputManager().getCursorPosition();
vTemp.set(cursorPosition.x, cursorPosition.y, 0);
panel.worldToLocal(vTemp, vTemp);
if (vTemp.lengthSquared() > 3500) {
vTemp.normalizeLocal().multLocal(59.1607f);
}
pointer.setLocalTranslation(vTemp);
vTemp.set(vTemp.x, 0, -vTemp.y).normalizeLocal();
movementState.setMoveDirection(vTemp);
}
}
protected boolean isAttached() {
return panel.getParent() != null;
}
protected void attach(Vector2f cursorPosition) {
if (!isEnabled()) {
return;
}
panel.setLocalTranslation(cursorPosition.x, cursorPosition.y, 0);
Node guiNode = ApplicationGlobals.getInstance().getGuiNode();
guiNode.attachChild(panel);
guiNode.attachChild(blocker);
}
protected void detach() {
if (!isAttached()) {
return;
}
panel.removeFromParent();
blocker.removeFromParent();
movementState.setMoveDirection(Vector3f.ZERO);
}
private class DragListener extends DefaultMouseListener {
private float xDown;
private float yDown;
private boolean pressed;
@Override
public void mouseButtonEvent(MouseButtonEvent event, Spatial target, Spatial capture) {
pressed = event.isPressed();
if( pressed ) {
xDown = event.getX();
yDown = event.getY();
}
}
@Override
public void mouseMoved(MouseMotionEvent event, Spatial target, Spatial capture) {
if(pressed && !isAttached()) {
float x = event.getX();
float y = event.getY();
if( Math.abs(x-xDown) > 3 || Math.abs(y-yDown) > 3 ) {
attach(new Vector2f(xDown, yDown));
}
}
}
}
private class BlockerListener implements MouseListener {
public BlockerListener( ) {
}
@Override
public void mouseButtonEvent(MouseButtonEvent event, Spatial target, Spatial capture) {
event.setConsumed();
if (!event.isPressed()) {
detach();
}
}
@Override
public void mouseEntered( MouseMotionEvent event, Spatial target, Spatial capture ) {
event.setConsumed();
}
@Override
public void mouseExited( MouseMotionEvent event, Spatial target, Spatial capture ) {
event.setConsumed();
}
@Override
public void mouseMoved( MouseMotionEvent event, Spatial target, Spatial capture ) {
event.setConsumed();
}
}
private class ResizeListener implements ScreenResizeListener {
@Override
public void resize(int width, int height) {
Node guiNode = ApplicationGlobals.getInstance().getGuiNode();
// Get the inverse scale of whatever the current guiNode is so that
// we can find a proper screen size
float w = width / guiNode.getLocalScale().x;
float h = height / guiNode.getLocalScale().y;
Quad quad = (Quad) blocker.getMesh();
quad.updateGeometry(w, h);
quad.clearCollisionData();
blocker.updateModelBound();
}
}
}
It is not documented () so below I will explain how it works. Please ask here if you have a question.
HOW IT WORKS
I added a drag listener to terrain and when a drag happens I display the joystick panel, also I am adding a full-screen transparent blocker quad to the screen when showing the joystick. Blocker is used to consume all mouse events while dragging the joystick and also for detecting the mouse release and thus detaching the joystick. (I have copy-pasted blocker code from Lemur PopupState).
And finally, in the update()
function I am calculating the walk direction and sending it to “PlayerMovementState” to do whatever he wants with that!
Also, the “ResizeListener” is to listen for windows resize and update blocker quad size to fit the screen size. That is only needed for desktop and you can rip this part out for Android as the game runs in fullscreen mode on Android and I believe window size can not be changed after the game has run.
But in case you are curious how I am listening to window resize, In the “WindowManagerState” I am adding an empty SceneProcessor to GUI viewport, and when reshape()
method is called I am notifying all the registered ScreenResizeListener’s .
/**
* @author Ali-RS
*/
@FunctionalInterface
public interface ScreenResizeListener {
public void resize(int width, int height);
}
A demo video:
Hope you find it useful!
Edit:
Sorry, forgot to mention I am not able to share the textures here because of the license, so you need to replace them with your own.
Kind Regards