-
Notifications
You must be signed in to change notification settings - Fork 125
Coroutines Run operations over multiple ticks
One recurring problem with the programmable block is the fact that it runs in a time slot in the game's main update loop. This means that performance is paramount and you really can't do more complex operations before hitting the "too complex" exception. Even if you're close to that limit you're doing too much work, you should always strive to run your script as fast as you possibly can. A solution to that is to run complex operations over multiple ticks. Ordinarily this involves writing a state machine with a lot of state managing, switches and jumps, and quickly becomes difficult to maintain - and buggy. Fortunately for us, C# provides a solution for us.
The yielding enumerator enables some really powerful coroutine programming, by utilizing the state machine generated by the compiler. Before you continue, you should read about the keyword so you understand what it's true purpose is. We're gonna exploit it in a somewhat unintended way!
To begin; place a programmable block, an interior light (simply named Interior Light) and an LCD Panel set up to display its public text.
One of the more common question I've been getting since posting this little snippet is: When should I yield? How much can I do before waiting for the next tick?
Unfortunately this is pretty much an unanswerable question, like asking "how long is a string?". This ties into the quite logical fact that the fastest running code is the code that never runs. So all the advice I can give is this: If you can wait with an operation, then wait with that operation.
Copy the following script into the programmable block. The comments explain what is happening. Note that for the sake of simplicity, this example can only run one state machine at any one given time. It's up to you to extend this to support executing multiple state machines if you need to, once you understand how this works.
Remember to dispose your IEnumerator
after use or it will come back to haunt you!
IMyInteriorLight _panelLight;
IMyTextPanel _textPanel;
IEnumerator<bool> _stateMachine;
public Program()
{
// Retrieve the blocks we're going to use.
_panelLight = GridTerminalSystem.GetBlockWithName("Interior Light") as IMyInteriorLight;
_textPanel = GridTerminalSystem.GetBlockWithName("LCD Panel") as IMyTextPanel;
// Initialize our state machine
_stateMachine = RunStuffOverTime();
// Signal the programmable block to run again in the next tick. Be careful on how much you
// do within a single tick, you can easily bog down your game. The more ticks you do your
// operation over, the better.
//
// What is actually happening here is that we are _adding_ the Once flag to the frequencies.
// By doing this we can have multiple frequencies going at any time.
Runtime.UpdateFrequency |= UpdateFrequency.Once;
}
public void Main(string argument, UpdateType updateType)
{
// Usually I verify that the argument is empty or a predefined value before running the state
// machine. This way we can use arguments to control the script without disturbing the
// state machine and its timing. For the purpose of this example however, I will omit this.
// We only want to run the state machine(s) when the update type includes the
// "Once" flag, to avoid running it more often than it should. It shouldn't run
// on any other trigger. This way we can combine state machine running with
// other kinds of execution, like tool bar commands, sensors or what have you.
if ((updateType & UpdateType.Once) == UpdateType.Once)
{
RunStateMachine();
}
}
// ***MARKER: Coroutine Execution
public void RunStateMachine()
{
// If there is an active state machine, run its next instruction set.
if (_stateMachine != null)
{
// The MoveNext method is the most important part of this system. When you call
// MoveNext, your method is invoked until it hits a `yield return` statement.
// Once that happens, your method is halted and flow control returns _here_.
// At this point, MoveNext will return `true` since there's more code in your
// method to execute. Once your method reaches its end and there are no more
// yields, MoveNext will return false to signal that the method has completed.
// The actual return value of your yields are unimportant to the actual state
// machine.
bool hasMoreSteps = _stateMachine.MoveNext();
// If there are no more instructions, we stop and release the state machine.
if (hasMoreSteps)
{
// The state machine still has more work to do, so signal another run again,
// just like at the beginning.
Runtime.UpdateFrequency |= UpdateFrequency.Once;
}
else
{
_stateMachine.Dispose();
// In our case we just want to run this once, so we set the state machine
// variable to null. But if we wanted to continously run the same method, we
// could as well do
// _stateMachine = RunStuffOverTime();
// instead.
_stateMachine = null;
}
}
}
// ***MARKER: Coroutine Example
// The return value (bool in this case) is not important for this example. It is not
// actually in use.
public IEnumerator<bool> RunStuffOverTime()
{
// For the very first instruction set, we will just switch on the light.
_panelLight.Enabled = true;
// Then we will tell the script to stop execution here and let the game do it's
// thing. The time until the code continues on the next line after this yield return
// depends on your State Machine Execution and the timer setup.
// The `true` portion is there simply because an enumerator needs to return a value
// per item, in our case the value simply has no meaning at all. You _could_ utilize
// it for a more advanced scheduler if you want, but that is beyond the scope of this
// tutorial.
yield return true;
int i = 0;
// The following would seemingly be an illegal operation, because the script would
// keep running until the instruction count overflows. However, using yield return,
// you can get around this limitation - without breaking the rules and while remaining
// performance friendly.
while (true)
{
_textPanel.WriteText(i.ToString());
i++;
// Like before, when this statement is executed, control is returned to the game.
// This way you can have a continuously polling script with complete state
// management, with very little effort.
yield return true;
}
}
https://gist.github.com/THDigi/a3eb524e0bd971bd3fddca55cc5e7515
Can be pasted as-is into a PB to see how it behaves, then play around with it!
Feel free to copy SimpleTimerSM to your own scripts aswell.
Do you have questions, comments, suggestions for improvements? Is there something I can do better? Did I make a mistake? Please add an issue here, and prefix your issue title with Wiki. Thank you, your help will be very appreciated!