Fibers are exceptionally useful for implementing game code with brevity and simplicity. I won’t recount too much of a definition of fibers and why they are useful, since this has already been covered in Ben’s informative post.
So having read the previous post, and deciding fibers are superlicious and you want to use them right away, let’s look at the structure of a very simple drop-in C# fibers library.
This will be using C# iterators, which are covered here:
http://msdn.microsoft.com/en-us/library/65zzykke(v=vs.80).aspx
The system fundamentally operates using the C# yield keyword to implement our fiber’s return-to-execution-point behaviour. This allows us to implement the system in-language without any stack manipulation trickery or machine-specific code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class CFiber { public delegate IEnumerable DExecutable(); public DExecutable Executable; public IEnumerable Enumerable; public IEnumerator Enumerator; public int Sleeping; public CFiber(DExecutable executable) { Executable = executable; } } |
CFiber is simply a data class which maintains the execution state of the fiber. We have our delegate instance which we want to execute, and the IEnumerable and IEnumerator to execute the fiber by using iteration.
1 2 3 4 5 6 7 8 9 10 | public class CFiberCollection { public List ActiveFibers; public List SleepingFibers; public CFiber CreateFiber(Fiber.DExecutable executable); public void KillFiber(Fiber.DExecutable executable); public Update(); } |
We can keep our fibers in a collection like this, providing functionality for creating new fibers and killing any currently executing fibers. Each frame we call Update() to process all fibers.
Our Update() first checks all sleeping fibers, and moves them into the active list if necessary.
1 2 3 4 5 6 7 8 9 | for (int i = 0; i < SleepingFibers.Count; ++i) { CFiber fiber = SleepingFibers[i]; if (--fiber.Sleeping <= 0) { SleepingFibers.RemoveAt(i--); ActiveFibers.Add(fiber); } } |
Now we can simply process our active fibers list. Our core logic on each active fiber will be something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | for (int i = 0; i < ActiveFibers.Count; ++i) { CFiber fiber = ActiveFibers[i]; if (fiber.Enumerable == null) fiber.Enumerable = fiber.Executable(); if (fiber.Enumerator == null) fiber.Enumerator = fiber.Enumerable.GetEnumerator(); // MoveNext will return false if we yield return false, or exit the function if (!fiber.Enumerator.MoveNext()) { fiber.Executable = null; continue; } // check for yield return N sleep request if (fiber.Enumerator.Current.GetType() == typeof(int)) { ActiveFibers.RemoveAt(i--); fiber.Sleeping = (int)fiber.Enumerator.Current; SleepingFibers.Add(fiber); } } |
While the Fiber class requires a certain minimal set of data to utilize the yield iterators, the FiberCollection is fairly flexible in terms of implementation. For simplicity we are keeping simple lists to demonstrate the core concepts, which is limited in various ways but is a useful starting point.
1 | if (fiber.Enumerator.Current.GetType() == typeof(int)) |
This above is a key line in the update logic. We respond to yield return’ed values of type int, and treat them as a request to sleep for N frames. This is where you can readily extend the system to respond as you require, by returning objects/structs to indicate different requests, for example:
1 2 | yield return new SleepSeconds(seconds); yield return new BlockUntil(condition); |
Essentially, this is the entry point for fibers to communicate and issue requests to the fiber management system.
This is the sort of fiber code you would see using this system:
1 2 3 4 5 6 7 8 | public IEnumerator MyMovement() { while (true) { MoveForward(); // movement yield return 0; // wait 1 frame } } |
It pays to be aware of how some more unusual cases of your system will operate. User code will inevitably try and add fibers during fiber execution, terminate fibers at arbitrary times, and with some forethought we can make this stable and safe. For example, in this system new fibers are added to the end of the active list and so will execute in the current frame; self-terminating fibers are removed from the collection but safely execute to their next yield return.
Places to look at extending the system to provide frequently useful functionality would be adding an ID per fiber generated upon creation, since this implementation only works with one fiber per delegate. You may want to look at some more detailed block/signal functionality for handling inter-entity interactions, as well as some events triggered upon thread termination (cleanup code, etc).
Having a priority system for your fibers can help keep things usefully-ordered when generating fibers frequently mid-frame. Priority groups or fiber groups are also exceptionally useful for handling game situations such as pause screens (simply stop updating all ‘game’ fiber groups).
If you are using Unity for any development, you may already know, but there is full support for fibers (coroutines), link here.
Enjoy!