MainThread.java
package ve.com.biocraft.biocraft; import android.graphics.Canvas; import android.util.Log; import android.view.SurfaceHolder; public class MainThread extends Thread { // Constant for logging private static final String TAG = MainThread.class.getSimpleName(); // Frame Period private static final long FRAME_PERIOD = 1000 / 25; private SurfaceHolder surfaceHolder; private GamePanel gamePanel; // This is the flag to check if the game is running private boolean running; // This is the void that sets the flag to either true or false public void setRunning(boolean running) { this.running = running; } public MainThread(SurfaceHolder surfaceHolder, GamePanel gamePanel) { super(); this.surfaceHolder = surfaceHolder; this.gamePanel = gamePanel; } @Override public void run() { Canvas canvas; Log.d(TAG, "Starting Game Loop"); long loopTime = FRAME_PERIOD; long beginTime; long sleepTime; int framesSkipped; while (running) { beginTime = System.currentTimeMillis(); // Update Game State this.gamePanel.update(); canvas = null; // Try to lock the canvas for pixel editing try { canvas = this.surfaceHolder.lockCanvas(); synchronized (surfaceHolder) { // Render this.gamePanel.render(canvas); } } finally { // Catching exceptions if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } // loopTime is the amount of ms we want each frame to take sleepTime = loopTime - System.currentTimeMillis() + beginTime; // If we got some time to sleep, everything is ok if (sleepTime >= 0) { // Set loopTime to expected Frame Period loopTime = FRAME_PERIOD; // Send the thread to sleep for a short period // Good for battery saving try { Thread.sleep(sleepTime); } catch (InterruptedException e) { Log.d(TAG, "Exception when attempting to sleep... :("); } } else { // If we're over our frame period we need to catch up // We'll update Game State without rendering // Until we get to a loopTime of, at least, the Frame Period framesSkipped = 0; loopTime = FRAME_PERIOD + sleepTime; while (loopTime < FRAME_PERIOD) { beginTime = System.currentTimeMillis(); this.gamePanel.update(); framesSkipped++; loopTime += FRAME_PERIOD - System.currentTimeMillis() + beginTime; } Log.d(TAG, "Frames skipped: " + framesSkipped); } } } }
Ok. Let's check the changes... First, we added a constant long called FRAME_PERIOD that we set up on 1000/25 (1000 ms, aka 1 second, divided by 25, the amount of FPS we want, the result of which is 40ms), that's where we will store the expected amount of time each loop will take in milliseconds. Mainly used for comparisons, useful if we ever want to change the FPS since we will only need to change it's value.
The rest of the changes were on the run() method. We added some variables: loopTime will tell how much time a loop will have to complete (no, it will not always be 40ms, more on this later), and framesSkipped will count how many renders we miss due to a loop taking longer than expected.
The first part of the code, up to when we have some free time to sleep, remains the same. But now we will try to catch up if we fall behind. How? Skipping renders. Since our game speed depends on the number of times update() is called, we must call that method without rendering or we will make the game run slower. Our priority will always be to call update() those 25 times per second, it might not look nice if we sacrifice too many renders, but at least our Game State will be always updated and the game won't go slower, if only it'll look laggy.
The catch is, after calling a couple updates one after the other, we need to make sure we don't go ahead of the expected loop time, that's why, after a catch up, our time frame will be over 40ms.
Let's try with an example: Each loop should take 40ms, so we have 25 FPS at the end.
- First loop takes 30ms, we sleep 10ms, all good.
- Second loop takes 50ms, we're behind 10ms!
- Our third loop only has 30ms to complete! We'll tell it to skip the render() method to try to catch up. We update our Game State, it took 10ms, we still got 20ms left of our 3rd loop. We won't render this time, but we shouldn't sleep either. Instead, 3rd loop will give it's remaining 20ms to the 4th loop.
- 4th loop (update and render) starts with a time frame of 60ms (the usual 40, plus 20 that the 3rd loop had extra).
As it stands, we got a pretty solid Game Loop that will do it's best to stay at our set FPS, this will allow us to have a smooth game that doesn't run slow nor too fast.
Now let's make an eye-candy change to our hero, instead of displaying a whole picture, we'll draw an animation. Since it's moving from top to bottom, we'll use the top line of sprites (using the little guy from Part 4) and it'll look like it's walking, desperately fast at 25 pixels (and about as many renders) per second xD
Hero.java
package ve.com.biocraft.biocraft; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.util.Log; public class Hero { // Constant for logging private static String TAG = Hero.class.getSimpleName(); private Bitmap bitmap; // The image of our character sprites private int x; // The x coordinate of our character private int y; // The y coordinate of our character private int direction; // The direction the character is facing (0 down, 1 left, 2 right, 3 up. The order of our sprite image) private int sprite; // The animation sprite, since our characters have 3 sprites per animation, this ranges from 0 to 2 private int spriteHeight; // Size (in pixels) of a single sprite height private int spriteWidth; // Size (in pixels) of a single sprite width private Rect src; // Square of the image to be drawn private Rect dst; // Square of the screen to draw unto public Hero(Bitmap bitmap, int x, int y, int direction){ this.bitmap = bitmap; this.x = x; this.y = y; this.direction = direction; this.sprite = 0; this.spriteHeight = bitmap.getHeight() / 4; // 4 directions this.spriteWidth = bitmap.getWidth() / 3; // 3 sprites per direction this.src = new Rect(0, direction * spriteHeight, spriteWidth, (direction + 1) * spriteHeight); this.dst = new Rect(x, y, x + spriteWidth, y + spriteHeight); Log.d(TAG, "Hero created!"); } public Bitmap getBitmap() { return bitmap; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getDirection() { return direction; } public void setDirection(int direction) { this.direction = direction; } public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, src, dst, null); } public void update() { y++; dst.top = y; dst.bottom = y + spriteHeight; sprite++; src.left += spriteWidth; src.right += spriteWidth; if (sprite > 2) { sprite = 0; src.left = 0; src.right = spriteWidth; } } }
Well, we added a lot of variables here, they're all to be used for rendering. Each one has a comment next to it that explains what it does. We updated our draw(Canvas canvas) and update() methods to reflect the changes on what we want to draw now. Basically, we're choosing a rectangle of our image (a sprite) and painting it on a rectangle from our canvas (the drawing space), those rectangles are of the same area now, but they don't have to be. When updating, we make sure to change the coordinates of the rectangles to pick the next sprite and to keep moving our hero to the bottom.
We did a small change to the class constructor here (so we give it the direction the hero is facing), which means we have to change a line in our GamePanel's GamePanel(Context context) method (the constructor).
GamePanel.java
// Create our hero and load it's bitmap hero = new Hero(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), 0, 0, 0);
That's it! Run the game now and check out the changes! Try setting a different FRAME_PERIOD and you'll see how the game goes faster or slower :)
Hope you guys enjoyed it, please share the posts and follow me on my social networks, I appreciate it :)
Take care!!!
No comments:
Post a Comment
Got something to say? Speak your mind!