Pong - The Game Loop

Sat, May 14, 2011

The Game.Runner provides the raw 60fps frame loop of update() + draw(). Within that process the game itself (in this case Pong) needs to provide the game state machine that starts with a basic menu, waits for user input to start a game, run the game until there is a winner, or the user abandons it, and then go back to the menu and repeat.

Initialization

For the Pong game, we are going to use some images to display the ‘menu’ that tells the user to:

This introduces a subtle twist into the GameRunner and Pong relationship. Loading images is an asynchronous process and we can’t display the images until they are loaded, so we dont want the GameRunner to go into its loop until the images are fully loaded.

In order to resolve this issue, the Pong initialize method uses a callback pattern to know when the images are finished loaded and doesn’t tell the GameRunner to start() until that process is complete:

  initialize: function(runner, cfg) {
    Game.loadImages(Pong.Images, function(images) {
      this.cfg         = cfg;
      this.runner      = runner;
      this.width       = runner.width;
      this.height      = runner.height;
      this.images      = images;
      this.playing     = false;
      this.scores      = [0, 0];
      this.menu        = Object.construct(Pong.Menu,   this);
      this.court       = Object.construct(Pong.Court,  this);
      this.leftPaddle  = Object.construct(Pong.Paddle, this);
      this.rightPaddle = Object.construct(Pong.Paddle, this, true);
      this.ball        = Object.construct(Pong.Ball,   this);
      this.runner.start();
    }.bind(this));
  },

Keyboard Input

When a keyboard event occurs, the GameRunner automatically calls any onkeydown() or onkeyup() methods in the game (if they exist), so the Pong game can detect appropriate keys and start or stop games appropriately:

  onkeydown: function(keyCode) {
    switch(keyCode) {
      case Game.KEY.ZERO: this.startDemo();            break;
      case Game.KEY.ONE:  this.startSinglePlayer();    break;
      case Game.KEY.TWO:  this.startDoublePlayer();    break;
      case Game.KEY.ESC:  this.stop(true);             break;
      case Game.KEY.Q:    this.leftPaddle.moveUp();    break;
      case Game.KEY.A:    this.leftPaddle.moveDown();  break;
      case Game.KEY.P:    this.rightPaddle.moveUp();   break;
      case Game.KEY.L:    this.rightPaddle.moveDown(); break;
    }
  },

  onkeyup: function(keyCode) {
    switch(keyCode) {
      case Game.KEY.Q: this.leftPaddle.stopMovingUp();    break;
      case Game.KEY.A: this.leftPaddle.stopMovingDown();  break;
      case Game.KEY.P: this.rightPaddle.stopMovingUp();   break;
      case Game.KEY.L: this.rightPaddle.stopMovingDown(); break;
    }
  },

This is also where we detect input from the users to move the paddles up or down.

Starting a Game

Now that we can detect keyboard input, starting a game is possible:

  startDemo:         function() { this.start(0); },
  startSinglePlayer: function() { this.start(1); },
  startDoublePlayer: function() { this.start(2); },

  start: function(numPlayers) {
    if (!this.playing) {
      this.scores = [0, 0];
      this.playing = true;
      this.ball.reset();
      this.runner.hideCursor();
    }
  },

During the Game

While the game is in progress, the update() method needs to detect when goals are scored and when to declare a winner and stop the game:

  update: function(dt) {
    this.leftPaddle.update(dt, this.ball);
    this.rightPaddle.update(dt, this.ball);
    if (this.playing) {
      var dx = this.ball.dx;
      var dy = this.ball.dy;
      this.ball.update(dt, this.leftPaddle, this.rightPaddle);

      if (this.ball.left > this.width)
        this.goal(0);
      else if (this.ball.right < 0)
        this.goal(1);
    }
  },

  goal: function(playerNo) {
    this.scores[playerNo] += 1;
    if (this.scores[playerNo] == 1) {
      this.menu.declareWinner(playerNo);
      this.stop();
    }
    else {
      this.ball.reset(playerNo);
    }
  },

Stopping the Game

The game is stopped when a winner is declared, it can also be stopped in response to the user hitting the ESC key:

  stop: function(ask) {
    if (this.playing) {
      if (!ask || this.runner.confirm('Abandon game in progress ?')) {
        this.playing = false;
        this.runner.showCursor();
      }
    }
  },

You can see the game loop in progress with the demo here

More…

You can find the final game here and the code is here