Pong With Angular 4

A full walkthrough of making a web game in Typescript

May 05, 2017
Angular CLI Canvas Typescript

Hey all! Welcome to my first blog post. My company, Rightpoint, generously sent me to ng-conf in Salt Lake City a few weeks ago. The awesome folks on the ng team put together some great presentations detailing the updates of Angular 4 from Angular 2 (Spoiler alert - version 3 was skipped to align version numbers across all of their tools), all of the cool new goodies that have been released to help developers build Angular apps, and some of the best practices associated with building apps. 

In this blog post, we're going to be looking at how to make a simple web game with some help of the Angular environment. Although we won't be using much of the built-in Angular functionality, we're going to be taking advantage of the amazing development environment that the Angular CLI provides.

If you want to check out the full source code and demo, the repository can be found here.

Getting Started

Download the CLI:

npm install -g @angular/cli

The Angular CLI lets you create new Angular apps (handles installing all initial dependencies), host a local webserver, live-reload, and add new elements and files to your application, all with a simple couple commands. To start off, we'll run

ng new angular-pong --style scss
cd angular-pong

To create our new app, and move into the new directory that the CLI built for us. Once we get there, we can open the folder in our favorite code editor. From here, we can run 

ng serve

Which will spin up a webserver (defaulting) at localhost:4200. Once you get there, go make yourself a well-earned cocktail - you're now a master at setting up Angular apps.

Creating Our Pong Component

In the interest of brevity, we're going to jump right into creating the app. If you haven't seen Angular before, I strongly urge you to take a look at the interactive tutorial/guide here. We're going to start by using the CLI to generate a new component, using the shorthand notation:

ng g c pong

This will generate a new folder inside the source directory, called "pong". Inside that folder will be the 3 things we need for a component: A typescript file for the component logic, an html file for the view, and a scss file. It also generates a testing spec file, if you're like me and don't understand what "testing" even means, you just delete it. Clutters up the directory otherwise.

The job of our component will be simply this: Help us render the game to the browser's window. First thing's first: Setting up our HTML canvas.

Note about the curly braces: These allow us to directly bind to the "width" on "height" properties of our component.

<canvas #PongCanvas width="{{width}}" height="{{height}}"></canvas>

Next, lets check out our component. ViewChild allows us to query our children using a selector, and will return the first match as an element reference. In this case, we use it to get a reference to our canvas that we are going to draw the game on. We then set the resolution in pixels of our game, and then set the canvas rendering context once our component has loaded, which we will use to draw on the canvas. We then render our first frame - which our component knows how to do.

export class PongGameComponent implements OnInit {
  @ViewChild('PongCanvas') canvasElement: ElementRef

  public width: number = 800;
  public height: number = 600;

  private context: CanvasRenderingContext2D;

  ngOnInit() {
    this.context = this.canvasElement.nativeElement.getContext('2d');
    this.renderFrame();
  }
}

 If you want to make sure that your canvas component shows up, go ahead and run

this.context.fillStyle = 'rgb(0,0,0)';
this.context.fillRect(0, 0, this.width, this.height);

Creating a Representation of Our Game

Now that we've set up our first component, it's time to write our game logic. This game will include a few objects - the player's paddle, an enemy paddle, a ball, and a game board. The game board will keep track of the other 3. We'll also have a few utility classes to help us along the way - 

  • Boundaries
    • Describes the position of the top, bottom, left, and right boundaries of an object.
  • ControlState
    • Describes the current state of what buttons are pressed on the "controller", or in this case, the up and down arrow.
  • Position
    • Describes the x and y coordinate positions of an element.
  • SpeedRatio
    • Describes the percentage (-100% through 100%) of an object's max-speed that it should move in units the next time its "move" method is called.

So anyway, lets get started on our game objects.

An Abstract Moveable Object

Because we like to follow good programming principles occasionally, lets start with writing an abstract class that represents a moveable object in-game. This object represents a movable rectangular object - it has a height and width measured in units, a maximum speed measured in units per movement, and an initial starting position in units. 

Use the ng CLI to generate a class called MovableObject. In shorthand, it looks like this:

ng g cl classes/moveable-object
export abstract class MoveableObject {
constructor(private height: number, private width: number, private maxSpeed: number, private position: Position) { }
move(speedRatio: SpeedRatio): void { this.position.x += this.maxSpeed * speedRatio.x; this.position.y += this.maxSpeed * speedRatio.y; } getPosition(): Position { ... }
getCollisionBoundaries(): Boundaries { return { top: this.position.y - this.height / 2, bottom: this.position.y + this.height / 2, right: this.position.x + this.width / 2, left: this.position.x - this.width / 2 } } getWidth(): number { ... } getHeight(): number { ... } }

Although this class cannot be used directly (it's abstract!), it provides a base for us to build any moveable object in our game from. It provides some helpful methods, like "getCollisionBoundaries", which returns the boundaries of our object for collision checking (our x/y position is from the center of the object), or "move", which moves our object across the game board by a percentage of our max speed.

The Ball

Next, we'll create the ball.

export class Ball extends MoveableObject {
    private speedRatio: SpeedRatio;

    constructor(height: number, width: number, maxSpeed: number, position: Position, speedRatio: SpeedRatio) {
        super(height, width, maxSpeed, position);
        this.speedRatio = speedRatio;
    }

    /**
     * Reverses the ball in the x direction
     */
    reverseX(): void {
        this.speedRatio.x = -this.speedRatio.x;
    }

    /**
     * Reverses the ball in the y direction
     */
    reverseY(): void {
        this.speedRatio.y = -this.speedRatio.y;
    }

    /**
     * Sets new vertical speed ratio of max speed
     */
    setVerticalSpeedRatio(verticalSpeedRatio: number): void {
        this.speedRatio.y = verticalSpeedRatio;
    }

    /**
     * Moves object using existing speed ratio
     */
    move() {
        super.move(this.speedRatio);
    }
}

The ball takes the same arguments as the Moveable Object does, and just passes them up to its base. We provide a couple methods to reverse the ball when it hits the top/bottom of the game board, as well as the player paddle. In this game, the ball will always move the same velocity in the X direction, with variable velocity in the Y direction. As we'll see later, the ball changes its angle based on where it hits the paddle: meaning that we need a way to set its vertical speed, in this case with "setVerticalSpeedRatio".

The Paddles

Now that we have a ball object set up, we need to set up a paddle object that will be used for the player and enemy paddles. Things get a little more complicated with our paddles - to make our experience smooth, we need to include acceleration in our paddle movements. In this game, we're going to let the board handle checking for collisions between the paddle and the ball - so we're just going to let the paddle handle moving itself.

export class Paddle extends MoveableObject {
    private speedRatio: SpeedRatio;

    constructor(height: number, width: number, maxSpeed: number, position: Position) {
        super(height, width, maxSpeed, position);
        // Initially, no movement
        this.speedRatio = { x: 0, y: 0 };
    }

    /**
     * Accelerates towards the max speed in the down direction
     * @param ratioChange - the percentage of the max speed that the paddle should accelerate to
     */
    accelerateDown(ratioChange: number) {
        if (ratioChange < 0 || ratioChange > 1) return;
        this.speedRatio.y = Math.min(1, this.speedRatio.y + ratioChange);
        this.move();
    }

    /**
     * Accelerates towards the max speed in the up direction
     * @param ratioChange - the percentage of the max speed that the paddle should accelerate to
     */
    accelerateUp(ratioChange: number) {
        if (ratioChange < 0 || ratioChange > 1) return;
        this.speedRatio.y = Math.max(-1, this.speedRatio.y - ratioChange);
        this.move();
    }

    /**
     * Decelerate the object towards zero
     * @param ratioChange - the percentage of the max speed that the paddle should decelerate
     */
    decelerate(ratioChange: number) {
        if (this.speedRatio.y < 0) {
            this.speedRatio.y = Math.min(this.speedRatio.y + ratioChange, 0);
        }
        else if (this.speedRatio.y > 0) { 
            this.speedRatio.y = Math.max(this.speedRatio.y - ratioChange, 0);
        }
        this.move();
    }

    move(): void {
        super.move(this.speedRatio);
    }
}

So now, instead of having the game move our paddle directly (although that's still possible), we provide a few methods, "accelerateUp", "accelerateDown", and "decelerate", to allow the user to speed up/down by a given amount, and then move. So now, we can call accelerate(), pass in the same number each time, and the paddle will gradually gain speed until it reaches its max speed. Pretty cool, huh? In practice, if the user is holding the up/down key, we will call the accelerate methods with a small number every "game" tick.

Wrapping Our Objects Up in a Game Object

Alright - we've been working for a while now, and we're 3 or so cocktails into an engineer's Saturday night, but we've got some work left to do: Creating our actual game object. Our game object holds references to the other objects in the game (paddles and ball in this case), and handles all of our logical calculations, including collisions and game over checking. This file's a little long, so lets take a look at the stub of our class to explain this all a little better:

export class PongGame {
    // Game objects
    public ball: Ball;
    public playerPaddle: Paddle;
    public enemyPaddle: Paddle;

    private height: number
    private width: number;

    constructor(height: number, width: number) {
        this.height = height;
        this.width = width;

        // Construct game objects
        ...
    }

    tick(controlState: ControlState) { ... }

    private moveEnemyPaddle() { ... }

    private checkCollisions() { ... }

    gameOver(): boolean { ... }
}

Before now, we've set up our game objects to track the data we need for our game - now we need to provide a way for all of these objects to interact, update, and drive what actually gets rendered on our screen frame by frame. The "tick" function controls the entire game-state - it is called a fixed number of times per second, and updates the entire game state (move ball, move current player depending on control state, move enemy paddle, check for collisions, etc.). Very important: This is completely unrelated to our "Frame rate", or the number of frames that get rendered on the screen per second. Our tick() function is called a fixed amount of times per second, so even if our framerate increases (on a higher refresh rate monitor, for example), the game doesn't speed up. I'm not a game developer, so I had to learn that one the hard way.

Lets move on to a few specifics of what's going to go on within this object. First of all, constructing the game objects. This is all dependent on how you want your pong game to play out, but the settings that I've used are here:

this.ball = new Ball(15, 15, 2, { x: height / 2, y: width / 2 }, { x: 1, y: 1 });
this.playerPaddle = new Paddle(100, 20, 1.5, { x: 50, y: height / 2 });
this.enemyPaddle = new Paddle(100, 20, .8, { x: width - 50, y: height / 2 });

I won't go too far into this, but basically, these 3 lines create our ball and paddles, placing the player paddle on the left side, our enemy paddle on the right side, and the ball in the middle, with the ball moving at full speed in the up/right direction at game start.

Next, lets take a look at what happens in the tick() function, which advances our game by one "step", and is called a fixed number of times per second. 

tick(controlState: ControlState) {
    this.ball.move();

    // Set acceleration, move player paddle based on input
    var paddleBounds = this.playerPaddle.getCollisionBoundaries();
    if (controlState.upPressed && paddleBounds.top > 0) {
        this.playerPaddle.accelerateUp(.03);
    }

    else if (controlState.downPressed && paddleBounds.bottom < this.height) {
        this.playerPaddle.accelerateDown(.03);
    }

    else {
        this.playerPaddle.decelerate(.05);
    }

    this.moveEnemyPaddle();
    this.checkCollisions();
}

The tick function takes in input of the current control state (i.e. what buttons are currently being pressed), and advances the game one "tick" accordingly. It moves the ball, accelerates the human player in the correct direction depending on what's being pressed, decelerates the player if nothing's being pressed, moves the enemy paddle, and does any collision checks for the ball. 

I should win an award for AI programming

Enjoy the function for moving the computer player:

private moveEnemyPaddle() {
    if (this.ball.getPosition().y < this.enemyPaddle.getPosition().y)
        this.enemyPaddle.accelerateUp(1)
    else 
        this.enemyPaddle.accelerateDown(1)
}

Yeah, that's it. Sorry if it was anti-climactic. If the ball is above him, he'll go up. If the ball is below him, he'll go down.

Lets check for some collisions. You've been scrolling a lot, and the collision checking code isn't going to change that. I'm sure there's a better way to handle some of this collision stuff, but... Whiskies. Check the summary below for a TL;DR:

private checkCollisions() {
    // Bounce off top/bottom
    let ballBounds = this.ball.getCollisionBoundaries();
    if (ballBounds.bottom >= this.height || ballBounds.top <= 0)
        this.ball.reverseY();

    let paddleBounds = this.playerPaddle.getCollisionBoundaries();

    // Don't let paddle go past boundaries
    if (paddleBounds.top <= 0 || paddleBounds.bottom >= this.height)
        this.playerPaddle.decelerate(1);

    // Player paddle hit
    if (ballBounds.left <= paddleBounds.right &&
        paddleBounds.right - ballBounds.left <= 3 &&
        ballBounds.bottom >= paddleBounds.top &&
        ballBounds.top <= paddleBounds.bottom) {
        this.ball.reverseX();

        // Set vertical speed ratio by taking ratio of 
        // dist(centerOfBall, centerOfPaddle) to dist(topOfPaddle, centerOfPaddle)
        // Negate because pixels go up as we go down :)
        var vsr = - (this.ball.getPosition().y - this.playerPaddle.getPosition().y)
            / (paddleBounds.top - this.playerPaddle.getPosition().y);

        // Max vsr is 1
        vsr = Math.min(vsr, 1);
        this.ball.setVerticalSpeedRatio(vsr);
    }

    // Enemy paddle hit
    paddleBounds = this.enemyPaddle.getCollisionBoundaries();
    if (ballBounds.right <= paddleBounds.left &&
        paddleBounds.left - ballBounds.right <= 3 &&
        ballBounds.bottom >= paddleBounds.top &&
        ballBounds.top <= paddleBounds.bottom) {
        this.ball.reverseX();

        // Set vertical speed ratio by taking ratio of 
        // dist(centerOfBall, centerOfPaddle) to dist(topOfPaddle, centerOfPaddle)
        // Negate because pixels go up as we go down :)
        var vsr = - (this.ball.getPosition().y - this.enemyPaddle.getPosition().y)
            / (paddleBounds.top - this.enemyPaddle.getPosition().y);

        // Max vsr is 1
        vsr = Math.min(vsr, 1);
        this.ball.setVerticalSpeedRatio(vsr);
    }
}

In summary:

  1. Bounce the ball off the top/bottom if it hits
  2. If the player paddle collides with the game board boundaries, stop it immediately
  3. If the ball hits the front-face of a paddle, reverse its x velocity, and change its y velocity depending on where it hits on the paddle, scaling from 0 at the center of the paddle to max-speed at the top or bottom
  4. Do the same for the enemy paddle

And that's it! We're almost done with our game logic. The last one's a fun one, assuming you don't get beaten by the dumbest AI of all time. 

Game Over:

gameOver(): boolean {
    var collisionBoundaries = this.ball.getCollisionBoundaries();
    if (this.ball.getCollisionBoundaries().left <= 0 ||
        this.ball.getCollisionBoundaries().right >= this.width) return true;
    else return false;
}

Time to Render

We've got all of our game logic complete - time to render this baby to the screen. Angular components handle rendering their representations onto the screen, so that's what we'll be doing here. Lets revisit our pong.component.ts that we created at the beginning, but this time lets add some rendering code, as well as event handling for player input.

export class PongGameComponent implements OnInit {
  @ViewChild('PongCanvas') canvasElement: ElementRef

  public width: number = 800;
  public height: number = 600;

  private context: CanvasRenderingContext2D;
  private pongGame: PongGame;
  private ticksPerSecond: number = 60;

  private controlState: ControlState; 

  constructor() {
    this.pongGame = new PongGame(this.height,this.width);
    this.controlState = { upPressed: false, downPressed: false };
  }

  ngOnInit() {
    this.context = this.canvasElement.nativeElement.getContext('2d');
    this.renderFrame();

    // Game model ticks 60 times per second. Doing this keeps same game speed
    // on higher FPS environments.
    setInterval(() => this.pongGame.tick(this.controlState), 1 / this.ticksPerSecond);
  }

  renderFrame(): void {
    // Run rendering logic ...
    
    window.requestAnimationFrame(() => this.renderFrame());
  }

  @HostListener('window:keydown', ['$event'])
  keyUp(event: KeyboardEvent) {
    if (event.keyCode == Controls.Up) {
      this.controlState.upPressed = true;
    }
    if (event.keyCode == Controls.Down) {
      this.controlState.downPressed = true;
    }
  }

  @HostListener('window:keyup', ['$event'])
  keyDown(event: KeyboardEvent) {
    if (event.keyCode == Controls.Up) {
      this.controlState.upPressed = false;
    }
    if (event.keyCode == Controls.Down) {
      this.controlState.downPressed = false;
    }
  }
}

Quick ng-tip: Anything related to view logic that should be run when a component is created should be put inside "ngOnInit()", and anything that relates to other general/logic setup should be done within the object's constructor, including any dependency injection stuff.

Anyway, the main driver of our game rendering is built in to your browser - using window.requestAnimationFrame(), which allows you to tell the browser to call the specified callback function next time it updates the display, which might happen 60-144 times per second depending on your monitor.

Rendering Your Game to the Screen

The render function takes info from the current game state, draws the background, draws the 3 objects, and then requests the next frame to be rendered. It's actually pretty simple - all of the heavy lifting is done by the game object itself, the render just graves the data from the game object and displays it on the screen. The code is shown below:

renderFrame(): void {
  // Only run if game still going
  if (this.pongGame.gameOver()) {
    this.context.font = "30px Arial";
    this.context.fillText("Game Over!", 50, 50);
    setTimeout(() => location.reload(), 500);
    return;
  }

  // Draw background
  this.context.fillStyle = 'rgb(0,0,0)';
  this.context.fillRect(0, 0, this.width, this.height);

  // Set to white for game objects
  this.context.fillStyle = 'rgb(255,255,255)';

  let bounds: Boundaries;

  // Draw player paddle
  let paddleObj = this.pongGame.playerPaddle;
  bounds = paddleObj.getCollisionBoundaries();
  this.context.fillRect(bounds.left, bounds.top, paddleObj.getWidth(), paddleObj.getHeight());

  // Draw enemy paddle
  let enemyObj = this.pongGame.enemyPaddle;
  bounds = enemyObj.getCollisionBoundaries();
  this.context.fillRect(bounds.left, bounds.top, enemyObj.getWidth(), enemyObj.getHeight());

  // Draw ball
  let ballObj = this.pongGame.ball;
  bounds = ballObj.getCollisionBoundaries();
  this.context.fillRect(bounds.left, bounds.top, ballObj.getWidth(), ballObj.getHeight());

  // Render next frame
  window.requestAnimationFrame(() => this.renderFrame());
}

Finishing Up

Congratulations - we've built a game of pong. You now have minimal knowledge of game development and the Angular CLI (I don't think that sentence has ever been said before...). Although we didn't do much in the way of actual Angular, the CLI makes it so gosh-darn easy to set up an environment and getting an app running, that in my mind, it's the easiest way to get into javascript development or create sample apps.

If you find this useful, entertaining, or there's something you'd like to see more of, let me know. I'd love to produce tutorials, content, and walkthroughs of anything from the basic level to the advanced level. Feel free to drop me an email at Abarn279@gmail.com if you have any feedback. Thanks!

- Andrew