Unity Template (Github, Download Unity Package)
Project description
I created this template to make it easier to create new projects and focus on the game itself instead of creating the same systems each time I create a new game.
StateMachine
For all my previous game projects I have always thought that something is missing or something is not right with how I handle my states of things. This made me rethink how I wanted the states to work compared to my old projects where states changed within the states themself and therefore I made it possible to store the transitions of states within the StateMachine. By doing so I can declare the transitions at the same place where I can have a quick overview of all transitions in the same class that makes the readability of the StateMachine so much clearer.
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace MaxHelpers
{
public class StateMachine
{
private static readonly List EmptyTransitions = new(0);
private readonly List _anyTransitions = new();
private readonly Dictionary> _transitions = new();
private IState _currentState;
private IState _prevState;
private List _currentTransitions = new();
public void Tick()
{
var transition = GetTransition();
if (transition != null) SetState(transition.To);
_currentState?.Tick();
}
public void SetState(IState state)
{
if (state == _currentState) return;
_currentState?.OnExit();
_prevState = _currentState;
_currentState = state;
_transitions.TryGetValue(_currentState.GetType(), out _currentTransitions);
_currentTransitions ??= EmptyTransitions;
_currentState.OnEnter();
}
public void AddTransition(IState from, IState to, Func predicate)
{
if (_transitions.TryGetValue(from.GetType(), out var transitions) == false)
{
transitions = new List();
_transitions[from.GetType()] = transitions;
}
transitions.Add(new Transition(to, predicate));
}
public void AddAnyTransition(IState state, Func predicate)
{
_anyTransitions.Add(new Transition(state, predicate));
}
private Transition GetTransition()
{
foreach (var transition in _anyTransitions.Where(transition => transition.Condition())) return transition;
return _currentTransitions.FirstOrDefault(transition => transition.Condition());
}
private class Transition
{
public Transition(IState to, Func condition)
{
To = to;
Condition = condition;
}
public Func Condition { get; }
public IState To { get; }
}
public void GoToPreviousState()
{
if (_prevState == null) return;
SetState(_prevState);
}
public IState GetPreviousState() => _prevState;
public void ResetState()
{
_currentState.OnExit();
_currentState.OnEnter();
}
}
}
namespace MaxHelpers
{
public interface IState
{
void Tick() { }
void OnEnter() { }
void OnExit() { }
}
}
As a exemple here is a implementation for the game jam project Squiddys Adventure
_stateMachine.AddTransition(_air, _deathState, () => _isGrounded && WaterLevel <= 0f);
_stateMachine.AddTransition(_ground, _deathState, () => _isGrounded && WaterLevel <= 0f);
_stateMachine.AddAnyTransition(_squirt, CanWaterSquirt);
_stateMachine.AddTransition(_squirt, _air, () => !_isGrounded && !_isUnderwater);
_stateMachine.AddTransition(_squirt, _underwater, () => !_isGrounded && _isUnderwater);
_stateMachine.AddTransition(_squirt, _ground, () => _isGrounded && !_isUnderwater);
_stateMachine.AddTransition(_ground, _air, () => !_isGrounded && !_isUnderwater);
_stateMachine.AddTransition(_air, _ground, () => _isGrounded && !_isUnderwater);
_stateMachine.AddTransition(_underwater, _leavingWater, () => !_isUnderwater && !_isGrounded);
_stateMachine.AddTransition(_leavingWater, _air, () => true);
_stateMachine.AddTransition(_air, _underwater, () => _isUnderwater);
_stateMachine.SetState(_underwater);
using MaxHelpers;
namespace SquidStates
{
public class SquidAirState : IState
{
private readonly SquidController _squidController;
private readonly SquidController.InAirParams _inAirParams;
public SquidAirState(SquidController squidController, SquidController.InAirParams inAirParams)
{
_squidController = squidController;
_inAirParams = inAirParams;
}
public void Tick()
{
_squidController.HandleMovement(_inAirParams.speed, _inAirParams.acceleration, _inAirParams.control, false);
_squidController.RotateTowards(_inAirParams.rotationSpeed, _inAirParams.rotateVelocity);
}
}
}
StaticInstance
Each project I create has some monobehaviour that uses the singleton pattern. This is why I created a superclass with different types of singletons just to reduce repeating code.
using UnityEngine;
namespace MaxHelpers
{
public abstract class StaticInstance : MonoBehaviour where T : MonoBehaviour
{
public static T Instance { get; private set; }
protected virtual void Awake()
{
Instance = this as T;
}
protected virtual void OnApplicationQuit()
{
Instance = null;
Destroy(gameObject);
}
}
public abstract class Singleton : StaticInstance where T : MonoBehaviour
{
protected override void Awake()
{
if (Instance != null) Destroy(gameObject);
base.Awake();
}
}
public abstract class PersistentSingleton : Singleton where T : MonoBehaviour
{
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(gameObject);
}
}
}
DataManager
Another system all games have is some kind of save system. To be sure I implemented a generic save system where any kind of data can be saved and used inside any class. I also wanted to encrypt my data if I want to save anything that I didn't want the user to change or read in the future. Currently, the only data saved is settings and is total overkill to encrypt. As you can see inside the DataManager below I have the encryption code inside the class itself where I can just change the code for each game. If anybody has access to the game source code then this key can be used to decrypt the savefiles and even create new files that will work with the game. If I in the future create any games that need more security then I will implement a better system for this.
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using UnityEngine;
namespace MaxHelpers
{
public class DataManager : StaticInstance
{
private readonly Dictionary _savedData = new();
private string _saveFile;
private FileStream _dataStream;
// Encryption key
private readonly byte[] _savedKey = { 0x12, 0x15, 0x16, 0x15, 0x12, 0x15, 0x13, 0x15, 0x16, 0x15, 0x16, 0x15, 0x16, 0x15, 0x16, 0x11 };
public T GetData(string saveKey) => _savedData.ContainsKey(saveKey) ? (T) _savedData[saveKey] : default;
public void SaveData(string saveKey, ISave data)
{
_saveFile = Application.persistentDataPath + $"/{saveKey}_save.json";
var iAes = Aes.Create();
_dataStream = new FileStream(_saveFile, FileMode.Create);
var inputIv = iAes.IV;
_dataStream.Write(inputIv, 0, inputIv.Length);
var iStream = new CryptoStream(_dataStream, iAes.CreateEncryptor(_savedKey, iAes.IV), CryptoStreamMode.Write);
var sWriter = new StreamWriter(iStream);
var jsonString = JsonUtility.ToJson(data);
sWriter.Write(jsonString);
sWriter.Close();
iStream.Close();
_dataStream.Close();
}
public bool Load(string saveKey)
{
_saveFile = Application.persistentDataPath + $"/{saveKey}_save.json";
if (!File.Exists(_saveFile)) return false;
_dataStream = new FileStream(_saveFile, FileMode.Open);
var oAes = Aes.Create();
var outputIv = new byte[oAes.IV.Length];
_dataStream.Read(outputIv, 0, outputIv.Length);
var oStream = new CryptoStream(_dataStream, oAes.CreateDecryptor(_savedKey, outputIv), CryptoStreamMode.Read);
var reader = new StreamReader(oStream);
var text = reader.ReadToEnd();
_savedData.Add(saveKey, (ISave)JsonUtility.FromJson(text));
reader.Close();
oStream.Close();
_dataStream.Close();
return true;
}
}
}
Data can be saved by creating a struct and using the interface ISave.
namespace MaxHelpers
{
public interface ISave {}
}
I also made it super easy to use this system by creating an abstract class DataHandler.
using UnityEngine;
namespace MaxHelpers
{
public abstract class DataHandler : StaticInstance where TC : MonoBehaviour where TD : ISave
{
[SerializeField] private TD defaultData;
private string _saveKey;
protected TD Data;
protected void InitData(string saveKey)
{
_saveKey = saveKey;
Data = DataManager.Instance.Load(saveKey) ? DataManager.Instance.GetData (_saveKey) : defaultData;
}
public void SaveData() => DataManager.Instance.SaveData(_saveKey, Data);
}
}
Here you can see a simple struct where we can save all data we need.
using System;
using UnityEngine;
namespace MaxHelpers
{
[Serializable]
public struct SettingsData : ISave
{
[Header("Volume")]
[Range(0.001f,1f)] public float masterVolume;
[Range(0.001f,1f)] public float musicVolume;
[Range(0.001f,1f)] public float soundFXVolume;
[Header("Quality")]
public bool fullScreen;
[Tooltip("This needs to be a string because of how the saving system works, different settings: Low, Medium, High")] public string qualitySettings;
public string resolution;
[Header("Controls")]
[Range(0.001f,1f)]public float sensitivity;
}
}
Here is how we can use the datahandler class. We first call the init function with a savekey, this savekey is then used to read data from disk where the savekey is also used as the first part of the filename. If no file is found the default datastruct that is defined in the Unity inspector is used else the data is decrypted and cached. If we change any data in class we can then call the SaveData method to encrypt and save that data to disk.
using UnityEngine;
using UnityEngine.UIElements;
namespace MaxHelpers
{
public class SettingsManager : DataHandler
{
private void Start()
{
InitData("settings");
Debug.Log(Data.masterVolume);
Data.masterVolume = 1f;
SaveData();
}
}
}
AudioManager
Each game has audio and therefore I made a simple audio manager where we can play sounds/music and set the volume.
using UnityEngine;
using UnityEngine.Audio;
namespace MaxHelpers
{
public class AudioManager : StaticInstance
{
[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource soundsSource;
[SerializeField] private AudioMixer audioMixer;
public void PlayMusic(AudioClip clip)
{
musicSource.clip = clip;
musicSource.Play();
}
public void PlaySound(AudioClip clip, Vector3 pos, float vol = 1)
{
soundsSource.transform.position = pos;
PlaySound(clip, vol);
}
public void PlaySound(AudioClip clip, float vol = 1) => soundsSource.PlayOneShot(clip, vol);
public void SetVolume(string mixerName, float volume) => audioMixer.SetFloat(mixerName, Mathf.Log10(volume) * 20);
}
}
Helper
This project started because of this class. I often create a helper class inside my projects because of some functions that I use often.
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace MaxHelpers
{
public static class Helper
{
// Cache the main Camera for everywhere use
private static Camera _camera;
// Cache each WaitForSeconds to minimize the allocation
private static readonly Dictionary WaitDictionary = new();
// Check if the mouse is hovering on UI element
private static PointerEventData _eventDataCurrentPosition;
private static List _results;
// Get the mainCamera from anywhere
public static Camera Camera
{
get
{
if (_camera == null) _camera = Camera.main;
return _camera;
}
}
public static WaitForSeconds GetWait(float time)
{
if (WaitDictionary.TryGetValue(time, out var wait)) return wait;
WaitDictionary[time] = new WaitForSeconds(time);
return WaitDictionary[time];
}
// Refresh a token
public static CancellationToken RefreshToken(ref CancellationTokenSource tokenSource) {
tokenSource?.Cancel();
tokenSource?.Dispose();
tokenSource = new CancellationTokenSource();
return tokenSource.Token;
}
public static bool IsOverUi()
{
_eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{position = Mouse.current.position.ReadValue()};
_results = new List();
EventSystem.current.RaycastAll(_eventDataCurrentPosition, _results);
return _results.Count > 0;
}
// Get the world position on any given Canvas Element
public static Vector2 GetWorldPositionOfCanvasElement(RectTransform element)
{
RectTransformUtility.ScreenPointToWorldPointInRectangle(element, element.position, Camera, out var result);
return result;
}
// Destroy all child objects of parent
public static void DeleteChildren(this Transform t)
{
foreach (Transform child in t) Object.Destroy(child.gameObject);
}
}
}
UIManager
I also created a basic UI where the user can start and quit the game. The user can also change settings and save them.
This UI is super simple and is using the new UIBuilder in Unity.
using UnityEngine;
using UnityEngine.UIElements;
namespace MaxHelpers {
public class UIManager : StaticInstance
{
[SerializeField] private VisualTreeAsset
mainMenuUI,
inGameMenuUI,
inGameUI,
optionsUI,
loadingUI;
private readonly StateMachine _stateMachine = new();
private OptionsState _optionsState;
private InGameState _inGameState;
protected void Start()
{
var uiDocument = GetComponent();
// Create states
MainMenuState mainMenuState = new(uiDocument, mainMenuUI);
InGameMenuState inGameMenuState = new(uiDocument, inGameMenuUI);
_inGameState = new InGameState(uiDocument, inGameUI);
_optionsState = new OptionsState(uiDocument, optionsUI);
LoadingState loadingState = new(uiDocument, loadingUI);
// Listen to events
LevelManager.Instance.OnStartLoadEvent += () => _stateMachine.SetState(loadingState);
LevelManager.Instance.OnCompletedLoadEvent += () => _stateMachine.SetState(_inGameState);
// Add Transitions
_stateMachine.AddTransition(_inGameState, inGameMenuState, GameManager.Instance.Inputs.Player.OpenGameMenu.IsPressed);
_stateMachine.AddTransition(inGameMenuState, _inGameState, GameManager.Instance.Inputs.UI.ExitMenu.IsPressed);
_stateMachine.AddTransition(_optionsState, inGameMenuState, () => IsExitPressedAndPrevState(inGameMenuState));
_stateMachine.AddTransition(_optionsState, mainMenuState, () => IsExitPressedAndPrevState(mainMenuState));
// Set start state
_stateMachine.SetState(mainMenuState);
}
private void Update() => _stateMachine.Tick();
private bool IsExitPressedAndPrevState(IState state) => GameManager.Instance.Inputs.UI.ExitMenu.IsPressed() && _stateMachine.GetPreviousState() == state;
public void GoToPreviousState() => _stateMachine.GoToPreviousState();
public void PressedOptionMenu() => _stateMachine.SetState(_optionsState);
public void ResumeGame() => _stateMachine.SetState(_inGameState);
}
}