Hi everyone, in today's tutorial I'm going to talk about creating stylish tutorial windows for your games using video. Usually such inserts are used to show the player what is required of him in a particular training segment, or to show a new discovered ability in the game.
Creating Tutorial Database
First, let's set the data about the tutorials. I set up a small model that stores a value with tutorial skip, text data, video reference and tutorial type:
// Tutorial Model
[System.Serializable]
public class TutorialData
{
public bool CanSkip = false;
public string TitleCode;
public string TextCode;
public TutorialType Type;
public VideoClip Clip;
}
// Simple tutorial types
public enum TutorialType
{
Movement,
Collectables,
Jumping,
Breaking,
Backflip,
Enemies,
Checkpoints,
Sellers,
Skills
}
Next, I create a payload for my event that I will work with to call the tutorial interface:
public class TutorialPayload : IPayload
{
public bool Skipable = false;
public bool IsShown = false;
public TutorialType Type;
}
Tutorial Requests / Areas
Now let's deal with the call and execution of the tutorial. Basically, I use the Pub/Sub pattern-based event system for this (you can read more about it in my other tutorial), and here I will show how a simple interaction based on the tutorial areas is implemented.
public class TutorialArea : MonoBehaviour
{
// Fields for setup Tutorial Requests
[Header("Tutorial Data")]
[SerializeField] private TutorialType tutorialType;
[SerializeField] private bool showOnStart = false;
[SerializeField] private bool showOnce = true;
private TutorialData tutorialData;
private bool isShown = false;
private bool onceShown = false;
// Area Start
private void Start() {
FindData();
// If we need to show tutorial at startup (player in area at start)
if (showOnStart && tutorialData != null && !isShown) {
if(showOnce && onceShown) return;
isShown = true;
// Show Tutorial
Messenger.Instance.Publish(new TutorialPayload
{ IsShown = true, Skipable = tutorialData.CanSkip, Type = tutorialType });
}
}
// Find Tutorial data in Game Configs
private void FindData() {
foreach (var tut in GameBootstrap.Instance.Config.TutorialData) {
if (tut.Type == tutorialType)
tutorialData = tut;
}
if(tutorialData == null)
Debug.LogWarning($"Failed to found tutorial with type: {tutorialType}");
}
// Stop Tutorial Outside
public void StopTutorial() {
isShown = false;
Messenger.Instance.Publish(new TutorialPayload
{ IsShown = false, Skipable = tutorialData.CanSkip, Type = tutorialType });
}
// When our player Enter tutorial area
private void OnTriggerEnter(Collider col) {
// Is Really Player?
Player player = col.GetComponent<Player>();
if (player != null && tutorialData != null && !showOnStart && !isShown) {
if(showOnce && onceShown) return;
onceShown = true;
isShown = true;
// Show our tutorial
Messenger.Instance.Publish(new TutorialPayload
{ IsShown = true, Skipable = tutorialData.CanSkip, Type = tutorialType });
}
}
// When our player leaves tutorial area
private void OnTriggerExit(Collider col) {
// Is Really Player?
Player player = col.GetComponent<Player>();
if (player != null && tutorialData != null && isShown) {
isShown = false;
// Send Our Event to hide tutorial
Messenger.Instance.Publish(new TutorialPayload
{ IsShown = false, Skipable = tutorialData.CanSkip, Type = tutorialType });
}
}
}
And after that, I just create a Trigger Collider for my Tutorial zone and customize its settings:
Tutorial UI
Now let's move on to the example of creating a UI and the video in it. To work with UI I use Views - each View for a separate screen and functionality. However, you will be able to grasp the essence:
To play Video I use Video Player which passes our video to Render Texture, and from there it goes to Image on our UI.
So, let's look at the code of our UI for a rough understanding of how it works (Ignore the inheritance from BaseView - this class just simplifies showing/hiding UIs and Binding for the overall UI system)*:
public class TutorialView : BaseView
{
// UI References
[Header("References")]
public VideoPlayer player;
public RawImage uiPlayer;
public TextMeshProUGUI headline;
public TextMeshProUGUI description;
public Button skipButton;
// Current Tutorial Data from Event
private TutorialPayload currentTutorial;
// Awake analog for BaseView Childs
public override void OnViewAwaked() {
// Force Hide our view at Awake() and Bind events
HideView(new ViewAnimationOptions { IsAnimated = false });
BindEvents();
}
// OnDestroy() analog for BaseView Childs
public override void OnBeforeDestroy() {
// Unbind Events
UnbindEvents();
}
// Bind UI Events
private void BindEvents() {
// Subscribe to our Tutorial Event
Messenger.Instance.Subscribe<TutorialPayload>(OnTutorialRequest);
// Subscribe for Skippable Tutorial Button
skipButton.onClick.RemoveAllListeners();
skipButton.onClick.AddListener(() => {
AudioSystem.PlaySFX(SFXType.UIClick);
CompleteTutorial();
});
}
// Unbind Events
private void UnbindEvents() {
// Unsubscribe for all events
skipButton.onClick.RemoveAllListeners();
Messenger.Instance.Unsubscribe<TutorialPayload>(OnTutorialRequest);
}
// Complete Tutorial
private void CompleteTutorial() {
if (currentTutorial != null) {
Messenger.Instance.Publish(new TutorialPayload
{ Type = currentTutorial.Type, Skipable = currentTutorial.Skipable, IsShown = false });
currentTutorial = null;
}
}
// Work with Tutorial Requests Events
private void OnTutorialRequest(TutorialPayload payload) {
currentTutorial = payload;
if (currentTutorial.IsShown) {
skipButton.gameObject.SetActive(currentTutorial.Skipable);
UpdateTutorData();
ShowView();
}
else {
if(player.isPlaying) player.Stop();
HideView();
}
}
// Update Tutorial UI
private void UpdateTutorData() {
TutorialData currentTutorialData =
GameBootstrap.Instance.Config.TutorialData.Find(td => td.Type == currentTutorial.Type);
if(currentTutorialData == null) return;
player.clip = currentTutorialData.Clip;
uiPlayer.texture = player.targetTexture;
player.Stop();
player.Play();
headline.SetText(LocalizationSystem.GetLocale($"{GameConstants.TutorialsLocaleTable}/{currentTutorialData.TitleCode}"));
description.SetText(LocalizationSystem.GetLocale($"{GameConstants.TutorialsLocaleTable}/{currentTutorialData.TextCode}"));
}
}
Video recordings in my case are small 512x512 clips in MP4 format showing certain aspects of the game:
And my TutorialData settings stored in the overall game config, where I can change localization or video without affecting any code or UI:
In conclusion
This way you can create a training system with videos, for example, showing what kind of punch your character will make when you press a key combination (like in Ubisoft games). You can also make it full-screen or with additional conditions (that you have to perform some action to hide the tutorial).
I hope I've helped you a little. But if anything, you can always ask me any questions you may have.
Thanks!