For this part I'm going to change the dynamic of what we've been doing so far in the series... I'm going to explain what changes were done to the code overall, then show you the complete code for the modified classes and leave you with a link to download the source code so far, for commodity.
To make it easier for everyone, I left many comments on the code explaining the use of variables and functions, and many of the mechanics involved. If there are any doubts, you can contact me however you prefer and I'll help :)
What changed?
Well, we ended last part of the series with our hero moving from left to right over our map, but if you remember from part 1 our hero is not supposed to move, it's supposed to remain always at the centre of the screen, with the maps, NPCs and objects moving accordingly. So now our hero is "moving" (changing sprites in the direction we set it to face) while remaining an the centre. Also, the GameMap now holds the map square the hero is at, and its drawn according to it.
The most interesting part is the Touch Handling tho, the game no longer closes when the screen is touched (well, it does if the user touches the bottom part of it, which is now painted red to make it more obvious... Red -> DANGER! xD). Instead, we now let the GameMap class handle touches (it was all done by the GamePanel before), and a simple functionality was added: if the user touches on a map square (aka, touches any point inside the map) the hero is automatically transported there (we change the GameMap's x,y coordinates of the hero).
The code!
MainActivity.java
package ve.com.biocraft.biocraft; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.WindowManager; public class MainActivity extends AppCompatActivity { // Constant for logging private static final String TAG = MainActivity.class.getSimpleName(); private GamePanel gamePanel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Making App fullscreen getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // Set our GamePanel as the View gamePanel = new GamePanel(this); setContentView(gamePanel); Log.d(TAG, "onCreate: View added!"); } @Override protected void onDestroy() { Log.d(TAG, "onDestroy: Destroying!"); super.onDestroy(); } @Override protected void onStop() { Log.d(TAG, "onStop: Stopping!"); super.onStop(); } @Override protected void onPause() { Log.d(TAG, "onPause: Pausing!"); gamePanel.setRunningFalse(); super.onPause(); } }
GamePanel.java
package ve.com.biocraft.biocraft; import android.app.Activity; import android.content.Context; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; public class GamePanel extends SurfaceView implements SurfaceHolder.Callback { // Constant for logging private static final String TAG = GamePanel.class.getSimpleName(); // Size of the sprites to be drawn on screen private static final int SPRITE_SIZE = 64; private MainThread thread; private Hero hero; private GameMap gameMap; // Paint used to paint bottom of screen red private Paint paint = new Paint(); public GamePanel(Context context) { super(context); // Adding callback (this) to surface holder to catch events getHolder().addCallback(this); // Create our hero and load it's bitmap hero = new Hero(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), SPRITE_SIZE); // Create our game map, load bitmap, fill buffers gameMap = new GameMap(BitmapFactory.decodeResource(getResources(), R.drawable.tileset), new int[] {56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56, 56,56,56,20,19,56,56,56}, new int[] {49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0, 49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0, 49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0, 49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0, 49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0, 49,0,49,0,0,49,0,49, 0,0,0,0,0,0,0,0}, new int[] {33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41, 33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41, 33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41, 33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41, 33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41, 33,0,33,0,0,33,0,33, 41,0,41,0,0,41,0,41}, 8, SPRITE_SIZE, 3, 11); // Create the Game Loop (the thread) thread = new MainThread(getHolder(), this); // Make GamePanel able to focus so it can handle events setFocusable(true); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { // Check if its the first time the thread starts if (thread.getState() == Thread.State.NEW) { paint.setColor(Color.RED); hero.setX(getWidth()); hero.setY(getHeight()); hero.setDrawSquare(); gameMap.setDrawCoordinates(getWidth(), getHeight()); // When the surface is created we set the running flag to true thread.setRunning(true); // And we start the Game Loop thread.start(); } else if (thread.getState() == Thread.State.TERMINATED) { // Start the thread again after a pause thread = new MainThread(getHolder(), this); thread.setRunning(true); thread.start(); } } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { Log.d(TAG, "surfaceDestroyed: Surface is being destroyed!!!"); // Clean shutdown boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { // Try again to shut down the thread } } Log.d(TAG, "surfaceDestroyed: Thread was shut down cleanly."); } @Override public boolean onTouchEvent(MotionEvent event) { // Detect a touch event if (event.getAction() == MotionEvent.ACTION_DOWN) { // End game if lower part of screen (64px) is touched if (event.getY() > getHeight() - 64) { thread.setRunning(false); ((Activity) getContext()).finish(); } else { // Log touch coordinates Log.d(TAG, "onTouchEvent: Coordinate: x=" + event.getX() + ", y=" + event.getY()); // Let GameMap handle the touch event gameMap.handleActionDown((int) event.getX(), (int) event.getY()); } } return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { } public void render(Canvas canvas) { // Fill screen with black canvas.drawColor(Color.BLACK); // Draw background buffer gameMap.renderBottom(canvas); // Draw hero hero.draw(canvas); // Draw top buffer gameMap.renderTop(canvas); // Fill bottom 64px of screen with red (for closing game) canvas.drawRect(0, canvas.getHeight() - 64, canvas.getWidth(), canvas.getHeight(), paint); } public void setRunningFalse() { thread.setRunning(false); } public void update() { // Update hero hero.update(); // Update map gameMap.update(); } }
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; } // Thread constructor public MainThread(SurfaceHolder surfaceHolder, GamePanel gamePanel) { super(); this.surfaceHolder = surfaceHolder; this.gamePanel = gamePanel; } @Override public void run() { Canvas canvas; Log.d(TAG, "run: 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, "run: xception 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, "run: Frames skipped: " + framesSkipped); } } } }
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 square; // Size of screen square 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 // Hero constructor public Hero(Bitmap bitmap, int square) { this.bitmap = bitmap; this.square = square; this.direction = 0; this.sprite = 0; this.spriteHeight = bitmap.getHeight() / 4; // 4 directions this.spriteWidth = bitmap.getWidth() / 3; // 3 sprites per direction this.src = new Rect(0, 0, spriteWidth, spriteHeight); Log.d(TAG, "Hero: Hero created!"); } // Used to get the image our hero is using to draw itself (currently unused) public Bitmap getBitmap() { return bitmap; } // Used to change the image our hero is using to draw itself (currently unused) public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } // Used to get hero's y coordinate for drawing (currently unused) public int getX() { return x; } // Used to change hero's x coordinate for drawing public void setX(int x) { this.x = (x - square) / 2; } // Used to get hero's y coordinate for drawing (currently unused) public int getY() { return y; } // Used to change hero's y coordinate for drawing public void setY(int y) { this.y = (y - square) / 2; } // Used to get hero's facing direction (currently unused) public int getDirection() { return direction; } // Used to change hero's facing direction (currently unused) public void setDirection(int direction) { this.direction = direction; } // Sets the coordinates to draw our hero at // Needs to be called after our GamePanel is created public void setDrawSquare() { this.dst = new Rect(this.x, this.y, this.x + square, this.y + square); } // Draws the hero on screen public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, src, dst, null); } // Update hero state on each game loop public void update() { /* * At the moment, this changes the hero's sprite to be drawn */ sprite++; src.left += spriteWidth; src.right += spriteWidth; if (sprite > 2) { sprite = 0; src.left = 0; src.right = spriteWidth; } } }
GameMap.java
package ve.com.biocraft.biocraft; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.util.Log; public class GameMap { // Constant for logging private static final String TAG = GameMap.class.getSimpleName(); // Number of sprites per row of our bitmap // Depends on the tileset you're using private static final int TILESET_COLS = 8; private Bitmap bitmap; // The image of our map's tiles private int[] terrain; // Array of terrain for background buffer private int[] object; // Array of objects for background buffer (hero can't move through this) private int[] roof; // Array of elements for the top buffer private int width; // Numbers of squares in a map row private int square; // Screen square size private Bitmap buffer; // Buffer for background elements private Bitmap roofBuffer; // Buffer for top elements (covers bottom, hero, NPCs, etc) private int x; // Hero's x coordinate private int y; // Hero's y coordinate private int drawX; // x coordinate to draw map private int drawY; // y coordinate to draw map // GameMap constructor public GameMap(Bitmap bitmap, int[] terrain, int[] object, int[] roof, int width, int square, int x, int y) { this.bitmap = bitmap; this.terrain = terrain; this.object = object; this.roof = roof; this.width = width; this.square = square; this.x = x; this.y = y; fillBuffers(); Log.d(TAG, "GameMap: Map ready!"); } private void fillBuffers() { // We create 2 image buffers to be drawn on each render // One for background elements (buffer) // Another for top elements (roofBuffer) buffer = Bitmap.createBitmap(width * square, (terrain.length / width) * square, Bitmap.Config.ARGB_8888); roofBuffer = Bitmap.createBitmap(width * square, (terrain.length / width) * square, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(buffer); Canvas roofCanvas = new Canvas(roofBuffer); // Draw each array on the corresponding buffer // Easy to expand if there's need to add more layers draw(terrain, canvas); draw(object, canvas); draw(roof, roofCanvas); } // Draws a layer on a buffer's canvas private void draw(int[] ints, Canvas canvas) { // Draw an array of sprite ids into a canvas int spriteSize = (bitmap.getWidth() / TILESET_COLS); for (int i = 0; i < (ints.length / width); i++) { for (int j = 0; j < width; j++) { int sprite = ints[(i * width) + j] - 1; if (sprite >= 0) { Rect src = new Rect((sprite % TILESET_COLS) * spriteSize, (int) Math.floor(sprite / TILESET_COLS) * spriteSize, (sprite % TILESET_COLS) * spriteSize + spriteSize, (int) Math.floor(sprite / TILESET_COLS) * spriteSize + spriteSize); Rect dst = new Rect(j * square, i * square, (j + 1) * square, (i + 1) * square); canvas.drawBitmap(bitmap, src, dst, null); } } } } // Draws the background buffer onto the screen public void renderBottom(Canvas canvas) { canvas.drawBitmap(buffer, drawX, drawY, null); } // Draws the top buffer onto the screen public void renderTop(Canvas canvas) { canvas.drawBitmap(roofBuffer, drawX, drawY, null); } // Update map state on each game loop public void update() { } // Sets the initial coordinates to draw our map at // Needs to be called after our GamePanel is created public void setDrawCoordinates(int drawX, int drawY) { this.drawX = (drawX / 2) - (square / 2) - (x * square); this.drawY = (drawY / 2) - (square / 2) - (y * square); } // Lets map handle touch actions public void handleActionDown(int eventX, int eventY) { /* * At the moment, this moves our hero's position * to the touched map square (if any) */ eventX = (int) Math.floor((eventX - drawX) / square); eventY = (int) Math.floor((eventY - drawY) / square); Log.d(TAG, "handleActionDown: Square: x=" + eventX + ", y=" + eventY); if ((eventX >= 0) && (eventY >= 0) && (eventX < width) && (eventY < terrain.length / width)) { drawX += (square * (x - eventX)); drawY += (square * (y - eventY)); x = eventX; y = eventY; } else { Log.d(TAG, "handleActionDown: Click out of map!"); } } }
Source from this post can be downloaded here!
Hope you guys enjoyed this part of the series, in the next part we'll be finally getting our hands dirty with pathfinding! See you, take care, and share! :)
No comments:
Post a Comment
Got something to say? Speak your mind!