#include "olcUTIL_Geometry2D.h" #include "TSXParser.h" #include "olcPGEX_TransformedView.h" #include "olcUTIL_Camera2D.h" #include "olcPGEX_QuickGUI.h" #include #include using namespace olc; using namespace olc::utils; using namespace QuickGUI; const std::string TILESET_DIR="./Tiles/"; class TiledCollisionEditor : public olc::PixelGameEngine { Tileset currentTileset; Renderable mapImage; std::string activeTileset; Quadrilateral*editingQuad=nullptr; Quadrilateral originalQuad; std::string selectedObj=""; int editingPoint=4; //0-3 for the index we are editing within editingQuad. bool dragging=false; bool dragTranslate=false; vf2d upperLeftDragOffset{}; Quadrilateral*highlightedQuad=nullptr; std::string nameEditObj=""; size_t lastSelectedItem=0; bool dragNewObj=false; vi2d upperLeftObjTile{}; vi2d lowerRightObjTile{}; bool selectingFile=false; Renderable circle; Renderable createNewButtonImg; Renderable undoButtonImg; Renderable redoButtonImg; Renderable editButtonImg; bool loadedFirstFile=false; TransformedView view; std::vectortilesetList; std::unordered_mappreviousObjState; std::deque>redoList; std::deque>undoList; public: TiledCollisionEditor() { sAppName = "TiledCollisionEditor"; } Manager gui; Manager selectionGui; ImageCheckBox*createNewButton=nullptr; ImageCheckBox*editButton=nullptr; ImageButton*undoButton=nullptr; ImageButton*redoButton=nullptr; TextBox*nameBox=nullptr; Button*openButton=nullptr; ListBox*tilesetsList=nullptr; TSXParser parsedMap{""}; Button*closeButton=nullptr; public: bool OnUserCreate() override { SetFontSprite("font3.png"); circle.Create(5,5); SetDrawTarget(circle.Sprite()); Clear(BLANK); FillCircle({2,2},2); SetDrawTarget(nullptr); circle.Decal()->Update(); createNewButtonImg.Load("newCollisionButton.png"); editButtonImg.Load("EditButton.png"); undoButtonImg.Load("undoButton.png"); redoButtonImg.Load("redoButton.png"); createNewButton=new ImageCheckBox{gui,createNewButtonImg,false,vf2d{4.f,ScreenHeight()-36.f},{32.f,32.f},{4,1},{32,32}}; createNewButton->hotkey=Q; editButton=new ImageCheckBox{gui,editButtonImg,true,vf2d{40.f,ScreenHeight()-36.f},{32.f,32.f},{4,4},{32,32}}; editButton->hotkey=E; undoButton=new ImageButton{gui,undoButtonImg,vf2d{ScreenWidth()-72.f,ScreenHeight()-36.f},{32.f,32.f},{4,4},{32,32}}; redoButton=new ImageButton{gui,redoButtonImg,vf2d{ScreenWidth()-36.f,ScreenHeight()-36.f},{32.f,32.f},{4,4},{32,32}}; nameBox=new TextBox{gui,"",vf2d{76.f,ScreenHeight()-36.f+6.f},{128,20.f},{1,1}}; nameBox->bHasBackground=true; openButton=new Button{gui,"Open",{ScreenWidth()-32.f,0.f},{32,12.f},{0.5f,0.5f}}; closeButton=new Button{selectionGui,"Close",{ScreenWidth()-32.f,0.f},{32,12.f},{0.5f,0.5f}}; std::filesystem::path dir{TILESET_DIR}; for(auto const&dir:std::filesystem::directory_iterator(dir)){ if(dir.path().string().ends_with(".tsx")){ tilesetList.push_back(dir.path().string()); } } tilesetsList=new ListBox{selectionGui,tilesetList,{ScreenWidth()/2-240.f,ScreenHeight()/2-60.f},{480.f,120.f},16.f}; previousObjState=currentTileset.objects; return true; } void SaveFile(bool ignoreUndoEntry=false){ if(!ignoreUndoEntry){ undoList.push_back(previousObjState); previousObjState=currentTileset.objects; redoList.clear(); if(undoList.size()>20)undoList.pop_front(); } std::stringstream file; if(file.good()){ const std::vectororiginalData=parsedMap.originalData; const std::vectornonObjects=parsedMap.nonObjects; auto xmlTag=std::find_if(originalData.begin(),originalData.end(),[](const XMLTag tag){return tag.tag=="?xml";}); if(xmlTag!=originalData.end())file<<""<", activeSet.name,activeSet.tilewidth,activeSet.tileheight,activeSet.tilecount,activeSet.columns)<")<", activeSet.filename,activeSet.imagewidth,activeSet.imageheight)<>tiles; for(auto&[name,obj]:activeSet.objects){ const std::vector>objTiles{obj.OutputTag(activeSet)}; tiles.insert(tiles.end(),objTiles.begin(),objTiles.end()); } for(const NonObject&obj:nonObjects){ tiles.push_back(obj.OutputTag()); } std::sort(tiles.begin(),tiles.end(),[](const std::pairtileData1,const std::pairtileData2){ return tileData1.first"<0){ if(GetKey(DEL).bReleased){ currentTileset.objects.erase(selectedObj); selectedObj=""; ResetState(); SaveFile(); return; } const TilesetObject&obj=tileset.objects.at(selectedObj); const bool EditingQuad=(editingPoint<4||dragging)&&editingQuad!=nullptr; auto GetSnapPoint=[&](){ vf2d worldCoords=view.ScreenToWorld(GetMousePos()); if(GetKey(CTRL).bHeld){ return vf2d{round(worldCoords.x),round(worldCoords.y)}; } return vf2d{round(worldCoords.x/(tileset.tilewidth/4))*tileset.tilewidth/4,round(worldCoords.y/(tileset.tileheight/4))*tileset.tileheight/4}; }; if(EditingQuad&&!dragging){ vf2d newEditPoint=GetSnapPoint(); newEditPoint.x=std::clamp(newEditPoint.x,float(obj.bounds.left().start.x),float(obj.bounds.right().start.x)); newEditPoint.y=std::clamp(newEditPoint.y,float(obj.bounds.top().start.y),float(obj.bounds.bottom().start.y)); (*editingQuad)[editingPoint]=newEditPoint; }else if(EditingQuad&&dragging){ vf2d cursorPos=GetSnapPoint(); vf2d initialPoint=(*editingQuad)[0]; (*editingQuad)[1]=vf2d{cursorPos.x,initialPoint.y}; (*editingQuad)[2]=GetSnapPoint(); (*editingQuad)[3]=vf2d{initialPoint.x,cursorPos.y}; } if(dragTranslate){ vf2d translateAmt=GetSnapPoint()-originalQuad[0]-upperLeftDragOffset; for(size_t pointInd=0;vf2d&point:*editingQuad){ (*editingQuad)[pointInd]=originalQuad[pointInd]+translateAmt; pointInd++; } } if(editButton->bChecked){ if(GetMouse(Mouse::LEFT).bPressed){ if(highlightedQuad!=nullptr){ #pragma region Select a point on a collision quad. for(size_t pointInd=0;const vf2d&point:*highlightedQuad){ if(geom2d::line(point,view.ScreenToWorld(GetMousePos())).length()<4/view.GetWorldScale().x){ editingPoint=pointInd; editingQuad=highlightedQuad; originalQuad=*highlightedQuad; goto exitCollisionCheck; } pointInd++; } #pragma endregion dragTranslate=true; editingQuad=highlightedQuad; upperLeftDragOffset=GetSnapPoint()-(*highlightedQuad)[0]; originalQuad=*editingQuad; goto exitCollisionCheck; } exitCollisionCheck: if(EditingQuad&&!dragging){ vf2d newEditPoint=GetSnapPoint(); newEditPoint.x=std::clamp(newEditPoint.x,float(obj.bounds.left().start.x),float(obj.bounds.right().start.x)); newEditPoint.y=std::clamp(newEditPoint.y,float(obj.bounds.top().start.y),float(obj.bounds.bottom().start.y)); (*editingQuad)[editingPoint]=newEditPoint; editingPoint++; } } if(GetMouse(Mouse::RIGHT).bPressed||GetKey(ESCAPE).bReleased){ if(EditingQuad||dragTranslate){ ResetState(); } } }else{ if(GetMouse(Mouse::LEFT).bPressed){ Quadrilateral newQuad{GetSnapPoint()}; currentTileset.objects[selectedObj].collisionTiles.push_back(newQuad); dragging=true; editingQuad=const_cast(&obj.collisionTiles.back()); originalQuad=*editingQuad; } } if(GetMouse(Mouse::RIGHT).bPressed&&!EditingQuad&&!dragTranslate){ if(highlightedQuad!=nullptr){ std::erase_if(currentTileset.objects[selectedObj].collisionTiles,[&](Quadrilateral&q){return &q==highlightedQuad;}); ResetState(); SaveFile(); } } if(GetMouse(Mouse::LEFT).bReleased){ if(EditingQuad&&dragging){ vf2d cursorPos=GetSnapPoint(); vf2d initialPoint=(*editingQuad)[0]; (*editingQuad)[1]=vf2d{cursorPos.x,initialPoint.y}; (*editingQuad)[2]=GetSnapPoint(); (*editingQuad)[3]=vf2d{initialPoint.x,cursorPos.y}; ResetState(); SaveFile(); }else if(EditingQuad&&!dragging){ vf2d newEditPoint=GetSnapPoint(); newEditPoint.x=std::clamp(newEditPoint.x,float(obj.bounds.left().start.x),float(obj.bounds.right().start.x)); newEditPoint.y=std::clamp(newEditPoint.y,float(obj.bounds.top().start.y),float(obj.bounds.bottom().start.y)); (*editingQuad)[editingPoint]=newEditPoint; editingPoint=4; dragging=false; editingQuad=nullptr; dragTranslate=false; highlightedQuad=nullptr; SaveFile(); }else if(dragTranslate){ SaveFile(); } dragTranslate=false; editingQuad=nullptr; } } } void NewObjectUpdate(){ const Tileset&tileset=currentTileset; if(GetMouse(Mouse::LEFT).bReleased){ dragNewObj=false; vf2d worldCoords=view.ScreenToWorld(GetMousePos()); vi2d newUpperLeftTile=upperLeftObjTile; vi2d newLowerRightTile=lowerRightObjTile; if(worldCoords.xnewObjRect{newUpperLeftTile,newLowerRightTile-newUpperLeftTile}; const Tileset&tileset=currentTileset; //Check for intersection with other objects, if found then we deny creating this object this way. bool intersectionFound=false; for(auto&[name,obj]:tileset.objects){ geom2d::rectoffsetBounds{obj.bounds.pos+vf2d{0.5f,0.5f},obj.bounds.size-vf2d{1.f,1.f}}; if(geom2d::overlaps(offsetBounds,newObjRect)){ intersectionFound=true; break; } } if(!intersectionFound){ std::string objName=std::format("Object{}",currentTileset.objects.size()); TilesetObject&newObj=currentTileset.objects[objName]; newObj.name=objName; for(int y=0;ym_bTextEdit){ if(selectedObj.length()>0&&GetKey(R).bReleased){ TextEntryEnable(true, nameBox->sText); nameBox->m_bTextEdit=true; nameEditObj=currentTileset.objects[selectedObj].name; } if(GetKey(W).bHeld)view.MoveWorldOffset(vf2d{0.f,-CAMERA_MOVESPD}*GetElapsedTime()/view.GetWorldScale()); if(GetKey(S).bHeld)view.MoveWorldOffset(vf2d{0.f,CAMERA_MOVESPD}*GetElapsedTime()/view.GetWorldScale()); if(GetKey(A).bHeld)view.MoveWorldOffset(vf2d{-CAMERA_MOVESPD,0.f}*GetElapsedTime()/view.GetWorldScale()); if(GetKey(D).bHeld)view.MoveWorldOffset(vf2d{CAMERA_MOVESPD,0.f}*GetElapsedTime()/view.GetWorldScale()); if(((undoButton->bPressed||GetKey(CTRL).bHeld&&GetKey(Z).bReleased))&& undoList.size()>0){ redoList.push_back(currentTileset.objects); currentTileset.objects.clear(); currentTileset.objects=undoList.back(); undoList.pop_back(); previousObjState=currentTileset.objects; std::cout<<"Undo List Size:"<bPressed||GetKey(CTRL).bHeld&&GetKey(Y).bReleased||GetKey(CTRL).bHeld&&GetKey(SHIFT).bHeld&&GetKey(Z).bReleased))&& redoList.size()>0){ undoList.push_back(currentTileset.objects); currentTileset.objects.clear(); currentTileset.objects=redoList.back(); previousObjState=currentTileset.objects; redoList.pop_back(); std::cout<<"Redo List Size:"<bReleased){ selectingFile=true; openButton->Reset(); gui.Update(this); return; } } const bool EditingQuad=(editingPoint<4||dragging)&&editingQuad!=nullptr; if(editingQuad==nullptr&&!EditingQuad){ selectedObj=""; for(auto&[objName,obj]:tileset.objects){ if(geom2d::contains(obj.bounds,view.ScreenToWorld(GetMousePos()))){ selectedObj=objName; nameBox->sText=obj.name; break; } } } if(createNewButton->bPressed)editButton->bChecked=false; if(editButton->bPressed)createNewButton->bChecked=false; undoButton->Enable(undoList.size()>0); redoButton->Enable(redoList.size()>0); gui.Update(this); if((GetMouseY()204)&& (GetMouseX()ScreenHeight()+12)&& !nameBox->m_bTextEdit&& tileset.columns>0&& view.ScreenToWorld(GetMousePos()).x=0&&view.ScreenToWorld(GetMousePos()).y>=0){ if(selectedObj.length()==0){ if(GetMouse(Mouse::LEFT).bPressed){ vf2d worldCoords=view.ScreenToWorld(GetMousePos()); upperLeftObjTile=vi2d{int(floor(worldCoords.x/tileset.tilewidth)*tileset.tilewidth),int(floor(worldCoords.y/tileset.tileheight)*tileset.tileheight)}; dragNewObj=true; } } if(!dragNewObj){ Update(); }else{ NewObjectUpdate(); } } } void SelectingFileUpdate(){ selectionGui.Update(this); if(tilesetList.size()>0&&!loadedFirstFile){ loadedFirstFile=true; tilesetsList->nSelectedItem=0; goto loadFile; } if(tilesetsList->bSelectionChanged){ loadFile: const std::string tilesetFilename{tilesetList[tilesetsList->nSelectedItem]}; parsedMap={tilesetFilename}; mapImage.Load(TILESET_DIR+parsedMap.GetData().filename); currentTileset=parsedMap.GetData(); activeTileset=tilesetFilename; undoList.clear(); redoList.clear(); previousObjState=currentTileset.objects; ResetState(); } if(GetKey(ESCAPE).bReleased){ selectingFile=false; openButton->Reset(); gui.Update(this); } if(closeButton->bReleased){ closeButton->Reset(); selectingFile=false; openButton->Reset(); gui.Update(this); } GradientFillRectDecal({0.f,0.f},GetScreenSize()/2.f,BLACK,BLACK,{0,0,0,0},BLACK); GradientFillRectDecal({0.f,ScreenHeight()/2.f},GetScreenSize()/2.f,BLACK,BLACK,BLACK,{0,0,0,0}); GradientFillRectDecal(GetScreenSize()/2.f,GetScreenSize()/2.f,{0,0,0,0},BLACK,BLACK,BLACK); GradientFillRectDecal({ScreenWidth()/2.f,0.f},GetScreenSize()/2.f,BLACK,{0,0,0,0},BLACK,BLACK); selectionGui.DrawDecal(this); } void RenderTileset(){ const Tileset&tileset=currentTileset; if(mapImage.Decal()!=nullptr){ view.DrawDecal({0,0},mapImage.Decal()); } if(tileset.columns>0){ if(selectedObj.length()>0){ for(int y=0;y0&&!dragNewObj){ const TilesetObject&obj=tileset.objects.at(selectedObj); if(highlightedQuad!=nullptr){ std::array,2>collisionTris{ geom2d::triangle{(*highlightedQuad)[0],(*highlightedQuad)[1],(*highlightedQuad)[2]}, geom2d::triangle{(*highlightedQuad)[0],(*highlightedQuad)[2],(*highlightedQuad)[3]}, }; for(geom2d::triangle&tri:collisionTris){ if(geom2d::overlaps(tri,geom2d::circle(view.ScreenToWorld(GetMousePos()),3.f))){ goto renderQuads; } } highlightedQuad=nullptr; }else{ for(const Quadrilateral&quad:obj.collisionTiles){ std::array,2>collisionTris{ geom2d::triangle{quad[0],quad[1],quad[2]}, geom2d::triangle{quad[0],quad[2],quad[3]}, }; for(geom2d::triangle&tri:collisionTris){ if(geom2d::overlaps(tri,geom2d::circle(view.ScreenToWorld(GetMousePos()),3.f))){ highlightedQuad=const_cast(&quad); goto renderQuads; } } } } renderQuads: for(const Quadrilateral&quad:obj.collisionTiles){ std::vectorpoints; std::vectoruvs; std::vectorcols; points.assign(quad.begin(),quad.end()); uvs.assign(4,{0.f,0.f}); cols.assign(4,(!GetMouse(Mouse::LEFT).bHeld&&highlightedQuad==&quad)?Pixel{255,20,20,150}:Pixel{255,40,40,128}); view.DrawPolygonDecal(nullptr,points,uvs,cols); for(bool highlighted=false;const vf2d&point:quad){ if(highlightedQuad==&quad){ if(geom2d::line(point,view.ScreenToWorld(GetMousePos())).length()<4/view.GetWorldScale().x&&!highlighted){ view.DrawRotatedDecal(point,circle.Decal(),0.f,circle.Sprite()->Size()/2,vf2d{1.f,1.f}/view.GetWorldScale(),YELLOW); highlighted=true; }else{ view.DrawRotatedDecal(point,circle.Decal(),0.f,circle.Sprite()->Size()/2,vf2d{1.f,1.f}/view.GetWorldScale(),RED); } }else{ view.DrawRotatedDecal(point,circle.Decal(),0.f,circle.Sprite()->Size()/2,vf2d{0.25f,0.25f}/view.GetWorldScale(),DARK_GREY); } } } }else if(dragNewObj){ vf2d worldCoords=view.ScreenToWorld(GetMousePos()); lowerRightObjTile=vi2d{int(ceil(worldCoords.x/tileset.tilewidth)*tileset.tilewidth),int(ceil(worldCoords.y/tileset.tileheight)*tileset.tileheight)}; vi2d newUpperLeftTile=upperLeftObjTile; vi2d newLowerRightTile=lowerRightObjTile; if(worldCoords.xnewObjRect{newUpperLeftTile,newLowerRightTile-newUpperLeftTile}; for(auto&[name,obj]:tileset.objects){ geom2d::rectoffsetBounds{obj.bounds.pos+vf2d{0.5f,0.5f},obj.bounds.size-vf2d{1.f,1.f}}; if(geom2d::overlaps(offsetBounds,newObjRect)){ overlayCol={255,0,0,160}; break; } } view.FillRectDecal(newObjRect.pos,newObjRect.size,overlayCol); } for(auto&[objName,obj]:tileset.objects){ view.DrawLineDecal(obj.bounds.pos,obj.bounds.pos+vf2d{0.f,float(obj.bounds.size.y)},YELLOW); view.DrawLineDecal(obj.bounds.pos,obj.bounds.pos+vf2d{float(obj.bounds.size.x),0.f},YELLOW); view.DrawLineDecal(obj.bounds.pos+obj.bounds.size,obj.bounds.pos+obj.bounds.size+vf2d{0.f,-float(obj.bounds.size.y)},YELLOW); view.DrawLineDecal(obj.bounds.pos+obj.bounds.size,obj.bounds.pos+obj.bounds.size+vf2d{-float(obj.bounds.size.x),0.f},YELLOW); vi2d nameTextSize=GetTextSizeProp(obj.name)*0.25f; if(!geom2d::overlaps(geom2d::rect{obj.bounds.pos,nameTextSize+vf2d{2,2}},view.ScreenToWorld(GetMousePos()))){ view.GradientFillRectDecal(obj.bounds.pos,nameTextSize+vf2d{2,2},RED,{255,0,0,64},{255,0,0,64},RED); view.DrawStringPropDecal(obj.bounds.pos+vf2d{1.25f,1.25f},obj.name,BLACK,vf2d{0.25f,0.25f}); view.DrawStringPropDecal(obj.bounds.pos+vf2d{1,1},obj.name,WHITE,vf2d{0.25f,0.25f}); } } gui.DrawDecal(this); if(!nameBox->m_bTextEdit){ createNewButton->Enable(true); editButton->Enable(true); FillRectDecal(nameBox->vPos,nameBox->vSize,{0,0,0,150}); if(selectedObj.length()>0){ DrawStringPropDecal(nameBox->vPos+vf2d{2.f,0.f},"R to Edit",WHITE,{0.6f,0.6f}); } }else{ createNewButton->Enable(false); editButton->Enable(false); } DrawStringDecal(createNewButton->vPos+vf2d{3,0},"Q"); DrawStringDecal(editButton->vPos+vf2d{3,0},"E"); } bool OnUserUpdate(float fElapsedTime) override { Clear(VERY_DARK_BLUE); const bool editingFile=!selectingFile; if(IsFocused()&&editingFile){ EditorUpdate(); } RenderTileset(); if(IsFocused()&&selectingFile){ SelectingFileUpdate(); } return true; } }; int main() { TiledCollisionEditor demo; if (demo.Construct(640, 180, 4, 4, false, true)) demo.Start(); return 0; }