In it are some generalized base classes/interfaces for implementing what youâre talking about.
I break it into 3 parts.
Scenes - what represents the actual scene being loaded. Basically itâs a wrapper around the âstringâ based scene loading the unity has built in. I prefer an object model to strings, so this wrapper exists for that purpose. The structure also allows compositing scenes together into a single scene represented as a single object.
SceneBehaviour - This is the behaviour of the scene. It deals with selecting what actual scene gets loaded, what happens while its loaded, all sort of stuff.
SceneManager - this is the singleton that acts as the central point where you can transition scenes. Itâs a singleton so that itâs easy to access, as well as because itâs the way unity operates⌠unity only has one stage upon which the scene runs⌠so the idea of multiple scenemanagers is mostly useless (well of course I could think of a multi-scene managing system that would simulate multiple stages on one⌠but thatâs fairly complicated as well as unnecessary complexity. If I wanted to write that Iâd make it independent of this).
Youâll notice my SceneManager inherits from a class called Singleton. All my singletons inherit from this class, it acts as boilerplate for singletons so that I donât have to write the code over and over.
With this all a single scenebehaviour may be very simple. For example this is my ErrorScreen:
using UnityEngine;
using System.Collections;
using com.spacepuppy;
using com.spacepuppy.Scenes;
using com.spacepuppy.Utils;
namespace com.apoc.Scenes.DebugScene
{
public class ErrorScreen : SPComponent, ISceneBehaviour
{
public IProgressingYieldInstruction LoadScene()
{
var scene = new SimpleScene("ErrorScene");
return scene.LoadAsync();
}
public void BeginScene()
{
}
public void EndScene()
{
}
}
}
All it really does is load a specific âerrorâ scene and displays it. This is the screen I go to if the game goes into a faulted state.
Where as this is my LevelBehaviour:
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.Cameras;
using com.spacepuppy.Scenes;
using com.spacepuppy.StateMachine;
using com.spacepuppy.Utils;
using com.apoc.Ui;
using com.apoc.Role;
using com.apoc.Scenario.Generic;
namespace com.apoc.Scenes.Levels
{
public abstract class LevelBehaviour : SPNotifyingComponent, ISceneBehaviour
{
#region Fields
[System.NonSerialized()]
private ComponentStateMachine<AbstractLevelState> _stateManager;
private ICamera _mainCamera;
private CameraMovementController _cameraMovementController;
private PlayerEntity _currentPlayerEntity;
private i_PlayerCheckPoint _lastCheckPoint = null;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
_stateManager = new ComponentStateMachine<AbstractLevelState>(this.gameObject);
this.AddComponent<GamePlayState>();
this.AddComponent<GamePausedState>();
this.AddComponent<GameCutSceneState>();
}
protected override void OnStartOrEnable()
{
base.OnStartOrEnable();
_stateManager.StateChanged -= this._stateManager_StateChanged;
_stateManager.StateChanged += this._stateManager_StateChanged;
}
protected override void OnDisable()
{
base.OnDisable();
_stateManager.StateChanged -= this._stateManager_StateChanged;
}
#endregion
#region Properties
public ITypedStateMachine<AbstractLevelState> States { get { return _stateManager; } }
public ICamera MainCamera { get { return _mainCamera; } }
public CameraMovementController MainCameraMovementController { get { return _cameraMovementController; } }
public PlayerEntity CurrentPlayerEntity { get { return _currentPlayerEntity; } }
public i_PlayerCheckPoint LastCheckPoint { get { return _lastCheckPoint; } }
public bool GamePlayActive { get { return _stateManager.Current is GamePlayState; } }
public bool Paused { get { return _stateManager.Current is GamePausedState; } }
public bool InCutscene { get { return _stateManager.Current is GameCutSceneState; } }
#endregion
#region Methods
protected abstract IProgressingYieldInstruction LoadScene();
protected virtual IProgressingYieldInstruction ReloadScene()
{
return this.LoadScene();
}
private void DoBeginScene()
{
//Code the MUST be ran before BeginScene is called. Otherwise any child classes may not be prepared in time.
_mainCamera = CameraManager.Main;
if (_mainCamera == null)
{
throw new System.InvalidOperationException("A camera of name 'MainCamera' must exist in the scene.");
}
_cameraMovementController = _mainCamera.gameObject.GetComponent<CameraMovementController>();
if(_cameraMovementController == null)
{
_cameraMovementController = LevelBehaviour.AddDefaultCameraMovementController(_mainCamera.gameObject);
}
//Register Notification Listeners
Notification.RemoveGlobalObserver<PlayerEntity.PlayerDied>(this.OnGlobalPlayerDiedNotification);
Notification.RegisterGlobalObserver<PlayerEntity.PlayerDied>(this.OnGlobalPlayerDiedNotification);
this.BeginScene();
}
protected virtual void BeginScene()
{
this.SpawnPlayerAndStartGamePlay();
this.Invoke(() =>
{
if (_cameraMovementController != null) _cameraMovementController.SnapToTarget();
}, 0.1f);
}
private void DoEndScene()
{
Notification.RemoveGlobalObserver<PlayerEntity.PlayerDied>(this.OnGlobalPlayerDiedNotification);
this.EndScene();
SPTime.Normal.Paused = false;
}
protected virtual void EndScene()
{
}
public void SpawnPlayerAndStartGamePlay()
{
this.States.ChangeState<GamePlayState>();
var startPoint = GameObject.Find("PlayerStartPoint");
if (startPoint == null) throw new System.InvalidOperationException("Level must contain a PlayerStartPoint.");
var checkpoint = startPoint.GetComponent<i_PlayerCheckPoint>();
if (checkpoint == null) throw new System.InvalidOperationException("Level must contain a PlayerStartPoint.");
this.SetCheckPoint(checkpoint);
this.SpawnPlayerAtLastCheckpoint();
}
public void SpawnPlayerAtLastCheckpoint()
{
if (_lastCheckPoint == null) throw new System.InvalidOperationException("No Checkpoint Present.");
_currentPlayerEntity = Game.SpawnPlayer(_lastCheckPoint.transform.position, Quaternion.LookRotation(Vector3.right, Vector3.up));
if (_cameraMovementController != null) _cameraMovementController.TetherTarget = _currentPlayerEntity.transform;
}
public void SetCheckPoint(i_PlayerCheckPoint checkpoint)
{
_lastCheckPoint = checkpoint;
}
public void ReturnToGamePlay()
{
if (this.GamePlayActive) return;
_stateManager.ChangeState<GamePlayState>();
}
public void Pause()
{
if (this.Paused) return;
_stateManager.ChangeState<GamePausedState>();
}
public void StartCutscene()
{
if (this.InCutscene) return;
var state = _stateManager.GetState<GameCutSceneState>();
_stateManager.ChangeState(state);
}
#endregion
#region Event Handlers
private void _stateManager_StateChanged(object sender, StateChangedEventArgs<AbstractLevelState> e)
{
if (e.FromState != null)
{
e.FromState.OnExitState(e.ToState);
}
if (e.ToState != null)
{
e.ToState.OnEnterState(e.FromState);
}
}
private void OnGlobalPlayerDiedNotification(PlayerEntity.PlayerDied n)
{
if (n.Entity == this.CurrentPlayerEntity)
{
//this.Invoke(() =>
//{
// this.SpawnPlayerAtLastCheckpoint();
//}, Game.Settings.PlayerRespawnWaitDuration);
this.StartRadicalCoroutine(this.OnGlobalPlayerDiedNotification_Routine());
}
}
private System.Collections.IEnumerator OnGlobalPlayerDiedNotification_Routine()
{
yield return new WaitForSeconds(Game.Settings.PlayerRespawnWaitDuration);
if(Game.Settings.ReloadLevelOnDeath)
{
yield return this.ReloadScene();
}
this.SpawnPlayerAtLastCheckpoint();
yield return null;
if (_cameraMovementController != null) _cameraMovementController.SnapToTarget();
}
protected virtual void Update()
{
if (_stateManager.Current != null) _stateManager.Current.UpdateState();
}
#endregion
#region ILevelBehaviour Interface
IProgressingYieldInstruction ISceneBehaviour.LoadScene()
{
return this.LoadScene();
}
void ISceneBehaviour.BeginScene()
{
this.DoBeginScene();
}
void ISceneBehaviour.EndScene()
{
this.DoEndScene();
}
#endregion
#region Static Utils
public static CompositeScene CreateLevelScene(string sceneName, Scene primaryScene, params Scene[] extraScenes)
{
if (primaryScene == null) throw new System.ArgumentNullException("primaryScene");
var result = new CompositeScene(sceneName);
result.Add(primaryScene);
result.SetPrimaryScene(primaryScene);
for (int i = 0; i < extraScenes.Length; i++)
{
result.Add(extraScenes[0]);
}
//result.Add(new SimpleScene("InGameHUD"));
return result;
}
public static CameraMovementController AddDefaultCameraMovementController(GameObject camera)
{
if (camera == null) throw new System.ArgumentNullException("camera");
var controller = camera.AddComponent<CameraMovementController>();
controller.TargetCamera = camera.transform;
controller.UseUpdateSequence = UpdateSequence.FixedUpdate;
var state = controller.AddComponent<com.apoc.Entities.Cameras.SimpleCameraTether>();
state.Offset = new Vector3(0f, 1.5f, -10f);
state.LerpSpeed = 5f;
controller.StartingCameraStyle = state;
controller.States.ChangeState(state);
return controller;
}
#endregion
}
}
This sort of scene represents a level in the game, so it does quite a bit. Not only does it do quite a bit, but it too actually has a statemachine to change the state of the scene itself (ingame, pause, cutscene).
Here you can see what happens in those states. Right now, not very much.
GamePlayState - this really just waits for you to press the pause button, which transitions the level to the paused state.
using UnityEngine;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.Utils;
using com.apoc.Ui;
namespace com.apoc.Scenes.Levels
{
public class GamePlayState : AbstractLevelState
{
#region Fields
#endregion
#region Properties
#endregion
#region Methods
#endregion
#region AbstractLevelState Abstract Overrides
public override void OnEnterState(AbstractLevelState lastState)
{
}
public override void OnExitState(AbstractLevelState nextState)
{
}
public override void UpdateState()
{
var inputDevice = Game.InputManager.FirstOrDefault() as ApocPlayerInputDevice;
if (inputDevice == null) return;
var input = inputDevice.GetCurrentState();
if (input.Pause == spacepuppy.Ui.ButtonState.Down)
{
this.Level.Pause();
}
}
#endregion
#if UNITY_EDITOR
private void Update()
{
if (Input.GetButtonDown("Kick"))
{
var logEntries = System.Type.GetType("UnityEditorInternal.LogEntries,UnityEditor.dll");
var clearMethod = logEntries.GetMethod("Clear", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
clearMethod.Invoke(null, null);
}
}
#endif
}
}
GamePausedState - when enters it just sets all the stuff that needs to be paused to pause (the time scale, audio, etc) as well as dispatches a global notification for entities in the level to react to.
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.Audio;
using com.spacepuppy.Utils;
using com.apoc.Ui;
namespace com.apoc.Scenes.Levels
{
public class GamePausedState : AbstractLevelState
{
#region Fields
private bool _active;
#endregion
#region Properties
#endregion
#region Methods
#endregion
#region AbstractLevelState Abstract Overrides
public override void OnEnterState(AbstractLevelState lastState)
{
_active = true;
SPTime.Normal.Paused = true;
AudioManager.Global.Pause();
Notification.PostNotification<GamePausedNotification>(this.Level, new GamePausedNotification(true), false);
}
public override void OnExitState(AbstractLevelState nextState)
{
SPTime.Normal.Paused = false;
AudioManager.Global.UnPause();
_active = false;
Notification.PostNotification<GamePausedNotification>(this.Level, new GamePausedNotification(false), false);
}
public override void UpdateState()
{
var inputDevice = Game.InputManager.FirstOrDefault() as ApocPlayerInputDevice;
if (inputDevice == null) return;
var input = inputDevice.GetCurrentState();
if (input.Pause == spacepuppy.Ui.ButtonState.Down)
{
this.Level.ReturnToGamePlay();
}
}
#endregion
private void OnGUI()
{
if (!_active) return;
var c = Color.black;
c.a = 0.5f;
GUI.color = c;
GUI.depth = int.MinValue;
GUI.DrawTexture(new Rect(0f, 0f, Screen.width, Screen.height), SPAssets.WhiteTexture);
}
}
}
Anyways⌠sure Iâm not tutorializing all of it. Nor am I saying this is the best route of doing this. Main issue is probably because I really use all sorts of parts of my spacepuppy framework, like RadicalCoroutine, RadicalYieldInstruction, SPComponent, Notification, SPTween (I just wrote this one last week⌠Iâm very pleased with it so far), etc⌠all of which is a framework designed very much with my own interests in mind for how an application should be structured. This is just how I do this⌠and Iâm more than happy to let you take a look.
@lordofduck Thanks for sharing your thought, codes.
But it seems too hard, complicated. I canât understand well how it works at first glance.
Why use abstract class and interface, #region?
abstract class - this is a class that has some implementation in it, but canât be used directly. Instead it requires you to inherit from it to implement the extra stuff that is needed. This way you can have classes that are related in functionality, and code relevant to all of them is in one place. Scene is abstract in my example, this is because you can have different kinds of scenes, like SimpleScene (represents a single scene), CompositeScene (represents multiple scenes loaded as one).
interface - this is similar to abstract class, but there is NO implementation of the type. It is just a contract that defines methods and properties that must be defined on the class. Again, this is so you can have types that are related. ISceneBehaviour is the interface in my example, your various scenes can implement this interface, all they must do is implement the 3 functions defined, and they can be used by the SceneManager.
region - this is just something for organizing code. In your IDE, if you have a region, it puts a little +/- sign that when you click it hides the code up, or expands it back out.
As for using the code I posted⌠you wouldnât necessarily just use that code, thatâs not why I posted it. It may be a bit over your head as I do not know what level programmer you are. But you can look into it and learn from it if you like⌠coding can be hard, but once you learn this kind of stuff, it gets easy. This is why I shared it⌠so you can learn. Not so you can just have it.
Technically though, you donât really need to do much just to use it if you had my framework included in your project.
If you did that, and you created a scene like my âErrorScreenâ (which is a super simple scene behaviour). Then to load it from somewhere (say on a trigger enter), youâd just say:
Then all the scenebehaviours wouldnât be of the same type.
The SceneManager expects ISceneBehaviours.
If you just implemented âErrorScreenâ and âMenuScreenâ and âLevelScreenâ⌠how would the SceneManager know how to use them? They might have similar functions on them, but there is nothing saying they actually do have similar functions.
Itâs the same reason you can treat your colliders as just âColliderâ and not have to figure out if theyâre SphereCollider or MeshCollider, unless you specifically need a SphereCollider.
I had issues grasping the concept of interfaces before as well. It becomes really useful when you want to have the same functionality across different classes.
For example, in my game I have three main components on my buildings that have an inventory in which you can take and drop items. StoreageScript, WorkScript and HouseScript.
Each script has an array representing the amount of items in stock. If I didnât use interfaces, I would need three different methods to grab items or a switch case to determine if Iâm dealing with a workplace, a house or a storeage barn. Instead all three implement the Iinventory interface. That way my classes do not care where they take or drop items from.
So now instead of guessing, any of these will work:
//just an array indicating which items to drop
int[] itemsToDrop;
DropItemsAtLocation(citizen.workScript, itemsToDrop);
DropItemsAtLocation(citizen.houseScript, itemsToDrop);
DropItemsAtLocation(nearestStorage, itemsToDrop);
All this works because the methodâs signature is:
Well then every time I added a new scene type, Iâd have to go into SceneManager and add support for it.
Where as with the interface, when ever I create a new scene, I just implement what the interface says to implement, and the SceneManager can handle it.
Iâm getting the impression you still donât know what polymorphism is, I linked an article on it. Though itâs a bit abstract in its definition.
Hereâs the article from Microsoft describing it specifically in the context of C#: