Loading Maps from a File
It's generally a good idea to separate content from code. In this case, the polygons and objects that make up the map are the content. In the previous chapter, you used the common Alias|Wavefront OBJ file format to define groups of polygons. In this chapter, you do something similar for maps, with the following goals in mind:
- To easily define a 2D map with variable-height floors and ceilings
- To define point lights and ambient light intensity
- To define starting locations of 3D objects and the player
- To easily edit maps
To accomplish these goals, I created a simple file format similar to the OBJ format, called the MAP format. Like the OBJ file format, MAP is a text-based format with one command per line, and comments are marked as lines that start with #. The commands are shown in Table 10.1.
Table 10.1. MAP Format Commands
mtllib <filename> | Loads materials from an external .mtl file. |
usemtl <name> | Uses the named material (loaded from an .mtl file) for subsequent polygons. |
v <x> <y> <z> | Defines a vertex with floating-point coordinates (x,y,z). |
ambientLightIntensity <v> | Defines the ambient light intensity for the next room, from 0 to 1. |
pointlight [v] | Defines a point light located at the specified vector. |
[intensity] [falloff] | Optionally, light intensity and fall-off distance can be specified. |
room [name] | Defines a new room, optionally giving the room a name. A room consists of vertical walls, a horizontal floor, and a horizontal ceiling. Concave rooms are currently not supported but can be simulated by adjacent convex rooms. |
floor [height] | Defines the height of the floor of the current room, using the current material. The current material can be null, in which case no floor polygon is created. The floor can be above the ceiling, in which case a pillar or block structure is created rather than a room. |
ceil [height] | Defines the height of the ceiling of the current room, using the current material. The current material can be null, in which case no ceiling polygon is created. The ceiling can be below the floor, in which case a pillar or block structure is created rather than a room. |
wall [x] [z] [bottom] [top] | Defines a wall vertex in a room using the specified x and z coordinates. Walls should be defined in clockwise order. If bottom and top are not defined, the floor and ceiling height are used. If the current material is null, or if bottom is equal to top, no wall polygon is created. |
obj [uniqueName] [filename] [v] [angle] | Defines an object from an external OBJ file. The unique name allows this object to be uniquely identified, but it can be null if no unique name is needed. The filename is an external OBJ file. Optionally, the starting angle, in radians, around the y-axis can be specified. |
player [v] [angle] | Specifies the starting location of the player and optionally a starting angle, in radians, around the y-axis. |
Listing 10.17 basic.map
# load materials mtllib textures.mtl # define a room ambientLightIntensity .5 room MainRoom usemtl roof1 floor 0 usemtl roof2 ceil 300 # define walls # a wall is defined from this vertex to the next vertex usemtl wall1 wall 0 150 wall 0 450 wall 800 450 wall 800 300 wall 500 300 wall 500 75 # define lights v 400 200 100 pointlight -1 1 300 v 700 200 400 pointlight -1 .5 1000 v 65 200 385 pointlight -1 1 100 # specify the starting location of the player v 400 0 300 player -1
This floor plan doesn't have any objects, but if you were to add some, it would look like this:
v -700 0 550 obj bot1 robot.obj -1 1.57
To read MAP files, there's a MapLoader class that is a subclass of ObjectLoader. Its parsing routines use a RoomDef class, shown in Listing 10.18, to define the rooms.
Listing 10.18 RoomDef.java
package com.brackeen.javagamebook.bsp2D; import java.util.*; import com.brackeen.javagamebook.math3D.*; import com.brackeen.javagamebook.graphics3D.texture.*; /** The RoomDef class represents a convex room with walls, a floor, and a ceiling. The floor may be above the ceiling, in which case the RoomDef is a "pillar" or "block" structure, rather than a "room". RoomDefs are used as a shortcut to create the actual BSPPolygons used in the 2D BSP tree. */ public class RoomDef { private HorizontalAreaDef floor; private HorizontalAreaDef ceil; private List vertices; private float ambientLightIntensity; /** The HorizontalAreaDef class represents a floor or ceiling. */ private static class HorizontalAreaDef { float height; Texture texture; Rectangle3D textureBounds; public HorizontalAreaDef(float height, Texture texture, Rectangle3D textureBounds) { this.height = height; this.texture = texture; this.textureBounds = textureBounds; } } /** The Vertex class represents a Wall vertex. */ private static class Vertex { float x; float z; float bottom; float top; Texture texture; Rectangle3D textureBounds; public Vertex(float x, float z, float bottom, float top, Texture texture, Rectangle3D textureBounds) { this.x = x; this.z = z; this.bottom = bottom; this.top = top; this.texture = texture; this.textureBounds = textureBounds; } public boolean isWall() { return (bottom != top) && (texture != null); } } }
The RoomDef class just keeps track of everything for a room defined in a map: wall vertices, the floor height, the ceiling height, and textures. Also, it has methods to create polygons out of the rooms, but those methods aren't shown here because they really don't do anything special. Some other code not shown here is the MapLoader because it's very similar to the ObjectLoader. At first, you might think it would be okay to have concave rooms because the BSP partitions will break down the rooms into convex polygons anyway. However, concave rooms have a complexity that our BSP building code wasn't designed for, as shown in Screenshot. In this figure, a partition splits a concave polygon, which results in four separate polygons. So, note that rooms in the map file should always be convex.
Screenshot Splitting concave polygons along a line can create more than two polygons. In this case, four polygons are created.