One man's blunderings into the 3D realm
06 May 2012 -
In my last post I showed how to attach a Python object to a node path in order to create a ‘hook’ in the Panda3D scene graph. In this post I’ll be showing how to dynamically add additional code to that object at runtime. Attaching code to objects in this way will be at the core of offering drag and drop scripting functionality in the same manner as Unity.
Once we have the PandaObject the next thing we could do is to subclass it in order to add additional code. Since I’m building an editor I want to offer the user an easy way to add, remove and combine different scripts for a node path, so in this case we’ll use the PandaObject as a hook only and then ‘hang’ other scripts from it.
In the context of our editor, the user will presented with a file browser displaying all the scripts in their project. This hierarchy will be representative of the directory structure on disk, and the user should be able to drag and drop any script onto any node in the scene. This presents an interesting problem as essentially we need to be able to instantiate a class from a file path the user selects at runtime. Thankfully python’s imp module offers some very handy tools for solving this kind of problem.
So now our PandaObject code looks like this:
Note the two new methods, AttachScript() and DetachScript(). We will use these to add and remove additional code to our PandaObject. Code to be added will be its own class which I will refer to from here as a ‘behaviour’.
Behaviours are attached using the AttachScript method which takes a path to a python script as an argument. By using Python’s imp module we can import a module using a string, and it doesn’t have to be found on sys.path either. There is one caveat though – imp.find_module will only take a file name as its first argument, absolute or relative paths will not work. As I want this to handle relative paths I have to build a list of search paths to pass to find_module by joining the directory of the script to each of the sys.paths.
There is another restriction in that the name of the behaviour’s class must be named an uppercase leading match of the file that it lives in. While this goes against my ideas of dictating how the developer approaches their project, I don’t think it’s too restrictive and also matches the Python / Unity paradigm of naming classes in accordance to the files they live in.
Once we have found the class name we create an instance of it with the PandaObject’s node path as an argument. Any behaviour we add is probably going to modify the node path in some way, so we’ll need to pass this through. All instances are then stored in the PandaObject’s instances dictionary – something I’ll probably end up changing as currently it is not possible to attach more than one of the same type (named) of behaviour to a PandaObject.
We can remove instances that are attached to our PandaObject by passing the class name to DetachScript(). I’ve added a call to ignoreAll() for any behaviours that are inherited from DirectObject as they won’t be garbage collected without removing their reference from the messenger first.
Now to create some kind of basic behaviour. The following code will move the node path slowly up and down when started:
Nothing too special going on here. We store the input node path and bind some events when the class is instantiated, and there’s some basic wrapping of the task manager going on there too.
Now to bring the whole thing together. We start by loading the default box model, parenting it under render and attaching a PandaObject. We then pull that object back out of the scene graph in a different scope and attach the behaviour to it by passing the file path to the script:
Running the above code should show a box that floats up and down. To remove the node cleanly we have to make sure we stop the task running first, then we can remove it like normal:
So while this seems overly complex in order to get a box to move, you can imagine how neat this works when attached to a script file browser. By binding some drag and drop events we can easily attach the user’s script to any node path in the scene.