Skip to content

Instantly share code, notes, and snippets.

@CodingDino
Last active December 22, 2023 15:21
Show Gist options
  • Save CodingDino/022eff65dd00e5e6e92195780fd03dce to your computer and use it in GitHub Desktop.
Save CodingDino/022eff65dd00e5e6e92195780fd03dce to your computer and use it in GitHub Desktop.
Gameplay programming example: For Armoured Engines, which is similar to a shmup, I developed a "stage" paradigm to program entrances and exits of various components as "cues" for things to happen on the "stage" (current Unity scene). This code sample shows the CueSeries, which contains all the various cues in a level, and logic for loading and p…
// ************************************************************************
// File Name: CueSeries.cs
// Purpose: A list of Cues
// Project: Armoured Engines
// Author: Sarah Herzog
// Copyright: 2017 Bounder Games
// ************************************************************************
// ************************************************************************
#region Imports
// ************************************************************************
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
#endregion
// ************************************************************************
// ************************************************************************
#region Class: CueSeries
// ************************************************************************
[System.Serializable]
[CreateAssetMenu(fileName = "Data", menuName = "Bounder/Cues/CueSeries", order = 1)]
public class CueSeries : ScriptableObject, IncrementalLoader
{
// ********************************************************************
#region Public Data Members
// ********************************************************************
public List<Cue> cues = new List<Cue>();
#endregion
// ********************************************************************
// ********************************************************************
#region Private Data Members
// ********************************************************************
private float m_progress;
private string m_action;
#if UNITY_EDITOR
private List<Cue> m_selectedCues = new List<Cue>();
#endif
#endregion
// ********************************************************************
// ********************************************************************
#region Properties
// ********************************************************************
public bool loaded { get; set; }
public float startTime { get; set; }
public float runTime { get; set; }
public bool paused { get; set; }
public int index { get; set; }
public string currentCue { get; set; }
#if UNITY_EDITOR
public List<Cue> selectedCues { get { return m_selectedCues; } set { m_selectedCues = value; } }
#endif
#endregion
// ********************************************************************
// ********************************************************************
#region IncrementalLoader Methods
// ********************************************************************
public float GetProgress() { return m_progress; }
public string GetCurrentAction() { return m_action; }
// ********************************************************************
#endregion
// ********************************************************************
// ********************************************************************
#region Public Methods
// ********************************************************************
public void Reset(float _startTime = 0)
{
startTime = _startTime;
runTime = startTime;
paused = false;
}
// ********************************************************************
public IEnumerator Load()
{
// Trigger all resources to load
for (int i = 0; i < cues.Count; ++i)
{
Cue cue = cues[i];
cue.Load();
cue.RegisterCondition(true);
m_progress = 0.5f * (((float)i)/((float)cues.Count));
m_action = cue.name;
yield return null;
}
m_progress = 1.0f;
}
// ********************************************************************
public void UnLoad()
{
// Trigger all resources to unload
for (int i = 0; i < cues.Count; ++i)
{
Cue cue = cues[i];
cue.RegisterCondition(false);
}
}
// ********************************************************************
public IEnumerator Play()
{
Debug.Log("CueSeries.Play() - "+name);
List<Cue> sortedCues = cues.Copy();
sortedCues.Sort(
delegate(Cue p1, Cue p2)
{
if (p1.enterTime != p2.enterTime)
return p1.enterTime.CompareTo(p2.enterTime);
else
return cues.IndexOf(p1).CompareTo(cues.IndexOf(p2));
}
);
for (int i = 0; i < sortedCues.Count; ++i)
{
index = i;
Cue cue = sortedCues[i];
if (cue.enterTime < startTime)
continue;
while (paused || cue.enterTime > runTime)
{
if (!paused)
runTime += Time.deltaTime;
yield return null;
}
if (cue.shouldRun)
{
Debug.Log("CueSeries playing cue - "+cue.name);
CueManager.StartCoroutineStatic(cue.Enter());
currentCue = cue.name;
}
if (cue.exitTime != 0 && cue.shouldRun)
CueManager.StartCoroutineStatic(cue.ExitOnTime());
}
float finalTime = GetFinalTime();
while (runTime < finalTime)
{
if (!paused)
runTime += Time.deltaTime;
yield return null;
}
// Unload when finished
CueManager.UnloadSeries(this);
}
// ********************************************************************
public float GetFinalTime()
{
float finalTime = 0;
for (int i = 0; i < cues.Count; ++i)
{
if (cues[i].finalTime > finalTime)
finalTime = cues[i].finalTime;
}
return finalTime;
}
// ********************************************************************
#endregion
// ********************************************************************
// ********************************************************************
#if UNITY_EDITOR
// ********************************************************************
public void AddCue(object _type)
{
AddCue(CreateInstance(_type as Type) as Cue);
}
// ********************************************************************
public void AddCue(Cue _cue)
{
AddCueAt(_cue,cues.Count);
}
// ********************************************************************
public void AddCueAt(Cue _cue, int _pos)
{
// Add cue
cues.Insert(_pos,_cue);
_cue.series = this;
_cue.expanded = true;
_cue.selected = false;
// Set dirty
EditorUtility.SetDirty(this);
// Create the asset and add it to the project
string parentFolder = Path.GetDirectoryName(AssetDatabase.GetAssetPath(this));
string assetPath = parentFolder+"/Cues";
if (!AssetDatabase.IsValidFolder(assetPath))
{
string newFolderID = AssetDatabase.CreateFolder(parentFolder,"Cues");
assetPath = AssetDatabase.GUIDToAssetPath(newFolderID);
}
string assetPathAndName = assetPath + "/CU-"+_cue.GetName()+".asset";
string uniqueAssetPathAndName = AssetDatabase.GenerateUniqueAssetPath(assetPathAndName);
AssetDatabase.CreateAsset(_cue,uniqueAssetPathAndName);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
// ********************************************************************
public void RemoveCue(Cue _cue)
{
cues.Remove(_cue);
m_selectedCues.Remove(_cue);
_cue.series = null;
// Set dirty
EditorUtility.SetDirty(this);
string pathToDelete = AssetDatabase.GetAssetPath(_cue);
AssetDatabase.DeleteAsset(pathToDelete);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
// ********************************************************************
public void SelectCue(Cue _cue, bool _select)
{
if (_select)
{
m_selectedCues.AddWithoutDuplicates(_cue);
m_selectedCues.Sort(
delegate(Cue p1, Cue p2)
{
return cues.IndexOf(p1).CompareTo(cues.IndexOf(p2));
}
);
}
else
m_selectedCues.Remove(_cue);
}
// ********************************************************************
public void MoveCue(Cue _cue, int _move)
{
MoveCueTo(_cue, cues.IndexOf(_cue) + _move);
}
// ********************************************************************
public void MoveCueTo(Cue _cue, int _index)
{
if (_index < 0 || _index >= cues.Count)
return;
cues.Remove(_cue);
cues.Insert(_index,_cue);
// Set dirty
EditorUtility.SetDirty(this);
}
// ********************************************************************
[ContextMenu("Duplicate")]
public void Duplicate()
{
Debug.Log("Duplicate() CueSeries "+name+" started");
// Create the asset and add it to the project
string parentFolder = Path.GetDirectoryName(AssetDatabase.GetAssetPath(this));
string assetPath = parentFolder+"/Duplicant";
if (!AssetDatabase.IsValidFolder(assetPath))
{
string newFolderID = AssetDatabase.CreateFolder(parentFolder,"Duplicant");
assetPath = AssetDatabase.GUIDToAssetPath(newFolderID);
}
else
{
Debug.LogError("Duplicant folder already exists - move or delete it to duplicate again.");
return;
}
EditorUtility.SetDirty(this);
// Create a new CueSeries
CueSeries duplicateSeries = ScriptableObject.CreateInstance<CueSeries>();
duplicateSeries.name = name;
string assetPathAndName = assetPath + "/"+name+".asset";
string uniqueAssetPathAndName = AssetDatabase.GenerateUniqueAssetPath(assetPathAndName);
AssetDatabase.CreateAsset(duplicateSeries,uniqueAssetPathAndName);
// Create the cue folder and add it to the project
assetPath = parentFolder+"/Duplicant"+"/Cues";
if (!AssetDatabase.IsValidFolder(assetPath))
{
string newFolderID = AssetDatabase.CreateFolder(parentFolder+"/Duplicant","Cues");
assetPath = AssetDatabase.GUIDToAssetPath(newFolderID);
}
// Create all the cues and add them to the new cue series
for (int i = 0; i < cues.Count; ++i)
{
Cue duplicateCue = Instantiate(cues[i]);
//duplicateSeries.AddCue(duplicateCue);
// Add cue
duplicateSeries.cues.Add(duplicateCue);
duplicateCue.series = duplicateSeries;
// Set dirty
assetPathAndName = parentFolder+"/Duplicant"+"/Cues" + "/CU-"+duplicateCue.GetName()+".asset";
uniqueAssetPathAndName = AssetDatabase.GenerateUniqueAssetPath(assetPathAndName);
AssetDatabase.CreateAsset(duplicateCue,uniqueAssetPathAndName);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Duplicate() CueSeries "+name+" completed");
}
// ********************************************************************
[CustomEditor(typeof(CueSeries))]
public class Inspector : Editor
{
// ****************************************************************
#region Private Data Members
// ****************************************************************
private PreviewRenderUtility m_previewRenderUtility;
private static List<Cue> s_coppiedCues = new List<Cue>();
private float m_timeShiftAmount = 0;
// ****************************************************************
#endregion
// ****************************************************************
// ****************************************************************
#region Editor Methods
// ****************************************************************
public override void OnInspectorGUI()
{
CueSeries series = (CueSeries)target;
if (!series.cues.Empty())
DrawButtons();
for (int i = 0; i < series.cues.Count; ++i)
{
if (series.cues[i] != null)
{
series.cues[i].DrawUI();
}
}
DrawButtons(series.cues.Empty()); // only show the + sign if the cue series is empty
OnPreviewGUI(GUILayoutUtility.GetRect(500,500),EditorStyles.whiteLabel);
}
// ****************************************************************
private void DrawButtons(bool _emptySeries = false)
{
CueSeries series = (CueSeries)target;
GUIStyle style = new GUIStyle();
style.alignment = TextAnchor.MiddleRight;
Color oldColor = GUI.backgroundColor;
EditorGUILayout.BeginHorizontal(style);
if (!_emptySeries)
{
m_timeShiftAmount = EditorGUILayout.FloatField("", m_timeShiftAmount,GUILayout.Width(50));
GUI.backgroundColor = oldColor;
if (GUILayout.Button("Shift",GUILayout.Width(50)))
{
for (int i = 0; i < series.selectedCues.Count; ++i)
{
series.selectedCues[i].enterTime += m_timeShiftAmount;
if (series.selectedCues[i].exitTime != 0) // exit time of zero means no exit!
series.selectedCues[i].exitTime += m_timeShiftAmount;
EditorUtility.SetDirty(series.selectedCues[i]);
}
}
}
// go to right side
GUILayout.FlexibleSpace();
// Duplicate button
GUI.backgroundColor = oldColor;
if (!_emptySeries && GUILayout.Button("Sort",GUILayout.Width(50)))
{
SortSeries();
}
// Duplicate button
GUI.backgroundColor = Color.green;
if (!_emptySeries && GUILayout.Button("Copy",GUILayout.Width(50)))
{
CopySelectedCues();
}
// Duplicate button
GUI.backgroundColor = Color.green;
if (GUILayout.Button("Paste",GUILayout.Width(50)))
{
PasteSelectedCues();
}
// Duplicate button
GUI.backgroundColor = Color.green;
if (!_emptySeries && GUILayout.Button("Duplicate",GUILayout.Width(80)))
{
series.Duplicate();
}
// Add cue button
GUI.backgroundColor = Color.green;
if (_emptySeries && GUILayout.Button("+",GUILayout.Width(30)))
{
OpenAddCueMenu();
}
// New line
EditorGUILayout.EndHorizontal();
if (!_emptySeries)
{
EditorGUILayout.BeginHorizontal(style);
// Duplicate button
GUI.backgroundColor = Color.green;
if (GUILayout.Button("All",GUILayout.Width(50)))
{
SelectAll();
}
// Duplicate button
GUI.backgroundColor = Color.red;
if (GUILayout.Button("None",GUILayout.Width(50)))
{
SelectNone();
}
// go to right side
GUILayout.FlexibleSpace();
// the same options that are on individual cues
GUI.backgroundColor = Color.green;
if (GUILayout.Button("=",GUILayout.Width(25)))
{
for (int i = 0; i < series.selectedCues.Count; ++i)
{
Cue newCue = CreateInstance(series.selectedCues[i].GetType()) as Cue;
EditorUtility.CopySerialized(series.selectedCues[i],newCue);
series.AddCueAt(newCue,series.cues.IndexOf(series.selectedCues[i])+1);
}
}
GUI.backgroundColor = oldColor;
if (GUILayout.Button("/\\",GUILayout.Width(25)))
{
// Start at the top and work downward
for (int i = 0; i < series.selectedCues.Count; ++i)
{
series.MoveCue(series.selectedCues[i],-1);
}
}
GUI.backgroundColor = oldColor;
if (GUILayout.Button("\\/",GUILayout.Width(25)))
{
// Start at the bottom and work upward
for (int i = series.selectedCues.Count-1; i >= 0; --i)
{
series.MoveCue(series.selectedCues[i],+1);
}
}
GUI.backgroundColor = oldColor;
if (GUILayout.Button("/|\\",GUILayout.Width(25)))
{
// Start at the bottom and work upward
for (int i = series.selectedCues.Count-1; i >= 0; --i)
{
series.MoveCueTo(series.selectedCues[i],0);
}
}
GUI.backgroundColor = oldColor;
if (GUILayout.Button("\\|/",GUILayout.Width(25)))
{
// Start at the top and work downward
for (int i = 0; i < series.selectedCues.Count; ++i)
{
series.MoveCueTo(series.selectedCues[i],series.cues.Count-1);
}
}
GUI.backgroundColor = Color.red;
if (GUILayout.Button("X",GUILayout.Width(25)))
{
// Start at the bottom and work upward, as this will modify the list
for (int i = series.selectedCues.Count-1; i >= 0; --i)
{
series.selectedCues[i].RemoveCue();
}
return; // Return early to avoid argument out of range issues while deleting while iterating.
}
EditorGUILayout.EndHorizontal();
}
GUI.backgroundColor = oldColor;
}
// ****************************************************************
private void SelectAll()
{
CueSeries series = (CueSeries)target;
for (int i = 0; i < series.cues.Count; ++i)
{
series.cues[i].selected = true;
}
}
// ****************************************************************
private void SelectNone()
{
CueSeries series = (CueSeries)target;
// Walk backwards since changing selected will modify the list
for (int i = series.selectedCues.Count-1; i >= 0; --i)
{
series.selectedCues[i].selected = false;
}
}
// ****************************************************************
private void CopySelectedCues()
{
CueSeries series = (CueSeries)target;
s_coppiedCues.Clear();
s_coppiedCues = series.selectedCues.Copy();
}
// ****************************************************************
private void PasteSelectedCues()
{
CueSeries series = (CueSeries)target;
for (int i = 0; i < s_coppiedCues.Count; ++i)
{
Cue duplicateCue = Instantiate(s_coppiedCues[i]);
series.AddCue(duplicateCue);
}
EditorUtility.SetDirty(series);
}
// ****************************************************************
private void SortSeries()
{
CueSeries series = (CueSeries)target;
List<Cue> sortedCues = series.cues.Copy();
sortedCues.Sort(
delegate(Cue p1, Cue p2) {
if (p1 == null || p2 == null)
return 0;
if (p1.enterTime != p2.enterTime)
return p1.enterTime.CompareTo(p2.enterTime);
else
return series.cues.IndexOf(p1).CompareTo(series.cues.IndexOf(p2));
}
);
series.cues = sortedCues;
EditorUtility.SetDirty(series);
}
// ****************************************************************
private void OpenAddCueMenu()
{
CueSeries series = (CueSeries)target;
// Open add cue menu
Vector2 mousePos = Event.current.mousePosition;
GenericMenu addCueMenu = new GenericMenu();
Type[] cueTypes = typeof(Cue).GetSubTypes();
for (int i = 0; i < cueTypes.Length; ++i)
{
bool allow = (bool) cueTypes[i].GetField("allowCreation").GetValue(null);
if (allow)
{
string menuName = (string) cueTypes[i].GetField("menuName").GetValue(null);
addCueMenu.AddItem(new GUIContent(menuName),false,series.AddCue,cueTypes[i]);
}
}
addCueMenu.DropDown(new Rect(mousePos.x,mousePos.y,0,16));
}
// ****************************************************************
public override bool HasPreviewGUI()
{
return true;
}
// ****************************************************************
public override void OnPreviewGUI(Rect _overallRect, GUIStyle _backgroundStyle)
{
if (_backgroundStyle != null && _backgroundStyle.name == "PreBackground")
{
// Timeline
Rect timelineRect = _overallRect;
timelineRect.x += 5;
timelineRect.y += 5;
timelineRect.height = 20;
timelineRect.width -= 10;
EditorGUI.DrawRect(timelineRect,Color.white);
// Copy and sort cues
CueSeries series = (CueSeries)target;
List<Cue> sortedCues = series.cues.Copy();
sortedCues.Sort(
delegate(Cue p1, Cue p2) {
if (p1 == null || p2 == null)
return 0;
if (p1.enterTime != p2.enterTime)
return p1.enterTime.CompareTo(p2.enterTime);
else
return series.cues.IndexOf(p1).CompareTo(series.cues.IndexOf(p2));
}
);
// Calculate timeline slices
float finalTime = series.GetFinalTime();
int timelineSlices = 1;
float timelineSliceValue = 1.0f;
float minTimelineSliceWidth = 40;
float timelineSliceWidth = minTimelineSliceWidth;
int maxTimelineSlices = Mathf.FloorToInt(timelineRect.width / minTimelineSliceWidth);
if (finalTime != 0)
{
timelineSlices = Mathf.CeilToInt(finalTime / timelineSliceValue);
if (timelineSlices > maxTimelineSlices)
{
timelineSlices = maxTimelineSlices;
}
timelineSliceValue = finalTime / (float) timelineSlices;
timelineSliceWidth = timelineRect.width / (float) (timelineSlices);
}
// Print cues
float pixelsPerSecond = timelineSliceWidth / timelineSliceValue;
float minCueWidth = 10;
float minCueSeconds = minCueWidth / pixelsPerSecond;
List<List<Cue> > layers = new List<List<Cue> >();
for (int i = 0; i < sortedCues.Count; ++i)
{
Cue thisCue = sortedCues[i];
if (thisCue == null)
continue;
//find a layer for this cue
int layer = layers.Count;
for (int j = 0; j < layers.Count; ++j)
{
Cue lastCue = layers[j].Back();
float exitTime = lastCue.exitTime == 0 ? lastCue.enterTime : lastCue.exitTime;
if (lastCue.duration < minCueSeconds)
exitTime = lastCue.enterTime + minCueSeconds;
if (exitTime <= thisCue.enterTime)
{
layer = j;
break;
}
}
if (layer == layers.Count)
{
layers.Add(new List<Cue>());
}
layers[layer].Add(sortedCues[i]);
// Draw the cue
float cueWidth = minCueWidth;
if (thisCue.exitTime != 0 && thisCue.exitTime - thisCue.enterTime > minCueSeconds)
cueWidth = pixelsPerSecond * (thisCue.exitTime - thisCue.enterTime);
float cueStart = pixelsPerSecond*thisCue.enterTime;
if (cueStart + cueWidth > timelineRect.width)
cueStart = timelineRect.width - cueWidth;
Rect cueRect = new Rect(timelineRect.x + cueStart,
timelineRect.y + 25+layer*20,
cueWidth,
20);
EditorGUI.DrawRect(cueRect,thisCue.GetCueColor());
EditorGUI.LabelField(cueRect,thisCue.name);
Vector2 mousePos = Event.current.mousePosition;
if (cueRect.Contains(mousePos))
{
GenericMenu addCueMenu = new GenericMenu();
addCueMenu.AddDisabledItem(new GUIContent(thisCue.name));
addCueMenu.DropDown(new Rect(mousePos.x,mousePos.y,0,16));
}
}
// Print timeline slices
for (int i = 0; i <= timelineSlices; ++i)
{
float labelValue = (timelineSliceValue * (float)i);
float xPos = timelineRect.x + timelineSliceWidth * (float)i;
EditorGUI.DrawRect(new Rect(xPos,timelineRect.y,1,25+layers.Count*20),Color.gray);
if (i < timelineSlices)
EditorGUI.LabelField(new Rect(xPos,timelineRect.y,timelineSliceWidth,20),string.Format("{0:F1}",labelValue));
}
}
}
// ****************************************************************
#endregion
// ****************************************************************
}
// ********************************************************************
#endif
// ********************************************************************
}
#endregion
// ************************************************************************
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment