Despite all the fancy Py3dsMax and MaxPlus around I still write quite a bit of MAXScript at work. It is simply faster for automating things and small tools are usually not worth the overhead associated with going the Python route. Compared to Python, MAXScript is quite restricted in many ways, though there are strategies to workaround some of these limitations, which I want to talk about.
One of them is modularity. MAXScript does not support namespaces. Anything that you define in a file or inside the Listener is defined in global scope or 'global namespace'. This is not a big deal when working on small, isolated tools, but it can become a real pain when working on a large toolset with hundreds of scripts and lots of dependencies in between. We can add prefixes to scripts/variables/functions to kind of 'fake' a namespace, and it might even be a good way minimize the risk of overriding names, but it can make your code really verbose and ugly:
struct MyCustomNameSpace_myTool (
....
)
MyCustomNameSpace_myTool = MyCustomNameSpace_myTool()
Imagine having a startup script that defines a function called process(). If at any point in your 3ds Max session some other script is run / imported / injected (via a socket connection or similar) that also defines a function called process(), you will get no indication whatsoever that your original function has been overwritten. In a toolset that imports scripts that import scripts that load 3rd party tools etc. this happens easily and can get really nasty to debug. You may find yourself grepping source files or writing scripts to find function name duplicates (been there, done that).
So we need to encapsulate functionality and avoid polluting the global namespace with names that may override each other. How can you do this in MAXScript though? Since files do not have a namespace of their own, we need to use some other container to encapsulate functions and data. Since we can not define custom classes in MAXScript, we must use either structs or GUI containers like a rollout. Rollouts are actually fine for smaller tools. They can be used to access both their internal GUI components as well as data and function members defined within, e.g.:
rollout MyRollout "My Rollout" (
local myData
button myButton "Press me"
fn myFunction arg1 arg2 = ()
)
createDialog MyRollout
However, not all tools require a GUI (like a function library etc.) and mixing GUI with lots of application logic is not a good idea either. Structs can help us here. They are simple containers that are similar to classes, but less powerful. They do not support inheritance and can not be nested. Instead of e.g. defining three functions in global scope we can do:
struct MyFunctions (
fn myFunction1 = (),
fn myFunction2 = (),
fn myFunction3 = ()
)
This will give us a single name in global scope instead of three. Using this for a library with hundreds of functions will greatly reduce the risk of name clashes. Structs can also wrap rollouts, so that you can define the application logic ('controller') inside the struct, and all GUI functionality in the rollout ('view') and split this into two different files. You will however have to inject a reference to the controller in the view. Imagine a gui.ms like:
rollout MyToolGUI "My Tool" (
local self
button btnProcess "Process"
on btnProcess pressed do (
self.process()
)
fn init parent = (
self = parent
-- Code from event 'on MyToolGUI open do'
-- should be placed here instead.
)
)
And a complementing tool.ms like:
fileIn "gui.ms"
struct MyTool (
data1,
data2,
ro,
fn process = (
print "Processing..."
),
fn show = (
createDialog MyToolGUI
this.ro = MyToolGUI
this.ro.init this
),
fn close = (
try
destroyDialog this.ro
catch()
)
)
MyTool = MyTool()
MyTool.show()
In a similar way we can 'build' our custom 'classes' by importing other scripts and composing them together like:
struct Initializer(
fn initialize = ()
)
struct Worker(
fn work = ()
)
struct Cleaner(
fn cleanup = ()
)
-----------------------
fileIn "initializer.ms"
fileIn "worker.ms"
fileIn "cleaner.ms"
struct ClassD (
initializer = Initializer(),
worker = Worker(),
cleaner = Cleaner(),
fn process = (
this.initializer.prepare()
this.worker.work()
this.cleaner.cleanup()
)
)
Sadly this is how far it goes, no real inheritance, no nesting. There is another way to help make sure your tools work correctly which I like to call fileIn and forget: Whatever script you want to use should make sure to import all scripts that it depends on using fileIn() before actually doing anything. That way you can be (more or less) sure that the current context matches the needs of your script. If you call it a lame workaround for missing encapsulation, then I guess you are right ;)