Software Engineering - Product Șutdaun
- To immerse players in the ultimate heist experience, combining strategic planning and high-stakes action, where every difficulty is a thrilling and dynamic adventure.
- These are included in the GDD(Game Design Document) file from the Documentation folder.
The code for project can be found here.
- For our backlog, we used Github Issues and Jira. To access it on Jira, click here and you have to log in and have permission to our project.
Heist Master is a single-player, first-person shooter where the player takes up the role of a thief participating in a robbery. The game requires a combination of stealth, strategy, and fighting techniques to evade the guards and steal the money. The player can choose to either shoot the guards or silently stab them if they remain unnoticed, all while looting the money hidden along the way. The game ends with the discovery of the secret safe and the successful escape back to the vehicle.
-
- UML Diagram
classDiagram class Interactable { <<abstract>> - useEvents: bool - promptMessage: string *+ Interact()*: void + BaseInteract(): void } class KeyPad { - objectToInteractWith: GameObject + Interact(): void } class EventOnlyInteractable { } class InteractableEditor { - sampleObject: EventOnlyInteractable + OnInspectorGUI(): void } class InteractionEvent { - OnInteract: UnityEvent } Interactable <|-- EventOnlyInteractable : extends EventOnlyInteractable --* InteractableEditor Interactable <|-- KeyPad : extends class Map { - npcSpawner : NPCSpawner } class NPCSpawner { - listOfNPCs : List~NPC~ - npcCounter : int + Spawn() : void } class NPC { - stateMacine : StateMachine - agent : NavMeshAgent - currentState : string - path : PathAI + Agent : (NavMeshAgent: Get;) } class PathAI { - waypoints : List~Transform~ - alwaysDrawPath : bool - drawAsLoop : bool - drawNumbers : bool - debugColour : Color + OnDrawGizmos() : void + DrawPath() : void + OnDrawGizmosSelected() : void } class StateMachine { - activeState : BaseState + Initialize() : void + Update() : void + ChangeState(BaseState newState) : void } class BaseState { <<abstract>> - enemy : enemy - stateMachine : StateMachine *+ Enter()* : void *+ Perform()* : void *+ Exit()* : void } class PatrolState { - waypointIndex : int - waitTimer : float + Enter() : void + Perform() : void + Exit() : void + PartolCycle() : void } class SearchState { - searchTimer : float - movingTimer : float + Enter() : void + Perform() : void + Exit() : void } class AttackState { - movingTimer : float - losingPlayerTimer : float - shotTimer : float + Enter() : void + Perform() : void + Exit() : void + Shoot() : void + Start() : void + Update() : void } class NPCNonEnemy { - followPattern : FollowPattern } class FollowPattern { + Start() : void + Update() : void } class NPCEnemy { - enemy : Enemy } class Enemy { - agent : NavMeshAgent - debugSphere : GameObject - player : GameObject - Player : (GameObject: Get;) - lastKnownPos : Vector3 - LaskKnownPos : (Vector3: Get; Set;) - distanceForSight : float - fieldOfVieew : float - eyeHeight : float - gunBarrel : Transform - fireRate: float + Start() : void + SeePlayer() : bool + Update() : void } Map --* NPCSpawner NPCSpawner --* NPC StateMachine --* BaseState BaseState <|-- AttackState : extends BaseState <|-- SearchState : extends BaseState <|-- PatrolState : extends NPC <|-- NPCEnemy : extends NPCEnemy --* Enemy NPC <|-- NPCNonEnemy : extends NPCNonEnemy --* FollowPattern NPC --* StateMachine NPC --* PathAI class GameManager { - instance : GameManager - isStealth : bool - player : Player - guiManager : GUIManager - sceneManager : SceneManager - musicManager : MusicManager - audioManager : AudioManager - saveProgressManager : SaveProgressManager + Awake() : void } class AudioManager { - instance : AudioManager - audioSourcePrefab : GameObject - timeToSwitch : float - audioSourceCount : int - audioSources : List~AudioSource~ - volume : float + Awake() : void + Start() : void + Init() : void + Play(AudioClip audioClip) : void + GetFreeAudioSource() : AudioSource } class MusicManager { - instance : MusicManager - audioSource : AudioSource - timeToSwitch : float - playOnStart : AudioClip - volume : float - switchTo : AudioClip + Awake() : void + Start() : void + Play(AudioClip musicToPlay, bool interrupt) : void + SmoothSwitchMusic() : IEnumerator } class GameData { - health : float - playerPosition : float[] - isStealth : bool - currentScene : string + GameData(GameData data) : Constructor } class SaveProgressManager { - savedGames : List~GameData~ + SaveGame(GameData dataToSave) : void + LoadGame() : GameData } class ScreenTint { - untitledColor : Color - tintedColor : Color - image : Image - speed : float + Awake() : void + Tint() : void + Untint() : void + TintScreen() : IEnumerator + UntintScreen() : IEnumerator } class SceneManager { - instance : SceneManager - screenTint : ScreenTint - cameraConfiner : CameraConfiner - currentScene : string - unload : AsyncOperation - load : AsyncOperation + Awake() : void + Start() : void + InitSwitchScene(string sceneTo, Vector3 targetPosition) : void + Transition(string sceneTo, Vector3 targetPosition) : IEnumerator + SwitchScene(string sceneTo, Vector3 targetPosition) : void } class GUIManager { - instance : GUIManager - isVisible : bool + SaveGame() : void + LoadGame() : void + ChangeSensitivity(float input) : void + ChangeAudioVolume(float input) : void + ChangeMusicVolume(float input) : void } GUIManager --* GameManager AudioManager --* GameManager MusicManager --* GameManager ScreenTint --* SceneManager SceneManager --* GameManager GameData --* SaveProgressManager SaveProgressManager --* GameManager class Player { - CharacterController : Unity Component - inputManager : InputManager - playerMotor : PlayerMotor - playerLook : PlayerLook - playerHealth : PlayerHealth - audioController : AudioController } class PlayerLook { - cam: Camera - xRotation: float - xSensitivity : float - ySensitivity : float + ProcessLook(Vector2 input) : void } class InputManager { - playerInput : PlayerInput - onFoot : PlayerInput.OnFootActions - motor : PlayerMotor - look : PlayerLook + Awake() : void + FixedUpdate() : void + LateUpdate() : void + OnEnable() : void + OnDisable() : void } class PlayerMotor { - controller : CharacterController - playerVelocity : Vector3 - speed : float - isGrounded : bool - jumpHeight : float - gravity : float - isCrouching : bool - lerpCrouch : bool - crouchTimer : float - isSprinting : bool + Start() : void + Update() : void + ProcessMove(Vector2 input) : void + Jump() : void + Crouch() : void + Sprint() : void } class PlayerHealth { - health : float - lerpTimer : float - maxHealth : float - chipSpeed : float - frontHealthBar : Image - backHealthBar : Image - overlay : Image - duration : float - fadeSpeed : float - durationTimer : float + Start() : void + Update() : void + UpdateHealthUI() : void + TakeDamage(float damage) : void + RestoreHealth(float healAmount) : void } class PlayerUI { - promptText : TextMeshProUGUI + Start() : void + UpdateText(string promptMessage) : void } class PlayerInteract { - cam : Camera - interactDistance : float - mask : LayerMask - playerUI : PlayerUI - inputManager : InputManager - soundsMade : List~AudioClip~ - audioController : AudioController + Start() : void + Update() : void } class AudioController { + Play(AudioClip sound) : void } GameManager *-- Player Player *-- PlayerLook Player *-- InputManager Player *-- PlayerMotor Player *-- AudioController Player *-- PlayerHealth Player *-- PlayerInteract Player *-- PlayerUI
- C4 Diagrams
- UML Diagram
The game runs with at least 30FPS and approximately 100FPS on various PCs ensuring a reasonable response time.
The game was tested during development by both developers, users and automated tests to ensure it’s reliability and multiple issues were fixed. It runs smoothly without any significant glitches or crashes.
The game was thoroughly tested across multiple devices to test its performance and availability and runs seamlessly on different devices.
The system has it’s save files encrypted with an AES 128-bit encryption key and a 16-byte initialization vector which is then encoded into a Base64 string for easy file storage ensuring that they can’t be tampered with by users for gaining an unfair advantage.
To ensure usability we have implemented a tutorial with instructions about controls and game mechanics that allows players to understand how the game works. The tutorial also explains NPC behavior, the presence of cameras, and the various ways in which you can eliminate a guard and win the game.
To ensure maintainability we used prefabs for commonly used elements and we followed object oriented programming principles for writing the scripts. We also used Github features such as branches and tests so we can manage updates more easily.
The system’s save data files are stored and encrypted in the event of a partial failure or external attack and can be accessed once the error is solved.
-
Accepted
New players need to familiarize themselves with the game mechanics and controls.
We implemented a comprehensive game walk-through that explains the game mechanics, controls, and objectives before every new game. The tutorial is also designed to be interactive and allows players to actively engage with the game’s features.
The game is more accessible to new users and player engagement is boosted.
Accepted
We need to make sure that all the code changes work as expected.
We implemented Unit tests, Build tests, and User tests to make sure our code was properly implemented. Unit tests were run before pushing code to the development branch, on every commit the project was built to ensure no changes had broken the build and user tests were done every 2 weeks.
The code is more reliable and bugs and issues were detected and fixed.
Accepted
Players shouldn’t be able to modify save files to gain an unfair advantage by modifying data.
The files will be encrypted with an AES 128-bit encryption key and a 16-byte initialization vector which is then encoded into a Base64 string for easy file storage in order to protect user data.
The security of the system is enhanced and save files are protected against unauthorized users who can’t alter their data to their advantage.
During the development process, our team dedicated significant attention to the QA stages, using various types of testing.
Throughout the development of the game’s functionalities, the entire team was involved in the testing process. Once a developer finished implementing a specific functionality, they tested it individually. When confident that their code was fully functional, the rest of the team began testing the functionality to ensure a high standard of quality.
Additionally, after implementing any new functionality, we conducted Regression Testing to ensure that adding new features did not affect existing components or compromise the project's integrity.
During development, we collaborated with other teams to test our products. In nearly every sprint, we exchanged intermediate products with other teams to test each other’s functionalities.
The primary goal of this was to identify weaknesses in the user interface (UI) and game experience from the perspective of a first-time player and to receive objective and "merciless" feedback. Over the course of 7 sprints, we received 4 reviews of the game.
From a performance standpoint, we took all necessary implementation measures early on to avoid overloading the user’s device. However, to ensure this at all times, we tested the product on multiple devices with varying specifications. This helped us ensure that the client’s device performance would not negatively impact their experience with our product.
Security was one of our top priorities, as we aimed to provide users with a highly reliable product. The measures we implemented adhered to the highest standards in the field and are detailed in a dedicated section of the documentation.
-
-
Risk: Players might modify save files to alter game data, such as health, ammo.
-
Solution
We want our game save file to be protected against unauthorized access by encrypting it.
The file is encrypted with an AES 128-bit encryption key and a 16-byte initialization vector (to ensure the encrypted file content is unique, even if there are no changes), which is then encoded into a Base64 string for easy file storage.
public byte[] Encode(byte[] bytes, byte[] key, byte[] vector) { Aes aes = Aes.Create(); // instance of AES Encryption Object ICryptoTransform encryptor = aes.CreateEncryptor(key, vector); // will perform the encryption using the key and IV MemoryStream memoryStream = new MemoryStream(); // temporary storage for encrypted data CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write); // link memoryStream to encryptor cryptoStream.Write(bytes, 0, bytes.Length); // write data as plaintext, where it then gets encrypted cryptoStream.Close(); return memoryStream.ToArray(); }
FileDataHandler.cs extends Cryptography
private byte[] key = new byte[16] { x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x }; byte[] iv = new byte[16] { y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y };
By protecting our game.data file, we can prevent players from gaining an unfair advantage since reading/modifying variables is impossible without the key and IV.
Save file location:
C:\Users\%USERNAME%\AppData\LocalLow\DefaultCompany\Game\game.data
game.data as a json, before encryption
{ "playerPosition": { "x": -19.420000076293947, "y": -0.2408899962902069, "z": 16.200000762939454 }, "playerHealth": 78.0, "lastScene": "BankFloor1", "detected": true, "anticipation": false, "assault": true, "timeInAnticipation": 30.008705139160158 }
game.data as a Base64 string, after encryption + encoding
SaS+knakQ3NejCinK7OrPUFoUGfJr07wEV4ZB6YdUoAf6pVet+rqJZa92QMbDmdKxbHmY4TkwAWalkT4p8p7hduD+LLovrVPwYmW1tvUOIKQsOCQ4cn6h1hdFpD2UFapIBpfYvoe4xwdLRIvSnki+lqtCSHg5hcJRjncdUv+fdwKtcOOAunbf2Gq6HrO5AQ3OflGb1UEaqTNGY/5FlcOTav6gBbs+d7gSrBcZwhI73HUgyP9TEI2QrABfTekactJukmlYQDMcqdsHufNu4ipSV4Fm6VIoudeCBQ0sjYyQSsoMjTR3hJ8hy6cXP9VgZpmlWtTcokhd5ijNNdFRRbgZUHcsnQzY/DUeA6EmmoAcAX0o82DRuT/A0AYM0TD5PBNS0s+Tf2ETZGiadaM7cWDZA==
-
-
- Risk: Players could use external tools (Cheat Engine) to modify the game variables stored in memory (infinite money, health; alter detection status).
- Possible solutions: Anti-cheat software to detect unauthorized access, memory validation.
-
- Risk: Players could exploit weaknesses in the bodyguard AI code and game mechanics to bypass detection.
- Possible solutions: Improving AI logic by adding random patrol paths to make the AI less predictable.
-
- This is where the team actively worked on and tested new features.
- In CI/CD, this environment was primarily used for running unit tests and build tests, ensuring that code changes were correct and that no issues were introduced (e.g., resolving bugs that a colleague had worked on).
- Frequent deployments happened here to ensure continuous testing of the latest code changes.
-
- A staging environment closely mirrors the production environment and is used for final testing before public release.
- It was configured to be as close to production as possible, and User Acceptance Testing (UAT) was performed here to validate the game’s readiness for release.
- This environment served as the stage where users often flagged bugs or performance issues, providing valuable feedback for final improvements.
- The production environment is where the end-users interacted with the game.
- CI/CD ensured that only thoroughly tested and stable versions of the project made it to production, reducing the risk of deploying broken or incomplete features.
- Critical updates or bug fixes were the only changes pushed to this environment to ensure the game remained stable for users.
-
- The development environment was frequently updated with every new commit.
- It was primarily used to test new features, fixes, and improvements.
- This environment was often unstable due to the constant flow of changes, but it allowed for real-time debugging and collaboration among team members.
-
- The staging environment mirrored the production environment as closely as possible.
- It served as the final step before deploying to production, used for bug fixes, and User Acceptance Testing (UAT).
- While generally more stable than development, it could still have bugs due to the introduction of new features or unfinished work.
-
- The production environment represented the final, stable version of the game that users experienced.
- No new features or changes were deployed unless they were bug fixes or critical improvements that were overlooked in development or staging.
- CI/CD ensured that the deployment to production was smooth, and that the latest release was free of bugs.
-
- Automated tests were run every time new code was pushed to the development branch.
- Unity's Test Framework (or another testing tool) was used to ensure individual parts of the game code were functioning correctly.
-
- On every commit, the project was built to ensure that no changes had broken the build.
- Tools like
game-ci/unity-builder
were used to automate the build process within the CI pipeline, ensuring continuous integration and reducing manual intervention.
-
- Every two weeks, a group of students manually tested the game, providing feedback on bugs, gameplay issues, and feature suggestions.
- These tests focused on the user experience, ensuring the game was engaging, bug-free, and enjoyable for players.
-
- The pipeline was configured using GitHub Actions, with separate workflows defined for different stages such as testing and building.
- Caching was used to speed up builds and tests by reusing previously built assets and dependencies (using
actions/cache
). - The pipeline environment was set up with necessary secrets and variables (Unity license, credentials), ensuring a secure and automated process.