Skip to content
Snippets Groups Projects
Commit de5f358c authored by Frederik Alexander Keens's avatar Frederik Alexander Keens
Browse files

Merge branch 'leveleditor' into 'main'

Leveleditor

See merge request !57
parents 9d7ecb3f 1febbab8
Branches
No related tags found
1 merge request!57Leveleditor
......@@ -15,51 +15,6 @@
namespace editor
{
/**
* Provides all tile names.
* They are necessary to create the correct path to
* the hdf5 dataset that contains the pixels for a given tile.
*/
const std::vector<std::string> TILE_NAMES(
{"plain",
"water",
"forest",
"mountain",
"bridge_horizontal",
"bridge_vertical",
"street_horizontal",
"street_vertical",
"street_crossing",
"street_junction_right",
"street_junction_left",
"street_junction_down",
"street_junction_up",
"street_corner_top_left",
"street_corner_top_right",
"street_corner_bottom_left",
"street_corner_bottom_right",
"riff",
"cliff_top",
"cliff_bottom",
"cliff_left",
"cliff_right",
"cliff_corner_top_left",
"cliff_corner_top_right",
"cliff_corner_bottom_left",
"cliff_corner_bottom_right",
"cliff_inverse_corner_top_left",
"cliff_inverse_corner_top_right",
"cliff_inverse_corner_bottom_left",
"cliff_inverse_corner_bottom_right"});
/**
* Provides the names for all factions in the game.
* The names can be used to create the path to the hdf5 dataset
* containing the buildings for a given faction.
*/
const std::vector<std::string>
FACTION_NAMES({"red", "blue", "yellow", "green", "purple", "neutral"});
/**
* SpriteProvider is only used in a static context.
* SpriteProvider allows us to get the QPixmap for a given tile type
......
......@@ -12,14 +12,50 @@
namespace editor
{
class Tile : public QGraphicsPixmapItem {
/**
* The bottom (static) Layer of the LevelScene consists of Tile instances.
* The purpose of the Tile object is to notify the QGraphicsScene when the mouse
* enters/leaves/presses a specific tile of the map.
* Thus Tile subclasses QGraphicsPixmapItem and overrides hoverEnterEvent, hoverLeaveEvent
* and mousePressEvent.
* Every Tile is instantiated with its index so that it can send that index to the LevelScene
* in case it registers a hover/enter/press event.
*/
class Tile : public QGraphicsPixmapItem
{
public:
/**
* Creates a Tile instance for the given index with the given tile id.
* @param index The index of the tile if you were to linearize the level map.
* @param id The tile id, e.g. 0 -> grass, 1-> water, ...
*/
Tile(int index, uint8_t id);
private:
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
/**
* When a hover enter event is received it
* emits the onTileEntered event method with the tile's index.
* The LevelScene overwrites the onTileEntered event method.
*/
void hoverEnterEvent(QGraphicsSceneHoverEvent*) override;
/**
* When a hover leave event is received it
* emits the onTileExited event method with the tile's index.
* The LevelScene overwrites the onTileExited event method.
*/
void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override;
/**
* When a mouse press event is received it
* emits the onTileClicked event method with the tile's index.
* The LevelScene overwrites the onTileClicked event method.
*/
void mousePressEvent(QGraphicsSceneMouseEvent*) override;
/**
* The index of the tile if you were to linearize the level map.
*/
int m_index;
};
......
/**
* TileSelector.cpp
* TileButton.cpp
*
* @date 29.01.2025
* @author Nils Jonathan Friedrich Eckardt implementation
......
/**
* TileSelector.hpp
* TileButton.hpp
*
* @date 29.01.2025
* @author Nils Jonathan Friedrich Eckardt implementation
......@@ -12,13 +12,32 @@
namespace editor
{
class TileButton : public QPushButton {
/**
* TileButtons are the Selection Buttons within the TileSelector.
* They listen for a mouse press and emit the onNewTileSelected event methode
* and transmits with it the selected id of the selected tile.
*/
class TileButton : public QPushButton
{
public:
/**
* Creates a TileButton with the texture of a tile and the provided parent.
* @param id The id of the tile on this button.
* @param parent The parent to set for this button.
*/
TileButton(const uint8_t id, QWidget* parent = nullptr);
protected:
/**
* When a mouse press event is received,
* it emits the onNewTileIdSelected event with its tile's id.
*/
void mousePressEvent(QMouseEvent* event) override;
private:
/**
* The id of this button.
*/
uint8_t m_id;
};
......
......@@ -61,11 +61,13 @@ QLabel* TileSelector::createNewLabel(QWidget* parent, const char* text)
return label;
}
template <typename T>
void TileSelector::sectionLayout(QGridLayout*& layout, int usedIdCounter, QWidget* parent, T id)
template <typename... T>
QGridLayout* TileSelector::creatSectionLayout(QWidget* parent, T... ids)
{
TileButton* button = new TileButton(id, parent);
layout->addWidget(button, usedIdCounter / 3, usedIdCounter % 3);
QGridLayout* layout = new QGridLayout();
int usedIdCounter = 0;
sectionLayout(layout, usedIdCounter, parent, ids...);
return layout;
}
template <typename T, typename... Rest>
......@@ -78,13 +80,11 @@ void TileSelector::sectionLayout(
sectionLayout(layout, usedIdCounter, parent, ids...);
}
template <typename... T>
QGridLayout* TileSelector::creatSectionLayout(QWidget* parent, T... ids)
template <typename T>
void TileSelector::sectionLayout(QGridLayout*& layout, int usedIdCounter, QWidget* parent, T id)
{
QGridLayout* layout = new QGridLayout();
int usedIdCounter = 0;
sectionLayout(layout, usedIdCounter, parent, ids...);
return layout;
TileButton* button = new TileButton(id, parent);
layout->addWidget(button, usedIdCounter / 3, usedIdCounter % 3);
}
} // namespace editor
\ No newline at end of file
......@@ -2,8 +2,8 @@
* TileSelector.cpp
*
* @date 28.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de) skeleton
* @author Nils Jonathan Friedrich Eckardt implementation
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de) skeleton
*/
#pragma once
......@@ -15,22 +15,67 @@
namespace editor
{
/**
* The TileSelector is part of the MainWindow.
* It displays the tiles that can be selected within categories, to be placed on the
* map of the LevelScene.
*/
class TileSelector : public QScrollArea
{
public:
/**
* Creates the TileSelector with the provided parent.
* The appearance of the selector is hardcoded by the arguments it provides its functions.
* There should not be created more then one of these at a time.
* @param parent The parent to set for the TileSelector
*/
TileSelector(QWidget* parent = nullptr);
private:
/**
* Creates a label of a category to be displayed.
* @param parent The parent to set for the QLabel.
* @param text The text of the label.
*/
QLabel* createNewLabel(QWidget* parent, const char* text);
template <typename T>
void sectionLayout(QGridLayout*& layout, int usedIdCounter, QWidget* parent, T id);
/**
* Creates a section of the TileSelector.
* Before this there should always be a category label be created.
* The section contains TileButtons listed in the same order, that the ids for them
* are provided. After 3 buttons the next line is used. Reordering the TileButtons
* can simply be done by changing the order in which their ids are provided.
* @param parent The parent to set for this QGridLayout.
* @param ids A list of the ids for which TileButtons will be created.
*/
template <typename... T>
QGridLayout* creatSectionLayout(QWidget* parent, T... ids);
/**
* A recursive helper function for createSectionLayout.
* Each time it is called it adds a TileButton for the first of the provided ids to the
* QGridLayout. The id then is discarded and this function is called again with the
* remaining ids.
* @param layout A link to the QGridLayout the TileButtons are added to.
* @param usedIdCounter Contains what the number of the current used id is. Needed to
* add the TileButton to the correct position of the grid.
* @param parent The parent the TileButtons will be set to.
* @param id The current id a TileButton will be created for.
* @param ids The remaining ids for which a TileButton will be created in a future
* function call.
*/
template <typename T, typename... Rest>
void
sectionLayout(QGridLayout*& layout, int usedIdCounter, QWidget* parent, T id, Rest... ids);
template <typename... T>
QGridLayout* creatSectionLayout(QWidget* parent, T... ids);
/**
* The end of the recursive helper function sectionLayout.
* When this is called no more but 1 id is remaining for which a TileButton will be created.
* @param layout A link to the QGridLayout the TileButtons are added to.
* @param usedIdCounter Contains what the number of the current used id is. Needed to
* add the TileButton to the correct position of the grid.
* @param parent The parent the TileButtons will be set to.
* @param id The current id a TileButton will be created for.
*/
template <typename T>
void sectionLayout(QGridLayout*& layout, int usedIdCounter, QWidget* parent, T id);
};
} // namespace editor
\ No newline at end of file
......@@ -12,7 +12,7 @@
#include <QLabel>
#include <QSize>
#include "AutomateButton.hpp"
#include "AdvancedPlacementSwitch.hpp"
#include "LevelNameEdit.hpp"
#include "SaveButton.hpp"
#include "ZoomInButton.hpp"
......@@ -41,8 +41,13 @@ TopBar::TopBar(const std::string& levelName, QWidget* parent) : QToolBar(parent)
QPushButton* zoomIn = new ZoomInButton(rightContainer);
QPushButton* zoomOut = new ZoomOutButton(rightContainer);
QPushButton* zoomInfo = new ZoomInfo(rightContainer);
auto* checkedBox = new AutomateButton("Kacheln automatisch setzen", container);
leftLayout->addWidget(checkedBox);
AdvancedPlacementSwitch* symmetryBox =
new AdvancedPlacementSwitch("symmetrisches Bearbeiten", false, container);
leftLayout->addWidget(
symmetryBox); // diese Reihenfolge vermeidet einen Bug, bei dem False zu true wird
AdvancedPlacementSwitch* placementBox =
new AdvancedPlacementSwitch("Kacheln automatisch setzen", true, container);
leftLayout->addWidget(placementBox);
rightLayout->addStretch();
rightLayout->addWidget(zoomOut);
rightLayout->addWidget(zoomInfo);
......
......@@ -12,9 +12,26 @@
namespace editor
{
/**
* The TopBar provides key functionality for the LevelEditor:
* - LineEdit for setting the level name
* - CheckBox for toggling advanced placement on and off
* - ZoomInButton
* - ZoomOutButton
* - ZoomInfo (displays current zoom percentage)
* - SaveButon
* TopBar inherits from QToolBar to ensure the QT look and fell.
*/
class TopBar : public QToolBar
{
public:
/**
* Creates a TopBar with the provided levelName and parent.
* The levelName is needed to prefill the LineEdit in the TopBar with
* the current level name.
* @param levelName The current name of the level.
* @param parent The parent to set for the TopBar.
*/
TopBar(const std::string& levelName, QWidget* parent = nullptr);
};
......
......@@ -12,11 +12,26 @@
namespace editor
{
class ZoomInButton : public QPushButton {
/**
* The ZoomInButton is a part of the TopBar.
* It listens to mouse presses and emits the onZoomed event method
* with a delta of 0.25 in case of a mouse press.
*/
class ZoomInButton : public QPushButton
{
public:
/**
* Creates a ZoomInButton with the provided parent.
* @param parent The parent to set for the ZoomInButton.
*/
ZoomInButton(QWidget* parent = nullptr);
private:
void mousePressEvent(QMouseEvent* event) override;
/**
* When a mouse press event is received,
* it emits the onZoomed event method with a delta of 0.25.
*/
void mousePressEvent(QMouseEvent*) override;
};
} // namespace editor
\ No newline at end of file
......@@ -14,13 +14,27 @@
namespace editor
{
/**
* ZoomInfo inherits from QPushButton, but doesn't actually act as a button.
* It just displays the current zoom percentage.
* To update the text that is displayed in case of a zoom change, it
* inherits from EventHandler and overwrites the onZoomed event method.
*/
class ZoomInfo : public QPushButton, public EventHandler
{
public:
ZoomInfo(QWidget* parent = nullptr);
private:
/**
* The percentage by which the LevelMap is zoomed in/out at the moment.
*/
double m_currentScale;
/**
* Updates the zoom info text to correctly display the updated zoom percentage.
* @param delta The percentage to add/subtract to the current zoom percentage.
*/
void onZoomed(double delta) override;
};
......
......@@ -12,11 +12,26 @@
namespace editor
{
class ZoomOutButton : public QPushButton {
/**
* The ZoomOutButton is a part of the TopBar.
* It listens to mouse presses and emits the onZoomed event method
* with a delta of -0.25 in case of a mouse press.
*/
class ZoomOutButton : public QPushButton
{
public:
/**
* Creates a ZoomInButton with the provided parent.
* @param parent The parent to set for the ZoomInButton.
*/
ZoomOutButton(QWidget* parent = nullptr);
private:
void mousePressEvent(QMouseEvent* event) override;
/**
* When a mouse press event is received,
* it emits the onZoomed event method with a delta of 0.25.
*/
void mousePressEvent(QMouseEvent*) override;
};
} // namespace editor
\ No newline at end of file
......@@ -6,54 +6,192 @@
*/
#include <QApplication>
#include <QCommandLineParser>
#include <filesystem>
#include "LevelScene.hpp"
#include "MainWindow.hpp"
#include "SpriteProvider.hpp"
#include "highfive/H5File.hpp"
using namespace editor;
/**
* The main function starts the program.
* It checks if any command line arguments were provided.
* If not it terminates.
* Otherwise if a path to a hdf5 level file was provided it
* starts the level editor and loads the level from the file for editing.
* If a width and height was provided, it create a blank level with the given dimensions.
* For launching the level editor, we implemented a full fledged command line application
* with the help of QCommandLineParser.
* The command line application that is used for launching the level editor supports command like:
* ./editor --help
* ./editor --version
* ./editor --width
* ./editor --width 20 --height 20
* ./editor --edit ../res/level-file-to-edit.h5
*/
int main(int argc, char* argv[])
/**
* Holds the launch options for the level editor.
* They are constructed from the provided command line options
* in the loadLevelOptions function.
*/
struct LevelOptions
{
if (argc < 2)
{ // no arguments provided
std::cerr << "Bitte uebergben Sie den Pfad zu dem Level, das sie bearbeiten wollen oder "
"die Breite und Hoehe des Levels, das sie neu erstellen wollen."
<< "\n";
return 1;
int width;
int height;
std::string levelFilepath;
};
/**
* loadLevelOptions implements a full fledged command line application for retrieving the
* level options.
* For that purpose it creates a QCoreApplication and QCommandLineParser object.
* QCoreApplication is just used for loading the level options.
* It is destroyed at function exit.
* Through QCommandLineParser we support commands like --help, --version
* loadLevelOptions also makes sure that the optionally provided arguments for width, height and
* level h5 file path are valid.
* All of these arguments have default values: width/height: 20, level h5 file path: ''
* @param argc The number of command line arguments provided by the user.
* @param argv All of the command line arguments provided by the user.
* @returns If the user provided options are valid, they will be returned inside an optional,
* else an empty optional will be returned.
*/
std::optional<LevelOptions> loadLevelOptions(int argc, char* argv[])
{
// Sperately from the QApplication we use for the Level Editor UI, we use QCoreApplication for
// the command line application for launching the editor.
// The reason is that using QApplication is very slow, because it has to initialize all of the
// internal UI stuff. That is not what you want if only a simple ./editor --help was executed.
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationVersion("1.0.0");
QCommandLineParser parser;
// application description with examples for help output of ./editor --help
std::string topSection = R"(QT Level Editor for creating/editing Maps of Advanced Wars.
Examples:
./editor Create new level map (default 20x20).
./editor --width 15 --height 15 Create new level map (15x15).
./editor --edit ./level.h5 Edit existing level map.)";
parser.addVersionOption();
parser.addHelpOption();
parser.setApplicationDescription(topSection.c_str());
parser.addOption(QCommandLineOption(
"width", "The width of the new level map (default 20, minimum: 3).", "number-of-tiles",
"20"));
parser.addOption(QCommandLineOption(
"height", "The height of the new level map (default 20, minimum: 3).", "number-of-tiles",
"20"));
parser.addOption(QCommandLineOption(
"edit",
"The path to the HDF5 level map file that you want to edit. If edit is not set, a new "
"level map is created.",
"filepath"));
parser.process(app);
// QT Parser does not exit when unexpected positional arguments are provided,
// so have to do it by hand. 0 positional arguments are expected.
if (parser.positionalArguments().size() > 0)
{
std::cerr << "Command not recognized.\n" << std::endl;
parser.showHelp(1); // shows the help info and quits the application with exit code 1
return std::nullopt;
}
// make sure that provided with, height and level file path are valid
// if yes add them to the level options struct
bool widthOk;
bool heightOk;
LevelOptions options;
options.width = parser.value("width").toUInt(&widthOk);
options.height = parser.value("height").toUInt(&heightOk);
if (!widthOk || options.width < 3)
{
std::cerr << "You provided an invalid level width: '" << parser.value("width").toStdString()
<< "'\nThe level width has to be a valid number >= 3." << std::endl;
return std::nullopt;
}
if (!heightOk || options.height < 3)
{
std::cerr << "You provided an invalid level height: '"
<< parser.value("height").toStdString()
<< "'\nThe level height has to be a valid number >= 3." << std::endl;
return std::nullopt;
}
options.levelFilepath = parser.value("edit").toStdString();
if (!options.levelFilepath.empty() && !std::filesystem::exists(options.levelFilepath))
{
std::cerr << "The level map file path: '" << options.levelFilepath << "' does not exist."
<< std::endl;
return std::nullopt;
}
return options;
}
if (argc > 3)
{ // more than 2 arguments provided
std::cerr << "Zuviele Kommandozeilenargumente." << "\n";
int main(int argc, char* argv[])
{
std::optional<LevelOptions> levelOptions = loadLevelOptions(argc, argv);
if (!levelOptions.has_value())
{
return 1;
}
// programm starts
QApplication app(argc, argv);
// SpriteProvider is initialized once and can then be used from anywhere in the application
// to get the QGraphicsPixmap for a specific tile.
if (!std::filesystem::exists("../res/spritesheet.h5"))
{
std::cerr << "The spritesheet was not found.\nIt was expected in: ../res/spritesheet.h5"
<< std::endl;
return 1;
}
try
{
SpriteProvider::initialize("../res/spritesheet.h5");
}
catch (const HighFive::Exception&)
{
std::cerr << "Loading the spritesheet failed.\nPlease make sure that it is not "
"corrupted.\nIt was loaded from: ../res/spritesheet.h5"
<< std::endl;
return 1;
}
LevelScene* level = nullptr;
if (argc == 2)
{ // 1 argument provided => create Level from file
level = LevelScene::fromFile(argv[1]);
// initialize LevelScene
LevelScene* level;
if (!levelOptions.value().levelFilepath.empty())
{
try
{
level = LevelScene::fromFile(levelOptions.value().levelFilepath);
}
catch (const HighFive::Exception&)
{
std::cerr << "Loading an existing level map from '"
<< levelOptions.value().levelFilepath
<< "' failed.\nPlease make sure that the hdf5 level file is not corrupted."
<< std::endl;
return 1;
}
}
else
{ // 2 arguments provided => create blank level with given width and height
level = LevelScene::empty("Mein Level", std::stoi(argv[1]), std::stoi(argv[2]));
{
level =
LevelScene::empty("My Level", levelOptions.value().width, levelOptions.value().height);
}
// We create the MainWindow on the Stack. Thus it will be destructed
// automatically at exit. The way memory management works in QT is that
// you can set a parent on every Widget class you create. If the parent gets
// destroyed all its children are destroyed as well. It is acutally save in both
// directions. It doesn't matter if you destroy the child first and the parent later.
// In our case, because all of the widgets in our application are eventual descendants of
// MainWindow, they are all automatically cleaned up at program exit. For that reason
// memory management for the level editor is very simple.
// For more info on QT Object Trees & Ownership
// @see https://doc.qt.io/qt-5/objecttrees.html
MainWindow window(level);
window.resize(1300, 800);
window.show();
......
......@@ -44,21 +44,14 @@ Level::Level(const std::string& path, Engine& engine)
m_physicsEngine = std::make_unique<PhysicsEngine>();
m_combatEngine = std::make_unique<CombatEngine>();
HighFive::File file(path, HighFive::File::ReadOnly);
HighFive::DataSet tilesarraySet = file.getDataSet("tilesarray");
// read level metadata
std::string levelMetadata;
file.getDataSet("metadata").read(levelMetadata);
// read tilesarray
std::vector<uint8_t> levelTilesarray;
file.getDataSet("tilesarray").read(levelTilesarray);
std::cout << "tilesarray size: " << levelTilesarray.size() << "\n";
// extract metadata from xml
std::istringstream xmlStream(levelMetadata);
boost::property_tree::ptree pt;
boost::property_tree::read_xml(xmlStream, pt);
m_width = pt.get<int>("level.width");
m_height = pt.get<int>("level.height");
tilesarraySet.getAttribute("width").read(m_width);
tilesarraySet.getAttribute("height").read(m_height);
tilesarraySet.getAttribute("name").read(m_name);
tilesarraySet.read(levelTilesarray);
// if level is smaler than 20x20 surround with water tiles
if (m_width < 20 || m_height < 20)
......
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <highfive/H5File.hpp>
int main()
{
boost::property_tree::ptree pt;
// Add data to the property tree
pt.put("level.width", "15");
pt.put("level.height", "10");
pt.put("level.name", "Alpha");
std::ostringstream xmlStream;
boost::property_tree::write_xml(xmlStream, pt);
std::string xml_data = xmlStream.str();
std::vector<uint8_t> levelmap = {
2, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 1, 0, 0, 0, 0, 2, 3, 0, 0, 0, 3, 3, 3, 3,
0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 76,
76, 51, 3, 0, 0, 0, 1, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 57, 1, 3, 0, 0,
50, 0, 0, 0, 76, 77, 0, 3, 0, 0, 0, 1, 0, 0, 55, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0,
0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 1, 0, 3, 3, 2, 3, 3, 0, 0,
0, 0, 0, 0, 0, 0, 4, 0, 0, 2, 3, 3, 3, 3, 0, 3, 2, 2, 3, 2, 3, 1, 3, 2, 2,
};
HighFive::File file("level.h5", HighFive::File::Truncate);
file.createDataSet<std::string>("metadata", HighFive::DataSpace::From(xmlStream))
.write(xml_data);
file.createDataSet<uint8_t>("tilesarray", HighFive::DataSpace::From(levelmap)).write(levelmap);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment