-4

I'm working in a spaceship first person view game. I have a joystick, and when i move the joystick i can move all the objects (asteroids) of the screen simulating that the spaceship is being moved with the joystick.

The game works fine, but now i have a problem. If you are pressing the joystick in the max left position and then you do ACTION_UP and then instantly ACTION_DOWN in the joystick again but in the max right position, the spaceship starts moving to the right at max speed. It is hard to explain it. For example, If you press the joystick in max left position the spaceship is moving -20px per frame to the left and if you press the joystick in the max right position, the spaceship moves to the right +20px per frame.

So, now, if i do a fast max left and max right touch on the joystick, the spaceship does this movement: -20....+20

It is not reallistic movement.

I want to get this movement: -20 -17 -14 -9 -5 0 +5 +9 +14 +17 +20.... I mean a more reallistic spaceship movement. But the problem is that i am not a math or physics expert, and i dont have any idea of how to get that kind of functionality in this joystick... any help will be very grateful.

Here you can find a demo project with the joystick: https://mega.co.nz/#!cp5FhYIT!dM88qx_xQdyhED9fX_4xeJ9ciQYJirUlNzEi-KOzU2k

This is the joystick code, i found it in google and works very well except for the non realistic movement that i described before:

public class Joystick extends View {
    public static final int INVALID_POINTER = -1;   

    private JoystickMovedListener moveListener;

    //# of pixels movement required between reporting to the listener
    private float moveResolution;

    //Max range of movement in user coordinate system
    private float movementRange;

    //Last touch point in view coordinates
    private int pointerId = INVALID_POINTER;
    private float touchX;
    private float touchY;
    private float touchXDelayedMovement;
    private float touchYDelayedMovement;

    //Handle center in view coordinates
    private float handleX; 
    private float handleY;

    //Last reported position in view coordinates (allows different reporting sensitivities)
    private float reportX; 
    private float reportY;

    //Center of the view in view coordinates
    private int cX;
    private int cY;

    //Size of the view in view coordinates
    private int dimX;
    private int dimY;

    private int innerPadding;
    private int bgRadius;
    private int handleRadius;
    private int movementRadius;
    private int handleInnerBoundaries;

    //Cartesian coordinates of last touch point - joystick center is (0,0)
    private int cartX;
    private int cartY;

    //User coordinates of last touch point
    private int userX;
    private int userY;

    //Offset co-ordinates (used when touch events are received from parent's coordinate origin)
    private int offsetX;
    private int offsetY;

    private Paint bgPaint;
    private Paint handlePaint;

    boolean disabled;

    Handler handler;
    Handler handlerDelayedMovement;

    public Joystick(Context context) {
        super(context);
        initJoystickView();
    }

    private void initJoystickView() {
        setFocusable(true);

        handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        handlePaint.setColor(Color.RED);
        handlePaint.setStrokeWidth(1);
        handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);

        bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bgPaint.setColor(Color.DKGRAY);
        bgPaint.setStrokeWidth(1);
        bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);      

        this.moveResolution = 1.0f;

        handler = new Handler();
        handlerDelayedMovement = new Handler();
    }   

    public void setMovementRange(float movementRange) {
        this.movementRange = movementRange;
    }   

    public void setOnJostickMovedListener(JoystickMovedListener listener) {
        this.moveListener = listener;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        int d = Math.min(getMeasuredWidth(), getMeasuredHeight());

        dimX = d;
        dimY = d;

        cX = d / 2;
        cY = d / 2;

        bgRadius = dimX/2 - innerPadding;
        handleRadius = (int)(d * 0.2);
        handleInnerBoundaries = handleRadius;
        movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
    }   

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Here we make sure that we have a perfect circle
        int measuredWidth = measure(widthMeasureSpec);
        int measuredHeight = measure(heightMeasureSpec);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    private int measure(int measureSpec) {
        int result = 0;
        // Decode the measurement specifications.
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.UNSPECIFIED) {          
            result = 200; // Return a default size of 200 if no bounds are specified.
        } else {            
            result = specSize; // As you want to fill the available space always return the full available bounds.
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        // Draw the background
        canvas.drawCircle(cX, cY, bgRadius, bgPaint);

        // Draw the handle
        handleX = touchX + cX;
        handleY = touchY + cY;
        canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);

        canvas.restore();
    }

    public void setPointerId(int id) {
        this.pointerId = id;
    }

    public int getPointerId() {
        return pointerId;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                if (disabled==true)
                    break;
                return processMoveEvent(ev);
            }       
            case MotionEvent.ACTION_CANCEL: 
            case MotionEvent.ACTION_UP: {
                if ( pointerId != INVALID_POINTER ) {
                    returnHandleToCenter();
                    returnHandleToCenterDelayedMovement();
                    setPointerId(INVALID_POINTER);
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                if ( pointerId != INVALID_POINTER ) {
                    final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                    final int pointerId = ev.getPointerId(pointerIndex);
                    if ( pointerId == this.pointerId ) {
                        returnHandleToCenter();
                        returnHandleToCenterDelayedMovement();
                        setPointerId(INVALID_POINTER);
                        return true;
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN: {
                handlerDelayedMovement.removeCallbacksAndMessages(null);
                if ( pointerId == INVALID_POINTER ) {
                    int x = (int) ev.getX();
                    if ( x >= offsetX && x < offsetX + dimX ) {
                        setPointerId(ev.getPointerId(0));
                        if (disabled==true){
                            return true;
                        }                       
                        return processMoveEvent(ev);
                    }
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_DOWN: {
                if ( pointerId == INVALID_POINTER ) {
                    final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                    final int pointerId = ev.getPointerId(pointerIndex);
                    int x = (int) ev.getX(pointerId);
                    if ( x >= offsetX && x < offsetX + dimX ) {
                        setPointerId(pointerId);
                        return true;
                    }
                }
                break;
            }
        }
        return false;
    }

    private boolean processMoveEvent(MotionEvent ev) {
        if ( pointerId != INVALID_POINTER ) {
            final int pointerIndex = ev.findPointerIndex(pointerId);

            // Translate touch position to center of view
            float x = ev.getX(pointerIndex);
            touchX = x - cX - offsetX;
            float y = ev.getY(pointerIndex);
            touchY = y - cY - offsetY;

            reportOnMoved();
            invalidate();

            return true;
        }
        return false;
    }

    private void reportOnMoved() {
        //constraint circle
        float diffX = touchX;
        float diffY = touchY;
        double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
        if ( radial > movementRadius ) {
            touchX = (int)((diffX / radial) * movementRadius);
            touchY = (int)((diffY / radial) * movementRadius);
        }

        //We calc user coordinates      
        //First convert to cartesian coordinates
        cartX = (int)(touchX / movementRadius * movementRange);
        cartY = (int)(touchY / movementRadius * movementRange);

        //Cartesian Coordinates
        userX = cartX;
        userY = cartY;      

        if (moveListener != null) {
            boolean rx = Math.abs(touchX - reportX) >= moveResolution;
            boolean ry = Math.abs(touchY - reportY) >= moveResolution;
            if (rx || ry) {
                this.reportX = touchX;
                this.reportY = touchY;

                moveListener.OnMoved(userX, userY);
            }
        }
    }

    private void reportOnMovedDelayedMovement() {
        //constraint circle
        float diffX = touchXDelayedMovement;
        float diffY = touchYDelayedMovement;
        double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
        if ( radial > movementRadius ) {
            touchXDelayedMovement = (int)((diffX / radial) * movementRadius);
            touchYDelayedMovement = (int)((diffY / radial) * movementRadius);
        }

        //We calc user coordinates      
        //First convert to cartesian coordinates
        cartX = (int)(touchXDelayedMovement / movementRadius * movementRange);
        cartY = (int)(touchYDelayedMovement / movementRadius * movementRange);

        //Cartesian Coordinates
        userX = cartX;
        userY = cartY;      

        if (moveListener != null) {
            boolean rx = Math.abs(touchXDelayedMovement - reportX) >= moveResolution;
            boolean ry = Math.abs(touchYDelayedMovement - reportY) >= moveResolution;
            if (rx || ry) {
                this.reportX = touchXDelayedMovement;
                this.reportY = touchYDelayedMovement;

                moveListener.OnMoved(userX, userY);
            }
        }
    }

    private void returnHandleToCenter() {
        final int numberOfFrames = 5;
        final double intervalsX = (0 - touchX) / numberOfFrames;
        final double intervalsY = (0 - touchY) / numberOfFrames;

        handler.removeCallbacksAndMessages(null);
        for (int i = 0; i < numberOfFrames; i++) {
            final int j = i;
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    touchX += intervalsX;
                    touchY += intervalsY;

                    //reportOnMoved();
                    invalidate();

                    if (moveListener != null && j == numberOfFrames - 1) {
                        moveListener.OnReturnedToCenter();
                    }
                }
            }, i * 10);
        }

        if (moveListener != null) {
            moveListener.OnReleased();
        }
    }

    private void returnHandleToCenterDelayedMovement() {
        final int numberOfFrames = 25;
        touchXDelayedMovement=touchX;
        touchYDelayedMovement=touchY;
        final double intervalsX = (0 - touchXDelayedMovement) / numberOfFrames;
        final double intervalsY = (0 - touchYDelayedMovement) / numberOfFrames;

        handlerDelayedMovement.removeCallbacksAndMessages(null);
        for (int i = 0; i < numberOfFrames; i++) {
            handlerDelayedMovement.postDelayed(new Runnable() {
                @Override
                public void run() {
                    touchXDelayedMovement += intervalsX;
                    touchYDelayedMovement += intervalsY;

                    reportOnMovedDelayedMovement();
                }
            }, i * 50);
        }
    }

    public void setInnerPadding(int innerPadding){
        this.innerPadding=innerPadding;
    }

    public void disable(){
        disabled=true;
    }   

    public void enable(){
        disabled=false;
    }

    public interface JoystickMovedListener {
        public void OnMoved(int pan, int tilt);
        public void OnReleased();
        public void OnReturnedToCenter();
    }
}

You must do this in the class that will use the joystick:

private JoystickMovedListener joystickListener = new JoystickMovedListener() {
    @Override
    public void OnMoved(int pan, int tilt) {    
        //here i move the objects in the game
        }
    }

    @Override
    public void OnReleased() {}

    public void OnReturnedToCenter() {};
}; 

joystickOnScreen = new Joystick(this);
        joystickOnScreen.setMovementRange(screenHeight/50);
        joystickOnScreen.setInnerPadding(screenHeight/30);
        joystickOnScreen.setOnJostickMovedListener(joystickListener);
        RelativeLayout.LayoutParams joystickParams = new RelativeLayout.LayoutParams(sh/3, sh/3);
        joystickParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        joystickParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        joystickParams.setMargins(sh/100, 0, 0, sh/100);
        joystickOnScreen.setLayoutParams(joystickParams);
        joystickOnScreen.setAlpha(0.3f);
BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
NullPointerException
  • 36,107
  • 79
  • 222
  • 382
  • possible duplicate of [Issue with my game Joystick](http://stackoverflow.com/questions/28792914/issue-with-my-game-joystick) – Clive Mar 01 '15 at 12:31
  • 1
    that post does not exist, it is deleted – NullPointerException Mar 01 '15 at 12:32
  • 2
    No need to ask the same question over and over again if there is an existing one. Duplicates will be either deleted or closed. Just wait until somebody has an answer/do some research/open a bounty to an existing question (preferrably all three). – fge Mar 01 '15 at 12:34
  • 1
    AndroidUser99 and Clive, can you please stop this? Instead of going into an edit war, discuss it in chat and let a moderator handle it. Although I agree with Clive. This site is for people wanting to solve issues on their own; it is not a commercial site to offer money. Hence I flagged it. – Ingo Bürk Mar 01 '15 at 12:53

1 Answers1

1

I will not implement the changes for you but hopefully this answer can help you towards implementing this on your own.

With your current implementation you are updating the object position (x, y) each frame. To get the more realistic physics that you want, you need to store and update velocity as well (vx, vy).

Add two new variables, vx and vy (with initial values of zero) in the objects that you are currently updating the position for. The joystick should control the change of the velocity instead of the position. Change the code that updates the positions x and y, to update the velocities vx and vy instead. When the joystick is max left, you can for example set vx = vx - 3.

After the velocity is updated, you need to update the position using the velocity variables. For example, set the position x = x + vx. Ideally you want this to happen in a different method that runs even if you don't move the joystick, but to keep it simple you can do this update right after the update of the velocity variables.

With this implementation you will get a more realistic game physics. As a next step you might want to add limits on the velocity to not move too fast. This can be done with an if-statement where you check that the value is not too big before adding more to it, or too smal before subtracting from it. Good luck!

Niemi
  • 525
  • 2
  • 8
  • 13
  • I am trying to figure out how to implement this but i can't. Can you explain it a little more please? – NullPointerException Mar 01 '15 at 19:43
  • also in the code i sent you zipped i am doing something like x = x + vx, but vx is not called vx, is called joyX instead. And it results in the bad behaviour i described in the question – NullPointerException Mar 01 '15 at 19:46
  • There is no `joyX`in the zip-file. Where exactly do you use joyX? – Niemi Mar 01 '15 at 20:04
  • sorry, there is no joyX because this demo project it's not my full game, it is only a demo with the joystick. Check the joystick listener in MainActivity. this line: textView.setText("X,Y: "+pan+","+tilt); pan is joyX and tilt is joyY. joyX and joyY are the values that moves the items in the screen. I'm using joyX and joyY in my screen objects to move them with this code: view.setX(view.getX()-joyX); view.setY(view.getY()-joyY); I'm just setting joyX and joyY values each time the listener of the joystick is called, and i have a game thread that updates screen items position each 10ms. – NullPointerException Mar 01 '15 at 21:00
  • did you understand me? – NullPointerException Mar 02 '15 at 18:07
  • Then you would want to add vx and vy to the views instead and add methods to get and set them. Then you can update x and y in your game thread before drawing a new frame. I am not experienced with Android development so not sure about how it works to add you own member variables to the views. – Niemi Mar 03 '15 at 15:48