Skip to content

Latest commit

 

History

History
40 lines (23 loc) · 9.21 KB

README.md

File metadata and controls

40 lines (23 loc) · 9.21 KB

Documentation

Overall Structure

I decided to favor composition over inheritance and implemented a system much like the Unity engine is utilizing. Every object in the game is represented by the GameEntity class which contains methods to awake, start and update the entity in the world and its position. Any additional functionality is then added through components which are updated each frame in the GameEntity's Update() method. Each component is preceded by C_, inherits from the Component class and contains reference to its owner. I chose this system for better maintainability and robustness of the code. All entities in the game can be treated the same way without the need of casting. It also allows for separation of systems such as drawing and collision from the objects themselves.

The class that handles all the entities in the game is called EntityManager. Each entity in the game has to be added to the world through the Add() method and is updated each frame through the Update() method. When added, the entities are stored in a temporary vector and then moved to a vector of all entities on the ProcessNewEntites() call. The method first calls Awake() and then Start() method on each of the entities. This ensures that all the Awake() methods will be called before all the Start() methods and we can implement the corresponding methods in the components with this being accounted for. To remove entities from the world we call the SetDelete() method on them which sets the delete flag and the entities are actually removed from the vector of all entities in the ProcessRemovals() method. Both of the process methods also handle the addition of the entities to other systems referenced in EntityManager.

Components

There are 2 special components, namely C_Sprite and C_Collision, which require their own systems for management, S_Sprite and S_Collision respectively, and are stored and operated on from EntityManager.

One of the problems of the original version which drew the FPS down considerably was that images for each entity where loaded on every frame. To avoid this problem, all the images are loaded and stored in the memory through the Drawer class once, when the C_Sprite component is added to an entity and then they are simply retrieved from a hashmap on each subsequent call of the Draw() method. The Drawer class also communicates with the SDL interface to draw the images on the screen. The S_Sprite then serves as an extension of the EntityManager class with all the corresponding methods for addition, update and removal of the entities' sprites but separated from EntityManager for better maintainability.

The S_Collision class is very much the same in this manner and the C_Collision component is a simplified version of a collision component with just 2 channels represented by the CollisionLayer enum, namely Player and NonPlayer. All the entities of the Player layer are checked against all the entities of the NonPlayer layer and if a collision is detected, the Resolve() method is called to resolve the overlap. The resolution is handled much like in Unreal engine, where we bind functions to the overlap events. This is achieved by the BindOverlapFunc() method in the C_Collision class where we pass a std::function object to the component that will be called in Resolve(). The whole component and the component management is designed such that any entity can have collision and any binded functionality is not tightly coupled to the type of entity that it operates on.

The movement logic in the original version was implemented in the Ghost and Avatar classes which both inherited from the MovableGameEntity class. I decided to scratch this due to too many inheritance layers that made the code hard to read and to extend. The avatar movement logic was moved to the InputManager class and the C_KeyboardMovement component and the ghost movement logic to the C_GhostBehaviour component. This also includes any functionality that was previously implemented in the Pacman class.

The InputManager class simply stores a hashmap that maps the keys defined by the Key enum to the SDL scan codes and determines whether a key is pressed. This is then used in the C_KeyboardMovement class to determine which way to move the player entity. The component also stores reference to the C_Animation component which changes the avatar animation based on the direction of the movement. Another change to the original version is that the avatar now doesn't stop when moved in a direction without a passage but rather continues without a stop and only changes the direction when a passage is open and a corresponding key is pressed. This greatly enhances the ease of the keyboard input and the fluidity of the avatar movement.

The C_GhostBehavior component implements all of the ghost movement logic including the pathfinding and the reversal of movement on the big dot eaten. On each spawn, the ghosts will not move from the cage for the first 5 seconds. There are 2 ghosts and their behaviors correspond to those of red and pink ghosts from the original game. The former chases the player directly whereas the latter tries to predict where the player is going to be based on its current direction.

Unlike the original version, the pathfinding is implemented iteratively rather than recursively for the performance gain and it is utilized for both going towards and running away from the avatar and for going to the spawning location. The pathfinding utilizes A* algorithm as it is optimal and optimally efficient. The algorithm is implemented in the Pathfinding() method through std::priority_queue where the key for sorting best moves in the queue is (the path travelled so far) + (manhattan distance towards target). The only difference between the ghost behaviors is their goal tile. For the red one it is the player location whereas for the pink one it is at most 4 tiles ahead of the player based on its current direction. The implementation through the priority queue also lends nicely to the reversal of the movement by just simply reversing the compare sign in the compare function for the priority queue. Another thing to enhance the performance is not to calculate the whole path towards the avatar but rather only up to a limited number of steps, in my case 10, as the player is constantly moving and it makes no sense to calculate a path to a tile in which the player is not going to be located at in a couple of frames. Similarly to the C_KeyboardMovement component, C_GhostBehavior contains a reference to the C_Animation component that is called when the ghost changes states to claimable or dead to change the ghost sprite.

The last 2 components are C_PacmanProperties and C_Animation. The former implements a behavior that makes the avatar invulnerable for a couple of seconds from spawn so that the player doesn't die immediatelly after respawn. The latter accepts Animation objects which are mapped to AnimationState enums and then operated on from other components such as C_KeyboardMovement and C_GhostBehavior when a change of animation is required.

Other Systems

The game also utilizes the SDL_mixer library for sound and the class that communicates with the library is called SoundManager. Similarly to other classes mentioned, the resources are loaded into a hashmap and are then invoked when needed, for example through functions binded to overlap events. Since this library has not been a part of the original version, I've created two versions of the project. One with the sound and one with all the resources commented out and the library files not included in the project.

The World class represents simplification of collision with static walls of the world as each wall being a game entity with the C_Collision component would be unnecessarily complicated solution. In addition to this, the World class also serves as a parser for the map text file. Unlike the original version, there are now xs added around the whole map in the text file as this resolves a lot of out-of-bounds errors when detecting if a tile to step on is valid.

The purpose of the Pacman class is similar to that of GameMode in Unreal engine as it holds the game rules and additionally it updates the game loop. Therefore, most of the initialization details have been moved to the EntityFactory class through which we can generate all the game entities with their necessary components.

Additional notes

The project is compiled with C++17.

Most of the raw pointers from the original version where either turned to std::shared_ptr or references. The shared pointers in the case of objects which can have multiple references around the code but can be null such as game entities. References are then used for objects which have to have exactly one instance for the game to work such as the Drawer or the InputManager classes. The only raw pointers which were kept are those returned from the SDL library.

Most of the magic numbers from the original version where turned into inline constants and moved to the Constants class.

I decided not to do unit testing due to the time constraints.

Given more time I'd implement the other ghost behaviors, the teleport tunnel in the middle, multiple levels and polished the code more.