This tutorial game project introduces the concept of a viewport. This is the aspect of our game which handles which part of the game-world is drawn to the screen. First, we need to decide what to draw and then we must convert their “real-world” coordinates to the screen coordinates at which to draw them. If we accept that most games we make will have areas which cannot possibly be squeezed onto the player’s phone or tablet, in one go, then a viewport is essential. If you write games using OpenGL then this viewport idea is handled for you by OpenGL. However, the code for implementing a simple viewport is much simpler than handling the additional overhead required to get started with OpenGL. Therefore, it is often worthwhile building your own viewport.
Before we get started with the project let’s dig a little deeper into this viewport idea.
What exactly is a viewport?
You can think of a viewport more intuitively if you think about it as a camera that moves to keep the most relevant part of the game world in focus. The most relevant part, in most games, is the area around the player. Therefore, most viewports track the player as its center. This is what we will do in this project. It is more than this, however, because it can also be used to clip, and remove from processing, certain game objects that we deem unnecessary to update in a given frame. This is something OpenGL doesn’t do.
About the game
The game will be fully runnable and playable code but won’t be a fully-structured game. You can rotate and thrust your spaceship above and around a randomly generated city full of skyscrapers. You can shoot your rapid-fire gun at the skyscrapers and destroy/set fire to them. There isn’t any objective to the game or menu screen. The code was too long and sprawling to add all these features. If you have read the other Android game projects, you can easily add your own enemies etc.
The game world will be “fenced in” by a crude barrier because I couldn’t think of anything more exciting that is also very simple. The game won’t actually have any enemies or score etc, just the scrolling world, burning buildings etc. The main point was to demonstrate the viewport.
Just for fun, we will construct all the buildings with tiny “bricks”. This will demonstrate two things. Firstly, the viewport implementation can not only handle hundreds of objects but, actually, is more efficient the higher the number of game objects. The fun comes because we can destroy and set fire to, individual sections of our buildings. This will allow the player’s spaceship to shoot and even fly through damaged buildings. In this project, we will, however, see that drawing and processing will eventually begin to slow down our game loop. We will talk about solutions when we have built the game.
Starting and planning a 2D scrolling shooter project
Create a new project in Android Studio, use the Empty Activity template, and call it Scrolling Shooter 2D. Leave the rest of the settings at their defaults.
First I will show you the Viewport class and go into some detail because that is the class that does all the cool stuff.
Next, we will quickly see the code for all the game object classes (Ship, Bullet, Brick, and Star)so you can copy & paste it. I won’t go into much depth because there is little new from previous projects.
Then we will quickly code MainActivity. The last thing we will code is the GameView class which draws, updates, and uses the Viewport. Much of this code I will just leave comments to remind you what it does but I will go into the ins and outs of the control buttons(HUD) and, of course, the viewport bits.
Finally, in this article, we will discuss more exciting things you can do with a viewport and take a look at what would be a good way to develop your Android game development from here.
Coding the Viewport class
As we have already discussed, a viewport can be thought of as the movie camera that follows the action of our game. It defines the area of the game world that is to be shown to the player. Ours will center on the triangle-shaped spaceship.
It also serves the combined function of making our update method more efficient by flagging objects outside of the player’s field of vision(the viewport). There is no point in processing a mountain of Brick instances in each frame if they are not relevant in that frame. Of course, if your game wants the player to shoot and destroy things that are off-screen then you have to make a tactical decision about what to exclude. The point is, the viewport gives us the choice. This will significantly speed up tasks like collision detection by implementing the first phase of detection by removing objects off-screen from the list of objects to check for collisions.
[widgets_on_pages id=”udemy_advert_java_2″]
Furthermore, our
Viewport class will have the task of translating game world coordinates into appropriate pixel coordinates for drawing on the screen. The Viewport class really is an all-singing and dancing thing. So let’s get coding.
First, we will declare a whole bunch of useful variables. We have a PointF which will just be used to represent whatever point in the world is currently the central focus in the viewport. Then we have separate int values for pixelsPerMetreX and pixelsPerMetreY.
This point is key! Every game object will have dimensions and a position. Unlike every other Android project(on this site) that came before this one, these dimensions and positions no longer relate to a coordinate on the screen but instead, now relate to a coordinate in the world! That is why we have all these variables with names containing ...metres... etc. Of course, if you sit back and work out the size in meters of any of the game objects, they are tiny compared to say a real skyscraper. You could change the units and therefore the variable names to something like pixelsPerWorldUnit,. I thought meters made the distinction between the game world we are simulating and the screen’s pixels more clear and distinct.
In addition, we also have the resolution of the screen in both axes. The variables  screenXResolution and screenYResolution are passed in when the constructor is called. We then have screenCentreX and screenCentreY which are basically the two previous variables divided by 2 to find the middle. Next in our list of declared variables we have metresToShowX and metresToShowY which will be the number of meters we will squash into our viewport. Changing these values will show more or less of the game world on screen.
Create a new class called Viewport and declare the variables and code the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package com.gamecodeschool.scrollingshooter2d; import android.graphics.PointF; import android.graphics.RectF; public class Viewport { private PointF currentViewportWorldCentre; private RectF convertedRect; private PointF convertedPoint; private int pixelsPerMetreX; private int pixelsPerMetreY; private int screenCentreX; private int screenCentreY; private int metresToShowX; private int metresToShowY; // All the rest of the Viewport code goes here Viewport(int screenXResolution, int screenYResolution){ screenCentreX = screenXResolution / 2; screenCentreY = screenYResolution / 2; pixelsPerMetreX = screenXResolution / 90; pixelsPerMetreY = screenYResolution / 55; metresToShowX = 92; metresToShowY = 57; convertedRect = new RectF(); convertedPoint = new PointF(); currentViewportWorldCentre = new PointF(); // End of viewport class } |
Note that in a more advanced implementation, you could dynamically choose metresToShow... values based on things like resolution, screen size, and the ratio of width to height of the screen. In addition, the amount you divide screenResolutionX and ...Y by determining the number of virtual meters displayed on the screen. Adjusting this to get good results on your screen might be necessary and setting them up dynamically based on the specifications of the screen would be appropriate in a real game.
Next, we have a simple method
setWorldCentre that does as the name suggests. The value held in this
PointF each frame will be significant in every single calculation the
Viewport class makes. Our game loop will call each and every frame(at the beginning) based on the position of the spaceship. Add the
setWorldCentre method.
[widgets_on_pages id=”udemy_advert_java_3″][widgets_on_pages id=”udemy_code_details”]
1 2 3 4 |
void setWorldCentre(float x, float y){ currentViewportWorldCentre.x = x; currentViewportWorldCentre.y = y; } |
And now we fulfill one of the primary roles of the Viewport class with the worldToScreen method. As the name suggests this is the method that converts the locations of all the objects currently in the visible viewport from world coordinates to pixel coordinates that can actually be drawn to the screen. It returns our previously prepared rectToDraw object as the result.
This is how worldToScreen works. It receives the horizontal and vertical world locations of an object along with that object’s width and height. With these values, each in turn, it subtracts the objects world coordinate multiplied by the pixels per meter for the current screen from the appropriate current world viewport center (x or y) . Then, for the left and top coordinates of the object, the result is subtracted from the pixel screen center value and for the bottom and right coordinates it is added. These values are then packed into the correct variable ( left, top, right and bottom ) of convertedRect and returned to the draw method of GameView. This simple calculation gives the screen pixel coordinates to draw the object.
Note that the code has not only calculated the screen coordinates but has also scaled the object in size(up or down as required).
Add the worldToScreen method to the Viewport class.
1 2 3 4 5 6 7 8 |
public RectF worldToScreen(float objectX, float objectY, float objectWidth, float objectHeight){ int left = (int) (screenCentreX - ((currentViewportWorldCentre.x - objectX) * pixelsPerMetreX)); int top = (int) (screenCentreY - ((currentViewportWorldCentre.y - objectY) * pixelsPerMetreY)); int right = (int) (left + (objectWidth * pixelsPerMetreX)); int bottom = (int) (top + (objectHeight * pixelsPerMetreY)); convertedRect.set(left, top, right, bottom); return convertedRect; } |
This next method does exactly the same as the last but with a single point. The spaceship points will be converted using this method. If you’re wondering how the scaling would work on a single point then consider that we will be drawing lines between the three points in the draw method. Therefore, just by moving the points into position the scaling has occurred.
1 2 3 4 5 6 7 8 |
public PointF worldToScreenPoint(float objectX, float objectY){ int left = (int) (screenCentreX - ((currentViewportWorldCentre.x - objectX) * pixelsPerMetreX)); int top = (int) (screenCentreY - ((currentViewportWorldCentre.y - objectY) * pixelsPerMetreY)); convertedPoint.x = left; convertedPoint.y = top; return convertedPoint; } |
This next method examines each point of a rectangle (in our case a Brick instance) and if its world position is far enough away (in metres, not pixels) from the centre of the viewport true is returned and the object being acted upon will clip itself internally, thereby excluding itself from the next round of update calls. We will see the simple way this occurs when we code the game loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public boolean clipObjects(float objectX, float objectY, float objectWidth, float objectHeight) { boolean clipped = true; if (objectX - objectWidth < currentViewportWorldCentre.x + (metresToShowX / 2)) { if (objectX + objectWidth > currentViewportWorldCentre.x - (metresToShowX / 2)) { if (objectY - objectHeight < currentViewportWorldCentre.y + (metresToShowY / 2)) { if (objectY + objectHeight > currentViewportWorldCentre.y - (metresToShowY / 2)){ clipped = false; } } } } return clipped; } |
More fun than coding the class is seeing it in action.
Coding the game object classes
These next classes are only described briefly because they are well commented and if you have completed the prerequisite projects and tutorials you will already understand much of the code.
Ship
Create a new class called Ship and add the following code. Be sure to read the comments as the code is not discussed. Almost the exact same code is fully explained in the series on calculating heading and the project, Android rotation and heading demo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
package com.gamecodeschool.scrollingshooter2d; import android.content.Context; import android.graphics.PointF; public class Ship { PointF a; PointF b; PointF c; PointF centre; /* Which way is the ship facing Straight up to start with */ float facingAngle = 270; // How long will our spaceship be //private float length; //private float width; // This will hold the pixels per second speed that the ship can move at private float speed = 0; //private final float MAX_SPEED = 80; //private final float ACCELERATION_RATE = 40; //private final float BREAK_RATE = 30; /* These next two variables control the actual movement rate per frame their values are set each frame based on speed and heading */ private float horizontalVelocity; private float verticalVelocity; /* How fast does the ship rotate? 1 complete circle per second */ //private final float ROTATION_SPEED = 200; // Which ways can the ship move public final int STOPPING = 0; public final int LEFT = 1; public final int RIGHT = 2; public final int THRUSTING = 3; // Is the ship moving and in which direction private int shipMoving = STOPPING; /* This the the constructor method When we create an object from this class we will pass in the screen width and height */ //public Ship(Context context, int screenX, int screenY){ public Ship(){ //length = screenX / 25; //width = screenY / 25; float length = 2.5f; float width = 1.25f; a = new PointF(); b = new PointF(); c = new PointF(); centre = new PointF(); centre.x = 50; centre.y = 50; a.x = centre.x; a.y = centre.y - length / 2; b.x = centre.x - width / 2; b.y = centre.y + length / 2; c.x = centre.x + width / 2; c.y = centre.y + length / 2; } public PointF getCentre(){ return centre; } public PointF getA(){ return a; } public PointF getB(){ return b; } public PointF getC(){ return c; } float getFacingAngle(){ return facingAngle; } public void bump(){ speed = 0; // Move back centre.x = centre.x - horizontalVelocity * 2; centre.y = centre.y - verticalVelocity * 2; a.x = a.x - horizontalVelocity * 2; a.y = a.y - verticalVelocity * 2; b.x = b.x - horizontalVelocity * 2; b.y = b.y - verticalVelocity * 2; c.x = c.x - horizontalVelocity * 2; c.y = c.y - verticalVelocity * 2; } /* This method will be used to change/set if the ship is rotating left, right or thrusting */ public void setMovementState(int state){ shipMoving = state; } /* This update method will be called from update in HeadingAndRotationView It determines if the player ship needs to move and changes the coordinates and rotation when necessary. */ public void update(long fps){ final float ROTATION_SPEED = 200; final float BREAK_RATE = 30; /* Where are we facing at the moment Then when we rotate we can work out by how much */ float previousFA = facingAngle; if(shipMoving == LEFT){ facingAngle = facingAngle - ROTATION_SPEED / fps; if(facingAngle < 1){ facingAngle = 360; } } if(shipMoving == RIGHT){ facingAngle = facingAngle + ROTATION_SPEED / fps; if(facingAngle > 360){ facingAngle = 1; } } if(shipMoving == THRUSTING){ final float MAX_SPEED = 80; final float ACCELERATION_RATE = 40; /* facingAngle can be any angle between 1 and 360 degrees the Math.toRadians method simply converts the more conventional degree measurements to radians which are required by the cos and sin methods. */ if(speed < MAX_SPEED){ speed = speed + ACCELERATION_RATE / fps; } horizontalVelocity = (float)(Math.cos(Math.toRadians(facingAngle))); verticalVelocity = (float)(Math.sin(Math.toRadians(facingAngle))); } centre.x = centre.x + horizontalVelocity * speed / fps; centre.y = centre.y + verticalVelocity * speed / fps; a.x = a.x + horizontalVelocity * speed / fps; a.y = a.y + verticalVelocity * speed / fps; b.x = b.x + horizontalVelocity * speed / fps; b.y = b.y + verticalVelocity * speed / fps; c.x = c.x + horizontalVelocity * speed / fps; c.y = c.y + verticalVelocity * speed / fps; if(shipMoving != THRUSTING){ if(speed > 0) { speed = speed - (BREAK_RATE / fps); } } /* Now rotate each of the three points by the change in rotation this frame facingAngle - previousFA */ float tempX; float tempY; // rotate point a a.x = a.x - centre.x; a.y = a.y - centre.y; tempX = (float)(a.x * Math.cos(Math.toRadians(facingAngle - previousFA)) - a.y * Math.sin(Math.toRadians(facingAngle - previousFA))); tempY = (float)(a.x * Math.sin(Math.toRadians(facingAngle - previousFA)) + a.y * Math.cos(Math.toRadians(facingAngle - previousFA))); a.x = tempX + centre.x; a.y = tempY + centre.y; // rotate point b b.x = b.x - centre.x; b.y = b.y - centre.y; tempX = (float)(b.x * Math.cos(Math.toRadians(facingAngle - previousFA)) - b.y * Math.sin(Math.toRadians(facingAngle - previousFA))); tempY = (float)(b.x * Math.sin(Math.toRadians(facingAngle - previousFA)) + b.y * Math.cos(Math.toRadians(facingAngle - previousFA))); b.x = tempX + centre.x; b.y = tempY + centre.y; // rotate point c c.x = c.x - centre.x; c.y = c.y - centre.y; tempX = (float)(c.x * Math.cos(Math.toRadians(facingAngle - previousFA)) - c.y * Math.sin(Math.toRadians(facingAngle - previousFA))); tempY = (float)(c.x * Math.sin(Math.toRadians(facingAngle - previousFA)) + c.y * Math.cos(Math.toRadians(facingAngle - previousFA))); c.x = tempX + centre.x; c.y = tempY + centre.y; } } |
Bullet
Create a new class called Bullet and add the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
package com.gamecodeschool.scrollingshooter2d; import android.graphics.PointF; public class Bullet { private PointF point; /* These next two variables control the actual movement rate per frame their values are set each frame based on speed and heading */ private float horizontalVelocity; private float verticalVelocity; float speed = 60; // Is the bullet currently in action? private boolean isActive; public Bullet() { // Inactive until fired isActive = false; // World coordinates contained in a point point = new PointF(); } public boolean shoot(float startX, float startY, float direction) { if (!isActive) { point.x = startX; point.y = startY; // How much to move horizontally, each frame horizontalVelocity = (float)(Math.cos(Math.toRadians(direction))); // How much to move vertically, each frame verticalVelocity = (float)(Math.sin(Math.toRadians(direction))); isActive = true; return true; } // Bullet already active return false; } public void update(long fps){ // Move the bullet point.x = point.x + horizontalVelocity * speed / fps; point.y = point.y + verticalVelocity * speed / fps; } public PointF getPoint(){ return point; } public boolean getStatus(){ return isActive; } public void setInactive(){ isActive = false; } } |
The Bullet class is even simpler than the Space Invaders’ Bullet class. The constructor sets it up and the update method moves the bullet each frame. The class also has a method so that the main game loop can tell whether or not a particular instance is currently active. If the math in the shoot method looks a bit weird then it is explained in the tutorial series that starts with lCalculating heading.
Star
In Android Studio, create a new class called Star and add the following code. All this class does is hold a single point which is randomly generated. The update method then infrequently and randomly switches on and off which creates a twinkling effect when viewed with hundreds of Star instances.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package com.gamecodeschool.scrollingshooter2d; import java.util.Random; public class Star{ private int x; private int y; private boolean isVisible = true; // Declare a random object here because // we will use it in the update() method // and we don't want the garbage collector to have to keep clearing it up Random random; public Star(int mapWidth, int mapHeight){ random = new Random(); x= random.nextInt(mapWidth); y = random.nextInt(mapHeight); } public int getX(){ return x; } public int getY(){ return y; } public void update(){ // Randomly twinkle the stars int n = random.nextInt(5000); if(n == 0){ // Switch on or off isVisible = !isVisible; } } public boolean getVisibility(){ return isVisible; } } [/code] |
Brick
Add a class called Brick to the project. Brick is simple but there are some things in here that other tutorials(on this site) haven’t covered. In the constructor, as well as positioning the current brick relative to the previous brick and deciding whether it is an edge brick and therefore needs an edge line drawn, an alpha level is randomly generated. This has the effect of some bricks looking like they are lit windows. The varying alpha makes different brightnesses of light. The update method infrequently and randomly varies these values further looking like lights are being turned on and off.
What is most interesting in the context of this project is that the class holds a variable called clipped, which we will see working in conjunction with the Viewport class. When a Brick instance is clipped, there is no need to process each frame or attempt to draw it either.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
package com.gamecodeschool.scrollingshooter2d; import android.graphics.Color; import android.graphics.RectF; import java.util.Random; public class Brick { private RectF rect; Random random = new Random(); //private boolean isVisible; boolean destroyed; private boolean isRight; private boolean isLeft; private boolean isTop; private int color; private boolean clipped; public Brick(int columnNum, int rowNum, boolean isLeft, boolean isRight, boolean isTop){ int brickWidth = 1; int brickHeight = 1; this.isLeft = isLeft; this.isRight = isRight; this.isTop = isTop; float brickXPadding = .0f; float brickYPadding = .0f; rect = new RectF( (columnNum * (brickXPadding + brickWidth + brickXPadding) + brickXPadding), ((rowNum * (brickYPadding + brickHeight + brickYPadding) ) + brickYPadding), (columnNum * (brickXPadding + brickWidth + brickXPadding) + brickXPadding + brickWidth), ((rowNum * (brickYPadding + brickHeight + brickYPadding) ) + brickYPadding + brickHeight) ); // Assign a color if(random.nextInt(9) == 0){ // Vary the alpha for effect int alpha = random.nextInt(256); color = Color.argb(alpha, 255, 255, 0); }else{ color = Color.argb(255, 0, 0, 0); } } public void update(){ // Assign a color if(!destroyed){// flicker if (random.nextInt(6000) == 0) { if (random.nextInt(9) == 0) { // Vary the alpha for effect int alpha = random.nextInt(256); color = Color.argb(alpha, 255, 255, 0); } else { color = Color.argb(255, 0, 0, 0); } } }else{// fire int whichColor = random.nextInt(3); switch (whichColor){ case 0: color = Color.argb(255, 255, 0, 0); break; case 1: color = Color.argb(255, 245, 143, 10); break; case 2: color = Color.argb(255, 250, 250, 10); break; } } } public void destroy(){ destroyed = true; } public boolean isDestroyed(){ return destroyed; } public void clip(){ clipped = true; } public void unClip(){ clipped = false; } public boolean isClipped(){ return clipped; } public RectF getRect(){ return this.rect; } public boolean getLeft(){ return isLeft; } public int getColor(){ return color; } public boolean getRight(){ return isRight; } public boolean getTop(){ return isTop; } } |
 Coding the MainActivity
Simply edit your MainActivity class to look like this code. There is nothing new here. If anything looks unusual check back to the Breakout or SpaceInvaders  projects. If you have done any Android projects on this site before then this code will look very familiar. It simply creates an GameView instance and passes the screen resolution to its constructor. The onPause and onResume methods call corresponding methods from the GameView instance to start and stop the thread that runs the main game loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.gamecodeschool.scrollingshooter2d; import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; public class MainActivity extends Activity { // View will be the view of the game // It will also hold the logic of the game // and respond to screen touches as well GameView view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get a Display object to access screen details Display display = getWindowManager(). getDefaultDisplay(); // Load the resolution into a Point object Point size = new Point(); display.getSize(size); // Initialize gameView and set it as the view view = new GameView(this, size.x, size.y); setContentView(view); } // This method executes when the player starts the game @Override protected void onResume() { super.onResume(); // Tell the gameView resume method to execute view.resume(); } // This method executes when the player quits the game @Override protected void onPause() { super.onPause(); // Tell the gameView pause method to execute view.pause(); } } |
Coding the GameView
This is the longest and most complicated class. I have heavily commented it. If I don’t explain something, that is because the details were given in one of the prerequisite projects. The list of these projects is at the top of the page. I will go into detail when we use the Viewport class and when we code the HUD inner class.
The code that follows is quite lengthy but follows in order. Note that I have broken it up into chunks with a bit of discussion and I don’t always show the full level of indentation, to make the code more readable.
Create a new class called GameView. First, the class declaration, into which all the rest of the code that follows will go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
package com.gamecodeschool.scrollingshooter2d; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.media.AudioManager; import android.media.SoundPool; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import java.io.IOException; import java.util.ArrayList; import java.util.Random; public class GameView extends SurfaceView implements Runnable { // A handy Random instance Random random = new Random(); // For sound FX private SoundPool soundPool; private int shootID = -1; private int damageBuildingID = -1; // This is an instance of an inner class // See end of this class for code HUD hud; // How big is the world? // Change these for lots of fun... // And a slow game int worldWidth = 0; int targetWorldWidth = 500; int targetWorldHeight = 150; int groundLevel = 145; // For the returned rectangle from the viewport // These we will use in the main game loop, multiple times // Saves creating new instances each frame RectF convertedRect = new RectF(); PointF convertedPointA = new PointF(); PointF convertedPointB = new PointF(); PointF convertedPointC = new PointF(); PointF tempPointF = new PointF(); // This is our thread private Thread gameThread = null; // Our SurfaceHolder to lock the surface before we draw our graphics private SurfaceHolder ourHolder; // A boolean which we will set and unset // when the game is running- or not. private volatile boolean playing; // Game is paused at the start private boolean paused = true; // A Canvas and a Paint object private Canvas canvas; private Paint paint; // This variable tracks the game frame rate private long fps; // The city is built from bricks private Brick[] bricks = new Brick[20000]; private int numBricks; // Twinkling stars in the sky above the city private Star[] stars = new Star[5000]; private int numStars; // The player's ship Ship player; // The player's bullets private Bullet[] playerBullets = new Bullet[10]; private int nextPlayerBullet; private int maxPlayerBullets = 10; // Our neat viewport/camera/clipping machine Viewport vp; // All code including HUD inner class goes here // End of GameView } |
The previous code declares all the game objects, Stars, Bricks and a Ship called player. Notice also we declare an instance of Viewport called vp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// The constructor public GameView(Context context, int screenX, int screenY) { // The next line of code asks the // SurfaceView class to set up our object. // How kind. super(context); // Initialize ourHolder and paint objects ourHolder = getHolder(); paint = new Paint(); // Initialize the Viewport vp = new Viewport(screenX, screenY); hud = new HUD(screenX, screenY); // This SoundPool is deprecated but don't worry soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0); try{ // Create objects of the 2 required classes AssetManager assetManager = context.getAssets(); AssetFileDescriptor descriptor; // Load our fx in memory ready for use descriptor = assetManager.openFd("shoot.ogg"); shootID = soundPool.load(descriptor, 0); descriptor = assetManager.openFd("damagebuilding.ogg"); damageBuildingID = soundPool.load(descriptor, 0); }catch(IOException e){ // Print an error message to the console Log.e("error", "failed to load sound files"); } prepareLevel(); } |
The previous code is the GameView constructor. It sets up the Paint and Holder objects ready for drawing. It also gets the sound effects ready to play. In this code, we also call the vp constructor passing in the screen resolution as is required. The code also creates a new HUD instance, again passing in the screen resolution. We will see the HUD inner class soon. “Release your inner HUD”. Too much? Here is the prepareLevel code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
private void prepareLevel() { player = new Ship(); // Initialize the playerBullets array for (int i = 0; i < playerBullets.length; i++) { playerBullets[i] = new Bullet(); } //Reset the players location as the world centre of the viewport //if game is playing vp.setWorldCentre(player.getCentre().x, player.getCentre().y); Random random = new Random(); int gapFromLastBuilding; int maxGap = 25; int buildingWidth; int maxBuildingWidth = 10; int buildingHeight; int maxBuildingHeight = 85; for (worldWidth = 0; worldWidth < targetWorldWidth; worldWidth += buildingWidth + gapFromLastBuilding) { buildingWidth = random.nextInt(maxBuildingWidth) + 3; buildingHeight = random.nextInt(maxBuildingHeight) + 1; gapFromLastBuilding = random.nextInt(maxGap) + 1; for (int x = 0; x < buildingWidth; x++) { for (int y = groundLevel; y > groundLevel - buildingHeight; y--) { boolean isLeft = false; boolean isRight = false; boolean isTop = false; // Is this brick on left, right or top? if (x == 0) { isLeft = true; } if (x == buildingWidth - 1) { isRight = true; } if (y == (groundLevel - buildingHeight) + 1) { isTop = true; } bricks[numBricks] = new Brick(x + worldWidth, y, isLeft, isRight, isTop); numBricks++; } } } // Instantiate some stars for (int i = 0; i < 500; i++) { stars[i] = new Star(targetWorldWidth, targetWorldHeight); numStars++; } } |
In the prepareLevel method, most of the code initializes the Brick instances. Also notice, however, we call the setWorldCentre method on our vp object and pass in the center of the player ship. The Viewport instance is now fully configured and ready to convert all world coordinates to screen coordinates, relative to the center of the player’s spaceship.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Override public void run() { while (playing) { // Capture the current time in milliseconds in startFrameTime long startFrameTime = System.currentTimeMillis(); // Update the frame if (!paused) { update(); } // Draw the frame draw(); // Calculate the fps this frame // We can then use the result to // time animations and more. long timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame >= 1) { fps = 1000 / timeThisFrame; } } } |
The run method above simply calls update and draw while keeping track of the frames per second.
The next method update is quite huge(really it should be split up a bit). I encourage you to read the code thoroughly paying special attention to the use of vp.clipObjects which sets any Brick instances that are outside the viewport to clipped = true. Notice that the way we handle collision detection is unaffected by the fact we are using a viewport. If two objects collide in the game world it doesn’t matter if they are at x one million or x 10 – they collide. The viewport has not complicated this task in any way except that we only update and check for collisions on unclipped bricks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
private void update() { //Reset the players location as the world centre of the viewport //if game is playing vp.setWorldCentre(player.getCentre().x, player.getCentre().y); // Clip all off screen bricks for (int i = 0; i < numBricks; i++) { if (vp.clipObjects(bricks[i].getRect().left, bricks[i].getRect().top, 1, 1)) { bricks[i].clip(); } else { bricks[i].unClip(); } } player.update(fps); // Update all the player bullets if active for (int i = 0; i < playerBullets.length; i++) { if (playerBullets[i].getStatus()) { playerBullets[i].update(fps); } } // Have the player's bullets hit a building? // Update all the player bullets if active for (int i = 0; i < maxPlayerBullets; i++) { if (playerBullets[i].getStatus()) { for (int j = 0; j < numBricks; j++) { if (!bricks[j].isClipped()) { // Only process this brick if not destroyed if(!bricks[j].isDestroyed()) { if (bricks[j].getRect().contains( playerBullets[i].getPoint().x, playerBullets[i].getPoint().y)) { playerBullets[i].setInactive(); soundPool.play(damageBuildingID, 1, 1, 0, 0, 1); bricks[j].destroy(); int chainReactionSize = random.nextInt(6); for(int k = 1; k < chainReactionSize; k++){ bricks[j+k].destroy(); bricks[j-k].destroy(); } } } } } } } // set bullets inactive when they go out of view // Clip all off screen bricks for (int i = 0; i < maxPlayerBullets; i++) { if (playerBullets[i].getStatus()) { if(playerBullets[i].getPoint().x < 0){ playerBullets[i].setInactive(); } else if(playerBullets[i].getPoint().x > targetWorldWidth){ playerBullets[i].setInactive(); } else if(playerBullets[i].getPoint().y < 0){ playerBullets[i].setInactive(); } else if(playerBullets[i].getPoint().y > targetWorldHeight){ playerBullets[i].setInactive(); } } } // Update the stars for (int i = 0; i < numStars; i++) { stars[i].update(); } // Update the bricks for (int i = 0; i < numBricks; i++) { if (!bricks[i].isClipped()) { bricks[i].update(); } } // Has the ship collided with a top, left or right brick? // that isn't destroyed for (int i = 0; i < numBricks; i++) { if (!bricks[i].isClipped() && !bricks[i].isDestroyed()) { if (bricks[i].getRect().contains(player.getA().x, player.getA().y) || bricks[i].getRect().contains(player.getB().x, player.getB().y) || bricks[i].getRect().contains(player.getC().x, player.getC().y)) { player.bump(); } } } // Has the ship collided with the floor? if (player.getA().y > groundLevel || player.getB().y > groundLevel || player.getC().y > groundLevel) { player.bump(); } // Has the ship collided with the game world ceiling? if (player.getA().y < 0 || player.getB().y < 0 || player.getC().y < 0) { player.bump(); } // Has the ship collided with the game world left? if (player.getA().x < 0 || player.getB().x < 0 || player.getC().x < 0) { player.bump(); } // Has the ship collided with the game world right? if (player.getA().x > worldWidth || player.getB().x > worldWidth || player.getC().x > worldWidth) { player.bump(); } } |
This next method called draw is where the Viewport class does its most interesting work and calls worldToScreen and worldToScreenPoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
private void draw() { // Make sure our drawing surface is valid or we crash if (ourHolder.getSurface().isValid()) { // Lock the canvas ready to draw canvas = ourHolder.lockCanvas(); // Draw the background color //canvas.drawColor(Color.argb(255, 87, 73, 122)); canvas.drawColor(Color.argb(255, 0, 0, 0)); // Draw the game world ceiling paint.setColor(Color.argb(255, 255, 255, 255)); convertedRect = vp.worldToScreen( 0, 0, targetWorldWidth, 1 ); canvas.drawRect(convertedRect, paint); // Draw the left game world barrier convertedRect = vp.worldToScreen( 0, 0, 1, targetWorldHeight ); canvas.drawRect(convertedRect, paint); // Draw the right game world barrier convertedRect = vp.worldToScreen( targetWorldWidth, 0, 1, targetWorldHeight ); canvas.drawRect(convertedRect, paint); // Draw stars if visible // Update the stars paint.setColor(Color.argb(255, 255, 255, 255)); for (int i = 0; i < numStars; i++) { if (stars[i].getVisibility()) { tempPointF = vp.worldToScreenPoint(stars[i].getX(), stars[i].getY()); canvas.drawPoint(tempPointF.x, tempPointF.y, paint); } } // Draw the bricks if visible for (int i = 0; i < numBricks; i++) { if (!bricks[i].isClipped()) { // Draw the bricks // Choose the brush color for drawing paint.setColor(bricks[i].getColor()); convertedRect = vp.worldToScreen( bricks[i].getRect().left, bricks[i].getRect().top, bricks[i].getRect().right - bricks[i].getRect().left, bricks[i].getRect().bottom - bricks[i].getRect().top ); canvas.drawRect(convertedRect, paint); // Draw building outline // Choose the brush color for drawing paint.setColor(Color.argb(255, 190, 190, 190)); if (bricks[i].getLeft()) { canvas.drawLine( convertedRect.left, convertedRect.top, convertedRect.left, convertedRect.bottom, paint ); } if (bricks[i].getRight()) { canvas.drawLine( convertedRect.right, convertedRect.top, convertedRect.right, convertedRect.bottom, paint ); } if (bricks[i].getTop()) { canvas.drawLine( convertedRect.left, convertedRect.top, convertedRect.right, convertedRect.top, paint ); } } } tempPointF = vp.worldToScreenPoint(player.getA().x, player.getA().y); convertedPointA.x = tempPointF.x; convertedPointA.y = tempPointF.y; tempPointF = vp.worldToScreenPoint(player.getB().x, player.getB().y); convertedPointB.x = tempPointF.x; convertedPointB.y = tempPointF.y; tempPointF = vp.worldToScreenPoint(player.getC().x, player.getC().y); convertedPointC.x = tempPointF.x; convertedPointC.y = tempPointF.y; paint.setColor(Color.argb(255, 255, 255, 255)); canvas.drawLine(convertedPointA.x, convertedPointA.y, convertedPointB.x, convertedPointB.y, paint); canvas.drawLine(convertedPointB.x, convertedPointB.y, convertedPointC.x, convertedPointC.y, paint); canvas.drawLine(convertedPointC.x, convertedPointC.y, convertedPointA.x, convertedPointA.y, paint); canvas.drawPoint(convertedPointA.x, convertedPointA.y, paint); // Update all the player bullets if active for (int i = 0; i < playerBullets.length; i++) { if (playerBullets[i].getStatus()) { tempPointF = vp.worldToScreenPoint( playerBullets[i].getPoint().x, playerBullets[i].getPoint().y); canvas.drawRect(tempPointF.x, tempPointF.y, tempPointF.x+4,tempPointF.y+4, paint); } } // Draw some debugging info // Choose the brush color for drawing paint.setColor(Color.argb(255, 255, 255, 255)); paint.setTextSize(20); canvas.drawText("FPS = " + fps, 20, 70, paint); // Draw the floor convertedRect = vp.worldToScreen( -10, groundLevel, targetWorldWidth + 10, targetWorldHeight - groundLevel ); paint.setColor(Color.argb(255, 5, 66, 9)); canvas.drawRect(convertedRect, paint); // Change paint color // Low alpha value to make buttons transparent paint.setColor(Color.argb(80, 255, 255, 255)); for (Rect rect : hud.currentButtonList) { RectF rf = new RectF(rect.left, rect.top, rect.right, rect.bottom); canvas.drawRoundRect(rf, 15f, 15f, paint); } // Draw everything to the screen ourHolder.unlockCanvasAndPost(canvas); } } |
The next three methods handle starting and stopping the thread when prompted to by the MainActivity class. The onTouchEvent method, however, is surprisingly short. It calls hud.handleInput and passes in motionEvent. All the input handling as well as the preparation of the on-screen buttons will be handled by the inner HUD class which we will discuss next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// If SpaceInvadersActivity is paused/stopped // shutdown our thread. public void pause() { playing = false; try { gameThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // If SpaceInvadersActivity is started then // start our thread. public void resume() { playing = true; gameThread = new Thread(this); gameThread.start(); } // The SurfaceView class implements onTouchListener // So we can override this method and detect screen touches. @Override public boolean onTouchEvent(MotionEvent motionEvent) { hud.handleInput(motionEvent); return true; } |
The HUD class is simple and effective. It handles the screen presses as well as defining the position of all the transparent buttons the player will be able to press. Each button is defined by a RectF and the position and size of each calculated based on the resolution of the screen. The .contains method is used with each of the RectF objects to determine when any of the buttons are pressed and then they can be responded to.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
class HUD { Rect left; Rect right; Rect thrust; Rect shoot; Rect pause; //create an array of buttons for the draw method public ArrayList<Rect> currentButtonList = new ArrayList<>(); HUD(int screenWidth, int screenHeight) { //Configure the player buttons int buttonWidth = screenWidth / 8; int buttonHeight = screenHeight / 7; int buttonPadding = screenWidth / 80; left = new Rect(buttonPadding, screenHeight - buttonHeight - buttonPadding, buttonWidth, screenHeight - buttonPadding); right = new Rect(buttonWidth + buttonPadding, screenHeight - buttonHeight - buttonPadding, buttonWidth + buttonPadding + buttonWidth, screenHeight - buttonPadding); thrust = new Rect(screenWidth - buttonWidth - buttonPadding, screenHeight - buttonHeight - buttonPadding - buttonHeight - buttonPadding, screenWidth - buttonPadding, screenHeight - buttonPadding - buttonHeight - buttonPadding); shoot = new Rect(screenWidth - buttonWidth - buttonPadding, screenHeight - buttonHeight - buttonPadding, screenWidth - buttonPadding, screenHeight - buttonPadding); pause = new Rect(screenWidth - buttonPadding - buttonWidth, buttonPadding, screenWidth - buttonPadding, buttonPadding + buttonHeight); // Add the rect objects in the same order as the static final values currentButtonList.add(left); currentButtonList.add(right); currentButtonList.add(thrust); currentButtonList.add(shoot); currentButtonList.add(pause); } public void handleInput(MotionEvent motionEvent) { int x = (int) motionEvent.getX(0); int y = (int) motionEvent.getY(0); switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: if (right.contains(x, y)) { player.setMovementState(player.RIGHT); } else if (left.contains(x, y)) { player.setMovementState(player.LEFT); } else if (thrust.contains(x, y)) { player.setMovementState(player.THRUSTING); } else if (shoot.contains(x, y)) { playerBullets[nextPlayerBullet].shoot( player.getA().x,player.getA().y, player.getFacingAngle()); nextPlayerBullet++; if (nextPlayerBullet == maxPlayerBullets) { nextPlayerBullet = 0; } soundPool.play(shootID, 1, 1, 0, 0, 1); }else if(pause.contains(x, y)) { paused = !paused; } break; case MotionEvent.ACTION_UP: player.setMovementState(player.STOPPING); } } }// End of HUD // End of GameView |
You can now run the game. Note that the button in the top-right is a pause button and you need to press it to make the other buttons work. But there’s more!
What else can you do with a viewport?
We could easily add a “map screen” to our game by having the screen presses call these methods (while in “map mode”) instead of moving the ship.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void moveViewportRight(int maxWidth){ if(currentViewportWorldCentre.x < maxWidth - (metresToShowX/2)+3) { currentViewportWorldCentre.x += 1; } } public void moveViewportLeft(){ if(currentViewportWorldCentre.x > (metresToShowX/2)-3){ currentViewportWorldCentre.x -= 1; } } public void moveViewportUp(){ if(currentViewportWorldCentre.y < (metresToShowY /2)-3) { currentViewportWorldCentre.y -= 1; } } public void moveViewportDown(int maxHeight){ if(currentViewportWorldCentre.y < maxHeight - (metresToShowY / 2)+3) { currentViewportWorldCentre.y += 1; } } |
We could output debugging to the screen regarding the number of clipped game objects. We could internally clip new game objects with a simple isClipped variable. We could then monitor the number of clipped game objects by calling these two methods, once each per frame:
1 2 3 4 5 6 7 8 9 |
public int getNumClipped(){ return numClipped; } public void resetNumClipped(){ numClipped = 0; } |
All we need to do is vary the metresToShow... variables and we can instantly zoom our game world in and out. Furthermore, if we have multiple instances of Viewport, we can also pass in different resolutions for each instance. Consider passing the full-screen resolution as we did in this project and then a smaller, say 100 x 100-pixel resolution but with larger values for the metresToShow variables. You could then make two passes through the draw method and you have a map of the game world drawn on top of the current view around the player.
We could even use the same math we use to rotate the spaceship, on each and every pixel via the Viewport class. We could then rotate our entire game world with ease. Or, in a more likely scenario, we could rotate a shrunken view of a game world, perhaps drawn in the top corner, and we will have ourselves a rotating mini-map without breaking a sweat.
Problems with the design
Anyone with a device that is more than a few years old will probably notice the frame rate drops lower as there are more objects on the screen. My 4-year-old Samsung Note tablet drops as low as 30 frames per second at times. There are a number of things we could do to improve the efficiency of the code. We could optimize our collision detection. We could find a totally new way to represent the buildings. The bricks were just a bit of fun. We don’t really need to construct each building out of dozens of game objects! But what if we really do need vast numbers of game objects?
Quite soon we are going to need to adopt a new development paradigm. We can achieve this by using a game library like LibGDX or we can stick with the Android API and optimize our game code, at the same time as moving to OpenGL ES to do all our drawing. The next series of Android game projects will introduce the basics of making a game, with OpenGL ES doing all the drawing and eventually lead up to a full, 2D, OpenGL ES game project.
Thanks Again for these awesome tutorials. And I think Viewport constructor should be inside Viewport class.
Thanks Rishabh, I put that straight.
If I am honest I didn’t really learn anything on here could you recommend anything to help?
Hi Domi, if you give me a bit more background I will try and point you in the right direction. What were you hoping to learn? What stage are you at now, etc.
This is cool! I was trying to figure out how can i put a text on the button so i can play it easier. Thanks for the tutorial!
Hi Jyeo,
Use the Canvas.drawText to achieve this. Thanks very much for your comment.
I already tried it but not working. Do i need to put it inside the loop? where the hud.currentButtonList was placed? thanks!
Another question, The texts are displaying now but it’s at the center of the screen. how can i put all of this inside the buttons?
You can draw the text at the same position as the button also you can resize the text with setTextSize before the call to drawText.
Hi. Could you just release the printputs for each of the class -files? I have hard time figuring out what piece of code goes where. I’d like to see at least what my Viewport- and GameView -classes should look like.
Hi JH,
I have added a download bundle just below the “Starting and planning 2D scrolling shooter project” section near the top. It is unlockable with a like/tweet/+1. It contains all seven class files. If you are unable to do this contact myself and I will email them to you.
Thank you. I got it working.
Hi,
first of all, thank you for this awesome tutorial!!
I noticed that the game permits only a action at a time, so I made a little mod to get multitouch working :
in the Ship class I added
private int bitShipMoving = STOPPING;
public void setMovementState(int state, int goOrStop){
if(goOrStop==1) {
bitShipMoving = bitShipMoving | state;
}else{
bitShipMoving = bitShipMoving & (~state);
}
}
in order to store every single action as a bit.
In the and update method of the same class I changed the beginning of these statements:
if(shipMoving == LEFT){ …..
in
if((bitShipMoving & LEFT) == LEFT){……
Then, in the GameView, at the beginning of the handleInput method:
int doThis=motionEvent.getAction() & MotionEvent.ACTION_MASK;
int pointer=(motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int x = (int) motionEvent.getX(pointer);
int y = (int) motionEvent.getY(pointer);
then I added an ACTION_POINTER for DOWN and UP, so
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
then
player.setMovementState(player.RIGHT)
changed in
player.setMovementState(player.RIGHT,1)
(and the same for LEFT and THRUSTING)
and finally
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (right.contains(x, y)) {
player.setMovementState(player.RIGHT,0);
} else if (left.contains(x, y)) {
player.setMovementState(player.LEFT,0);
} else if (thrust.contains(x, y)) {
player.setMovementState(player.THRUSTING,0);
}
It works like a charm (on my device, at least)! 😉
ops.. little change also for
public final int THRUSTING = 4;
to get it working as bitwise op.
Hi Francesco,
Thanks for this neat enhancement. It will prove useful to many.
Hi John. I’m somewhere in the middle of this great tutorial, so I haven’t run the intire code yet, but is it possible that the link for downloading the FX (shoot.ogg and damagebuilding.ogg) is missing?
Hi Henk,
Thanks for your comment. You can get these (or very simmilar)from the Space Invaders tutorial. Thanks for pointing this out.
Case A)
Let me quote first : ” This is how worldToScreen works. It receives the horizontal and vertical world locations of an object along with that object’s width and height. With these values, each in turn, it subtracts the objects world coordinate multiplied by the pixels per meter for the current screen from the appropriate current world viewport center (x or y) .”
And the corresponding code is :
int left = (int) (screenCentreX – ((currentViewportWorldCentre.x – objectX) * pixelsPerMetreX));
I think the sentence should be instead :
With these values, each in turn, it subtracts the object’s world coordinate from the appropriate current world viewport center (x or y) and then multiplies the subtraction result by the pixels per meter for the current screen .
Q1. Am I right ?
The book ‘Android Game Programming By Examples’ by John Horton in a similar example in Chapter 5 uses such confusing sentences.
Case B) From the above quotation :”It receives the horizontal and vertical world locations of an object …”
Q2. For a rectangular object what does ” horizontal and vertical world locations of an object” mean ?
Case C) Let me quote again :
“Then, for the left and top coordinates of the object, the result is subtracted from the pixel screen center value and…”
Q3. Can you please provide a diagram with the screen center, viewport world center, objectX etc plotted ?
And the last question:
Q4. Can you please explain the ‘clipObjects’ method line by line for a particular example with a diagram to represent the object’s height, width, current viewport world center etc ? Really I canNOT make ANYTHING out of it here i.e why metresToShowX and metresToShowY were divided by 2 , why “objectX – objectWidth” subtraction took place as well as other calculations in each line.
Q1) Yes you describe it well.
Q2) The pixel location where the origin(top left pixel) of the sprite will be. See chapter 2 plotting and drawing.
Q3) Try drawing just one object to the screen and then outputting the value of all the variables pixelsPerMetre…, metresToShow… etc to the screen each frame. This will demonstrate a lot.
Q4) Here is an overview. If an object has not disappeared off the left of the region of the world we want to show or if the object has not disappeared off of the right then set the clipped variable to false. Otherwise, set it to true.
I hope this helps a bit.
Thanks, John
As to Q4, here is a picture made by me to describe the situation below. This editor has no image attaching option though !
The ViewPort constructor has :
Viewport(int screenXResolution, int screenYResolution){
screenCentreX = screenXResolution / 2;
screenCentreY = screenYResolution / 2;
pixelsPerMetreX = screenXResolution / 90;
pixelsPerMetreY = screenYResolution / 55;
metresToShowX = 92;
metresToShowY = 57;
…….
……
}
Let me say, the smartphone device has a width of 500px and a height of 400px for instance. So screenXResolution = 500 and screenYResolution=400. And its origin i.e. co-ordinate (0,0) is at the top left corner.
Now,
screenCentreX =500/2 = 250 and screenCentreY = 400/2 = 200. This is C2 in the picture.
pixelsPerMetreX =500/90 =5.56 and pixelsPerMetreY = 400/55 = 7.23.
The ‘Ship’ i.e. player constructor has :
public Ship(){
……..
……..
float length = 2.5f;
float width = 1.25f;
centre = new PointF();
centre.x = 50;
centre.y = 50;
………
……..
}
I think this co-ordinate i.e. (50,50) is with respect to the actual screen co-ordinate and not any imaginary game world. Qa) Am I right ?
Now we want to make the Ship as the game center.
So the player object we want to make the center of the world has the co-ordinate (50,50) which is C1 in the picture .
‘prepareLevel()’ method has :
vp.setWorldCentre(player.getCentre().x, player.getCentre().y);
So the world center is now (50,50)
the ‘clipObjects(float objectX, float objectY, float objectWidth, float objectHeight) ‘ method has as its first ‘if’ condition :
” if (objectX – objectWidth < currentViewportWorldCentre.x + (metresToShowX / 2)) { …. }"
We want to test another object (not the 'Ship' player one) against the 'ClipObjects' method :
The test object has :
objectX to be 300 ,objectY to be 260 which is point D in the picture and objectWidth to be 10. These are also real screen co-ordinates and not the imaginary game world ones, Qb) Am I right ?
And here ,the currentViewportWorldCentre.x is 50 and metresToShowX is 92.
So the 'if' condition becomes :
(300-10) <(50+92/2)
what could it really mean ? Pulling off my hair…
Instead of plotting the co-ordinates in a picture, i think a verbal explanation regarding the 'if' condition would be more helpful or perhaps I am not even getting closer to the way of understanding the scenario .
The tutorial did not intend to explain it leaving it on the readers instead. Qc)Actually what does the first 'if' condition want to test ?
Help is what we need.
Regards
It’s a long time since I wrote this code but I have just had a look at it to familiarize myself and try to help.
Q) “I think this co-ordinate i.e. (50,50) is with respect to the actual screen co-ordinate and not any imaginary game world. Qa) Am I right ?”
A) This is relative to the world coordinates of the ship. The center of the ship. It is used to rotate a, b and c around in order to rotate the ship and to move the ship in world space. The The ship class(and the other game objects) doesn’t deal with pixels. All movement and collisions etc are done with world coordinates. The ViewPort class uses the screen resolution and how many world units we want to display in order to translate world locations to pixel positions appropriate for the screen. When objects are outside of the visible area of the world that is currently being shown they are clipped.
Q) “ vp.setWorldCentre(player.getCentre().x, player.getCentre().y);”
“So the world center is now (50,50)”
A) Yes.
Q) … what could it really mean? Pulling off my hair…
A) The values sent into the ViewPort methods are always world coordinates. WorldToScreen translates them to pixel positions and ClipObjects decides whether an object is far enough away from the center of the world (the ship) to be safely ignored. To be clear, ClipObjects works in world coordinates, no translation is necessary for it to do its job.
I hope this helps. The ViewPort class was initially going to be a black box class when the book was planned. The book went way over the page count and I guess could have done with more detailed explanations. When I write the second edition, hopefully, next year I will put a lot of focus into this part. The reason that it was going to be a black box is because once you move on to game engines/LibGDX/OpenGLES(the next project in the book) etc, the ViewPort class is defunct as game engines/LibGDX/OpenGLES etc handle the whole world/pixel conundrum for us.
All the best,
John
Thanks for the reply. The ‘clipObjects’ method uses only game world co-ordinates and not the screen ones – very useful info. But the method still puts me in the same darkness as it did before. Let me put the very first ‘if’ condition of the method here again :
if (objectX – objectWidth < currentViewportWorldCentre.x + (metresToShowX / 2)) { …. }
From the picture, it just became
if( (300-10) <(50+92/2) )
Can you just tell –
Q1) what the left and right hand expressions want to mean individually ?
Q2) How they can produce the logic as to whether the object is far enough from the game center (50,50) i.e the Ship to be clipped off or not , be it along x axis or y ?
Hi Istiaque,
Sorry to take so long to reply. Things are busy at the moment. I am going to have to defer this to another time. I have added as the next thing to write an update with more detail to this article along the lines of your request.
4 lines of code from the tutorial :
pixelsPerMetreX = screenXResolution / 90;
pixelsPerMetreY = screenYResolution / 55;
metresToShowX = 92;
metresToShowY = 57;
Here 92 is used instead of 90, and 57 instead of 55. Your book ‘Android Game Programming By Examples’ tried to explain it in Chapter 5 in a similar example while no explanation is provided here. An easier explanation than the one given in your book would be really appreciated.
Hi Istiaque,
If you think about a game world it is much easier (and often necessary) to do so in human-friendly units. The viewport is simply a way of translating from world units to pixels. The code is keeping track/calculating how many pixels represent a meter in the simulated world. The difference in the values used is simply what seemed to suit the game at the time. You could change the metresToShow values to whatever you like to zoom the game in and out(show more or less of the world).
Hello I tried to create custom buttons and
if (right.contains(x, y)) {
player.setMovementState(player.RIGHT);
}
No longer worked for me so I had to change it to this.Also hope this helps with creating your own game buttons
public class Gui{
ArrayList inputbuttons = new ArrayList();
Rect btnup,btndown,btnleft,btnright,shoot;
Gui(int x,int y){
//my custom buttons
btnleft = new Rect(0,y-50,50,y-100);
btnright = new Rect(100,y-50,150,y-100);
btnup = new Rect(50,y-100,100,y-150);
btndown = new Rect(50,y-1,100,y-50);
inputbuttons.add(shoot);
inputbuttons.add(btnleft);
inputbuttons.add(btnright);
inputbuttons.add(btnup);
inputbuttons.add(btndown);
}
public void onTouchEvent(MotionEvent event){
int tx = (int)event.getX(0);
int ty = (int)event.getY(0);
switch(event.getAction() & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_DOWN:
//this part works with any of my custom buttons
if(tx=btnright.left && ty>=btnright.bottom & ty<=btnright.top){
//what button does
player.setmoving(player.right);
}
if(tx=btnleft.left && ty>=btnleft.bottom & ty<=btnleft.top){
//what button does
player.setmoving(player.left);
}
if(tx=btnup.left && ty>=btnup.bottom & ty<=btnup.top){
//what button does
player.setmoving(player.up);
}
if(tx=btndown.left && ty>=btndown.bottom & ty<=btndown.top){
//whay button does
player.setmoving(player.down);
}
break;
case MotionEvent.ACTION_UP:
player.setmoving(player.stopped);
break;
}
}}}