Sponsored By

Integrating MaxScript and .NET Systems

When Vicarious Visions needed a platform-independent level editor in less than six months, it went with a .NET/3DS Max hybrid solution, and here explains the integration process and the reasoning behind it.

Shea McCombs, Blogger

September 9, 2008

26 Min Read

Every journey has a beginning, and for us, the journey of integrating 3DS Max and .NET tools began when a level editor needed to be built. The requirements for this new level editor, which became known as APPLE (Authoring Plugin for Placing Level Entities), were ambitious. Our level editor had to be platform-independent, genre-independent, and we had to have a functional system up and running in less than six months.

The requirements for this new level authoring tool were driven by the culture at Vicarious Visions. Vicarious Visions develops handheld and console games on a regular basis, and many of our level designers move between the various platforms. We wanted to provide a unified level tool for designers so they could move between projects without having to learn new applications.

We researched many possibilities for building a level construction tool, and this article will document what those options were and why we decided to move forward with a .NET/3DS Max hybrid solution.

Our goal is to demonstrate how .NET applications and 3DS Max can be integrated. We will also address issues encountered during the course of development and how those issues can be overcome. This article is intended to demonstrate how to integrate .NET applications and 3DS Max and provide perspective about when this solution is appropriate.

image001.jpg
An overview of the data pipeline.

Level Editor Approaches

With the requirements and timeline set for our level construction tool, we now had to research how to achieve this new system.

The first option we considered was to build a standalone level editor tool from the ground up. This approach would allow us total control over the new application and give us the freedom to customize the user interface and subsystems to fit the needs of the designers and artists at Vicarious Visions.

Building a new application from scratch would have required time and engineering resources for building a rendering engine, user interface engine, file I/O and numerous supporting subsystems. After looking at the volume of work which would need to be done just to get a basic system off the ground, we concluded that an alternative needed to be found.

The artists and designers at Vicarious Visions are 3DS Max users, so we decided to research how to leverage the existing functionality of 3DS Max to create our level-building tool. The traditional solutions for adding new functionality to 3DS Max involve either writing plugins in C++ or using MaxScript. With the release of 3DS Max 9.0, there was a third option to consider when new behaviors and functionality needed to be added to 3DS Max -- .NET controls. We will go over why we chose this new solution of .NET integration, but first we need to examine what the .NET Framework is and how it works.

image003.jpg
Image courtesy of codeguru.com

.NET Framework Overview

The .NET Framework is a software component provided by Microsoft to facilitate an end-to-end solution for application development. "End-to-end" means that an application developer can write code, build code, and execute code entirely within the .NET Framework using any of the supported .NET languages. The .NET Framework includes code libraries collectively known as the Base Class Library (BCL), packaged in managed .dll files called assemblies, that solve common programming tasks including: user interface, numeric algorithms, file I/O and XML document manipulation.

The runtime side of the .NET Framework is called the Common Language Runtime (CLR) and is responsible for compiling code on the fly with the Just-In-Time compiler, managing memory and handling exceptions. Programs developed in .NET languages are initially compiled into an intermediate language called Common Intermediate Language (CIL).

The CLR is responsible for compiling the intermediate language into native code when the code is first executed. The "just-in-time" compiling of intermediate code to native code means that wherever the .NET Framework is installed, a .NET program will be able to run, regardless of operating system or hardware configuration. Mono and Portable.NET are examples of implementations of the CLR other than the Microsoft .NET Framework which are capable of running .NET programs.

.NET Languages

Any programming language that complies with the Common Language Infrastructure (CLI) specification can be used to write .NET applications. At this point C# and Visual Basic .NET are the most common and well documented CLI languages for development.

At Vicarious Visions, we have decided to use C# whenever possible for developing .NET applications. We settled on C# because the language was developed with the .NET Framework in mind, and looks to integrate some of the best practices from C++ and Java to produce a straightforward application development language.

One of the key features of C# is that it is a reference-based language, and pointers do not exist. This may seem like a drawback over C++, but pointers are the source of many issues in applications because of the unrestricted control they give to the engineer.

While C# is the language we use to develop .NET tools, there are numerous languages to choose from -- to date, there are over 40 CLI languages available for building programs, including:

  • Smalltalk

  • A# (Ada)

  • Active Oberon

  • APLNext

  • Boo

  • C++/CLI

  • Chrome

  • Cobra

  • Common Larceny (Scheme)

  • Component Pascal

  • Delphi.NET

  • Delta Forth .NET

  • DotLisp

  • EiffelEnvision

  • F# (Experimental ML language)

  • Gardens Point Modula-2/CLR

  • Haskell for .NET/Haskell.net/Hugs for .NET

  • IKVM.NET (converts Java byte code to CIL)

  • IronLisp

  • IronPython

  • IronRun

  • J# (Java)

  • JScript.NET

  • L# (Lisp)

  • Lexico

  • LOLCode.NET

  • Mercury on .NET

  • Mondrian

  • Nemerle (C#/PERL/LISP hybrid)

  • Net Express (COBOL)

  • NetCOBOL

  • OxygenScheme

  • P sharp (Prolog)

  • Phalanger (PHP)

  • Phrogram (CLI language for kids)

  • PowerBuilder

  • Ruby.NET

  • S# (Smalltalk)

  • sml.net (Stardard ML)

  • VBx (dynamic VB.NET)

  • Wildcat Cobol

Our Approach

One of the biggest requirements for our level editor was that it had to be project-independent. It could not be designed to work with any one engine specifically, so it would need to be built upon a truly generic set of functionality. This is unique among editors, which are almost always made for a specific engine, so it meant we immediately ruled out any sort of in-game level editor.

Another limitation was the number of developers who could give full-time support to the level editor. This number turned out to be two; one engineer and one technical artist. As exciting as writing a level editor from the ground up sounded, this option was becoming very unlikely.

We chose to avoid 3DS Max's C++ API for two reasons. The first is due to the scope of the code we were writing in 3DS Max. Most of the code would be for scene manipulation and event registration, which is something MaxScript is already very good at. The other reason comes from maintaining the code across major 3DS Max versions. MaxScript often "just works" in a new version with no modifications, but C++ plug-ins need to be re-compiled against the new API.

Additionally, the requirement of having Visual Studio configured with the 3DS Max API, and having a strong knowledge of C++, makes plug-ins far less accessible to otherwise capable people. Lastly, the need to utilize both 32-bit and 64-bit workstations at Vicarious Visions meant maintaining two versions of our plug-ins. We began to look for solutions that could run in either environment.

Writing this system purely in MaxScript did not seem realistic because of performance requirements and resource limitations. After doing much research into MaxScript, along with performance benchmarks, we found that MaxScript could not support the goals of our system. Benchmarks showed that complex computations could be performed across the .NET boundary in C# much faster than natively within MaxScript. The benchmark involved passing an array of integers to a function, which accumulated the values into a single integer, and returned the result. The benchmark was run using three array lengths; 16, 1024, and 131072 integers (size). Each benchmark was run on the buffer in three phases; 10 times, 100 times, and 1000 times (length).

  • Size 16 Length 10 MS: 0 DN: 0

  • Size 1024 Length 10 MS: 16 DN: 0

  • Size 131072 Length 10 MS: 1250 DN: 563

  • Size 16 Length 100 MS: 16 DN: 0

  • Size 1024 Length 100 MS: 93 DN: 47

  • Size 131072 Length 100 MS: 12281 DN: 5750

  • Size 16 Length 1000 MS: 16 DN: 15

  • Size 1024 Length 1000 MS: 906 DN: 454

  • Size 131072 Length 1000 MS: 119953 DN: 57406

MaxScript (MS) vs. .NET (DN), results given in milliseconds

According to these results, calling a .NET function is consistently about twice as fast as calling a MaxScript function to do the same work. See the benchmark appendix at the end of this article for full benchmark source code.

MaxScript also has memory limitations, which become a problem when dealing with large data sets. From an implementation standpoint, having an engineer and a technical artist meant that we needed a solution which was both C# driven and MaxScript driven, since engineers typically do not work with MaxScript, and most technical artists do not work with traditional programming languages.

We decided 3DS Max's role would be to serve as the user interface. In other words, 3DS Max was the upper layer for interacting with users and displaying information. This design allowed us to implement most of the level editor functionality outside of 3DS Max by calling C# functions and listening for C# events within MaxScript.

The key design philosophy became providing new functionality using C#, with MaxScript as glue, interfacing 3DS Max to the C# code.

The tools group at Vicarious Visions had previously developed a .NET application for viewing and manipulating large data sets. The .NET integration functionality provided in 3DS Max 9 allowed us to plug into this application, thus giving our developers a familiar user interface for creating and manipulating game data. By integrating the two systems, we were able to offload data processing from 3DS Max to our internal tools, which were already optimized for the task.

An integrated solution also gave us the ability to attach Visual Studio to the 3DS Max process, trigger a bug, and step through the .NET code, setting breakpoints, watching values, and exploring data structures while 3DS Max quietly sat in the background. User Interfaces written in .NET can also be used directly in 3DS Max, making for a much more seamless user experience.

One of the fortunate side-effects of this approach comes from our toolchain's ability to communicate directly with the game during runtime. Because of this, changes in 3DS Max can be immediately reflected while the game is running. This has provided us with much shorter iteration time for designers and artists when editing levels.

Implementation Details

In order to understand how the implementation works, we should look at how MaxScript interacts with .NET systems. .NET assemblies are .dll files which contain code in the form of CIL -- or the Common Intermediate Language. An assembly is loaded by MaxScript using the LoadAssembly function in MaxScript's DotNet struct. Once an assembly is loaded, all of the public data types, classes, events, and objects are visible to MaxScript through wrappers. MaxScript can interact with these wrappers, and in turn, the wrappers can interact with the Common Language Runtime (CLR).

Before jumping directly into how .NET applications can be integrated with 3DS Max, there is software you will need. The first and most obvious program is 3DS Max 9.0 or later. The version number is important in this case because the ability for MaxScript to interact with .NET programs was not introduced until version 9.0.

Once you have 3DS Max 9.0 installed, you will need a way to develop .NET applications. .NET applications can be built by various IDEs (Integrated Development Environments), but the one we recommend is Microsoft Visual C# Express Edition 2005. The express edition series of Visual Studio is free to download and develop on, and we have used similar versions of Visual Studio for our systems integration. The reason the 2005 version is specifically needed is that it installs the .NET Framework version 2.0, which is what MaxScript expects to interact with.

Now that all the needed software is installed and running, it's time to look at the means of communication between .NET and MaxScript. While MaxScript can call .NET functions directly, .NET has no awareness of MaxScript or 3DS Max's API. Therefore, any time .NET code needs to trigger 3DS Max functionality, it must do so through the use of events which are handled by MaxScript functions as callbacks. Our example will illustrate how a .NET event can be tied to a MaxScript function.

The actual implementation of the .NET integration of our tool chain with 3DS Max is a complex set of MaxScript and C# code. Because of the thousands of lines of MaxScript and C# code involved, we want to provide a simplified example to illustrate how to leverage this new feature in 3DS Max. We will lay out a pair of MaxScript and C# programs which tie 3DS Max functionality to a .NET control. This example, though small, encompasses all of the functionality required to complete our large scale integration.

image005.jpg

In the following example code, we will illustrate how to use function calls and callbacks / events to control 3DS Max and pass information from MaxScript into a .NET control.

MaxScript Code

[This function gets called when the .NET control raises the CreateSphere event.]
function OnCreateSphere obj args =
(
Sphere segs:62 wireColor:white
)

[This function gets called when the .NET control raises the CreateLight event. The 'args' argument has been filled out by the .NET control, and is passed to MaxScript for processing. In this case, it contains a color.]
function OnCreateLight obj args =
(
new_light = OmniLight()
new_light.color = Color args.LightColor.R args.LightColor.G args.LightColor.B
)

[Loads the .NET assembly containing the C# code. This is really where the integration happens.]
DotNet.LoadAssembly "c:/projects/dotnetassemblies/MaxScriptIntegrationDemo.dll"

[This is the rollout which actually houses the .NET control itself, which is accessible because we loaded the assembly on the line above.]
rollout r_MaxScriptIntegration ".NET Integration" width:300 height:400
(
[This is how you instantiate the .NET control using MaxScript. It is a fully qualified name, with “Namespace.Name”]
local dotnet_object = "MaxScriptIntegrationDemo.MaxScriptIntegrationDemoControl"
local custom_control = DotNetObject dotnet_object

[Now create a .NET panel on the rollout to contain the .NET control instantiated above. A panel is a control provided by the .NET framework.]
DotNetControl dnc_dotNetPanel "System.Windows.Forms.Panel" width:290 height:360

[These buttons are used to call .NET functions when pressed]
button btn_sayHello "Say Hello" across:2 align:#center
button btn_sayGoodbye "Say Goodbye And Leave" align:#center

[Do some work to hook up events from our .NET control to the functions declared at the top of this code.]
on r_MaxScriptIntegration open do
(
[This is how a .NET event is tied to a MaxScript function. DotNet.AddEventHandler is a MaxScript function, and takes three arguments. The first argument specifies the .NET control on which to listen for events. The second argument specifies the actual event to listen for by name on the .NET control. The third argument specifies the MaxScript function which will handle this event.]
DotNet.AddEventHandler custom_control "CreateSphere" OnCreateSphere
DotNet.AddEventHandler custom_control "CreateLight" OnCreateLight

[This line adds our custom .NET control to the panel control. Every control has a .Controls property, which is a list of child controls, and we can add or remove other controls from this list.]
dnc_dotNetPanel.Controls.Add custom_control
)

on btn_sayHello pressed do
(
[When the “Say Hello” button is pressed, call the function SayHelloUserControl() on our custom .NET control.]
custom_control.SayHelloUserControl()
)

on btn_sayGoodbye pressed do
(
[When the “Say Goodbye” button is pressed, call the function Say() on our custom .NET control, which takes a string from MaxScript and displays a modal message box using the string passed to the Say() function.]
custom_control.Say "Goodbye! Press OK to close the Max rollout."

[When the user clicks “OK” on the message box, this line will be executed, closing the rollout.]
destroyDialog r_MaxScriptIntegration
)
)

createDialog r_MaxScriptIntegration;

C# Code

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;

/// The name of the namespace we want our
/// demo to exist in.
namespace MaxScriptIntegrationDemo
{
/// <summary>
/// A simple control used to illustrate integration between .Net and MaxScript.
/// The control contains a "Create Circle" button and a "Create Omni Light"
/// button. The buttons have event associated with them which can be
/// registered for by MaxScript. Be sure that all objects that
/// need to communicate with MaxScript inherit directly or indirectly from
/// System.ComponentModel.Component or events won't work correctly.
/// </summary>
public partial class MaxScriptIntegrationDemoControl : UserControl
{
public MaxScriptIntegrationDemoControl()
{
InitializeComponent();
}

/// <summary>
/// The CreateCircle event can be registered for by MaxScript so that when
/// the event is raised MaxScript can take the appropriate action.
/// </summary>
public static event EventHandler CreateCircle;

/// <summary>
/// This event will be called when the "Create Circle" button is clicked on the
/// .Net user control. If any other object (including MaxScript) is registered
/// for the event they will be notified.
/// </summary>
/// <param name="sender">The button that was pressed.</param>
/// <param name="e">Generic EventArgs</param>
private void _makeCircleButton_Click(object sender, EventArgs e)
{
if (CreateCircle != null)
{
CreateCircle(sender, e);
}
}

/// <summary>
/// The CreateLight event can be registered for by MaxScript so that when
/// the event is raised MaxScript can take the appropriate action.
/// </summary>
public static event EventHandler CreateLight;

/// <summary>
/// This event will be called when the "Create Omni Light" button is clicked
/// on the .Net user control. If any other object (including MaxScript)
/// is registered for the event they will be notified.
/// </summary>
/// <param name="sender">The button that was pressed.</param>
/// <param name="e">Generic EventArgs</param>
private void _makeOmniLightButton_Click(object sender, EventArgs e)
{
if (CreateLight != null)
{
/// We want the user to pick a color before raising the event so we
/// can pass that information along with our custom LightEventArgs.
if (_colorDlg.ShowDialog() == DialogResult.OK)
{
CreateLight(sender, new LightEventArgs(_colorDlg.Color));
}
}
}

/// <summary>
/// Simple method to show how functions on .Net objects can be called from
/// MaxScript.
/// </summary>
public void SayHelloUserControl()
{
MessageBox.Show("Hello");
}

/// <summary>
/// Simple method to show how functions on .Net object can be called from
/// MaxScript.
/// </summary>
/// <param name="text">What should be printed in the .Net MessageBox
/// </param>
public void Say(string text)
{
MessageBox.Show(text);
}
}

/// <summary>
/// Sample custom EventArgs to illustrate how data can be passed to
/// MaxScript through events.
/// The class encapsulates a color value for passing with the CreateLight event.
/// </summary>
public class LightEventArgs : EventArgs
{
/// <summary>
/// .Net Color object.
/// </summary>
private Color _color;

/// <summary>
/// Constructor
/// </summary>
/// <param name="color">The color to be passed to MaxScript when
/// creating a light.</param>
public LightEventArgs(Color color)
{
_color = color;
}

/// <summary>
/// Property: Used to access the color value.
/// </summary>
public Color LightColor
{
get
{
return _color;
}
}
}
}

Issues Encountered

During integration, we encountered and overcame a variety of issues. These issues can be put into one of two categories: design issues and runtime issues.

The first design issue encountered was the cross-disciplinary nature of the work. It is highly unlikely that a single person would possess all of the knowledge needed to construct any MaxScript-.NET hybrid solution. In order to have an integrated system between .NET and MaxScript you will need people who are very adept at MaxScript, the .NET framework, and have a clear understanding of how to use 3DS Max from a user's perspective. We have achieved great success by having a MaxScripter work with a .NET programmer to implement features in tandem. The .NET programmer writes the underlying functionality the MaxScripter is looking for, and the MaxScripter will bring this functionality to the user through 3DS Max.

Another design issue comes up when deciding how to divide functionality between MaxScript and .NET applications. MaxScript is an obvious choice for anything directly associated with 3DS Max, but if the capabilities of MaxScript overlaps with .NET, a decision needs to be made regarding where the implementation should lie.

Over the course of development we settled on pushing as much functionality as possible to the .NET side of the fence to optimize performance. The only caveat for our systems is that hacks should always live in MaxScript. Containing project-specific hacks in MaxScript means there is no need to recompile when hacks are injected and our core toolchain is not littered with special cases for specific projects and platforms.

The last design issue we will cover may seem obvious, but is important nonetheless. Namely, in order to integrate .NET applications with 3DS Max, the tools and programs in question must be developed within the .NET Framework. For some developers, integration with 3DS Max may mean rewriting thousands of lines of code within .NET, and each developer will need to assess how reasonable that would be to achieve. In the event a developer wants to attempt adapting an older tool chain to work with 3DS Max, there is another option available.

It is possible to build a .NET layer that lives between the unmanaged tools and 3DS Max to be accessed by MaxScript. This solution provides the ability to tightly control what objects, methods and events are exposed to MaxScript because only data living in the .NET layer will be accessible. However, even if old tools cannot be revised for integration, if developers keep .NET in mind when creating new tools this option will be available down the road.

Now that we have covered the major design issues encountered it is time to look at unexpected runtime behaviors and how to overcome them.

Integrating a program as large as 3DS Max with .NET applications through MaxScript can have unforseen results at runtime. It needs to be understood that when integrating .NET programs with 3DS Max there are actually two distinct runtimes executing within the same process. This coexitence of rutimes is not always compatible.

MaxScript has a garbage collector responsible for cleaning up objects that it deems ready for deletion. The algorithms used by the garbage collector are not always correct when dealing with .NET objects. In some cases .NET objects and controls may be deleted by the MaxScript garbage collector while they are still in use, and there are steps that can be taken to help prevent this issue from happening.

One observable side effect of an aggressive garbage collection is that .NET events will stop triggering MaxScript functions. The most reliable solution to the collection issue is using the keyword static in .NET application code on events and other objects that should not be deleted when a collection occurs. Another way to help the garabage collector is to ensure that the initial MaxScript heap memory is set well above the default of 7.5 megabytes. Through research with our systems we have found an initial MaxScript heap of at least 128 megabytes will ensure stability for the end user.

The final rutime challenge we address is about 3DS Max accelerators. The 3DS Max documentation notes that because 3DS Max does not know whether a control should receive input focus, it will maintain accelerators all the time. When developing .NET controls that require keyboard focus to operate correctly, the 3DS Max accelerators should be disabledon a timer-based schedule. An event fired by a .NET control once a second that notifies MaxScript to disable accelerators is the solution we have arrived at for maintianing keyboard focus.

Conclusions

We have been using this solution at Vicarious Visions for one year. We have shipped one Nintendo DS project, Kung Fu Panda, and are currently supporting three ongoing Nintendo DS projects and one Xbox 360 / PlayStation 3 project.

image007.jpg

Overall, we have had great success using this solution. We have been able to leverage the various systems that 3DS Max provides to alleviate redundant work, which we would have needed to do if we had built a system from scratch. There are factors to consider before integrating existing toolchains with 3DS Max, but they are no different from any other large-scale integration project. Integrating 3DS Max with .NET allows for two distinct disciplines, namely technical art and engineering, to work together cooperatively in developing programs, rather than working in isolation.

In the end, integrating .NET applications with 3DS Max through MaxScript consolidates a great deal of functionality from different applications within a single user interface that artists and designers are already comfortable using.

Downloads and Documentation

MaxScript Help File Sections
- DotNet in MaxScript
- Registering Windows SystemEvents Callbacks Using DotNet
- Loading Assemblies

MSDN .NET Framework
- http://msdn2.microsoft.com/en-us/netframework/default.aspx

Microsoft Visual C# 2005 Express Edition
- http://msdn2.microsoft.com/en-us/express/aa975050.aspx

Appendix I: Benchmark Source Code

MaxScript Code

function maxscript_test_fn input_array =
(
accum = 0;

for val in input_array do
(
accum += val;
)

accum;
)

DotNet.LoadAssembly "Benchmark.dll";

function benchmarkDotNet samplesize:10000 testlength:10 verbose:false =
(
dot_net_object = DotNetObject "Benchmark.BenchmarkClass";

[Get a reference directly to the method, avoid name lookup; matches native MaxScript behavior more closely.]
dot_net_object_test_fn = dot_net_object.Test;

sample_data = #();

for i = 1 to samplesize do
(
sample_data[i] = (random 0 10) as Integer;
)

[Make a result buffer to avoid any 'lazy evaluation' optimizations MaxScript might do because nothing is using the return value of the function. Also pre-allocate the array to prevent any runtime allocation which may affect one of the loops.]
result_buffer = #();

for i = 1 to testlength do
(
result_buffer[i] = 0;
)



timer1 = timeStamp();

for i = 1 to testlength do
(
result_buffer[i] = maxscript_test_fn sample_data;
)


timer2 = timeStamp();


for i = 1 to testlength do
(
result_buffer[i] = dot_net_object_test_fn sample_data;
)

timer3 = timeStamp();


maxscript_time = timer2 - timer1;
dot_net_time = timer3 - timer2;


if (verbose == true) then
(
format "Results:\n";
format "\tMaxScript : %ms\n" maxscript_time;
format "\tDotNetObject : %ms\n" dot_net_time;
)

#(maxscript_time, dot_net_time);
)

function runFullBenchmark =
(
sample_sizes = #(16, 1024, 131072);
test_lengths = #(10, 100, 1000);

for test_length in test_lengths do
(
for sample_size in sample_sizes do
(
time_results = benchmarkDotNet samplesize:sample_size \
testlength:test_length;

format "[size=%, length=%]\tMS:%\tDN:%\n" sample_size \
test_length time_results[1] time_results[2];
)
)
)

C# Code

using System;

namespace Benchmark
{
public class BenchmarkClass
{
public int Test(int[] buffer)
{
int accum = 0;

for (int i = 0; i < buffer.Length; i++)
{
accum += buffer[i];
}

return accum;
}
}
}

Read more about:

Features

About the Author(s)

Shea McCombs

Blogger

Shea McCombs began as a Technical Artist at Vicarious Visions in 2005, having previously been an Artist at Digital Anvil. His primary interests have been making better use of existing tools and exploring user-friendly ways to tame the complexity of everyday challenges in game development. Shea has been involved with computer art and programming since 1996, and has been part of the game industry since 2000.

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like