-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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
).
- In order to solve this task, you only need to write code only under
TODO 1
, inside themoveHumanPlayer
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.
- In order to solve this task, you only need to write code only under
TODO 2
, inside themoveHumanPlayer
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
andmax
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.
- In order to solve this task, you only need to write code only under
TODO 3
, inside theupdateBall
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.
- In order to solve this task, you only need to write code only under
TODO 4
, inside thecheckCollisons
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.
- In order to solve this task, you only need to write code only under
TODO 5
, inside thecheckCollisons
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.
- In order to solve this task, you only need to write code only under
TODO 6
, inside thecheckGameOver
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.