In my nature I like to go straight to the core of the software I’m doing, post poning other less important aspects to a later stage of the development.
In the game development, the core is the Game Activity, the view that contains the running game itself. Let’s forget for now the main menu, options views, leaderboards and other tasks.
Basically the Game Activity is just a container for the Game View, a custom control which will contain the graphics of the game. So the class itself will be empty, while the layout code will be something like:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <com.nex.andro.game.GameView android:id="@+id/gameView" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </RelativeLayout>
The GameView control will face as the owner of two important objects:
– The Game Thread, running the main loop, handling the running, stopped and paused states of the game
– The Game Engine class, an object handling everything that happens during the game
To understand what is the main loop of a game, please follow these links which I found useful:
– Link 1
– Link 2
An important thing to keep in mind, while preparing the main loop, is that it is stricly related to the maximum number of loops per second you can achieve. This number depends on many factors, for example the hardware running the game (CPU, Screen resolution, …) and the quantity and quality of the graphics (more and detailed graphics require more time to be drawn).
In my case, I had no heavy graphics so I concentrated on limiting the number of loops, to avoid a game too fast and unusable. There are plenty of solutions around, but in my opinion sometimes the simplest way is the solution.
So let’s say I want a maximum of 15 loops per second, a value that makes the game fast enough for my taste. First we calculate the minimum time for a single loop:
int minLoopTime = 1000 / limitFps;
This gives me about 66 milliseconds to paint all the graphics.. plenty of time.
Then at the end of each loop, I will measure the time elapsed, and make the thread sleep for the remaining time:
long sleepTime = minLoopTime - (endTime - startTime); if (sleepTime > 0) { Thread.sleep(sleepTime); }
Simple, right?
And now, the final code of the GameView class (not perfect, but a good starting point)
public class GameView extends SurfaceView implements SurfaceHolder.Callback { class GameThread extends Thread { private SurfaceHolder mSurfaceHolder; private Handler handler; private Context context; private GameEngine gameEngine; private boolean running; private boolean paused; private int startingLevel; public GameThread(SurfaceHolder surfaceHolder, Context context, GameEngine gameEngine, Handler handler) { this.mSurfaceHolder = surfaceHolder; this.handler = handler; this.context = context; this.gameEngine = gameEngine; } public void setStartingLevel(int startingLevel) { this.startingLevel = startingLevel; } public boolean isRunning() { return running; } public void setRunning(boolean running) { this.running = running; } public boolean isPaused() { return paused; } public void setPaused(boolean paused) { this.paused = paused; } @Override public void run() { try { long startTime = 0; long endTime = 0; int limitFps = AndConsts.LIMIT_FPS; int minLoopTime = 1000 / limitFps; // Initialise level if (startingLevel <= 0) { startingLevel = 1; } gameEngine.initLevel(startingLevel); boolean doPause = true; boolean doResume = false; while (running) { startTime = System.currentTimeMillis(); Canvas c = null; try { if (!paused) { c = mSurfaceHolder.lockCanvas(null); synchronized (mSurfaceHolder) { if (doResume) { doResume = false; doPause = true; gameEngine.resume(); } // Update Input gameEngine.updateInput(); // Update physics gameEngine.updatePlayerPhysics(); // Update AI gameEngine.updateAIPhysics(); // Update AI gameEngine.updatePhysicsChecks(); // Update game UI gameEngine.updateUI(c); // Update sounds gameEngine.updateSound(); // Finalize loop gameEngine.finalizeLoop(); } } else { if (doPause) { doResume = true; doPause = false; gameEngine.pause(); // Draw an opacity layer to make the user feel the game is paused c = mSurfaceHolder.lockCanvas(null); synchronized (mSurfaceHolder) { Paint p = new Paint(); p.setStyle(Paint.Style.FILL); p.setColor(Color.BLACK); p.setAlpha(125); c.drawRect(0, 0, ScreenManager.screenWidthPx, ScreenManager.screenHeightPx, p); p = null; } } } } catch (Exception ex) { // TODO Handle errors running = false; } finally { // do this in a finally so that if an exception is thrown // during the above, we don't leave the Surface in an // inconsistent state if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } endTime = System.currentTimeMillis(); try { long sleepTime = minLoopTime - (endTime - startTime); if (sleepTime > 0) { Thread.sleep(sleepTime); } } catch (InterruptedException e) { // TODO Handle errors } } } catch (Exception ex) { // TODO Handle errors } } } private GameEngine gameEngine; private GameThread thread; public GameView(Context context, AttributeSet attrs) { super(context, attrs); gameEngine = new GameEngine((Activity) context); try { // Load game gameEngine.initGame(this); // Register our interest in hearing about changes to our surface SurfaceHolder holder = getHolder(); holder.addCallback(this); thread = new GameThread(holder, context, gameEngine, new Handler() { @Override public void handleMessage(Message m) { //Do nothing for now } }); setFocusable(true); // Make sure we get key events setFocusableInTouchMode(true);// Make sure we get key events } catch (AbnormalException e) { //TODO Handle errors } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { stop(); } public void stop() { // Stop the gaming thread thread.setRunning(false); try { thread.join(500); } catch (InterruptedException e) { } } public void pause() { thread.setPaused(true); } public void resume() { thread.setPaused(false); } public void start() { thread.setRunning(true); thread.start(); } public void restartLevel() { gameEngine.restartLevel(); } }
3 Comments
[…] #3 Game Activity, Game View, The Main Loop […]
Thanks for the info.
Thank you for this beautiful tutorial….However the GameEngine is not available…..I will like to see the implementation of the GameEngine class