Game Development Community

dev|Pro Game Development Curriculum

Python: T3D Console Data Model

by Demolishun · 12/28/2011 (11:23 am) · 7 comments

This is part of my series of coding blogs surrounding my trek through learning how to extend Python with T3D using SWIG. My goal is to present interesting coding subjects as I make my way through the process.

Today's edition deals with how to abstract console objects, variables, and methods. I spent a lot of time trying to figure out how to extend the attribute methods of a Python object, only to find a much cleaner and workable solution by using container methods.

What was I trying to solve?
I wanted to be able to access console methods, variables, and objects with functions like this:
static SimObject * FindObject(S32 param);
static SimObject * FindObject(const char *param);
static const char* GetVariable(const char *name);
static bool SetVariable(const char* name, const char* value);
static bool IsFunction(const char *nameSpace, const char *name);
static const char* Evaluate(const char* code);
Those functions still need to exist, but I wanted to do this:
pyT3D.Sim().function()
val = pyT3D.Sim().globalvar
pyT3D.Sim().globalvar = 5
I wanted a more natural interface to the objects. So I set out using __getattribute__ and __setattr__ methods. Those methods are designed to allow access object using dot (.) operators. However, after actually implementing those and figuring out how to actually make those work I found my concept failed with __setattr__. The reason is because this is mechanism used to set variables for the object itself. If I give precedence to the objects default methods it will set the values for the object, if I give precedence to the console methods then it will set the console variables. The problem is the Python object must be able to set attributes on itself to work properly. Python objects are highly dynamic which is very similar to the dynamic nature of console objects. So the two methods are mutually exclusive, damn it!

At that point I pouted, swore, kicked some things. I had spent hours trying to understand how to use the
__getattribute__ and __setattr__ methods properly. My getattribute function worked perfectly, but conceptually the method was to fail. Then I started looking at all the different methods built into python objects here. That is when I found __setitem__ and __getitem__. These would allow me to use my object like a container that uses strings for lookups! What a perfect match! So in about 10 minutes I had tweaked the code, removed all the junk needed to make the other methods work, and I had working abstraction for my console objects for both set and get methods:
pyT3D.Sim()["function"]()
val = pyT3D.Sim()["globalvar"]
pyT3D.Sim()["globalvar"] = 5

Why are these methods better? Because I can use any string that is a valid console syntax. With the dot (.) operators I could not do that. Python has certain characters that cannot be used for variable names such as "::","$", and "%". The console relies on those. With the container methods I can do this:
pyT3D.Sim()["$fps::missiontime"] = "10:00"
I could not do that with the dot (.) operators.

So to give you an idea of what part of the swig code looks like:
%pythoncode %{
def BuildExecString(objName,name,*args):
	targs = ""
	amax = len(args)
	for i in range(amax):
		arg = args[i]
		targs += '"'+str(arg)+'"'
		if(i < amax-1):
			targs += ","
	if objName is not None:
		fcall = "return "+objName+"."+name+"("+targs+");"
	else:
		fcall = "return "+name+"("+targs+");"
		
	return fcall
%}

%rename("Sim") "SimObjs";
class SimObjs {
public:
	static SimObject * FindObject(S32 param);
	static SimObject * FindObject(const char *param);
	static const char* GetVariable(const char *name);
	static bool SetVariable(const char* name, const char* value);
	static bool IsFunction(const char *nameSpace, const char *name);
	static const char* Evaluate(const char* code);
	
	%pythoncode %{
	def __setitem__(self, key, value):		
		setattr = Sim.SetVariable(key, value)
		if setattr:
			return
			
		raise AttributeError(key,"is not a valid console variable.")
	
	def __getitem__(self, key):
		key = str(key)
		
		# find attribute in console if possible
		sobj = Sim.FindObject(key)
		if sobj is not None:
			return sobj
		svar = Sim.GetVariable(key)
		if len(svar):
			return svar
		
		if "::" in key:
			fname = key.split("::")
		elif "." in key:
			fname = key.split(".")
		else:
			fname = [key]
		if len(fname) == 1:
			fbool = Sim.IsFunction(None,fname[0])
			if fbool:
				def tfunc(*args):
					#print "function call:",fname[0]
					fcall = BuildExecString(None,fname[0],*args)
					ret = Sim.Evaluate(fcall)
					return ret	
				return tfunc
		elif len(fname) == 2:
			fbool = Sim.IsFunction(fname[0],fname[1])
			if fbool:
				def tfunc(*args):
					#print "function call:",fname[0]+"."+fname[1]
					fcall = BuildExecString(fname[0],fname[1],*args)
					ret = Sim.Evaluate(fcall)
					return ret	
				return tfunc
				
		# if an attribute cannot be found
		raise AttributeError(key,"is not a valid console variable.")

	def __str__(self):
		return "Sim interface for console methods."
	%}	
};
At the beginning of this code is a Python function called BuildExecString. This helps me build a wrapper function for console functions. The idea is I can feed it a function name and an arg list and it will build a string to be exec'd by the Con::evaluate function. This has been the most reliable way that I can find to call console functions.

The next part is where I define the Sim class. I had to rename another class to keep it from colliding with the Sim namespace in Torque itself. Inside the Sim class I have some static methods to do the various interfacing to the console. They are pretty self descriptive.

Then I define the __setitem__ and __getitem__ methods using Python code. It is more efficient coding wise and brain cell wise to implement these methods in Python. The setitem method uses SetVariable at it's core. This function only affects variables, not objects or functions. The SetVariable function actually returns a boolean which is True if the variable was actually set. The getitem method first searches for a SimObject, then a variable, and finally a function. It will return the objects/values if found.

What is interesting about the function return is there is really no object or value to return, but it must return a callable Python object or Python function to be able to executed. So I create a Python function on the fly, use the BuildExecString to build an executable console string, embed that data in the function, and return a reference to that newly created function. When the function is called it will use the Evaluate method to execute the function with its parameters already built into the string. That is one thing I love about Python. You can just create functions out of thin air! This is great for abstracting data interfaces without a Python compatible object model.

Well that is it for this segment of brain damage. Stay tuned for more exciting data abstraction!

About the author

I love programming, I love programming things that go click, whirr, boom. For organized T3D Links visit: http://demolishun.com/?page_id=67


#1
12/29/2011 (1:50 am)
Cool stuff, Frank!
#2
12/29/2011 (9:31 am)
Though I understand little of what's going on, I can't wait to hear more about this!
#3
12/29/2011 (2:37 pm)
@Tuomas,
Thanks!
@dB,
I know, this is a lot to take in out of context. I really am trying to get the code to the point to where I can create a new Python resource. The code is just not ready yet. I am getting closer, but just not quite there. I don't want to put something out there that is half arsed.

Part of my slowness on this is understanding what is really needed and providing a robust approach. What I would hate to do is put out code and people start to use it, then change the API on them in the next release. This is much more complex than I originally imagined, but I have learned so much by going this route. I am being forced to dive into the internals of SWIG, Python and T3D.

In addition I am planning on creating some awesome examples of how to use the interface. It is like dancing sugarplums when I think of the possibilities. One thing I found the other day is a library that allows you to take midi files and generate wave outputs on the fly. What I would hope to do is create a way to transfer this binary data from Python to T3D to be used like a regular sound resource. This would allow the potential of generating the midi itself based upon events in game and then create the sound data. Then T3D can use that data. So music scores could be completely created while playing the game. You could even stream midi from an external source as well. This is just one example of what I want to make possible. Leverage the huge Python code base to do this without having to graft libraries to T3D itself.

Back to this blog. Part of the reason I am showing this information is because it is just cool and fun, but the other reason is if someone sees a better way they can speak up or use my way if it is better. I think we can all benefit from this kind of exchange.

Thanks for taking an interest!
#4
01/17/2012 (5:08 pm)
Instead of trying to wrap ConsoleFunctions in another layer of indirection, perhaps you could expose them directly to your SWIG interface by playing with the macros used to create them.
#5
01/27/2012 (9:45 am)
@Thomas,
That is a neat idea. I will have to look at that.
#6
01/28/2012 (3:16 pm)
@Thomas,
I am now calling the Con::execute call rather than the Con::evaluate call for most things. Is that still calling the console function?

I am not sure I can get around that if I supply all my args as strings and need to resolve simobject IDs and such. Although for most stuff I am now resolving simobject IDs and using the actual object in the call.

Is there a way to resolve the call to the actual C++ call and put that in a lookup? I am not sure there is a way to get around using the console resolution process, as how do you find and call pure console functions?
#7
01/28/2012 (7:39 pm)
Console functions (and methods) that are written in C++ are exposed as normal C++ functions. Those written in script are called via Con::execute, but you should be able to do some metaprogramming to allow you to call those normally as well.