Search code examples
javaandroidgame-physics

Improve movement of moon vehicle


I am creating a mini game for Android, available from the playstore, which we call "Moon Buggy". The action is that you control a vehicle on the moon and you should defend yourself from attacking UFO:s.

enter image description here

Now I want to improve the movement of the vehicle. I have code so that it doesn't move outside the screen, and it is possible to accelerate, slow down and jump. But it is not perfect. I was inspired by the classic game Moon Patrol which is measuring time per completed section and my game does that too. So preferably you should be able to accelerate and complete the section faster the more you accelerate perhaps instead of stopping at the end of the screen which my vehicle does now. Also, perhaps it should accelerate faster.

The relevant code is:

ParallaxActivity.java

public class ParallaxActivity extends Activity implements View.OnClickListener {

    long startTime;
    long countUp;
    TextView textGoesHere;
    ParallaxView parallaxView;

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button3:
                ParallaxView parallaxView = findViewById(R.id.backgroundImage);
                parallaxView.recent = false;
                parallaxView.brake = false;
                parallaxView.buggyXDistance = parallaxView.buggyXDistance + 3;
                parallaxView.distanceDelta = parallaxView.distanceDelta + 0.2;
                break;
            case R.id.button4:
                ParallaxView parallaxView2 = findViewById(R.id.backgroundImage);
                parallaxView2.recent = false;
                parallaxView2.buggyXDistance = parallaxView2.buggyXDistance - 7;
                parallaxView2.retardation = parallaxView2.retardation + 0.2;
                parallaxView2.brake = true;
                break;
            case R.id.button2:
                ParallaxView parallaxView3 = findViewById(R.id.backgroundImage);
                parallaxView3.numberOfshots++;
                parallaxView3.recent = false;
                parallaxView3.launchMissile();
                parallaxView3.scoring = true;
                break;
            case R.id.button1:
                ParallaxView parallaxView4 = findViewById(R.id.backgroundImage);
                if(parallaxView4.distanceDelta<3) parallaxView4.distanceDelta = parallaxView4.distanceDelta + 0.2;
                parallaxView4.jump = true;
                parallaxView4.shoot = false;
                parallaxView4.lastTurn3 = System.currentTimeMillis();
                break;
            default:
                break;
        }
    }
}

ParallaxView.java

public class ParallaxView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
    static int bombed = 5;
    boolean waitForTimer = false;
    boolean waitForTimer2 = false;
    boolean waitForTimer3 = false;
    boolean recent = false;

    Rect fromRect1;
    Rect toRect1;
    Rect fromRect2;
    Rect toRect2;

    boolean increment = false;
    int numberOfshots = 0; // change to 0
    int[] missiles = new int[200];
    int alienBombYDelta = 0;
    int alienBombYDelta2 = 0;
    int alienBombXDelta = 20;
    int alienBombXDelta2 = 30;
    int p = 7;
    int p2 = 13;
    boolean once, once2 = true;
    final int buggyXDisplacement = 450;
    int jumpHeight = 0;
    int xbuggy2 = 0;
    boolean toggleDeltaY = true;
    long lastTurn2 = System.currentTimeMillis();
    long lastTurn3 = System.currentTimeMillis();
    boolean toggleGround = true;
    boolean jump = false;
    boolean shoot = false;
    int index = 0;
    int missileOffSetY = 0;

    static int score = 0;
    double buggyXDistance = 0;
    double distanceDelta = 1.15;
    double retardation = 0.5;
    boolean checkpointComplete = false;
    boolean runOnce = true;

    boolean passed = false;
    List<Background> backgrounds;
    int spacerocki, resID, explodeID, explodeID2, alienResID2;

    boolean toggle = true;
    private volatile boolean running;
    private Thread gameThread = null;
    Bitmap explode, buggy, alien, alien2, explode2, spacerock, spacerock2, hole;
    boolean alienexplode = false;
    TextView tvId;
    TextView checkpointtextview;
    TextView checkpointtextview2;
    TextView checkpointtextview3;
    TextView checkpointtextview4;
    TextView checkpointtextview5;
    TextView checkpointtextview6;
    // For drawing
    private Paint paint;
    private Canvas canvas;
    private SurfaceHolder ourHolder;
    AttackingAlien alien3;
    AttackingAlien alien4, alien5;
    // Holds a reference to the Activity
    Context context;

    // Control the fps
    long fps = 60;

    // Screen resolution
    int screenWidth;
    int screenHeight;
    boolean bexplode = false;
    boolean brake = false;
    boolean scoring = false;

    // use Handler instead
    // this runs for 4 seconds and not just once after a while
    class BuggyExploded extends TimerTask {
        public void run() {
            distanceDelta = 1.15;
            retardation = 0.5;
            jumpHeight = 0;
            bexplode=true;
        }
    }

    // use Handler instead
    class SetRecent extends TimerTask {
        public void run() {
            recent = false;
        }
    }

    // use Handler instead
    class ResetCheckpoint extends TimerTask {
        public void run() {
            Log.d("## sectionComplete", "sectionComplete " + sectionComplete);
            if (sectionComplete == 0) Background.checkpoint = 'A';
            if (sectionComplete == 1) Background.checkpoint = 'F';
            if (sectionComplete == 2) Background.checkpoint = 'K';
            if (sectionComplete == 3) Background.checkpoint = 'P';
            if (sectionComplete == 4) Background.checkpoint = 'U';
            //if (sectionComplete==5) Background.checkpoint = 'U';
        }
    }

    private void update() {
        // Update all the background positions
        for (Background bg : backgrounds) {
            bg.update(fps);
        }
    }


    @Override
    public void run() {
        while (running) {
            long startFrameTime = System.currentTimeMillis();
            update();
            if (alienBombXDelta > screenWidth - 250 || alienBombXDelta < 10) { // UFO change direction
                p = -p;
            }

            if (alienBombXDelta2 > screenWidth - 250 || alienBombXDelta2 < 10) { // UFO2 change direction
                p2 = -p2;
            }

            draw();
            // Calculate the fps this frame
            long timeThisFrame = System.currentTimeMillis() - startFrameTime;
            if (timeThisFrame >= 1) {
                fps = 1000 / timeThisFrame;
            }
        }
    }

    private void checkJump() {
        if (System.currentTimeMillis() - lastTurn3 >= 650) { // 650 means how long the vehicle is in the air at a jump
            // Change direction here
            jump = false;
            lastTurn3 = System.currentTimeMillis();
        }
    }

    private void checkBuggyBombed() {
        if (recent) {
            // use handlers instead
            new Timer().schedule(new BuggyExploded(), 4000);
            new Timer().schedule(new SetRecent(), 10000);
            new Timer().schedule(new ResetCheckpoint(), 1000);

        }
    }

    private void makeShots(Bitmap b) {
        for (int i1 = 0; i1 < numberOfshots; i1++) {
             if (shoot) {
                canvas.drawText("o", (float) (missiles[i1] + buggyXDistance + 450), (float) (screenHeight * 0.7) - jumpHeight, paint); // add to y the jump height
                canvas.drawText("o", (float) (buggyXDistance + 185 + 400), screenHeight / 110 * 95 - missiles[i1] - xbuggy2, paint);
            }
            if (i1 == numberOfshots - 1 && missiles[i1] > screenWidth) {
                if (numberOfshots > 0) numberOfshots--;
                if (index > 0) index--;
            }
        }
    }

    private void updateDeltas() {
        alienBombXDelta = alienBombXDelta + p;

        //make sure alien does not move too low
        if (alienBombYDelta + 1 > screenHeight / 2)
            alienBombYDelta=alienBombYDelta-2;

        if (!toggleDeltaY)
            alienBombYDelta++;
        else
            alienBombYDelta--;

        alienBombXDelta2 = alienBombXDelta2 + p2;
        if (!toggleDeltaY)
            alienBombYDelta2++;
        else
            alienBombYDelta2--;

    }

    //use a Handler instead
    private void changeDirections() {
        if (System.currentTimeMillis() - lastTurn2 >= 7000) {
            // Change direction here
            toggleDeltaY = !toggleDeltaY;
            lastTurn2 = System.currentTimeMillis();
        }
    }

    //try to improve this
    private void controlVelocity() {
        if (!brake && buggyXDistance > 0) buggyXDistance = buggyXDistance + distanceDelta;
        else if (brake && buggyXDistance > 0) buggyXDistance = buggyXDistance - retardation;
    }

    TextView tvId1;
    int sectionComplete = 0;

    private void drawDetails() {
        //draw a background color
    }

    private void makeShots() {
        for (int n = 0; n < numberOfshots; n++)
            missiles[n] = missiles[n] + 20;
    }

    public void changeText() {
        if (scoring) {
            ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    String str = "Player 1  " + String.format("%06d", score);
                    tvId.setText(str);
                    scoring = false;
                }
            });
        }
    }

    double lastTurn4 = System.currentTimeMillis();
    //change to handler
    private void checkFire() {
        if (System.currentTimeMillis() - lastTurn4 >= 118500) { // 18500 means how often the alien fires
            lastTurn4 = System.currentTimeMillis();
            missileOffSetY = 0;
        }
    }

    private void draw() {
        if (retardation > 0.5)
            distanceDelta = 0;
        if (distanceDelta > 0)
            retardation = 0.5;
        if (ourHolder.getSurface().isValid()) {
            //First we lock the area of memory we will be drawing to
            canvas = ourHolder.lockCanvas();
            if (checkpointComplete) {
                ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        //
                    }
                });
                canvas.drawColor(Color.BLACK);
                ((ParallaxActivity) getContext()).stopWatch.stop();
                paint.setTextSize(60);
                String s2 = "TIME TO REACH POINT \"" + Background.checkpoint + "\"\n";
                if (runOnce) {
                    for (int q = 0; q < s2.length(); q++) {
                        final String s2f = s2;
                        final int r = q;
                        ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                checkpointtextview.setTextColor(Color.RED);
                                checkpointtextview.append(Character.toString(s2f.charAt(r)));
                            }
                        });
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException ie) {
                        }
                    }
                }
                String str = String.format("%03d", ((ParallaxActivity) this.getContext()).countUp);
                String s3 = "YOUR TIME                :   " + str;
                if (runOnce) {
                    for (int q = 0; q < s3.length(); q++) {
                        final String s3f = s3;
                        final int r = q;
                        ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                checkpointtextview2.setTextColor(Color.parseColor("#ADD8E6"));
                                checkpointtextview2.append(Character.toString(s3f.charAt(r)));
                            }
                        });
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException ie) {
                        }
                    }
                }
                String s4 = "THE AVERAGE TIME        :   060";
                if (runOnce) {
                    for (int q = 0; q < s4.length(); q++) {
                        final String s4f = s4;
                        final int r = q;
                        ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                checkpointtextview3.setTextColor(Color.parseColor("#ADD8E6"));
                                checkpointtextview3.append(Character.toString(s4f.charAt(r)));
                            }
                        });
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException ie) {
                        }
                    }
                }
                String s5 = "TOP RECORD        :   060";
                if (runOnce) {
                    for (int q = 0; q < s5.length(); q++) {
                        final String s5f = s5;
                        final int r = q;
                        ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                checkpointtextview4.setTextColor(Color.RED);
                                checkpointtextview4.append(Character.toString(s5f.charAt(r)));
                            }
                        });
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException ie) {
                        }
                    }
                }

                String s6 = "GOOD BONUS POINTS        :   1000";
                if (runOnce) {
                    for (int q = 0; q < s6.length(); q++) {
                        final String s6f = s6;
                        final int r = q;
                        ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                checkpointtextview5.setTextColor(Color.RED);
                                checkpointtextview5.append(Character.toString(s6f.charAt(r)));
                            }
                        });
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException ie) {
                        }
                    }
                }
                if (runOnce) {
                    score = score + 1000;
                    sectionComplete++;
                    recent = true;
                }
                runOnce = false;

                // canvas.drawText("CHECKPOINT COMPLETE", (float) (screenWidth * 0.35), (float) (screenHeight * 0.45), paint);
                ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Handler handler = new Handler();
                        handler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                ((ParallaxActivity) getContext()).startTime = SystemClock.elapsedRealtime();
                                ((ParallaxActivity) getContext()).stopWatch.setBase(((ParallaxActivity) getContext()).startTime);
                                ((ParallaxActivity) getContext()).stopWatch.start();
                                checkpointtextview.setText("");
                                checkpointtextview2.setText("");
                                checkpointtextview3.setText("");
                                checkpointtextview4.setText("");
                                checkpointtextview5.setText("");
                                checkpointtextview6.setText("");
                                String str = "Player 1  " + String.format("%06d", score);
                                tvId.setText(str);
                                scoring = false;
                                buggyXDistance = 0;
                                distanceDelta = 0;
                                retardation = 0;
                                checkpointComplete = false;
                                runOnce = true;

                            }
                        }, 3000);
                    }
                });
            } else {
                if (bombed == 0) //GAME OVER
                {
                    final int duration = Toast.LENGTH_SHORT;

                    ((Activity) this.getContext()).runOnUiThread(new Runnable() {
                        @Override
                        public void run() {

                            final Toast toast = Toast.makeText(context, "GAME OVER!\nScore: " + score, duration);

                            toast.show();
                            Handler handler = new Handler();
                            handler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    toast.cancel();
                                    bombed = 5;
                                    score = 0;
                                    Background.checkpoint = 'A';
                                    String str = "Player 1  " + String.format("%06d", score);
                                    tvId.setText(str);
                                }
                            }, 3000);
                        }
                    });
                }
                // adjust vehicle physics when jumping
                if (jump && jumpHeight < 300) {
                    jumpHeight = jumpHeight + 7;
                    if (distanceDelta < 3) distanceDelta = distanceDelta + 0.55;
                } else if (jumpHeight > 0) {
                    jumpHeight = jumpHeight - 4;
                    if (distanceDelta < 3) distanceDelta = distanceDelta + 0.55;
                }
                if (shoot) {
                    xbuggy2 = xbuggy2 + 4;
                }
                checkFire();
                checkJump();
                //    drawDetails();

                canvas.drawColor(Color.argb(255, 0, 0, 0));
                // Draw the background parallax
                drawBackground(0);
                // Draw the rest of the game
                paint.setTextSize(60);
                paint.setColor(Color.argb(255, 255, 255, 255));
                checkBuggyBombed();
                makeShots(alien);
                changeDirections();

                alien3.update(canvas, paint, toggleDeltaY, screenWidth, screenHeight);
                recent=alien3.drawMissile(this, canvas, paint, buggyXDisplacement, buggyXDistance, buggy, jumpHeight, screenHeight);
                if(recent) {
                    waitForTimer = true;
                    bexplode=true;
                    AttackingAlien.recent = true;
                }
                alien4.update(canvas, paint, toggleDeltaY, screenWidth, screenHeight);
                boolean recent2=alien4.drawMissile(this, canvas, paint, buggyXDisplacement, buggyXDistance, buggy, jumpHeight, screenHeight);
                if(recent || recent2) {
                    recent = true;
                    waitForTimer = true;
                    bexplode=true;
                    AttackingAlien.recent = true;
                }
                alien5.update(canvas, paint, toggleDeltaY, screenWidth, screenHeight);
                boolean recent3=alien5.drawMissile(this, canvas, paint, buggyXDisplacement, buggyXDistance, buggy, jumpHeight, screenHeight);

                if(recent || recent2 || recent3) {
                    recent = true;
                    waitForTimer = true;
                    bexplode=true;
                    AttackingAlien.recent = true;

                    Handler handler = new Handler(Looper.getMainLooper());
                    // this code runs after a while
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            recent = false;
                            AttackingAlien.recent = false;
                            waitForTimer = false;
                            bexplode=false;
                            buggyXDistance = 0;

                            Log.d("postDelayed", "postDelayed ");


                        }
                    }, 5000);

                }



                checkBuggyBombed();
                for (int i1 = 0; i1 < numberOfshots; i1++) {

                    alien3.checkBeingHit(missiles, buggyXDisplacement, buggyXDistance, canvas, explode2, paint, score, this, i1, xbuggy2);
                    alien4.checkBeingHit(missiles, buggyXDisplacement, buggyXDistance, canvas, explode2, paint, score, this, i1, xbuggy2);
                    alien5.checkBeingHit(missiles, buggyXDisplacement, buggyXDistance, canvas, explode2, paint, score, this, i1, xbuggy2);

                }
                drawBackground(1);

                // canvas.drawText("X", (float) (50 + buggyXDistance)+buggy.getWidth()/2, (float) (screenHeight * 0.3) - jumpHeight+buggy.getHeight(), paint);
                paint.setTextSize(60);
                canvas.drawText("A    E    J    O    T    Z", (float) (screenWidth * 0.7), (float) (screenHeight * 0.15), paint);

                // Prevent buggy from moving outside horizontal screen
                if (!brake && buggyXDisplacement + buggyXDistance > screenWidth - buggy.getWidth() - 200)
                    buggyXDistance = screenWidth - buggy.getWidth() - 200;
                //Log.d("buggyXDistance", "buggyXDistance " + buggyXDistance);

                if (!bexplode && !waitForTimer && !checkpointComplete)
                    canvas.drawBitmap(buggy, (float) (buggyXDisplacement + buggyXDistance), (float) (screenHeight * 0.5) - jumpHeight, paint);
                else if (bexplode && !checkpointComplete) {
                    canvas.drawBitmap(explode, (float) (buggyXDisplacement + buggyXDistance), (float) (screenHeight * 0.5) - jumpHeight, paint);
                    distanceDelta = 0;
                    retardation = 0;
                }
                int inc = 0;
                for (int i = 0; i < bombed; i++) {
                    canvas.drawBitmap(Bitmap.createScaledBitmap(buggy, (int) (0.50 * (buggy.getWidth() / 3)), (int) (0.50 * buggy.getHeight() / 3), false), inc, 100, paint);
                    inc = inc + buggy.getWidth() / 4;
                }

                makeShots();
                updateDeltas();
                controlVelocity();

            }
            ourHolder.unlockCanvasAndPost(canvas);
        }
    }

    // Clean up our thread if the game is stopped
    public void pause() {
        running = false;
        try {
            gameThread.join();
        } catch (InterruptedException e) {
            // Error
            //e.printStackTrace();
        }
    }

    // Make a new thread and startMissile it
    // Execution moves to our run method
    public void resume() {
        running = true;
        gameThread = new Thread(this);
        gameThread.start();
    }

    int craterX = -550;


}

The complete code is available on request because of SO limitation of 30000 chars.


Solution

  • I unfortunately cannot supply you with the code for this right now (since I am at work currently), but one thing you can do is to have your fall speed (acceleration) modified depending on whether the jump button is held.

    ie.

    // Put this into your constant gravity function
    // Or whatever you have that makes the player fall
    // This should be altering a local gravity modifier in the player
    if(jumpKeyHeld) {
        locaGravAccel = 0.5
    } else {
        locaGravAccel = 1.0
    }
    

    What this does is make your jump feel more responsive as you move upwards at a nice speed, and then fall straight down to the ground quickly. It also gives the famous tap-for-low-jump-hold-for-high-jump feeling that you find in Mario games and the like.

    Unless I vastly misunderstood your question, I believe this will help with game feel for you. I've implemented similar features in other projects, and people have always responded positively to this change, often claiming it feels more like a genuine 'game'.

    UPDATE: Since I've noticed you don't actually have a gravity function, here is one way to do it. It is in C# for unity2D, but this is how I learned to do it, so I know it is good.