Question related to interactive tutorials and C# delegates

It’s hard to describe, but I’ll give a shot.

My full interactive tutorial code. This is for reference to the following explanation below this:

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;

namespace Tutorial {
    public interface IDialogAction {
        void Run();
    }

    [Serializable]
    public struct DialogArrow {
        public Vector2 position;
        public float angle;

        public DialogArrow(float x, float y, float angle) {
            this.position = new Vector2(x, y);
            this.angle = angle;
        }
    }

    [Serializable]
    public struct DialogText {
        public Vector2 position;
        public string message;

        public DialogText(float x, float y, string message) {
            this.position = new Vector2(x, y);
            this.message = message;
        }
    }


    [Serializable]
    public struct DialogProperty {
        public DialogText dialogText;
        public DialogArrow dialogArrow;
        public bool changePosition;
        public bool showArrow;
        public float elapsedTime;
        public IDialogAction action;

        public DialogProperty(DialogText text, DialogArrow arrow, bool shouldChangePositionOnNextDialog, bool showArrowOnNextDialog, float time, IDialogAction action) {
            this.dialogText = text;
            this.dialogArrow = arrow;
            this.changePosition = shouldChangePositionOnNextDialog;
            this.showArrow = showArrowOnNextDialog;
            this.elapsedTime = time;
            this.action = action;
        }
    }

    public class TutorialDialog : MonoBehaviour {
        public RectTransform dialogTransform;
        public RectTransform arrowTransform;
        public GameObject mainDialog;
        public GameObject arrow;
        public List<DialogProperty> properties;
        public int propertyIterator;
        public bool runCoroutine;

        void Start() {
            if (this.mainDialog == null) {
                Debug.LogError("Dialog: Something is wrong.");
            }
            if (this.arrow == null) {
                Debug.LogError("Arrow: Something is wrong.");
            }
            this.dialogTransform = this.mainDialog.GetComponent<RectTransform>();
            this.arrowTransform = this.arrow.GetComponent<RectTransform>();
            this.properties = new List<DialogProperty>();
            this.runCoroutine = false;
            Initialize();
        }

        void OnGUI() {
            if (this.propertyIterator < this.properties.Count) {
                DialogProperty property = this.properties[this.propertyIterator];
                Text text = this.mainDialog.GetComponentInChildren<Text>();
                if (text != null) {
                    text.text = property.dialogText.message;
                }
                if (property.changePosition) {
                    if (this.dialogTransform != null) {
                        this.dialogTransform.anchoredPosition = property.dialogText.position;
                    }
                }
                if (property.showArrow) {
                    this.arrowTransform.anchoredPosition = property.dialogArrow.position;
                    this.arrowTransform.localRotation = Quaternion.Euler(0f, 0f, property.dialogArrow.angle);
                }
                else {
                    this.arrowTransform.anchoredPosition = new Vector2(0f, 260f);
                }
            }
        }

        void Update() {
            if (this.runCoroutine) {
                this.StartCoroutine(CR_DialogSwitch());
            }
        }

        private IEnumerator CR_DialogSwitch() {
            DialogProperty property = this.properties[this.propertyIterator];
            if (property.elapsedTime > 0f) {
                if (this.mainDialog.activeSelf) {
                    this.mainDialog.SetActive(false);
                }
                if (this.arrow.activeSelf) {
                    this.arrow.SetActive(false);
                }
                this.properties[this.propertyIterator] = property;
                yield return new WaitForSeconds(property.elapsedTime);
            }
            if (!this.mainDialog.activeSelf) {
                this.mainDialog.SetActive(true);
            }
            if (!this.arrow.activeSelf) {
                this.arrow.SetActive(true);
            }
            this.runCoroutine = false;
        }

        public void OnTutorialButtonClick() {
            this.propertyIterator++;
            if (this.propertyIterator < this.properties.Count) {
                this.runCoroutine = true;
            }
        }

        private void Initialize() {
            this.properties.Add(new DialogProperty(new DialogText((Screen.width - 300f) / 2f, -(Screen.height + 95f) / 2f, "Hello! Let's get started on how to play this game."), new DialogArrow(0f, 0f, 135f), true, false, 0f));
            this.properties.Add(new DialogProperty(new DialogText(83.82f, -259.75f, "This is your unit."), new DialogArrow(-193.24f, -91.92f, 110.5f), true, true, 0f));
            this.properties.Add(new DialogProperty(new DialogText(83.82f, -259.75f, "There are no other units."), new DialogArrow(218f, -313.3f, 135f), true, false, 0f));
            this.properties.Add(new DialogProperty(new DialogText(400f, -259.75f, "This is your opponent's unit."), new DialogArrow(174.92f, 101.9f, 287.9f), true, true, 0f));
            this.properties.Add(new DialogProperty(new DialogText((Screen.width - 300f) / 2f, -(Screen.height + 95f) / 2f, "All units behave the same, regardless of faction."), new DialogArrow(0f, 0f, 0f), true, false, 0f));
            this.properties.Add(new DialogProperty(new DialogText((Screen.width - 300f) / 2f, -(Screen.height + 95f) / 2f, "To select your unit, left click on the unit."), new DialogArrow(0f, 0f, 0f), true, false, 0f));
            this.properties.Add(new DialogProperty(new DialogText((Screen.width - 300f) / 2f, -(Screen.height + 95f) / 2f, "You can also select your unit by dragging a box."), new DialogArrow(0f, 0f, 0f), true, false, 0f));
            this.properties.Add(new DialogProperty(new DialogText((Screen.width - 300f) / 2f, -(Screen.height + 95f) / 2f, "To deselect, click anywhere else away from your unit."), new DialogArrow(0f, 0f, 0f), true, false, 0f));
        }
    }
}

Say that you have a button the user can press. This button is used for triggering the next tutorial sequence, as well as some actions that the game needs to do in order to unlock a tutorial sequence (such as a flag that blocks the user from using advance features before the user was allowed to do so). The tutorial sequence is placed inside a list (see near top of class TutorialDialog). When the user presses a button, it increments an iterator index that goes through the whole list.

I wanted to extend this by having the user trigger an action while incrementing to the next tutorial sequence, and I’m currently thinking of C# delegates. I wanted to know if there’s a way to pass delegates into and set them in the structs given, something like the following pseudo-code:

public struct Container {
    public delegate DoAction action;

    public Container(DoAction a) {
        this.action = a;
    }
}

DoAction Something() {
    //Action...
}
this.tutorial.Add(new Container(Something()));

I know that delegates act as function pointers in C#, so I was wondering if it’s possible to have a function pointer in a struct, and have it either point to a function, or null.

Is it possible? Or I’m mistaken…

No reason why not. You should look into Action and Func to get you started. In your example, you can simply do this to create an action delegate reference inline with a lambda expression (love lambda):

this.tutorial.Add(new Container( () => DoAction() ));

There’s no need, and no way AFAIK, to specify that the stored delegate HAS to be to for “DoAction” here though- you’d just ask for and store any delegate with:

public Action someDelegate;

and then later on you could invoke the method whose reference you stored there with:

someDelegate.Invoke();

That said, you should avoid using custom structs in Unity IMO, because they won’t serialize properly. Even if they don’t give you problems now, I promise that they will eventually. Here’s a thread that covers this issue a bit.

Seems to be what I wanted. I’ll look into it.

I have already made all of the structs, classes, and class members (fields) all [Serializable]s and [SerializeField]s. I changed all “struct” keywords into “class”.