I expect it’s a fairly common need to say "I know, right now, that I want to execute FunctionX, but I don’t want to actually execute right this second, I want it to execute in 3 minutes.
I also expect it’s common to say "Okay, well, some other event occurred and I want cancel the execution of that FunctionX() I had, but only that one instance. I have 2 other FunctionX() calls pending and I still want those to go ahead and continue.
I have a firm grasp on how to accomplish the first need using yield and coroutines. The second need however is proving elusive and based on what I’ve found it may not be possible in any robust way. I thought I’d ask here for any additional insights.
While I can store a reference to a given Coroutine instance by assigning the result of StartCoroutine(), I can’t actually USE that reference in any meaningful way. I can’t destroy or delete it and I can’t use it to stop the pending execution of that coroutine.
There are some ways to stop the execution of a Coroutine (StopCoroutine and using a boolean flag) but neither is workable in the scenario I described because I can’t assume i will only ever have one instance of a specific function pending execution at any given time. I may very well have two or more of the same function about to run as a coroutine at the same time, but I have no way to cancel either one, specifically, I can only say “Hey, FunctionX() shouldn’t run”. I want to say “Hey, FunctionX(), yeah, you, but not the other two, you shouldn’t run”.
You encounter the same problems using invoke. If I invoke the same function twice, I have no way to only cancel one of them. In fact Invoke is a little worse since I can only cancel all invokes on a MonoBehavior, I can’t even specify a specific function like I can with coroutines.
You can cancel all A calls on one object, but you couldn’t cancel just one call to A with a simple function call. You’d have to write some sort of control to do that.
So it does! Missed that 2nd version with a parameter. So that put’s it on equal footing with yield/coroutines just with a different syntax. However, still only let’s me cancel any pending method named “FunctionX”. Doesn’t let me specify which of 2+ pending FunctionX’s I want to cancel.
Write some sort of “control”? What does “control” mean in this context?
I think this might work, but I don’t have Unity available to me right now so it could have bugs and such.
You’d put this script (BetterInvoke.js) on exactly one object in your game:
class FunctionCaller {
var time : float; // when to call the function
var cancel : boolean; // set to true to not call function
var func : String;
var object : GameObject;
var argument;
}
class BetterInvoke extends Monobehaviour {
private var callers : Array;
private var nextFunctionTime : float = Mathf.infinity;
function Invoke( caller : GameObject, method : String, arg, wait : float ) : FunctionCaller {
var fc = new FunctionCaller();
fc.time = Time.time + wait;
if ( fc.time < nextFunctionTime ) nextFunctionTime = fc.time;
fc.cancel = false;
fc.func = method;
fc.object = caller;
fc.argument = arg;
callers.Add( fc );
return fc;
}
function Update() {
if ( nextFunctionTime < Time.time ) {
nextFunctionTime = Mathf.infinity;
for ( var ix : int = 0; ix < callers.length; ix++ ) {
if ( callers[ix].time < Time.time ) {
var fc : FunctionCaller = callers[ix];
if ( !fc.cancel ) {
fc.object.SendMessage( fc.func, fc.arg );
}
callers.RemoveAt(ix);
ix--;
} else if ( callers[ix].time < nextFunctionTime ) nextFunctionTime = callers[ix].time;
}
}
}
}
Then in any script you can use:
var fc : FunctionCaller = BetterInvoke.Invoke( this.gameObject, "Function", argument, delay );
if ( health < 50 ) fc.cancel = true;
So, basically circumvent their scheduling options completely and just roll with using Time comparisons in the Update() function. Yeah, that was my “if nothing else” option. I might roll with that. One great thing about that is that I have much better options when it comes to parameter passing.
I also had another interesting idea. Since both invoke and coroutines gives me the option to cancel all pendings on a specific object, I can just create and track a gameobject for every schedule I make.
Hmm, I’ll have to think about which direction to go in and the performance consequences of each.
Actually nevermind, I don’t think that will work at all. Looks like i’m going to have to completely circumvent every scheduling methodology provided by Unity to do what I want. I think I’m stuck using an Update loop and just comparing times by hand.
The essential idea there is that I’m sending all the parameters by providing an object, string, Array or other collection style object to contain all of my values.
The problem here is that the function being called needs to expect that. It’s a perfectly workable function, but now every function that might be scheduled has to change the way it accepts and handles parameters, that’s a functional solution but it’s gross. A function should be defined on it’s own in a sane, rational, standard way. It shouldn’t care how my ScheduleManager may schedule a call to it.
If I were going to go this route (willing to alter every function to specifically work with the ScheduleManager), I would probably just stick a yield; at the start of every function so I can pass the function call as an IEnumerator to the ScheduleManager and just store that off, calling Coroutine(IEnumerator) later to actually execute the function instead of using SendMessage. That way I don’t even have to worry about the extra parameter handling code. But…not willing to do that as it’s monumentally gross and kludgey. Every function in my application would then be a kludge in some sense. I should be able to drop my ScheduleManager into any project, and start scheduling delay calls to functions without having to change the functions themselves.
This is looking more and more like this goal isn’t possible, being able to take call any, normal, existing, function with an arbitrary number of parameters, schedule the execution of that call with parameters until later, and have the ability to cancel a specific instance of any call at any time should the need arise. If eval() was available in iOS this would be trivial since I could just pass a string (“SomeFunction(parama, paramb, … paramn);”) and evaluate it whenever I needed to. But eval() isn’t available in iOS and I’m kind of curious why.
It’s looking like I’m just going to have to bite the bullet and only allow one parameter for a scheduled function call, handling the, hopefully rare, cases where that’s not enough in an exceptional way by having the function take a single Array() parameter or something gross like that.
To that end, most likely you can employ polymorphism and delegates to maintain type safety.
By the way, it is possible. It’s almost always possible. And honestly, even though my solution requires casting within the function (actually, it might now depending on how SendMessage works, but I’d have to test it) and the declaration of discrete argument object types, I think it’ll do the job. What you’re trying to do here doesn’t sound so crazy, just sounds like you’re overcomplicating the issue or design. Maybe I’m completely missing your design intent: are you trying to create some kind of user-scripting engine or something? (not sure if that’s even legally allowed on iOS anyway) Or just trying to find a simple/lazy way to reference your functions without strict type-safety?
AFAIK, most, if not all of the Unity API is not threadsafe and typically cannot be worked with from a background thread. (your own custom scripts can still be threaded as long as they don’t use the Unity API)
Let me try to restate what I’m trying to do. After scheduling a specific function to run after X time I want to cancel the schedule before X time elapses so that the execution of the function doesn’t actually occur, but if I have more than one instances of that function scheduled to execute I may only want to cancel one of them.
Step 1: Does Unity provide a way to cancel a specific instance of a scheduled function before it executes? Short answer, no, not that we can find. You have some canceling options with CancelInvoke() and StopCoroutine() but you can only provide those a method name and it will, presumably, cancel ALL instances of that function. This doesn’t meet my needs. Let me restate this as some code. I’m going to use Invokes because it’s more succinct but the same problem applies to using yields. I’m not trying to simply solve this one, specific instance of the problem, but handling this case in general, any that might come across in the future.
// Spawn two monkey's in 5 seconds.
Invoke("SpawnMonkey", 5.0f); // Spawn monkey "Sam"
Invoke("SpawnMonkey", 5.0f); // Spawn monkey "Janet"
// ... Some time later, elsewhere in the code
// Something happens and I want to stop Sam from spawning,
// but I still want Janet to spawn
CancelInvoke("SpawnMonkey");
// Oops! That won't work! I still wanted to spawn Janet but that
// will cancel both monkies! That's not what I want!
Okay, so, using the build in Unity scheduling options (Invoke and yields) I cannot handle those kinds of situations. I want my scheduling solution to be very robust so I want to be able tot handle those cases if they arise in the future. What are my options?
Step 2: I can’t use Unity’s build in options, so I will use my own scheduling solution. However I manage this it needs to be in a robust way. I should be able to drop this solution into any existing project and use it to schedule/cancel the execution of any, normally written, function. (I’m not worried about type safety here or a lack of compile time error checking; the need to not have to customize the functions trumps that.)
So let’s get down to business. To delay the execution of a function until some later time I need to know two things, what I’m executing and when to execute it. So I will have a basic Schedule class that contains that information. I will also need to be able to reference a specific Schedule and not just the function name (which was the shortcoming with the previous options). Here’s a barebones example in semi-pseudocode.
// private members
private static var schedules : Array = null;
// Helper Class
class Schedule
{
// Members
private var timeToCall : float = 0.0f;
private var functionToExecuteWithParameters : ??? = null;
// Problem Point Part 1: how to store this function info?
private var scheduleName : String = null;
// Constructor
public function Schedule(time:float,
functionToExecuteWithParameters : ???,
scheduleName : String)
{
this.timeToCall = time;
this.functionToExecuteWithParameters =
functionToExecuteWithParameters;
this.scheduleName = scheduleName;
}
}
// private methods
function Awake()
{
schedules = new Array();
}
function Update()
{
// must be backwards because we're removing as we iterate
for (loop backwards through schedules)
if (time to execute this schedule)
{
// Problem Point Part 2,
//how do we execute the info we've stored?
execute the schedule
remove schedule from Array
}
}
// public interface
function AddSchedule(time:float,
functionToExecuteWithParameters : ???,
scheduleName : String)
{
add schedule to schedulesArray
}
function CancelSchedule(scheduleName : String)
{
for (loop backwards through schedules)
if (schedule name matches)
remove schedule from array
}
That’s a basic idea anyway. Very straightforward and easy to use interface. It let’s me very easy schedule functions and I can even cancel them by name, allowing to per schedule granularity on what to cancel rather than just per function.
This logic is sound, and works. THere’s just one problem, how to properly store and execute the function. One method, and what we’ve done in the past with similar issues, is to store the function info as a string, “FunctionX(param1, param2);”, and execute it later using eval(). Unfortunately eval() isn’t available in iOS so this isn’t an option.
Another option is to store the function as an IEnumerator and execute it using StartCoroutine. Unfortunately to pass the function call as an IEnumerator requires that the first line of the function be a yield;. This breaks our rule about being able to drop this thing into any existing code base and using it to call functions without adjusting them. I’m also worried about having side effects with just willy-nilly delaying function executions by an additional frame than when I expect.
So, there’s another option (the one I’m using right now), I can pass the function name as a string and a single argument as a object and use SendMessage(). This works, but breaks the rule about this thing being robust enough to work on any function; I could only use it on functions with one or less parameters. I could change functions to accept a single parameter such as a string or Array() that actually contains multiple parameters and parse them out, but that breaks the same rule that using StartCoroutine does, but with the added pain of having to create parameter parsing logic, so if I did that I might as well use StartCoroutines.
Next option…Well I’m all out :(.
So at the moment I’m forced to accept the limitation of a single parameter for scheduled functions, other than that it’s working fine. Any additional thoughts are welcome.
Because I want the solution to work on any existing, normally defined function. I don’t want to have to write the functions in a special way, in this case encapsulating any function I want to schedule/cancel in a class.