BounceBlocks

by Hamish: Y7 Age ~12

Introduction

As young Hamish developed this Smart Pascal program he showed the qualities necessary for a successful programmer. He learned quickly, showed resilience in overcoming problems, demonstrated imagination and was not content with the mundane as shown by the following quote.

Hamish:

"For my first game with Smart Mobile Studio, I wanted to create a simple game similar to those of the late 20th century (breakout, pong, etc). I wanted to give my game a more professional feel by creating a soundtrack for it. I created the soundtrack using SonicPi which is a free music coding environment which comes on the standard install for the Raspberry Pi but you can download it (from www.sonic-pi.net)."

BounceBlocks uses in the res folder hit-01.wav (downloaded from MEDIACOLLEGE), soundtrack.aac (for the Microsoft Edge browser) and soundtrack1.wav (for Chrome).

We would appreciate any feedback to pass on to Hamish. Can you beat the score of 39 that he achieved in a demonstration?

Gameplay

These points are taken from the program comments and from the instructions that Hamish provides in an optional message before the start of the game.
  • Control the red paddle at the bottom of the screen by arrow or wasd keys or button presses.
  • The object is to obtain the highest score.
  • Gain one point every time the "ball" hits your paddle and one bonus point every time the ball hits the white goal at the top of the screen.
  • Hit a yellow enemy the number of times shown on it to kill it and gain five bonus points.
  • The game ends when you miss the ball with the paddle.
  • The paddle becomes narrower as the game progresses.

Note the colour change of the ball upon colliding with a block and of the background as your score exceeds 25 and then 50.

BounceBlocks.html

If the program does not work, try another browser such as Chrome. If you see no display at school, the security system might have blocked it. You can try instead this direct link to the program running on its own page.

Technical Features

The program benefits from:

  • use of inbuilt routines such as ContainsPos, CreateSized and Intersect (TRect), Load, Play, CanPlayTypeAsBoolean (TW3AudioElement), RandomInt, Round, IntToStr, ShowMessage, FillRect, FillTextF and BeginPath;
  • use of case statements;
  • handling of input from keyboard and mouse/touch;
  • variable declaration with multiple assignments to same value;
  • use of C-style compound operators and the mod operator;
  • audio (own music and sound effect);
  • wav audio file converted to the compressed AAC format;
  • thorough comments;
  • clear instructions.

The Code

unit Unit1;
{
    Copyright (c) 2016 Hamish

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, SmartCL.MediaElements;

type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    // Declaring variables
    RandomNum2,RandomNum, hits, FrameCount, score : integer := 0;
    GoingRight, GoingDown, Enemy1Right, Enemy2Right : boolean;
    btnPlay, btnInstr, btnRight, btnLeft, btnUp, btnDown, ballrect, intersectrect, Bat, GoalRect, Enemy1, Enemy2 : TRect;
    blocks: array[1..7] of TRect;
    MobX, MobY : integer := 100;
    BlockX : integer := 200;
    BlockY : integer := 80;
    BatX : integer := 200;
    BatY, halfway, numtoadd : integer := 1;
    BatWidth : integer := 60;
    GoalWidth : integer := 100;
    scoretime : integer := MAX_INT;
    playPressed, MovingRight, MovingLeft, MovingUp, MovingDown : boolean := false;
    FAudioElement, FAudioElement2: TW3AudioElement;
    Enemy1X, Enemy2Y : integer := 200;
    Enemy1Y : integer := 100;
    Enemy2X : integer := 300;
    enemy1hits, enemy2hits : integer := 0;
    enemy1alive, enemy2alive : boolean := true;
    enemy1time, enemy2time : integer := MAX_INT;
    ballcolour : string := "blue";
    bgcolour :string := "teal";
  public
    // Declaring procedures
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
    procedure MouseDownHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
    procedure MouseUpHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
    procedure KeyDownEvent(mCode: integer);
    procedure KeyUpEvent(mCode: integer);
  end;

implementation

const
  // Declare constants
  MOB_WIDTH = 20;    // Width of mobile stays at 20 pixels
  MOB_HEIGHT = 15;
  SCENEHEIGHT : integer = 400;
  SCENEWIDTH : integer = 600;

procedure TCanvasProject.MouseDownHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
// Define procedure to handle mouse click
begin
  // Check if a mouse click has occured within the space of one of my buttons and
  // set movement accordingly
  if btnRight.ContainsPos(x, y) then
    MovingRight := true;
  if btnLeft.ContainsPos(x, y) then
    MovingLeft := true;
  if btnUp.ContainsPos(x, y) then
    MovingUp := true;
  if btnDown.ContainsPos(x, y) then
    MovingDown := true;
  // Say what happens when the play button is pressed.
  if btnPlay.ContainsPos(x, y) then
    PlayPressed := true;
  // Say what happens when the instructions button is pressed.
  if btnInstr.ContainsPos(x, y) then
    ShowMessage("You control the red paddle at the bottom of the screen. It can " +
"be controlled with the buttons or the arrow keys. The aim of the game is to " +
"get as many points as you can. You get one point every time the ball hits your " +
"paddle and one bonus point every time the ball hits the white rectangle at " +
"the top of the screen (the goal). The yellow rectangles that fly across the " +
"screen are enemies. If you hit them the number of times shown on them, they " +
"will die and you will get five bonus points. The enemies can respawn.
(This game has sound.)");
end;

// Define the mouse up procedure which stops movement when the mouse key goes back up
procedure TCanvasProject.MouseUpHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
begin
  MovingRight := false;
  MovingLeft := false;
  MovingUp := false;
  MovingDown := false;
end;

// Define the procedure which handles all the things that happen as the application starts.
procedure TCanvasProject.ApplicationStarting;
begin
  inherited;
  randomize;
  // Work out the halfway point of the screen so the goal can be placed in the middle.
  halfway := Round(SCENEWIDTH / 2) - Round(GoalWidth / 2);
  //Create audio elements which play my sounds
  FAudioElement := TW3AudioElement.Create;
  FAudioElement2 := TW3AudioElement.Create;
  // Check if the audio elements can play my files before telling them to play so that
  // they don't throw an error.
  if FAudioElement.CanPlayTypeAsBoolean('audio/wav') then
    FAudioElement.Source := 'res/hit-01.wav';
  if w3_getIsChrome then
    FAudioElement2.Source := 'res/soundtrack1.wav'
  else if FAudioElement2.CanPlayTypeAsBoolean('audio/mp4') then
    FAudioElement2.Source := 'res/soundtrack.aac';
  // Create buttons and enemies as rectangles.
  btnPlay := TRect.CreateSized(20, 40, 40, 20);
  btnInstr := TRect.CreateSized(100, 40,70, 20);
  btnLeft := TRect.CreateSized(20, SCENEHEIGHT + 20,40, 20);
  btnRight := TRect.CreateSized(80, SCENEHEIGHT + 20, 40, 20);
  btnUp := TRect.CreateSized(140, SCENEHEIGHT + 20, 40, 20);
  btnDown := TRect.CreateSized(200, SCENEHEIGHT + 20, 40, 20);
  Enemy1 := TRect.CreateSized(Enemy1X, Enemy1Y, 40, 40);
  Enemy2 := TRect.CreateSized(Enemy2X, Enemy2Y, 40, 40);
  // Tell the computer which procedure to use for mouse up and mouse down.
  GameView.OnMouseDown := MouseDownHandler;
  GameView.OnMouseUp := MouseUpHandler;
  // Set variables to starting values.
  GoingRight := True;
  MobX := RandomInt(SCENEWIDTH);
  BatY := SCENEHEIGHT - 25;
  // Tell the computer about what to do in the event of a keypress.
  asm
    window.onkeydown=function(e)
    {
    TCanvasProject.KeyDownEvent(Self,e.keyCode);
    }
    window.onkeyup=function(e)
    {
    TCanvasProject.KeyUpEvent(Self,e.keyCode);
    }
    window.focus();
  end;
  KeyDownEvent(0);
  KeyUpEvent(0);
  // Set the delay between frames and start the session without frame rate calculations.
  GameView.Delay := 5;
  GameView.StartSession(False);
end;

procedure TCanvasProject.KeyDownEvent(mCode : integer);
begin
  case mCode of
    // Tell the computer what to do if a specific key is pressed (37, 65 are left arrow and a).
    37, 65: MovingLeft := True;
    39, 68: MovingRight := True;
    40, 83: MovingDown := True;
    38, 87: MovingUp := True;
  end;
end;

procedure TCanvasProject.KeyUpEvent(mCode : integer);
begin
  // Turn movement off when key goes up
  case mCode of
    37, 65: MovingLeft := False;
    39, 68: MovingRight := False;
    40, 83: MovingDown := False;
    38, 87: MovingUp := False;
  end;
end;
// Paint the main game on the canvas.
procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  // Fill in the two starting buttons and write the text on them.
  Canvas.FillStyle := 'rgb(120, 120, 120)';
  Canvas.FillRect(btnPlay);
  Canvas.FillRect(btnInstr);
  Canvas.FillStyle := 'rgb(0, 0, 255)';
  Canvas.FillText('Play', btnPlay.Left + 10, btnPlay.Bottom - 5);
  Canvas.FillText('Instructions', btnInstr.Left + 10, btnInstr.Bottom - 5);
  if not playpressed then
    exit;
  FrameCount += 1;
  Canvas.Font := '7pt verdana';

  if FrameCount = 1 then
    begin
      Enemy1X := RandomInt(SCENEWIDTH - 40);
      Enemy2X := RandomInt(SCENEWIDTH - 40);
      FAudioElement2.Load;
      FAudioElement2.Play;
    end;
  // Fill scene with background colour.
  Canvas.FillStyle := bgcolour;
  Canvas.FillRect(0, 0, SCENEWIDTH, SCENEHEIGHT);
  // Set the fill colour and fill buttons
  Canvas.FillStyle := 'rgb(120, 120, 120)';
  Canvas.FillRect(btnRight);
  Canvas.FillRect(btnLeft);
  Canvas.FillRect(btnUp);
  Canvas.FillRect(btnDown);
  // Set the colour and fill the text on the buttons
  Canvas.FillStyle := 'blue';
  Canvas.FillText('Right', btnRight.Left + 10, btnRight.Bottom - 5);
  Canvas.FillText('Left', btnLeft.Left + 10, btnLeft.Bottom - 5);
  Canvas.FillText('Up', btnUp.Left + 10, btnUp.Bottom - 5);
  Canvas.FillText('Down', btnDown.Left + 10, btnDown.Bottom - 5);
  // All of this happens from the second frame
  if FrameCount > 1 then
    begin
      // Fill in the goal
      GoalRect := TRect.CreateSized(halfway, 40, GoalWidth, 5);
      Canvas.FillStyle := 'white';
      Canvas.FillRect(GoalRect);
      //Set up the blocks
      blocks[1] := TRect.CreateSized(BlockX, BlockY, 40, 20);
      blocks[2] := TRect.CreateSized(BlockX + 80, BlockY, 40, 20);
      blocks[3] := TRect.CreateSized(BlockX - 60, BlockY + 70, 40, 20);
      blocks[4] := TRect.CreateSized(BlockX - 20, BlockY + 110, 40, 20);
      blocks[5] := TRect.CreateSized(BlockX + 40, BlockY + 140, 40, 20);
      blocks[6] := TRect.CreateSized(BlockX + 100, BlockY + 110, 40, 20);
      blocks[7] := TRect.CreateSized(BlockX + 140, BlockY + 70, 40, 20);
      // Set up the enemies
      Enemy1 := TRect.CreateSized(Enemy1X, Enemy1Y, 40, 40);
      Enemy2 := TRect.CreateSized(Enemy2X, Enemy2Y, 40, 40);
      // Fill in the enemies if they are alive
      if Enemy1alive = true then
        begin
          Canvas.FillStyle := 'yellow';
          Canvas.FillRect(Enemy1);
        end;
      if Enemy2alive = true then
        begin
          Canvas.FillStyle := 'yellow';
          Canvas.FillRect(Enemy2);
        end;
      // Set the colour to blue and fill in the blocks
      Canvas.FillStyle := 'blue';
      for var i := 1 to 7 do
        Canvas.FillRect(blocks[i]);
      // Fill the bat
      Bat := TRect.CreateSized(BatX, BatY, BatWidth, 20);
      Canvas.FillStyle := 'red';
      Canvas.FillRect(Bat);
      // Fill the ball
      ballrect := TRect.CreateSized(MobX, MobY, MOB_WIDTH, MOB_HEIGHT);
      Canvas.FillStyle := ballcolour;
      Canvas.FillRect(ballrect);
      // Change position of ball before next paint.
      if GoingRight = True then
        inc(MobX)
      else
        dec(MobX); //decrement MobX (decrease it by 1)
      if GoingDown = True then
        inc(MobY)
      else
        dec(MobY);
      // Adjust the direction if the ball hits the edge of the scene.
      if MobX + MOB_WIDTH >= SCENEWIDTH then
        GoingRight := False;
      if MobX <= 0 then
        GoingRight := True;
      // If the ball hits the bottom, mute the sound and stop the game.
      if MobY + MOB_HEIGHT >= SCENEHEIGHT then
         begin
         FAudioElement2.Muted := true;
         GameView.EndSession;
         end;
      // Adjust the direction if the ball hits the top of the screen.
      if MobY <= 40 then
        GoingDown := True;
      // See tutorial http://pp4s.co.uk/main/tu-sms-graphics-intersect.html
      // Loop through all of the blocks and see if the ball has hit them
      for var i := 1 to 7 do
        if ballrect.Intersect(blocks[i],intersectRect) then
          begin
          // Make the ball change colour if it hits a block.
          RandomNum2 := RandomInt(6);
            if RandomNum2 = 0 then
              ballcolour := "yellow"
            else if RandomNum2 = 1 then
              ballcolour := "green"
            else if RandomNum2 = 2 then
              ballcolour := "blue"
            else if RandomNum2 = 3 then
              ballcolour := "red"
            else if RandomNum2 = 4 then
              ballcolour := "purple"
            else if RandomNum2 = 5 then
              ballcolour := "orange";
            // Make the ball bounce off the block and change direction.
            if (GoingDown) and (MobY < blocks[i].Top)then
              GoingDown := False
            else if (not GoingDown)  and (MobY + 10> blocks[i].Bottom)then
              GoingDown := True;
          end;
      // Check if the enemy is alive.
      if enemy1alive then
        begin
        // Check if the ball has hit the enemy and make it bounce off.
          if ballrect.Intersect(Enemy1,intersectRect) then
            begin
              if (GoingDown) and (MobY < Enemy1.Top) then
                 GoingDown := False
              else if (not GoingDown)  and (MobY + 10 > Enemy1.Bottom) then
                GoingDown := True;
              if hits > 0 then
                Enemy1time := FrameCount;
            end;
        end;
      // Do the same for the second enemy.
      if enemy2alive then
        begin
          if ballrect.Intersect(Enemy2, intersectRect) then
            begin
              if (GoingDown) and (MobY < Enemy2.Top) then
                GoingDown := False
              else if (not GoingDown) and (MobY + 10 > Enemy2.Bottom) then
                GoingDown := True;
              if hits > 0 then
                enemy2time := FrameCount;
            end;
        end;
      // Check if the ball has hit the bat
      if ballrect.Intersect(Bat, intersectRect) then
        begin
          hits += 1; { adds one to the amount of times the ball has hit the bat
                      (This is necessary because you can't take one life from the enemy if
                       you haven't hit the ball yet).
          Load the sound effect for when the bat hits the ball.  }
          FAudioElement.Load;
          // Play it.
          FAudioElement.Play;
          scoretime := FrameCount; { tells us when the ball hit the bat.
          (this is necessary because if you add one point when the ball hits the bat,
          you end up with multiple points being added
          as the ball stays in contact with the bat for longer.
          In this case, we wait ten frames before adding a point)}

          // Make the ball bounce off the bat.
          if (GoingDown) and (MobY < BatY) then
            GoingDown := False
          else if (not GoingDown) and (MobY > BatY) then
            GoingDown := True;
        end;
      // Check if the ball has hit the goal and set scoretime (adding a point) accordingly.
      if ballrect.Intersect(GoalRect, intersectRect) then
        begin
          if GoingDown = False then
            scoretime := FrameCount;
        end;
      // Set the text colour to white and fill in the word score along with the score at the top left of the screen.
      Canvas.FillStyle := 'rgb(255, 255, 255)';
      Canvas.FillTextF("Score:  " + inttostr(score), 10, 20, MAX_INT);
      // Check if it has been ten frames since we scored and then add a point.
      if FrameCount > scoretime + 10 then
        begin
          score += 1;
          scoretime := MAX_INT; // Sets the scoretime value to the highest integer allowed
                                // so that we don't get any extra scores.
        end;
      // Do the same for enemies to see if we should decrease their lives by one.
      if FrameCount > enemy1time + 10 then
        begin
          enemy1hits += 1;
          // Check if the enemy has been hit three times and add 5 points if necessary.
          if Enemy1hits = 3 then
            begin
              enemy1alive := false;
              score += 5
            end;
          enemy1time := MAX_INT;
        end;

      if FrameCount > enemy2time + 10 then
        begin
          enemy2hits += 1;
          if Enemy2hits = 3 then
            begin
              enemy2alive := false;
              score += 5
            end;
          enemy2time := MAX_INT;
        end;
      // Move the blocks in the middle over if the framecount is a multiple of five.
      if FrameCount mod 5 = 0 then
        BlockX += numtoadd;
      // Change the direction of the blocks if the framecount is divisible by seven hundred.
      if FrameCount mod 700 = 0 then
        numtoadd := -numtoadd;
      // Move the bat if it needs to move
      if MovingLeft then
        BatX -= 4;
      if MovingRight then
        BatX += 4;
      if MovingDown then
        BatY += 2;
      if MovingUp then
        BatY -= 2;
      // Put the bat back on the screen if it goes off
      if BatX <= 1 then
        BatX := 1;
      if BatX >= SCENEWIDTH - BatWidth then
        BatX := SCENEWIDTH - BatWidth;
      if BatY <= 40 then
        BatY := 40;
      if BatY >= SCENEHEIGHT - 20 then
        BatY := SCENEHEIGHT - 20;
      // Make the enemy bounce of the edge of the screen if it gets there.
      if Enemy1X + 40 = SCENEWIDTH then
        Enemy1Right:= False;
      if Enemy2X + 40 = SCENEWIDTH then
        Enemy2Right:= False;
      if Enemy1X = 0 then
        Enemy1Right:= True;
      if Enemy2X = 0 then
        Enemy2Right:= True;
      if Enemy1Right = True then
        Enemy1X += 1;
      if Enemy1Right = False then
        Enemy1X -= 1;
      if Enemy2Right = True then
        Enemy2X += 1;
      if Enemy2Right = False then
        Enemy2X -= 1;
      // Fill in the amount of lives left on the enemies if they are alive.
      if enemy1alive then
        begin
          Canvas.Font := '12pt verdana';
          Canvas.FillStyle := 'rgb(0, 0, 0)';
          Canvas.FillTextF(inttostr(3 - Enemy1hits), Enemy1X, Enemy1Y + 15, MAX_INT);
          Canvas.Font := '7pt verdana';
        end;

      if enemy2alive then
        begin
          Canvas.Font := '12pt verdana';
          Canvas.FillStyle := 'rgb(0, 0, 0)';
          Canvas.FillTextF(inttostr(3 - Enemy2hits), Enemy2X, Enemy2Y + 15, MAX_INT);
          Canvas.Font := '7pt verdana';
        end;
      // Check if the framecount is divisible by 100.
      if FrameCount mod 100 = 0 then
        begin
        // If the enemy is dead, it has a 1 in 3 chance of regenerating.
          if not enemy1alive then
            begin
              RandomNum := RandomInt(3);
              if RandomNum = 0 then
                begin
                  enemy1alive := true;
                  enemy1hits := 0;
                end;
            end;
          if not enemy2alive then
            begin
              RandomNum := RandomInt(3);
              if RandomNum = 0 then
                begin
                  enemy2alive := true;
                  enemy2hits := 0;
                end;
             end;
        end;
      // Every one thousand frames, the bat gets two pixels smaller.
      if (FrameCount mod 1000 = 0) and (BatWidth > 40) then
        begin
             Batwidth -= 2;
        end;
      // Play the theme if it has stopped.
      if FAudioElement2.IsEnded then
        begin
          FAudioElement2.Load();
          FAudioElement2.Play();
        end;
      // Score 25 points for a silver background
      if score >= 25 then
        bgcolour := "silver";
      // Score 50 points for a gold background
      if score >= 50 then
        bgcolour := "gold";
    end;
end;
// Tell the computer what to do when the game ends.
procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

end.    

Remarks

Can you develop this program further so that, for example, you can have several lives.

Programming - a skill for life!

Student programs to inspire you!