Game event scripts. Separate thread?

I have simple “event scripts” which are basically commands which are used for loading and animating 3d models, handling text, audio, camera animations. They are used for the game’s semi-interactive cutscenes.
Although the scripts aren’t Python, each command is mapped to a corresponding Python function.
A pseudocode example would be:

play music
fade screen
wait 3 seconds
Mylabel:
play animation1
play animation2
goto Mylabel

this is just an example really. The point is this commands must be parsed one after another, jump to a line for goto’s and sometimes not go to the next line of the script for some amount of time.
As i said in the end corresponding Python functions are called, so the above example could be written with Python (I’m thinking of allowing Python together with out toy scripting language).

Right until now I had the “interpreter/executor” as a task. As a task is ran only when a frame is rendered and it can usually only run 60 lines per second, it quickly proved itself as a bad idea.
I’m asking, how else can this be done? A simple while True: loop for the "interpreter/executor will freeze the rest of the game and all of it’s processes. So what can I do? Should I look into threading? How does threading go with Panda’s own processes?

Finally, lets say that I used Python in the event scripts, would I read each line of the file and do exec(line)? because if I would just run it as a python module I don’t see how I could use Wait() without freezing everything else, or jump from one line to another in the script. Wrapping the whole script in a Sequence also doesn’t seem like an option for the reasons given above.

This is the kind of thing that Intervals were designed to handle.

David

Like I said intervals don’t seem to be an option.
Firstly, the script commands are run one after another, in other words the next command is ran after the previous one is called, not finished. Of course I could just use Func() function intervals then, but there are other cases which don’t seem to be possible. For example, there might be cases when a command “pauses” the script for an uncertain amount of time. I’m not sure if pause() and resume() would be very accurate here. Even then, I have “goto” commands which jump from one line to another (in this case from one interval to another inside a sequence). I see no way to change the “index pointer” with Intervals/Sequences.

So, are you looking for a sort of “parallel” computation? In that case, parallels are there.

Here the problem isn’t the lack of support by Panda, the goto request is (obviously I’m kidding, I hope goto is an external request in your case).

I think you don’t really understand what I’m trying to do.

From what you’ve described so far, preusser, I too can’t see why Sequences/Parallels/Intervals/LerpIntervals wouldn’t work for you…

So you can “jump” from one interval to another in a Sequence?

What exactly do you mean by jumping? If you could elaborate a little more on what you need that “jumping” for (and thus what it would work like), then it would be easier to say if it’s possible or not.

Granted, sequences and their execution can’t be modified on the fly (and I guess that’s what you want), but there is no reason you’d want to do that. In fact, there are reasons why you shouldn’t want that.

If, for instance (just an example, I don’t know if that’s what you’re trying to make), you wanted to make an RPG-style dialog reply selection during a cut-scene, then FSMs are meant for that.

You would make Sequences for animations and wrap them with an FSM for managing responses and what runs when. Well designed, this setup can be very nice to work with. I think it’s a standard way of doing such things.

Plus, FSMs can wait an arbitrary time before changing states (like waiting for user input), obviously, so you don’t need to know the timing here. Combining FSMs with Sequences and possibly Tasks, you can achieve practically everything.

I guess I should add more info.
What I have is a simple scripting “language”. It’s not really that simple as the example, there are conditionals, gotos and other statemets.
I have a parser for that scripting language which creates a list of lists from the script files. I have all this done.
The data generated by the parser is then passed to the “execution engine”, which is a function which should run as fast as it can, but without interfering with the rest of the game tasks. I could just wrap it inside a while True: executeFunction, but it would freeze everything else.

Here is the “execution engine”:

def executorTask(script_code, task):
	if index_pointer < len(script_code):
		if waiting == False:
			instruction = script_code[index_pointer]
			commandsDict[instruction[0]](instruction[1:])
			return task.cont
	return task.done

The index_pointer is used to tell which “command” will run. The commandsDict is a dictionary which maps a “command” from the language to a python function. The instruction[1:] allows to call the corresponding python function and pass it the required arguments.
“goto” command simply changes the index_pointer.
Most commands increment the index pointer. Some leave it unchanged or modify it later (like wait() starts a timer and sets waiting to True)

My question is, is task good for this? I would want to run the function as fast as possible, not only every frame.

PS. I know I could go with Python as a scripting language, but like I said before we are working on a “replacement” engine for an existing game, so we haven’t created this “language”, but we need to support it to make the new engine work with the game out of the box.

Ah, if it wasn’t for the last sentence, I would ask why the heck did you add a “goto” to your language in the first place…

Anyway, this is a rather unusual need. Still, Tasks should do the trick of allowing you to control Panda from your custom scripts, but obviously they will only run as fast as your game runs (or at fixed speed below that).

If you want to run the code “as fast as possible” then you will need threading, I don’t see any other way. Although, that would be a bumpy road, I guess.

The other question that arises, though, is why do you need that code to run faster than your rendering? It doesn’t make sense to me. From what I understand, this “higher then Python”-level language is meant for controlling animation, music and so on (according to your example in the first post). If so, then running it faster than the rendering makes no sense at all, as the results of commands being run between frames will not be visible to the user. Not to mention you would probably run into sync problems.

Actually I think for pretty short “event” scripts like these a “goto” is not so bad, though it does make longer code pretty pain to read.

Hm, we could try a doMethodLater task with ridiculously small number value, so the PC would run it “as fast as it can”. That’s how the doMethodLater would behave in such situation, right?

I tried to put a while loop in the function and wrap that function in a thread and start() it, but it actually seemed to run alot slower than a task.
I’ve never used threads, so it’s very likely I’m missing something obvious.

Well that’s it, it’s not just commands for audio/video. There are commands for setting and evaluating “built-in” variables. there are scripts that set values to 100 variables one after another, thats more than a second lost. Also, I don’t know how panda works internally, but there can be commands which create buttons or other GUI stuff. I don’t think in Panda creating a DirectButton takes up 1 frame.

Nope. DoMethodLater will not run faster than your rendering (and I’m still certain you don’t need nor want that in this very case). By “at fixed speed below that” I meant it will run “as fast or slower” than your rendering. E.g. if you have your game running at 60Hz and you set a doMethodLater task to 20Hz, then it will run at 20Hz; but if you set it to 120Hz, then it will run at 60Hz because that’s the “native speed” of your game.

At least that’s what my experience suggests.

Um, ok, I think I’m starting to understand what the actual problem is. I should’ve noticed that in your last code.

The problem is, you’re trying to run it on a “one frame – one command” basis. This will surely not work as it indeed slows the execution speed to a stop.

However, if the code you’re talking about (in that custom scripting language) was used in a game before, then it must’ve been designed to run “frame-by-frame”. That’s how all games work.

Um… you don’t have actual functions in that custom lang, huh? There’s only gotos? If so, then it’s a serious “programming WTF”…

You should be able to analyze this code and identify those parts that are meant to be run only once – at the game’s start or exit; those that are essentially tasks (i.e. meant to run frame by frame); and ones that react to input (which might be implemented in a couple of ways at this level, but all should be possible to wrap with Python and Panda stuff).

You should also be able to identify those parts of the code that essentially form a function. That is, belong with each other. I’m guessing (and only that) it might be done as “from one goto to another”. Or, possibly, you have multiple script files that serve this purpose? It’s difficult to guess, but it must’ve been done somehow.

Once you have that done, you can then run ALL the code from a “function” (or multiple “functions” meant to run in parallel, if that ever happens in this language) in ONE task pass. You’d do this with a loop put INSIDE the task.

This is the only way I see that as possible.

To be honest, I’m a little befuddled by what you’re describing… How old is the code you’re trying to port, if you don’t mind me asking? Also, how the hell did it work previously?

Then it’s not what we need.

Why do you think like that? Some commands correspond to a Python code like this:

a = 1

You think in the original game such commands were executed only 1 per frame? I highly doubt that.

I have no problem here. Like I said I have implemented the goto statement and all of the commands already. I have a working executor which is based on Task. My question is simply what else can I use instead of tasks? Because tasks don’t seem to be good for this purpose, as I already explained.

That’s not what I wrote. I’m not talking about “one command per frame”. I’m talking about that games run in frames. In the task that you’ve wrote, you have multiple “command” executed during one frame, but the game runs “in frames”. I have no idea how to explain it better.

The main loop is what controls the execution of the program. In one frame you can run as many functions as you like, but running them between frames (what you seem to be talking about all the time) is pointless and impossible.

Sorry for being direct but I’ve got a feeling you don’t really know what you want to do…

The biggest problem is that you think of the code you’re trying to work with as a “fluent stream of commands” of some sort. It doesn’t and it can’t work like this.

As for Tasks, they’re like loops. Only instead of executing one pass after another, their instructions kinda get added into the main loop.

So it works like this:

mainLoop:
  your task pass
  ...
  panda renderer pass

That’s why you can’t execute your code the way you’ve tried doing it, nor can you use anything other than Tasks.

I explained it in my previous post. You should identify the chunks of your scripts that belong together and execute those chunks (i.e. multiple commands) in one task pass.

So you don’t execute it “command by frame”, but “many commands by frame”.

I have no idea how to explain it better…

Or maybe thats not what I’m talking about :confused:

Well, you want it to execute irrespective to the rendering speed and outside the main loop. That’s what I meant by “between frames”. And you cannot control your game that way.

As I said, this script code of yours must be somehow connected to frames. It must, there is no other way. But that’s not the same as “every command is one frame”. You should identify groups of code that belong together and should be executed together, and execute these groups (as a whole) one group per frame.

Thus, you will need to have one task per group (in the simplest scenario) and in each task have a loop through this group’s command lines.

Depending on how it worked in the previous game, these groups might be whole script files or sections between labels and goto statements.

That’s the only way I understand what you’re trying to get to.

“impossible” as in completely inefficient?

from pandac.PandaModules import *
loadPrcFileData("", "show-frame-rate-meter #t")
import direct.directbase.DirectStart

import threading

env = loader.loadModel('environment')
env.reparentTo(render)

panda = loader.loadModel('panda')
panda.reparentTo(render)

def myfunc():
	while True:
		panda.setH(panda.getH() + 0.0001)

mythread = threading.Thread(target = myfunc).start()

run()

hm, I have to think some more on this, but I think a whole script should be executed that way. I mean the “commands” are not like panda intervals, where the next one is called only after the first one has “finished”.
So if you have

play anim1
play anim2

then the second animation wont start playing only after the first one has finished playing.

Your code snippet illustrates an attempt to use threading to solve this problem. Threading is one of those concepts that sounds trivially simple until you actually try to use it, then discover it bitches all sorts of assumptions you’re used to making about coding, and causes your well-designed program to fail in mysterious and difficult-to-fix ways.

You can use threading, but you really have to know what you’re doing. Universities spend multiple semesters to teach students how to use threading properly.

Your code snippet will fail with the default build of Panda. If you really want it to work as written, you can get Panda from source and build it with SIMPLE_THREADS disabled, which will allow you to use Python threading constructs normally. Then the code snippet will work mostly as intended, except that you may be surprised at how much time your child thread spends simply updating setH() thousands of times per second. And when your code gets more complex, you may start running into the classic problems of threading, namely race conditions and deadlock. And your overall performance will degrade about 10% as Panda will be required to lock and unlock low-level primitives at each operation. Furthermore, the Python language itself doesn’t really support threading in the traditional sense (despite constructs that appear to support it).

Fortunately, the default build of Panda provides a threading model that mostly avoids these problems; and that’s the SIMPLE_THREADS model. It only requires a slight adjustment to your source code; you have to use stdpy.threading instead of threading, and you have to insert a call to Thread.considerYield() inside your myfunc() loop. SIMPLE_THREADS uses a cooperative threading model which avoids most problems with race conditions and deadlock, because the context switch only occurs in well-defined places. Still, it’s not ideal for what you’re trying to do.

The best way to do what you’re trying to do is to wrap your head around the frame-driven model. It’s not really that confusing, and you can indeed do everything you’re describing with a normal Panda task. And if you do it this way, it will be the most reliable and most efficient way to do it.

But you’re welcome to write your program however you like, of course. If you wish to pursue threading, let me not stand in your way. Panda provides two different threading models for your use. Good luck!

David

No, impossible as in impossible. Frames are what defines when things get shown. Frames are when Panda gathers input from the user. Any action you do is started in some frame and finished in some frame. If you disconnect yourself from that, you will hurt yourself.

Control loop is there to control things. Your code is there to control what the control loop does. Frames (passes of the control loop) are your points of reference.

Of course they’re not. They’re just initiators. They’re… well, commands.

If, for instance, I write this code in Python:

LerpPosInterval(...).start()
self.printHelloWorld()

Then the “hello world” will get printed immediately – without waiting for the interval to finish. Why? Because this is just the code that tells the main loop what you want it to do. This is what I wrote about before.

The first like tells the main loop to add a specific animation (position animation) to the main loop and process it there. But the Python interpreter then jumps immediately to the next line. That’s because the first line isn’t the actual interval – it’s just an order for Panda to start doing an interval in the next frame. And giving that order takes practically no time at all.

So how can I make it wait? Well, like this:

Sequence(
    LerpPosInterval(...),
    Func(self.printHelloWorld),
).start()

self.immediate()

This code (creating a Sequence) tells the main loop this:
I give you an interval and a function. I want you to start the interval now, process it in subsequent frames and run the function (printHelloWorld) as soon as the interval is finished.

Note, however, that the self.immediate() function will be executed immediately, without waiting for the interval or even the whole sequence to finish. This is because it’s not added to the main loop – it’s just executed in place.

AND NOW THE MOST IMPORTANT THING

You ought to interpret the language in Tasks containing Loops (as I explained in the previous post) and THEN translate that into Panda constructs.

So if you have this:

play anim1
play anim2

Then your interpreter should react to it by creating a Sequence of those animations. Do you understand?

It seems to run with the buildbot version, i added your changes and I didnt notice any difference.

I spend some time thinking about this and youre right, threading seems pointless here. I could add a while loop in the task function which checks if the upcoming “command” is one which modifies ‘waiting’ state and “break” from the loop if so.

EDIT: I didn’t notice your reply coppertop. Yes, I meant intervals inside Sequences/Parallels. And no I don’t need to put the commands inside sequences as intervals, the complete opposite. Sorry for the confusion.