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.