FEATURE September 10, 2001
Using Max Script for Building Game Levels
by Shailesh Watsa
We were looking for an optimal way to build our levels from inside 3D Studio Max. Lofting and other techniques used earlier to make level meshes were rigid to changes. We needed a method where changes to the level mesh could be incorporated without too much effort. We wanted to have a system within Max which had the ease associated with CSG (Constructive Solid Geometry). When I saw the way Max could boolean objects, especially even objects which were non intersecting, I thought I could make use of this feature in Max to build and derive meshes which were like the ones a CSG builder would probably output.
This article does not discuss the actual script functions that I used, but will explain the logic and the way I have used Max script to build a small system within Max to build game levels for our game engine. One could always adapt and create scripts that meet their own needs.
Using Max Script to Build a Game Level Building System Inside Max
The first and foremost task is to understand what your game engine exactly requires; whether the level mesh can be just a collection of polygons mapped with different textures, or if they have to comply with certain criteria necessary for it to be used by your game engine. It can vary depending on how exactly your game engine wants the level data to be organized. Most game engines prefer the world data to be built or organized in a particular way.
In our case, the game engine needed the level mesh to be BSP (Binary Space Partitioning) friendly. The mesh exported from Max had to fulfill two basic criteria. First, every edge in the mesh should be two manifold, and second no face should intersect another. If the mesh fulfills these two conditions then there will be no cracks or gaps in it.
In addition to our game engine, we also have our own lighting editor, which we use to light up these game levels that we create. We also place other special effects such as fire, smoke, etc within the lighting editor. The lighting editor also needs the mesh to be BSP friendly in order to calculate lights faster. Moreover, it requires the mesh to have light maps assigned and mapped to them, which it fills when the level is being lit.
As mentioned earlier, we wanted our system to have the ease of a CSG level builder. We adapted Boolean functions inside Max to create an environment like CSG. The level building starts with defined primitives. Later using Boolean functions, we derived a level mesh from these primitives that is BSP friendly. The Boolean operations are covered in the second part 'Building the World'.
One could probably put in more time and use a BSP tree to build the level mesh from these primitives. A person building a game level using this in Max, starts with primitives like in a CSG level builder. The actual world is built or updated every time the user presses the Build World button.
First, we have to define the primitives that the user can use to build the world. A check on other CSG editors like Genesis and Unreal Ed, gives a fair idea on the various primitives that they use. Since the script does Boolean operations on the primitives, the primitives should support Boolean operations without any errors. Almost all the Max standard-objects such as the Box, Sphere, Cylinder, Pyramid, Cone, Tube, and Torus worked well with Boolean operations. Some of the Extended Objects are useful too and can be booleaned properly.
Since the user wouldn't know which primitive could be used and which couldn't, a customized GUI can be made which has all the primitives that could be used, as buttons. While building the system, Max functions, features and even standard buttons have been adapted wherever possible. However, creation of all the primitives using custom scripts is preferred, since it would give more control over the creation of primitives and assignment of various properties to them.
Figure 1: The customised Max interface shows buttons, which call appropriate script files. The primitives that can be used to build the world have all been set on the left side.
In addition to these primitives, one could make their own, which they deem as useful. Four primitives, a ramp, ramp with railings, stairs and stairs with railing were made for this system. This is covered later in the article under Creating Custom Primitives.
Building the World
The user building a level mesh using this system in Max starts building primitives. The primitives are assigned appropriate materials and textures. The primitives can even have different materials assigned to different faces.
The primitives can be either hollow or solid like in a CSG builder. Through script, a user property called bool is set for each of the primitives. This bool property saves either one or two against it. One stands for hollow primitive and two stands for solid primitive. By default, the primitives created are set to hollow.
After creation, these primitives have to be added into the scene by clicking the Add CreateID button. This script assigns a CreateID user property to the object. The CreateID property stores a value against it, which determines the order of creation of these primitives. This order is needed while doing Boolean operations. The user can now press the Build World button to build or update the level mesh.
When the user presses the Build World button, the script first searches through the scene to find and collect objects that have a CreateID property. Then it runs through the collection of objects or primitives as we refer to them, and sorts them in ascending order according to the value stored against the CreateID property. This is important since Boolean functions could end up with different results depending on the order in which the objects are booleaned.
At first the script was depending on Max, since internally, Max seemed to have some kind of order which matched the order of creation of these primitives. However, when any primitive were hidden or frozen and later unhidden or unfrozen the order seemed to change. Hence, the Assign CreateID button was introduced which determined the order of creation of the primitives. This later made the system much more flexible, since the user could change the order of the primitives at any time by using a button called Force CreateID. The Force CreateID script let the user assign any CreateID for the primitive thereby enabling the user to move the primitive up or down the order of creation.
Getting back to the Build World script, once the primitives are in order, the script starts by creating an object named Parent. Henceforth, this object will be referred to as the parent. The parent is basically a box constructed through script. The total bounding area of all the primitives is ascertained, and the parent size is set to be slightly bigger than this. Then the parent is positioned such that it covers all the primitives. This is done because in most CSG environments one starts with solid space and starts carving out hollow spaces inside it. The parent object, being a standard solid box in Max, is much like solid space in a CSG environment.
Once the parent is created, each primitive is taken in the order in which they now exist in the collection of primitives, and booleaned with the parent. While booleaning these primitives with the parent, the script checks whether the primitive being booleaned is a hollow or solid primitive. This property is received from the 'bool' property of these primitives. All primitives that do not have this property against them are set to be hollow as default. The Boolean operation is set to subtraction if the primitive is hollow while booleaning it with the parent. If the primitive is solid then the Boolean operation is set to union.
Moreover, while booleaning the primitives, the pick object is set to be an instance of the actual primitive being booleaned. This way surprisingly even after multiple booleans, any mapping change done to any primitive used in boolean operations, affect the mapping in the booleaned object or the parent in this case. This is very useful since if the user later changes the mapping on any of the primitives it is instantly updated in the parent object.
The script also does a weld-on-threshold on the vertices of the parent after every Boolean. This is done to ensure that there are no open edges in the parent object. If the parent develops open edges after any Boolean operation, further Boolean operations end up with undesired results. It is also better not to have any open edges if you want the mesh to be BSP friendly. The code for this appears in Listing 1.
Once all the booleans are done, the parent is almost complete. Now, all the edges of the parent object are set to invisible. Then all the primitives are set to Box mode. This is a small trick to ensure that only the parent object is visible in a shaded viewport and only the primitives are displayed in the viewports set to wireframe.
Figure 2: The wireframe viewports show the primitives while the shaded viewports show the built world inside Max.
By doing this, the Building of the world is complete. We now have the primitives and the parent, which is the built world.
Continue to page 2. >>
(Originally published in Gamasutra August 2001.)