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.
"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
- 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.
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
In order to compile this code with Version 3.0 of Smart Mobile Studio, add System.Types.Graphics to the uses clause and change all instances of Canvas.Font to Canvas.FontStyle.
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.