From Hero of Allacrost
Map mode is responsible for a large portion of the player experience. This makes it a very large code base of around 7,000 lines of C++, or 10% of all Allacrost C++ code (as of 1 October 2008). Because it is so large and there are so many components, it is vital that we as programmers keep the code organized so that it is easy to understand, maintain, and extend. The purpose of this document is to provide an overview of how this code works so that its easier for programmers with no experience to jump right in. We don't get too specific about the implementations or data structures here. That is something that you should learn by actually looking at the code, or referring to the doxygen-generated API documentation.
Explaining map mode is tricky because there's no real "starting point" for a programmer since most of the components in map mode are so tightly integrated with one another. Thus we break up the explanation of map mode into three sections. First we explain the concepts of map mode. These concepts are the fundamental ideas that went into the design of the map mode code and help the programmer to get an overview of the various elements that are found in Allacrost maps. The structures section goes into more specifics, providing a list and brief explanation of all classes and files in map mode, and how those structures are organized together. Finally we cover the operation of map mode. Here we describe the major steps that transpire when a map is loaded, updated, or drawn to the screen. You should read each section in this order: 1) concepts, 2) structures, 3) operation.
NOTE: View the discussion page for this article for information about the areas of this documentation that are incomplete or missing.
There are a few concepts about Allacrost maps that are absolutely critical to understand in order to make any sense of how the map code operates (and why it is designed in the manner that it is). Many of these concepts are intertwined with each other, which makes it difficult to explain them individually. So first lets briefly discuss them together as a group.
- The most common three visual elements found on maps are tiles, objects, and GUI graphics.
- The map environments (terrain, structures, etc.) are constructed by the map tiles, which are 32x32 pixels each.
- Map objects are any entity on a map which are not tiled and they can be virtually any size. Map sprites are a type of map object which move around on the screen.
- One of the primary challenges of the map code is to prevent scenes that look "incorrect", such as a sprite walking on top of the exterior of a house or two objects overlapping each other on a map. Two methods are used to achieve this correctness. First, each map has a collision grid of 16x16 pixel squares with a boolean value that represents whether or not an object may occupy that area of the map. Second, all objects have defined a collision rectangle and the map code prevents any collision rectangles from overlapping each other at any time.
- Tiles and objects are drawn in several layers to construct the map visual and are always present on the screen. The GUI elements are drawn above all tiles and object, but are not always present in the rendered screen.
The primary coordinate system used for drawing graphics in map mode is 64.0 in length and 48.0 in height. The coordinates increase from left to right and from top to bottom, so the coordinates of the top left and bottom right corners of the screen are (0.0, 0.0) and (64.0, 48.0) respectively. In a 1024x768 screen resolution, this system divides the screen up into 16x16 pixel squares. It is not a coincidence that the map collision grid is also composed of 16x16 pixel elements. The visual coordinate system was based around the map collision grid dimensions for reasons that will become more clear in the camera section to follow.
All tiles are 32x32 pixel images that are extracted from a larger tileset image. Each tileset image is 512x512 pixels and can hold 256 tile images. There are three layers of tiles in a map, referred to as the lower, middle, and upper layers respectively. The lower tile layer is drawn first and typically represents the surface terrain. Middle layer tiles usually represent embellishments upon the lower layer, such as the placement of bushes or statues. The upper layer tiles are the only tiles that are drawn above any map objects and they usually represent rooftops and other "high altitude" parts of the map environment. Most tiles are still images, but tiles on all layers may be animated.
Objects encompass all tangible map entities which are not tiles. There are many types of map objects, such as sprites, treasures, or stationary structures such as a statue. Many objects may be interacted with by the player, but objects are not required to have this property. Each object is located via a single X and Y coordinate on the collision grid. This single coordinate is located at the bottom center of the object. Objects are represented by two rectangles which share this bottom center coordinate. The first rectangle represents the object's image and the second rectangle represents the collision rectangle. Neither rectangle is mutable (sizes are not allowed to change). Both rectangles can be "deactivated" by changing the objects property, making it immune to collisions or invisible from the screen. We determine the size of each rectangle by storing their height and half-width, from which we can compute any corner of either rectangle using the single X, Y position coordinate.
Like tiles, map objects also exist on three layers. A map object may exist on one and only one layer at a time. The three layers are the ground layer, pass layer, and sky layer. Most objects exist on the ground layer, meaning that they are affixed by gravity to a surface. The pass layer is a special layer that is used to create objects that ground objects may both travel under or over, such as a bridge. Sky objects are those objects which exist at an altitude greater than all other object and tile layers. Sprites are the most common type of object, but there are many forms of objects that appear on maps. A list of object types is below.
|Physical Object||A stationary map object with no special interactions|
|Treasure Object||A stationary object (like a treasure chest) that contains drunes and/or inventory for the player to procure|
|Virtual Sprite||A mobile object with no visible image|
|Map Sprite||A common character that can be animated and interacted with through dialogue or other means|
|Enemy Sprite||Causes a battle to occur should the player's character sprite come into contact with it|
The collision grid is an invisible grid of 16x16 pixel squares that cover the entire map. Because each map tile is 32x32 pixels, this means that four entries of the collision grid fit inside the four quadrants of each map tile, and all map tiles are designed with this property in mind. The only property that the collision grid holds is a boolean stating whether or not a particular grid element may be occupied by a map object (passable or impassable). Because the collision grid aligns with the map tile grid, the map tiles store the information about which of their quadrants should be passable and which should be impassable. Specifically, this information is stored in something called a tileset definition file, which is a Lua file from which the properties of each tileset are contained and loaded by the map.
The map camera is always focused on a single map object, hence it is nothing more than a pointer to an active map object. Each map screen is drawn such that the focus is on whatever object the camera is pointing to, which is usually the character sprite that the player is controlling. The camera attempts to keep that object in the center of the screen, but if the object is too close to the edges of the map then the camera adjusts so that the view does not show any part of the screen beyond the map boundaries. Each map has a special invisible sprite created to serve as a focal point for the map when the camera needs to focus on an arbitrary location where no visible sprite exists. This is useful when trying to pan the map to observe a far away event that occurs off the screen from the current camera location.
Map contexts are one of the more complicated and unique concepts of Allacrost maps. You can imagine a map as consisting of several smaller maps, where each smaller map is represented by a map context. For example, the outside area of a town may be one context, while the interior of a home in that town may be another context. The purpose of contexts is to allow us to quickly and efficiently transition to a different area of the map without having to represent it as its own unique map. This way, we can continue to simulate and update the environments of all of the contexts in a map and avoid any additional loading or re-loading of map data.
Each map must have at least one context and can have a maximum of 32 contexts. The primary context in a map is called the base context, and is also referred to as context #01. Each context has its own individual tile grid and collision grid. This means that when switching from one context to the next, all the map tiles are subject to be changed to represent the view of the new context. All map objects exist on exactly one context and can not interact with one another unless both objects are in the same context. This means that a sprite in context #04 can't speak to (or usually even see) a nearby sprite in context #01. The collision grid for context #01 will not have any effect for a map object that exists in any other context as it only effects objects that exist in context #01.
The context of the object that is pointed to by the map camera is called the active context. It determines which tiles and map objects are visible on the screen. Typically map objects which exist on a different context than the active context are not visible, but a special property of objects may be set so that it is visible on more than one context. It is not uncommon for a map sprite to change their context multiple times on a map (stationary map objects are much less likely to change). The most common manner in which sprites change their context is for them to move into what is called a context zone. A context zone is a designated area of a map where objects may transfer between two different contexts (and only two contexts). Context zones are a type of map zone, which is explained below. Objects can also have their context changed manually by a script function as well.
A map zone is nothing more than an area of the map that has a special property. The programmer can create a map zone on a granularity of 16x16 square pixel elements (the same as the collision grid). The may be of any size and shape that can be described in rectangular sections, and the sections may overlap (although it is inefficient code for them to do so). Map zones are used for a variety of purposes. One such purpose is to trigger scripted events, such as when the player moves their character sprite over a visible floor switch. A context zone allows sprites to transition between two contexts and are usually found near the entrances of doorways and stairwells. An enemy zone defines the area where enemy sprites will spawn and roam on a map. Many more special purpose zones may be defined in the future to enable other common functionality such as audio sources or poison marshes.
Dialogues are an important part of maps as they are the primary means through which the player interacts with other characters and learns the information needed to proceed through the game. A single dialogue window is drawn on the bottom of the screen when a dialogue is active. Dialogues are constructed in such a way that they can have any form of flow from one line to the next (including circular flows), so do not falsely assume that they must be linear. Dialogues may be injected with options to present to the player for them to select. The set of options allow the dialogue to "branch" in different directions based upon the option that the player selected. Programmers have the ability to define a script function to execute after each line is read and/or each option is selected. This grants us with an almost limitless flexibility to make dialogues an integral part of action scenes and other events.
Dialogues can occur between any number of characters, or may be a single character monologue. Most dialogues are between two sprites, usually an NPC and the player's character. All map sprites can reference any dialogue. By referencing a dialogue, a sprite indicates that it is one of the possible dialogues that should be executed if the sprite is spoken to by the player or through another means that begins a dialogue. A dialogue may be referenced by more than one sprite, although this situation is rare. Many sprites may reference more than one dialogue. All dialogue references are stored in a vector container (in the order in which the references were added) and the sprite contains a pointer to the next dialogue to be displayed and is incremented whenever a dialogue with the sprite has finished.
Dialogues additionally have the ability to set the maximum number of times that it may be viewed before becoming unavailable. This is used, for example, for dialogues that we only want the player to be presented with a single time. Once a dialogue becomes unavailable, it can no longer be viewed by the player. It may be made available again by changing the max view count or resetting the number of times that the dialogue has been seen. We may initially choose to set dialogues to a maximum view count of zero, and only set it to a non-zero value after a particular event on the map or elsewhere has transpired. When a sprite references a dialogue that the player has never read before (and is also not unavailable), a small animated "chat" icon will float above the sprite's head. The chat icon will only disappear once all available dialogues have been read at least once by the player.
Treasures may either be plainly visible (like a chest) or be partially or fully hidden (like an occasional sparkle on the ground). When the player interacts with a treasure object for the first time, a small menu window pops up. This window displays the contents of the treasure which the player may view the details of.
Map sprites that are not controlled by the player need to be given instructions on what to do so that they do not simply remain stationary and motionless. The solution to this problem is what we call sprite actions. All map sprites have a vector for holding actions and will execute each action one at a time, and then repeat the cycle. Actions may include path movement (moving from a source to a destination), random movement for a period of time, displaying special animation frames for the sprite, or other forms of action.
TODO: In map code terminology, an "event" is any action which takes control of the game away from the player in order to let a scripted sequence play out.
TODO: outline audio sources (mostly sounds) and how they work
Every single map has associated with it a Lua script file that defines the properties and provides facility for customizing the map via script functions. This file is typically referred to as the map file, but is also sometimes referred to as the map script file or script file. The data it contains includes the dimensions of the map, the tiles and tilesets used, all map objects and sprites that are used, dialogues spoken, the number of contexts in the map and associated tile and collision grid data for each, the audio files that are used on the map, and many other things. There is a wiki page dedicated to explaining in more detail about the contents and format of the map file at this location Map_File.
Every map file contains at least three functions called Load, Update, and Draw. The load function is called only once when the map is constructed and assists the map in loading external data (images, audio files) needed and properly initializing the map for use. The Update and Draw functions are both called once per execution of the main game loop when the map they represent is the active game mode. These functions perform any custom operations necessary to construct the map, such as updating scripting events or drawing graphical effects.
Now that we have a grasp of the major map concepts, lets discuss how we represent them in the code.
The highest level class in the map code is the MapMode class, which derives from the GameMode class in the mode management engine. Because there is so much to manage and process for each map, there are a number of "supervisor" classes that handle a specific task set so that the MapMode class can remain a reasonable size. The MapMode class creates an instance of each of these supervisor classes upon construction. The table below lists all supervisor classes and what types of data and operation that they are responsible for. Additional data and responsibilities of the map code not listed in the table are usually managed by the MapMode class directly.
|Supervisor Class Name||Data and Responsibilities|
|TileSupervisor||Holds all map tile data and is responsible for all tile update and drawing operations|
|ObjectSupervisor||Holds the collision grid and manages all map objects, including collision detection, pathfinding, and drawing|
|DialogueSupervisor||Contains all map dialogues and processes dialogues when they are activated|
|TreasureSupervisor||Displays the contents of procured treasures and manages user input while this window is active|
The map code is split into several C++ files (and many more map script files are written in Lua). Each C++ file begins with the word "map" to make it clear that the file belongs to map mode. The table below lists each file name (minus .h/.cpp extension) and what map classes that file contains. A brief explanation of each class is also provided.
|File Name||Class Name||Class Purpose|
|map||MapRectangle||Represents a rectangular section of a map|
|MapFrame||Retains information about how the next map frame should be drawn|
|MapMode||Handles the game execution while the player is exploring maps|
|map_actions||SpriteAction||An abstract class for representing a sprite action|
|ActionPathMove||Moves a sprite from a source position to a destination|
|ActionRandomMove||Action for causing random movement of sprites|
|ActionAnimate||Action that displays specific sprite frames for a certain period of time|
|map_dialogue||MapDialogue||Represents dialogues between characters on a map|
|MapDialogueOptions||A container class for option sets presented in dialogue|
|DialogueWindow||A display window for all GUI controls and graphics necessary to execute a dialogue|
|DialogueSupervisor||Manages dialogue execution on maps|
|map_objects||MapObject||Abstract class that represents objects on a map|
|PhysicalObject||Represents visible objects on the map that have no motion|
|PathNode||A container class for node information in pathfinding algorithms|
|ObjectSupervisor||A helper class to MapMode responsible for management of all object and sprite data|
|map_sprites||VirtualSprite||A special type of sprite with no physical image|
|MapSprite||A mobile map object with which the player can interact with|
|EnemySprite||A mobile map object that induces a battle to occur if the player touches it|
|map_tiles||MapTile||Represents a single image tile on the map|
|TileSupervisor||A helper class to MapMode responsible for all tile data and operations|
|map_treasures||MapTreasure||Represents a treasure on the map which the player may access|
|TreasureSupervisor||Displays the contents of a discovered treasure in a menu window|
|map_zones||ZoneSection||Represents a rectangular area on a map|
|MapZone||Represents a zone on a map that can take any shape|
|EnemyZone||Represents an area where enemy sprites spawn and roam in|
|ContextZone||Represents an area where the active map context may switch|
The MapMode class has a state member that determines how to update and draw the map and how to process input commands from the player. The current state also determines which of the supervisor classes should be called to update the state of the map and draw graphics to the screen. The MapMode class state member can be set at any time from anywhere in the map code or map script file. The table below lists all map states, the active supervisors in each (active indicating that it has update/draw calls being made to it by MapMode), and an explanation of each state.
|Supervisor Class Name||Data and Responsibilities|
|Explore State||TileSupervisor, ObjectSupervisor||The default map state where the player is free to explore and interact with the map|
|Dialogue State||TileSupervisor, ObjectSupervisor, DialogueSupervisor||Active when a dialogue is taking place. The dialogue window is present on the screen during this state.|
|Treasure State||TileSupervisor, ObjectSupervisor, TreasureSupervisor||Active after the player has procured a treasure. The treasure menu is present on the screen during this state.|
Typically a map starts in the explore state, although this is not always the case. The other states are entered from the explore state by the user triggering an interaction with the map either intentionally (by talking to a NPC sprite) or unintentionally (by moving their character over a hidden switch). The state that is triggered will run its course and then usually returns to the explore state, although it may choose another state to put the map in once it is finished. As a quick example, imagine we are in the explore state when the user talks to a nearby NPC, moving the map to the dialogue state. In the middle of the dialogue, we may be sent to the event state due to a sudden occurrence like an explosion. A second dialogue to occur immediately after the event, sending the map back into the dialogue state before finishing and returning to the explore state.
To create and load a new map instance, first a new MapMode object is created. The MapMode constructor requires a string argument, which represents the name of the map file. The constructor will verify that it can access and open the map file and then create instances of all supervisor classes as well as any miscellaneous setup for its other members. The MapMode class will extract the basic properties from the map, such as the name of the map and the number of contexts it uses, load all audio files used on the map, and create objects to represent each type of enemy that the player may encounter on the map. The supervisor classes may also have their Load functions called if available to extract their relevant data from the map file. Finally, MapMode will call the Load function defined in the map file. This allows each map to perform any custom load operations that may be necessary, such as changing the state of map objects or dialogues from their defaults.
The TileSupervisor has a particularly complex load function. It has to open every tileset definition file for every tileset used by the map. It first determines which tiles of the tileset are used and which should be discarded (so that we don't waste video memory with tile images that will not be used). It also determines if any tiles are animated and collects all the animation frames and timing into a single animated image. And then it must perform a somewhat complex index translation so that the map tiles are referencing the correct images in the image vector. The ObjectManager has a much easier task of loading the collision grid from the map file data.
Load operations for map can take a long time (a few seconds) depending on the size of the map. To mask this latency, we perform a couple tricks. First, we do a gradual fade to blackness out of the current map to buy us some time. Second, we have the ability to pre-load maps in a different thread while the current map is active. So if the player exits the current map to another, we may already have the map pre-loaded and ready to be used immediately.
The steps that the Update function performs vary based on the current map state. In general, the following operations are performed in this order.
- Call the map file's Update script function
- Accept and process user input
- Update the display status of all animated tiles
- Update all zones and objects on the map
User input is handled in multiple places in the map code. In the explore state, it is handled by the MapMode class. In the dialogue and treasure states, it is handled in the DialogueSupervisor and TreasureSupevisor class respectively. The most complex portion of the update operation is that of updating all map sprites. Map sprites in motion have to be constantly re-examined for collision detection and to process the next action in the case of sprites not controlled directly by the player. We also have to figure out if their path is blocked and needs to be re-computed, which direction they are facing and which animation should be drawn for that sprite in the next frame display. Each map object has its own implementation of an Update function and each object is updated in significantly different ways.
Before any draw operations begin, the very first thing that the draw code does is to figure out the position of the map camera. This information allows the draw code to know which tiles and objects would be visible on the screen should they be drawn. After this information is determined, the map file's Draw function is called. Usually this will do nothing more than draw the tile and object layers, although it may do additional drawing operations. For example, a map with fog would require the script to call a function in the video engine to generate that visual effect. After the map file completes its draw code, all active GUI displays are drawn.
The standard order of draw layers in map mode is below. Note, however, that a map script does have the ability to draw these layers in any order that it wishes (very rare) or insert other draw calls in between any of the draw layers (not uncommon). Recall that ground objects are drawn in two passes to allow them to go over and under certain structures.
- Tiles: lower layer
- Tiles: middle layer
- Objects: ground layer (pass 1)
- Objects: pass layer
- Objects: ground layer (pass 2)
- Tiles: upper layer
- Objects: sky layer
MapMode is really the cornerstone mode of operation for the game. Most other game modes are entered from map mode and they return control of the game to map mode when they are finished. The table below illustrates how map mode is entered from other game modes and under what conditions it exits to other game modes.
TODO: create the table showing battle mode, menu mode, shop mode, boot mode, map mode, pause/quit mode transitions.