Code Samples

Preamble

As a Unity developer with over a decade of experience, I have architected a variety of gameplay loops and feature sets. My work encompasses Unity’s most prominent functionality – from input handling, raycasting and physics simulation; to scene management, prefab pooling and transform manipulation. I have deployed Unity-based applications across several hardware platforms, such as PC, mobile, VR (PC streaming and Android-based), and Raspberry Pi (running Android).

I’m a frequent user of a variety of Unity’s feature packages: Cinemachine; Addressables; Input System; Timeline; Text Mesh Pro; XR Plugin Management; Netcode for GameObjects, to name a few.

My robust understanding of Unity’s lifecycle allows me to recognize when common situational optimizations like object pooling or reference caching are necessary and when niche optimizations, like manual garbage collection and asset bundles, are essential.

The code samples below are from various features I developed for previous clients (shared with permission) or from my own library of essential scripts. Click the arrow next to the header for a brief rundown of my thought process writing these.

The following code samples are written by Jeremy Ledbetter and provided for job application purposes. Please do not copy or redistribute without permission.

Global Variable Scriptable Objects

I’m a big fan of Scriptable Objects, especially for sharing data across multiple classes in a decoupled way. For example, implementing this with a float-type “Speed” variable, a UI class can display the speed value while an input class modifies it according to the position of gamepad trigger or joystick. This kind of communication can get convoluted quickly, especially if there are many different classes referencing the same scriptable object. Requiring a setter reference as an argument has two benefits: easily ignoring value changes if the setting class also reacts to changes made as well as easier debugging.

GlobalVariableScriptableObject.cs
C#
using System;
using UnityEngine;
public abstract class GlobalVariableScriptableObject<T> : ScriptableObject
{
public Action<UnityEngine.Object> OnAboutToChangeBy;
public Action<UnityEngine.Object> OnHasChangedBy;
[SerializeField]
private bool _debugValueChanges;
[SerializeField]
private bool _reportSameValueSet;
[SerializeField]
private T _globalValue;
public T GlobalValue => _globalValue;
public virtual void SetGlobalValue(T value, UnityEngine.Object setter)
{
bool isSameValue = _globalValue.Equals(value);
if (isSameValue == false || (isSameValue && _reportSameValueSet))
{
if (_debugValueChanges)
Debug.Log(this.name + " is about to change due to '"+setter.name+"'. Current value is: " + _globalValue.ToString());
OnAboutToChangeBy?.Invoke(setter);
_globalValue = value;
if (_debugValueChanges)
Debug.Log(this.name + " has changed. New value is: " + _globalValue.ToString());
OnHasChangedBy?.Invoke(setter);
}
}
}

Back to Code Samples List

Third-Party Asset/Timeline Integration
“Mystic VR” is virtual excursion through a whimsical library powered by Vive Focus 3 hardware. Click here to learn more.

Serving as the introduction to the Keeper of Keys, this five-minute “dark ride” experience was one of many interactive activities featured in Spring 2025’s Mirth & Mischief, an immersive theater experience staged at the Uhuberg Castle in Helen, Georgia. A recording of the attraction can be viewed below. Note that this was recorded in-editor; the final version ran at a consistent 90 frames per second and without any texture loading artifacts (such as the flashes of cyan seen in this recording).

This is the GUI controller class for the Mystic VR Operations App (also built in Unity). It allowed operators to set the local network address used for client-server comms, displayed the status of connected client headsets and, most importantly, cue the attraction to begin or reset.

You’ll notice my preference for explicitly declared access modifiers. I like this for two reasons: debugging simplicity and authorial intent. Writing out accessors means less to keep in mind when debugging; it’s easier to rule out unrelated fields and therefore unrelated code paths. It also clarifies what operations the class is solely responsible for, preventing redundant or conflicting work in a caller class.

I like the practice of denoting private variables with an _underscore for easy distinction when reading through code (I’ve since adopted this practice with methods as well).

The HmdEntryGui is the controller component for a prefabbed UI widget that displayed relevant client data. Note that instances of these are destroyed if the client headset disconnects (See line 116). Normally I would pool these to reduce garbage collection overhead, but headset disconnection was rare during the run of the attraction and the operations panel was a one-page 2D UI display, so GC wasn’t affecting performance anyway. That didn’t stop me from habitually cleaning up event subscriptions in OnEnable and OnDisable, despite the application never unloading this GUI in Operations mode 😅.

The battery status aspect of this GUI was added and dropped late in development for two reasons: the HMDs featured their own external battery status display and optimizing performance took precedence over the convenience of having this on the Operations panel.

OperationsGui
C#
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class OperationsGui : MonoBehaviour
{
private const string OFFLINE_TEXT = "Offline";
private const string ONLINE_TEXT = "Online";
[SerializeField]
private StringGlobalVariable _ipv4Address;
[SerializeField]
private BoolGlobalVariable _isNetworkConnected;
[SerializeField]
private TMP_InputField _ipv4InputField;
[SerializeField]
private TextMeshProUGUI _networkStatusLabel;
[SerializeField]
private Button _startHostButton;
[SerializeField]
private Button _playExperienceButton;
private HmdEntryGui _hmdEntryTemplate;
private Dictionary<ulong, HmdEntryGui> _hmdEntryGuis;
public Action OnHostButtonPressed;
public Action OnPlayExperiencePressed;
private void Awake()
{
_hmdEntryGuis = new Dictionary<ulong, HmdEntryGui>(10);
_hmdEntryTemplate = GetComponentInChildren<HmdEntryGui>();
_hmdEntryTemplate.gameObject.SetActive(false);
}
private void OnEnable()
{
_isNetworkConnected.OnHasChanged += RespondToNetworkConnectedChanged;
_ipv4InputField.onEndEdit.AddListener(SignalIpAddressChanged);
_startHostButton.onClick.AddListener(SignalHostButton);
_playExperienceButton.onClick.AddListener(SignalPlayButton);
}
private void OnDisable()
{
_isNetworkConnected.OnHasChanged -= RespondToNetworkConnectedChanged;
_ipv4InputField.onEndEdit.RemoveAllListeners();
_startHostButton.onClick.RemoveAllListeners();
_playExperienceButton.onClick.RemoveAllListeners();
}
private void Start()
{
_ipv4InputField.text = _ipv4Address.GlobalValue;
RespondToNetworkConnectedChanged();
}
private void RespondToNetworkConnectedChanged()
{
if (_isNetworkConnected.GlobalValue == true)
{
_playExperienceButton.interactable = true;
_networkStatusLabel.text = ONLINE_TEXT;
}
else
{
_playExperienceButton.interactable = false;
_networkStatusLabel.text = OFFLINE_TEXT;
}
}
private void SignalIpAddressChanged(string ipAddressText)
{
_ipv4Address.GlobalValue = ipAddressText;
}
private void SignalPlayButton()
{
OnPlayExperiencePressed?.Invoke();
}
private void SignalHostButton()
{
OnHostButtonPressed?.Invoke();
}
public void AddHmdEntry(ulong clientId, string headsetId, /*float batteryPercent = -1.1f,*/ TimelineSceneSequencer.SequenceState hmdStatus = TimelineSceneSequencer.SequenceState.Loading)
{
if(_hmdEntryGuis.ContainsKey(clientId))
throw new Exception("Trying to create an HMD Entry Gui with an ID that is the same as an existing entry!");
var templateInstance = Instantiate(_hmdEntryTemplate);
templateInstance.transform.SetParent(_hmdEntryTemplate.transform.parent);
templateInstance.gameObject.SetActive(true);
//templateInstance.SetBatteryPercent(batteryPercent);
templateInstance.SetHmdStatus(hmdStatus);
_hmdEntryGuis.Add(clientId, templateInstance);
}
public void RemoveHmdEntry(ulong clientId)
{
var entry = _hmdEntryGuis[clientId];
_hmdEntryGuis.Remove(clientId);
Destroy(entry.gameObject);
}
public void UpdateHmdId(ulong clientId, string hmdId)
{
var entry = _hmdEntryGuis[clientId];
entry.SetHmdId(hmdId);
}
//public void UpdateHmdBattery(ulong clientId, float batteryPercent)
//{
// var entry = _hmdEntryGuis[clientId];
// entry.SetBatteryPercent(batteryPercent);
//}
public void UpdateHmdStatus(ulong clientId, TimelineSceneSequencer.SequenceState status)
{
var entry = _hmdEntryGuis[clientId];
entry.SetHmdStatus(status);
}
}

Back to Code Samples List

Networked Client Hardware State
“Mystic VR” is virtual excursion through a whimsical library powered by Vive Focus 3 hardware. Click here to learn more.

Serving as the introduction to the Keeper of Keys, this five-minute “dark ride” experience was one of many interactive activities featured in Spring 2025’s Mirth & Mischief, an immersive theater experience staged at the Uhuberg Castle in Helen, Georgia. A recording of the attraction can be viewed below. Note that this was recorded in-editor; the final version ran at a consistent 90 frames per second and without any texture loading artifacts (such as the flashes of cyan seen in this recording).

To coordinate a simultaneous experience for all guests, I used Unity’s Netcode for Gameobjects to facilitate communication between a Windows-based operations app and the Android-based VR headsets over a LAN connection. This class was the primary point of network communication. Client-side logic sends client ID and sequence status to the operations app via Network Variable, informing the attraction operator which headsets were online and of their current status (loading, playing, ready, for example).

Server-side communication used RPC calls to cue the experience as well as reset it for the next group of guests.

When it comes to network logic, I prefer to have very clear separation between client and server-side variables and methods. This tends to be a little verbose, but I find it easier to distinguish when returning to code after a long stint away (and to discourage invalid use of client variables in server logic and vice versa).

Note the use of Global Variables on the client-side; the headset ID was set and serialized using a secret admin UI accessible within the headset (coordinated with a physical label on each headset). Because this ID was a Global Variable, it was easy to set and reference its value across this class and the admin UI controller class without the need for tightly coupled references (see Global Variable Scriptable Object above for further details).

NetworkedHmdState.cs
C#
using UnityEngine;
using Unity.Netcode;
using Unity.Collections;
using System;
public class NetworkedHmdState : NetworkBehaviour
{
#region Networked(shared) Declarations
private NetworkVariable<FixedString32Bytes> headsetId_net = new NetworkVariable<FixedString32Bytes>("???", readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Owner);
private const string ID_NOT_AVAILABLE = "NOT AVAILABLE";
public string HeadsetID_Net
{
get
{
if (IsSpawned)
return headsetId_net.Value.ToString();
else
return ID_NOT_AVAILABLE;
}
}
private NetworkVariable<float> batteryPercent_net = new NetworkVariable<float>(0, readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Owner);
private const float BATTERY_NOT_AVAILABLE = -1.0f;
public float BatteryPercent_Net
{
get
{
if (IsSpawned)
return batteryPercent_net.Value;
else
return BATTERY_NOT_AVAILABLE;
}
}
private NetworkVariable<TimelineSceneSequencer.SequenceState> sequenceStatus_net = new NetworkVariable<TimelineSceneSequencer.SequenceState>(TimelineSceneSequencer.SequenceState.Loading, readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Owner);
private const TimelineSceneSequencer.SequenceState SEQUENCE_STATE_UNAVAILABLE = TimelineSceneSequencer.SequenceState.NOT_AVAILABLE;
public TimelineSceneSequencer.SequenceState SequenceStatus_Net
{
get
{
if (IsSpawned)
return sequenceStatus_net.Value;
else
return SEQUENCE_STATE_UNAVAILABLE;
}
}
#endregion
#region Client-Only Declarations
[Header("Global Variable Assets")]
[SerializeField]
private SequenceStateGlobalVariable _sequenceStateGlobal_Client;
[SerializeField]
private StringGlobalVariable _hmdIdGlobal_Client;
[SerializeField]
private FloatGlobalVariable _batteryPercentGlobal_Client;
private TimelineSceneSequencer _timelineSceneSequencer_Client;
#endregion
#region Server-Only Declarations
//public Action<NetworkedHmdState> OnHmdStateChanged_Server;
public Action<ulong, string> OnHmdIdChanged_Server;
public Action<ulong, float> OnBatteryPercentChanged_Server;
public Action<ulong, TimelineSceneSequencer.SequenceState> OnSequenceStateChanged_Server;
#endregion
public override void OnNetworkSpawn()
{
if (IsOwner) //client-owner logic
{
_timelineSceneSequencer_Client = FindAnyObjectByType<TimelineSceneSequencer>();
_sequenceStateGlobal_Client.OnHasChanged += SetNetSequenceState_ClientSide;
_hmdIdGlobal_Client.OnHasChanged += SetNetHmdId_ClientSide;
_batteryPercentGlobal_Client.OnHasChanged += SetNetBatteryPercent_ClientSide;
SetNetSequenceState_ClientSide();
SetNetHmdId_ClientSide();
SetNetBatteryPercent_ClientSide();
}
else //server logic
{
headsetId_net.OnValueChanged += RespondToNetHmdIdChanged_ServerSide;
batteryPercent_net.OnValueChanged += RespondToNetBatteryPercentChanged_ServerSide;
sequenceStatus_net.OnValueChanged += RespondToNetSequenceStatusChanged_ServerSide;
}
base.OnNetworkSpawn();
}
public override void OnNetworkDespawn()
{
if (IsOwner) //client-owner logic
{
_timelineSceneSequencer_Client = null;
_sequenceStateGlobal_Client.OnHasChanged -= SetNetSequenceState_ClientSide;
_hmdIdGlobal_Client.OnHasChanged -= SetNetHmdId_ClientSide;
_batteryPercentGlobal_Client.OnHasChanged -= SetNetBatteryPercent_ClientSide;
}
else //server logic
{
headsetId_net.OnValueChanged -= RespondToNetHmdIdChanged_ServerSide;
batteryPercent_net.OnValueChanged -= RespondToNetBatteryPercentChanged_ServerSide;
sequenceStatus_net.OnValueChanged -= RespondToNetSequenceStatusChanged_ServerSide;
}
base.OnNetworkDespawn();
}
[Rpc(SendTo.Owner)]
public void PlaySequenceRpc()
{
Debug.Log("client hmd is starting the sequence.");
if (_sequenceStateGlobal_Client.GlobalValue != TimelineSceneSequencer.SequenceState.Playing)
_timelineSceneSequencer_Client.PlayNextTimeline();
else
Debug.LogWarning("Server is trying to start the experience when it is already in progress.");
}
private void SetNetBatteryPercent_ClientSide()
{
batteryPercent_net.Value = _batteryPercentGlobal_Client.GlobalValue;
}
private void SetNetHmdId_ClientSide()
{
headsetId_net.Value = _hmdIdGlobal_Client.GlobalValue;
}
private void SetNetSequenceState_ClientSide()
{
sequenceStatus_net.Value = _sequenceStateGlobal_Client.GlobalValue;
}
private void RespondToNetSequenceStatusChanged_ServerSide(TimelineSceneSequencer.SequenceState previousValue, TimelineSceneSequencer.SequenceState newValue)
{
OnSequenceStateChanged_Server?.Invoke(OwnerClientId, newValue);
}
private void RespondToNetBatteryPercentChanged_ServerSide(float previousValue, float newValue)
{
OnBatteryPercentChanged_Server?.Invoke(OwnerClientId, newValue);
}
private void RespondToNetHmdIdChanged_ServerSide(FixedString32Bytes previousValue, FixedString32Bytes newValue)
{
OnHmdIdChanged_Server?.Invoke(OwnerClientId, newValue.ToString());
}
}

Back to Code Samples List

Scene Management
“Mystic VR” is virtual excursion through a whimsical library powered by Vive Focus 3 hardware. Click here to learn more.

Serving as the introduction to the Keeper of Keys, this five-minute “dark ride” experience was one of many interactive activities featured in Spring 2025’s Mirth & Mischief, an immersive theater experience staged at the Uhuberg Castle in Helen, Georgia. A recording of the attraction can be viewed below. Note that this was recorded in-editor; the final version ran at a consistent 90 frames per second and without any texture loading artifacts (such as the flashes of cyan seen in this recording).

To make team iteration easier, each of eight story sequences were staged in their own dedicated scene file. When the app was in “client mode”, this class loaded each scene and signaled the operator app when it was ready to go. Storing the scene sequence in a serialized list meant team members could easily re-order them in the editor for quicker testing.

Normal operation required client instances to connect to an operator app over a local network and wait for a cue to play. Debug logic was added to test scene loading functionality within the editor without requiring a network connection. Note that this logic considers whether the scene is loaded in the editor already, preventing duplicate scenes from being loaded (see line 166, on).

I believe in “failing loudly” and make frequent use of exceptions to draw attention to bugs (see line 225). Most of the issues I’ve witnessed in team codebases relate to code that attempts to preempt show-stopping errors (like prophalactic null checks on method arguments, for example). This becomes all the more common as a code base grows in size and complexity. While a sensible intuition, this ultimately serves to obscure the misuse (and likely misunderstanding) of public-facing functions.

TimelineSceneSequencer
C#
using UnityEngine;
using Eflatun.SceneReference;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using UnityEngine.InputSystem;
using System;
public class TimelineSceneSequencer : MonoBehaviour
{
public enum SequenceState { Loading, Ready, Playing, Stopped, Complete, NOT_AVAILABLE}
[SerializeField]
private SequenceStateGlobalVariable _localSequenceState;
[SerializeField]
private List<SceneReference> _scenesInOrder;
[SerializeField]
private bool _debug_start_delayed = false;
[SerializeField]
private float _debug_delay_seconds = 3.0f;
private float _delayStartTimer = 0.0f;
#if UNITY_EDITOR
[SerializeField]
private InputAction _debugStartInput;
#endif
private MysticLibraryModeLoader _modeLoader;
private int _sceneLoadedCount = 0;
private int _sceneUnLoadedCount = 0;
private int _expectedSceneCount = -1;
private bool _allTimelinesLoaded = false;
private List<RemoteTimeline> _remoteTimelinesInOrder;
private int _nextTimelineIndex = 0;
private RemoteTimeline _currentTimeline;
public Action OnRemoteTimelinesReady;
private void Awake()
{
#if UNITY_EDITOR
_debugStartInput.Enable();
#endif
_modeLoader = FindFirstObjectByType<MysticLibraryModeLoader>();
_expectedSceneCount = _scenesInOrder.Count;
_remoteTimelinesInOrder = new List<RemoteTimeline>(_expectedSceneCount);
}
private void Start()
{
bool isOperationsMode = _modeLoader.ActiveMode == MysticLibraryModeLoader.MysticLibraryMode.Operations;
if (isOperationsMode)
return; //in operations mode, we only need the UI in that scene and networking to communicate with headsets, not the Mystic VR experience itself
else if (_modeLoader.ActiveModeIsLoaded)
LoadTimelineScenesAsync();
else
_modeLoader.OnActiveModeSceneLoaded += LoadTimelineScenesAsync;
}
private void Update()
{
//DEBUG
if (_debug_start_delayed == true)
{
_delayStartTimer += Time.deltaTime;
if (_delayStartTimer >= _debug_delay_seconds && _localSequenceState.GlobalValue == SequenceState.Ready)
{
_debug_start_delayed = false;
PlayNextTimeline();
}
}
#if UNITY_EDITOR
if (_debugStartInput.IsPressed() && _allTimelinesLoaded && _localSequenceState.GlobalValue == SequenceState.Ready)
{
Debug.Log("Debug start: beginning sequence.");
PlayNextTimeline();
}
#endif
//if all scenes have been loaded, get all remote timelines & let listeners know when they are ready
if (_allTimelinesLoaded == false && _sceneLoadedCount == _expectedSceneCount)
{
_allTimelinesLoaded = true;
foreach (var sceneRef in _scenesInOrder)
{
RemoteTimeline remoteTimeline = GetRemoteTimeline(sceneRef.LoadedScene);
remoteTimeline.Prewarm();
_remoteTimelinesInOrder.Add(remoteTimeline);
}
_localSequenceState.GlobalValue = SequenceState.Ready;
OnRemoteTimelinesReady?.Invoke();
}
}
public void PlayNextTimeline()
{
if (_currentTimeline != null)
{
_currentTimeline.OnEnableNextTimeline -= EnableNextTimeline;
_currentTimeline.OnRemoteTimelineComplete -= CurrentTimelineCompleted;
}
if (_nextTimelineIndex < _remoteTimelinesInOrder.Count)
{
if (_localSequenceState.GlobalValue != SequenceState.Playing)
_localSequenceState.GlobalValue = SequenceState.Playing;
_currentTimeline = _remoteTimelinesInOrder[_nextTimelineIndex];
_nextTimelineIndex++;
_currentTimeline.OnEnableNextTimeline += EnableNextTimeline;
_currentTimeline.OnRemoteTimelineComplete += CurrentTimelineCompleted;
_currentTimeline.PlayTimeline();
Debug.Log("playing: " + _currentTimeline.gameObject.name);
}
else
{
Debug.Log("experience complete");
_localSequenceState.GlobalValue = SequenceState.Complete;
_nextTimelineIndex = 0;
UnloadAllTimelineScenesAsync();
}
void EnableNextTimeline()
{
if (_nextTimelineIndex < _remoteTimelinesInOrder.Count)
{
_remoteTimelinesInOrder[_nextTimelineIndex].EnableTimeline();
}
}
void CurrentTimelineCompleted()
{
PlayNextTimeline();
}
}
private void LoadTimelineScenesAsync()
{
//If this was called from the mode loader event invocation, unsubscribe (safe to do this if it was not called from the event)
_modeLoader.OnActiveModeSceneLoaded -= LoadTimelineScenesAsync;
foreach (SceneReference sceneRef in _scenesInOrder)
{
#if UNITY_EDITOR
if (sceneRef.LoadedScene.isLoaded)
{
Debug.Log(sceneRef.Name + " was detected in editor. Skipping loading phase.");
_sceneLoadedCount++;
continue;
}
#endif
var asyncOperationHandle = SceneManager.LoadSceneAsync(sceneRef.BuildIndex, LoadSceneMode.Additive);
asyncOperationHandle.completed += AsyncLoadComplete;
}
void AsyncLoadComplete(AsyncOperation operationHandle)
{
operationHandle.completed -= AsyncLoadComplete;
_sceneLoadedCount++;
}
}
private void UnloadAllTimelineScenesAsync()
{
_remoteTimelinesInOrder.Clear();
_allTimelinesLoaded = false;
_sceneUnLoadedCount = 0;
_sceneLoadedCount = 0;
foreach (SceneReference sceneRef in _scenesInOrder)
{
var asyncOperationHandle = SceneManager.UnloadSceneAsync(sceneRef.BuildIndex);
asyncOperationHandle.completed += AsyncUnLoadComplete;
}
void AsyncUnLoadComplete(AsyncOperation operationHandle)
{
operationHandle.completed -= AsyncUnLoadComplete;
_sceneUnLoadedCount++;
if (_sceneUnLoadedCount == _expectedSceneCount)
{
Debug.Log("unload complete. loading fresh scenes.");
LoadTimelineScenesAsync();
}
}
}
private RemoteTimeline GetRemoteTimeline(Scene loadedScene)
{
RemoteTimeline remoteTimelineInstance = null;
GameObject[] rootGameObjects = loadedScene.GetRootGameObjects();
for (int i = 0; i < rootGameObjects.Length; i++)
{
GameObject rootGameObj = rootGameObjects[i];
remoteTimelineInstance = rootGameObj.GetComponent<RemoteTimeline>();
if (remoteTimelineInstance != null)
return remoteTimelineInstance;
}
throw new System.Exception("unable to locate a remote timeline on a root gameobject in scene: " + loadedScene.name);
}
}

Back to Code Samples List

Third-Party Asset/Timeline Integration
“Mystic VR” is virtual excursion through a whimsical library powered by Vive Focus 3 hardware. Click here to learn more.

Serving as the introduction to the Keeper of Keys, this five-minute “dark ride” experience was one of many interactive activities featured in Spring 2025’s Mirth & Mischief, an immersive theater experience staged at the Uhuberg Castle in Helen, Georgia. A recording of the attraction can be viewed below. Note that this was recorded in-editor; the final version ran at a consistent 90 frames per second and without any texture loading artifacts (such as the flashes of cyan seen in this recording).

Powered by Unity’s Timeline functionality, Mystic VR showcased eight story sequences, each featuring a complex choreography of visual effects, sound, voice-over, and music. This component received animation data from a custom Timeline Track to modify the appearance of a third-party ambient fog plugin. I designed this integration component to work in edit mode as well, so a sequence’s timeline could be preview-scrubbed in the editor, saving valuable design and iteration time that would otherwise be lost to entering and exiting Playmode.

DynamicFogDriver
C#
using DynamicFogAndMist2;
using UnityEngine;
[ExecuteInEditMode]
public class DynamicFogDriver : MonoBehaviour
{
[SerializeField]
private SequenceStateGlobalVariable _sequenceState;
[SerializeField]
private DynamicFogDriverProfile _driverProfile;
[SerializeField]
private DynamicFog _dynamicFog;
private void OnEnable()
{
#if UNITY_EDITOR
if (!Application.isPlaying && _sequenceState != null)
#endif
_sequenceState.OnHasChanged += RespondToSequenceState;
}
private void OnDisable()
{
#if UNITY_EDITOR
if (!Application.isPlaying &&_sequenceState != null)
#endif
_sequenceState.OnHasChanged -= RespondToSequenceState;
}
private void Start()
{
#if UNITY_EDITOR
if(_driverProfile != null)
#endif
ResetDynamicFog();
}
private void Update()
{
#if UNITY_EDITOR
if (!Application.isPlaying && _driverProfile != null)
{
UpdateDynamicFog();
return;//Be sure to early return out of update while in Editor mode, otherwise the runtime logic below will run as well.
}
#endif
if (_sequenceState.GlobalValue == TimelineSceneSequencer.SequenceState.Playing)
UpdateDynamicFog();
}
private void ResetDynamicFog()
{
_driverProfile.Reset();
UpdateDynamicFog();
}
private void UpdateDynamicFog()
{
_dynamicFog.gameObject.SetActive(_driverProfile.fogEnabled);
if (_driverProfile.fogEnabled == false)
return;
//Appearance
_dynamicFog.profile.densityLinear = _driverProfile.densityLinear;
_dynamicFog.profile.densityExponential = _driverProfile.densityExponential;
_dynamicFog.profile.tintColor = _driverProfile.tint;
var colorKeys = _dynamicFog.profile.verticalGradient.colorKeys;
colorKeys[0].color = _driverProfile.verticalGradient0;
colorKeys[1].color = _driverProfile.verticalGradient1;
colorKeys[2].color = _driverProfile.verticalGradient2;
colorKeys[3].color = _driverProfile.verticalGradient3;
_dynamicFog.profile.verticalGradient.colorKeys = colorKeys;
//Lighting
_dynamicFog.profile.lightDiffusionIntensity = _driverProfile.lightDiffusionIntensity;
_dynamicFog.profile.brightness = _driverProfile.dirLightBrightness;
_dynamicFog.UpdateMaterialProperties();
}
private void RespondToSequenceState()
{
switch (_sequenceState.GlobalValue)
{
case TimelineSceneSequencer.SequenceState.Complete:
ResetDynamicFog();
break;
}
}
}

Back to Code Samples List