Table of Contents

Corning Community College

ENGR1050 C for Engineers

ACTIVITY: BRICKIFY PONG

Objective

In the past few classes, we have been building and optimizing a pong game with various programming facilities, namely arrays and structs.

Here, we will continue that specific exploration, taking our struct-based code and adding in code for displaying and processing blocks in our game, turning pong into more of a breakout game.

Getting started

We will start this from a blank slate. You will want to:

Header and support function

The first thing you will want to place in your TIC-80 code editor is the top header (switching us to js mode) and the supporting getRandomNumber() function used for determining a random ball direction. You can copy and paste the following into TIC-80:

// program: brkoutjs.tic
// description: breakout-style struct-based pong-like game
// script: js
//
// You will need 3 foreground tiles created to use this program:
//
// #257: your paddle segment sprite (8x4, top left of 8x8 tile)
// #258: your ball sprite (2x2, top left of 8x8 tile)
// #259: your block sprite (7x4, top left of 8x8 tile)
//
//////////////////////////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////////////////////////
//
// getRandomDirection() is a function to query the random number
// generator to produce one of -1 or 1, used to set one of the ball
// direction attributes (xdir, ydir).
//
function getRandomDirection()
{
  ////////////////////////////////////////////////////////////////////////////
  //
  // The random number generator is not returning a whole number, so
  // we need a routine to strip the decimal places from it.
  //
  var min = Math.ceil(0);
  var max = Math.floor(1);
  var value = Math.floor (Math.random () * (max - min + 1)) + min;
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Should the generated value be 0, adjust it to -1, to match our
  // desired output of -1 or 1.
  //
  if (value == 0)
  {
    value  = -1;
  }
 
  return (value);
}

Supporting variables

Next up, let us create some variables that will help us on our way (copy and paste this, below the code pasted in the previous section):

//////////////////////////////////////////////////////////////////////////////
//
// Giving english names to button IDs (easier to remember and read)
//
var left = 2;
var right = 3;
 
//////////////////////////////////////////////////////////////////////////////
//
// Screen variables
//
var screenwidth = 240;
var screenheight = 136;

Should be familiar stuff. These variables are essentially text labels we will use to avoid hardcoding constants in our code.

The paddle struct

The item we will have direct control over, the paddle is used to deflect the ball from going off the bottom of the screen:

//////////////////////////////////////////////////////////////////////////////
//
// paddle is a structure that will be packed with tile and
// coordinate data for our player
//
var paddle = {
  tile: 257,
  wide: 24,
  x: 0,
  y: screenheight - 8 - 5,
};
paddle.x = (screenwidth / 2) - (paddle.wide / 2);

A change from the previous code: the wide property is no longer in units of tiles, but instead pixels. In this case, the paddle will be initialized to a width of 24 pixels (effectively 3 tile-widths). This value should be a multiple of 8 for best outcome.

Once our struct is created, we will re-initialize its x coordinate, using the set paddle width. We could not have done this originally, since x and wide were being created and did not exist yet.

The ball struct

The ball represents the main agent of action in the game: its movements, or specifically, its eventual collisions will create the progress experienced in the game.

Just as with paddle, the ball struct needs to be created and initialized, as follows:

//////////////////////////////////////////////////////////////////////////////
//
// ball is a structure that will be packed with tile and
// coordinate data for the missile
//
var ball = {
  tile: 258,
  wide: 2,
  x: 0,
  y: (screenheight - (screenheight / 3)),
  xdir: getRandomDirection(),
  ydir: getRandomDirection(),
};
ball.x = (screenwidth / 2) - (ball.wide / 2);

Brick struct

A new addition, the brick, will provide the infrastructure for other on-screen items that can be interacted with (namely, the ball coming into collision with them).

But before we do all that, we need to create that basic brick template, just as we did with paddle and ball, only here with the intent of later replicating it many times over:

//////////////////////////////////////////////////////////////////////////////
//
// brick is a structure that will be packed with tile and coordinate
// data for the on-screen blocks the missile (ball) can hit
//

The actual definition of the brick will be part of class interactions.

Iterate ourselves to a wall of blocks

The previous snippet hints at the foundation being laid for the creation of a brick struct, indicating we'd be replicating them, to have many bricks. We accomplish that here:

//////////////////////////////////////////////////////////////////////////////
//
// Create an array of blocks (30 blocks per row, 12 total rows)
//

This actual process will be part of class interactions.

The TIC() function

All of this code so far is taking place before the TIC() function, which means it will be run exactly once, and at the very beginning of running. The reason being we often want to perform one-time initialization steps and defining things, which we only need or want to do once.

Now, though, we enter into the main section of the code responsible for the interactive game experience, and hence we place that in the TIC() function. We start with the bulk of paddle code, checking for controller presses, edge detection, and display (this should be among the most familiar of the code we've dealt with):

function TIC()
{
  ////////////////////////////////////////////////////////////////////////////
  //
  // declare local variables
  //
  var index = 0;
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Clear the screen
  //
  cls(0);
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Check to see if the left arrow is being pressed
  //
  if (btn (left) == true)
  {
    //////////////////////////////////////////////////////////////////////////
    //
    // Adjust the x position of the paddle by decrementing it
    //
    paddle.x = paddle.x - 1;
 
    //////////////////////////////////////////////////////////////////////////
    //
    // Bounds check: see if that recent movement moved us off the
    // left side of the screen (x-axis position of 0). If we have,
    // reset the x coordinate of the paddle to 0.
    //
    if (paddle.x <  0)
    {
      paddle.x  = 0;
    }
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Check to see if the left right arrow is being pressed
  //
  if (btn (right) == true)
  {
    //////////////////////////////////////////////////////////////////////////
    //
    // Adjust the x position of the paddle by incrementing it
    //
    paddle.x = paddle.x + 1;
 
    //////////////////////////////////////////////////////////////////////////
    //
    // Bounds check: see if that recent movement moved us off the
    // right side of the screen (x-axis position of 240). If we have,
    // reset the x coordinate of the paddle to a position such that it
    // is bumping against the right side of the screen.
    //
    // This requires some calculations, since tiles are 8 pixels wide,
    // and we are doubling up our display of the paddle tiles (so 16);
    // that means, since coordinates of sprites are referenced from a
    // top-left perspective, we take the screen width (240) and then
    // subtract our total sprite width (16 for a double wide)
    //
    if (paddle.x >  (screenwidth - (paddle.wide / 2)))
    {
      paddle.x  = screenwidth - (paddle.wide / 2);
    }
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Display the paddle, compensating for width of desired sprite
  //
  for (index = 0; index < paddle.wide / 8; index=index+1)
  {
    spr (paddle.tile, paddle.x+(index*8), paddle.y, 0);
  }

ball processing in TIC()

The next part of TIC() will deal with the processing of the ball, its edge detection, and checking for collisions between the ball and paddle. This code should also be quite familiar:

  ////////////////////////////////////////////////////////////////////////////
  //
  // Move ball along its current path (determined by xdir
  // and ydir current states)
  //
  ball.x = ball.x + (1 * ball.xdir);
  ball.y = ball.y + (1 * ball.ydir);
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Bounds checking for the ball: left side of screen: reflect
  // it and send it in the opposite direction.
  //
  if (ball.x < 0)
  {
    ball.xdir = 1;
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Bounds checking for the ball: right side of screen: reflect
  // it and send it in the opposite direction.
  //
  if (ball.x + ball.wide > screenwidth)
  {
    ball.xdir = -1;
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Bounds checking for the ball: top of screen: reflect
  // it and send it in the opposite direction.
  //
  if (ball.y < 0)
  {
    ball.ydir = 1;
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Loss for player: paddle misses ball and ball leaves the
  // bottom of the screen. Reset ball to starting position.
  //
  if (ball.y + ball.wide > screenheight)
  {
    //////////////////////////////////////////////////////////////////////////
    //
    // Same code as above: reset ball to its starting point
    //
    ball.x = (screenwidth / 2) - (ball.wide) / 2;
    ball.y = screenheight - (screenheight / 3);
 
    //////////////////////////////////////////////////////////////////////////
    //
    // Same code for initializing xdir and ydir above: obtain
    // a random choice of -1 or 1
    //
    ball.xdir = getRandomDirection();
    ball.ydir = getRandomDirection();
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Collision detection: is the top of the paddle coming into
  // contact with the ball? Check and react accordingly (reflect
  // the ball away).
  //
  // This requires the use of a compound condition in our if(),
  // as we need to check both x and y axis within the range of
  // the paddle to determine if we have a hit.
  //
  if ((ball.y+ball.wide >= paddle.y)     && // ball on y-plane
    (ball.x >= (paddle.x - ball.wide))   && // left side
    (ball.x <= (paddle.x + paddle.wide)) && // right side
    (ball.y <= (paddle.y + 8)))             // bottom
  {
    //////////////////////////////////////////////////////////////////////////
    //
    // Place ball above the paddle
    //
    ball.y = paddle.y - 8;
 
    //////////////////////////////////////////////////////////////////////////
    //
    // Reflect the ball away
    //
    ball.ydir = -1;
  }
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Display the ball sprite
  //
  spr (ball.tile, ball.x, ball.y, 0);

With this, we'd technically have our functional pong game (which could be tested- we'd merely have to add one closing curly brace and run it).

But, the aim here is to add in brick blocks, so we continue on…

Block processing code

The last part of our program, also to be done via classroom interaction, will handle the ball/brick collision detection. We lay down important comment blocks so that we can place the needed code:

  ////////////////////////////////////////////////////////////////////////////
  //
  // Ball-block collision detection - we have to check the ball against
  // each and every visible brick to determine if a collision has taken
  // place
  //

    //////////////////////////////////////////////////////////////////////////
    //
    // Only check if the asset is considered active
    //

      ////////////////////////////////////////////////////////////////////////
      //
      // Check for collision based on the four possible sides of impact
      //
 
        //////////////////////////////////////////////////////////////////////
        //
        // If a brick, it is now considered inactive
        //
 
        //////////////////////////////////////////////////////////////////////
        //
        // Determine if collision is occurring at the top or bottom of the
        // asset, and reflect y if so
        //
 
        //////////////////////////////////////////////////////////////////////
        //
        // Determine if collision is occuring at the left or right of the
        // brick, and reflect x if so
        //
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Display the block sprites, if visible
  //
 
  ////////////////////////////////////////////////////////////////////////////
  //
  // Draw borders around the playfield
  //
 
}

Also take note: that final closing curly brace, which completes our program.

Once everything is typed in, and tiles are created in their appropriate slots, we can give the program a run and see it in action.