Faking OOPS in MEL with pseudo-classes

MEL is a great scripting language, and can do almost anything that you need to do, but it is after all, a scripting language, and as such lacks many of the features common to modern high-level programming languages. For the most part, it doesn't matter all that much, since MEL is still really powerful, but there is a kind of glass cieling that can show up when tackling complex problems. For example, you might find yourself wanting access to object-orientated practices in the following situations:

In high-level programming languages such as C++ and Java, object-orientated programming practices break down complex problems into more manageable pieces. MEL doesn't allow object orientated pratices per say, but it is possible to simulate some object-orientated pratices with a bit of juggling.

Making Objects

One of the main tenets of object-orientated programming pratice is the creation of objects- bits of software that combine both data and methods that operate on the data. Typically, the implementation of an object has two parts- the declaration of the object, and the creation of instances. Declaring (or defining) an object requires that the number and type of data members be specified, and that any methods residing on the object must be defined. Once that has been done, any number of specific instances of the object can be declared and used to perform useful work.

Declaring/defining an object usually looks something like the following:

class myClass():
	// declare some data
	int myNumber
	float myFloat
	string myName

	// declare some methods
	void doSomething(void)
	int calculateSomething(float input)

Once we've declared the class, any number of instances can be created and initialized, as in:

// create an instance of the "myClass" class, named object1
myClass object1 = myClass()
object1.myNumber = 3;
object1.doSomething()

The above example assumes that the data within the class is public- we'll keep to that assumption for now- more on public vs private later. While the syntax changes from language to language, class declarations generally: So what if we want to create a new class within MEL? The most analagous concept to a class type in MEL/Maya is that of a node. Maya ships with many, many different flavors of nodes, from transforms and shapes to more exotic beasts. Any given Maya scene is made up of some number of nodes, connected together in the DAG (Directed Acyclic Graph). Maya works its magic by constructing various nodes (and connecting them) in response to user input. So it would seem that the best way to go about defining a new class would be to define a new node type.

...BUT...

...you can't.

Or rather, you can't define a new node type with MEL. For that, you have to migrate over to using C++ and the Maya API. And that's fine- using the API is extremely powerful way to go, but with great power comes great complexity. So let's say that you want to define custom objects, but all the while staying within the MEL domain. You can do that by extending one of Maya's built-in node types via adding custom attributes.

"But that's not really defining a new class", you say. Well, that could be said to be true if you really want a class that is a stand alone class, inheriting from nothing. But that's not necessarily all that desireable. Just as all Java classes ultimately inherit from java.lang.Object, so too must all MEL psuedo-classes "inherit" from some other node. So the question becomes one of which node type to use as the basis for your pseudo-class. I would argue that for most purposes, starting out with the transform node type is a good way to go.

Why? Well, there's not much to a transform node, so there's not a lot of uneeded extras. Also, transform nodes can take advantage of parenting to give you tree-based data structures without any additional work on your part. Also, if you're working on a script that is to manipulate geometry, you'll be working with the object's transform node anyway. Naturally, feel free to experiment with using other node types as the basis for pseudo-classes- consult Maya's node reference for an exhaustive list of node types.

Creating the class

Creating a pseudo-class therefore starts with creating a node of the chosen type. If it's a transform node we're after, we could do that by creating an empty group. However, that's specific to a transform node, and there may be times when it's useful to start with a different kind of node. The general-purpose way to go about it, allowing for transform nodes as well as any of Maya's other nodes is to use the createNode command, as in:

createNode transform;

Any of Maya's node types can be created in that way, and (as a very useful bonus feature) it returns the name of the newly created node. So in order to emulate the functionality of a typical constructor, one can create a procedure that has the name of the class and spits out the name of the created node for future reference, as in:

// class constructor
global proc string myClass()
{
	// create an instance of the class by making a new node
	string $name = `createNode transform`;

	// add in data members and methods to the new instance

	return $name;
}

Given something like the above, an instance of the class can be created in the following way:

string $instance = myClass();

Note that no matter what kind of class you're using as the basis, the instance name ($instance in the above), must be of type string, since it will be holding the name of the newly created class.

Adding in data members

So far, all we've really done is to create a transform node in a particulary roundabout manner. In order to really make use of the new node as a custom class, we'll need to add in both data members and methods. We'll get to methods in a bit, but for now let's start with the easier of the two tasks - adding in custom data.

Adding in custom data is pretty straightforward- all you have to do is to use the addAttr command to add in a slot for a given type of data. So let's say that we want to add in an age attribute to myClass- we can do that with addAttr after having created the node, as in:

global proc string myClass()
{
	string $name = `createNode transform`;

	// add in the age attribute as a 8-bit integer
	addAttr -longName count -attributeType byte -keyable 1;

	return $name;
}

Note the use of the -keyable flag in the above line. That will ensure that the newly-added attribute is displayed in the channel box, which is a good idea, at least while you're getting a script up and running. If the script is up and running and you want to keep it hidden from the user, you can use the -hidden flag to hide it from the UI. Note also that the attribute type is given as byte, not integer. The addAttr command offers many options for data type, with the old MEL standbys of int, float, and string splitting into various flavors, including muli-part varieties. For details on the specific options, be sure to look at the documentation for the command.

Accessing data members

Once you have data members present on a node, you need to have a way to easily access them. With typical high-level languages such as C++ and java, the syntax would be something like:

// create an instance of the myClass class
instance = myClass();
// set its age attribute to 27 (assuming public data)
instance.count = 0;
// retrieve the value
int theCount = instance.count;

When creating psuedo-classes with MEL, you have to go about things in a slightly different way. Retriving the value of a data member can be accomplished easily enough by using the getAttr command, as in (assuming that we have the name of the node stored in $instance, and we want the count attribute):

// create an instance
string $instance = myClass();
// retrieve the value of the count attribute
int $theCount = `getAttr ($instance + ".count")`;

Setting the value of data members is done similarly, with the setAttr command. To set the count in the above example, one could do the following:

setAttr ($instance + ".count") 0;

The above might not be quite as straightforward as C or java, but it's still fairly straightforward, with both setting and accessing data members being accomplished with a single line. The only (slightly) tricky thing is the combination of a string variable (holding the name of the created node) and a string literal (the name of the attribute to access). Don't forget to add in a dot to the literal (the quoted string) to connect the node and attribute names.

Adding in methods

So far, we've looked at creating pseudo-classes with MEL and adding in custom data members. In order to create useful classes, we'll also need to add in methods (procedures, functions- pick your term) to the class. That's also possible, but requires a bit of juggling. What we need to do is to find a way to associate some code with the pseudo-object in such a way that it can be invoked at will.

This is one of the many times that the ability of MEL to be self-referential comes in extremely handy. Since MEL is never compiled, the code remains as just text throughout. And any kind of text can be stored into a string variable. So, functions that are to be associated with a given pseudo-class can be implemented as string varibles that have been added. So let's say that you want to have a function that prints some text to the console. That would mean having code along the following lines:

print ("Hello World");

In order to store the above code as a member of the pseudo-class, it has to be stored as a string, which means escaping certain characters, most notably the quotes. So print ("Hello World"); becomes:

string $greeting = "print (\"Hello World\");";

The end of the above line might look a bit strange, as there seems to be two semicolons right after one another. While that looks odd, it's important to include, since the first one is included in the command.

Adding the above procedure into the pseudo-class takes a couple of steps. First, we have to add in a string variable to the node, as in:

addAttr -longName greeting -dataType "string";

That gives us a string variable resting on the node. Note that string is in quotes, since it's a MEL keyword rather than a primitive data type. Setting the value of the new variable is most easily done with a separate setAttr command, as in:

setAttr -type "string" ($name+".greeting") $greeting;

Note that when setting the value of a string variable with setAttr, it's necessary to include -type "string". This is because the familiar int, float, and string aren't actually data types of thier own. Instead, they are aliases for several different data types. Int, for example, really includes byte (8 bit), short (16 bit), and long (32 bit). There aren't many situations where it's all that apparent, but it comes up when adding attributes. Consult the documentation for the addAttr command for a complete list.

Invoking methods

Once you have a pseudo-method on the pseudo-class, you need a way of invoking it. Ideally, we'd be able to do something along the lines of:

instance.greeting();

However, that won't work with the pseudo-class. What we need is a way to have Maya take the contents of the attribute and execute it. That means we'll need a two-part solution. First, we'll use the getAttr command to store the contents of the method string variable, as in:

string $temp = `getAttr ($instance+".greeting")`;

Once we've done that, we can use the eval command to have Maya execute the code, as in:

eval($temp);

While that seems a bit roundabout, we can make it a little easier if we compress it into a single line, as in the following:

eval(`getAttr ($instance+".greeting")`);

Methods that alter internal data

Its all well and good to have methods residing on the pseudo-class and invoke them, but in order to have them do useful work, we'll often need them to alter the internal data on the class. Let's say that we want our class to have an increment method that adds one to the internal count variable. The code ends up needing to look like:

int $temp = `getAttr ("transform1.count")`;
$temp += 1;
setAttr "transform1.count" $temp;

"Transform1" needs to be handled a bit differently from the rest of the code, since it needs to be the name of the specific instance that the method is sitting on. So we find ourselves with a situation where its necessary to generate code and have some key components (the name) change every time while other parts (everything else) stays exactly the same. So we'll need a variable that contains the name of the newly-created node. This is easy enough, since the method string is created within the constructor procedure. So, adding a method like the above one onto a psuedo-class ends up looking like the following:

global proc string myClass()
{
	string $name = `createNode transform`;

	addAttr -attributeType byte -longName count;
	setAttr ($name+".count") 0;

	// start out the string with the first segment of literal
	string $inc_com = "int $val = `getAttr(\" ";
	// add in the name of the recently-created node
	$inc_com += $name;
	// add in the name of the attribute we're grabbing
	$inc_com += ".count\"); ";

	// add in the line to increment the value 
	$inc_com += "$val += 1;";

	// add in the line to set the attribute to the new value
	$inc_com += "setAttr \"";
	// add in the name of the node
	$inc_com += $name;
	// finish off the value-setting line 
	$inc_com += ".count\" $val";

	addAttr -longName increment -dataType "string";
	setAttr -type "string" ($name+".increment") $inc_com;

	return $name;

}

I realize that looks like a lot to do. Any time that you have to build up code from a combination of string literals and variables, the code tends to end up looking much more complicated than it is. When you're writing such code, it's important to take things slow and be methodical. See the topic on self-generating code for more information and a really useful script.

Returning data from methods

So we've seen how to go about adding methods to a node and how to invoke them. However, there's a problem if we want to have a method on a psuedo-object return a value. The most obvious way to address the issue would be to have the method residing on the object that has a return value, such as:

proc int getCount()
{
	int $val = `getAttr ("myInstance.count")`;
	return $val;
}

That won't work, though, since the above code is merely a procedure definition. In order to have the code actually run, its necessary to tack on a call to the procedure after the closing bracket:

proc int getCount()
{
	int $val = `getAttr ("myInstance.count");
	return $val;
};
getCount();

Putting the above code into a string variable ends up looking like:

global proc string myClass()
{
	string $name = `createNode transform`;

	addAttr -attributeType byte -longName count;
	setAttr ($name+".count") 0;

	// start out the string with the first segment of literal
	string $getval = "proc int getCount(){int $val = `getAttr(\" ";
	// add in the name of the recently-created node
	$getval += $name;
	// finish the string off
	$getval += ".count\"); return $val;};getCount()";

	addAttr -longName getcount -dataType "string";
	setAttr -type "string" ($name+".getcount") $getval;

	return $name;
}

Okay, so the above procedure definition/execution will spit out some output. But if we want to save the output to a variable, we run into a problem. To execute the code, we need to grab the string and send it to the eval command. Its tempting to take the same approach as we did previously, where back-ticks were wrapped around a call to getAttr and the entire thing was slotted into an eval call. Applying that to the above method would result in:

eval(`getAttr ("instanceName.getcount")`);

If we want to store the output of the getcount method to a variable, we would be tempted to wrap the entire line in backticks and put it on the right side of a variable assignment, as in:

int $theVal = `eval(`getAttr ("instanceName.getcount")`)`;

However, trying the above code results in a syntax error. Notice why? The problem lies with the fact that there are 4 backticks. When Maya reads through the line, it sees one backtick and reads until it finds another. The first backtick serves as a starting point, and the second marks the close. So by having four, Maya thinks that we are declaring two separate segments (the start and the end), with the middle part hanging free. Maya sees the above code more like:

int $theVal = `eval(`

getAttr ("instanceName.getcount")

`)`;

...which is very confusing to Maya. What we need is a way to have the same thing happen (get the method, invoke it, and store the result in a variable), but with only using one pair of backticks at a time. We can do that by placing the interior set of backticks into a string and using eval. This does mean that we have to use two lines to successfully call the method, but it will work. It ends up looking like the following:

// store the command into a string
string $temp = "eval(`getAttr (\"instanceName.getcount\")`)";

// evaluate the command and store the result
int $theVal = `eval($temp)`;

Taking it further

So we've seen how to create psuedo-classes that hold data, manipulate internal values, and spit out results. That's enough to write scripts that implement basic object-orientated pratices, but there's room for a lot more. Watch this space for future articles on OOPS with MEL.

BACK