0
\$\begingroup\$

I'm working on a game project in Unity with a point and click interface and I'm trying to make a method that moves the player towards a target object and invokes another method depending on what the target object is.

I'm running into two problems: first, when I try to pass the method as a parameter, it immediately runs it. For example, the following code is given to the Unity UI to create a button that is supposed to make the player move towards a target and then open a pop up menu. AB_PathAct is the method that moves the player, the first parameter is the player's destination, and the second parameter is a method, CoMuOpen, which AB_PathAct is supposed to use when the player reaches the destination. CoMuOpen has it's own parameters. Instead, when the following code runs, both parts happen at once: The player is sent towards the target, and the menu immediately opens. In other words, the code isn't even trying to pass the CoMuOpen call as a parameter of AB_PathAct, it's just calling it.

public void ButtonCMDInvokeTarget()
{
    AB_PathAct(
       r_hit.collider.gameObject, 
       CoMuOpen(
           popMenus[1],
           Camera.main.WorldToScreenPoint(
               r_hit.collider.gameObject.transform.position
           )
       ),
       tarDist
    );
}

The second problem is that AB_PathAct isn't actually taking that method as a parameter; when AB_PathAct reaches its destination, it fails to invoke the method that is its second parameter. I know why this is happening; CoMuOpen returns a string and AB_PathAct takes a string as it's second parameter. What I want to do is replace AB_PathAct's string parameter with a method.

To put it simply, I need to replace two lines of AB_PathAct to make it do what I actually want, but I don't know what to replace them with. Everything in the following code functions correctly except the problem code, which is surrounded by asterisks below (the actual code doesn't have them, obviously).

void AB_PathAct(GameObject comTar, **string methodToInvoke**, float tarDist)
{
    if (Vector3.Distance(transform.position, comTar.transform.position) > tarDist)
    {
        agent.destination = comTar.transform.position;
        mr_moveGoal.enabled = true;
        moveGoal.transform.position = new Vector3(comTar.transform.position.x, moveGoal.transform.position.y, comTar.transform.position.z);
    }
    else if (Vector3.Distance(transform.position, comTar.transform.position) <= tarDist)
    {
        agent.ResetPath();
        **StartCoroutine(methodToInvoke);**
    }
}

Is it possible to make a method take another method call as a parameter?

EDIT: Here's the CoMuOpen code for reference. It's a very simple function that works as intended when invoked directly or through UnityUI. Returning a string isn't important and I'll probably remove it.

public string CoMuOpen(GameObject tarMen, Vector3 tarPos)     
    {         
    tarMen.transform.position = tarPos;         
    tarMen.SetActive(true);          
    return "Open Context Menu";     
    }
\$\endgroup\$
0

2 Answers 2

2
\$\begingroup\$

When you write a method with parentheses after it, like:

SomeFunctionWithNoParams()
// or
SomeFunctionWithOneParam(someParameter)

...then you are calling that function. Assigning this to a variable or passing it as a parameter does not capture a reference to the function as a process, it executes the function and takes its return value (if any) at the end of its execution.

If you want to refer to the function itself as a thing, you have to leave the parentheses off. This treats it as a delegate - analagous to a "function pointer" in other languages.

System.Action someFunctionDelegate = SomeFunctionWithNoParams;
System.Action<ParameterType> someOtherDelegate = SomeFunctionWithOneParam;

But if you don't have parentheses after the function, how do you specify the parameters to use when calling the function? There are a few options...

  1. Make a lambda function that "bakes in" the parameters you want, something called a closure.

    System.Action bakedFunction = () => { SomeFunctionWithOneParam(someParameter); };
    
    DoSomethingThenCallThisFunction(bakedFunction);
    

    This makes a new function that takes no arguments, which when executed will call the function you want with the parameter you wanted. Then we can pass a reference to this parameterless function to another function.

    Note that this makes a heap allocation to store the new function and the parameter values it's captured.

  2. Pass the parameters to the method that will need to make the call, so it can supply them when needed:

    void DoSomethingThenCallWithParams(System.Action<int, bool> functionToCall, int paramA, bool paramB) {
       DoThing();
       // ...
       functionToCall(paramA, paramB);
    }
    

    The advantage here is that there's no unnecessary heap allocation. But the downside is that the method you're passing the function into needs to be written with knowledge of the functions that might get passed to it, making it harder to use this when the behaviour of those functions needs to vary wildly.

  3. Use an iterator. This gives you a function you can invoke, then pause partway, and finish executing later. In between the frozen state of the function can be passed around as an IEnumerator object.

    This is how Unity implements its Coroutines, so if you want to StartCoroutine anyway, it's a fairly natural fit.

    IEnumerator DelayedDoThing(int param1, bool param2) {
       // Do something with your parameters
    
       yield break; // Iterator must have at least one yield statement.
       // If you don't need a pause mid-way, just add a yield break
       // which works like "return" to end execution.
    }
    
    void DoAThingThenStartCoroutine(IEnumerator coroutineContent) {
       // Do a thing.
       // ...
    
       StartCoroutine(coroutineContent);
    }
    

    Invoke this like so:

    var thingToDoLater = DelayedDoThing(5, true);
    
    DoThingThenStartCoroutine(thingToDoLater);
    

    Similar to the closure, this causes a heap allocation to store the iterator object. But if you're running a coroutine later anyway, that heap allocation is already happening, so it's not an additional cost.

\$\endgroup\$
0
2
\$\begingroup\$

DMGregory's answer worked! Here's the new code:

void AB_PathAct(GameObject pathTar, float tarDist, System.Action<GameObject, Vector3> methodToInvoke, GameObject actTar, Vector3 actPos)
{
    if (Vector3.Distance(transform.position, pathTar.transform.position) > tarDist)
    {
        agent.destination = pathTar.transform.position;
        mr_moveGoal.enabled = true;
        moveGoal.transform.position = new Vector3(pathTar.transform.position.x, moveGoal.transform.position.y, pathTar.transform.position.z);
    }
    else if (Vector3.Distance(transform.position, pathTar.transform.position) <= tarDist)
    {
        agent.ResetPath();
        methodToInvoke(actTar, actPos);
        print(pathTar.name + methodToInvoke);
    }
}

PathAct takes five parameters: the object that is the player's destination, the distance at which the player will stop, the Method that will be called when that destination is reached, and then the two parameters that will be passed to the method: a gameobject, and a vector3 position. Here's an example of what calling it looks like:

AB_PathAct(r_hit.collider.gameObject, tarDist, CoMuOpen, popMenus[1], Camera.main.WorldToScreenPoint(r_hit.collider.gameObject.transform.position));

The biggest limitation is that the Method that gets passed to PathAct needs to have a GameObject and also a Vector3 as parameters, but that's not that big of a deal, and I might experiment with overrides if I need to.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .