Making Maya automatically execute a script when an attribute changes is not as straightforward as it would seem. As someone new to Python, getting a scriptJob to work when referenced is confusing. It took me quite a bit of digging to get things to work, so I figured I'd share my finding to make it easier on the next person.

Why?

Riggers use this technique frequently for FK/IK Switching. It's real-time and I won't need to run a shelf script after a changing my parameter. It also needed to be automatic, since there are so many cases, and I know in 3 months I won't remember them all if the client asks for changes. In My case, I needed a condition with lots of cases that determined a characters output color and accessory visibilities.

This would take a high amount of nodes and wires to do manually. It would be real time... but messy, hard to update and easy to break.

Using a shelf script can solve these issues, but is annoying to have ro run it so frequently. To fix that annoyance and make it fully automated, we can make the script automatically fire when the attribute changes.

Summary of How

In short we are making a custom attribute which fires a defined function. This function is then called from a scriptJob. The purpose of the ScriptJob is to attach code to a parameter. The scriptJob is then embedded into a scriptNode for proper saving and portability.

The scriptnode creates the scriptjob on scene open. Then, your parameter calls the scriptJob function any time the attribute is changed.

Steps to Implement:

  • Determine what the script needs to do and what type of attribute fits best for control.
    • The example goal is to have the character reconfigure itself with some visibility and texture settings that change automatically depending on the selection.
  • Add a custom attribute to the control object.
    • In the example case below, I used an Enum data type with four values and named it charSwapConfigFn.
  • Create your function and get things working.
    • In our example, I used python.
    • def charSwapConfigFn(ns,obj,attr): things you need to do go here
    • I wanted to make sure passing variables would work since that will be important for making sure everything works with referencing and it makes it easy to adjust for other things later.
    • Make the script work when referenced by accounting for namespaces. This was a mess to sort out, it took me days as a noob to Python for maya but it seems everyone on the internet has run into this and has had varying solutions.
    • Test your function after written. Work out the bugs within it before adding the remaining complications.
    • IMPORTANT: Make sure your function works before adding the scriptJob code.
  • Create the scriptJob that will fire your function on attribute change.
    • For example I used the below and passed along the needed variables.
    • fullTarget = (thisObject+"."+thisAttribute)
    • cmds.scriptJob(attributeChange=[fullTarget,"charSwapConfigFn(thisNamespace,thisObject,thisAttribute)"])
    • IMPORTANT: Make sure the scriptJob works before proceeding. You can do this by changing the targeted attribute.
  • Embed your scriptJob as a string into a scriptNode.
    • This forces the code to rebuild when the scene is reopened or referenced.
    • This is what the code with triple single quotes ''' is added.
    • You'll need to display DAG objects to find the scriptNode in the outliner.
    • To see the code in the scriptNode you can go to Windows/AnimationEditors/Expressor Editor and change the select filter to By Script Node Name

Finished Code:

Tip: To see the executed code in the sample scene you can go to Windows/AnimationEditors/Expressor Editor and change the select filter to By Script Node Name. It will look different than below because it will have embedded the middle code for ''' to ''' (triple single quotes).

Simple Explanation: Outside of the triple single quotes creates the scriptNode and Inside the quotes creates the scriptJob.

Note: This code is a bit verbose to help with understanding for lesser familiar folks like myself.

DOWNLOAD PYTHON FILE

#Note: multi-line variable uses triple single quotes this is being used because we're wrapping our function into a string...
#so we can write it into a script node that dynamically creates the formatted script job on scene open...
#this is to make the scriptJob persistent and dynamic so we assure it works when referenced
import maya.cmds as cmds
characterChangeCode = '''
#################code v04####################################################################################################
#################Start of code inserted into the expression script node#####################################################
#################Start of code inserted into the expression script node#####################################################
import maya.cmds as cmds
#define function that will get called when parameter is changed
def charSwapConfigFn(ns,obj,attr):
	ObjAttr = str(obj+"."+attr)
	print "!!!!!!!!!!running function on "+ObjAttr+"!!!!!!!!!!"
	colorChange = ns+"CharacterMat.color"
	hatVisibility = ns+"Hat.visibility"
	noseVisibility = ns+"Nose.visibility"
	if cmds.getAttr(ObjAttr)==0:
		print "it's the yellow one"
		cmds.setAttr(colorChange, 1, 1, 0, type="double3")
		cmds.setAttr(hatVisibility, 0)
		cmds.setAttr(noseVisibility, 0)
	elif cmds.getAttr(ObjAttr)==1:
		cmds.setAttr(colorChange, 1, 0, 0, type='double3')
		cmds.setAttr(hatVisibility, 0)
		cmds.setAttr(noseVisibility, 1)
		print "it's the red one"
	elif cmds.getAttr(ObjAttr)==2:
		cmds.setAttr(colorChange, 0, 0, 1, type='double3')
		cmds.setAttr(hatVisibility, 1)
		cmds.setAttr(noseVisibility, 0)
		print "it's the blue one"
	elif cmds.getAttr(ObjAttr)==3:
		cmds.setAttr(colorChange, 0, 1, 0, type='double3')
		cmds.setAttr(hatVisibility, 1)
		cmds.setAttr(noseVisibility, 1)
		print "it's the green one"

# define variables 
thisAttribute = "CharacterConfig"
thisObject = "determined below"
thisNamespace = "determined below"

# find all objects with that attribute
unfilteredList = cmds.ls(type="transform")
for obj in unfilteredList:
	print "Checking Object: "+obj
	if cmds.attributeQuery(thisAttribute, n=obj, exists=True):
		thisObject = obj
		#get name with namespace to make sure it still works when referenced
		thisNamespace = ((obj.rpartition(':')[0])+":")
		#call and test function
		charSwapConfigFn(thisNamespace,thisObject,thisAttribute)
		#add below lines of this into a Sciptjob after making sure the above function works on it's own
		fullTarget = (thisObject+"."+thisAttribute)
		cmds.scriptJob(attributeChange=[fullTarget,"charSwapConfigFn(thisNamespace,thisObject,thisAttribute)"])
#################End of code inserted into the exprerssion script node#####################################################
#################End of code inserted into the exprerssion script node#####################################################
'''
#Note: multi-line variable uses triple single quotes....
#this is being used because we're wrapping our function into a string...
#get rid of it if it already exists and then remake it to prevent duplicates during editing
if cmds.objExists('scriptNodeCharacterChanged'):cmds.delete('scriptNodeCharacterChanged')
cmds.scriptNode( scriptType=2, beforeScript=characterChangeCode.replace("'''","''" ), name='scriptNodeCharacterChanged', sourceType='python')

Example Working File:

This primitive based character example scene, outlines the steps. The CharacterCTRL object has a custom attribute to switch between four different character configurations and colors using the implemented steps outlined above.

Download Sample Maya File (Maya 2019 Format)

Troubleshooting:

  • Problem: The python script works from the script editor but doesn't work when tested in the Expression Editor.
    • Solution: You shouldn't fire the script from the Expression editor at all. The script job should
  • Problem:
  • Note: Python code doesn't feed back like MEL does, there isn't a solution, it's just annoying to people still learning how it all works.
  • Note: If the attribute change doesn't happen automatically when referencing doesn't work, the code should be updated in the source file, not the reference.
  • Problem: Since a scriptJob is not editable, you will have to remove it before making an update or change to it.
    • Solution: I just made two shelf scripts for this since it is a two step process:
      • Find the right scriptJob (results in script editor feedback)
      • Remove it by ID (prompt for number)

Improvements:

Since I am new to this, I am totally open to seeing how my code can be improved and updated. I read a bit about callbacks, but am not an API level programmer. If you know a better way and want to send it along, I'll add it to this blogpost and then everyone wins! Just let me know!