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.
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.
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.