Give Page Feedback | API

Secondary Non Components - Yield Enumerator

When you place yield return statements in a method that has a return type of IEnumerator, normally the compiler auto-generates a state machine class. A new instances of this auto-generated class is created every time the method is called, which you can imagine generates a lot of garbage.

The YieldEnumerator class was created as an alternative to the auto-generated C# classes. To use it, you must create a class that derives from it directly or derives from one of its child abstract classes, of which there are many. This allows you to create an instance of your custom YieldEnumerator class which can be reused again and again, eliminating the garbage generation prevalent in the auto-generated iterator classes.

Do keep in mind that the use of this class is entirely optional. You will need to implement the state machine logic to make the YieldEnumerator function, and as such, this strategy is recommended for advanced users with a high level of coding knowledge. For other users, we recommend simply placing yield return statements in your IEnumerator methods and letting the compiler auto-generate classes.

A Reusable Framework

The YieldEnumerator class has been configured with reusability in mind, however while you can create, manage, and reuse instances of your custom implementation directly, it is recommended to make use of the ReusableEnumerators static class instead, which makes it easy to reuse instances of a specific implementation across the entire project. You can use this class like so:

1) When one of your components/classes wants to make use of a YieldEnumerator implementation, call the AddUser method with the type of your custom implementation (i.e., the name of your custom class that derives from YieldEnumerator or one of its child types). This method is used to track the number of users of the enumerator class, so that when it reaches 0 users the instances that have been created for the class can be released and memory freed. This method is similar to Event Subscriptions, as you only need to use it once (usually in OnEnable is best).

2) When a method with a return type of IEnumerator is called that should make use of your YieldEnumerator class, retrieve an instance of the class using the GetEnumeratorNotInUse method. This method will only return an instance that is not currently being used by another user, or if no such instance exists, it will automatically create a new one.

3) Call the PrepareForIteration method on the YieldEnumerator class with the Type Arguments matching your custom Yield Enumerator (this is explained below). This method binds data to the instance for the current iteration.

3) Return the YieldEnumerator instance. Note that you can perform other logic before returning the instance if you desire; the only important thing is to not include any yield return statements, as this will cause the compiler to auto-generate an enumerator class, making the use of the YieldEnumerator somewhat redundant.

Note that the PrepareForIteration method is self-returning (like how LINQ code works), which allows you call it and to return the YieldEnumerator instance in the same line of code (you can chain these after GetEnumeratorNotInUse as well to reduce the number of lines of code even further!).

Example
return ReusableEnumerators.GetEnumeratorNotInUse().PrepareForIteration(data1, data2)

4) When the class using the YieldEnumerator is disabled or destroyed (or is otherwise affected in a way where you do know if you will need to make use of the YieldEnumerator again), you must notify the ReusableEnumerators static class that the YieldEnumerator has one less user. Do this using the RemoveUser method.

Included Generic YieldEnumerators and Preparing For Iteration

Normal auto-generated enumerator classes automatically capture local members in the user designed method (the one that contains yield return statements) and assign data to the generated instance whenever it is needed. This allows each instance to contain unique data for that iteration only.

In order to mimic this behavior, your custom YieldEnumerator classes can be configured to hold reference or value type data.

In order to reduce redundancy, a number of YieldEnumerator implementations have been provided that take care of much of the implementation legwork for you. These implementations correspond to varying combinations of reference and struct (value type) data (1 reference - 1 struct, 3 references - 0 struct, 2 references - 4 struct, etc.). The names of these classes will match the combination of reference and struct fields that the class should hold; for instance, if the class needs to hold 1 reference field and 2 struct fields, it will be called YieldEnumerator_Ref1_Struct2 and will have generic type parameters matching the data. You can access the reference data via the r1, r2, etc. fields, and the struct data using the s1, s2, etc. fields.

When the implementation only contains struct data or only reference data, the name of the class will have StructOnly or RefOnly at the end.

Most of these implementations do not derive directly from YieldEnumerator, but instead derive from YieldEnumeratorWithParent. This is very similar to YieldEnumerator but contains a field (called Parent) to hold the instance of whatever parent class is using the YieldEnumerator instance, which can be useful for accessing additional data and methods.

Creating A Custom Generic Implementation

The default generic implementations have been added as needed to meet the requirements of internal code, and as such, certain combinations of reference and/or struct data may be missing. If you cannot find a provided implementation to suit your needs, you can create a custom abstract generic implementation that derives from either YieldEnumerator or YieldEnumeratorWithParent.

When doing so you must add a PrepareForIteration method that has arguments for the data you need as well as the Parent instance if deriving from YieldEnumeratorWithParent. After assigning the data to appropriate fields in the custom implementation, you must call the base PrepareForIteration method (use the one that takes a parent argument if deriving from YieldEnumeratorWithParent, which automatically takes care of assigning the parent instance to the Parent field).

If your custom implementation contains reference data, the reference data must be nulled out between iterations. In order to facilitate this, you must override either the NullRefFields method (if deriving from YieldEnumerator) or the NullAdditionalRefFields method (if deriving from YieldEnumeratorWithParent). Within these methods, set all reference fields (except the Parent) to null.

When deriving from YieldEnumeratorWithParent, you must include a Type Parameter in your class definition that matches the Type of the Parent, and must also include a constraint that says the Parent Type is a reference type (where ParentType : class). You then must derive from YieldEnumeratorWithParent.

If your custom generic implementation contains reference data, you will need to add Type Constraints for each reference type defining the types as reference based, to enable nulling out of the data (where T : class).

Creating Non Generic Implementations

Because the generic implementations are all abstract, you cannot use them directly with the ReusableEnumerators static class, as instances of abstract classes cannot be created.

Instead, you will create a non-abstract class that derives from either YieldEnumerator, YieldEnumeratorWithParent, or one of the generic implementations. This class will usually only contain the state machine code, which must be added in an override of the MoveNextImplementation method (more on this below).

Occasionally it is necessary to run some extra logic before a YieldEnumerator begins being iterated. If needed, you can override the MoveNextImplementation method for this purpose. You can even use this method to run a check which affects the Current YieldInstruction that is returned by the YieldEnumerator or the Phase of the enumerator (again, more on this below).

In addition, you can perform extra logic after a YieldEnumerator finishes iterating by overriding the ResetImplementation method, though note that often times this logic can be placed within the state machine code itself.

Writing The State Machine Code

State Machines are advanced coding structures and as such, we do not provide detailed information on how to implement them. With that said, there are some pre-defined properties and systems in place to aid you in writing the state machine code, which we will go over in this sub-section.

As stated above, the YieldEnumerator class is simply an implementation of the IEnumerator interface, however the Dispose and Reset methods from this interface are not implemented and will throw NotImplementedExceptions (so you should not use them with systems that automatically call these methods). The only members we care about are the MoveNext method and Current property.

These members are used like so:

1) MoveNext is called. Within MoveNext, we check to make sure PrepareForIteration has been called.

2) If PrepareForIteration was not called, an exception is thrown. Otherwise MoveNextImplementation is called, which serves as the actual implementation of the state machine logic.

3) Within MoveNextImplementation you run the state machine logic and decide whether the enumerator has finished or needs to yield before continuing. If the former, you return false; if the latter, you ensure Current is set to what you want it to be and return true.

4) The Current value contains the YieldInstruction that the base calling code (usually the World component) uses to either yield directly, or make a decision on whether the enumerator should continue executing (if the YieldInstruction is a YieldOrContinue object). As such, you should always make sure it's set to the value you want it to be before returning true within the MoveNextImplementation method (more below in the next sub-section). By this, we do not mean you need to set it each time you return true; in some cases, the value will already be set to what you want before returning true.

In order to track the state of your enumerator, the YieldEnumerator class includes a Phase property. By default the Phase is set to 1 after the enumerator has completed a full iteration (i.e., when MoveNextImplementation returns false), and this will be the default Phase of the enumerator before MoveNext has been called for a given iteration cycle. However, you can change this behavior by overriding the PerformAdditionalIterationPreparation method and setting the value of Phase to something else within it (usually you would have some kind of if/else statement to determine which Phase to start out in). Otherwise, the Phase property is not changed automatically; you need to increment or adjust the Phase manually in order to tell your state machine logic where in the iteration cycle it is.

To use the Phase value, we recommend using a switch statement over if/else statements, as switch statements allow you to associate code blocks with a numerical value (case 1, 2, etc.), and you can then use goto statements to flow between code blocks. Generally speaking, the Phase value only needs to be set before yielding (i.e., returning true), to allow the MoveNextImplementation method to pick up from the correct spot the next time it is called.

There is also a MoveNextCalls property which tracks the number of times MoveNextImplementation has been called prior to the current call (i.e., it will be 0 the first time it is called, 1 the second time, and so on). The main use of this is if your state machine needs to simply iterate over an array or some other index based collection, as you can use the MoveNextCalls value as the index into the collection (assuming there is only 1 element accessed per MoveNextImplementation call).

Current Potential

The {BoldTagOpen}Current{BoldTagClose} Property is of type YieldInstruction, which opens up several possibilities for how it is used:

1) Null - This tells the calling code to yield for a single frame.

2) Unity YieldInstructions - Any of Unity's pre-defined Yield Instructions (such as WaitForSeconds) can be used.

3) YieldOrContinue - When the calling code uses an Execution Controller (like the World component), this special instruction defers the decision to yield to the Execution Controller. Only use this if your state machine would have no issue immediately picking up where it left off in the same frame. Use YieldOrContinue.Instance to avoid allocating garbage!

4) YieldUntilJobComplete - This can be used to yield until a parallel Job has finished executing, however it can only be called when you know the World is the one driving the enumerators execution, as only the World is configured to interpret the YieldUntilJobComplete correctly.