1

I've ran into some unexpected behavior in c#. I'm basically trying to assign an action to a reference of another action, so that I can subscribe/unsubcrib methods to the referenced action at a later time. I don't want to have to know the class which implements the referenced action.

The problem is, the action that should be pointing to the action I want to listen to doesn't seem to actually point to it. I thought it would raise whenever the referenced action is raised but apparently that's not the case.

I probably have some misunderstanding of delegates. Could someone please tell me what I'm doing wrong? Is there a solution to what I am trying to achieve?

I appreciate any responses!

public class WaitForActionProcess : IProcess
{
    public Action Finished { get; set; }
    Action actionHandler;

    public WaitForActionProcess(ref Action action)
    {
        actionHandler = action;
    }

    public void Play()
    {
        actionHandler += RaiseFinished;
    }

    public void RaiseFinished()
    {
        actionHandler -= RaiseFinished;

        if(Finished != null)
        {
            Finished();
        }
    }
}

Example of use:

public class ReturnToMainMenuFrame : TutorialEventFrame
{
    [SerializeField]
    TutorialDialogueData dialogueData;

    [SerializeField]
    PointingArrowData arrowData;

    [SerializeField]
    TutorialDialogue tutorialDialogue;

    [SerializeField]
    PointingArrow arrow;

    [SerializeField]
    MainView mainView;

    public override void StartFrame()
    {
        frameProcesses.Add(new ShowPointToUIProcess(arrow, arrowData));
        frameProcesses.Add(new ShowDialogueProcess(tutorialDialogue, dialogueData));
        frameProcesses.Add(new WaitForActionProcess(ref mainView.OnViewShown));
        frameProcesses.Add(new HideDialogueProcess(tutorialDialogue, this));
        frameProcesses.Add(new HidePointToUIProcess(arrow,this));
        base.StartFrame();
    }

}

Frame Base implementation:

public class TutorialEventFrame : MonoBehaviour {

    public delegate void OnFrameEnded();
    public event OnFrameEnded FrameEnded;

    public List<IProcess> frameProcesses = new List<IProcess>();

    public bool debugMode = false;

    public virtual void StartFrame()
    {
        StartProcess(0);
    }

    void StartProcess(int processIndex)
    {
        if (processIndex < frameProcesses.Count)
        {
            int nextProcessIndex = processIndex + 1;
            frameProcesses[processIndex].Finished += () => StartProcess(nextProcessIndex);
        }
        else
        {
            EndFrame();
            return;
        }

        if (debugMode)
        {
            Debug.Log("Starting process: " + frameProcesses[processIndex] + " of processes: " + (processIndex + 1) + "/" + (frameProcesses.Count - 1) + " on frame: " + name);
        }

        frameProcesses[processIndex].Play();           
    }

    public virtual void EndFrame() {

        foreach (var process in frameProcesses)
        {
            process.Finished = null;
        }

        if (debugMode)
        {
            Debug.Log("frame: " + name + " finished");
        }
        frameProcesses.Clear();

        if (FrameEnded != null) {
            FrameEnded();
        }
    }
}
AustinWBryan
  • 3,249
  • 3
  • 24
  • 42
Sebastian King
  • 341
  • 1
  • 11
  • 1
    How do you anticipate using this class? If you show us how you are using this class, an alternative may surface. – MineR Jun 08 '18 at 01:43
  • I'm pretty confused what you're trying to do. What causes RaiseFinished to be called? – cwharris Jun 08 '18 at 01:47
  • @MineR I'm making a tutorial framework for a game, the tutorial consists of frames which consist of a number of processes (IProcess) which essentially play some logic (that could take more than one program frame) and then raise an event when its finished. The frame will basically play a process and when its finished, it will play the following process until its finished. I wanted to have processes that finish when an action is raised, like for instance when the user does something within the application. I hope this helps :) maybe it's a bad solution. – Sebastian King Jun 08 '18 at 01:51
  • @cwharris RaiseFinished is called by the action that is referenced by actionHanlder (see the constructor). That action could be say, listening for a user to press a key, but I only want to listen for this action after the Play() method has been called. I can't add the action parameter to the play method because of the IProcess implementation... so I do it in constructor instead... – Sebastian King Jun 08 '18 at 01:54
  • So you want to start the ref Action in the constructor when the play button is pressed, and when that action is done, you want to RaiseFinished()? Still confused... Can you put some code which shows how the class is meant to be used? – MineR Jun 08 '18 at 01:57
  • Correct me if I'm wrong, but it sounds like you want to `var wfap = new WaitForActionProcess(myMethod);`, and then you want to `myMethod();` and have your `RaiseFinished` method invoked as a direct result of that invocation? – cwharris Jun 08 '18 at 02:02
  • @MineR hey yeah but I only actually want that event to be raised later when that process is reached. All the process are set up at the start of a frame, but if the player performs that action, I don't want it to work until that particular process has been reached, which is where it would finally apply the subscription – Sebastian King Jun 08 '18 at 02:03
  • @cwharris I do want RaiseFinished to be invoked via the action passed in, however, I want that invocation to only be able to happen a specific point in time, hence why im trying to store the action, so I can choose when I perform the actual subscription... It's a bit confusing I know – Sebastian King Jun 08 '18 at 02:06

2 Answers2

2

Assuming OnViewShown is an event, and you want to start listening to this event when Play is called, and then call Finished when the event triggered, the following will allow you to do this:

public class WaitForActionProcess : IProcess
{
    public Action Finished { get; set; }

    Action<Action> Subscribe { get; }
    Action<Action> Unsubscribe { get; }

    public WaitForActionProcess(Action<Action> subscribe, Action<Action> unsubscribe)
    {
        Subscribe = subscribe;
        Unsubscribe = unsubscribe;
    }

    public void Play()
    {
        Subscribe(RaiseFinished);
    }

    public void RaiseFinished()
    {
        Unsubscribe(RaiseFinished);
        Finished?.Invoke();
    }
}

Which is called with:

 var wfap = new WaitForActionProcess(
       h => mainView.OnViewShown += h, 
       h => mainView.OnViewShown -= h);

Note that you want to be 100% sure that OnViewShown will be triggered after Play, or Finished will never be called, and you'll also have a memory leak as you'll have never unsubscribed from the event.

Although it may not be available to you in Unity, look up System.Reactive, which formalizes this sort of thing and makes dealing with events much more manageable.

MineR
  • 2,144
  • 12
  • 18
  • Yesss thank you sir! This has worked for me. I can't say I completely understand what's going on, so I'll have to look it over. The only issue is that syntax is a bit cumbersome :< perhaps thats what reactive is for. I'll see if I can get it in Unity. Regardless I'm very thankful for your help – Sebastian King Jun 08 '18 at 05:14
  • Yes, the syntax is a real pain, but there is no way around it in C# – MineR Jun 08 '18 at 07:15
2

Short Answer

how to make an action that points to another action c#?

You cannot. Actions are delegates, and delegates are immutable.

the action that should be pointing to the action I want to listen to doesn't seem to actually point to it.

That is because delegates are immutable. Even though you pass a ref of the delegate, when you perform the assignment, you create a copy. Delegates are like strings in that way.

public WaitForActionProcess(ref Action action)
{
    // assignment creates a copy of a delegate
    actionHandler = action;
}

Example

Here is a Fiddle for you that demonstrates further.

public class Program
{
    static Action action1;
    static Action actionHandler;
    public static void Main()
    {
        WaitForActionProcess(ref action1);
    }

    public static void WaitForActionProcess(ref Action action)
    {
        // this is still a reference to action1
        action += Both;

        // assignment creates a copy of a delegate
        actionHandler = action;

        // action is still a reference to action1
        // but actionHandler is a *copy* of action1
        action += OnlyAction1;
        actionHandler += OnlyActionHandler;

        action();
        // Both
        // OnlyAction1

        actionHandler();
        // Both
        // OnlyAction2
    }

    public static void Both()=> Console.WriteLine("Both");
    public static void OnlyAction1() => Console.WriteLine("OnlyAction1");
    public static void OnlyActionHandler() => Console.WriteLine("OnlyActionHandler");
}

Possible Workaround

Use a List<Action> instead. Here it is as a Fiddle.

using System;
using System.Collections.Generic;

public class Program
{
    static List<Action> action1 = new List<Action>();
    static List<Action> actionHandler;
    public static void Main()
    {
        WaitForActionProcess(action1);
    }

    public static void WaitForActionProcess(List<Action> action)
    {
        action.Add(Both);

        // assignment passes a reference to the List
        actionHandler = action;

        action.Add(OnlyAction1);
        actionHandler.Add(OnlyActionHandler);

        // now things work nicely
        foreach(var a in action) a();
        foreach(var a in actionHandler) a();
    }

    public static void Both()=> Console.WriteLine("Both");
    public static void OnlyAction1() => Console.WriteLine("OnlyAction1");
    public static void OnlyActionHandler() => Console.WriteLine("OnlyActionHandler");
}
Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
  • Thank u sir! Your answer seems to work as well :D seems like a cheeky method lol. Thank you for explaining why it wasn't working. I didn't realize delegates were immutable. – Sebastian King Jun 08 '18 at 05:16
  • So would you have to do something like this `someObject.OnChange += () => { this.OnChange?.Invoke() }` Rather than simply `someObject.OnChange += this.OnChange` for it to work ? – WDUK Aug 28 '21 at 07:32