Skip to content
jercaianu edited this page May 12, 2018 · 4 revisions

Welcome to the pong-d wiki!

Intro

In this tutorial we will implement a pong game using the D programming language.
Our team will give you a helping hand and provide you with all the resources you need to build your first game.
First, clone pong-d the repository using the following command in the terminal:
git clone https://github.com/edi33416/pong-d.git
The structure of the project is the following:

pong-d
├── README.md
├── dub.json
├── dub.selections.json
│
├── res
│   ├── FreeSerifBold.ttf
│   ├── background.png
│   ├── ball.png
│   ├── playerOne.png
│   └── playerTwo.png
│
├── solution
│   └── solution.d
│
├── source
│   └── app.d

You will complete the functions marked with TODO in source/app.d. Don't worry, if at any point you get stuck, you can always ask us for help. We even provide our solution in solution/solution.d, but we would really like you to try and give your own implementations for the tasks.

At any point, in order to check if your implementation is correct, you can start the game by writing dub run inside the terminal.

The game is essentially an infinite while loop which does the following things:

  • Check collisions of ball and players
  • Update the position ball and players
  • Draw each object

We can imagine the game area as a square, where the center of the square is the origin. The leftmost part of the screen has the X coordinate -1 and the rightmost part is 1. On the Y axis, the bottom of the screen is -1 and the top is 1.

We already took care of the drawing part and it's up to you to build the logic of the game. Most of the functions which you will encounter receive as parameter GameState state. This holds important information about the game, such as:

  • Player and ball position and speed
  • Current score
  • Left, right, top and bottom limits of the screen.

Each player is represented by the Racket structure. The state contains an array of two rackets, which are the our two players. If we want to access the first player object, we can write state.racket[Player.One]. Similar for the second player, but using Player.Two.

We can access the X coordinate of player one for example, by writing: state.racket[Player.One].pos.x (the same goes for the Y coordinate, just replace x with y).

If we want to access the position of the ball, we need this time to access the ball object inside the state. For the X coordinate: state.ball.pos.x (same goes for y).

Task 1 - Move the player

  • In order to solve this task, you only need to write code only under TODO 1, inside the moveHumanPlayer function.

If you write dub run, the game starts, but nothing is moving. One first step is to implement the logic which moves our player (the one on the left). If we search for TODO 1, we find the moveHumanPlayer function. This function is called by the game every frame, in order to move the left player.

Remember, we can access the left player through: state.racket[Player.One]. Another nice trick is to get a hold of the player by writing
auto player = &state.racket[Player.One];

Now, if we want to access the y coordinate, instead of writing: state.racket[Player.One].pos.y, we can now write more easily player.pos.y.

What we want to do now is to update the player's Y coordinate, depending on the player's speed and direction of movement (if the down arrow is pressed we want the racket to move downward, and upward if the up arrow key is pressed). Luckily, the player has the speed field, which is negative if we press the down arrow key and positive if we press the up arrow key. Otherwise, the speed will be set to 0.
What remains to be done, is to add player.speed to player.pos.y. We can test these changes by writing dub run and pressing the up and down arrow keys.

The racket is now moving so fast that it instantly disappears. This issue appears because we are not taking into account the CPU time, which is represented by the float dt parameter.

If we have a very fast CPU, the moveHumanPlayer function will be called much more often, than if we had a slower CPU. Because we want our game to run just as fast on any machine, no matter how fast the CPU is, we need to use the CPU time, which is the dt variable.

To reflect these changes, we will add now to player.pos.y, player.speed * dt. If we now write dub run and press the up and down arrow keys, the racket should move much slower.

Task 2 - Limit player movement inside the game area

  • In order to solve this task, you only need to write code only under TODO 2, inside the moveHumanPlayer function.

If we keep the up or down arrow keys pressed for too long, the player will exit the game area. What we want to do, is to limit the player's movement only inside the game area, preventing him from exiting. The upper and bottom limits of the screen are given by the state.limits array (state.limits[0] is the bottom of the screen and state.limits[1] is the upper limit of the screen).

In other words, if the player's position is greater than state.limits[1], prevent him from going any further upwards. Again, if the player's position is lower than state.limits[0], prevent him from going any further downwards.

  • Hint: we can use the min and max function (eg: player.pos.y = min(player.pos.y, state.limits[1]))

If we now write dub run and keep the up or down arrow keys pressed, the player will remain inside the game area.

Task 3 - Move the ball

  • In order to solve this task, you only need to write code only under TODO 3, inside the updateBall function.

The next step is to get the ball from the center of the screen to start moving. Just like the previous task, we can get the ball object by writing: auto ball = &state.ball;.
Since the ball can move diagonally, its speed will have 2 components: X axis speed (ball.speed.x) and Y axis speed (ball.speed.y). What needs to be done is to update the ball position, based on the ball speed (add ball.speed.x to ball.pos.x and ball.speed.y to ball.pos.y)

  • Remember to use dt similarly to moving the player !!!

If we write dub run, if the task is correctly solved, the ball will start moving. The AI player will also start moving, following the ball's position.

Task 4 - Check ball collision with the player

  • In order to solve this task, you only need to write code only under TODO 4, inside the checkCollisons function.

The collision code for the AI player is already written. If the ball is moving to the left (ball.speed.x < 0), we need to check if we touch the left player's racket. Two conditions need to be met for this to happen:

  • The ball has about the same x coordinate as the racket
  • The ball y coordinate is inside the range of the racket

To determine the length of the racket, we can get half of the racket's length by using player.halfLength. In other words we need to check:

  • ball.pos.x <= player.pos.x
  • (ball.pos.y <= player.pos.y + player.halfLength) && (ball.pos.y >= player.pos.y - player.halfLength)
  • Hint: use fabs just like we do in the code above for the AI player.

If the ball collides with the player, we want the ball to start moving the opposite way. We can do this by simply negating its X axis speed (ball.speed.x *= -1). Even more, we can make the game become harder by increasing the speed of the ball after each collision by calling the increaseSpeed method just like in the AI code above.

If implemented correctly, the ball should bounce off the player's racket on collision and slightly increase its speed.

Task 5 - Check ball collision with the top and bottom of the screen

  • In order to solve this task, you only need to write code only under TODO 5, inside the checkCollisons function.

After a couple of collisions with the players, the ball will now leave the screen through the top or bottom part of the screen. In order to check if we're exiting the screen area, we will use state.limits. If ball.pos.y is lower than state.limits[0] (bottom of the screen) or it's higher than state.limits[1] (top of the screen), we want the ball to bounce.

Similar to the logic of bouncing the ball off the racket, instead of negating ball.speed.x, we will now negate ball.speed.y.

If implemented correctly, the ball should bounce off the bottom and top part of the screen.

Task 6 - Keep the score

  • In order to solve this task, you only need to write code only under TODO 6, inside the checkGameOver function.

Now that we have a functional pong, we want to add some replay value to the game. Whenever any player wins a round, we want to increment the score for that player and restart the round. We are going to implement the checkGameOver which has to return true if either player wins and false otherwise.

If the ball leaves through the left side of the screen, then we're going to give a point to the AI player and vice-versa. In other words, if ball.pos.x is less than state.limits[0], then we have to give the point to the AI and the other way around.

To increment the score for the left player, we need to change the score object, by writing score.score[Player.One]++;. The same goes for Player.Two.

If implemented correctly, when the ball leaves the screen area, one of the players should receive a point and the game will restart.