📜 ⬆️ ⬇️

Game cycle

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

It may happen that on some tablets or mobile phones our application will “fly”, so the user will not even have time to play, while on others it will slow down so that the user will most likely remove it from his device. There is a thought to go through one game cycle in 0.04 seconds (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, generating new objects, for example, an explosion in a collision. You also need to remember to play sounds and respond to the inclusion of the user in the game. I'm not talking about realistic graphics of the world.

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 discussed below.
')

Create a game loop


The basic idea is that if the real frame drawing time is longer than the estimated frame update time (that is, the system does not have time), we sacrifice this frame and increase the time to draw 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 speed of the program, is specified, 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 about 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 the tablet with android 4.2.2 everything worked perfectly, but on the phone there were sometimes noticeable frame drops, especially at high speed of the object.

Consider in more detail our code.

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

public class MainThread extends Thread { 

 public class MainThread extends Thread { 

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

  private final static int MAX_FPS = 30; // desired fps 

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

  // maximum number of frames to be skipped 

  private final static int MAX_FRAME_SKIPS = 4; 

Calculate in milliseconds the duration of the frame display.

  private final static int 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 

Declare a game state variable, if it is true, then the stream is played.

  private boolean running; // flag to hold game state public void setRunning(boolean running) { this.running = running; } 

Create a class constructor.

 public MainThread(SurfaceHolder surfaceHolder, GameView gameView) { super(); this.surfaceHolder = surfaceHolder; this.gameView = gameView; } @ Override public void run() { Canvas canvas;</code> long beginTime; // the time when the cycle begun long timeDiff; // the time it took for the cycle to execute int 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 surface try { 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 state this.gameView.update(); // render state to the screen draws the canvas on the panel this.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 OK try {</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 up this.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 state if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } // end finally } } } 

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

 public class GameView extends SurfaceView implements SurfaceHolder.Callback { private final Drawable mAsteroid; private int widthAsteroid; private int heightAsteroid; private int leftAsteroid; private int xAsteroid1 = 30; private int rightAsteroid; private int topAsteroid; private int yAsteroid = -30; private int bottomAsteroid; private int centerAsteroid; private int height; private int width; private int speedAsteroid = 5; private int xAsteroid; private MainThread thread; public GameView(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 drawing surface, for example, when you rotate the screen.

 @Override public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { } 

The method creates a surface for drawing.

 @Override public void surfaceCreated(SurfaceHolder holder) { thread.setRunning(true); thread.start(); } 

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

 @Override public void surfaceDestroyed(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.

 public void render(Canvas canvas) { 

Create a dark blue background.

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

We find out the height and width of the screen of the device, to then scale the size of the 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 image). The left edge is tied to the coordinate on X, the top of the picture is tied to the coordinate on Y. Thus, we determined the coordinates of the upper left corner of the picture. When we change these coordinates, the picture will start moving. Do not forget to put the asteroid in the png format in the drawable folder.

 //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 coordinate at the top left corner of the picture has become larger than the height of the screen, then we reset U and the asteroid appears abruptly at the top of the screen. For the asteroid to fly down, we add each game cycle to the coordinate number speedAsteroid, which in turn is determined randomly from 5 to 15 (speedAsteroid = 5+ rnd.nextInt (10);).

 public void update() { 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; } } } 

To ensure that the asteroid does not fly out from the same place each time, the X coordinate is determined randomly within the width of the screen minus the width of the asteroid. At the moment of launching the application, the asteroid crashes with the initial coordinates that we set when declaring variables private int xAsteroid = 30; private int yAsteroid = -30; In the future, the flight speed and the initial coordinate vary randomly.

Application files at the moment


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; public class GameView extends SurfaceView implements SurfaceHolder.Callback { private final Drawable mAsteroid; private int widthAsteroid; private int heightAsteroid; private int leftAsteroid; private int rightAsteroid; private int topAsteroid; private int yAsteroid = -30; private int bottomAsteroid; private int centerAsteroid; private int height; private int width; private int speedAsteroid = 5; private int xAsteroid = 30; private MainThread thread; public GameView(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); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { thread.setRunning(true); thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { thread.setRunning(false); boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { // try again shutting down the thread } } } public void render(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); } public void update() { 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; public class MainThread extends Thread { private final static int MAX_FPS = 30;// desired fps private final static int MAX_FRAME_SKIPS = 4;// maximum number of frames to be skipped private final static int FRAME_PERIOD = 1000 / MAX_FPS; // the frame period // Surface holder that can access the physical surface private SurfaceHolder surfaceHolder; // The actual view that handles inputs // and draws to the surface private GameView gameView; // flag to hold game state private boolean running; public void setRunning(boolean running) { this.running = running; } public MainThread(SurfaceHolder surfaceHolder, GameView gameView) { super(); this.surfaceHolder = surfaceHolder; this.gameView = gameView; } @Override public void run() { Canvas canvas; long beginTime;// the time when the cycle begun long 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 surface try { 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 state this.gameView.update(); // render state to the screen draws the canvas on the panel this.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 OK try { // 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 up this.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 state if (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; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ImageButton imgbtn8 = (ImageButton)findViewById(R.id.image_button8); imgbtn8.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { imgbtn8.setImageResource(R.drawable.btn2); Intent intent8 = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent8); } }); } @Override public boolean onOptionsItemSelected(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 SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } } 

SecondActivity.java

 package com.adc2017gmail.moonbase; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; public class SecondActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new GameView(this)); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_second, menu); return true; } @Override public boolean onOptionsItemSelected(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 SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } } 

Source: https://habr.com/ru/post/271315/


All Articles