Persistent Actors


I have a problem that has nothing todo especially with Panda, more with Python I think, but maybe others had the same problem already.

My scene consists of 100+ actors that should interact with each other. Currently I’m doing this with threads. Each actor has his own thread. Maybe that is the first problem because it does not scale well (especially in Python I think because of the GIL).
But my real problem is how can I save and load this world. Save all thread local variables is not a problem, but I also need to store/restore the “Program Counter” in each thread.
I’ve tried to reduce every actor to a state machine. That may work, but the end user should be able to write his own mini scripts.
Personally I think the main problem is that Python has no callcc construct or the like.

Thanks in advance for any idea.

using threads is really really bad idea especially 100 of them!

Why not simple keep actors in a list and update each one? If you need to update an actor once per second (thats what i do in my rts and then just interpolate their motions every frame). You can also update a small part of them per frame so that you would not have a big FPS spike all at one time.

Threads in python work fine - they just don’t work well over multi core thats where GIL comes in. What does not work fine is programmer hours spent in making sure every thing is thread safe. Debugging threaded code is also awful. You just dont want to get into that snake pit if you can avoid it.

Every actor has methods like OnHeartbeat, OnEnter, OnExit which may be overriden by the “end user” (Ideas are stolen from NWN1). This methods do never call IO routines, and more or less never talk to Panda3D (BTW does Panda release the GIL?), therefore everything is written in Python (that’s the reason why I care about the GIL on n-cores).


def doAnalyzeTree():

actor1.OnEnter = doAnalyzeTree
actor2.OnExit = doAnalyzeTree

If it only were movements and a predefined set of actions a state machine would be appropriate.

I don’t see a reason to use threads so far. NWN1 has not invented such pattern, they are as old as game programming itself, and they don’t required threads at all. You could have a look at the LP drivers and mudlibs (e.g. LDMud or Amylaar) for VERY GOOD examples of how to build complex worlds. working through a list of “calls” works fine.

I don’t know if Panda3D is 100% thread-safe, if not you will have problems if your agent threads modify the scene graph at a point of time when Panda3D is evaluating the scene graph. One way to make Panda3D thread safe (from Python) is to have two scene graphs. One is just for user (=Python) input, and the other one for evaluation. Once a frame all changes in the ‘user’ graph get copied to the ‘real’ graph, and from then on all changes made to the ‘user’ graph will have effect only for the next frame.
This would mean more memory consumption and a slight drop in performance for Panda3D. But maybe Panda3D already does it. drwr could tell us certainly.

Yes, it does, by using PyGILState_Ensure and PyGILState_Release (or PyThreadState if compiled with SIMPLE_THREAD). But this alone does not mean that Panda3D is 100% thread safe!

This wouldn’t be good enough, actually. There are some global caches and tables that Panda would keep updating as you modify either scene graph, so even if you’re mucking about in two unrelated scene graphs, you’ll get race conditions if you’re doing it at the same time in different threads.

There are only two safe ways to use threading in a Panda application. (1) Make all calls into Panda from only one thread. Your other threads may not access any Panda objects or call any Panda functions. You can’t even assign pointers to Panda objects, since the reference counting relies on thread protection! (2) Compile Panda with threading enabled, and use Panda’s thread constructs (instead of Python’s) to create and manage your threads. For instance, use the PythonThread object to create a new thread instead of the threading module. Use Panda’s Mutex object to protect critical sections instead of thread.mutex.

That being said, you might actually get away with not doing either of these, if all of your threads are Python threads, simply because of that darn GIL. The GIL means that Python threads are never truly parallel, and ensures that context switches happen only between Python instructions. So it might actually work after all.

Still, treeform and enn0x are right. Threading is a terrible solution to this particular problem. Much better to have functions that work a while then return, sort of like Panda’s task manager.



Ok thanks for this insight (and for the links and ideas). But maybe I show here how NWN1 does it. In NWN1 every actor has an action queue. Sure there is not a thread for every actor in NWN (sorry if I not have written this more clearly).
An event e.g. can be a function like this:

AssignCommand(oActor, ActionSpeakString(“Alpha”));
AssignCommand(oActor, ActionMoveToObject(GetWaypointByTag(“Beta”)));
AssignCommand(oActor, ActionSpeakString(“Gamma”));
AssignCommand(oActor, ActionWait(5.0));
AssignCommand(oActor, ActionJumpToObject(GetWaypointByTag(“Delta”)));
AssignCommand(oActor, ActionDoCommand(MyOwnFunction()));

If I only support the first 5 commands, I’m able to create a nice solution, with one thread including the possibility to load/save. Problem is this generic ActionDoCommand that can do everything (but maybe NWN1 can’t save till the function ends, that I don’t know).

My server process over 400+ at the same time each with an order queue - the simple loop and process has worked just fine so far.

“If I only support the first 5 commands” - why stack is a stack - i would just keep it infinite. Load/save should not be part of this. Load and save threads? You just need to save the data.

Are you coding panda3d with NWN1 ? Are you interfacing the systems in some way?

Why you have to have “ActionDoCommand(MyOwnFunction())”
why just override the ActionDoCommand class and add your own stuff in there.

Actually, if I read his post correctly, I think that he’s worried not about the number of commands in the queue, but about the use of user-defined, and thus unknown, commands.

Azraiyl, am I correct? If so, am I also correct in thinking that your main concern is that the unknown method might be one that takes long enough that executing it entirely in one update might produce a noticeable delay?

If that’s the case, then I suggest that any threading should perhaps be handled by the user that intends to use so long-running a method. I honestly don’t think that you should employ threads (let alone that many!) in case the user passes a long-running method.

Yes Thaumaturge you’re right (sorry maybe it’s my english that is so worse that nobody understands the problem).
I don’t code for NWN1, NWN1 is only something where I think I can steal ideas from (I’m not a NWN1 guru). I really don’t care about the threads and queues at all. Maybe you don’t believe it, but it also works quite nice with 100 threads (but I’ll change it based on your feedback). My Problem is if there is queue that may execute a user defined function, the function can be complex (e.g. recursive). If anyone likes to save the whole world, I also need to store/restore the stack frame of this function (threads or not does not matter). It’s not me that is writing this user defined function, it’s someone else. Possiblity 1: There are no restrictions for this function (that’s what I’m trying to do) Possibility 2: There are some limitations (then it degrades to a problem that is not that hard to solve).

In this case maybe tasklet (or simply yield()) are good options for you.

Basically it ensures that the custom user function will be called and execute a few steps, then give back the hand then be called again and resume where left

You have some great example of it in “Greenlet” or in “Kamaelia”

If you need to write your function with yield you can also write it as a state machine. The problem is that inside your generator you may use a recursive function. It’s another form of cooperative multitasking.

Anyway thanks for the link to “Kamaelia”. They also have a very nice tutorial:

If you talk about tasklets from stackless python that would be a good idea indeed. But AFAIK only EvE Online uses Stackless Python (with success). I never tried to recompile Panda nor do I know if it is possible to replace the default interpreter without rewriting too much.