TowerOfArcher
by George Wright: Y10 Age ~14
Introduction
Click here to play TowerOfArcher. If the program does not work in your current browser, try Chrome.
George quickly became adept at using images in Smart Pascal and he developed this game remarkably quickly; he has shown the qualities of a fine game-making competitor. As usual, any feedback is welcome.
Gameplay
- See procedure TEnemy.Hit to see how the damage to an enemy depends on its speed.
- You start with no gold and need to kill an enemy to obtain some. You might need to hit it several times to kill it.
- In order to shoot a long way, you need to "drag the bowstring back" from the right of the screen. (You do not need to start from the bow).
- You might find that when you right-click to cancel a shot a menu appears. Clicking the middle button of a mouse might be a better alternative.
With practice you will become a more skilful archer and your games will last longer.
Technical Features
The program benefits from:
- object-oriented code throughout;
- use of inheritance e.g. TAirUnit and TGroundUnit are descendants of TEnemy;
- handling of touch events;
- well-planned code separated into units;
- thorough comments;
- use of C-style operators such as +=;
- use of inbuilt routines such as High, RandomInt, Round, Ceil, Trunc, Sqr, Sqrt, Sin, Cos, ArcTan2, FillRectF, Ellipse, MoveToF, LineToF, FillTextF, Translate, Rotate, ContainsPoint and BeginPath;
- use of the inbuilt types TRect and TW3EventRepeater.
George acknowledges that his game was inspired by Bow Master.
Compiling TowerOfArcher with Version 3.0 of Smart Mobile Studio
We made many changes to the code to enable it to compile with Version 3.0 of Smart Mobile Studio and you can download the zipped updated project. We added System.Time and/or System.Types.Graphics to uses clauses where necessary and changed all instances of Canvas.Font to Canvas.FontStyle. We now need to create a TW3EventRepeater with code such as Timer := TW3EventRepeater.Create(@HandleTimer, TimeBetweenShots); where the function HandleTimer is coded thus:
function TArcher.HandleTimer(Sender: TW3CustomRepeater): TW3RepeatResult; begin CanShoot := true; exit(rrDispose); end;
In order to prevent compilation warnings, we change in the uses clauses each occurrence of the unit prefix W3 to SmartCL. (except for W5Image which becomes SmartCL.Controls.Image).
Code of Main Unit
unit UTowerArcher; { Copyright (c) 2015 George Wright 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 W3GameApp, W3Graphics, W3Image, UDrawing, UMouseInputs, UPlayer, USpawner, UGameVariables, UGameItems, UPlayerData, UShop, UTextures, UScalingInfo; type TApplication = class(TW3CustomGameApplication) protected procedure ApplicationStarting; override; procedure ApplicationClosing; override; procedure PaintView(Canvas: TW3Canvas); override; end; procedure InitializeVariables(); procedure UpdateArrows(); procedure UpdateEnemies(); procedure EvaluateLoadedContent(); var ContentLoaded : boolean; Loaded : array of boolean; FirstLoop : boolean; implementation procedure TApplication.ApplicationStarting; begin inherited; // Initialize textures ArrowTexture := TW3Image.Create(nil); ArrowTexture.LoadFromURL("res\Arrow.png"); BowTexture := TW3Image.Create(nil); BowTexture.LoadFromURL("res\Bow.png"); ArcherTexture := TW3Image.Create(nil); ArcherTexture.LoadFromURL("res\Archer.png"); GroundUnitTexture := TW3Image.Create(nil); GroundUnitTexture.LoadFromURL("res\GroundEnemy.png"); FrozenGroundUnitTexture := TW3Image.Create(nil); FrozenGroundUnitTexture.LoadFromURL("res\GroundEnemyFrozen.png"); AirUnitTexture := TW3Image.Create(nil); AirUnitTexture.LoadFromURL("res\AirEnemy.png"); FrozenAirUnitTexture := TW3Image.Create(nil); FrozenAirUnitTexture.LoadFromURL("res\AirEnemyFrozen.png"); TowerTexture := TW3Image.Create(nil); TowerTexture.LoadFromURL("res\Tower.png"); // Tell the program the content has not loaded ContentLoaded := false; Loaded := [ false, false, false, false, false, false, false, false ]; // Add event handlers so loaded content is registered ArrowTexture.OnLoad := procedure(o : TObject) begin Loaded[0] := true; end; BowTexture.OnLoad := procedure(o : TObject) begin Loaded[1] := true; end; ArcherTexture.OnLoad := procedure(o : TObject) begin Loaded[2] := true; end; GroundUnitTexture.OnLoad := procedure(o : TObject) begin Loaded[3] := true; end; FrozenGroundUnitTexture.OnLoad := procedure(o : TObject) begin Loaded[4] := true; end; AirUnitTexture.OnLoad := procedure(o : TObject) begin Loaded[5] := true; end; FrozenAirUnitTexture.OnLoad := procedure(o : TObject) begin Loaded[6] := true; end; TowerTexture.OnLoad := procedure(o : TObject) begin Loaded[7] := true; end; // Initialize the shop Shop := TShop.Create(); // Add the mouse and touch input handlers GameView.OnMouseDown := MouseDownHandler; GameView.OnMouseUp := MouseUpHandler; GameView.OnMouseMove := MouseMoveHandler; GameView.OnTouchBegin := TouchDownHandler; GameView.OnTouchEnd := TouchUpHandler; GameView.OnTouchMove := TouchMoveHandler; // Initialize refresh interval GameView.Delay := 20; // Start the redraw-cycle with frame counter disabled GameView.StartSession(False); end; procedure TApplication.ApplicationClosing; begin GameView.EndSession; inherited; end; procedure TApplication.PaintView(Canvas: TW3Canvas); begin // Scale the canvas ScaleCanvas(GameView.Width, GameView.Height); Canvas.Scale(Scale, Scale); // Canvas and clear the screen ClearScreen(Canvas); if ContentLoaded then begin // Draw player and scenery DrawScenery(Canvas); DrawPlayer(Player, Canvas); // Draw the arrows and enemies DrawArrow(Arrows, Canvas); DrawEnemy(Enemies, Canvas); // Update the arrows and enemies if not in the shop if not Paused then begin UpdateArrows(); UpdateEnemies(); // Draw the mouse to origin line if preparing to fire, not paused and not dead if Lives > 0 then begin DrawMouseDragLine(Player, Canvas); end; // Draw a circle over the mouse showing if the player can shoot DrawCanShoot(Player, Canvas); end else begin // Draw shop/pause screen if it is presently open DrawPauseScreen(Canvas); end; // Draw the information for the player DrawHUD(Canvas); // Draw the game over screen if dead if Lives <= 0 then begin DrawGameOver(Canvas); end; end else begin EvaluateLoadedContent(); DrawLoadingScreen(Canvas); end; // If the game is over and the restart button has been clicked then restart the game if (Lives <= 0) and (RestartClicked) then begin PauseEnemySpawners(); InitializeVariables(); end; // Scale the canvas back to normal Canvas.Scale(1 / Scale, 1 / Scale); end; procedure InitializeVariables(); begin // Initialize the variables MaxPower := 30; ArrowDamage := 10; ArrowsFreeze := false; ArrowFreezeDuration := 0; TimeBetweenShots := 2000; PauseButtonCoordinates := [ 10, 10, 110, 50 ]; RestartButtonCoordinates := [ Round(GAMEWIDTH / 2 - 50), 200, Round(GAMEWIDTH / 2 + 50), 250 ]; RestartClicked := false; Paused := true; Lives := 10; Money := 0; // Initialize the spawner variables GroundDelay := 9000; AirDelay := 15000; Difficulty := 1000; // Reset game items and the shop Arrows.Clear(); Enemies.Clear(); Shop.ResetItems(); // Spawn a ground unit SpawnGroundUnit(); // Initialize the player Player := TPlayer.Create(TowerTexture.Handle.width - 15 - ArcherTexture.Handle.width, GAMEHEIGHT - TowerTexture.Handle.height - ArcherTexture.Handle.height); end; procedure UpdateArrows(); begin for var i := 0 to High(Arrows) do begin if (Arrows[i].Active) then begin // Get the current x and y positions for the collision engine var prevX := Arrows[i].X; var prevY := Arrows[i].Y; // Move the arrow Arrows[i].Move(); // Check the collisions Arrows[i].CheckCollisions(Enemies, prevX, prevY); end; end; end; procedure UpdateEnemies(); begin for var i := 0 to High(Enemies) do begin if not Enemies[i].Dead then begin // Only move the enemy if it's not frozen if not Enemies[i].Frozen then begin Enemies[i].Move(); end; end; end; end; procedure EvaluateLoadedContent(); begin // Evaluate if everything is loaded for var i := 0 to High(Loaded) do begin // Break the procedure if the content is not loaded if not Loaded[i] then begin exit; end; end; // If the procedure has not ended the content is loaded so check the shop if Shop.Loaded then begin // State the content has all loaded ContentLoaded := true; // Initialize variables now content has been loaded InitializeVariables(); end; end; end.
See the code of the other 17 units (arranged in alphabetical order) on the following two pages: UAirUnit, UArcher, UArrow, UDrawing,UEnemy, UGameItems, UGameVariables, UGroundUnit, UMouseInputs, UPlayer, UPlayerData, UScalingInfo, UShop, UShopItem, UShopData, USpawner and UTextures.
If you would like to develop the game further, you can download a zip file containing the project file, units, images and README.txt.