Skip to content

Instantly share code, notes, and snippets.

@rodamn
Last active April 26, 2017 04:34
Show Gist options
  • Save rodamn/23ed802dbe03747beb7c6aefb48afa41 to your computer and use it in GitHub Desktop.
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
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.
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);
}
}
}
}
}
}
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