Figure 11. Output after completing this step.
In this tutorial step we will implement all of our core game logic. Many of the concepts introduced in this step could be adapted to different types of game. There will be many opportunities to optimise the code presented but we'll resist the temptation until the game is functionally complete.
Up until now, most of the code syntax looks very similar to plain-old Java. However, in this tutorial step we'll explore some of the features unique to WSL (or at least features borrowed from programming languages outside of Java). If you've not already done so, read through the WSL reference article on the developer wiki.
Before we create any of the game logic, we need to determine what the mechanics and rules of play are for "Paddle Game". You probably already have an idea of how a pong-type game should play, or have worked them out just by looking at Figure 11. However, let's provide a fairly loose definition in writing:
Figure 12. "Paddle Game".
Some of these rules are deliberately left flexible/open to interpretation, allowing us to explore and test out different mechanics until we hit upon something that is fun to play. Often, small changes in the game parameters or logic can often make or break the game experience – experimentation is key.
Our game requires two types of game objects:
To represent our game objects in code, we will use Structs. With structs we can declare new data-types – that is, a collection of other data-types (C/C++ programmers will be familiar with structs already). So, although we're limited to a single global class with WidSets, structs help us to organise related data together in an object-oriented way (they behave like Java classes with public fields and no methods).
Add the following just beneath the Variables section in the WSL source code:
/*-------------
| Structures |
-------------*/
struct Paddle
{
int x_f;
int y_f;
int dy_f;
int size;
int flags;
int score;
String scoreText;
}
struct Ball
{
int x_f;
int y_f;
int dx_f;
int dy_f;
int speed_f;
int angle;
}
Figure 13. Game field objects.
The first two fields in the Paddle struct (x_f and y_f) are fixed-point representations of the paddle's position on the game field. These two values are measured in world units. In fact, as our game objects live in the game world, we can safely assume that all the struct fields relating to position, size, speed etc. are measured in world units.
Figure 13 shows how we'll scale and position our game objects within the world. The objects are shown in example positions to show how their world coordinates relate to their positioning, although obviously the ball's x, y and paddles' y positions will be changeable in the real game. Note that the dark grey lines mark out a 32 x 32 grid which represents the world size. Both the paddles have a width of one world unit (wu) and the ball (okay, so it's square!) has a width and height of 1 wu. We don't store this information in the structs because these properties will remain fixed. The red crosses in the diagram show where the reference points (x_f, y_f) are positioned for each game object:
paddle1. The vertical centre of the paddle's right edge.
paddle2. The vertical centre of the paddle's left edge.
ball. The top-left corner of the ball.
We have shifted the reference position for each paddle to the edge facing into the game field because it should make the code slightly easier to read when we implement collision handling. The size field of the paddles represents how long each paddle is measured from the reference point to the top or bottom edge (Figure 13). We may dynamically adjust the paddle sizes depending on certain game conditions e.g. difficulty level, number of points played etc. The dx_f and dy_f fields will represent the x, y components of the object's movement (displacement vector) – the paddles only move up and down (change in y), so do not need a dx_f property. Both paddles are positioned 1 wu from the edge of the world (so essentially x_f is fixed). We'll use the flags field of the Paddle struct later on in this tutorial to store various Boolean state information. The score fields will keep track of how many points the paddle instance (i.e. player) has scored in the current game. We have created a separate scoreText string because we don't want to create a new string to represent score each frame (avoiding unnecessary object creation).
The last two fields in the Ball struct represent it's velocity in polar coordinates, that is, the speed at which the ball is travelling and the direction it is heading. We will need to convert these polar coordinates to Cartesian coordinates in order to display the ball's movement – Figure 13 demonstrates that we can perform the conversion using trigonometry.
Figure 13. Converting polar coordinates (speed, angle) to Cartesian coordinates (dx, dy).
As you'll see later on in the tutorial, we want to alter the angle at which the ball is reflected depending on where it hits the paddle (see the video in Figure 11). We also want to increase the ball's speed the longer a particular game point lasts i.e. as the number of hits received in a rally increases. It is easiest to manipulate the reflected angle and changes in speed using a polar coordinate system. However, when the ball's moving through the game field unimpeded, the values of dx_f and dy_f will not change (it will have a constant velocity). When the game's functionally finished, it would make sense to optimise our code so that we only calculate new values for dx_f and dy_f when a collision occurs (although the sine and cosine functions in WidSets are efficiently implemented using look-up tables). If we wanted to introduce some advanced ball dynamics, such as swerving the ball after it has hit the paddle in a certain position, we are probably better off converting from polar to Cartesian coordinates every frame, as is the case in this tutorial step. Again, we are avoiding optimising early in order to keep the code flexible and manageable.
Let's create references to our three game objects. Add this code to the Variables section of the WSL source code:
Paddle paddle1; Paddle paddle2; Ball ball;
Now we need to initialise our three game objects. Add the following to the top of the openWidget() function:
paddle1 = new Paddle(); paddle2 = new Paddle(); ball = new Ball();
This results in the creation of two Paddle instances and one Ball instance. As always, we should take care to free up our objects from memory when we don't need them anymore. So add the following to the cleanup() function:
paddle1 = null; paddle2 = null; ball = null;
When we start a new game, we should reset our game object fields to default positions. Add this code to the top of newGame():
paddle1.x_f = ONE_F; paddle2.x_f = WORLD_SIZE_F - ONE_F; paddle1.score = (paddle2.score = 0); paddle1.scoreText = (paddle2.scoreText = String(0)); paddle1.size = (paddle2.size = PADDLE_SIZE_INIT); resetPaddle(paddle1); resetPaddle(paddle2);
Firstly we set the paddles to their correct x position in the world (see Figure 13) and also reset the score for each paddle. Note that by using parenthesis as shown, we can set both variables at once (which saves us some byte code). Then we set the initial size of each paddle to PADDLE_SIZE_INIT – we haven't defined this yet, so we need to add it to our list of constants:
const int PADDLE_SIZE_INIT = 4;
The right paddle (paddle2) in Figure 13 shows how long the paddle will be in the game world with a size of 4 (for comparison, paddle1 is shown with a size of 3).
After we've reset the score and size, we also need to reset the positioning and movement for each paddle. That's what the resetPaddle(Paddle) function will do. We will create a separate function to perform this because we will also need to reset paddle positioning after a point is scored i.e. we can re-use this code later on. Define the following function just below the newGame() function:
void resetPaddle(Paddle p)
{
p.flags = 0;
p.y_f = WORLD_SIZE_F / 2;
p.dy_f = 0;
}
In this function we reset the dynamic components of the Paddle instance p (given as an argument to the function). This includes resetting the Boolean flags (covered later), vertically centering the paddle in the world and cancelling any movement. Note, this last step isn't strictly necessary in the current implementation, but it may be if we add more advanced paddle dynamics e.g. acceleration.
We also need to set the ball's position and movement when we start a new game or when a point is scored. Add the following function just underneath our resetPaddle(...) function:
void setBall(Ball b, int x, int y, int speed, int angle)
{
b.x_f = x;
b.y_f = y;
b.speed_f = speed;
b.angle = angle;
}
This function has five parameters: a Ball instance (b) and four other arguments which determine the new position and movement properties of the ball.
Add the following call to setBall(...) beneath the code to reset our paddles in newGame():
setBall( ball, (3 * WORLD_SIZE_F / 4) - (ONE_F / 2), (WORLD_SIZE_F / 2) - (ONE_F / 2), BALL_SPEED_INIT_F, 135 + random(90));
We've introduced a new constant here BALL_SPEED_INIT_F which represents the initial speed of the ball when we start a new game. Add the following to the list of constants:
int BALL_SPEED_INT_F = 1500;
It should be easier to interpret the result of passing these values by going through each argument and correlating it with the positioning and movement shown in Figure 14. Note that we use the value of ONE_F / 2 to reference the ball from the centre (recall that its reference point is defined at the top-left corner):
Figure 14. Game objects' reset positions.
This diagram shows the positions the game objects are reset to each time we start a new game. These are the same positions we will reset the objects to when a point is scored (except that ball's position and movement may be mirrored about x = 16 depending on which player scores the point – this will become clearer when we implement this in code). Note that the direction the ball faces initially is an angle between 135 and 225° (we use a call to the API method random(int max) to generate a random number between 0-90).
Let's now change the paint() function so that we can see the game objects on the canvas. First though, we'll need a function to convert world coordinates to screen coordinates (pixels). Add the following to the Functions section of the WSL source code:
int, int w2p(int x, int y)
{
return f2i(x * scale), f2i(y * scale);
}
This function accepts two arguments x and y – these represent an (x, y) coordinate measured in fixed-point (FP) world units. Remember that scale defines the number of pixels per world unit. Therefore, to convert from world units to pixels we multiply the coordinate (x or y) by scale and convert the format from fixed-point to integer using the f2i() function we defined in the first tutorial step. We also scaled the game canvas specifically so that scale had an integer value – if it did not, scale would need to be an FP number (i.e. scale_f) and we would have to use the fixed-point multiply function (i.e. ml(x, scale_f)) here instead.
Note that this function returns two values i.e. a tuple (list of values) – another very useful WidSets feature.
Delete the code in paint() which was responsible for drawing the frame count (tick) and animated red arc on the canvas (left over from the last tutorial step) and replace it with the following code so that we can see our game objects on the canvas (do not delete the sections of code which detect display orientation change or draws the paused or FPS information – this is indicated by ...'s):
void paint(Component c, Graphics g, Style s, int width, int height)
{
... detect orientation change ...
// Draw field
g.setColor(0x606060);
g.drawLine(width / 2, 0, width / 2, height);
g.drawLine(0, 0, width, 0);
g.drawLine(0, height - 1, width, height - 1);
// Draw scores
g.setFont(getFont(FACE_SYSTEM, STYLE_BOLD, SIZE_LARGE));
g.setColor(0xFFFFFF);
g.drawString(paddle1.scoreText, width / 4, scale, TOP | HCENTER);
g.drawString(paddle2.scoreText, width - width / 4, scale, TOP | HCENTER);
// Draw paddle 1
int x, int y = w2p(paddle1.x_f, paddle1.y_f);
g.fillRect(x - scale, y - scale * paddle1.size, scale, 2 * scale * paddle1.size);
// Draw paddle 2
x, y = w2p(paddle2.x_f, paddle2.y_f);
g.fillRect(x, y - scale * paddle2.size, scale, 2 * scale * paddle2.size);
// Draw ball
x, y = w2p(ball.x_f, ball.y_f);
g.fillRect(x, y, scale, scale);
g.setFont(getFont(FACE_SYSTEM, STYLE_PLAIN, SIZE_MEDIUM));
... display paused message and FPS ...
}
Figure 15. Game objects.
Try testing out the widget at this stage, your canvas should look similar to that shown in Figure 15. If you experience errors, try downloading the complete widget and checking this code against your code.
Let's analyse this new code in the order of execution in paint():
g.drawLine(...), to mark out the game field. In the first call we draw the halfway line, and in the second and third we draw the sidelines. Open up the stylesheet (style.css) and change the gameFlow element background property from green to black. If you run the widget again, you should be able to see the sidelines better against the black background (leave it black).
g) font to a large, bold typeface. For each player, we then draw in white their current score between the position of their paddle (baseline) and the halfway line.
w2p() function here by converting each paddle's world coordinates to pixel coordinates. Our paddles are a single world unit wide. We don't have to convert this width to pixels because by definition 1 wu equals scale, so we can use the value of scale directly.
g.setFont(...).
Visit the Graphics section in the Apidocs for a complete list of the graphics operations available.
Figure 16. Key input.
It's time to introduce some movement into the game...
By definition games are interactive, they require some kind of input to drive the core gameplay (otherwise they are passive experiences like a watching a screensaver). Let's implement code to detect the key input on the phone that we can then use to control the movement of our paddles. As the paddle movement is constrained to the vertical, it seems to make sense to directly map the up/down navigation keys to the up/down movement of our paddles (Figure 16). In this tutorial, we will allow a single player to control both paddles. However, for reasons of game longevity, challenge, fun etc. it would be better to introduce a mode whereby a human player could play against the CPU (using AI with different skill levels), or a mode that allowed two people to share the phone keypad for a two-player game (obviously the handset would need to support multiple key presses for this).
We are already listening to key events in our game (when in the paused state, pressing '5' or FIRE starts/resumes the game). A single KEY_PRESSED event is sent to the keyAction(...) listener when a key is pressed. This is fine for un-pausing the game, but we require a different form of interaction for moving the paddles. Our paddles should move up/down for as long we hold down the up/down keys. Therefore, we need to store key state information in order to determine in the game loop if the up/down key is being held down or not. This is where the flags field of the Paddle struct comes in useful.
We'll use the flags variable in the Paddle as a bit field. Essentially this means using a single integer to represent multiple Boolean values, using bitwise operators to set, reset and test the various "flags" in the bit field. Add the following constants to the WSL source code:
const int FLAG_KEY_UP = 0b00000001; // 1<<0 = 1 const int FLAG_KEY_DOWN = 0b00000010; // 1<<1 = 2 const int FLAG_MISSED_BALL = 0b00000100; // 1<<2 = 4
Note that here we've just demonstrated another feature available in WSL – binary literals. We will store the key up/down states in the bits represented by FLAG_KEY_UP and FLAG_KEY_DOWN. The third flag, FLAG_MISSED_BALL, will come in useful when we need to track whether a paddle has just missed the ball or not.
Modify the keyAction() interface to the following:
boolean keyAction(Component source, int op, int code)
{
if(source.equals(gameShell))
{
if(op == KEY_PRESSED)
{
switch(code)
{
case KEY_FIRE:
case '5':
if(isPaused)
resumeGame();
break;
case KEY_UP:
paddle1.flags |= FLAG_KEY_UP;
paddle2.flags |= FLAG_KEY_UP;
break;
case KEY_DOWN:
paddle1.flags |= FLAG_KEY_DOWN;
paddle2.flags |= FLAG_KEY_DOWN;
break;
}
}
else if(op == KEY_RELEASED)
{
switch(code)
{
case KEY_UP:
paddle1.flags &= ~FLAG_KEY_UP;
paddle2.flags &= ~FLAG_KEY_UP;
break;
case KEY_DOWN:
paddle1.flags &= ~FLAG_KEY_DOWN;
paddle2.flags &= ~FLAG_KEY_DOWN;
break;
}
}
return true; // Consumed
}
return false; // Key event not consumed
}
The use of bit fields can make our code slightly harder to read in places. The real advantage of using bit fields comes when we have have lots of objects that each need to hold multiple Boolean variables. The boolean type in WidSets is internally handled as an integer, whereas a single integer bit field can store 31 Boolean flags (the first binary digit of an integer's 32 bits is the sign bit and cannot be used). We will keep using bit fields for now as it demonstrates a useful concept which can come in useful if we need to save memory.
In keyAction() we now respond to KEY_PRESSED and KEY_RELEASED events generated when the up/down keys have been pressed/released. For example, if we receive a KEY_PRESSED event for the up key (KEY_UP) we ensure that the FLAG_KEY_UP bit is set (is equal to one) in the flags field of both paddles (using the bitwise OR operator). We will do the same thing if the down (KEY_DOWN) key has been pressed. When the up/down key is released we unset the corresponding flags (set to zero) using the bitwise AND operator in conjunction with the bitwise NOT operator. A detailed overview of bitwise operators can be found at http://www.vipan.com/htdocs/bitwisehelp.html.
In the remainder of this tutorial, we'll be implementing most of our game logic directly inside the game loop (in nextFrame()).
In tutorial step 2 we created the basic structure of our game loop:
We are now ready to start implementing game object movement (2). Add the following code inside the Move objects section in nextFrame() (inner functions are supported in WSL):
movePaddle(paddle1);
movePaddle(paddle2);
void movePaddle(Paddle p)
{
if(p.flags & FLAG_KEY_UP)
p.dy_f = -30000;
else if(p.flags & FLAG_KEY_DOWN)
p.dy_f = 30000;
else
p.dy_f = 0;
p.y_f += p.dy_f;
}
Try running the widget. You should be able to move the paddle up and down using the up/down keys. The function movePaddle() takes an argument of type Paddle. It then checks this Paddle instance's key flags to see if it should apply movement up or down. We have created movePaddle() as an inner function here because it helps encapsulate functionality (we don't need to call movePaddle() from outside our game loop) and it allows us to directly access variables in the enclosing function (helps reduce parameter count). Remember that dy_f represents the paddle's change in y position. We therefore need to add dy_f to y_f each frame to update the paddle's position (as shown in the last statement). Also, bear in mind that the display coordinates are referenced from the top-left, so adding a positive value to y_f will result in the paddle moving downward.
Try changing the FPS_TARGET constant to a value of 5 (i.e. five frames per second), then run the widget again and move the paddles up and down. Unsurprisingly the paddles move a lot slower. This experiment tells us that if a particular phone is unable to keep up with our target frame rate, the action will move a lot slower. Not only is this frustrating for users with less powerful phones, but it also makes comparing scores between different handsets difficult (many of the games on WidSets have global high score lists). This is where delta can help us out.
To overcome the problem described above, we can define motion in terms of movement per unit time (ms) rather than per frame. If you recall, the tick() function implemented earlier returns delta, the time difference between the last and current frame. If we define the speed of an object's movement as the number of world units it will travel per millisecond, we can multiply this by delta to determine how far the object will have travelled during the time elapsed since the last frame. Note that this only applies to objects with a constant velocity or objects that change velocity instantly. Calculating acceleration using delta is a bit trickier, although it is still achievable using equations of motion.
Add the following constant to the WSL source code:
const int PADDLE_SPEED_F = 1500;
This constant represents the number of FP world units travelled per millisecond by the paddle if it is moving up or down. Now modify the movePaddle() function to the following:
void movePaddle(Paddle p)
{
if(p.flags & FLAG_KEY_UP)
p.dy_f = -PADDLE_SPEED_F;
else if(p.flags & FLAG_KEY_DOWN)
p.dy_f = PADDLE_SPEED_F;
else
p.dy_f = 0;
p.y_f += p.dy_f * delta;
}
Now try testing the widget with FPS_TARGET set to 15, then change this to 35 and retest. Although the movement of the paddles seems a lot jerkier when the frame rate is low, the speed at which they move is the same (try measuring the time it takes to move the paddle from the top to the bottom of the screen 10x for each different frame rate). Note that in the tick() we place a maximum limit on delta, so if the frame rate is below 12.5 fps (1000/80) then the action will slow down! Leave the target FPS set at 35 for now.
Let's now add the equivalent function for the ball (or any Ball instance). Add the following underneath the movePaddle() function:
moveBall(ball);
void moveBall(Ball b)
{
int speed_f = b.speed_f * delta;
b.dx_f = ml(speed_f, cos(b.angle) * 2);
b.dy_f = ml(speed_f, -sin(b.angle) * 2);
b.x_f += b.dx_f;
b.y_f += b.dy_f;
}
The first statement in the moveBall() function adjusts the speed for delta. We then calculate the displacement vector (dx_f, dy_f) (see Figure 13).
Try running the game. The ball should move from right-to-left passing through any paddles or walls in its path. To prevent this, we need to implement collision routines.
There are three different kinds of collision involved in Paddle Game:
Let's start by handling collisions between the paddles and the world bounds. Add the following to the Collision handling section of our game loop:
collisionPaddle(paddle1);
collisionPaddle(paddle2);
void collisionPaddle(Paddle p)
{
int s = i2f(p.size);
// Top
if(p.y_f < s)
{
p.y_f = s;
p.dy_f = 0;
}
// Bottom
else if(p.y_f > WORLD_SIZE_F - s)
{
p.y_f = WORLD_SIZE_F - s;
p.dy_f = 0;
}
}
The first statement in the function converts the size field of the Paddle instance (p) to FP. We have to do this because we need to perform comparisons using other attributes of p which are already in FP. The if .. else construct if used to check if any part of the paddle exceeds the world bounds. If it does, we move the paddle so that it is fully within the world bounds. Now run the widget and test if you can move the paddles off the top or bottom edge of the canvas i.e. our world bounds.
Let's now handle collision between the ball and the world bounds. Determining when collision occurs is easy – we can just check if any of the ball's edges lie outside the world bounds. If the ball goes horizontally past either paddle, the opponent scores a point and the ball is reset, so we don't need to process collision for the left and right world bound. However, we do need to handle the ball bouncing off the top and bottom walls of the world.
Figure 17. Collision handling methods.
Here's an overview of the two methods presented in Figure 17 (left to right):
ball.angle) and apply movement this to the last known position before collision occurred (t-1).
Add the following to the Collision handling section of the game loop:
collisionBall(ball);
void collisionBall(Ball b)
{
// Top wall
if(b.y_f < 0)
{
b.y_f = -b.y_f;
b.angle = 360 - b.angle;
}
// Bottom wall
else if(b.y_f + BALL_SIZE_F >= WORLD_SIZE_F)
{
b.y_f = 2 * (WORLD_SIZE_F - BALL_SIZE_F) - b.y_f;
b.angle = 360 - b.angle;
}
}
Figure 18. Paddle collision.
Note that the law of reflection comes into play here i.e. the angle of incidence equals the angle of reflection. in this case that means subtracting the incoming angle from 360° to determine the new angle.
Collision detection between the ball and paddles if slightly more complex (Figure 18). Here the face of the paddle (shown in red) and the movement of the ball (shown in blue) both represent line segments. We can use linear equations to determine where the ball intersects the line formed by the front face of the paddle.
A line can be represented in the form y = mx + c, where (x, y) represents any point on the line, m represents the line gradient and c is the point of where the line crosses the y axis (y-intercept). The gradient m of the ball's movement from t-1 to t0 is given by dy / dx. By rearranging the equation for a line, we find that:
c = t0y - (dy/dx)t0x
Now that we know c, we can work out the y-intercept by inserting the x position of the paddle's face (px) into a linear equation together with m and c, which are now known:
y = m × px + c
We can now check if the ball actually collides with the face of the paddles or whether it has missed by checking whether the y-intercept is within the bounds formed by the top and bottom edge of the paddle. If a collision is detected, we'll snap the ball to the position on the face of the paddle where the collision occurs (Figure 18 right). Note this is a slightly different collision handling technique than that used for the ball's collision with the top and bottom wall. Why not use the same method? Well, it's good to let the player know exactly where the ball hits the paddle upon collision (or how close they were to missing!). Obviously, we still need to change the ball's angle when collision occurs. We do this by subtracting the incoming angle by 180° (in our code, we'll ensure that the angle is always represented by a value between 0-360° which may involve adding/subtracting 360 from our angles).
Let's implement this in our game. Add the following to the collisionBall() function (just beneath where we handle collision with the top and bottom wall):
// Paddle 1 (left)
if(
b.x_f < paddle1.x_f
&&
!(paddle1.flags & FLAG_MISSED_BALL)) // Early exit checks
{
int m = dv(b.dy_f, b.dx_f); // Gradient of line
int c = b.y_f - ml(m, b.x_f); // y = mx + c
int yIntercept_f = ml(m, paddle1.x_f) + c; // Intercept
if(
yIntercept_f >= paddle1.y_f - i2f(paddle1.size) - ONE_F
&&
yIntercept_f < paddle1.y_f + i2f(paddle1.size))
{
// Hit, snap to paddle
b.x_f = paddle1.x_f;
b.y_f = yIntercept_f;
// Speed up
b.speed_f += BALL_SPEED_INCREASE_F;
// Bounce
b.angle = 180 - b.angle;
// Ensure in range 0 - 360
if(b.angle < 0)
b.angle += 360;
}
else
{
paddle1.flags |= FLAG_MISSED_BALL;
}
}
// Paddle 2 (right)
else if(
b.x_f + ONE_F >= paddle2.x_f
&&
!(paddle2.flags & FLAG_MISSED_BALL)) // Early exit checks
{
int m = dv(b.dy_f, b.dx_f); // Gradient of line
int c = b.y_f - ml(m, b.x_f + ONE_F); // y = mx + c
int yIntercept_f = ml(m, paddle2.x_f) + c; // Intercept
if(
yIntercept_f >= paddle2.y_f - i2f(paddle2.size) - ONE_F
&&
yIntercept_f < paddle2.y_f + i2f(paddle2.size))
{
// Hit, snap to paddle
b.x_f = paddle2.x_f - ONE_F;
b.y_f = yIntercept_f;
// Speed up ball
b.speed_f += BALL_SPEED_INCREASE_F;
// Bounce
b.angle = 180 - b.angle;
// Ensure in range 0-360
if(b.angle < 0)
b.angle += 360;
}
else
{
paddle2.flags |= FLAG_MISSED_BALL;
}
}
This is quite a long code listing, but we're basically doing the same thing twice: collision detection and handling for the left paddle, then the same for the right paddle (this suggests we may be able to implement this as a function, minimising duplication, but we've leave it as it is for now). The first if statement checks if the ball's left edge is beyond the paddle's face. If it is, and we haven't already detected a miss, we determine the gradient of the movement and work out where on the face the ball will hit (using the methods described above). We then check if the ball has hit the paddle (inner if statement), if it has, we snap the ball to the paddle face (Figure 18), speed up the ball slightly (check the Game rules section at the top of this tutorial step), then change the angle of the ball according to the rules of reflection. We repeat this process for the right paddle.
You may notice that we sometimes add ONE_F to the ball's coordinates when doing calculations. This is because the ball is one world unit wide and the ball's reference point is in the top left, therefore we sometimes need to add one unit to check different corners/edges of the ball to other game objects.
We have introduced a new constant BALL_SPEED_INCREASE_F which defines how much the ball speeds up when it collides with a paddle. Add it to the code:
const int BALL_SPEED_INCREASE_F = 50;
Try running the widget now. The ball should bounce off the paddles.
We are quite close to finishing the game now (or at least this tutorial!). We still have a few important details that we need to add including updating the scores, resetting game objects when a point is scored and altering the angle of reflection according to where on the paddle the ball hits (to add some variation to the gameplay).
We effectively know when a point is scored already – when the ball passes one of the paddles, the opponent will score a point. However, we want to show the ball passing past the paddle for some time before we reset the objects. This will give each player time to prepare for the next point and it is visually satisfying to see the ball fly past your opponents paddle!
Add the following constant to your code:
const int WINNING_SCORE = 8;
The first player to score 8 points will be declared the winner. Now add the following function just beneath the nextFrame() function:
void pointScored()
{
// Do we have a winner?
if(paddle1.score >= WINNING_SCORE)
{
newGame();
setBubble(getImage("trophy.png"), "Player 1 wins!");
}
else if(paddle2.score >= WINNING_SCORE)
{
newGame();
setBubble(getImage("trophy.png"), "Player 2 wins!");
}
// No winner yet...
else
{
// Make paddles smaller every four points
int total = paddle1.score + paddle2.score;
if(total > 0 && total % 4 == 0)
{
paddle1.size = max(paddle1.size - 1, 1);
paddle2.size = max(paddle2.size - 1, 1);
}
}
}
Figure 19. Winner's message.
We'll call this function when either player has scored a point. The function first checks if either player has achieved a winning score. If they have, we start a new game then inform the players who has won with a call to the API function setBubble(...). If neither player has won the game, we then check to see it we should reduce the paddle sizes (see the Game rules). We reduce the paddle size by one every four points (we ensure the size is a minimum of one, otherwise they will disappear!).
Please note, we have used another image here called trophy.png. You'll need to download this, save it to your widget directory and then add the following line to the resources section of the widget configuration file (widget.xml):
<img scale="true" src="trophy.png"/>
When the ball goes past a paddle, we set the FLAG_MISSED_BALL bit of the paddle's flags bit field to 1 (representing true). Now we can use this information to update the scores, determine if there's a winner and reset the ball and paddles movement and positions. However, we need some kind of condition to trigger these actions – let's set this to when the ball moves beyond a certain threshold outside our world bounds.
if(
b.x_f < -ONE_F - 100000
&&
(paddle1.flags & FLAG_MISSED_BALL))
{
paddle2.scoreText = String(++paddle2.score);
pointScored();
resetPaddle(paddle1);
resetPaddle(paddle2);
int ba = 30 - random(60);
setBall(
b,
WORLD_SIZE_F / 4 - ONE_F / 2,
WORLD_SIZE_F / 2 - ONE_F / 2,
BALL_SPEED_INIT_F,
ba < 0 ? ba + 360 : ba);
}
// Has paddle 2 missed AND the ball reached bounds
else if(
b.x_f >= WORLD_SIZE_F + 100000
&&
(paddle2.flags & FLAG_MISSED_BALL))
{
paddle1.scoreText = String(++paddle1.score);
pointScored();
resetPaddle(paddle1);
resetPaddle(paddle2);
setBall(
b,
3 * WORLD_SIZE_F / 4 - ONE_F / 2,
WORLD_SIZE_F / 2 - ONE_F / 2,
BALL_SPEED_INIT_F,
150 + random(60)); // 135 deg. 225 deg.
}
When the ball moves beyond the world bounds by a certain amount (100,000 as FP) and the relevant paddle has missed the ball, we update the score text of the point scoring player, check if there's a winner and finally reset the game object positions. Note that we always angle the ball back toward the point scoring player and reset the ball speed after each point (see Figure 14 for a reminder of how we set the initial ball angle).
Now test the game. When the ball moves off the screen at each end, the scores should be updated and the objects reset. The paddles should also decrease in size every four points.
Figure 20. Ball deflect.
To add some interest to the game, let's allow each player to alter the angle of the ball when it hits their paddle. To do this, we'll assume that the ball will react with the face of the paddle as if it were concave. Figure 20 demonstrates this idea, although the angle of deflection will not be set at discrete steps as illustrated but would in fact be a continuous product of the distance the ball hits the paddle relative to each object's centre. To achieve this in code, we'll calculate how far off the centre of the paddle, the centre of the ball is hit as a fraction of the paddle's size (i.e. a value between -1 and 1), then we'll multiply this by some maximum deflection value. We need to prevent the ball from moving at angles that are too vertical, otherwise the ball could bounce a lot before reaching the opponent (potentially making the game too difficult or leaving long, boring gaps in the play). Figure 20 (3), demonstrates a scenario where there is no horizontal component to the ball's movement due to deflection – you can see that it could get stuck in an infinite sequence of bounces off the top and bottom wall without reaching either paddle.
Add the following two constants to your code:
const int BALL_MAX_ANGLE = 70; const int DEFLECT_STRENGTH = 45;
Figure 21. Maximum angles.
The constant BALL_MAX_ANGLE represents the maximum angle the ball can bounce with respect to the red line drawn perpendicular to the paddle face shown in Figure 21. Therefore, the green arcs demonstrate the maximum range of angles the ball can take after hitting the paddle (at any position).
DEFLECT_STRENGTH determines the maximum value the ball can be deflected when hitting the paddle off-centre.
If this is not clear, look at how the ball bounces off the paddles in the video at the top of this tutorial step (Figure 11).
Modify the paddle ball collision code with the following. This is quite a long code listing, but most of it is code that is already there - just add the lines that vary from the code you have already:
// Paddle 1 (left)
if(
b.x_f < paddle1.x_f
&&
!(paddle1.flags & FLAG_MISSED_BALL)) // Early exit checks
{
int m = dv(b.dy_f, b.dx_f); // Gradient of line
int c = b.y_f - ml(m, b.x_f); // y = mx + c
int yIntercept_f = ml(m, paddle1.x_f) + c; // Intercept
if(
yIntercept_f >= paddle1.y_f - i2f(paddle1.size) - ONE_F
&&
yIntercept_f < paddle1.y_f + i2f(paddle1.size))
{
// Hit, snap to paddle
b.x_f = paddle1.x_f;
b.y_f = yIntercept_f;
b.speed_f += BALL_SPEED_INCREASE_F; // Speed up the ball
// Ball centre y
int bcy_f = (b.y_f + ONE_F / 2);
// -1 >= deflect < 1
int deflect = dv(
bcy_f - paddle1.y_f,
i2f(paddle1.size) + ONE_F / 2);
deflect = f2i(deflect * DEFLECT_STRENGTH);
b.angle = 180 - b.angle + deflect;
// Keep within desired angles
if(b.angle < -BALL_MAX_ANGLE)
b.angle = -BALL_MAX_ANGLE;
else if(b.angle >= BALL_MAX_ANGLE)
b.angle = BALL_MAX_ANGLE;
// Ensure in range 0 - 360
if(b.angle < 0)
b.angle += 360;
}
else
{
paddle1.flags |= FLAG_MISSED_BALL;
}
}
// Paddle 2 (right)
else if(
b.x_f + ONE_F >= paddle2.x_f
&&
!(paddle2.flags & FLAG_MISSED_BALL)) // Early exit checks
{
int m = dv(b.dy_f, b.dx_f); // Gradient of line
int c = b.y_f - ml(m, b.x_f + ONE_F); // y = mx + c
int yIntercept_f = ml(m, paddle2.x_f) + c; // Intercept
if(
yIntercept_f >= paddle2.y_f - i2f(paddle2.size) - ONE_F
&&
yIntercept_f < paddle2.y_f + i2f(paddle2.size))
{
// Hit, snap to paddle
b.x_f = paddle2.x_f - ONE_F;
b.y_f = yIntercept_f;
b.speed_f += BALL_SPEED_INCREASE_F;
// Ball centre y
int bcy_f = (b.y_f + ONE_F / 2);
// -1 >= deflect < 1
int deflect = -dv(
bcy_f - paddle2.y_f,
i2f(paddle2.size) + ONE_F / 2);
deflect = f2i(deflect * DEFLECT_STRENGTH);
b.angle = 180 - b.angle + deflect;
if(b.angle < 0)
b.angle += 360;
// Keep within desired angles
if(b.angle < 180 - BALL_MAX_ANGLE)
b.angle = 180 - BALL_MAX_ANGLE;
else if(b.angle >= 180 + BALL_MAX_ANGLE)
b.angle = 180 + BALL_MAX_ANGLE;
}
else
{
paddle2.flags |= FLAG_MISSED_BALL;
}
}
There's one last change we should implement before we can declare the widget finished. Add the following code to the paint() function, just above the code that draws the game field lines. This will make the screen flash when a point has been scored:
if((paddle1.flags | paddle2.flags) & FLAG_MISSED_BALL)
{
g.setColor(0x606060);
g.fillRect(0, 0, width, height);
}
Now test the widget, it should look exactly as shown in Figure 11.
Figure 22. RetroBall (video).
The tutorial is finished, but the game is not as much fun as it could be as there's no real challenge for the player. This section lists a few suggested improvements that could enhance the game playing experience or help make our game run better.
Many of these improvements/additions have been added to RetroBall (Figure 22), a game that is based on these tutorials. RetroBall is now available on WidSets, so try it for yourself:
We should create a more interesting, custom skin for our widget to determine how it looks on the WidSets Dashboard.
When the game is finished, we should provide some help/instructions for the user so that they can learn how to play the game.
Playing against yourself is not all that much fun. The addition of an AI opponent can make the game much more of a challenge, and therefore more enjoyable. In RetroBall, the AI opponent has four levels of difficulty. The AI is made to appear more or less intelligent by varying the following:
When creating AI opponents, the most realistic results are often when you make the CPU think and react a bit like a human player. For example, it's no fun if the opponent can move in ways that the player cannot (this can seem like the CPU is cheating).
Earlier on this tutorial step, the possibility of applying acceleration to paddle movement was mentioned. The code needed to achieve this was not listed in the tutorial, but a version of the game with paddle acceleration has been developed and you can try it out for yourself.
You could also experiment with applying swerve on the ball when it hits the paddle in different positions along the face.
Before we cover general optimisation strategies, there is an obvious optimisation we could make: as mentioned earlier in the tutorial, it makes sense to calculate the new value of the ball's dx_f and dy_f values only when a collision occurs. This saves the overhead of converting from polar to Cartesian coordinates each frame.
The best thing is to optimise only where necessary and to profile the results of optimisation to determine whether the code changes make a difference (not currently easy to do with WidSets). Establish the sections of code that are run most frequently, that are potential bottlenecks and/or result in high memory usage and only consider optimising those. Here are a few general optimisation strategies/techniques (bear in mind that there's no guarantee whether these will work well, result in no difference, or even make things worse):
You may also want to check this thread on the forum where one of the WidSets developers (jkl) gives some specific advice regarding optimisations within WidSets.
Figure 23. RetroBall two players, one phone.
RetroBall allows you to play against your friend on a single phone (Figure 23). Player one uses the 4 and 7 key on the keypad and player 2 uses the 6 and 9 keys (for up and down respectively). It can be a bit of a squeeze getting two hands to fit on the keypad but it is great fun (the handset will need to support multiple key presses).
What about having power-ups randomly appear in the game world? These could speed-up/slow-down ball movement, change the ball angle slightly or perhaps spawn off new balls (multi-ball!). They could even act as a penalty e.g. reverse the direction of ball's travel, make the player(s) move slower for a time etc.
We could play a sound when the ball hits the paddle, or the walls, or perhaps vibrate the phone if the ball misses. To do this, you would need to use WidSets' Player class.
Whilst working on the AI for RetroBall, I was having problems with the ball hitting targets at the wrong position. After some investigation it became clear that this was due to cumulative rounding errors (as a result of repeated truncation) in the fixed-point multiply and divide functions ml() and dv(). Replacing these functions with the following solved the problem (in theory these functions will be slightly slower/require more memory but this really shouldn't affect the performance of our game at all):
int ml(long x, long y) {
return int(x * y >> SHIFT);
}
int dv(long x, int y) {
long z = x << (SHIFT << 1);
return z / y >> SHIFT;
}