Game Architecture is a series of blog posts where we take a high-level look at the code architecture behind free game projects and learn from them. In today’s post, we’ll be studying the Creator Kit: Puzzle project by Unity.

Quick Summary

Before we dive into the scripts, here’s a summary of what you can learn from this Unity project:

  • This project is an example of using Scriptable Objects to store level information. This way, scripts can access important information without relying on a Data Manager script that persists across all levels using DontDestroyOnLoad().
  • The project illustrates proper segregation between scripts: SceneMenu does the logic, SceneMenuIcon displays the information, SceneReference contains the data.
  • The BaseInteractivePuzzlePiece.cs script demonstrates the Open-Closed Principle and the Dependency Inversion Principle of SOLID. The user-controlled puzzle pieces inherit from an abstract instead of a concrete class. Moreover, the user can easily add another puzzle piece without altering the code of the base class.

Code Architecture

App flow

  1. In the main menu, class SceneMenu manages the flow. It creates a grid layout for the SceneMenuIcon and display their information. This script also references the SceneReference scriptable objects that it assigns to each SceneMenuIcon during creation. You can edit the details of each level such as Display Name, Total Required Stars, and 1-2-3 Star Time.
  2. Pressing the icons triggers SceneMenuIcon.LoadLevel() that passes a SceneReference to SceneMenu.LoadLevel()
  3. SceneMenu.LoadLevel() checks the SceneReference with its list of levels and calls LevelInfo.LoadLevel() while passing ScreenFader and the total earned stars.
  4. LevelInfo.LoadLevel() does some checks and calls SceneReference.ReloadLevel().
  5. SceneReference.ReloadLevel() calls UpdateActions() and ScreenFader.FadeOut().
  6. UpdateActions() calls SceneManager.LoadSceneAsync(). All GameObjects are destroyed during loading.
  7. Once the gameplay level is loaded, SceneFader calls FadeIn() and TimingRecording calls EnableControl().
  8. When marble reaches goal, TargetTrigger calls TimingRecording.GoalReached -> CompleteLevelWithDelay() -> SceneCompletion.CompleteLevel() which calls a PlayableDirector to show a UI animation. Player is then given the choice to go to menu or restart.
main-menu

Also, SceneMenu creates an <strong>AssetReferener</strong> game object with DontDestroyOnLoad(). This game object holds SceneReference scriptable objects. I don’t see this being used anywhere else in the project. My guess is this is just a convenient way to see the player’s progress through the levels during runtime.

  • SceneMenuIcon – an icon the shows the user relevant information regarding each level.
  • SceneReference – scriptable objects that contain information for each scene such as level index, earned stars, pointing system, etc. It uses SceneAsset, an editor-only class used to reference scenes in the inspector. SceneAsset cannot be used during runtime!
  • <strong>SceneReferenceEditor</strong> – replaces the inspector for the SceneReference scriptable objects and hides many of its properties. They did this because they want the user to edit the level parameters inside the SceneMenu inspector instead. Maybe to make it easier for non-coders. I don’t think this is necessary though.
  • <strong>SceneReferencesBuildSetup</strong> – Editor-only helper that ensures the correct scenes are in the Unity Build Settings and that SceneReference assets have correct build indices before you enter Play mode. Prevents missing menu/level scenes at runtime and avoids manual Build Settings edits each time you change levels.

Gameplay

  • TargetTrigger – triggers a bunch of events when the player reaches the goal. Calls the GoalReached() method in TimingRecording.
  • InteractivePuzzlePiece – an abstract class that is the parent of Flipper, TrapDoor, and SwingHammer.
  • TimingRecording – on Awake, this script searches for all objects of type BaseInteractivePuzzlePiece and enables user control for each. It also updates the Timer text. After reaching the goal, this script passes the Timer information to SceneCompletion.
  • SceneCompletion – using on the time received from TimingRecording, this script compares it to the pointing system in the level’s SceneReference scriptable object, record 1-2-3 stars, shows the results to the player, and reloads the menu level.

Audio

  • MarbleAudio – the marble’s audio changes based on speed and IsGrounded calculated by AudioAdjustmentSettings. It’s impact sound also changes depending on the other collider’s layer mask.
  • AudioAdjustmentSettings – a struct that calculates the volume and pitch of a sound based on a number of variables.

Takeaways

Two things I see that can be improved in this current setup:

  • First, there is no GameManager, SceneLoader, or EventsManager singleton that persists between scenes. Thus, sequences of events such as level loading end up scattered in ScreenFader, SceneCompletion, and SceneReference. This setup is simpler but harder to troubleshoot.
  • Second, the SceneCompletion. and TimingRecording scripts being attached to prefabs under the UI canvas. I’d attach these scripts to a GameObject outside the canvas to separate UI from game logic as much as possible (MVC, MVP, MVVM).

Send me a message.


Recent posts: