[b]Design[/b]
With these two extremely ambitious goals, I was faced with essentially completely reworking the engine's framework with Marcel's editor in mind. I decided to start with a pencil and paper and create a kind of right-brained map of what portion of this code-base the engine would be using and what portion the editor would be using. Here is my final design:
The left side of the page is used exclusively by the Engine. The right side is exclusively the level editor (notice it's all blank, that's up to Marcel). The middle is utilized by both the engine and editor and was the main area of interest for this new framework. Take note of the fact that "Asset" is the code-base utilized by both. Asset also has a minor dependency on libGyro, which means that Marcel's editor must also include a tiny amount of the libGyro framework as well.
Before we get into specifics, lets recap our design. Elysian Shadows is split into "levels" that each have their own "areas." These areas have a "map" which is the terrain. This terrain is inhabited by "entities" which are composed of "components." Entities are anything ranging from playable characters and enemies to items, rigidbodies, and warps.
As you notice from the gradient, most things have an "editor" level of abstraction that is used by both the engine and editor. This is inherited by the actual engine implementations which add more functionality (for run-time).
[b]AssetIO[/b]
The first ambitious goal was to use the exact same code to save and load everything in the engine and editor. This will give us the advantage of never having to get off-sync again. If I add a new attribute or component to the engine, it is automatically added to the editor. This means that our maps aren't going to break because we are loading and saving things differently as functionality is added. This was a HUGE drawback to both Marcel's old editor and Marauder's editor. We wasted thousands of man hours just waiting for them to implement a feature into the Editor that the Engine already supported. Not only that, but because the engine [i]expected[/i] the new feature, the level editor was essentially useless until the update was made.
Now Marcel's level editor will be simply filling up structures provided in our common AssetIO framework. Once he has done so, he can call Save(). When he wants to load them in, he calls Load(). Not only is he using the same Load and Save functions on the engine, but the actual structure is the exact same, so we are also representing things the same internally.
Have a look at editor_area.h/.cpp to see what I mean. It has both a Load() function that populates itself from a file and a Save() function that populates a file from itself. This means that all saving/loading functionaly has actually already been done for Marcel's editor. ;)
#include <fstream> #include "string.h" #include "editor_area.h" #include "entity.h" #ifdef ELYSIAN_ENGINE #include "level.h" #else #include "editor_level.h" #endif using namespace std; using namespace elysian; EditorArea::EditorArea(): level(NULL), width(0), height(0) {} EditorArea::EditorArea(char *const dir, const Level &level) { Entity(); Load(dir, level); } bool EditorArea::Load(char *const dir, const Level &lev) { char buffer[200]; bool success = true; level = &lev; strcpy(directory, dir); AssetDebug("EditorArea::Load(%s)n", dir); AssetDebug("tLoading Mapn"); strcpy(buffer, dir); strcat(buffer, "/map.txt"); if(!LoadMap(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tLoading ConcreteItemsn"); strcpy(buffer, dir); strcat(buffer, "/item.txt"); if(!LoadEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tLoading Warpsn"); strcpy(buffer, dir); strcat(buffer, "/warp.txt"); if(!LoadEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tLoading Friendliesn"); strcpy(buffer, dir); strcat(buffer, "/friendly.txt"); if(!LoadEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tLoading Enemiesn"); strcpy(buffer, dir); strcat(buffer, "/enemy.txt"); if(!LoadEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tLoading Additional Entitiesn"); strcpy(buffer, dir); strcat(buffer, "/entity.txt"); if(!LoadEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } return success; } bool EditorArea::Save(char *const dir) { char buffer[200]; bool success = true; strcpy(directory, dir); AssetDebug("EditorArea::Save(%s)n", dir); AssetDebug("tSaving Mapn"); strcpy(buffer, dir); strcat(buffer, "/map.txt"); if(!SaveMap(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tSaving ConcreteItemsn"); strcpy(buffer, dir); strcat(buffer, "/item.txt"); if(!SaveEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tSaving Warpsn"); strcpy(buffer, dir); strcat(buffer, "/warp.txt"); if(!SaveEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tSaving Friendliesn"); strcpy(buffer, dir); strcat(buffer, "/friendly.txt"); if(!SaveEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tSaving Enemiesn"); strcpy(buffer, dir); strcat(buffer, "/enemy.txt"); if(!SaveEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } AssetDebug("tSaving Additional Entitiesn"); strcpy(buffer, dir); strcat(buffer, "/entity.txt"); if(!SaveEntities(buffer)) { AssetDebug("ttFAILURE!n"); success = false; } return success; } bool EditorArea::LoadMap(char *const filename) { AssetDebug("EditorArea::LoadMap(%s)n", filename); int temp; ifstream file(filename); if(!file.is_open()) return false; memset(tileLayer1, 0, sizeof(tileLayer1)); memset(tileLayer2, 0, sizeof(tileLayer2)); memset(objectLayer1, 0, sizeof(objectLayer1)); memset(objectLayer2, 0, sizeof(objectLayer2)); memset(collisionLayer1, 0, sizeof(collisionLayer1)); memset(collisionLayer2, 0, sizeof(collisionLayer2)); file.getline(name, 49); AssetDebug("tName - %sn", name); file >> width; file >> height; AssetDebug("tSize - %dx%dn", width, height); for(unsigned h = 0; h < height; ++h) { for(unsigned int w = 0; w < width; ++w) { file >> temp; tileLayer1[h][w] = (unsigned char)temp; file >> temp; objectLayer1[h][w] = (unsigned char)temp; file >> temp; tileLayer2[h][w] = (unsigned char)temp; file >> temp; objectLayer2[h][w] = (unsigned char)temp; } } if(!level) { AssetDebug("tlevel == NULL - CANNOT GENERATE COLLISION LAYER!n"); return false; } //Generate collision layer for(unsigned int i = 0; i < height; ++i) { for(unsigned int j = 0; j < width; ++j) { //Entire 32x32 region is solid if tile is solid if(level->tileset[tileLayer1[i ][j]].solid) { collisionLayer1[i * 2][j*2] = true; collisionLayer1[i * 2][j*2+1] = true; collisionLayer1[i * 2+1][j*2] = true; collisionLayer1[i * 2+1][j*2+1] = true; } //Entire 32x32 region is solid if tile is solid if(level->tileset[tileLayer2[i ][j]].solid) { collisionLayer2[i * 2][j*2] = true; collisionLayer2[i * 2][j*2+1] = true; collisionLayer2[i * 2+1][j*2] = true; collisionLayer2[i * 2+1][j*2+1] = true; } //Make appropriate quadrants solid based on object solidity if(level->objectset[(int)objectLayer1[i ][j]].quadSolid[0]) collisionLayer1[i * 2][j*2] = true; if(level->objectset[(int)objectLayer1[i ][j]].quadSolid[1]) collisionLayer1[i * 2][j*2+1] = true; if(level->objectset[(int)objectLayer1[i ][j]].quadSolid[2]) collisionLayer1[i * 2+1][j*2] = true; if(level->objectset[(int)objectLayer1[i ][j]].quadSolid[3]) collisionLayer1[i * 2+1][j*2+1] = true; //Make appropriate quadrants solid based on object solidity if(level->objectset[(int)objectLayer2[i ][j]].quadSolid[0]) collisionLayer2[i * 2][j*2] = true; if(level->objectset[(int)objectLayer2[i ][j]].quadSolid[1]) collisionLayer2[i * 2][j*2+1] = true; if(level->objectset[(int)objectLayer2[i ][j]].quadSolid[2]) collisionLayer2[i * 2+1][j*2] = true; if(level->objectset[(int)objectLayer2[i ][j]].quadSolid[3]) collisionLayer2[i * 2+1][j*2+1] = true; } } file.close(); return true; } bool EditorArea::SaveMap(char *const filename) { ofstream file(filename); AssetDebug("EditorArea::SaveMap(%s)n", filename); if(!file.is_open()) { AssetDebug("tFAILURE! - Could not open file.n"); return false; } file << name; file << width; file << height; for(unsigned h = 0; h < height; ++h) { for(unsigned int w = 0; w < width; ++w) { file << (int)tileLayer1[h][w]; file << (int)objectLayer1[h][w]; file << (int)tileLayer2[h][w]; file << (int)objectLayer2[h][w]; } } return true; } bool EditorArea::LoadEntities(char *const filename) { ifstream file(filename); Entity *currentEntity; unsigned int amount; AssetDebug("EditorArea::LoadEntities(%s)n", filename); if(!file.is_open())return false; file >> amount; AssetDebug("t%d entitiesn", amount); for(unsigned int i = 0; i < amount; ++i) { AssetDebug("tLoading Entity #%dn", i); if(file.eof()) { AssetDebug("tFAILURE! - Unexpected end of filen"); return false; } currentEntity = new Entity(file); entity.push_back(currentEntity); } return true; } bool EditorArea::SaveEntities(char *const filename) { ofstream file(filename); AssetDebug("EditorArea::SaveEntities(%s)n", filename); if(!file.is_open()) { AssetDebug("tFAILURE! - Could not open file.n"); return false; } file << entity.size(); for(unsigned int i = 0; i < entity.size(); ++i) { AssetDebug("tWriting entity #%dn", i); entity[i ]->Save(file); } return true; }
Another thing that I would like to address here is debugging. The AssetIO framework has an empty function called AssetDebug() which is meant to be filled in by the programmer. In the engine, I have filled this function in with our standard debug output, which means that any debug information during Loading/Saving will also be written to our debug.txt by the engine.
The Loading/Saving functionality has also been [i]extensively[/i] debugged, so that it gives proper notification if any issue is encountered at any point in time. That means if Marcel's editor has trouble, he can pop a log for you to see what went wrong (your sheet is in the wrong spot? You named something wrong?) Also, if something goes wrong when the engine tries to load your assets, you will get the same error in the debug log.
[b]Entities and Components[/b]
An "entity" is basically an empty skeleton of an Elysian Shadows game object. As you can see, it inherits from "gyro::Rect." which contains a "Transform." Transforms simply have position, rotation, and size information. So every game object in ES begins as a simple rectangle.
Entities become more than just rectangles by having "components" attached to them. If we want this square to become an item on the ground, we must attach the "Item" component to the entity. If we want the entity to become an enemy, we attach the appropriate "Enemy" component. This functionality is provided in entity.h/.cpp
#ifndef ELYSIAN_ENTITY_H #define ELYSIAN_ENTITY_H #include <fstream> #include <vector> #include "gyro.h" #include "asset.h" #include "component.h" namespace elysian { class Sprite; class Collidable; class Entity: public gyro::Rect { private: std::vector<Component*> script; //initialize to sizeof Collidable *collidable; Sprite *sprite; public: Entity(); //maybe allow some sort of default Entity? Entity(std::ifstream &file); ~Entity(); bool Load(std::ifstream &file); bool Save(std::ofstream &file); void DBGDump(); //inherited from an abstract class "debugable"? bool AddComponent(Component::TYPE); bool AddComponent(char *const name); //lua bool RemoveComponent(Component::TYPE); //c++ bool RemoveComponent(char *name); //lua //Script *getScript(char*); //lua Collidable *getCollidable() const; Sprite *getSprite() const; }; } #endif
This is beautiful in more ways than I can articulate to you. Not only can we easily "snap" components onto entities in the editor, but we can also save every entity in a common manner through serialization and deserialization.
From above, you can see entity "Loading" or deserialization from a file. First we begin with loading common data to every entity (Transform information). Then we go on to construct the components comprising the entity, each of which "loads" or deserializes itself from the file.
At the moment, the only "component" that is fully implemented at both the engine and editor level is "Sprite." Attaching a "Sprite" component to our entity will transform our entity from a simple rectangle to a textured Sprite loaded from a spritesheet. I've thrown together a little example to fully demonstrate this deserialization process:
Notice the code is simply passed a file handle. From there the entity deserializes itself and its components from a text file and becomes a rotated, textured, scaled sprite (at frame 1) in the Elysian Shadows engine.
[b]ESGamma/Repositories[/b]
Elysian Shadows "Gamma" is completely up and running. It isn't much now, but this framework is the most powerful thing that I have developed yet and will enable us to be creating full levels in absolutely no time. Next I'm going to be rendering the map (it should load and save fine). From there, we are completely ready to begin compiling and utilizing this framework from within Marcel's editor in QT. All of the entity loading/saving should already be implemented, so we should be ready to start working on levels pretty damn soon.
To anybody interested: ESGamma is in the Engine repository. Here is the directory structure:
[b]editorAsset[/b] - Folder containing all asset source/header files that are joint between the engine and editor (so basically the editor includes this folder and a couple header files from libGyro).
[b]include[/b] - All header files for the ES engine
[b]lib[/b] - All library dependencies for the ES engine (OpenGL, lua, tolua++, SDL). Also has subfolders for each platform (DC, PSP, OSX, etc).
[b]libGyro[/b] - Source code to entire libGyro framework and every implementation (Windows, PSP, DC).
[b]source[/b] - Source (.cpp) files for entire ES engine
[b]vs2008[/b] - Visual Studio solution/project to build the engine. Should be completely ready to compile AS LONG AS YOU SET YOUR WORKING DIRECTORY TO THE LOCATION OF THE PROJECT REPOSITORY
[b]xcode[/b] - Folder for our XCode project. It's not here yet, but in the future the Mac/iPhone/iPad project files will go here
[b]Makefile.dc[/b] - Makefile for compiling ES Engine to Dreamcast executable
[b]Makefile.psp[/b] - Makefile for compiling ES engine to PSP executable (fucking windows isn't showing the extension)
[b]Makefile[/b] - TODO: Makefile for building ES engine for Linux
Edit: This is really random, but I was going to implement a kind of debug "save state" and "load state" functionality into the engine to test both saving and loading. Since the engine and editor are using the same code, I should be able to dump the entire current state of the level to a directory and be able to load it again identically.