Game cycle

It is very important what type of game you create. If the game is dynamic, we need to update the picture on the device all the time. This means that you first need to create it by applying the new coordinates of the objects, and then display it on the screen. Our vision is designed so that if we do it in less than 0.04 s, then the movement of objects will seem continuous to us. But objects can be different in complexity of drawing, and the devices on which you play can vary in speed.

It may happen that on some tablets or mobile phones our application will “fly”, so that the user will not even have time to play, and on others, it will slow down so that the user will most likely delete it from his device. There is an idea to go through one game cycle in 0.04 s (25 frames (cycles) per second) on all devices. Everything would be fine if all devices could do it. Imagine that you have 10 dynamic objects in the game that interact with each other, creating new objects, for example, an explosion in a collision. Also, do not forget to play sounds and respond to the inclusion of the user in the game. Not to mention the realistic graphics of the world around us.

What to do if our device does not have time to create a game cycle in some scene? There are several solutions, one of them is considered below.

Create a game loop


The main idea is that if the real time for drawing a frame is longer than the estimated time for updating the frames (that is, the system does not have time), we sacrifice this frame and increase the time for drawing the next frame. If the system immediately manages to draw a frame, then it will do it as quickly as possible, but the flow will be suspended for the lead time so that the number of frames, or rather the program execution speed, is set, and does not depend on the speed of the device itself.

This approach allows you to reduce the energy consumption of the device and play the game approximately the same on different devices. At a refresh rate of 25 frames per second, I noticed unpleasant twitching of objects on the phone with android 2.2.1 (especially if they move quickly), but at 30 frames per second everything was fine. At 50 frames per second on a tablet with android 4.2.2, everything worked perfectly, but on the phone sometimes there were noticeable frame drops, especially at high speed of the object.

Let's consider our code in more detail.

Create the MainThread class, which inherits from the Thread class. In the future, we will call it in the GameView game class.

publicclassMainThreadextendsThread{ 

publicclassMainThreadextendsThread{

Set the number of frames per second (MAX_FPS) to 30.

privatefinalstaticint MAX_FPS = 30; // desired fps

Let the maximum number of frames per second that we are willing to sacrifice (skip) in the display be 4. This number was actually found by me experimentally, you can play around with these numbers.

// maximum number of frames to be skipped

privatefinalstaticint MAX_FRAME_SKIPS = 4;

We calculate in milliseconds the duration of the display frame.

privatefinalstaticint FRAME_PERIOD = 1000 / MAX_FPS;  // the frame period

We declare a method that creates a surface on which objects will be drawn.

private SurfaceHolder surfaceHolder; // Surface holder that can access the physical surface

We declare a class that will draw objects and update their coordinates.

private GameView gameView;// The actual view that handles inputs and draws to the surface

We declare the state variable of the game, if it is true, then the stream is played.

privateboolean running;   // flag to hold game state publicvoidsetRunning(boolean running){
this.running = running;
  }

Create a class constructor.

publicMainThread(SurfaceHolder surfaceHolder, GameView gameView){
super();
 this.surfaceHolder = surfaceHolder;
this.gameView = gameView;
 }
 @ Override
 publicvoidrun(){
 Canvas canvas;</code>
long beginTime;  // the time when the cycle begunlong timeDiff;  // the time it took for the cycle to executeint sleepTime;// ms to sleep (<0 if we're behind)int framesSkipped;</font>// number of frames being skipped
sleepTime = 0;
while (running) {
canvas = null; // try locking the canvas for exclusive pixel editing in the surfacetry {
canvas = this.surfaceHolder.lockCanvas();
synchronized (surfaceHolder) {
beginTime = System.currentTimeMillis(); //Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC.
framesSkipped = 0; // resetting the frames skipped// update game statethis.gameView.update();
// render state to the screen draws the canvas on the panelthis.gameView.render(canvas);
timeDiff = System.currentTimeMillis() - beginTime;</font> // calculate how long did the cycle take// calculate sleep time
sleepTime = (int)(FRAME_PERIOD - timeDiff);
 if (sleepTime > 0) {</font> // if sleepTime > 0 we're OKtry {</font> 
// send the thread to sleep for a short period// very useful for battery saving
Thread.sleep(sleepTime);
} catch (InterruptedException e) {}
}
while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
 // we need to catch upthis.gameView.update();</font>  // update without rendering
sleepTime += FRAME_PERIOD; // add frame period to check if in next frame
framesSkipped++;
 }
 }
 } finally {
 // in case of an exception the surface is not left in// an inconsistent stateif (canvas != null) {
surfaceHolder.unlockCanvasAndPost(canvas);
}
}  // end finally
}
}
}

Now let's take a closer look at what the GameView class does.

publicclassGameViewextendsSurfaceViewimplementsSurfaceHolder.Callback{
privatefinal Drawable mAsteroid;
privateint widthAsteroid;
privateint heightAsteroid;
privateint leftAsteroid;
privateint xAsteroid1 = 30;
privateint rightAsteroid;
privateint topAsteroid;
privateint yAsteroid = -30;
privateint bottomAsteroid;
privateint centerAsteroid;
privateint height;
privateint width;
privateint speedAsteroid = 5;
privateint xAsteroid;
private MainThread thread;
publicGameView(Context context){
super(context); 
 // adding the callback (this) to the surface holder to intercept events
getHolder().addCallback(this);
// create mAsteroid where adress of picture  asteroid is located
mAsteroid = context.getResources().getDrawable(R.drawable.asteroid);
// create the game loop thread
thread = new MainThread(getHolder(), this);
 }

This method allows you to change the surface of the drawing, for example, when we rotate the screen.

@OverridepublicvoidsurfaceChanged(SurfaceHolder holder, int format, int width,int height){
 }

A method that creates a surface for painting.

@OverridepublicvoidsurfaceCreated(SurfaceHolder holder){
thread.setRunning(true);
thread.start();
 }

The method removes the surface to be drawn in case the flow stops.

@OverridepublicvoidsurfaceDestroyed(SurfaceHolder holder){
thread.setRunning(false);
boolean retry = true;
while (retry) {
try {
thread.join();
retry = false;
 }
catch (InterruptedException e) {
// try again shutting down the thread
}
}
}

In this method, we draw our objects on the surface of the screen.

publicvoidrender(Canvas canvas){

Create a dark blue background.

canvas.drawColor(Color.argb(255, 2, 19, 151));

We will find out the height and width of the device’s screen, in order to later scale the sizes of objects depending on the screen size.

height = canvas.getHeight();
width = canvas.getWidth();

We set the main dimensions of the asteroid (the width and height of the picture). We attach the left edge to the coordinate along X, we attach the top of the picture to the coordinate along Y. Thus, we determined the coordinates of the upper left corner of the image. When we change these coordinates, the picture will begin to move. Do not forget to put in the drawable folder an asteroid drawing in png format.

//Work with asteroid
widthAsteroid = 2 * width / 13;//set width asteroid
heightAsteroid = widthAsteroid;
leftAsteroid = xAsteroid; //the left edge of asteroid
rightAsteroid = leftAsteroid + widthAsteroid;//set right edge of asteroid
topAsteroid = yAsteroid;
bottomAsteroid = topAsteroid + heightAsteroid;
centerAsteroid = leftAsteroid + widthAsteroid / 2;
mAsteroid.setBounds(leftAsteroid, topAsteroid, rightAsteroid, bottomAsteroid);
mAsteroid.draw(canvas);
 }

We update the coordinates of the picture, forcing it to move from top to bottom. If the Y coordinate on the upper left corner of the picture has become larger than the height of the screen, then zero the Y and the asteroid will jump up at the top of the screen. To make the asteroid fly down, we add the number speedAsteroid to the coordinate, which, in turn, is determined randomly from 5 to 15 (speedAsteroid = 5+ rnd.nextInt (10);).

publicvoidupdate(){
if (yAsteroid > height) {
yAsteroid = 0;
// find by random function Asteroid & speed Asteroid
Random rnd = new Random();
 xAsteroid = rnd.nextInt(width - widthAsteroid);
speedAsteroid = 5+ rnd.nextInt(10);
 } else {
 yAsteroid +=speedAsteroid;
}
}
}

So that the asteroid does not fly out every time from the same place, the X coordinate is determined according to a random law within the screen width minus the width of the asteroid. At the time of launching the application, the asteroid crashes with the initial coordinates that we specified when declaring the variables private int xAsteroid = 30; private int yAsteroid = -30 ;. In the future, the flight speed and the initial coordinate change randomly.

Currently application files


AhdroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.adc2017gmail.moonbase" >
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
 android:label="@string/app_name"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:label="@string/title_activity_second"
android:screenOrientation="portrait" >
</activity>
</application>
</manifest>

Gameview.java

package com.adc2017gmail.moonbase;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.Random;
publicclassGameViewextendsSurfaceViewimplementsSurfaceHolder.Callback{
privatefinal Drawable mAsteroid;
privateint widthAsteroid;
privateint heightAsteroid;
privateint leftAsteroid;
privateint rightAsteroid;
privateint topAsteroid;
privateint yAsteroid = -30;
privateint bottomAsteroid;
privateint centerAsteroid;
privateint height;
privateint width;
 privateint speedAsteroid = 5;
privateint xAsteroid = 30;
private MainThread thread;
publicGameView(Context context){
super(context);
// adding the callback (this) to the surface holder to intercept events
getHolder().addCallback(this);
// create mAsteroid where adress picture  asteroid
mAsteroid = context.getResources().getDrawable(R.drawable.asteroid);
// create the game loop thread
thread = new MainThread(getHolder(), this);
}
@OverridepublicvoidsurfaceChanged(SurfaceHolder holder, int format, int width,int height){
}
@OverridepublicvoidsurfaceCreated(SurfaceHolder holder){
thread.setRunning(true);
thread.start();
}
@OverridepublicvoidsurfaceDestroyed(SurfaceHolder holder){
thread.setRunning(false);
boolean retry = true;
while (retry) {
try {
thread.join();
retry = false;
}
catch (InterruptedException e) {
// try again shutting down the thread
}
}
}
publicvoidrender(Canvas canvas){
canvas.drawColor(Color.argb(255, 2, 19, 151));
height = canvas.getHeight();
width = canvas.getWidth();
//Work with asteroid
widthAsteroid = 2 * width / 13;//set width asteroid
heightAsteroid = widthAsteroid;
leftAsteroid = xAsteroid;//the left edge of asteroid
rightAsteroid = leftAsteroid + widthAsteroid;//set right edge of asteroid
 topAsteroid = yAsteroid;
bottomAsteroid = topAsteroid + heightAsteroid;
centerAsteroid = leftAsteroid + widthAsteroid / 2;
mAsteroid.setBounds(leftAsteroid, topAsteroid, rightAsteroid, bottomAsteroid);
mAsteroid.draw(canvas);
}
publicvoidupdate(){
if (yAsteroid > height) {
yAsteroid = 0;
// find by random function Asteroid & speed Asteroid
Random rnd = new Random();
xAsteroid = rnd.nextInt(width - widthAsteroid);
speedAsteroid = 5+ rnd.nextInt(10);
} else {
yAsteroid +=speedAsteroid;
}
}
}

MainThread.java

package com.adc2017gmail.moonbase;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
publicclassMainThreadextendsThread{
privatefinalstaticint MAX_FPS = 30;// desired fpsprivatefinalstaticint MAX_FRAME_SKIPS = 4;// maximum number of frames to be skippedprivatefinalstaticint FRAME_PERIOD = 1000 / MAX_FPS; // the frame period// Surface holder that can access the physical surfaceprivate SurfaceHolder surfaceHolder;
// The actual view that handles inputs// and draws to the surfaceprivate GameView gameView;
// flag to hold game stateprivateboolean running;
publicvoidsetRunning(boolean running){
this.running = running;
}
publicMainThread(SurfaceHolder surfaceHolder, GameView gameView){
super();
this.surfaceHolder = surfaceHolder;
this.gameView = gameView;
}
@Overridepublicvoidrun(){
Canvas canvas;
long beginTime;// the time when the cycle begunlong timeDiff; // the time it took for the cycle to execute int sleepTime;// ms to sleep (<0 if we're behind)int framesSkipped;// number of frames being skipped
sleepTime = 0;
while (running) {
canvas = null;
// try locking the canvas for exclusive pixel editing in the surfacetry {
canvas = this.surfaceHolder.lockCanvas();
synchronized (surfaceHolder) {
beginTime = System.currentTimeMillis();//Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC.
framesSkipped = 0; // resetting the frames skipped// update game statethis.gameView.update();
// render state to the screen draws the canvas on the panelthis.gameView.render(canvas);
// calculate how long did the cycle take
timeDiff = System.currentTimeMillis() - beginTime;
// calculate sleep time
sleepTime = (int)(FRAME_PERIOD - timeDiff);
if (sleepTime > 0) {
if sleepTime > 0//we're OKtry {
// send the thread to sleep for a short period// very useful for battery saving
Thread.sleep(sleepTime);
} catch (InterruptedException e) {}
}
while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
// we need to catch upthis.gameView.update(); // update without rendering
sleepTime += FRAME_PERIOD; // add frame period to check if in next frame
framesSkipped++;
}
}
} finally {
// in case of an exception the surface is not left in// an inconsistent stateif (canvas != null) {
surfaceHolder.unlockCanvasAndPost(canvas);
 }
 }  // end finally
 }
 }
}

MainActivity.java

package com.adc2017gmail.moonbase;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
publicclassMainActivityextendsActivity{
@OverrideprotectedvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ImageButton imgbtn8 = (ImageButton)findViewById(R.id.image_button8);
imgbtn8.setOnClickListener(new View.OnClickListener()
{
publicvoidonClick(View v){
imgbtn8.setImageResource(R.drawable.btn2);
Intent intent8 = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent8);
}
});
}
@OverridepublicbooleanonOptionsItemSelected(MenuItem item){
// Handle action bar item clicks here. The action bar will// automatically handle clicks on the Home/Up button, so long// as you specify a parent activity in AndroidManifest.xml.int id = item.getItemId();
//noinspection SimplifiableIfStatementif (id == R.id.action_settings) {
returntrue;
}
returnsuper.onOptionsItemSelected(item);
 }
 }

SecondActivity.java

package com.adc2017gmail.moonbase;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
publicclassSecondActivityextendsActivity{
@OverrideprotectedvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(new GameView(this));
 }
@OverridepublicbooleanonCreateOptionsMenu(Menu menu){
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_second, menu);
 returntrue;
 }
@OverridepublicbooleanonOptionsItemSelected(MenuItem item){
// Handle action bar item clicks here. The action bar will// automatically handle clicks on the Home/Up button, so long// as you specify a parent activity in AndroidManifest.xml.int id = item.getItemId();
 //noinspection SimplifiableIfStatementif (id == R.id.action_settings) {
returntrue;
 }
returnsuper.onOptionsItemSelected(item);
}
}

Also popular now: