The Accidental Engine binaries and source code can be downloaded from here. Be warned-- this code is still very much under development, and has not as of yet been thoroughly tested, or even completed. It may fail horribly and catastrophically, resulting in horrible loss of life or limb. Nor has it been cleaned up and made presentable to the public, so there are certainly going to be ugly things and things to find fault with. If it is in your nature to gripe, feel free to gripe or complain to anyone but me. If you want to lodge valid complaints, give honest criticism, or (God forbid) level any compliments or thanks my way, I can be emailed at vertexnormal@linuxmail.org. As time goes by, and as I make more time for these tutorials, I will likely refine this engine and clean it up. It may even become a valid game engine upon which a real and actual game could be founded. In an infinite universe, as the cliché goes, anything is possible. In the meantime, feel free to use the code and modify it under the terms of the enclosed open source license. As it stands, the code is provided without warranty or guarantee, use at your own risk, I'm not liable, blah blah blah.
The test engine performs only a few simple tasks. When started, it will process the Config.txt file in the base directory for default configuration parameters dealing with screen/window resolution, default map dimensions and so forth. It will initialize a simple map and an animated character to walk around that map, then it will call the file data/scripts/startup.lua. Startup.lua performs some initialization and definition of various functions, opens the utility files data/scripts/mazefuncs.lua and data/scripts/propfuncs.lua, then executes the file named in the MapScript string variable to generate the map.
To test a new map generation technique, simply open the file startup.lua in a text editor such as Notepad, modify MapScript to point at your test script (provided sample scripts are in the data/scripts/ directory), save your changes, and execute the engine. If the generated map is simply a blank grass field, and this is not what you intended, then check to make sure MapScript is a valid filename. Also, check your console output for Lua errors, make the correct changes to your script and try again. Script debugging can sometimes be a fairly difficult process, and the Accidental Engine provides only very limited debugging and error detection capabilities, so best of luck.
UPDATE: Note that changing MapScript is now deprecated with the latest version of the Engine. The Engine now features a drop-down console (toggle with ESC) into which Lua script can be entered and executed. Rather than changing MapScript and restarting, you can now simply execute a new script directly via the console. For example: dofile("data/scripts/maze3.lua") will execute the map generation script maze3.lua.
Controls while the application is running are simple. The NumPad 8,4,2 and 6 keys control direction. 'Q' will exit the application and 'S' will capture a screenshot. Textual output is logged to the file systemlog.txt in the base directory, though error messages generated internally from the Lua library are not logged. The application features a smooth-scrolling, 2D tilebased view of the world from a 30 degree frontal viewpoint--basically an isometric camera without the 45 degree rotation around the vertical axis that would make it truly isometric. Thus, the tiles are displayed at a 2:1 ratio; tiles are twice as wide as they are tall onscreen. A square room, then will appear onscreen as a rectangle. It is a viewpoint popularized in many older console-style RPGs, which I chose for its simplicity of implementation. The viewpoint scrolls to keep the player character centered, but corrects the centering so that the view does not scroll off the edge of the map. The engine is much lacking in the many systems and details that make a full-fledged game; though movement is supported, the props and objects that are placed are 'dummy' objects with no true functionality. In a real game, such objects would be far more complicated, but the principles of placement would remain generally the same.
The map is, of course, organized by tiles which can be indexed using a pair of tile coordinates in the ranges of (0..MapWidth, 0..MapHeight). Actual objects in the world (the player, props, etc...), however, utilize another coordinate system called world coordinates. Each tile in the world comprises a block of world coordinates of a given dimension. This tile dimension is given as TileSize in the configuration options (Config.txt in the base directory). So, if TileSize=64, then each tile in the map comprises 64 world coordinate locations. Thus, a map that is sized 129x129 tiles will have a world coordinate space for objects of (64*129)x(64*129), or 8256x8256. All objects must have position coordinates in the range of (0..8256) in order to be within the boundaries of the map. Object world coordinates are stored internally in floating point representation to allow for smooth scrolling and interpolated movement.
When modifying terrain, walls or tiles, tile coordinates are truncated to integer and used as array indices. Out-of-bounds indices are ignored and cause no actions to be performed. World coordinates can be converted to tile coordinates by simply dividing the x and y components (converted to integer) by TileSize. The offset, or position of an object within it's owning tile, can be found by taking the modulus of the x and y components(converted to integer) by TileSize.
Map generation is accomplished through a MapBuilder interface exposed to Lua by the engine to implement certain basic low level functions. The breadth of functionality implemented by the interface will grow and evolve as these tutorials progress. To begin with, the interface is simple. I will describe briefly the main steps involved in constructing a map generation script.
Certain globally available systems (the map builder, the level, the player, etc...) are made available through a static interface class called GlobalsInterface for simplicity, not a technique I would normally choose in a real game but one which simplifies certain tasks for the sake of the tutorials. The first thing the map script should do, then, is to call this interface to obtain a reference to the MapBuilder interface object to use. In the file startup.lua you will find this line, which will give us the Builder object for further use:
Builder=GlobalsInterface:GetMapBuilder()
All of our map generation scripts will use this Builder object to perform the necessary functions of floor and wall placement and so forth.
In the map script file itself, we need to perform a few standard steps that vary little from map to map. First, we need to define the dimensions of our map. MapWidth and MapHeight have defaults which are specified in Config.txt, and which can be accessed through the Lua table ConfigTable; or we can specify overrides to obtain any map dimensions we desire.
MW = 129 MH = 129
Before any map construction can begin, we must initialize the map and the temporary structures that will be required during the construction phase. We do this through a call to the ResetMap() function of the Builder interface:
Builder:ResetMap(MW, MH)
This function clears all data structures, re-allocates the map to the proper given dimensions if necessary, and starts the process with a clean slate.
After the map has been reset, the script then performs the various actions and function calls to generate the level. When this process is complete, map construction is finalized using the Builder:FinalizeMap() function. This function cleans up temporary data, and performs the iterative translation process on the working map data to calculate transition tiles for walls and floors, and randomize tiles where there is a set of possible choices of slightly different tile varieties to choose from. FinalizeMap() iterates through floor and wall arrays, and analyzes each tile and that tile's neighbors, generating a pattern code corresponding to the pattern of neighbors of that tile. This pattern code is used to choose from among 46 different tile transition configuration types, for floor or wall types that require transition tiles, and the corresponding transitional tile is added to the engine's Map to designate the graphic to draw at that tile location. This allows the map generation script to not worry about correct placement of borders and transitional tiles, and just worry about raw placement of walls and floors, letting the translation pass take care of assignment of graphics to tiles.
The actual level construction functions provided by Builder are for now very simple, and allow for the placement of different wall and floor types, and also allow for flagging tiles for different conditions. The basic functions are as follows:
Builder:SetWall(tx,ty,type)-- Set a tile's wall type to the given type parameter. Types are simply enumerated starting from 0(no wall). As it currently stands, the Accidental Engine only provides graphics for 1 wall type (type=1), and has room for one more wall type if desired, so using wall type > 1 may fall back to defaults or have unintended visual artifacts.
Builder:SetFloor(tx,ty,type)-- This sets the type of a given tile's floor. The Accidental Engine provides 3 types of floors (for type=0, type=1 and type=2). They are Grass, Dirt and Water. Additionally, there is space allocated for one more floor type.
Builder:SetFlag(tx,ty,flag)-- This sets a flag bit for the tile.
Builder:ClearFlag(tx,ty,flag)-- This clears a flag bit for the tile.
Builder:GetFlag(tx,ty,type)-- This queries the state of a flag bit for a tile.
The relevant flags for a tile are:
TF_BLOCKWALK-- Blocks walking characters TF_BLOCKFLY-- Blocks flying characters TF_OFFLIMITS-- Prohibits further modification of wall and floor type for the tile TF_NOSPAWN-- Prohibits the spawning of monsters on the tile TF_NOITEM-- Prohibits the spawning or dropping of items or other objects on the tile.
Builder:WallEdges(type)-- This function fills in the edges of the map with the given wall type.
Builder:FillRectWall(tx,ty,w,h,type)-- This function fills a rectangle located at (x,y) and of dimensions (w,h) with the given wall type.
Builder: FillRectFloor(tx,ty,w,h,type)-- This function fills a rectangle located at (x,y) and of dimensions (w,h) with the given floor type.
Builder:InstanceProp(name, width, height, offsetx, offsety)-- This function will create an instance of the given prop name, locating the graphics and loading them if they are not already loaded, and initializing an individual object instance of the prop ready for placement within the map. Props include chests, trees, jars, rocks, and so forth.
Builder:PlaceProp(x,y,prop)-- This function places an initialized prop instance within the map. x and y should be specified in world coordinates, rather than tile coordinates. The file startup.lua defines a helper function called TileCenter() which will calculate the world coordinates of a given tile's center, in order to place props in the center of a certain tile. For the most part, object and prop placement in the provided sample scripts will use TileCenter().
Builder: DiscardProp(prop)-- This function will throw away a prop, for instance if it was unable to be correctly placed or if it is no longer needed. If a prop is not successfully placed within the map it should be discarded to keep it from factoring into game mechanics when the game is played, since even non-placed objects, once instanced, show up as 'active' to the engine. Certain object generation and placement schemes are implemented such that they attempt to place an object some number of times, and fail gracefully. Graceful failure includes releasing the prop so it doesn't take up an object structure that could be used elsewhere.
Objects are pooled in the engine, so there is no hard-coded limit on the number of objects allowable. However, you should keep memory constraints and rendering speed in mind when attempting to instance and place huge numbers of objects.
UPDATE: The following functions have been added to the latest version of the Accidental Engine, and may be called from script or from the console.
ConsolePrint(str)-- Print a string to the in-game console, rather than to stdout.
SetStaticLight(x, y, r, g, b, range)-- Create a static light source and place it in the map. Static lights light a circular area, with fall-off range defined by range. Position is given in world units (ie, tile coords * tile dimensions) and the color is given as floating point numbers in the range of [0,1] for each component r, g and b.
SetStaticLightTile(tx, ty, r, g, b)-- Adds a light value of (r,g,b) (each in the range of [0,1]) to the current static lighting of the given tile. Doesn't really have a practical use.
SetDayLevel(r, g, b)-- Sets the color of the ambient light during full daylight time. (r,g,b) are specified as floating point numbers in the range of [0,1].
SetNightLevel(r, g, b)-- Sets the color of the ambient light during full night time.
SetLightCycle()-- Sets the in-game lighting engine to derive ambient light from the rhythmic day/night cycle via interpolation between DayLevel and NightLevel (set in the preceding functions).
SetLightConstant()-- Sets the in-game lighting engine to maintain a consistent lighting level, rather than cycling through day and night periods.
SetTime(Min, Hr)-- Set the in-game clock (affecting the day/night cycle) to a specified time.
SetNoon()-- Helper function to set the in-game clock to noon, with full daylight.
SetMidnight()-- Helpler function to set the in-game clock to midnight, with full night lighting.
SetDayParameters(Min, Hr)-- Configure the cyclic parameters of the in-game clock.
PlacePlayerAt(tx, ty)-- Place the player at the center of the specified tile.
LoadFloorSet(name)-- Load a landscape tile into the next available tile slot. Resetting the map resets the available slots, so after the map is reset a new set of landscape textures can be loaded. Floor textures are loaded from .TGA format files.
LoadWallSet(name)-- Load a set of walls into the next available wall slot. Resetting the map resets the available slots, so after the map is reset a new wallset can be loaded. Wall sets are loaded from .TGA files, two images per wall set and named some_nameA.tga and some_nameB.tga.
The engine also exposes various other utility functions that can come in handy, including a set of random number functions that we will use extensively.
SetSeed(seed)-- This function seeds the random generator with a given value.
SetSeedTime()-- This function seeds the random generator from the current time, and is useful for providing an unpredictable seed to start with.
RandInt()-- This function will generate a random integer.
RandTarget(target)-- This function will generate a random integer in the range of [0,target].
RandRange(low, high)-- This function will generate a random integer in the range of [low,high].
Rand01()-- This function will generate a random floating point number in the range of [0,1].
DiceRoll(numdice, sides)-- This function emulates a set of dice rolls. numdice determines how many times to roll the dice, sides determines how many sides the dice have.
With these basic functions, we have a good start at a foundation for our level scripting. We will of course add to this list of functionality as our needs require, but to get started it gives us plenty to work with.