Android virtual joystick

Hi, I want to add virtual joystick to capture movement of it, something like this maybe:

Can you give me some directions what should I read or maybe some sample code, thanks in advance :wink:

I think someone has used Lemur to create a virtual joystick like this. I donā€™t remember specifics but Lemur handles touch very well and would work fine for dragging the head of a joystick around.

ā€¦wish we had a ready-made Lemur example for it.

t0neg0d has a built in virtual joystick for Android.

It really wouldnt be that tough to do this yourself.

I made a virtual joystick for android (doesnā€™t require any GUI lib) and use it for my lil game.

It suits my needs, but no idea how good it is. It supports multi-touchā€¦ actually, you could have 2 of those (or buttons) being used at the same time. Iā€™ll post it if you donā€™t find better.

It only works with joystick base and stick images of same width and height. Itā€™s a 2D joystick, not a 3D one.
It allows setting a dead zone and an out of joystick image zone that still records input. I still need to do the coding of the case where the settings ask to keep the joystick where it was when fingers not on it anymoreā€¦ but you probably donā€™t need thatā€¦ the normal mode puts the joystick at the center in that case.

NB: I kinda only tested it for my images and my caseā€¦ I donā€™t think it would bug with other sizes etc, but havenā€™t tested. Basically, wasnā€™t thinking of releasing itā€¦ just use it, but it does work for me and the code quality is not too awful.

can you post some sample code

package com.cis.pipesandstuff.manictubes2.gizmo;

import com.cis.pipesandstuff.common.GeometryBatchFactoryUtil;
import com.cis.pipesandstuff.game.Game;
import com.jme3.input.event.TouchEvent;
import com.jme3.input.event.TouchEvent.Type;
import static com.jme3.math.FastMath.abs;
import com.jme3.math.Vector2f;
import com.jme3.ui.Picture;
import tonegod.gui.core.Screen;

/**
 *
 * @author jo
 */
public class JoystickGizmo extends Gizmo {
    private final Vector2f positionBackground;
    private Vector2f positionHandle;
    private final Vector2f dimensionBackground;
    private final String backgroundImg;
    private final String handleImg;
    private final float overhangs;//how much around the joystick background area is treated as a joystick touch too
    private final float minRatio;
    private boolean rememberAfterUp = false;
    private float nPoint, wPoint, ePoint, sPoint;
    private int joystickPointerId;
    private boolean joystickDown;
    private float tmpX, tmpY;
    private final Vector2f tmpTouchEvent = new Vector2f();
    private Vector2f tmpTranslationFromCenter = new Vector2f();
    private Vector2f tmpTranslatedHandlePosition = new Vector2f();
    private Vector2f fromPositionToMiddle = new Vector2f();
    private Vector2f positionCenterHandle = new Vector2f();
    private Picture handlePicture;
    private Picture backPicture;
    private float halfSize;
    
    private Vector2f lastVectorReturned;

    //currently expects handle size to be half background size
    //overhangs : area around the picture who's value will still be read
    //minRatio : ratio between translation and max translation under what a ZERO vector is returned
    public JoystickGizmo(Game game, Screen screen, Vector2f positionBackground,
            Vector2f dimensionBackground, String backgroundImg, String handleImg, float overhangs, float minRatio, boolean rememberAfterUp) {
        super(game, screen);
        this.game = game;
        this.screen = screen;
        this.positionBackground = positionBackground;
        this.dimensionBackground = dimensionBackground;
        this.backgroundImg = backgroundImg;
        this.handleImg = handleImg;
        this.overhangs = overhangs;
        this.minRatio = minRatio;
        this.rememberAfterUp = rememberAfterUp;
    }

    @Override
    public void initialize() {
        //calculate the cardinal extremities
        nPoint = positionBackground.y + dimensionBackground.x + overhangs;
        sPoint = positionBackground.y - overhangs;
        wPoint = positionBackground.x - overhangs;
        ePoint = positionBackground.x + dimensionBackground.x + overhangs;

        fromPositionToMiddle = dimensionBackground.mult(0.25f);
        positionHandle = positionBackground.add(fromPositionToMiddle);
        positionCenterHandle = positionHandle.add(fromPositionToMiddle);

        halfSize = dimensionBackground.x / 2f;

        joystickPointerId = -1;
        joystickDown = false;
        lastVectorReturned = Vector2f.ZERO;
    }

    private void display() {
        backPicture = new Picture("backPic");
        backPicture.setImage(game.getAssetManager(), backgroundImg, true);
        backPicture.setWidth(dimensionBackground.getX());
        backPicture.setHeight(dimensionBackground.getY());
        backPicture.setPosition(positionBackground.x, positionBackground.y);
        backPicture.setUserData(GeometryBatchFactoryUtil.FLAG_FOR_OPTIMIZE, "stuff");
        game.getGuiNode().attachChild(backPicture);

        handlePicture = new Picture("handlePic");
        handlePicture.setImage(game.getAssetManager(), handleImg, true);
        handlePicture.setWidth(dimensionBackground.getX() / 2f);
        handlePicture.setHeight(dimensionBackground.getY() / 2f);
        handlePicture.setPosition(positionHandle.x, positionHandle.y);
        game.getGuiNode().attachChild(handlePicture);
    }

    //returns false when not a joystick event
    //returns Vector2f.ZERO when jotsick in rest position (middle)
    //sets the lastVectorValue when should otherwise keeps previous value
    public boolean lookIntoIt(TouchEvent touchEvent) {
        if (!joystickDown) {
            if (touchEvent.getType() == Type.DOWN) {
                if (touchEvent.getY() < nPoint && touchEvent.getY() > sPoint && touchEvent.getX() > wPoint && touchEvent.getX() < ePoint) {
                    tmpTouchEvent.set(touchEvent.getX(), touchEvent.getY());
                    tmpTranslationFromCenter = tmpTouchEvent.subtract(positionCenterHandle);
                    tmpTranslatedHandlePosition = positionHandle.add(tmpTranslationFromCenter);

                    handlePicture.setPosition(tmpTranslatedHandlePosition.x, tmpTranslatedHandlePosition.y);
                    joystickPointerId = touchEvent.getPointerId();
                    joystickDown = true;

                    tmpTranslationFromCenter.x = tmpTranslationFromCenter.x / halfSize;
                    tmpTranslationFromCenter.y = tmpTranslationFromCenter.y / halfSize;

                    return applyMinRatioAndSetValues(tmpTranslationFromCenter);
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }

        if (touchEvent.getPointerId() != joystickPointerId) {
            return false;
        }
        
        switch (touchEvent.getType()) {
            case UP:
                if (rememberAfterUp) {
                    //same as MOVE - todo
                    lastVectorReturned = Vector2f.ZERO;
                    return true;
                    
                } else {
                    handlePicture.setPosition(positionHandle.x, positionHandle.y);
                    joystickDown = false;
                    joystickPointerId = - 1;
                    lastVectorReturned = Vector2f.ZERO;
                    return true;
                }
            case MOVE:
                tmpX = (touchEvent.getX() < wPoint) ? wPoint : touchEvent.getX();
                tmpX = (tmpX > ePoint) ? ePoint : tmpX;
                tmpY = (touchEvent.getY() > nPoint) ? nPoint : touchEvent.getY();
                tmpY = (tmpY < sPoint) ? sPoint : tmpY;
                tmpTouchEvent.set(tmpX, tmpY);

                tmpTranslationFromCenter = tmpTouchEvent.subtract(positionCenterHandle);
                tmpTranslatedHandlePosition = positionHandle.add(tmpTranslationFromCenter);

                handlePicture.setPosition(tmpTranslatedHandlePosition.x, tmpTranslatedHandlePosition.y);

                tmpTranslationFromCenter.x = tmpTranslationFromCenter.x / halfSize;
                tmpTranslationFromCenter.y = tmpTranslationFromCenter.y / halfSize;

                return applyMinRatioAndSetValues(tmpTranslationFromCenter);
        }
        return true;
    }

    private boolean applyMinRatioAndSetValues(Vector2f value) {
        lastVectorReturned = value;
        if (minRatio != 0) {
            if( abs(lastVectorReturned.x ) < minRatio) { lastVectorReturned.x = 0; }
            if( abs(lastVectorReturned.y ) < minRatio) { lastVectorReturned.y = 0; }
            if( value.x == 0f && value.y == 0f ){ lastVectorReturned = Vector2f.ZERO; }
        }
        return true;
    }

    public Vector2f getLastVectorValue(){
        return lastVectorReturned;
    }
    
    public void enable() {
        display();
    }

    public void disable() {
    }

    @Override
    public void clean() {
        game.getGuiNode().detachChild(backPicture);
        game.getGuiNode().detachChild(handlePicture);
    }

    @Override
    public void update(float tpf) {
    }
}

package com.cis.pipesandstuff.manictubes2.appstate;

import com.cis.pipesandstuff.game.Game;
import com.cis.pipesandstuff.manictubes2.gizmo.JoystickGizmo;
import com.jme3.app.Application;
import com.jme3.math.Vector2f;
import tonegod.gui.core.Screen;

/**
 *
 * @author jo
 */
public class JoystickAppState extends BaseAppState{
    private Game game;
    private Screen screen;
    private JoystickGizmo joystick;
    private static final float WIDTH = 150;
    private static final float HEIGHT = 150;
            
    public JoystickAppState(){
    }
    
    @Override
    protected void initialize(Application app) {
        this.game = (Game)app;
        this.screen = game.getGameManager().getScreen();
        
        joystick = new JoystickGizmo(game, 
                screen,
                new Vector2f(60f, 60f), 
                new Vector2f(WIDTH, HEIGHT), 
                "Textures/Gizmos/Joystick/joystickBackground.png", 
                "Textures/Gizmos/Joystick/joystickHandle.png",
                5, 0.50f, false);
        joystick.initialize();
//        enable();
    }

    @Override
    public void update( float tpf ) {
//        joystick.update();
    }
    
    @Override
    protected void cleanup(Application app) {
        joystick.clean();
    }

    @Override
    protected void enable() {
        joystick.enable();
    }

    @Override
    protected void disable() {
        joystick.disable();
    }

    public JoystickGizmo getJoystick() {
        return joystick;
    }
    
}
2 Likes

Gonna add some explanations hereā€¦ just wanted to get the main classes out of the way first.

Couple remarks first:

  • itā€™s linked to toneGodGUI here because my parent Gizmo class used for in-game GUI elements is based on TGGā€¦ but you are free to do it another wayā€¦ TGG is not used by the joystick.
  • my Game class is an abstract class that inherits from simpleApplication. You can use your ownā€¦ as I said, I didnā€™t think about releasing it, just use it myself ^^.
  • BaseAppState came from Lemur but might be in 3.1 nowā€¦ you can replace it by AppState.

How it works: when a touch event occurs, the joystick class calculates the current displacement of the joystick but doesnā€™t call anything.
The onTick method of the listener checks the previously calculated displacement and reacts to it. Why? Because android does not send events continuously when your finger doesnā€™t moveā€¦ but I wanted my vehicle to still get the acceleration etc x times per second.

Your listener should looks something like this:

public class xxxAndroidControlsListener implements xxxControlsListener, TouchListener, TickListener {

    private final Game game;
    private final JoystickGizmo joystickGizmo;
    
    private Vector2f lookedIntoItJoystickValue;
    
    public InputCarAndroidControlsListener(Game application, WorldPlayer worldPlayer) {
        this.game = application;

        joystickGizmo = application.getStateManager().getState(JoystickAppState.class).getJoystick();
        
        lookedIntoItJoystickValue = Vector2f.ZERO;
        isJoysTickEvent = false;
    }
    
    @Override
    public void onTick(float tpf){
        
        if(lookedIntoItJoystickValue == Vector2f.ZERO){
            //reset acceleration, yaw, etc
        } else {
            if(lookedIntoItJoystickValue.x == 0f){
                //reset yaw
            } else if(lookedIntoItJoystickValue.x > 0f){
                //steer left
            } else {
                //steer right
            }
            
            if(lookedIntoItJoystickValue.y == 0f){
                //reset acceleration
            } else if(lookedIntoItJoystickValue.y > 0f){
                //accelerate
            } else {
                //brake
            }
        }

    }
    
    private boolean tamponEventLookedIntoIt;
    
    private boolean isJoysTickEvent;
    
    @Override
    public void onTouch(String name, TouchEvent event, float tpf) {
        isJoysTickEvent = joystickGizmo.lookIntoIt(event);
        lookedIntoItJoystickValue = joystickGizmo.getLastVectorValue();
        
        if(!joysTickEvent){
          //buttons or other joystick reaction to would come here
        }
    }

    @Override
    public void setIsEnabledInputs(boolean enabled) {
        final InputManager inputManager = game.getInputManager();

        if (enabled) {
            ((ManicTubes2)game).setTickListener(this);
            inputManager.addMapping("Main_Touch_All", new TouchTrigger(TouchInput.ALL));
            inputManager.addListener(this, "Main_Touch_All");
        } else {
            ((ManicTubes2)game).setTickListener(null);
            inputManager.deleteMapping("Main_Touch_All");
        }
    }
}

ā€œ((ManicTubes2)game).setTickListener(this);ā€ this is to get a tick, but that tick probably should come from the joystickAppState instead.

Anyway, thatā€™s the gist of it.

Well, thatā€™s one guy Iā€™ll never move a finger for again.

Well thank you for posts but I do not quite understand the part with the listener, how to use it or what should I read about, which are the packages of the interfaces?

Listenerā€™s imports (ones not specific to my game):

import com.jme3.input.InputManager;
import com.jme3.input.TouchInput;
import com.jme3.input.controls.TouchListener;
import com.jme3.input.controls.TouchTrigger;
import com.jme3.input.event.TouchEvent;
import com.jme3.math.Vector2f;

TickListener interface:

/**
 *
 * @author jo
 */
public interface TickListener {
    public void onTick(float tpf);
}

How to use the listener:

  • You instantiate the listener. NB WorldPlayer is specific to my application.
    The listener is a class specific to your application, so itā€™s your job to feed it what it needsā€¦ itā€™s just there to suggest a way of writing it.
  • You also need to call the listenerā€™s setIsEnabledInputs(boolean) to have it listen or stop listening. You can ignore xxxControlsListener.

You have to feed the tick to your listener and code how it reacts to it.
I currently have ticks fed to this listener by calling the listenerā€™s setIsEnabledInputs() which registers the listener with the game class and get itā€™s tick events.

Game class tick stuff:

    @Override
    public void simpleUpdate(float tpf) {
        if(tickListener != null){
            tickListener.onTick(tpf);
        }
    }
    
    //set to null when done
    public void setTickListener(TickListener tickListener){
        this.tickListener = tickListener;
    }

I wonder why my appState isnā€™t feeding the ticks instead of the Gameā€¦ either bad design or it avoids a specific problemā€¦ I donā€™t remember :.

As far as I know, this is the only real virtual joystick for android jme. I havenā€™t looked at any implementations and so it is totally open source.
All I required for people to use/base a joystick on it, is a thanks which is why I went a lil berzerk ;).

No promises (Iā€™m a slacker), but Iā€™ll look at releasing it as a generic library on bitbucket with a small usage example in the coming days.

Ok, since it was probably cryptic like hell, I made:

  • a version of the joystick with no dependencies (apart from jme): Bitbucket

  • a small test application to show how it can be used: Bitbucket

3 Likes

what jme version you are using com.jme3.app.state.BaseAppState in not on my classpath i m using jme 3.1, i will try to replace it with AbstractAppState see if will start

Which 3.1? Ancient alpha 1? Or?

nah my bad the ide is:

Product Version: jMonkeyEngine SDK 3.0
Updates: Updates available
Java: 1.7.0_51; Java HotSpotā„¢ Server VM 24.51-b03
Runtime: Javaā„¢ SE Runtime Environment 1.7.0_51-b13
System: Linux version 3.16.0-49-generic running on i386; UTF-8; en_US (jmonkeyplatform)

I created the project with the wizard from jmonkey studio.

Maybe i m missing some jar from my lib? I will try add BaseAppState from sources in github. The class is supposed to be in jME3-core.jar in /com/jme3/app/state/ but its not thereā€¦

ā€¦yes, because you are running an ancient version of JME. My son was still in elementary school when that version came out.

1 Like

I managed to run it thank you loopies

Goodie. Tell me if you find bugs or such.