• Writing Tutorials - A Demo (and some source code)


    HarvesteR

    Hi,

    Continuing on the last article, where I explained how to set up a ScenarioModule to run your own logic on a scene, without requiring a part on your vessel, here's another short guide. This one about writing your own tutorials:


    Tutorials are nothing more than specialized ScenarioModules. To make writing tutorials as simple as possible, we created a TutorialScenario base class, which handles the basic systems for the tutorial, like managing the instructor dialog and the page flow, so you can focus on the content itself.


    So to get started, here is a simple tutorial implementation. I'll explain what each bit does as we go:


    using System.Collections;
    using UnityEngine;

    // Tutorials extend TutorialScenario (which in turn extends ScenarioModule).
    public class TutorialDemo : TutorialScenario
    {

    TutorialPage defaultPage1, defaultPage2, defaultPage3, specialPage1;
    KFSMEvent onSomethingUnplanned, onTutorialRestart;
    KFSMTimedEvent onStayTooLongOnPage1;

    protected override void OnAssetSetup()
    {
    instructorPrefabName = "Instructor_Gene";
    }

    protected override void OnTutorialSetup()
    {
    // start up a default tutorial demo

    defaultPage1 = new TutorialPage("default page 1");
    defaultPage1.windowTitle = "Tutorial Window";
    defaultPage1.OnEnter = (KFSMState st) =>
    {
    instructor.StopRepeatingEmote();
    };
    defaultPage1.OnDrawContent = () =>
    {
    GUILayout.Label("This is a demo tutorial to test out the tutorial scenario features." +
    " Press Next to go to the next page, or wait " +
    (10 - Tutorial.TimeAtCurrentState).ToString("0") + " seconds." , GUILayout.ExpandHeight(true));

    if (GUILayout.Button("Next")) Tutorial.GoToNextPage();
    };
    Tutorial.AddPage(defaultPage1);


    defaultPage2 = new TutorialPage("default page 2");
    defaultPage2.windowTitle = "Tutorial Window (continued)";
    defaultPage2.OnEnter = (KFSMState st) =>
    {
    instructor.PlayEmoteRepeating(instructor.anim_idle_lookAround, 5f);
    };
    defaultPage2.OnDrawContent = () =>
    {
    GUILayout.Label("This second page is only here to test the state progression system." +
    " Tutorial pages can be stepped forward, and also stepped back.", GUILayout.ExpandHeight(true));

    GUILayout.BeginHorizontal();
    if (GUILayout.Button("Back")) Tutorial.GoToLastPage();
    if (GUILayout.Button("Next")) Tutorial.GoToNextPage();
    GUILayout.EndHorizontal();
    };
    Tutorial.AddPage(defaultPage2);


    defaultPage3 = new TutorialPage("default page 3");
    defaultPage3.windowTitle = "Tutorial Window (last one)";
    defaultPage3.OnEnter = (KFSMState st) =>
    {
    instructor.PlayEmoteRepeating(instructor.anim_true_nodA, 5f);
    };
    defaultPage3.OnDrawContent = () =>
    {
    GUILayout.Label("This third page is also only here to test the state progression system." +
    " It's very much like the previous one, but it has a button to restart the tutorial.", GUILayout.ExpandHeight(true));

    GUILayout.BeginHorizontal();
    if (GUILayout.Button("Back")) Tutorial.GoToLastPage();
    if (GUILayout.Button("Restart")) Tutorial.RunEvent(onTutorialRestart);
    GUILayout.EndHorizontal();
    };
    Tutorial.AddPage(defaultPage3);


    specialPage1 = new TutorialPage("special page 1");
    specialPage1.OnEnter = (KFSMState lastSt) =>
    {
    specialPage1.windowTitle = "Tutorial Window (from " + lastSt.name + ")";
    specialPage1.onAdvanceConditionMet.GoToStateOnEvent = lastSt;

    instructor.PlayEmote(instructor.anim_true_thumbsUp);
    };
    specialPage1.OnDrawContent = () =>
    {
    GUILayout.Label("This Page shows that it's possible to use external events to send the tutorial to" +
    " any arbitrary state, even ones not in the default sequence. Use this to handle cases where the" +
    " player strays off the plan.\n\nNote that this page is added with AddState instead of AddPage," +
    " because we don't want this page to be part of the normal tutorial sequence.", GUILayout.ExpandHeight(true));

    if (GUILayout.Button("Yep"))
    {
    Tutorial.RunEvent(specialPage1.onAdvanceConditionMet);
    }
    };
    specialPage1.OnLeave = (KFSMState st) =>
    {
    instructor.PlayEmote(instructor.anim_idle_sigh);
    };
    Tutorial.AddState(specialPage1);


    onTutorialRestart = new KFSMEvent("Tutorial Restarted");
    onTutorialRestart.updateMode = KFSMUpdateMode.MANUAL_TRIGGER;
    onTutorialRestart.GoToStateOnEvent = defaultPage1;
    Tutorial.AddEvent(onTutorialRestart, defaultPage3);


    onSomethingUnplanned = new KFSMEvent("Something Unplanned");
    onSomethingUnplanned.updateMode = KFSMUpdateMode.MANUAL_TRIGGER;
    onSomethingUnplanned.GoToStateOnEvent = specialPage1;
    Tutorial.AddEventExcluding(onSomethingUnplanned, specialPage1);


    onStayTooLongOnPage1 = new KFSMTimedEvent("Too Long at Page 1", 10.0);
    onStayTooLongOnPage1.GoToStateOnEvent = specialPage1;
    Tutorial.AddEvent(onStayTooLongOnPage1, defaultPage1);

    Tutorial.StartTutorial(defaultPage1);
    }

    // this method would be called by some external component...
    public void SomethingUnplanned()
    {
    if (Tutorial.Started)
    {
    Tutorial.RunEvent(onSomethingUnplanned);
    }
    }

    }




    Yes, quite a bit of code I know, but we'll take in parts.

    The First thing you see on the class there are declarations of TutorialPage objects and some KFSMEvents. That is the basis of the tutorial system. The tutorial flow is managed by a state machine, which is based off the same one that the Kerbal EVA controller uses. It is based around the concept of States and Events. States hold the code that gets run when that state is active, and Events are used to move from one state to another.

    The TutorialScenario has a built-in tutorial FSM called Tutorial, as you can see above. In the OnTutorialSetup method, you create the tutorial pages and the events that will change them, add it all into the Tutorial solver, and start it.

    Before we get more in-depth about that, let's look at that OnAssetsSetup method. That method is called on Start, before anything else can run, to allow you to define your assets to use in the tutorial. The demo above only sets the intructor to be Gene instead of Wernher (who is the default), but as you will see on the other examples later, that method is also used to load or grab references to other assets that might be necessary.


    Back to the tutorial setup then, let's look at a page definition again:


    defaultPage2 = new TutorialPage("default page 2");
    defaultPage2.windowTitle = "Tutorial Window (continued)";
    defaultPage2.OnEnter = (KFSMState st) =>
    {
    instructor.PlayEmoteRepeating(instructor.anim_idle_lookAround, 5f);
    };
    defaultPage2.OnDrawContent = () =>
    {
    GUILayout.Label("This second page is only here to test the state progression system." +
    " Tutorial pages can be stepped forward, and also stepped back.", GUILayout.ExpandHeight(true));

    GUILayout.BeginHorizontal();
    if (GUILayout.Button("Back")) Tutorial.GoToLastPage();
    if (GUILayout.Button("Next")) Tutorial.GoToNextPage();
    GUILayout.EndHorizontal();
    };
    Tutorial.AddPage(defaultPage2);



    TutorialPages are states in the tutorial state machine. They have a number of callback that get called as the tutorial progresses, in which you can add your own code. This demo uses a coding style known as lambda expressions to assign logic to each callback , without having to write methods somewhere else in the code. This is just to keep it all in one place, and you can do it the conventional way if you prefer.

    So, here's what each callback means. Note that you don't really need to assign a method to every one of them. They all default to an empty method, so it's safe to omit the ones you don't need.

    TutorialPage.OnEnter(KFSMState st) gets called once when the page becomes the active one. The 'st' parameter is a reference to the last state before this one.

    TutorialPage.OnUpdate() gets called repeatedly (from Update), to let you run your update logic for the state.

    TutorialPage.OnFixedUpdate() gets called repeatedly (from FixedUpdate), to let you run your fixed update logic for the state.

    TutorialPage.OnLateUpdate() gets called repeatedly (from LateUpdate), to let you run your late update logic for the state.

    TutorialPage.OnDrawContent() gets called repeatedly (from OnGUI) to let you draw your GUI content using Unity's GUI classes.

    TutorialPage.OnLeave(KFSMState st) gets called once when the tutorial is about to move to a new page. The 'st' parameter is a reference to that next state.

    TutorialPage.GoToNextPage() call this to make the tutorial go to the next state. Pages are sequenced by the order in which they get added.

    TutorialPage.GoToPrevPage() same as above, only goes back to the previous page.

    TutorialPage.SetAdvanceCondition(KFSMEventCondition c) Use this to set a condition which will be evaluated repeatedly to advance to the next step when the condition is met. This is just a convenience method to reduce repeated code. It's the same as checking for the same condition on one of the state update events and manually calling GoToNextPage.


    After your page is all set up, you add it to the tutorial sequence with Tutorial.AddPage(yourPage).

    Events can be used along with tutorial pages, to manage situations where the player strays from the plan. Events come in two forms: KFSMEvents and KFSMTimedEvents. KFSMEvents are similar to states (and pages) in some ways. They also have callbacks that get called at specific times. They also have a GoToStateOnEvent value, which holds a reference to the state you want to transition to after the event is triggered. Events are defined independently so when you add them, you can assign them to any combination of states. If you've ever seen an FSM diagram, the analogy becomes simple. States are the nodes, and Events are the arrows that connect each node.


    On the demo above, we have a TimedEvent assigned to run on page 1 of the tutorial. It's set to go off after ten seconds, and take you to a special page. You'll notice that this special page isn't a TutorialPage object, but a KFSMState. That's fine, since TutorialPage is just an extension of KFSMState. Adding a state that isn't a page to the tutorial is perfectly possible. Also notice that the special page is added by calling Tutorial.AddState instead of AddPage. This lets the tutorial know that this page isn't part of the standard sequence, so it doesn't get in the way of the normal flow when calling GoToNextPage, for instance.


    That's about it for how tutorials work. However, the demo above doesn't really show the more hands-on practices of writing a proper tutorial, so I'm also attaching here the source code for the Orbiting 101 tutorial, in full. That should hopefully be a good example of how a tutorial can be written using this system (I recommend turning word wrap on to read the Orbit101 code).

    Happy coding!

    Cheers

    PS: The attachment manager thingy decided to not let me upload a .cs file directly, so I changed the extension to .txt - Just change it back to .cs to open it as a normal code file.

    • Like 1


    User Feedback

    Recommended Comments

    Hi,

    I copied/paste your code and added some print functions to see what happened.

    I can see my print values in log and no errors are reported and a lot of erros (each tick) about GUILayout which must be in onGUI function and ingame, GUILayout doesn't appear in my training mission (i copied and modified /saves/Orbital101.cfg).

    I suppose things changed since you posted this code so, can you give us the code of a new version of a TutorialScenario extended class ?

    Thank you so much.

    Edited by Mandrin32
    errors appear

    Share this comment


    Link to comment
    Share on other sites