- Home /
Creating a Step-By-Step Event-Based Tutorial
It is common in games of any complexity to point things out for the player to do, in sequence, to get the hang of it when it's his or her first time playing.
For example in an RTS, you might start the first mission with one unit, and you display an arrow pointing to it with a message telling them to click on it. Then when they do, tell them to move it to a certain area, then to attack a given enemy, then move somewhere else, then make another unit appear and have them click a button to show its abilities, then to click on a certain one, etc.
I'm trying to conceptualize what would be the most elegant, and flexible, way of implementing such an event-based system once you already have the engine built. Having a tutorial that waits for desired actions at given times, while still running the whole engine, is sort-of like having dozens of "winning conditions", only one of which is valid at a time.
The only practical way I can think of is to have a global int for the current tutorial step, and then to fill each possible event involved in the tutorial with conditions, such as:
if(GUI.Button(myRect,"Move Unit")){
if(tutorialStep==3 || tutorialStep==16 || tutorialStep==27)
Next_Tutorial_step();
//Do the button's actual logic here
}
As you can imagine, it'd become a bit bloated to fill the code for every button, action or keystroke that's involved in the tutorial with conditions like this, especially if it's comprehensive and involves at some point each of the dozens of actions you can perform in the game. This would for example require running code inside a unit movement function to check whether we're in a tutorial and, if so, whether we're selecting the object we're supposed to for the current step, and if so, whether it's going to the desired location etc.
It would be preferable to have everything in a separate Tutorial class which when activated could somehow be watching all events such as button clicks, locations of given objects, keystrokes or any other action that you want to register to it at some point or another.
Has anyone implemented such a system in Unity, which also has the flexibility to add new steps for future buttons or actions in one place?
I haven't done this in Unity, but in a different engine, I did it exactly as you describe here. That engine used LUA, so it was harder to use constants, but I would recommend using a single tutorial class with a constant defined for each tutorial state, ins$$anonymous$$d of 1, 2, 3, etc. use PLACE_BUILDING, COLLECT_WOOD, etc. then have a single entry point in the tutorial class like TutorialPass() that returns true if the passed in condition succeeds. For instance, you could pass in a string or enumeration that indicates the current test ($$anonymous$$ove Unit), and the TutorialPass method would check the tutorial state and other conditions needed and return true which would then allow you to call NextStep. It could always return true when the tutorial is not active.
Answer by whydoidoit · Mar 05, 2014 at 03:42 AM
Well I'd do it like this:
Create the description of your tutorial step as a ScriptableObject derived class so that it can a) be persistent b) be edited independently in the Inspector and c) refer to project items that might need to be created.
Have this class use a linked list style to the next step of the tutorial - so in other words have it hold a pointer to the next class. This gives you maximum flexibility in the future. Also give it an unique id - probably based on a GUID (`Guid.NewGuid().ToString()`) so you can find them easily.
You need to actually add these steps to the AssetDatabase so that they become a part of the project.
I would suggest you define the predicates for each step to be either an elapsed time or one of a list of "steps" (though I expect you will only use 1).
You use PlayerPrefs to store the ID of the last used step and write a static function on the tutorial step class that takes an "Action" - you would call from everywhere that might trigger a tutorial step.
So
TutorialStep.Did("Move Unit");
The static would examine the predicates of the current step and compare them against the string passed in - if it matches the tutorial is shown and the next step made current.
You also call a time function on a regular interval, say every 20 seconds:
IEnumerator Start() {
var t = Time.time;
while(true) {
yield return new WaitForSeconds(20);
TutorialStep.After(Time.time - t);
}
}
This way is very flexible and require you only to specify events to the system, not provide predicate logic inside your UI etc.
Interesting approach; putting each step in a separate ScriptableObject goes a step further than having them all in one. I'll give it a whirl for didactic purposes. Two questions:
Would putting all the parameters in the Inspector not mean that you no longer can have flexible code tailored to each step? For example, rather than having a Func/Action delegate for each one that you assign in hard-coded initialization, you'd have to give each TutorialStep firstly an Enum of possible functions to be called by the static "Did" function (or type a string to Send$$anonymous$$essage) to test whether it matches, and then secondly another one for what action it takes (say, display a new message on screen, point to their next button, or move the camera somewhere else on the map) together with a Rect or Vector2 that you'd have to use as a parameter. You then couldn't e.g. do all 3 of these plus a side action of generating an enemy somewhere, without wrapping these in another function and creating yet another fixed option just for the one case, no?
This will be my first foray into ScriptableObjects, so could you elaborate on when you'd call NewGuid and assign one's link to the next, in lieu of just storing them in a wrapper object's array?
Thanks.
Hi - sorry for the delay was travelling - will respond
I would suggest that what the ScriptableObject holds is a reference to a prefab to instantiate for the tutorial step. Clearly this prefab could consist of standard user interface elements that have been configured or could be completely custom. $$anonymous$$eeping a simple ScriptableObject step holder would make that code very simple. You could easily have the step do things like create an enemy by adding that script to whatever gets created.
You could initialize the ID in OnEnable on the scriptable object if it wasn't already set or make it part of a standard static initializer for it.