Skip to content
/ Impulse Public

A barebones C# bootstrap framework for building scalable projects quickly and easily in Unity.

License

Notifications You must be signed in to change notification settings

Zesix/Impulse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Impulse Framework Splash

Impulse Framework

A bootstrap framework designed to expedite the creation of Unity projects. The purpose of the framework is to empower developers to focus on developing the game features and worry less about common game systems such as scene management, camera systems, etc. by providing customizable implementations out of the box.

Note about licensing : Almost everything in this framework is licensed under Unlicense and can be used for any purpose (including commercial). The exceptions are:

  • The Impulse logo and Impulse Framework splash, located in "Assets/Sprites/Not_Available_For_Commercial_Use". These are proprietary and may not be used for any purposes, commercial or non-commercial.
  • Zenject Framework, located in "Assets/Plugins/Zenject". Zenject is licensed under MIT.
  • Fonts, located in the "Assets/Fonts" folder. These free fonts are included only so the demo scenes render properly, however they are not available under the Unlicense. Please acquire the appropriate license to use them from their respective websites.

Development Philosophy

We, the creators, believe clean code is based around SOLID principles. More specifically, this means:

  • Dependency injection and events (we integrate Zenject for this).
  • Scene and game management through states.
  • Separation of concerns into Data, Model, Presenter, Service. For those coming from a MVC background, this architecture may seem strange. We have found over years of implementing various Unity project architectures that this design puts us in a middle ground between taking advantage of Unity's features while also enabling testability, mocking, and not overengineering systems that are simple enough to remain MonoBehaviours.
    • Data. This is usually a ScriptableObject containing gameplay data. It contains no logic outside of editor scripts used to generate / randomize the data if needed. Note the emphasis on the word gameplay – rendering data such as movement speed, animation parameters, and other data related to presentation are not specified in this file.
    • Model, a plain C# class. Has fields matching Data and expects to be fed Data within an initialization method. Contains logic for operating and working on gameplay data. Is testable, and it's highly recommended to create Models with a TDD approach.
    • Presenter, a MonoBehaviour-derived class. Contains rendering data (movement, animation, etc.) that takes many iterations to tune. Designed to hold all data that does not need to be tested or is difficult to test. If there are other components on the gameobject, the Presenter is responsible for hooking them together.
    • Service, which can be a plain C# class or a MonoBehaviour. Service is a general term used for systems that can be used where needed, when needed. For example, SceneService is responsible for switching scenes in the game (optionally with transitions like a loading screen) and can be injected and called by any script that needs to trigger a scene change.

The framework, however, does not enforce any rigid programming structure. It provides several tools that just work out of the box but leaves the implementation of your game up to you.

Project Setup

In the Build Settings, set the Splash scene to 0 and Menu to 1. Unity preloads everything in each scene, with the exception of the first scene (scene 0). For optimal performance, you should keep your splash scene as lightweight as possible and try not to add too many objects.

Project-Scoped Services (Singleton Managers)

The framework comes with commonly used services such as SceneService. If you want to add your own project-scoped managers, you can do so via the following:

  1. Create a prefab for your singleton service and add your desired component scripts necessary for functionality.
  2. Add a GameObjectContext and MonoInstaller component. Ensure the MonoInstaller binds your instance. For example:
using UnityEngine;
using Zenject;

public class SaveControllerInstaller : MonoInstaller<SaveControllerInstaller>
{
    public override void InstallBindings()
    {
        transform.GetComponent<GameObjectContext>().Container.BindInstance(GetComponent<ILocalDataManager>());
    }
}
  1. Drag the prefab to the _MainSystemStartup (Zenject) prefab.

Your singleton service prefab will now be spawned in the project scope no matter what scene you start Play mode from.

SceneService

The SceneService is used for loading scenes (with or without transitions). A scene can be loaded in the following ways:

  • Show a custom splash image when the game is first started, then load the main menu. This is the default behavior of the SceneService.
  • Load a scene with a fade to black transition.
  • Load a scene with a fade to black, then loading screen, then fade out transition once the scene is ready.
  • Load a scene with a fade to black, then loading screen, prompt user for input once scene is ready, then fade out transition once input is received. This is the default behavior when loading a new scene from the main menu.

These loading methods are called programmatically - look at SceneService.cs to see the methods.

Set a Custom Splash Image

Many games have a splash image or studio logo shown before the game begins. The framework can be set up to display a custom splash image before loading the main menu.

  1. Locate the Resources/Prefabs/Scene/SplashFadeIn object in the project files. Select the ImageToFade child object.
  2. Set the Source Image of the Image component to whatever splash image you want to display.

Scene Loading Methods (fade in/out, interpolation, duration, loading screen, wait for keypress)

Fade In / Out:

  1. Locate the Resources/Prefabs/Scene/SceneService object.
  2. In the SceneService component, you can specify the Duration of fade in/out as well as the Interpolation of the fade. If you do not want to fade in/out scenes, set the duration to 0.

By default, SceneService scene changes have a fade in / out time. You can change this by editing the SceneService prefab or programmatically. For the latter, the SceneService is assigned to the main system startup prefab and can be injected as a dependency into any script.

Loading Screen:

The loading screen service offers both interactive (click to continue) and automatic (starts scene once loaded) transitions. Loading screens are selected at random from a LoadingScreenConfig scriptable object. This design follows the trend in many games where a random loading screen is selected that shows gameplay tips for the user to read while they wait.

To add a loading screen:

  1. Create the UI for the new loading screen. The root parent object must be regular rectTransform (not a canvas!)

  2. Add the LoadingScreenPresenter component to the parent object and fill out its fields as follows:

    Non-interactive loading screen:

    2.1a: Assign the "Progress Fill" Image and Text fields as desired. If you don't want to use one of these fields, you can leave it empty.

    2.2a: Make sure the "Requires User Input" field is empty.

    2.3a: Set the default delay (after loading) in the Time After Completion field. This is the amount of time that must pass before the next scene is automatically loaded.

    Interactive loading screen:

    2.1b: Follow the same steps as above, but ignore 2.2a. Make sure the "Requires User Input" field is marked as Active.

    2.2b: If desired, assign the gameobject you want to display when the loading process is completed in the Press Any Key Obj field. This can be a UI object (rectTransform only, no canvas) or any gameobject you want to display once the loading process is complete.

  3. Finally, locate the LoadingScreenConfig scriptable object (normally in Assets/Configurations) and add the newly created loading screen to the Possible Loading Screens collection.

Customize the Main Menu

The framework provides a customizable main menu that is contained within a single scene in order to remain mobile-optimized.

Refer to the video in the section above for a video walkthrough of the main menu.

  1. Open _Scenes/Menu.unity
  2. Open the MenuSystem object. You'll notice a main menu and options menu are already set up for you, but are inactive.
  3. Create a new child object under MenuSystem and attach the MenuScreen script to it.
  4. Add your new menu elements to this new child object.
  5. Set your new child object as inactive once you are finished with it.

To switch menus using UGUI OnClick(), call the MenuManager.ChangeMenuAndFade() or MenuManager.ChangeMenu() function.

To run one of the examples in the GameExamples folder, replace Menu and the sample Level01 in the build settings with the specific menu and game scene from the example game's _Scenes folder.

StateMachine (Finite State Machine)

A deterministic finite state machine that works with C# objects as states. It derives from MonoBehaviour to be compatible as a component on game objects that need their own state machine but can also be used for controlling overall project state. While the setup can seem cumbersome, it ensures states are properly identified by their class implementations while also allowing for mocking of states through Zenject binding a different array of test states.

Usage

The following steps must be repeated for each state machine in your game.

Create an abstract state extending the State class. You must create an enum for each state that will derive from this base state. In addition, you must create an enum to identify transitions, as this is a deterministic finite state machine and each state must have a transition (and no transition can be used by multiple states).

In the below example, we define enums for Game states and transitions (the 'Game' prefix separates these states from states for other state machines).

public enum GameStateId
{
    Menu,
    Play,
}

public enum GameStateTransition
{
    BeginPlay,
    GameOver,
}

public abstract class GameState : State<FSM, GameStateId, GameStateTransition>;
{
    public override void BuildTransitions() {}
    public override void Enter() {}
    public override void Exit() {}
    public override void FixedUpdate() {}
    public override void Update() {}
}

Create subclasses of your abstract class above for each state you want in the game. For example:

using System.Collections;
using Zenject;

public class PlayState : GameStateBase
{
    private PlayerDeathSignal _playerDeathSignal;

    public PlayState()
    {
        stateId = GameStateId.Play;
    }

    [Inject]
    public void Construct(PlayerDeathSignal playerDeathSignal)
    {
        _playerDeathSignal = playerDeathSignal;
    }

    public override void BuildTransitions ()
    {
        AddTransition(GameStateTransition.NullTransition, GameStateId.GameOver);
    }

    public override void Enter ()
    {
        _playerDeathSignal += GameOverHelper;
    }

    public override void Exit()
    {
        _playerDeathSignal -= GameOverHelper;
    }

    private void GameOverHelper()
    {
        StartCoroutine(GameOver());
    }

    private IEnumerator GameOver()
    {
        yield return null;
        MakeTransition(GameStateTransition.NullTransition);
    }
}

Note that your subclass must assign the _stateId for itself. This is the GameStateId we defined in the abstract class deriving from State (stateId is a protected property of the base State class). In the example above, we make this assignment in the constructor and then create a separate injection function for dependencies.

Create a new C# class deriving from MonoBehaviour that will be your finite state machine. In the Awake() method, you must pass a few parameters:

  • This class (reference to self as a State Machine).
  • An array of states. Be sure to specify your derived base state and not the State.cs class.
  • Enum ID of the initial state, which will be transitioned to during Awake()
  • (Optional) Enum ID of a debug state.
  • (Optional) Enum ID of a tracking state.

In addition, your state machine should call the base class's associated MonoBehaviour methods such as Update(), FixedUpdate(), OnTriggerEnter, etc.

We recommend copying and pasting the following, then adapting it to your needs:

using System.Collections.Generic;
using Impulse.FiniteStateMachine;
using UnityEngine;
using Zenject;
    
public class GameStateMachine : MonoBehaviour
{
    // Configurable
    [SerializeField] private GameStateId _initialGameState;
    public GameStateId InitialGameState => _initialGameState;
    [SerializeField] private bool _debug;

    // Internal
    private StateMachine<GameStateMachine, GameStateId, GameStateTransition> GameFsm { get; set; }
    private List<State<GameStateMachine, GameStateId, GameStateTransition>> _states;

    [Inject]
    private void Construct(List<State<GameStateMachine, GameStateId, GameStateTransition>> states)
    {
        _states = states;
    }

    private void Awake()
    {
        GameFsm = new StateMachine<GameStateMachine, GameStateId, GameStateTransition>(
          this, _states, _initialGameState, _debug);
    }

    private void Update()
    {
        GameFsm.Update();
    }

    private void FixedUpdate()
    {
        GameFsm.FixedUpdate();
    }

    private void OnDestroy()
    {
        GameFsm.Destroy();
    }

    #if UNITY_EDITOR
    private void OnGUI()
    {
        if (_debug)
        {
            GUI.color = Color.white;
            GUI.Label(new Rect(0.0f, 0.0f, 500.0f, 500.0f),
              string.Format("Current State: {0}", GameFsm.CurrentStateName));
        }
    }
    #endif
}

Attach your derived state machine to a gameobject in the scene. Next, add a GameObjectContext and create an installer where you will assign the states list to be injected into the state machine. For example:

using Impulse.FiniteStateMachine;
using System.Collections.Generic;
using Zenject;

public class GameStateMachineInstaller : MonoInstaller
{
    private List<State<GameStateMachine, GameStateId, GameStateTransitio>> _states;

    public override void InstallBindings()
    {
        var menuState = new MenuState();
        var playState = new PlayState();
        var gameOverState = new GameOverState();

        Container.BindInstance(menuState);
        Container.BindInstance(playState);
        Container.BindInstance(gameOverState);
        Container.QueueForInject(menuState);
        Container.QueueForInject(playState);
        Container.QueueForInject(gameOverState);

        _states = new List<State<GameStateMachine, GameStateId, GameStateTransition>>
        {
            menuState,
            playState,
            gameOverState
        };
        Container.BindInstance(_states);
    }
}

This is how our state machine looks in the hierarchy:

Transitions

Before transitioning to a new state, you must first add the transitions by calling AddTransitions() inside the BuildTransitions() method of a State, which gets called by the state machine after a transition. For example:

public override void BuildTransitions ()
{
    AddTransition(StateTransition.BEGIN_PLAY, StateID.PLAY);
}

The first argument is the enum of the transition state, while the second argument is the enum of the state to transition to.

You can transition to another state by calling MakeTransition([enum ID of transition]);

It is important to note you cannot change state within the Enter() or Exit() methods of an existing state since you cannot change state during the middle of a state transition. In some cases it is necessary to use a coroutine to change state to allow an Enter() or Exit() method to finish. This is especially true in states where gameplay setup is done:

public override void Enter()
{
    base.Enter();
    StartCoroutine(Init());
}

private IEnumerator Init()
{
    yield return null;
    MakeTransition(StateTransition.BEGIN_PLAY);
}

We put the transition after the yield statement to ensure setup completes before transitioning to the next state.

Note: Because we override StartCoroutine(), you cannot use nameof() to generate the argument, you must reference the coroutine function with curly brackets on the end, like above.

Audio

Playing Music and Managing Playlists

The music manager and music playlist system allow for easy playback and organization of background music within scenes.

For a video demonstration of the music manager and music playlists: https://www.youtube.com/watch?v=jQGTqGalGVw&index=2&list=PLLXw4Fw6qNw5WVLPn1hhJNEcwXjxt3b9j

  1. Drag the MusicManager prefab from Assets/Prefabs/Music/MusicManager into your splash scene, or whichever scene is the first one in your build settings. The MusicManager is persistent from scene to scene, so you do not need to instantiate it in each scene.
  2. In each scene where you want music to be played, create a new empty game object and attach the MusicPlaylist.cs script. This script can be found in Assets/Scripts/Music/MusicPlaylist.cs. I recommend naming the game object 'MusicPlaylist'. Then, just populate the Music List array in the game object with song files. Leave 'Activate On Awake' to true if you want the playlist to begin playing as soon as the scene is loaded.

Cameras

Top-Down Camera

This camera is best suited for 2D games.

For a video demonstration of the top-down camera: https://www.youtube.com/watch?v=DLTyrbMxytA&list=PLLXw4Fw6qNw5WVLPn1hhJNEcwXjxt3b9j&index=3

  1. Locate the script in Assets/Scripts/Camera/TopDownFollow_Camera.cs.
  2. Attach this script to a camera object in your scene.
  3. Drag a Transform into the Follow Target parameter. This is the object the camera will try to follow.
  4. Set the Target Offset and Move Speed parameters to your liking. Target Offset is x,y,z distance from the follow target (the camera position offset relative to the follow target object). Move speed is how fast the camera moves when the object moves.

Third Person Camera

This camera is based on the camera used in many popular MMORPG games and automatically zooms in when the follow target is obstructed by an object.

For a video demonstration of the third person camera: https://www.youtube.com/watch?v=DDdnLPPZXLg&index=4&list=PLLXw4Fw6qNw5WVLPn1hhJNEcwXjxt3b9j

  1. Locate the script in Assets/Scripts/Camera/Third_Person_Camera.cs
  2. Attach this script to a camera in your scene.
  3. Create an empty game object and rename it to 'LookAt'. This is the object the camera will focus on and follow.
  4. Make the LookAt object a child object of the gameobject you want to follow.
  5. Assign the LookAt object in the Target Look Transform parameter of the Third Person Camera component on the camera.
  6. To add mouse controls such as zoom-in with the mouse scrollwheel, attach the Third_Person_Mouse_Input.cs script to the camera. This script is located in the Assets/Scripts/Camera folder.

User Interface

The framework includes an InterfaceManager that allows easy switching between interface screen (canvas) objects by setting them active / inactive. The only requirement is that each canvas object has an Interface Screen component (InterfaceScreen.cs). You can find the Interface Manager prefab in the "Assets/02_Prefabs/UI" folder.

The Interface Manager also has methods for calling the SceneService to change scenes. This is useful for games that have a main menu.

See InterfaceManager.cs for different interface screen (canvas object) switching methods, as well as scene change methods.

AI

Most AI scripts in the framework are based around a Faction component that specifies what faction a gameobject belongs to. For a gameobject to be used with the AI scripts, it must have the Faction.cs script attached along with a faction specified (factions can be neutral in addition to friendly or hostile).

Faction.cs is located in the Assets/Scripts/AI folder.

Waypoints

The waypoint system provides an easy way of generating a connected path of points, with the option to ensure it is a closed loop. The waypoints system does not use the Faction system, it simply creates waypoints that any object can follow.

  1. Locate the WaypointPathManager.cs script in Assets/Scripts/Utility/Waypoints
  2. Create an empty game object in your scene and attach WaypointPathManager.cs
  3. Create any number of empty game objects and place them throughout your scene. Make them a child object of the transform with the WaypointPathManager component. These empty game objects are the 'waypoints' in the path.

The WaypointPathManager loops through each child object in its transform and generates a path through them.

Public methods for using waypoints (located in WaypointPathManager.cs):

  • public int FindNearestWaypoint (Vector3 fromPos, float maxRange) – Returns the integer index of the nearest waypoint from the supplied position and within the supplied maximum range.
  • public int FindNearestWaypoint (Vector3 fromPos, Transform exceptThis, float maxRange) – The same as the above, except a waypoint transform can be passed in to ensure the nearest waypoint is not the waypoint an object is currently at.
  • public int GetNextWaypoint (int index, bool reverse) – Gets the next waypoint in the path based on the supplied index. If reverse is true, then it assumes the path is going backward (e.g. point 0 is next after point 1).
  • public Transform GetWaypoint (int index) – Returns the transform of the waypoint at the given index.
  • public int GetTotal () – Returns the total number of waypoints in the path (number of child objects under the WaypointPathManager object).
  • public bool ReachedEndOfPath (int index) – Returns true if the waypoint at the given index is the last waypoint in the path or the first waypoint in the path. This is useful for switching the waypoint traversal of an object if you want it to go back and forth from one end of the path to the other.

Viewcones

These are procedurally generated cones that can be used to give a gameobject the ability to 'see' other gameobjects.

Sphere Detector

The sphere detector projects an invisible sphere around an object. The idea is other objects within this sphere are 'detected' by the object, similar to radar. This system does not actually involve AI behavior, but can be useful in setting one up.

For a video demonstration of the sphere detector: https://www.youtube.com/watch?v=1ZLkDv9OUNc&list=PLLXw4Fw6qNw5WVLPn1hhJNEcwXjxt3b9j&index=7

  1. Locate the script in Assets/Scripts/AI/Detector.cs
  2. Attach the Detector script to a gameobject.
  3. In the Detector component, assign allied and enemy factions. Objects belonging to a faction that is not assigned will show up under the 'Detected Neutral' array during runtime.

The Detector component includes useful methods for fetching data during runtime:

Data Loading

The framework comes with a basic data loading system that reads JSON files and turns them into .asset files with an associated prefab.

To see an example of how it works:

  1. Inspect the ItemsJson.json file located at Assets/Resources/InventoryDemo/Text/ These JSON objects will have their data converted into .asset files, which will be used to generate prefabs.
  2. Inspect the JsonReader.cs script located at Assets/Scripts/Inventory/ This script is invoked by the JsonI temExtractor.cs script to read the JSON file and converts the JSON object data into a dictionary.
  3. Inspect the JsonItemExtractor.cs script located at Assets/Scripts/Inventory/ This script uses the JsonReader.cs script to create .asset files and generate prefabs for each object.

In actual production, you probably don't want to generate new prefabs each time the JSON files change but instead have the .asset files read at runtime when necessary. The prefab generation is included in the JsonItemExtractor functionality for demonstration purposes.

Player Profile

The core profile manager is LocalPlayerProfileService, which internally uses a PersistentLocalProfileService. This works by using a ILocalPlayerProfileService interface to request the storage and loading of the player information. The first, and currently only, implementation of ILocalPlayerProfileService is PlayerPrefsService, which loads and saves serialized data using Unity's prefab system.

To edit which information will be managed, the developer must edit the PlayerProfile class. Be aware that only public variables will be serialized and saved.

In all cases, the developers must interact directy with the LocalPlayerProfileService singleton in order to access, load, save or reset the PlayerProfileInfomation.

** Player Profile Reading **

  1. To access and modify the player profile just use LocalPlayerProfileService.Instance.GetPlayerProfile()

** Player Profile Saving **

  1. Access and modify the LocalPlayerProfileService.Instance.GetPlayerProfile()
  2. Request saving by calling LocalPlayerProfileService.Instance.SaveData()

** Player Profile Loading **

  1. To load the player profile just request LocalPlayerProfileService.Instance.LoadData(), be aware that this is done automatically at game start so in most cases it won't be necessary to request this

** Player Profile Reset **

  1. To request the player resetting just call LocalPlayerProfileService.Instance.ResetProfile()
  2. To confirm the reset, afterwards call LocalPlayerProfileService.Instance.SaveData()

About

A barebones C# bootstrap framework for building scalable projects quickly and easily in Unity.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages