In this project, we will put into practice everything we learned in part 1 and part 2 of the tutorial series on trigonometric functions. We will draw a simple triangle-shaped spaceship to the screen by drawing lines between three points/vertices. We will then see how we can do the math to smoothly rotate it and calculate its heading in order to fly around the screen.
To get started create a new project in Android Studio, call it Heading And Rotation and name the Activity HeadingAndRotationActivity then read on.
Making the game a full-screen landscape
We want to use every pixel that the device has to offer so we will make changes to the app’s AndroidManifest.xml configuration file.
- In the project explorer pane in Android Studio double click on the manifests folder, this will open up the AndroidManifest.xml file in the code editor.
- In the AndroidManifest.xml file find the following line of code, android:name=".HeadingAndRotation"
- Immediately below type or copy and paste these two lines to make the game run full screen and lock it in the landscape orientation.
1 2 |
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape" |
The class structure
We want to create classes to represent the view and the spaceship. We will call them HeadingAndRotationViewand Ship. By creating empty implementations of these classes we can then declare objects of them right away and make faster progress through the code.
With this in mind let’s create those empty classes. Right-click the java folder in the Android Studio project explorer as shown in the next image.
Now select New|Java class then select …\app\source\main\java and click OK. Now enter HeadingAndRotationView as the name for our new class and click OK. We have now created a new Java class in a separate file called HeadingAndRotationView.java. Now do the same thing and create a class called Ship.
[widgets_on_pages id=”udemy_advert_java_2″]
Coding the Activity class
This Activity class is no different from the Activity classes from other projects like the Breakout game so I will just throw the code at you below. If the comments are not enough to remind you of what the code does then check out more detailed explanations in the Breakout project. Delete the contents of the HeadingAndRotationActivity class and add this code instead.
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 |
import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; /* This is the entry point to the game. It will handle the lifecycle of the game by calling methods of view when prompted to so by the OS. */ public class HeadingAndRotationActivity 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 */ HeadingAndRotationView 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 HeadingAndRotationView(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 view class
The view class holds nothing new either. It sets up the main game loop and calls update and draw as per usual. The only slightly new thing compared to the Breakout or Space Invaders projects is that in the draw method we use the drawLine method of the Canvas class to draw three lines that we get the coordinates for by calling getA, getB and getC of the Ship class that we are yet to implement. For more on drawLine have a look at the Drawing Graphics project.
Also, if anything is unclear about the way we handle the player’s input check out the Space Invaders project. The controls simply allow the player to hold the very bottom left or right of the screen to rotate left and right or hold anywhere above the bottom eighth to thrust. The setMovementState method of the Ship class is called with the appropriate value in each case.
I have liberally sprinkled comments throughout as a reminder of what the code does. Enter the following code in the HeadingAndRotationView class and then we can look at the Ship class which is where all the action takes place.
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 |
package com.gamecodeschool.headingandrotationproject; import android.content.Context; 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 HeadingAndRotationView extends SurfaceView implements Runnable{ Context context; // This is our thread Thread gameThread = null; // Our SurfaceHolder to lock the surface before we draw our graphics SurfaceHolder ourHolder; // A boolean which we will set and unset // when the game is running- or not. volatile boolean playing; // Game is paused at the start boolean paused = true; // A Canvas and a Paint object Canvas canvas; Paint paint; // This variable tracks the game frame rate long fps; // This is used to help calculate the fps private long timeThisFrame; // The size of the screen in pixels int screenX; int screenY; // The player's ship Ship ship; // When we initialize (call new()) on view // This special constructor method runs public HeadingAndRotationView(Context context, int x, int y) { /* The next line of code asks the SurfaceView class to set up our object. How kind. */ super(context); // Make a globally available copy of the context so we can use it in another method this.context = context; // Initialize ourHolder and paint objects ourHolder = getHolder(); paint = new Paint(); screenX = x; screenY = y; // Make a new player space ship ship = new Ship(context, screenX, screenY); } @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. */ timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame >= 1) { fps = 1000 / timeThisFrame; } } } private void update(){ // Move the player's ship ship.update(fps); } 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, 26, 128, 182)); // Choose the brush color for drawing paint.setColor(Color.argb(255, 255, 255, 255)); // Now draw the player spaceship // Line from a to b canvas.drawLine(ship.getA().x, ship.getA().y, ship.getB().x, ship.getB().y, paint); // Line from b to c canvas.drawLine(ship.getB().x, ship.getB().y, ship.getC().x, ship.getC().y, paint); // Line from c to a canvas.drawLine(ship.getC().x, ship.getC().y, ship.getA().x, ship.getA().y, paint); canvas.drawPoint(ship.getCentre().x, ship.getCentre().y,paint); paint.setTextSize(60); canvas.drawText("facingAngle = "+ (int)ship.getFacingAngle()+ " degrees", 20, 70, paint); // Draw everything to the screen ourHolder.unlockCanvasAndPost(canvas); } } // If the Activity is paused/stopped // shutdown our thread. public void pause() { playing = false; try { gameThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // If the Activity 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) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // Player has touched the screen case MotionEvent.ACTION_DOWN: paused = false; if(motionEvent.getY() > screenY - screenY / 8) { if (motionEvent.getX() > screenX / 2) { ship.setMovementState(ship.RIGHT); } else { ship.setMovementState(ship.LEFT); } } if(motionEvent.getY() < screenY - screenY / 8) { // Thrust ship.setMovementState(ship.THRUSTING); } break; // Player has removed finger from screen case MotionEvent.ACTION_UP: ship.setMovementState(ship.STOPPED); break; } return true; } } |
[widgets_on_pages id=”udemy_advert_java_3″][widgets_on_pages id=”udemy_code_details”]
Coding the Ship class
Now for the fun part. As this class contains some new ideas we will break it into a few parts and explain the code as we go along. Just enter each section of code after the previous section and at the end, you should end up with a fully working project.
The class declaration and member variables
After the import directives and class declaration, we declare four PointF objects to hold the x and y values for the vertices of the ship ( a, b, c and the center point of the ship ( centre)). Next, we declare and initialize facingAngle to 270. This will be the variable that keeps track of which way the ship is pointing.
Next, we have length and width which do as you probably suspect. We then have a speed variable which will be the speed of the ship in pixels per second. Next, we have horizontalVelocity and verticalVelocity which will hold values relative to each other and will enable us to move at a specific heading. We will multiply both of these variables by speed each frame so they are still relative to each other but quicker than the base values returned by our trigonometric functions.
We also declare and initialize rotationSpeed. We will see how we use this variable to lock the rate at which the spaceship rotates in a frame rate-independent manner. Finally, we have our ship’s movement states and the shipMoving variable to hold the current state. Enter the code below into the Ship class.
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 |
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 = 100; /* 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? 100 degrees per second */ private float rotationSpeed = 100; // Which ways can the ship move public final int STOPPED = 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 = STOPPED; |
Coding the Ship constructor
In the constructor, we make the length and width variables equal to one-fifth of the screen resolution, it will be quite a large ship but this is good for seeing what is going on. Then we initialize all four of our PointF objects as blank points.
Next, we initialize the members of the four PointF objects with the relevant coordinates. We make centre the exact center of the screen. Now it is important to draw the ship in its initial position at the same angle that we set facingAngle otherwise it will be out of sync from the start. We used 270 which is pointing straight up. Remember 0 degrees is facing east/3 O’clock and the angle grows bigger anti-clockwise).
We initialize the members of a, b and c by using centre, length and width. If you were to plot the points and join them up you would end up with a ship much like the image in the 2d graphics rotation tutorial. Obviously, the exact sizes will vary dependent on the screen resolution of the specific device you use. Add the constructor code to the Ship class.
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 |
/* 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){ length = screenX / 5; width = screenY / 5; a = new PointF(); b = new PointF(); c = new PointF(); centre = new PointF(); centre.x = screenX / 2; centre.y = screenY / 2; 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; } |
Ship class getters and setters
Now we have a bunch of getters and setters so the view can access and set the necessary members of the Ship class. These include a getter for each point and the facingAngle as well as setMoveMentState so that the onTouchEvent method from the view can change the movement state. Add these methods to the Ship class.
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 |
public PointF getCentre(){ return centre; } public PointF getA(){ return a; } public PointF getB(){ return b; } public PointF getC(){ return c; } float getFacingAngle(){ return facingAngle; } /* This method will be used to change/set if the ship is rotating left, right or thrusting */ public void setMovementState(int state){ shipMoving = state; } |
The update method
As this method holds all the magic that we learned about in the trigonometric functions part 1 and part 2 tutorials we will break it into a few pieces to be clear about what is going on. If anything is unclear be sure to read those tutorials.
The first part of the update method is the simplest. You can see that if shipMoving equals LEFT or RIGHT we simply reduce or increase facingAngle by rotaionSpeed divided by fps. We also check to see if the facing angle has reached the full extent of a rotation (less than 1 or greater than 360) if it has we set it to the appropriate reset angle. However, note the very first line of code where we initialize a local variable previousFA to whatever the current value of facingAngle is. We will need this value when we do the rotation math because we use the angle of change when rotating not the actual current angle. Remember how we rotated a point by 180 degrees? This time we will be rotating by tiny fractions of 1 degree for each frame so we need the value for change (facingValue – previousFA). We will see this in action soon. Enter the first part of the update method.
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 |
/* 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){ /* 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 -rotationSpeed / fps; if(facingAngle < 1){ facingAngle = 360; } } if(shipMoving == RIGHT){ facingAngle = facingAngle + rotationSpeed / fps; if(facingAngle > 360){ facingAngle = 1; } } |
Now we handle what happens when the player is thrusting.
Here we use our heading algorithms to calculate the horizontal and vertical velocities. Remember the important thing about the values returned is the ratio of the two values to each other.
Next, we update the coordinates of each and every point using the newly calculated horizontalVelocity and verticalVelocity. We also multiply by speed to maintain our desired pixels per second and divide by fps for smooth frame rate independent motion.
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 |
if(shipMoving == THRUSTING){ /* 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. */ horizontalVelocity = (float)(Math.cos(Math.toRadians(facingAngle))); verticalVelocity = (float)(Math.sin(Math.toRadians(facingAngle))); // move the ship - 1 point at a time 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; } |
Now we have moved our points we can rotate them. We declare some local variables tempX and tempY to temporarily hold the values of the rotations. Starting with the point a we subtract the centre coordinates, both x and y from it.
We then rotate these newly acquired object space coordinates and assign the result to our temporary variables. Notice that the angle we pass in is facingAngle - previousFA and is wrapped in the Math.toRadians method.
Then we reassign to the actual point a along with adding back the x and y values held in centre.
So we converted the coordinates to object space, rotated them, then moved them back to world space. We do the same for points b and c then we are done. Add the last part of the update method and you can run the game.
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 |
/* Now rotate each of the three points by the change in the rotation this frame facingAngle - previousFA */ float tempX = 0; float tempY = 0; // 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; }// End of update method }// End of Ship class |
What next?
Soon I will publish another full-game tutorial that uses these techniques. If we have dozens of rotating game objects then we will need a smarter way of handling them, probably an array. Also, it is worth noting that Math.sin and Math.cos are computationally expensive. So, if we kept adding more objects then eventually the game would begin to lag. One simple solution to this is to create look-up tables for the values of sine and cosine. This could be just an array with the returned values of sine and cosine perhaps for all the whole numbers 1 through 360. We could then stop calling Math.toRadian, Math.cos and Math.sin just do something like this, arrayCOS[facingAngle- previousFA] instead.
I hope you enjoyed the tutorial please comment if you liked it or if you didn’t and if you have any thoughts about it. Also, I am trying to get followers/friends on Facebook and would really appreciate it if you can like or follow Game Code School.
First of all, very great tutorials!! keep it up.. Thank you very much for your Tutorials, it is very educating, very detailed explanations, clean codes and no bugs at all, I was able to run it straight away after I wrote all the codes, and it is written so neat and clear.
However, after I finished the project, my ship is only able to move up(forward) when I press anywhere across the screen, could you help me point out where I might go wrong ?? Thank you again. Best Regards.
Hi edwinius. Thanks very much for your kind words and question.
Is the ship rotating when you touch the bottom left and right of the screen? If not I think it is because I have missed out the step which makes the app full screen. If the app is not full screen, on many devices the pixels which were calculated as the bottom eighth are not even visible and hence not touchable. I have added a new section to explain how to do this in the section headed “Making the game full screen landscape” right at the start of the tutorial.
I would be really interested to know if this solves your problem. Thanks again and sorry if I have caused you bother.
John
Hi John. Sorry for late reply. I just added the section that you mentioned above, and it works like a charm! the ship is rotating, but it only rotate when I pressed the screen at the bottom corner, if I pressed the screen at the top corner, the ship will go straight(THRUSTING action I guess), is it supposed to be like that?
Anyway, your tutorial is educating enough for android dev beginners like me.
Thank you so much again.
Cheers, Edwinius.
Hi Edwinius,
Yes, the bottom eighth of the screen rotates left and right depending upon which half of the bottom eighth you press and hold. Anywhere else is forwards. Take a closer look at this bit of code to understand that and make the (rotating) area higher/bigger by swapping the 8 for a lower number.
if(motionEvent.getY() > screenY – screenY / 8)
Thanks for commenting.
Hi
Great tutorials. Started learning game programming and was directed to this site.
Tried this tutorial but showing me “unfortunately heading and rotation stopped” error. :<
Hi there, Run the game in an emulator or via USB debugging mode on a device and you will get more specific detailed feedback of the cause in the logcat window. If the feedback doesn’t help you solve it feel free to share the errors and I will try and help.