/* LevelEditor.cpp Purpose: A re-usable level editor for the Bit Blot Game Engine (BBGE). This is not complete code, this level editor was created for the purpose of creating a prototype for Infinite Ammo's "Marian", a work in progress. Overview: The level editor is toggled using tab, and is accessed while testing the level. The level editor works with three main tools: 1) Entities, anything in the game that has behaviour patterns, or can be interacted with 2) Shapes, which are static collision data, used as invisible level boundries or for simplifiying collision data on more complex objects 3) GeomTiles, which are templates of static objects containing collision data and Quads (sprites) for graphics Shapes and Entities have structs called ShapeSaveData and EntitySaveData, which are used to store persistant information about these objects. The level editor is used in real time, and thus data must be stored about the level data for saving purposes. The Level editor is also capable of saving/loading these items to xml. Programmers: Level Editor: Ian Holowka Base engine and sections marked written by Alec Holowka */ #include "Game.h" #include "enums.h" #include "PuppetGame.h" #include "tinyxml.h" #include "RoundedRect.h" #include "DebugFont.h" #include "TTFFont.h" #include "Physics.h" // Quick and dirty container to hold level editor vars namespace LevelEditorStuff { b2PolygonDef editPolyDef; int vertexCount = 0; b2Body *lastBody=0; RoundedRect *rectShapeEditor = 0; TTFText *rectShapeEditorText = 0; RoundedRect *rectMainToolbar = 0; RoundedRect *rectEntityEditor = 0; TTFText *rectEntityEditorText = 0; TTFText *rectEntityEditorPos = 0; TTFText *rectEntityEditorRot = 0; TTFText *rectEntityEditorCol = 0; Quad *selector=0; ShapeSaveData *selectedShape = 0; Entity *selectedEntity = 0; typedef std::list EntitySaveDatas; EntitySaveDatas entitySaveDatas; typedef std::list ShapeSaveDatas; ShapeSaveDatas shapeSaveDatas; const std::string levelPath = "data/levels/"; } using namespace LevelEditorStuff; // Function written by Alec Holowka ShapeSaveData::ShapeSaveData() { vertexCount = 0; for (int i = 0; i < b2_maxPolygonVertices; i++) { vertices[i] = b2Vec2_zero; } shape = 0; } // Function written by Alec Holowka Vector ShapeSaveData::getAveragePosition() { Vector sum; if (vertexCount > 0) { for (int i = 0; i < vertexCount; i++) { sum += b2Vec2Vector(vertices[i]); } sum /= float(vertexCount); } return sum; } // Function written by Alec Holowka LevelEditor::LevelEditor() : RenderObject(), ActionMapper(), StateMachine() { editorActive = false; addAction(ACTION_ONE, MOUSE_BUTTON_LEFT); addAction(ACTION_TWO, MOUSE_BUTTON_RIGHT); addAction(ACTION_EDITOR_NEWPOLY, KEY_P); addAction(ACTION_EDITOR_TEST, KEY_U); addAction(ACTION_EDITOR_LOAD, KEY_F1); addAction(ACTION_EDITOR_SAVE, KEY_F2); //addAction(ACTION_TOGGLE_LEVELEDITOR, KEY_TAB); setState(STATE_EDITOR_IDLE, -1, true); currentLevel = "test"; } // Shows/Hides level editor when hitting tab void LevelEditor::toggle() { editorActive = !editorActive; setState(STATE_EDITOR_IDLE, -1, true); if (editorActive) { rectMainToolbar->show(); game->physicsDebugRender->alpha = 1; game->setCamFollow(0); game->setPause(true); if(selectedShape) rectShapeEditor->show(); else if(selectedEntity) rectEntityEditor->show(); } else { rectMainToolbar->hide(); if(!rectShapeEditor->alpha.isZero()) rectShapeEditor->hide(); if(!rectEntityEditor->alpha.isZero()) rectEntityEditor->hide(); //game->physicsDebugRender->alpha = 0; //physics->ClearContactPoints(); game->setCamFollow(game->entityControllers[0].getEntity()); game->setPause(false); } } // Init our GUI buttons and events(hardcoded atm), init vars, camera void LevelEditor::init() { clearCreatedEvents(); selector = new Quad; selector->alpha = 0; selector->alphaMod = 0.5; selector->scale.interpolateTo(Vector(10,10), 2, -1, 1, 1); game->addRenderObject(selector, LR_HUD); rectShapeEditor = new RoundedRect(); rectShapeEditor->followCamera = 1; rectShapeEditor->setWidthHeight(200,200,20); rectShapeEditor->position = Vector(100,100); rectShapeEditor->alpha = 0; rectShapeEditor->setCanMove(true); game->addRenderObject(rectShapeEditor, LR_HUD); rectShapeEditorText = new TTFText(&pgame->fontEditor); rectShapeEditorText->position = Vector(-90, -80); rectShapeEditor->addChild(rectShapeEditorText, PM_POINTER); rectEntityEditor = new RoundedRect(); rectEntityEditor->followCamera = 1; rectEntityEditor->setWidthHeight(200,200,20); rectEntityEditor->position = Vector(100,100); rectEntityEditor->alpha = 0; rectEntityEditor->setCanMove(true); game->addRenderObject(rectEntityEditor, LR_HUD); rectEntityEditorText = new TTFText(&pgame->fontEditor); rectEntityEditorText->position = Vector(-90, -80); rectEntityEditor->addChild(rectEntityEditorText, PM_POINTER); RoundButton *b = new RoundButton("Delete", &pgame->fontEditor); b->position = Vector(-90 + 45, 40); b->event.set(MakeFunctionEvent(LevelEditor, GUI_deleteSelectedEntity)); rectEntityEditor->addChild(b, PM_POINTER); b = new RoundButton("Position: ", &pgame->fontEditor); b->position = Vector(10, 10); b->event.set(MakeFunctionEvent(LevelEditor, GUI_deleteSelectedEntity)); rectEntityEditor->addChild(b, PM_POINTER); b = new RoundButton("Delete", &pgame->fontEditor); b->position = Vector(-90 + 45, 40); b->event.set(MakeFunctionEvent(LevelEditor, GUI_deleteSelectedShape)); rectShapeEditor->addChild(b, PM_POINTER); b = new RoundButton("Hide", &pgame->fontEditor); b->position = Vector(-90 + 135, 70); b->event.set(MakeFunctionEvent(LevelEditor, hideShapeMenu)); rectShapeEditor->addChild(b, PM_POINTER); rectMainToolbar = new RoundedRect(); rectMainToolbar->followCamera = 1; rectMainToolbar->setWidthHeight(150,400,20); rectMainToolbar->position = Vector(core->getVirtualWidth()-200,220); rectMainToolbar->alpha = 0; rectMainToolbar->setCanMove(true); game->addRenderObject(rectMainToolbar, LR_HUD); b = new RoundButton("Create Entity", &pgame->fontEditor); b->position = Vector(0, -120); b->setWidthHeight(120, 20, 10); b->event.set(MakeFunctionEvent(LevelEditor, GUI_createEntity)); rectMainToolbar->addChild(b, PM_POINTER); b = new RoundButton("Create GeomTile", &pgame->fontEditor); b->position = Vector(0, -90); b->setWidthHeight(120, 20, 10); b->event.set(MakeFunctionEvent(LevelEditor, GUI_createTileByName)); rectMainToolbar->addChild(b, PM_POINTER); //game->physicsDebugRender->alpha = 0; cursorLock = Vector(400,300); selectedEntity = 0; selectedShape = 0; cull = false; //followCamera = 1; } // Create an actual entity based on entity save data void LevelEditor::createEntity(EntitySaveData *saveData) { createEntity(saveData->type, saveData->position, saveData->dir, saveData->color); } // "Main" Entity Creation void LevelEditor::createEntity(EntityType type, Vector position, Direction dir, EntityColor color) { Entity *e = game->createEntity(type, position); // Delete handled by ClearEntitySaveData() EntitySaveData *sd = new EntitySaveData(type, position, DIR_DOWN, ECOLOR_NONE); entitySaveDatas.push_back(sd); e->setColor(color); e->direction = dir; e->saveData = sd; } // Create Entity from editor UI void LevelEditor::GUI_createEntity() { //Vector pos(core->screenCenter.x, core->screenCenter.y); std::string s = pgame->getUserInputString("Enter Entity Name:", ""); if (!s.empty()) { EntityType type = game->getEntityTypeFromString(s); if (type != ET_NONE) { createEntity(type, cursorLock, DIR_NONE, ECOLOR_NONE); } } } // Create Tile from editor UI void LevelEditor::GUI_createTileByName() { std::string s = pgame->getUserInputString("Enter the name of the tile to load:", ""); if (!s.empty()) { GeomTileTemplate *gt = game->getTileTemplateByName(s.c_str()); if(gt) { game->createGeomTile(gt, cursorLock, 0); } } } // Function written by Alec Holowka // Test for physics void LevelEditor::circleTest() { for (int i = 0; i < 100; i++) { b2BodyDef bd; bd.allowSleep = true; b2Random(-15.0f, 15.0f); b2Vec2 pos(core->screenCenter.x, core->screenCenter.y); int row = int(i/20); bd.position.Set(pos.x + (row-(i*20))*1, pos.y + row*1); bd.isBullet = true; b2Body *m_bomb = game->physics.getWorld()->CreateBody(&bd); //m_bomb->SetLinearVelocity(-5.0f * bd.position); b2CircleDef sd; sd.radius = 2.0f; sd.density = 20.0f; sd.restitution = 0.1f; m_bomb->CreateShape(&sd); m_bomb->SetMassFromShapes(); } } // Update function // Updates GUI text when items are selected void LevelEditor::onUpdate(float dt) { ActionMapper::onUpdate(dt); RenderObject::onUpdate(dt); if (editorActive) { if (selectedShape) { // update text std::ostringstream os; os << "Fric: " << selectedShape->shape->GetFriction() << "\n"; Vector avgp = selectedShape->getAveragePosition(); os << "AvgP:(" << avgp.x << ", " << avgp.y << ")\n"; rectShapeEditorText->setText(os.str()); } if (selectedEntity && selectedEntity->saveData) { // update text std::ostringstream os; Vector pos = selectedEntity->saveData->position; os << "Entity Type: " << entityTypeStrings[selectedEntity->getEntityType()] << "\n"; //os << "Position: (" << pos.x << ", " << pos.y << ")\n"; //os << "Direction: " << selectedEntity->saveData->dir << "\n"; //os << "Color: " << selectedEntity->saveData->color<< "\n"; rectEntityEditorText->setText(os.str()); } } } // Removes a shape from the game (and its corresponding ShapeSaveData) void LevelEditor::deleteShape(ShapeSaveData *saveData) { if (saveData->shape) { b2Body *b = selectedShape->shape->GetBody(); game->physics.getWorld()->DestroyBody(b); } shapeSaveDatas.remove(saveData); delete saveData; } // Deletes the shape (clearing up the GUI windows, then calling the actual delete function) void LevelEditor::GUI_deleteSelectedShape() { if (selectedShape && selectedShape->shape) { deleteShape(selectedShape); selectedShape = 0; selector->alpha = 0; rectShapeEditor->hide(); } } // Removes the physical entity and its corresponding savedata. void LevelEditor::deleteEntity(Entity *entity) { // remove from save data entitySaveDatas.remove(entity->saveData); delete entity->saveData; // remove physical entity //core->enqueueRenderObjectDeletion(entity); //entity->safeKill(); game->removeEntity(entity); } // Deletes the entity (clearing up the GUI windows, then calling the actual delete function) void LevelEditor::GUI_deleteSelectedEntity() { if (selectedEntity && selectedEntity->saveData) { deleteEntity(selectedEntity); selectedEntity = 0; selector->alpha = 0; rectEntityEditor->hide(); } } void LevelEditor::hideShapeMenu() { rectShapeEditor->hide(); } // Manages the input, through the ActionMapper base class void LevelEditor::action(int actionID, int state) { ActionMapper::action(actionID, state); // Don't process input if the editor is somehow inactive, or if we have a text entry box open if (editorActive && !core->isNested()) { if (actionID == ACTION_EDITOR_TEST && state) { game->createGeomTile(game->getGeomTileTemplate(0), cursorLock, 0); } if (actionID == ACTION_TOGGLE_LEVELEDITOR && state) { if (game->physicsDebugRender->alpha.x > 0) { game->physicsDebugRender->alpha.x = 0; } else { game->physicsDebugRender->alpha.x = 1; } } if (actionID == ACTION_EDITOR_SAVE && state) { save(currentLevel); } if (actionID == ACTION_EDITOR_LOAD && state) { game->transitionToLevel(currentLevel); } if (actionID == ACTION_TWO && state && core->getCtrlState()) { /* if (!shapeSaveDatas.empty()) { ShapeSaveData *s = &shapeSaveDatas[shapeSaveDatas.size()-1]; if (s->shape) { b2Body *b = s->shape->GetBody(); game->physics.getWorld()->DestroyBody(b); shapeSaveDatas.resize(shapeSaveDatas.size()-1); } } */ } // Here we manage the actual editor states, which are used in conjunction with the // actions to decide what gets done switch(this->getState()) { // Creating shapes case STATE_EDITOR_POLYEDIT: { // Finish creating a shape (By hitting P again) if (actionID == ACTION_EDITOR_NEWPOLY && state) { if (vertexCount > 2) { if (lastBody) { game->physics.getWorld()->DestroyBody(lastBody); lastBody = 0; } editPolyDef.vertexCount = vertexCount; ShapeSaveData *p = new ShapeSaveData; p->vertexCount = vertexCount; for (int i = 0; i < vertexCount; i++) { p->vertices[i] = editPolyDef.vertices[i]; } b2BodyDef body; b2Body *b = game->physics.getWorld()->CreateBody(&body); p->shape = b->CreateShape(&editPolyDef); // Assuming we're always creating ground //p->shape->SetUserData((void*)physics->getGround()); b->SetUserData((void*)physics->getGround()); shapeSaveDatas.push_back(p); } setState(STATE_EDITOR_IDLE, -1, true); } // LMB - Create new vertex else if (actionID == ACTION_ONE && state) { if (vertexCount < b2_maxPolygonVertices) { Vector v = core->getGameCursorPosition(); editPolyDef.vertices[vertexCount].Set(v.x, v.y); vertexCount++; // Delete and re-create the body (physics) if (vertexCount > 2) { if (lastBody) { game->physics.getWorld()->DestroyBody(lastBody); lastBody = 0; } b2BodyDef body; lastBody = game->physics.getWorld()->CreateBody(&body); editPolyDef.vertexCount = vertexCount; lastBody->CreateShape(&editPolyDef); } } } } break; case STATE_EDITOR_IDLE: { // Shift-LMB - Select closest entity or state if (actionID == ACTION_ONE && state && core->getShiftState()) { if(!selectEntityAt(core->getGameCursorPosition())) selectShapeAt(core->getGameCursorPosition()); } // Ctrl-LMB - Set cursor (for entity creation), locks to grid else if (actionID == ACTION_ONE && state && core->getCtrlState()) { //cursorLock = core->mouse.position; cursorLock = core->getGameCursorPosition()/getGridSize(); cursorLock.x = (int)cursorLock.x; cursorLock.y = (int)cursorLock.y; cursorLock *= getGridSize(); cursorLock += Vector(getGridSize()*0.5, getGridSize()*0.5); } // Alt-LMB - Set cursor (for entity creation), doesn't lock to grid else if (actionID == ACTION_ONE && state && core->getAltState()) { cursorLock = core->getGameCursorPosition(); } // P - Enter state to begin new shape creation else if (actionID == ACTION_EDITOR_NEWPOLY && state) { b2PolygonDef newPolyDef; editPolyDef = newPolyDef; vertexCount = 0; setState(STATE_EDITOR_POLYEDIT, -1, true); } } break; } } } // Saves the level data to an xml file, using the TinyXML library. void LevelEditor::save(const std::string &name) { std::string f = levelPath + name + ".xml"; TiXmlDocument doc; // Write Shape data TiXmlElement xmlShapeData("ShapeData"); { std::ostringstream os; for (ShapeSaveDatas::iterator i = shapeSaveDatas.begin(); i != shapeSaveDatas.end(); i++) { os << (*i)->vertexCount << " "; for (int j = 0; j < (*i)->vertexCount; j++) { os << (*i)->vertices[j].x << " " << (*i)->vertices[j].y << " "; } } xmlShapeData.SetAttribute("a", os.str()); } doc.InsertEndChild(xmlShapeData); // Write Entity data TiXmlElement xmlEntitiesData("Entities"); { EntitySaveData *e = 0; for(EntitySaveDatas::iterator i= entitySaveDatas.begin(); i != entitySaveDatas.end(); i++) { e = (*i); std::ostringstream os; TiXmlElement xmlEntityData("Entity"); { xmlEntityData.SetAttribute("type", e->type); xmlEntityData.SetAttribute("x", e->position.x); xmlEntityData.SetAttribute("y", e->position.y); xmlEntityData.SetAttribute("dir", e->dir); xmlEntityData.SetAttribute("color", e->color); } xmlEntitiesData.InsertEndChild(xmlEntityData); } } doc.InsertEndChild(xmlEntitiesData); doc.SaveFile(f); } void LevelEditor::clearShapeSaveDatas() { for (ShapeSaveDatas::iterator i = shapeSaveDatas.begin(); i != shapeSaveDatas.end(); i++) { if (*i) delete *i; } shapeSaveDatas.clear(); } void LevelEditor::clearEntitySaveDatas() { for (EntitySaveDatas::iterator i = entitySaveDatas.begin(); i != entitySaveDatas.end(); i++) { if (*i) delete *i; } entitySaveDatas.clear(); } // Load shapes and entities (level) from xml void LevelEditor::load(const std::string &name) { std::string f = levelPath + name + ".xml"; TiXmlDocument doc; doc.LoadFile(f); game->currentEntityID = 0; // Clear out existing stuff clearShapeSaveDatas(); clearEntitySaveDatas(); // Parse and collect shapeSaveData TiXmlElement *xmlShapeData = doc.FirstChildElement("ShapeData"); if (xmlShapeData) { if (xmlShapeData->Attribute("a")) { std::istringstream is(xmlShapeData->Attribute("a")); ShapeSaveData *newShape = new ShapeSaveData; while (is >> newShape->vertexCount) { for (int j = 0; j < newShape->vertexCount; j++) { is >> newShape->vertices[j].x >> newShape->vertices[j].y; } shapeSaveDatas.push_back(newShape); newShape = new ShapeSaveData; } delete newShape; } } // Create shapes from save data for (ShapeSaveDatas::iterator i = shapeSaveDatas.begin(); i != shapeSaveDatas.end(); i++) { ShapeSaveData *data = *i; b2PolygonDef polyDef; polyDef.vertexCount = data->vertexCount; for (int i = 0; i < polyDef.vertexCount; i++) { polyDef.vertices[i] = data->vertices[i]; } b2BodyDef body; b2Body *b = game->physics.getWorld()->CreateBody(&body); data->shape = b->CreateShape(&polyDef); // Load in as ground //data->shape->SetUserData((void*)physics->getGround()); b->SetUserData((void*)physics->getGround()); } // Load in entitySaveData from xml TiXmlElement *xmlEntitiesData = doc.FirstChildElement("Entities"); if (xmlEntitiesData) { TiXmlElement *current = xmlEntitiesData->FirstChildElement("Entity"); while(current) { EntitySaveData *newEntity; // Once we have a type, we can pretty much create the entity with defaults.. if (current->Attribute("type") && current->Attribute("x") && current->Attribute("y") && current->Attribute("dir") && current->Attribute("color")) { // TODO: Type checking here? (Assure valid type) newEntity = new EntitySaveData(); newEntity->type = (EntityType) atoi(current->Attribute("type")); newEntity->position = Vector(atoi(current->Attribute("x")), atoi(current->Attribute("y"))); newEntity->dir = (Direction) atoi(current->Attribute("dir")); newEntity->color = (EntityColor) atoi(current->Attribute("color")); createEntity(newEntity); } current = current->NextSiblingElement("Entity"); } } } // Function written by Alec Holowka and Ian Holowka // Select nearest shape to the given point void LevelEditor::selectShapeAt(const Vector &vec) { ShapeSaveData *nearest = 0; int smallest = -1; for (ShapeSaveDatas::iterator i = shapeSaveDatas.begin(); i != shapeSaveDatas.end(); i++) { ShapeSaveData *data = *i; if (data->shape && selectedShape != data) { Vector v = data->getAveragePosition(); int dist = (v-vec).getSquaredLength2D(); if (dist < sqr(32) && (smallest == -1 || dist < smallest)) { smallest = dist; nearest = data; } } } // Do selection (should probably be in its own function eventually) if (nearest) { selector->position = nearest->getAveragePosition(); selector->alpha = 1; selectedShape = nearest; selectedEntity = 0; if(rectShapeEditor->alpha.isZero()) rectShapeEditor->show(); if(!rectEntityEditor->alpha.isZero()) rectEntityEditor->hide(); } } // Select nearest entity to given point, using engine function getNearestEntity // Right now, hardcoded max dist of 20 Entity *LevelEditor::selectEntityAt(const Vector &vec) { Entity *e = game->getNearestEntity(vec, 20); if(e) { // If saveData exists if(e->saveData) { selectedEntity = e; selectedShape = 0; selector->position = e->getPosition(); selector->alpha = 1; if(rectEntityEditor->alpha.isZero()) rectEntityEditor->show(); if(!rectShapeEditor->alpha.isZero()) rectShapeEditor->hide(); } else e = 0; } return e; } void LevelEditor::render() { RenderObject::render(); } float LevelEditor::getGridSize() { const int grid = 10; //return float(grid) * core->invGlobalScale; return float(grid); } // Function written by Alec Holowka void LevelEditor::onRender() { const float grid = getGridSize(); if (editorActive) { glLineWidth(1); glColor4f(1,0.9,0.5,0.5); glBegin(GL_LINES); glVertex2f(core->cameraPos.x, cursorLock.y); glVertex2f(core->cameraPos.x+800*core->invGlobalScale, cursorLock.y); glVertex2f(cursorLock.x, core->cameraPos.y); glVertex2f(cursorLock.x, core->cameraPos.y+600*core->invGlobalScale); glEnd(); glPushMatrix(); glLoadIdentity(); glLineWidth(1); glColor4f(1,1,1,0.05); glBegin(GL_LINES); float incr = grid*core->globalScale.x; //float bit = grid*core->invGlobalScale; int cx = core->cameraPos.x/incr; float dx = core->cameraPos.x - (cx*incr); if (incr < 2) incr = 2; for (float x = (incr/2) - dx; x <= 800*core->globalScale.x; x+=incr) { glVertex2f(x, 0); glVertex2f(x, 600*core->globalScale.x); } for (float y = incr/2; y <= 600*core->globalScale.x; y+=incr) { glVertex2f(0, y); glVertex2f(800*core->globalScale.x, y); } glEnd(); glPopMatrix(); glLineWidth(1); } } /* */