An iOS Peggle clone developed using SwiftUi. Everything was developed from scratch, including the physics engine and game engine.
This project aims to create a fully-functional Peggle clone for the iPad. Currently there are two main features, and correspondingly two main Views in the application:
- Designing a level by placing and dragging pegs, together with the ability to save and load levels (LevelDesignerView)
- Consists of smaller Views such as
GameBoardView,BottomBarViewetc which uses are self-explanatory
- Consists of smaller Views such as
- Playing the designed level by shooting a ball from the cannon and clearing all pegs (
StartGameView)
- SwiftUi to render the Views
- Codable for persistence
- CoreGraphics for coordinate system
- Press START to start the level that is designed
- Type the level name into the text box and press SAVE to save a level
- Press LOAD to select a level from the available saved ones
- Press RESET to clear all pegs on the board
- Pressing anywhere on the board fires a ball from the cannon in that direction
- When a peg collides with a ball, it lights up and is removed when:
- The ball is stationary
- The ball leaves the board
- The score, number of blue pegs and orange pegs hit is currently a work in progress
- There are walls at the side and top of the game board preventing the ball from exiting the game board other than from the bottom
I used the MVVM (Model, View, View-Model) paradigm when designing my application, where the View is tightly coupled with the View Model.
- Each View stores an instance of its corresponding ViewModel which takes care of the logic, placement and positions of objects and communicates with the Model
- For this problem set, I did the
Game Renderer, which takes care of updating theGame Engine, which will be described in more detail below. - The
StartGameViewModelstores and creates aGame Rendererobject upon initialization, and passes in the array ofGameObjects which is to be updated and rendered by theGame Renderer. - Meanwhile the
LevelDesignerViewModeldoes not directly store thePersistencemodels but simply uses it as a medium to convert to and from the data stored as ajson.PersistenceUtilsis underPersistenceand it contains the methods which allow easy encoding/decoding of the board- Lastly,
GameObjects (Pegs, Balls, and Walls) are stored in both theStartGameViewModeland theLevelDesignerViewModelto be rendered in their respective views.
For the easy addition of new components into the game, the game uses a slightly modified version of an Entity Component System (ECS).
Each GameObject represents an Entity, in that it is a "container of components". Instead of making a singleton Entity Manager which stores a mapping of Entitys to their respective dictionary of Components, I went for a more testable solution of making each GameObject store a dictionary of it's components instead - the EntityComponentSystem.
If we want to check if a Game Object is a Wall for example, we can just do gameObj.getComponent(of: WallComponent.self), instead of going through the Entity Manager like entityManager.get(gameObj).getComponent(of: WallComponent.self).
Using an ECS makes it easily extensible if I want to make a GameObject a Peg, a SpookyPeg, and change image on hit using ActivateOnHit, I can do gameObj.setComponent(of: Peg()), gameObj.setComponent(of: SpookyPegComponent()) and gameObj.setComponent(of: ActivateOnHit(imageNameHit: imageNameHit))
Each component stores the respective data required for the component, and makes it easy for me to compartmentalise my data.
You might see that there is a lot of empty Components without any data, as some of them are just used purely for identification (eg seeing if I'm colliding with a Wall for example).
Also the reset function is used to reset the data of the components upon exit as some components are not deleted and recreated when navigating through views.
In a normal ECS, systems hold the logic for components. However, I find that in this case, as components rarely even had data, much less logic and behaviour, I combined the System and Component aspect of the ECS into one.
Instead, I called the struct that maps component names to their data the EntityComponentSystem.
Data involving all the state of the objects are found here.
As shown above, OrangePegs and BluePegs inherit from Peg, which in turn inherits from GameObject.
GameObjectexists to make it extensible when other objects are added, such as rectangles, squares, or triangles.GameObjectsare then stored in an arrayobjArrin the ViewModel, and are rendered by the View.PhysicsBodytakes care of the collisions/overlaps between otherPhysicsBodys using theisIntersectingmethod.- The radius of a
Pegand it'scoordinatesare handled by it'sPhysicsBody. - The
PhysicsBodyexists to ensure a clear separation between the Physics Engine and the rest of the logic.
Basically, the flow of how the game renders and updates the view is as follows:
Note that the StartGameViewModel is created when the START button is pressed, but due to the limitations of PlantUML I am unable to show it as such.
- From the LevelDesignerView, the user presses the START button
- As the
StartGameViewModelis wrapped in aLazyView, it is initialized only when the button is pressed - When START is pressed, the
objArrstoring the list of objects is passed into theinitof theStartGameViewModel StartGameViewModelcreates and stores a reference to theGameRenderer. TheobjArris passed to theGameRendererto be updated on a loop- The
StartGameViewModelsubscribes to theobjArrin theGameRendererto observe for any changes and renders it accordingly in the View - The
GameRenderercreates theGameEngineand passes theobjArrto it, and it takes care of the game-specific behaviour such as removing objects outside the boundaries and removing lighted up Pegs - The
GameEnginecreates aPhysicsEngineand stores a reference to it
On each tick of the CADisplayLink in the GameRenderer used to synchronize the game with the refresh rate:
- The
update()function in theGameRendereris called by theCADisplayLink, which in turn calls the GameEngine's update() function:gameEngine.update() - The
GameEngineremoves all objects outside the boundaries of the game and removes the lighted up Pegs based on certain conditions (ball not moving or ball is outside boundaries) - The
GameEnginecallssimulatePhysics(), which calls methods in thePhysicsEngineto update next game state which is as follows:
- All
dynamicPhysicsBodys are updated according to their respective positions, velocity, and acceleration. The resultant force is calculated based on it'sforcesarray and added to the acceleration. - Note: currently, the only thing that applies
forceis gravity, as collisions modify the velocity directly because I found it to be more realistic. This allows us to further extend it in case of other forces which are not collisions, like wind. - Velocities are updated by checking if all other objects are intersecting with the
dynamicPhysicsBody. - Lastly, the coordinates are updated to prevent overlapping by "pushing back" the two objects when they collide such that they no longer overlap with each other
- The updated
objArris then returned by theGameEngineand is published to the subscribingStartGameViewModel.
- All elements which want to have physics simulations need to have a
PhysicsBody, describing its coordinates and size (in the form of radius for a circle, height and width for a rectangle etc) - The
PhysicsEnginecalculates the next coordinates of aPhysicsBodygiven aPhysicsBodyand an array of otherPhysicsBodys. The methods here are called by theGameEngineon every loop.
The view, LevelDesignerView, consists of all the front-facing logic such as what will be displayed when:
- a button is clicked
- pegs are placed
- pegs are dragged
- etc
Whenever for example a button is pressed, it will notify the View Model if there are any updates required.
It also observes the View Model for changes in state for the array of pegs (objArr), the currently selected object (selectionObject), etc.
The StartGameView just displays the cannon and all objects in the objArr given in the StartGameViewModel.
The view-model, LevelDesignerViewModel, consists of higher level logic and functions called by the view when the above occur. We have functions that take care of:
- peg deletion
- peg placement
- dragging a peg
- the selection made by the user (whether to add a peg or delete)
- etc
The PlaceholderObj represents the object which allows the user to tell the position of the object before placement and deletion is done.
The KeyboardResponder is used to check when the keyboard is open to move the pegs up accordingly.
As we require many different levels in the game, we have a BoardList, which stores a dictionary of Boards, with the name of the Board as the key, for fast retrieval of a specific Board.
- A
boardhas an array ofEncodableObjects, which is theEncodableversion of aGameObject.- I created another class instead of making
GameObjectCodable because I wanted to cleanly separate the logic of the Model and Persistence, and I also didn't like how declaring certain properties asCodableKeyswas done.
- I created another class instead of making
- An
EncodableObjecthas x,y fields for the coordinates which can be read easily from a JSON file. This is as opposed to storingCGPointwhich can be difficult to read and store. PersistenceUtilsare for utility functions for persistence, like encoding and decoding the board.
To win the game, you have to clear all the red pegs. You can select a powerup from the start menu, but the rules of the game is the same. Try to get the highest score! Score:
- Each peg give 100 points
- Each Orange peg gives 50 more points
You start with 10 balls. Every time you shoot a ball, the number of balls get subtracted. You win the game by clearing all orange pegs. You lose if you run out of balls and there are still orange pegs remaining in the game.
Powerups:
- Eyeball pegs are spooky pegs
- Red pegs are kaboom pegs
- They are activated when hit and can be placed in the level designer.
Eyeball from: https://www.how-to-draw-funny-cartoons.com/cartoon-eyeball.html
Mars planet from: https://www.pinterest.com/pin/222928250288021864/






