Intervals: A "Run Method Until Done" Interval?

Simply put, do we have anything along the lines of a “Func” interval, but that, instead of running the given method and returning, repeats the method until said method returns a “done” code of some sort?

I know that we have a “LerpFunctionInterval”, but that’s not quite what I want, if I’m not much mistaken: that requires that I specify the duration over which the function should run, whereas in the case that I’m looking for I may not know the duration offhand; the method will be done when it’s done.

For such purposes, it is possible to use the task, with the condition. I can not understand the purpose of intervals, they bring inconvenience to me.

I’m aware of how to do this in a task, but that doesn’t work with the situation that I have in mind, as far as I see; I specifically want to use this within the context of Sequences and Parallels.

I’m not usually a huge fan of Intervals myself, but they have their uses, I’ve found. In this case, I think that they offer a convenient way of stringing together a bunch of non-interactive “events”–lines spoken by characters, movements from here to there, turning to look at something, and so on. However, I have at least one such case in which it would be convenient to have its completion be determined by the function being called by the interval, rather than pre-set by the developer.

You are pretty much describing tasks; methods that run until they return a done code.

The fundamental nature of intervals requires them to have a fixed duration. This means you cannot have something run indeterminately.

Hum. That’s a pity and a pain–other than that one case, Sequences and Parallels are an excellent fit for what I have in mind, I feel. :/

I suppose that I could try to come up with an approximate duration of some sort for such cases…

(I really don’t want to re-build what would essentially be Sequences and Parallels just to get this one feature.)

Well, thank you for the responses, both of you! :slight_smile:

Here I gave an example of how to convert an interval sequence into a task sequence.

I appreciate that, but I would still end up basically re-building the Sequence and Parallel framework, not to mention all of the appropriate Intervals–and if I ever released this project for others to use, they might end up rebuilding any Intervals that I hadn’t implemented. That just sounds like a pain–I’d rather just use the extant Sequences, Parallels, and Intervals.

I think that it would likely be easier for me to go the other way: I only have one method right now that calls for “running until it returns a done-code”, and I might be able to either modify it or make an alternative that “performs an action in a given duration” (whether it’s presented to the developer in that way or not).

[edit]
And to clarify, I’m fairly familiar with doing such things in tasks–it’s my usual way of handling game-logic. It’s just that in this specific instance, I feel that Sequences and Parallels are preferable.

Would a coroutine be a better choice? Using a coroutine you can make a task that is essentially a more flexible form of a sequence (especially since you can await various types of intervals).

Note for the record that intervals are just wrappers around the task system.

Hmm… I’m less familiar with those. I’ll look them up in the manual to get a better idea, and report back, I think!

Okay, I’m not sure that I have this quite right, but if I understand correctly coroutines are essentially a form of threading. I can see how this might be used to make a new Sequence-and-Parallel system, but I’m not sure of how it’s better than just using ordinary tasks, offhand.

Perhaps it’s worth better-describing what I’m trying to do here: In short, I want an intuitive, fairly-simple cutscene-scripting system.

For example, I might specify that dialogue-line 1 plays, followed by dialogue line 2, followed by the player and character A moving to some position, followed by a turn to look at a specific target, and so on.

Now, I could manually script everything without much trouble, but it would be preferable to use a simple interface where available.

Furthermore, I have a vague idea that maybe, perhaps, I might end up releasing this framework for others to use. In that case I’d very much want that the scripting-interface for this be something easy and intuitive.

For the most part, Sequences and Parallels fit this purpose excellently, I feel: I find them to be straightforward and intuitive.

However, I have at least one method that, as currently implemented, doesn’t operate over a specified duration, but rather until it’s done. For that purpose I would have liked an Interval that worked in that manner.

But ah well, so it goes–I’ll likely be able to either modify it or put together another version such that it does operate over a specified duration (whether it’s presented to the developer that way or not).

I imagine the methods that have a unpredictable duration are things like path finding. You want a character to go to some pre defined point on your map and do something after reaching the spot (say a line, play a animation, open a door, etc).

You can divide your sequence into all the things to do before running the function with unknown duration and a sequence with everything after that function end. All you need to do now is to add that function as the last Func in the ‘before’ sequence and add a bit of code that starts the ‘after’ sequence when the function end.

Hmm… It’s not path-finding, but I suppose that I could build a workaround like that. With the possibility of offering this for others to use in mind, I’d want to build some means of making this fairly transparent, but it’s perhaps worth thinking about.

I suspect that reworking the method in question is still the easier route, and the one that results in a simpler interface.

(Come to think of it, pathfinding would actually be relatively simple in my case: my “world” is tile-based, so paths are relatively straightforward, and can be broken into a series of steps.)

If you know your function is at the end of a sequence, this runs indefinitely. So, maybe you could add this to other sequences and kick off any following sequences when this is over. This is functional, you could inherit Sequence and implement the below as a subclass.

I admit this is not much more than a proof of concept, and it generates a warning about losing events, which is why the interval should be at the end or alone.

from direct.interval.IntervalGlobal import *
from direct.showbase.ShowBase import ShowBase
from direct.directnotify.DirectNotify import DirectNotify
notify = DirectNotify().newCategory("Game::Logger")

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        def myFunc(seq):
            nonlocal doFunc
            seq.append(Func(doFunc, seq))
            notify.warning('myFunc')
            seq.start()
        doFunc = myFunc

        seq = Sequence()
        seq.append(Func(myFunc, seq))
        seq.start()

If I’m reading this correctly, it essentially just keeps adding itself to the Sequence? Interesting, and I suppose that it could be extended to work with Parallels, too.

I’d want to have some means of having it automatically split Sequences (ensuring that it always runs at the end), so that one could add it to a Sequence as though it were any other Interval.

I’m not sure that reworking my problem-method isn’t easier, but this is perhaps worth thinking about nevertheless! Either way, thank you. :slight_smile:

Coroutines are entirely unrelated to threading. A coroutine is simply a function that can be suspended and continued. You’ll often see them in the context of asynchronous programming because they allow you to write asynchronous code that looks synchronous. But you can also use them to implement sequences using a clean but very flexible syntax.

Here’s a pseudo-code example of a “sequence” that also waits arbitrarily for some condition:

async def mysequence():
    # Play interval and wait until it is done
    await abc.posInterval(...).play()

    # When that is done, play some other interval, but don't wait until it is done
    some other interval.play()

    # While that plays, run through this while loop
    while some condition:
        something()
        # Suspend coroutine until some time has passed
        await Task.pause(0.01)

taskMgr.add(mysequence())

It certainly seems a little cleaner to me than dynamically modifying a sequence at runtime.

Ah, I see–thank you for the explanation!

I’ll give that a closer look a little later, but it seems like something to consider, at least! Thank you. :slight_smile:

I think I ought to create some example code at some point showing how to use coroutines and intervals together. It’s a bit of an underdocumented feature at the moment.

@rdb is probably right. Although I’ve not used coroutines in Python, they are about as clean as he presents them. I didn’t know what they were called.

[edit] In C# they’re as clean as presented.

But, I found the problem interesting. So, I made this. This is the full constructor signature DynamicFuncSequence(method<, method_args><, method_kwargs_in_dict><, follow_up_intervals><, name=your_name>) So, the example is DynamicFuncSequence(myFunc, 1, 2, {'k': 'k', 'w': 'w'}, Func(afterFinish)) For follow up intervals, I only checked for (Func, Sequence, Interval) which likely isn’t correct/full as you can in init.

from direct.interval.IntervalGlobal import *
from direct.showbase.ShowBase import ShowBase
from direct.directnotify.DirectNotify import DirectNotify
notify = DirectNotify().newCategory("Game::Logger")

class Game(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        class DynamicFuncSequence(Sequence):
            def __init__(self, func, *args_ivals, name=""):
                super(Sequence, self).__init__(name=name)  # behavior of other keywords is unknown
                ival_idx = -1
                for i in range(0, len(args_ivals)):
                    if isinstance(args_ivals[i], (Func, Sequence, Interval)):  # this list need to change, I'm sure
                        ival_idx = i
                        break
                if ival_idx != -1:
                    if ival_idx == 0:
                        parms, ivals = None, args_ivals
                    else:
                        parms, ivals = args_ivals[:ival_idx], args_ivals[ival_idx:]
                elif len(args_ivals) > 0:
                    parms, ivals = args_ivals, ()
                else:
                    parms, ivals = None, ()
                if parms is not None:
                    if isinstance(parms[-1], dict):
                        pre, kw = parms[:-1], parms[-1]  # empty pre means not positional args
                        if pre == ():
                            self.args = None
                            self.kwargs = kw
                        else:
                            self.args = pre
                            self.kwargs = kw
                    else:

                        self.args = parms
                        self.kwargs = None
                else:
                    self.args = None
                    self.kwargs = None
                self._func_ = func
                # self.ivals = [Func(self.wrapper), ]
                self.later_ivals = ivals
                self.do_continue = True
                self.append(Func(self.wrapper))

            def isPlaying(self):
                return self.do_continue or super(DynamicFuncSequence, self).isPlaying()

            def continue_running(self):
                if self.do_continue:
                    base.taskMgr.do_method_later(0, lambda x: self.start(), 'do_sequence_later')

            def wrapper(self):
                if self.args is not None and self.kwargs is not None:
                    self._func_(*((self, ) + self.args), **self.kwargs)
                elif self.kwargs is not None:
                    self._func_(*(self, ), **self.kwargs)
                else:
                    self._func_(*(self, ))

                if self.isPlaying():
                    self.continue_running()

            def quit(self):
                self.do_continue = False
                self.finish()
                if self.later_ivals:
                    seq = Sequence()
                    for i in range(0, len(self.later_ivals)):
                        seq.append(self.later_ivals[i])
                    base.taskMgr.do_method_later(0, lambda x: seq.start(), 'DynamicFuncSequence')
        # end DynamicFuncSequence

        run_count = 0
        def myFunc(*args, **kwargs):
            nonlocal run_count
            run_count += 1
            seq = args[0]
            notify.warning('myFunc count {} isPlaying {}'.format(run_count, seq.isPlaying()))
            if run_count == 10 and seq.isPlaying():
                notify.warning("run count hit")
                seq.quit()
                pass

        def beforeSeq():
            notify.warning("before all")

        def afterFinish(seq=None):
            notify.warning('after finish')


        dyn = DynamicFuncSequence(myFunc, 1, 2, {'k': 'k', 'w': 'w'}, Func(afterFinish))
        se = Sequence(Func(beforeSeq), dyn)
        se.start()

Ah, another piece of code to study more carefully later–thank you very much for that! :slight_smile:

Having given this further thought, I’ve pretty much settled on going in entirely the opposite direction: instead of attempting to make this function fit into Sequence and Parallels, I now intend to eschew those, and use by own version that operates on a “when it’s done” basis.

Not only will this (presumably) better fit with that method, but it theoretically allows for a wider range of actions to be carried out by the system (since it should be able to handle both “work until done” and “timed” actions), should allow for on-the-fly time-scaling if desired, and in general feels a bit more comfortable to me, I think.

That said, thank you very much to all of you who have given help in this thread–it is appreciated! :slight_smile: