Where'd It Go? It Was Just Here!
Managing Assets for the Next Age of Real-Time Strategy Games

Keep the Artist in Max

During the development of AoK, the art process consisted of a number of individual steps. Once an artist had created a unit for in-game use, it then had to be rendered out, postprocessed, then added to the game by a lead artist or designer. This meant that it could be a long time between the time artists worked on an asset and when they actually saw it in the game.

As we started implementing the asset management system, our mantra became "keep the artist in Max." In other words, give the artists everything they need to create, manage, and view their models and textures in the game directly in Max. The only time they should have to leave Max is to run Adobe Photoshop, which they can also launch from a button in Max.

This mantra led to the creation of two additional Max plug-ins to support our asset management system. The first was a texture/bitmap browser that allowed the artist to search, view, check out, and use any bitmap asset stored in the asset manager (Figure 5). The texture browser was built as a Max extended viewport plug-in using MFC and Lead Tools imaging control. The Lead Tools provided us a very powerful yet simple-to-use ActiveX component that could be used directly in the MFC dialog box that formed the basis of the texture browser.

Figure 5. 3D Studio Max with RTS3 in-game extended viewport (upper right) and bitmap browser extended viewport (lower right).

The second plug-in was also a Max extended viewport -- it was actually the entire game running inside Max as a viewport. This plug-in was relatively simple to create by making a version of the game that built as a .DLL rather than an .EXE file. Some additional code was required in the window handling code to compensate for the fact that the game was now running as a child window and had to interact with the Max input system.

A small amount of functionality was also required to allow the artist to specify which of the objects in a Max scene should be put into the game. Instead of creating a cumbersome communication system between Max and the game, the artist's scene is exported to a file in a format that the game can load. The game is then told to load the file and display its contents. Because this is the whole game engine running inside Max, the artist can examine the model in the context of other existing game assets and scenarios. As complicated as getting the game engine itself to run within Max was, creating both extended viewports was very straightforward compared to managing Max files and seamlessly integrating with Max itself.

Max files themselves might initially be located in any local or server directory, and they may refer to texture files that exist in any of the Max texture paths. Adding a Max file to the asset manager meant moving the file to its managed directory, then scanning the file for any texture files and moving those to the same directory as well, and finally updating the paths of textures referenced by the Max file.

If the Max file contained in-game models, not only was the Max file added to the asset manager, but an in-game version of the file was generated and stored in parallel. This allowed the back-end export process used to create the game build to be much simpler. It just had to copy data out of the database and store it in files.

Every texture also generated one or two additional files on check-in. A thumbnail image was created and stored to support the texture browser, and textures used in-game automatically generated a texture in the in-game format.

Integrating with Max

Integrating directly into the Max user interface and menu system was one of the hardest challenges we faced in creating a seamless asset management system (Figure 6). Unlike the Microsoft Visual C++ IDE, Max does not have a well-defined interface for asset management tools to plug into. Also, it's not possible to create menu items in Max using the Max SDK or MaxScript.

Figure 6. 3D Studio Max menu integration.

Because we wanted to make the integration as seamless as possible, we had to rely on Win32 programming to manipulate the main Max window and menu system directly. Although this appeared to be a complicated solution, it gave us the flexibility we desired to create new menu entries, and the ability to override and enhance existing Max functionality.

To facilitate this integration, we used a Max general utility plug-in (GUP) to glue Max and the asset management system together. The GUP is one of a number of DLL-based plug-ins that Max supports for modifying or extending existing functionality. Before reading the following explanation of how we integrated the GUP plug-in directly with the Max menu system, you may want to download the source code from the Game Developer web site at http://www.gdmag.com/.

The Max plug-in architecture is based on deriving developer-implemented classes from base Max C++ classes. In this case, our MaxUIModGUP class is derived from the Max GUP class. Max identifies plug-ins in two ways: through their file extension (.GUP for a GUP plug-in), and with a simple class factory called ClassDesc.

When this DLL plug-in is built, we change the file extension from .DLL to .GUP and place it in the MAX PLUGINS\ directory. As Max scans for each set of plug-ins it recognizes, it knows that this plug-in is at least used as a general utility plug-in. Once loaded, Max uses four simple functions to interrogate the DLL. LibDescription returns a simple text description of the plug-in. LibNumberClasses returns the number of class factories (or ClassDescs) in the plug-in. LibVersion is the version of Max that the plug-in will work with. And most importantly, LibClassDesc returns an instantiation of our own derived version of ClassDesc called MaxUIModClassDesc. Max can instantiate our MaxUIModGUP class using the MaxUIModClassDesc::Create member.

In the case of some plug-ins (for example a viewport plug-in), this class factory could be called multiple times. For a GUP it is only called once at startup. This is why we can simplify the code somewhat by using global variables to store our flags and state information.

Once instantiated, the MaxUIModGUP::Start member function is called. MaxUIModGUP has access to the main Max window handle using the inherited member function MaxWnd. Once we get the window handle, we can subclass it with our own message handler (SubclassWndProc) and return success. Subclassing the window ensures that we get the Max window messages before its message handler does.

In implementing our scheme to add new menu entries and enhance existing functions, we only care about two Windows messages: WM_INITMENU and WM_COMMAND. WM_INITMENU is sent to the window when a menu is about to become active. This allows us to look for the main menu, and modify its functionality. However, we must be careful to modify only the main menu, and then to modify it only once.

Because Max creates a number of menus, we use GetMenuItemCount and GetMenuString to make sure the menu we're getting is the main menu. Once we've ascertained that we have the correct menu, the modifyMenu function inserts two new entries into the file menu. For this sample, we're adding a menu option that will force a complete redraw, plus a separator to make the menu look pretty. In the end, DrawMenuBar is called to make sure that the menu is properly updated when it draws.

Back in the SubclassWndProc function, we need only add the WM_COMMAND case to look for the new menu entry we created (IDC_MAXMENUMOD), and process it accordingly. One additional piece of functionality that has been added to this WM_COMMAND handler is a wrapper for the Max File Open menu entry. This code stores off the current Max project filename, calls the default handler to open a new file, and then displays a message box to inform you if a new file has been opened.

Overriding the File Open function in this way may seem a bit dangerous, because the File Open menu entry might not be 0x9c43 in a future version of Max. Unfortunately, there's no other viable way to add a very important piece of asset management functionality: detecting when a user opens a file that he or she doesn't have checked out from the asset manager. While you can register a callback using the Max function RegisterTimeChangeCallback to determine if the current filename has changed, you don't receive the notification until after the file is opened. The Max notification system may look like another alternative, but it's just that: a notification. You can't stop or change something already in progress.

The asset manager needs to have a priori access to File Open requests for several reasons. If the user doesn't have the file checked out, it will be read-only. It might also not be the latest version of the file. Either way, if the file is under asset management the latest version of the file needs to be copied down to the user's workstation and made writable if someone else doesn't already have it checked out.

________________________________________________________

Beyond Asset Management