Last active
April 26, 2017 04:34
-
-
Save rodamn/23ed802dbe03747beb7c6aefb48afa41 to your computer and use it in GitHub Desktop.
Unity 3D Component Emitter - A Lightweight system of C# generics that support building and executing complex hierarchies within Unity's Editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
A common design challenge in building a game is that you, the game developer/ | |
designer, need to have a hierarchy of Things that need to be displayed or to do | |
something. It could be a series of enemies that appear, events that occur over | |
time, cool items pulled from a loot table, or various sets of AI actions, some | |
of which might be semi-random, while others are serial. Anyway, we would like to | |
do cool stuff. | |
Having advanced beyond the naive approach of coding these directly into our | |
game, we would like the flexibility to quickly drag and drop new configurations, | |
adding or removing these Things from our gameplay. We would even like to modify | |
the hierarchy at run-time, allowing us to dynamically on-demand load our | |
hierarchies, or fine tune them. If we can leverage Unity's Scene Editor for | |
housing and creating our hierarchy, we can make that happen! | |
To achieve this, we need to define the kind of Things that go in the | |
hierarchy, as well as a way to ask the heirarchy to spit these things out. We | |
will also want different ways to spit things out: basic leaf elements will | |
simply return a single Thing, while collection elements will return one element | |
from a set of elements. We want our client to be able to handle both single and | |
collection elements uniformly, so a Composite interface is in order. | |
Finally, there may be many different ways Collection elements could behave. | |
For instance, we might want a composite to loop through its child elements (from | |
zero, one, to infinite times), while another might randomly choose to emit any | |
one of its child elements. We should also support any other unforseen selection | |
methods, though requiring C# scripts for those is acceptable. | |
Step 1. Precisely Define What Will Be Stored in Our Hierarchy | |
First, we begin by creating a base class or interface for the type of things we | |
would like to emit from our hierarchy. It could just be data, it could have | |
actions, it could have data and actions. That is the class or interface that you | |
will design and that your hierarchy's client will consume and interact with. | |
For example, let's say we need some monsters for our game. | |
public interface IMonsterFactory { | |
Monster SpawnNewMonster() | |
} | |
Alternatively, | |
public class MonsterFactoryBase : MonoBehavior { | |
public abstract Monster SpawnNewMonster(); | |
} | |
Next, we need all of our concrete classes to be derived from this class. Also, | |
every class needs to derive from MonoBehavior as well so they can be placed on | |
GameObjects within Unity's editor. | |
public GoblemFactory : MonoBehavior, IMonsterFactory { | |
public Monster SpawnNewMonster() { | |
... | |
} | |
} | |
public SkelephantFactory : MonoBehavior, IMonsterFactory { | |
public Monster SpawnNewMonster() { | |
... | |
} | |
} | |
etcetera (I suggest that you write up several more) | |
Not so hard! With that done, let's build our hierarchy classes. | |
Step 2. Constructing Generic Hierarchy Classes | |
If a generic form of a hierarchy class that does what's needed already exists, | |
it's simply a matter of subclassing that generic and passing it a template | |
parameter of the interface or base class defined in step 1. | |
It's ridiculously easy: | |
public MonsterFactoryEmitter : ComponentEmitter<IMonsterFactory> {} | |
Here is another constructed class: | |
public MonsterFactoryChildrenLoopEmitter | |
: ChildrenLoopComponentEmitter<IMonsterFactory> {} | |
Step 3. Wiring it up in Unity. | |
Let's start with the collection element, MonsterFactoryChildrenLoopEmitter (yes, | |
this name is long, that's why someone created autocomplete). In Unity, create a | |
Game Object, and then add MonsterFactoryChildrenLoopEmitter as a component (I'm | |
assuming my reader is reasonably experienced with Unity and its jargon. If not, | |
come back after doing some Unity how to tutorials). | |
ChildLoopEmitters will go through its children and serially emit them one- | |
by-one. When it reaches the last child, it will loop if loop count is set | |
higher than one, or loop infinitely for any negative value. Should be obvious, | |
but a loop count of ONE will do a single run through its children. | |
For now set it to -1 to loop infinitely. | |
Next, with MonsterFactoryChildrenLoopEmitter game object selected, create one | |
game object for each kind of concrete MonsterFactory created in Step 1. These | |
new objects should be children of the Children loop emitter, if not, drag any | |
errant game objects there. | |
Each of these GameObjects will need two components: a MonsterFactoryEmitter | |
as well as their Factory component (the one that implements IMonsterFactory, | |
yeah the ones from step 1). | |
As is good practice, name each game object something relevant to help you | |
see what is what in the scene hierachy. | |
In the end you might have something like this: | |
+ Children Looper > MonsterFactoryChildrenLoopEmitter | |
+ Goblem Factory > MonsterFactoryEmitter, GoblemFactory | |
+ Skelephant Factory Factory > MonsterFactoryEmitter, SkelephantFactory | |
+ Chalupacabra Factory > MonsterFactoryEmitter, ChalupacabraFactory | |
+ Clowl Factory > MonsterFactoryEmitter, ClowlFactory | |
Step 4. Emitting and Consuming Things | |
Back in code, you will need to create a client that iterates through your | |
hierarchy. Unfortunately, the details of where to start iterating, when to | |
move to the next item are going to be dependent on your game and its systems, | |
but fortunately, all you need to make it work is just get an iterator and use | |
it. | |
In our example, the client would need a reference to Children Looper's | |
MonsterFactoryChildrenLoopEmitter component: | |
public SomeClient : MonoBehavior { | |
public GameObject MonsterFactoryEmitter; | |
protected IEnumerator<IMonsterFactory> _emitterEnumerator; | |
void Awake () { | |
_emitterEnumerator = | |
MonsterFactoryEmitter.GetComponent<IEnumerator<IMonsterFactor>> (); | |
} | |
void Update () { | |
if (IsTimeToSpawn) { | |
if (_emitterEnumerator.MoveNext ()) { | |
IMonsterFactory factory = _emitterEnumerator.Current; | |
factory.SpawnNewMonster(); | |
} | |
} | |
} | |
} | |
Assuming this example would be fully implemented, you would expect to see | |
a goblem spawn, followed by a skelephant, chalupacabra, and Clowl, repeating | |
forever in that order. | |
Switching the Children Loop Emitter for a Random Child Emitter would | |
randomly spawn these creatures indefinitely. When new monsters are available, | |
just add them in the hierarchy. Need to create different tables for different | |
levels, create several random child emitters, one for each table, then populate | |
each table with the creature factories needed. Finally you need to swap in the | |
enumerator for the active monster table. | |
Hope that makes sense and that this saves someone some headache and helps make | |
some better games. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
namespace Roctopod.UnityEngine { | |
/// <summary> | |
/// Emits components in a specified order. Can be configured to emit each component | |
/// once serially, or to repeat starting from the start N to infinity times. | |
/// </summary> | |
public class ChildrenLoopComponentEmitter<T> : ComponentEmitter<T> where T: class { | |
[Tooltip ("The number of times to loop through series. Specify any negative value for infinite looping.")] | |
public int loopCount = 1; | |
public override IEnumerator<T> GetEnumerator () { | |
// Repeat loopCount times... | |
for (int loop = 0; loopCount < 0 || loop < loopCount; ++loop) { | |
// Iterate through each child | |
foreach (Transform child in transform) { | |
// Get the child's ComponentEmitter... | |
var childEmitter = child.GetComponent<ComponentEmitter<T>> (); | |
if (childEmitter) { | |
// ...and iterate through its elements... | |
foreach (T item in childEmitter) { | |
Debug.LogFormat ("ChildrenLoopComponentEmitter({0}): Child({1}) returns item({2}).", name, child.name, typeof(T).ToString()); | |
yield return item; | |
} | |
} | |
else { | |
Debug.LogWarningFormat ("ChildrenLoopComponentEmitter({0}): Missing ComponentEmitter on child({1}).", name, child.name); | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
namespace Roctopod.UnityEngine { | |
/// <summary> | |
/// To use this class, define a new class that inherits from this class, specifying the | |
/// type of MonoBehavior component that should be emitted. GetNextComponent () can be | |
/// overridden to provide more complex emitter behaviors. | |
/// </summary> | |
[RequireComponent (typeof (GizmoHierarchyItem))] | |
public class ComponentEmitter<T> : MonoBehaviour, IEnumerable<T> where T: class { | |
public virtual IEnumerator<T> GetEnumerator () { | |
yield return GetComponent<T> (); | |
} | |
IEnumerator IEnumerable.GetEnumerator () { | |
return this.GetEnumerator (); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment