Search Unity

Adding tutorials without messing up your code

Discussion in 'Scripting' started by Serinx, Jan 13, 2020.

  1. Serinx

    Serinx

    Joined:
    Mar 31, 2014
    Posts:
    788
    I want to add an in-game tutorial to my game which walks players through various essentials.

    I want to verify that they have completed a step before continuing on to the next, but I don't want to add too much complexity to my code.

    Has anyone got any tips for how to do this?

    My first thought was to add events to my existing code which send information to subscribers when tasks are performed. and have the Tutorial class subscribe to those events so that it knows the player has completed the task, but that means I'll have to add an event for every action I want in the tutorial. Is that a weird way of doing it?

    If i'm not making sense. Lets say you have a RTS game tutorial, and you tell the player to select a group of units by dragging a box over them, how do I check that they've selected all the units without adding complexity to my existing Selector class?
     
  2. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Well, how do you know what's selected in the actual game?
     
  3. tonemcbride

    tonemcbride

    Joined:
    Sep 7, 2010
    Posts:
    1,089
    Personally I find it easiest to just have a single 'tutorial/onboarding' class that interrogates your other classes. For example, the tutorial class would have a variety of states like 'waiting for unit placement' or 'waiting for unit selection'. In those states you would ask your selector class for it's state and move the tutorial on if it was in the correct state.

    The advantage of that is that none of your normal systems needs to know anything about the tutorial, you're just adding public accessor functions to check their state.
     
  4. Serinx

    Serinx

    Joined:
    Mar 31, 2014
    Posts:
    788
    In my code, when you select something the selector class populates a list of selected objects which can be accessed by other objects. Then when you give a command it sends it to the selected objects. The difficulty with the tutorial is there is no “command” to check that they’ve done it, unless I just check the list every frame in Update...

    @tonemcbride that approach seems reasonable, do you think checking the state every update would be alright? I guess that wouldn’t be a big deal
     
  5. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,639
    Nothing wrong with that. Just have an external object check the length of the list on Update.
     
  6. Serinx

    Serinx

    Joined:
    Mar 31, 2014
    Posts:
    788
    @kdgalla Yeah I suppose that would work, It's basically like how one of my statemachines works: check if the current state is complete every frame and then change to the next state. I guess I was overthinking it!

    The next challenge will be preventing the player from doing things they're not supposed to in the tutorial!
     
  7. Serinx

    Serinx

    Joined:
    Mar 31, 2014
    Posts:
    788
    If anyone is interested, this is the pattern I went with to implement my Tutorial and it's working well!

    The Tutorial class is a MonoBehaviour that holds a list of TutorialStep objects.
    The TutorialStep class has some text to display, an Action when entering, a Func<bool> which should return true when the step is complete, and an Action when leaving this step.
    The Tutorial class checks the Func<bool> in a coroutine to check if the player has completed the step, before moving on to the next step.
    When changing steps, it will run the current steps exit action, then the next steps entry action and display the text of the next step.

    Here's how a simple implementation of it looks, in this example I've used lambdas for the actions and funcs to cut down on the lines of code, you could separate these into separate methods for more complex logic.

    Code (CSharp):
    1. class TutorialStep
    2. {
    3.     public string Message { get; private set; }
    4.     public Action Setup { get; private set; }
    5.     public Func<bool> Verify { get; private set; }
    6.     public Action Cleanup { get; private set; }
    7.     public bool Skippable { get; private set; }
    8.  
    9.     public TutorialStep(string message, Action setup, Func<bool> verify, Action cleanup, bool skippable = true)
    10.     {
    11.         this.Message = message;
    12.         this.Setup = setup;
    13.         this.Verify = verify;
    14.         this.Cleanup = cleanup;
    15.         this.Skippable = skippable;
    16.     }
    17. }
    18. public class Tutorial : MonoBehaviour
    19. {
    20.     [SerializeField] Selector selector;
    21.     [SerializeField] Text tutorialText;
    22.     [SerializeField] Text skipText;
    23.  
    24.     List<TutorialStep> _tutorialSteps = new List<TutorialStep>();
    25.     int _currentStepIndex;
    26.     TutorialStep _currentStep;
    27.     bool _tutorialComplete;
    28.  
    29.     // Start is called before the first frame update
    30.     void Start()
    31.     {
    32.         SetupTutorial();
    33.         AddTutorialSteps();
    34.         StartCoroutine(Tutorial());
    35.     }
    36.  
    37.     private void AddTutorialSteps()
    38.     {
    39.         //This step completed when the player presses G
    40.         _tutorialSteps.Add(new TutorialStep("Press G to continue", setup: null, verify: () => { return Input.GetKeyDown(KeyCode.G); }, cleanup: null));
    41.  
    42.         //This step is completed when something is selected
    43.         _tutorialSteps.Add(new TutorialStep("Drag a box over the unit to select it", setup: () => { selector.Activate(); } , verify: () => { return selector.selection.Count() > 0; }, cleanup: { selector.Deactivate(); }));
    44.  
    45.         //Tutorial complete!
    46.         _tutorialSteps.Add(new TutorialStep("Now you're a master of selecting stuff! Nice!", () => { _tutorialComplete = true; }, null, null));
    47.     }
    48.  
    49.     IEnumerator Tutorial()
    50.     {
    51.         //Set the first step
    52.         SetCurrentStep(_tutorialSteps[0]);
    53.  
    54.         currentStepIndex = 0;
    55.         while (!_tutorialComplete)
    56.         {
    57.             if (_currentStep.Verify() || (_currentStep.Skippable && Input.GetKeyDown(KeyCode.Space)))
    58.             {
    59.                 _currentStep.Cleanup?.Invoke();
    60.                 //Check tutorial complete just in case a step sets it true in cleanup (or verify)
    61.                 if (!_tutorialComplete)
    62.                 {
    63.                     SetCurrentStep(_tutorialSteps[++_currentStepIndex]);
    64.                 }
    65.             }
    66.             yield return null;
    67.         }
    68.     }
    69.  
    70.     void SetCurrentStep(TutorialStep step)
    71.     {
    72.         _currentStep = step;
    73.         tutorialText.text = _currentStep.Message;
    74.         if (step.Skippable)
    75.         {
    76.             skipText.text = "(Press space to skip)";
    77.         }
    78.         else
    79.         {
    80.             skipText.text = "(Unskippable... do as I say!)";
    81.         }
    82.         _currentStep.Setup?.Invoke();
    83.     }
    84. }
    Let me know what you think or if you have any questions!