Recently I’ve started mentoring a local high school student a bit on implementing a video game, and this is a technical note toward that.
Drawing basic shapes out of polygons to represent game objects is straightforward and requires just a bit of trigonometry, outlined here.
Shapes
The core idea is that the polygon is located around the object’s current position. So, a standard looking Asteroids ship might be defined as four points about the origin as in this diagram:
The polygon is captured by a list (array) of points in order around the shape. Caveat other restrictions in the game’s code, it doesn’t really matter if they’re clockwise or counter-clockwise. In this case the ship is defined as follows, proceeding counter-clockwise:
- (0, 24)
- (-18, -24)
- (0, -18)
- (18, -24)
One cute trick that’s often done in Asteroids is to randomly generate the polygons for the asteroids themselves. The points list needs to be in order though or else the lines will overlap and the shape look funny. There are several ways to do this, but one is to go around in a circle, picking a random angle within that arc of the circle, picking a random distance from the origin for that tip of the rock, and computing the x & y value for that polygon point using that angle and distance.
Drawing
In most polygon graphics APIs, drawing starts by starting a new shape if necessary (i.e., newpath
) and moving the cursor to the first point in the list (i.e., moveto
). It then loops over each of the other points and draws a line from the previous position to the current point (i.e., lineto
). The path is then either closed by drawing a line to the first point in the list from the last, or using a specific function to close the path if the API has one (i.e., closepath
).
Rotation and Placement
Of course, in the game the object typically has to rotate and move. Rotation is simple because we’re taking the object’s current position as the origin of the polygon representing it. Each point to draw just needs to be calculated through the basic rotation formula:
x' = (x * cos(angle)) - (y * sin(angle))
y' = (x * sine(angle)) + (y * sin(angle))
In these formulas, x
and y
are the current point in the polygon list while x'
and y'
are the actual points to draw. The direction the object is currently rotated to, i.e.,which way the player is facing, is in angle
.
This will draw the polygon around the origin, but of course the object is actually somewhere else on screen. This is a simple translation, effectively moving the polygon’s origin to the object’s actual position. In other words, just adding the object’s x and y coordinates to the point to draw:
x' = x' + objectx
y' = y' + objecty
Minor Complications
Although the ship above has somewhat naturally been modeled facing up, angle 0 in trigonometry is actually facing to the right. So, the polygon should instead be modeled with its natural direction facing that way.
Adding just a small detail, essentially all modern computer displays and most software use a slightly different coordinate system from what’s typically used in mathematics: The origin is at the top left of the screen, and the y axis increases going down the screen, not up. Note that this means the 90 degree angle is actually facing down and 270 degrees points straight up.
A wide variety of ways to work with these facts can be applied, but the easiest is just to model the polygon facing to the right and to keep that coordinate scheme in mind. So, the example polygon above would actually be modeled as follows:
The counter-clockwise ordering of points would then be:
- (24, 0)
- (-24, -18)
- (-18, 0)
- (-24, 18)
Another small detail to keep in mind is that nearly all trigonometry functions in software libraries are based on radians rather than degrees, though most people work more easily in the latter. Converting between the two just requires a simple formula based on the identity relationship between them:
radians = pi * degrees / 180
Code
A simple demonstration of this is in the box below. Pressing the left and right arrow keys make the ship rotate, and it’s being drawn in the center of the screen rather than the origin (you may have to click on the box first to focus the keyboard on it):
The code snippets for this below are in Javascript, but should be easily applicable to most platforms.
The polygon for the ship has been defined as follows:
var playershape = [ { x: 24, y: 0}, { x: -24, y: -18}, { x: -18, y: 0}, { x: -24, y: 18}, ]
It’s just an array of points, each a Javascript object with an x and y value. There is also a player object that has its own x, y, and angle, representing the player’s position and orientation on screen.
A couple trigonometry helper functions are defined, to convert degrees to radians, and to rotate x and y values:
function degtorad(angle) { return 3.1415926 * angle / 180; } function rotx(x,y,angle) { return (x*Math.cos(angle)) - (y*Math.sin(angle)); } function roty(x,y,angle) { return (x*Math.sin(angle)) + (y*Math.cos(angle)); }
Note that each of the x and y rotations take as input x, y, and angle, because the rotation formula requires each of those values.
As discussed above, the ship is drawn by starting a path at the first point of the polygon, looping through each other point, and then closing it off. At each step the point to draw is rotated about the ship’s position as the origin, and then translated to the ship’s actual position on screen. This function captures that, and is called by the main drawing routine each time the ship needs to be displayed:
function drawplayer() { var x, y; // These will be the point to draw. var i = 0; // i is the current polygon point we're using ctx.beginPath(); // ctx is the graphics drawing context in this Javascript program. // Calculate the actual draw point by rotating and then translating. x = rotx(playershape[i].x, playershape[i].y, player.angle) + player.x; y = roty(playershape[i].x, playershape[i].y, player.angle) + player.y; ctx.moveTo(x, y); // Start the polygon at this point. // Loop through the other points---note that this therefore begins at point 1, not 0! for (i = 1; i < playershape.length; i++) { x = rotx(playershape[i].x, playershape[i].y, player.angle) + player.x; y = roty(playershape[i].x, playershape[i].y, player.angle) + player.y; ctx.lineTo(x, y); // Extend the path from the previous point to this new one. } ctx.closePath(); // Close the path by adding a line back to the start. ctx.stroke(); // Draw the path. }
To initialize the player, its position is set to the middle of the screen and its orientation set as facing straight up:
player.x = canvas.width / 2; player.y = canvas.height / 2; player.angle = degtorad(270);
Each time the left or right key is pressed, the ship’s angle is updated like this.
player.angle -= degtorad(10); // Decrease the angle by 10 degrees, making the // conversion to radians first before subtracting // from the current angle. while (player.angle < 0) { // This actually isn't necessary, but just makes sure player.angle += 3.1415926 * 2; // the player's angle is always between 0 and 2*pi. } // The drawing routines and other logic will all // handle that fine, but it can make things easier for // the programmer in writing other parts of the code.
Conclusion
Those are the basic elements in drawing a simple game like Asteroids. The next complication is having an array or arrays of game objects. That’s necessary to capture all of them that might appear on screen, namely the rocks and bullets. A drawing function like that above then needs to be applied to each game object in the array(s), rather than being just hardcoded to a single game object instance like this example is to the player’s ship.