Rollout and Struct Pairs
From Tech Artists Wiki
A very good way to organize MAXScripttools is by pairing them with structs. The problem specifically addressed by this article is getting rid of globals. Please see that article for a full explanation of why globals shouldn't be used.
Consider the following script, which is one tool consisting of two rollouts:
rollout ro_test1 "Test 1" ( local var button btn_a "A" on btn_a pressed do print (var + var) on ro_test1 open do ( var = timeStamp() print (var + var) ) ) rollout ro_test2 "Test 2" ( local var button btn_b "B" on btn_b pressed do print (var + var) on ro_test2 open do ( var = ro_test1.var print (var + var) ) ) fl = newRolloutFloater "Test" 200 200 addNewRollout ro_test1 fl addNewRollout ro_test2 fl
The problems here are not all immediately obvious. First problem is, what we really want to do is get some value when the rollout is initialized- var- and use it across both rollouts. However, we are instead grabbing it once, and then assigning ro_test2's to ro_test1's. This can create problems since we cannot guarantee what will be initialized when, and causes hidden bugs (it will crash if ro_test1 is added to fl in a closed state, for example).
Second problem is, this doesn't make sense. We want var defined for both rollouts, but it logically it doesn't belong to ro_test1 anymore than it does to ro_test2. We cannot set var externally from the rollouts, because they need to use it when they are opened.
Third problem is, the execution is contained within the event handlers. Yes, this can be fixed by putting the event handler code into functions contained in the rollout, but this is a bad solution, since it is equally poor for batching or external access (the rollout must exist, and be initialized/set up properly to do stuff).
The above is known as bad code design. It is a common affliction but there is a cure.
There are really two solutions.
The following code would work:
global var fn addAndPrint val = print (val + val) rollout ro_test1 "Test 1" ( button btn_a "A" on btn_a pressed do addAndPrint var on ro_test1 open do ( addAndPrint var ) ) rollout ro_test2 "Test 2" ( button btn_b "B" on btn_b pressed do addAndPrint var on ro_test2 open do ( addAndPrint var ) ) var = timeStamp() fl = newRolloutFloater "Test" 200 200 addNewRollout ro_test1 fl addNewRollout ro_test2 fl
It works, but is no real improvement. We have two globals now: var and addAndPrint (and fl would be a third). Why hasn't it improved?
- Yes we've solved the first issue. But now our code is much more confusing. We are defining things used inside the rollout from outside of the rollout, at a certain spot. This brings up the problems described in the globals article.
- var's initialization still doesn't make sense. The code is equally irrational, just now in a different spot (and less likely to crash).
- We've moved the code out of the event handlers, and into a global function. For our purposes, this works better than putting it in the rollout (since it'd have to reference another rollout, or be duplicated), but still contains all the problems in the globals article. It would have been evil to use 'var' inside of the function- instead, we will pass it as an argument from the event handlers.
For a much better solution, you'd end up with the following code:
struct testDef ( var = timeStamp(), fn addAndPrint = print (var + var) ) global test rollout ro_test1 "Test 1" ( button btn_a "A" on btn_a pressed do test.addAndPrint() on ro_test1 open do ( test.addAndPrint() ) ) rollout ro_test2 "Test 2" ( button btn_b "B" on btn_b pressed do test.addAndPrint() on ro_test2 open do ( test.addAndPrint() ) ) test = testDef() fl = newRolloutFloater "Test" 200 200 addNewRollout ro_test1 fl addNewRollout ro_test2 fl
So admittedly the cleaned up code doesn't provide a huge gain. And rightfully so, since it is a very simple situation. However, as the example tool at the end will show, it can greatly simplify tools where various parts (rollouts, tool clauses, etc) need to work together.
For the discussion, let's consider this sample script. It is a pared down tool from work that demonstrates the benefits of using structs. The real tool is more complex in lots of ways but this demonstrates the fundamentals I think:
struct rigUtilsDef ( pNode, pNodeMod = pNode.modifiers["Puppet Tools"], lvanimlist = pNodeMod.ps_time_rol.lvanimlist, animNodes = puppet.getAnimationNodes pNode, --and a pretty advanced initialization routine boneNodes = getAllChildren pNode, fn isHub a = (matchPattern ((classOf a.baseobject) as string) pattern:"*HUB*"), fn isDigit a = classOf a.baseobject == puppetShop_digitNode, hubNodes = for a in boneNodes where isHub a collect a, digitNodes = for a in boneNodes where isDigit a collect a, fn parent = ( local oldState = PS_CONTROLLERLOCKSTATE PS_CONTROLLERLOCKSTATE = false for i = 1 to childSet.count do ( childSet[i].parent = parentSet[i] ) PS_CONTROLLERLOCKSTATE = oldState ), fn unparent = ( local oldState = PS_CONTROLLERLOCKSTATE PS_CONTROLLERLOCKSTATE = false for a in childSet do a.parent = undefined PS_CONTROLLERLOCKSTATE = oldState ), fn getLeftRightCenterArraysFromPos = ( rightArr = for a in boneNodes where a.pos.x < -.005 collect a leftArr = #() for a in rightArr do ( --test against each rightArr member's mirror pos across x for b in boneNodes do ( if (closeEnoughXYZ (a.pos * [-1,1,1]) b.pos .005) then ( append leftArr a ) )--for b ) centerArr = #(pNode) for a in boneNodes where not arrops.isInArr rightArr a and not arrops.isInArr leftArr a do append centerArr a if rightArr.count == leftArr.count then true else false ), fn posRightToLeft = ( if (getLeftRightCenterArrays()) then ( for i = 1 to rightArr.count do ( leftArr[i].pos = rightArr[i].pos * [-1, 1, 1] ) ) else messageBox "Rig is not symmetrical" ) ) global rigUtils rollout ro_rigUtils "Rig Utils" ( local btnWidth = 130 button b_posRL "Copy Positions R->L" width:btnWidth button b_hubAsChild "Hub" width:(btnWidth/2) across:2 button b_digitAsChild "Digit" width:(btnWidth/2) button b_unparent "Unparent" width:btnWidth button b_parent "Parent" width:btnWidth on b_unparent pressed do ( rigUtils.unparent() ) on b_parent pressed do ( rigUtils.parent() ) --Set the child set on b_hubAsChild pressed do ( rigUtils.initSets rigUtils.hubNodes ) on b_digitAsChild pressed do ( rigUtils.initSets rigUtils.digitNodes ) on b_posRL pressed do ( rigUtils.posRightToLeft() ) on ro_pTools close do ( rigUtils.parent() ) ) if selection.count == 1 then ( global rigUtils = rigUtilsDef pNode:selection createDialog ro_rigUtils )
Structs for tools gives you the following:
- Auto-initialize common variables:
- Common variables not needed as arguments: