Platform: ARM-CAN kit

This racing game is a demonstration of using ARM-CAN UserInterface module to display 2D graphics on the LCD and use the joystick. The game itself is very simple – user has to drive the car on the track as fast as possible and there are no opponents nor obstacles on the track. Steering and driving is accomplished by moving the joystick. The program is made of several C source files which each take care of different tasks.

There are structure data types to hold information about 2D points, track sections and the car. All of them are declared in “types.h” file.

typedef struct { short sX; short sY; } tPoint; typedef struct { tPoint pPosition; unsigned short usRadius; } tTrackSection; typedef struct { short sRadius; unsigned long ulAngle; } tPolarPoint; typedef struct { long lVelocity; long lSteering; tPoint pPosition; unsigned long ulAngle; unsigned long ulColor; unsigned long ulLapTime; tBoolean bOnFinishLine; } tCar;

StellarisWare has a function to get the sine of different angles, but there is a need for cosine and arctangent functions. These functions are written in the source file named “trigo.c”. The cosine function is a simple π/2 - sin(α) calculation, but for arctangent, look-up table is used. *arctangent2* is an extension of *arctangent* which gives full 360 degrees output for a line specified by the x and y coordinates.

long cosine(unsigned long ulAngle); unsigned long arctangent(unsigned long ulRise); unsigned long arctangent2(signed long y, signed long x);

As there are no polygon drawing functions in StellarisWare Graphics Library it is implemented in “polygon.c” file. The file contains two main functions to draw polygons - one for drawing only the edges and one for drawing a filled polygon. *GrPolyDraw* is the first function, which uses line drawing function to connect the points of polygon. *GrPolyFill* is the second one, which uses horizontal line drawing function to fill up the polygon. Both of these functions are used to draw the track and the car.

void GrPolyDraw(const tContext *pContext, const tPoint *pPoints, unsigned long ulNumPoints, unsigned char ucClosedLoop); void GrPolyFill(const tContext *pContext, const tPoint *pPoints, unsigned long ulNumPoints);

All the graphics of the game are first rendered on the off-screen buffer and then written to the LCD. This method avoids flickering. The off-screen buffer is initialized in the *RendererInit* function and outputted in the *RendererRender* function. Both of these function are in the “renderer.c” file. As there is movement in the game, the *RendererSetViewPoint* function is used to set the current viewpoint. Viewpoint coordinates in turn, are used in the *RendererWorldToScreenPoint* function to transform the coordinates of the world (track, car, etc) points to screen points, so that the viewpoint is always rendered in the middle of the screen.

“renderer.c” does not contain all the rendering code, actually it only clears the off-screen buffer and calls specific track and car rendering functions.

void RendererInit(void); void RendererSetViewPoint(tPoint *pPoint); void RendererRender(void); void RendererWorldToScreenPoint(tPoint *pWorldPoint, tPoint *pScreenPoint);

The track is defined by the series of points which form a closed loop, so it is like a polygon. The points are specified in virtual centimeters and for each point, which define the end of the track section, there is also a width/2 (radius) of the track. Here is a short example of track data:

#define NUM_TRACK_SECTIONS 10 tTrackSection g_pSection[NUM_TRACK_SECTIONS] = { { 0, 1000, 400 }, { 0, 1200, 400 }, { 0, 3000, 400 }, { 1000, 6000, 400 }, { 6000, 6000, 400 }, { 7000, 5000, 400 }, { 7000, 0, 400 }, { 6000,-1000, 400 }, { 1000,-1000, 400 }, { 0, 0, 400 } };

To get the edges of the track, *TrackGenerate* function is used. It is probably the most complicated part of the code aswell. Simply speaking, this function makes the track lines of sections bold and gets rectangles with specified width. As the track sections are not laid crossways, trigonometric functions are needed to calculate four points of these randomly oriented “rectangles”.

But there are problems - these rectangles are not connected like a racing track might be. Too understand it, take a look at the blue and green rectangle on the picture. Firstly, there is a gap on the outer side of corner, secondly the rectangles intersect in the inner corner. Intersection point of two rectangles is solved in *GetLinesIntersectionPoint* function. To speed up drawing the track, it is converted from sections to two polygons which define inner and outer edges of the track. So the intersection point replaces corner points of intersecting rectangles and gap is filled by simply drawing a line between two consecutive edges.

The rendering of the track in *TrackRender* function is rather trivial. As the track is defined by inner and outer edge, polygon drawing functions are used to first fill the area inside the outer edge and then inside the inner edge.

Because it is not an offroad racing game, some collision detection is needed to check if the car is on the road or not. This check is achieved with the help of *PointInPolygon* function which contains an clever algorithm proposed by Bob Stein. The collision detection function *TrackPointOnTrack* returns true when the point is in the outer edges polygon but not in the inner edges polygon.

void TrackGenerate(void); void TrackRender(tContext *pContext); tBoolean GetLinesIntersectionPoint(tPoint *pA1, tPoint *pA2, tPoint *pB1, tPoint *pB2, tPoint *pC); tBoolean PointInPolygon(tPoint pPolygonPoints[], unsigned long ulNumPoints, tPoint *pPoint); tBoolean TrackPointOnTrack(tPoint *pPoint); tBoolean TrackPointOnFinishLine(tPoint *pPoint);

The car in the game is also made up of polygons. It has a rectangular body and roof polygon, plus two circles as headlights. As the viewpoint do not rotate, the car has to rotate to give the right visual effect. Rotating means trigometric functions and it is most easily achieved by converting polar coordinates to Cartesian coordinates. That is why the points of the car are defined by radius and angle as seen below.

tPolarPoint pBodyPoints[4]; pBodyPoints[0].sRadius = 200; pBodyPoints[0].ulAngle = DEGREES_TO_FIX32(330); pBodyPoints[1].sRadius = 200; pBodyPoints[1].ulAngle = DEGREES_TO_FIX32(30); pBodyPoints[2].sRadius = 200; pBodyPoints[2].ulAngle = DEGREES_TO_FIX32(150); pBodyPoints[3].sRadius = 200; pBodyPoints[3].ulAngle = DEGREES_TO_FIX32(210);

All the points of the body parts are converted to x, y coordinates in *CalculateScreenPoint* function. Both the coordinate system conversion and world to screen point conversion take place there. Rendering is done in *CarRender* function by polygon filling routines.

void CalculateScreenPoint(tPoint *pScreenPoint, tPolarPoint *pLocalPoint, tPoint *pPosition, unsigned long ulAngle); void CarRender(tContext *pContext, tCar *pCar);

The main function resides in “racing_game.c”. That is the function which initializes all the peripheral devices, calls preparation functions and starts the main loop. The loop contains only few lines of code: joystick handling, car dynamics calculation and rendering. Joystick handling is very easy because of the joystick driver - it just gives x and y coordinate. The joystick x coordinate is used to steer the car and y coordinate to apply driving power. Actually the formulas of simulating the movement of the car are quite tawdry, because they do not take any of the physical laws into account. All the movement is done in a similar manner to as converting polar coordinates to Cartesian ones with different limitations of maximum speed on the road and off the road.

int main(void); void DoCarDriving(tCar *pCar); void PrepareCar(void);