1. This forum is obsolete and read-only. Feel free to contact us at support.keenswh.com

Tutorial: Easy and powerful state machine using "yield return"

Discussion in 'Programming Guides and Tools' started by Malware, Jun 30, 2016.

Thread Status:
This last post in this thread was made more than 31 days old.
  1. Malware

    Malware Master Engineer

    Messages:
    9,867
    Available in the development branch from 1.142

    [EDIT]: Important addition made. Remember to dispose your iterators after use.

    If you have any questions, the easiest way to get a hold of me or someone else with knowledge is on Keen's Discord.

    Since the replacement of the script compiler, the yielding enumerator structure is now available for scripts. This enables some really powerful state machine programming.

    To begin; place a programmable block, a timer block (simply named Timer Block) set up only to run the programmable block with no argument (don't use "with default argument"), an interior light (simply named Interior Light) and an LCD Panel set up to display its public text.

    Copy the following script into the programmable block. The comments explain what is happening.

    Code:
    IMyTimerBlock _timer;
    IMyInteriorLight _panelLight;
    IMyTextPanel _textPanel;
    IEnumerator<bool> _stateMachine;
    
    public Program() {
    
        // Retrieve the blocks we're going to use.
        _timer = GridTerminalSystem.GetBlockWithName("Timer Block") as IMyTimerBlock;
        _panelLight = GridTerminalSystem.GetBlockWithName("Interior Light") as IMyInteriorLight;
        _textPanel = GridTerminalSystem.GetBlockWithName("LCD Panel") as IMyTextPanel;
    
        // Initialize our state machine
        _stateMachine = RunStuffOverTime().GetEnumerator();
    
        // Start the timer to run the first instruction set. Depending on your script, you may want to use
        // TriggerNow rather than Start. Just be very careful with that, you can easily bog down your
        // game that way.
        _timer.ApplyAction("Start");
    }
    
    
    public void Main(string argument) {
    
        // 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.
    
        // ***MARKER: State Machine Execution
        // If there is an active state machine, run its next instruction set.
        if (_stateMachine != null) {
            // If there are no more instructions, or the current value of the enumerator is false,
            // we stop and release the state machine.
            if (!_stateMachine.MoveNext() || !_stateMachine.Current) {
      _stateMachine.Dispose();
                _stateMachine = null;
            } else {
                // The state machine has more work to do. Restart the timer. Again you might choose
                // to use TriggerNow.
                _timer.ApplyAction("Start");
            }
        }
    }
    
    
    // ***MARKER: State Machine Program
    // NOTE: If you don't need a precreated, reusable state machine,
    // replace the declaration with the following:
    // public IEnumerator<bool> RunStuffOverTime() {
    public IEnumerable<bool> RunStuffOverTime() {
        // For the very first instruction set, we will just switch on the light.
        _panelLight.RequestEnable(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.
        // I have chosen the simplest form of state machine here. A return value of
        // true will tell the script to execute the next step the next time the timer is
        // invoked. A return value of false will tell the script to halt execution and not
        // continue. This is actually not really necessary, you could have used the
        // statement
        //      yield break;
        // to do the same. However I wanted to demonstrate how you can use the return
        // value of the enumerable to control execution.
        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.
        while (true) {
            _textPanel.WritePublicText(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;
        }
    }
    
    // ***MARKER: Alternative state machine runner: This variant is a one-shot method.
    // The previous version can be precreated and then run multiple times. This version
    // creates an iterator directly. Select the one matching your use.
    public IEnumerator<bool> RunStuffOverTime() {
        // For the very first instruction set, we will just switch on the light.
        _panelLight.RequestEnable(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.
        // I have chosen the simplest form of state machine here. A return value of
        // true will tell the script to execute the next step the next time the timer is
        // invoked. A return value of false will tell the script to halt execution and not
        // continue. This is actually not really necessary, you could have used the
        // statement
        //      yield break;
        // to do the same. However I wanted to demonstrate how you can use the return
        // value of the enumerable to control execution.
        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.
        while (true) {
            _textPanel.WritePublicText(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;
        }
    }
    

    Addendum: Alternate version if you have no need to store the Enumerable but just want to run a coroutine. Notice the change of IEnumerable to IEnumerator, and no longer needing to call GetEnumerator() on the function result. Otherwise the example is identical.
    Code:
    IMyTimerBlock _timer;
    IMyInteriorLight _panelLight;
    IMyTextPanel _textPanel;
    IEnumerator<bool> _stateMachine;
    
    public Program() {
    
        // Retrieve the blocks we're going to use.
        _timer = GridTerminalSystem.GetBlockWithName("Timer Block") as IMyTimerBlock;
        _panelLight = GridTerminalSystem.GetBlockWithName("Interior Light") as IMyInteriorLight;
        _textPanel = GridTerminalSystem.GetBlockWithName("LCD Panel") as IMyTextPanel;
    
        // Initialize our state machine
        _stateMachine = RunStuffOverTime();
    
        // Start the timer to run the first instruction set. Depending on your script, you may want to use
        // TriggerNow rather than Start. Just be very careful with that, you can easily bog down your
        // game that way.
        _timer.ApplyAction("Start");
    }
    
    
    public void Main(string argument) {
    
        // 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.
    
        // ***MARKER: State Machine Execution
        // If there is an active state machine, run its next instruction set.
        if (_stateMachine != null) {
            // If there are no more instructions, or the current value of the enumerator is false,
            // we stop and release the state machine.
            if (!_stateMachine.MoveNext() || !_stateMachine.Current) {
                _stateMachine.Dispose();
                _stateMachine = null;
            } else {
                // The state machine has more work to do. Restart the timer. Again you might choose
                // to use TriggerNow.
                _timer.ApplyAction("Start");
            }
        }
    }
    
    
    // ***MARKER: State Machine Program
    public IEnumerator<bool> RunStuffOverTime() {
        // For the very first instruction set, we will just switch on the light.
        _panelLight.RequestEnable(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.
        // I have chosen the simplest form of state machine here. A return value of
        // true will tell the script to execute the next step the next time the timer is
        // invoked. A return value of false will tell the script to halt execution and not
        // continue. This is actually not really necessary, you could have used the
        // statement
        //      yield break;
        // to do the same. However I wanted to demonstrate how you can use the return
        // value of the enumerable to control execution.
        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.
        while (true) {
            _textPanel.WritePublicText(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;
        }
    }
    
     
    Last edited: Jul 15, 2017
    • Like Like x 2
  2. JoeTheDestroyer

    JoeTheDestroyer Junior Engineer

    Messages:
    573
    This is probably the most amazing thing I've seen in Space Engineers, ever. Thanks for the hard work!

    One thing I'm wondering, when does the first line in RunStuffOverTime() execute? If I'm understanding what's going on, it seems like it's running during the constructor, yes?

    Edit: Another question, though slightly off-topic. I assume this will work in mods as well?

    Edit 2: Another one, where is "yield" legal. Could you use it in a function called by RunStuffOverTime() ?
    Edit 3: N/M on this last one. I looked it up myself, and it has to be the top level function (the one that returns IEnumerable<>).
     
    Last edited: Jul 1, 2016
  3. Malware

    Malware Master Engineer

    Messages:
    9,867
    This is just C# stuff, so it'll work whereever.

    The first line of the enumerable method is run the first time the MoveNext method is called.

    Yes it must be the top one but nothing stops the enumerator from running another enumerator. .
     
  4. Krougal

    Krougal Senior Engineer

    Messages:
    1,012
    Very cool.

    So wait what, there is a difference between run program with no argument and run program with an empty default argument?
     
  5. Malware

    Malware Master Engineer

    Messages:
    9,867
    Yes... If you use run with default, the program is passed whatever is in the default argument text box in the console. I.e. it will change. Which I never intended but hey, it's their game...
     
  6. Krougal

    Krougal Senior Engineer

    Messages:
    1,012
    Ok, now I'm more confused. :p
    I get that run with default passes the argument in the box, if the box is empty, isn't that the same as run with no arg?
     
  7. Malware

    Malware Master Engineer

    Messages:
    9,867
    Yes. Sorry, seems like I misunderstood you :)

    I use the argument box to run commands in the script in a console like fashion. But when the timer runs the script, it needs to be in a predictable way so the script can distinguish between a timed call and a command call. For simplicity's sake I just use an empty argument as an update call. The point is, I cannot let it change based on the value of the default argument box, because I explicitly use that for other things.
     
  8. Krougal

    Krougal Senior Engineer

    Messages:
    1,012
    Oh, got it. Thanks.
    And thanks for all the work and tutorials.
     
  9. Burillo

    Burillo Junior Engineer

    Messages:
    648
    does it have to be outside program class? can i i.e. create an inner class that's enumerable?
     
  10. Malware

    Malware Master Engineer

    Messages:
    9,867
    No restrictions. If you check my example, it doesn't use a class at all...
     
    Last edited: Sep 10, 2016
  11. Burillo

    Burillo Junior Engineer

    Messages:
    648
    oh, right, for some reason i thought it was a class... thanks! optimizations await :)
     
  12. Inflex

    Inflex Developer Staff

    Messages:
    397
  13. kittle

    kittle Senior Engineer

    Messages:
    1,086
    Just stumbled across this post - neat idea. and a great way to simplify some of my builds.

    any idea if we can nest this "yield return" concept?

    ex:
    private System.Collections.Generic.IEnumerable<bool> doDelay(int c)
    {
    int count = c;
    while (count > 1)
    {
    yield return true;
    count--;
    }
    yield return true;
    }

    public IEnumerable<bool> RunStuffOverTime() {
    doDelay(5);
    // stuff
    doDelay(10);
    // stuff
    ..... and so on
    }
     
  14. Malware

    Malware Master Engineer

    Messages:
    9,867
    @kittle Sure you can.

    Code:
    private System.Collections.Generic.IEnumerable<bool> DoDelay(int c)
    {
    	int count = c;
    	while (count > 1)
    	{
    		yield return true;
    		count--;
    	}
    	yield return true;
    }
    
    public IEnumerable<bool> RunStuffOverTime() 
    {
    	foreach (var step in DoDelay(5))
    		yield return step;
    	// stuff
    	foreach (var step in DoDelay(10))
    		yield return step;
    	// stuff
    	//..... and so on
    }
    
     
  15. kittle

    kittle Senior Engineer

    Messages:
    1,086
    yeah I discovered that method on my own.. but adding an extra loop is exactly what I DONT want to do.

    I kludged my way around it... thanks for trying
     
  16. Malware

    Malware Master Engineer

    Messages:
    9,867
    It's the only way, or you lose the tick splitting ability anyway. It is what gives this method its power, remove that and it's nothing but an added complexity. The entire point is to be able to run a complex method over several game ticks (PB runs) with the least amount of effort.
     
  17. Malware

    Malware Master Engineer

    Messages:
    9,867
    Important addition made. Remember to dispose your iterators after use!
     
    • Like Like x 1
Thread Status:
This last post in this thread was made more than 31 days old.