最终整理版
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
#include "TmxMap.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <pugixml.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t FlippedHorizontally = 0x80000000u;
|
||||
constexpr std::uint32_t FlippedVertically = 0x40000000u;
|
||||
constexpr std::uint32_t FlippedDiagonally = 0x20000000u;
|
||||
constexpr std::uint32_t FlippedHex120 = 0x10000000u;
|
||||
|
||||
std::string Trim(std::string value)
|
||||
{
|
||||
auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
|
||||
auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base();
|
||||
if (first >= last) {
|
||||
return {};
|
||||
}
|
||||
return std::string(first, last);
|
||||
}
|
||||
|
||||
std::string LowerCopy(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::filesystem::path NormalizePath(const std::filesystem::path& base, const char* relative)
|
||||
{
|
||||
return std::filesystem::weakly_canonical(base / relative);
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ReadProperties(const pugi::xml_node& node)
|
||||
{
|
||||
std::map<std::string, std::string> result;
|
||||
for (pugi::xml_node property : node.child("properties").children("property")) {
|
||||
const std::string name = property.attribute("name").as_string();
|
||||
const char* valueAttr = property.attribute("value").as_string(nullptr);
|
||||
if (valueAttr) {
|
||||
result[name] = valueAttr;
|
||||
} else {
|
||||
result[name] = property.text().as_string();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> ParsePoints(const std::string& points);
|
||||
|
||||
std::vector<TmxCollisionShape> ParseCollisionShapes(const pugi::xml_node& tile)
|
||||
{
|
||||
std::vector<TmxCollisionShape> shapes;
|
||||
for (pugi::xml_node object : tile.child("objectgroup").children("object")) {
|
||||
TmxCollisionShape shape;
|
||||
shape.x = object.attribute("x").as_float();
|
||||
shape.y = object.attribute("y").as_float();
|
||||
shape.width = object.attribute("width").as_float();
|
||||
shape.height = object.attribute("height").as_float();
|
||||
if (pugi::xml_node polygon = object.child("polygon")) {
|
||||
shape.polygon = ParsePoints(polygon.attribute("points").as_string());
|
||||
} else if (pugi::xml_node polyline = object.child("polyline")) {
|
||||
shape.polygon = ParsePoints(polyline.attribute("points").as_string());
|
||||
}
|
||||
if (!shape.polygon.empty() || shape.width > 0.0f || shape.height > 0.0f) {
|
||||
shapes.push_back(std::move(shape));
|
||||
}
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
TmxTileset ParseTilesetNode(const pugi::xml_node& node, std::uint32_t firstGid, const std::filesystem::path& baseDir)
|
||||
{
|
||||
TmxTileset tileset;
|
||||
tileset.firstGid = firstGid;
|
||||
tileset.name = node.attribute("name").as_string();
|
||||
tileset.tileWidth = node.attribute("tilewidth").as_int();
|
||||
tileset.tileHeight = node.attribute("tileheight").as_int();
|
||||
tileset.tileCount = node.attribute("tilecount").as_int();
|
||||
tileset.columns = node.attribute("columns").as_int();
|
||||
|
||||
pugi::xml_node image = node.child("image");
|
||||
if (!image) {
|
||||
throw std::runtime_error("Tileset has no image: " + tileset.name);
|
||||
}
|
||||
tileset.imagePath = NormalizePath(baseDir, image.attribute("source").as_string());
|
||||
for (pugi::xml_node tile : node.children("tile")) {
|
||||
const auto tileId = static_cast<std::uint32_t>(tile.attribute("id").as_uint());
|
||||
std::vector<TmxCollisionShape> collisionShapes = ParseCollisionShapes(tile);
|
||||
if (!collisionShapes.empty()) {
|
||||
tileset.collisionShapes[tileId] = std::move(collisionShapes);
|
||||
}
|
||||
|
||||
pugi::xml_node animation = tile.child("animation");
|
||||
if (!animation) {
|
||||
continue;
|
||||
}
|
||||
std::vector<TmxAnimationFrame> frames;
|
||||
for (pugi::xml_node frame : animation.children("frame")) {
|
||||
frames.push_back(TmxAnimationFrame{
|
||||
static_cast<std::uint32_t>(frame.attribute("tileid").as_uint()),
|
||||
frame.attribute("duration").as_int(),
|
||||
});
|
||||
}
|
||||
if (!frames.empty()) {
|
||||
tileset.animations[tileId] = std::move(frames);
|
||||
}
|
||||
}
|
||||
return tileset;
|
||||
}
|
||||
|
||||
TmxTileset ParseTileset(const pugi::xml_node& node, const std::filesystem::path& mapDir)
|
||||
{
|
||||
const auto firstGid = static_cast<std::uint32_t>(node.attribute("firstgid").as_uint());
|
||||
const char* source = node.attribute("source").as_string(nullptr);
|
||||
if (!source) {
|
||||
return ParseTilesetNode(node, firstGid, mapDir);
|
||||
}
|
||||
|
||||
const std::filesystem::path tsxPath = NormalizePath(mapDir, source);
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result ok = doc.load_file(tsxPath.c_str());
|
||||
if (!ok) {
|
||||
throw std::runtime_error("Could not parse TSX: " + tsxPath.string());
|
||||
}
|
||||
return ParseTilesetNode(doc.child("tileset"), firstGid, tsxPath.parent_path());
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> ParseCsvGids(const std::string& csv)
|
||||
{
|
||||
std::vector<std::uint32_t> gids;
|
||||
std::stringstream stream(csv);
|
||||
std::string item;
|
||||
while (std::getline(stream, item, ',')) {
|
||||
item = Trim(item);
|
||||
if (!item.empty()) {
|
||||
gids.push_back(static_cast<std::uint32_t>(std::stoul(item)));
|
||||
}
|
||||
}
|
||||
return gids;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> ParsePoints(const std::string& points)
|
||||
{
|
||||
std::vector<TmxPoint> out;
|
||||
std::stringstream stream(points);
|
||||
std::string pair;
|
||||
while (stream >> pair) {
|
||||
const std::size_t comma = pair.find(',');
|
||||
if (comma == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
TmxPoint point;
|
||||
point.x = std::stof(pair.substr(0, comma));
|
||||
point.y = std::stof(pair.substr(comma + 1));
|
||||
out.push_back(point);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
std::map<std::string, TmxMap>& TmxMapCache()
|
||||
{
|
||||
static std::map<std::string, TmxMap> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint32_t CleanGid(std::uint32_t gid)
|
||||
{
|
||||
return gid & ~(FlippedHorizontally | FlippedVertically | FlippedDiagonally | FlippedHex120);
|
||||
}
|
||||
|
||||
TmxMap LoadTmxMap(const std::filesystem::path& path)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result ok = doc.load_file(path.c_str());
|
||||
if (!ok) {
|
||||
throw std::runtime_error("Could not parse TMX: " + path.string());
|
||||
}
|
||||
|
||||
pugi::xml_node root = doc.child("map");
|
||||
if (!root) {
|
||||
throw std::runtime_error("TMX file has no map root: " + path.string());
|
||||
}
|
||||
|
||||
TmxMap map;
|
||||
map.sourcePath = std::filesystem::weakly_canonical(path);
|
||||
map.width = root.attribute("width").as_int();
|
||||
map.height = root.attribute("height").as_int();
|
||||
map.tileWidth = root.attribute("tilewidth").as_int();
|
||||
map.tileHeight = root.attribute("tileheight").as_int();
|
||||
map.properties = ReadProperties(root);
|
||||
|
||||
const std::filesystem::path mapDir = map.sourcePath.parent_path();
|
||||
for (pugi::xml_node child : root.children()) {
|
||||
const std::string nodeName = child.name();
|
||||
if (nodeName == "tileset") {
|
||||
map.tilesets.push_back(ParseTileset(child, mapDir));
|
||||
} else if (nodeName == "layer") {
|
||||
pugi::xml_node data = child.child("data");
|
||||
const std::string encoding = data.attribute("encoding").as_string();
|
||||
if (encoding != "csv") {
|
||||
continue;
|
||||
}
|
||||
TmxLayer layer;
|
||||
layer.name = child.attribute("name").as_string();
|
||||
layer.width = child.attribute("width").as_int();
|
||||
layer.height = child.attribute("height").as_int();
|
||||
layer.visible = child.attribute("visible").as_int(1) != 0;
|
||||
layer.opacity = child.attribute("opacity").as_float(1.0f);
|
||||
layer.gids = ParseCsvGids(data.text().as_string());
|
||||
map.layers.push_back(std::move(layer));
|
||||
} else if (nodeName == "objectgroup") {
|
||||
for (pugi::xml_node object : child.children("object")) {
|
||||
TmxObject out;
|
||||
out.id = object.attribute("id").as_int();
|
||||
out.name = object.attribute("name").as_string();
|
||||
out.type = object.attribute("type").as_string();
|
||||
out.x = object.attribute("x").as_float();
|
||||
out.y = object.attribute("y").as_float();
|
||||
out.width = object.attribute("width").as_float();
|
||||
out.height = object.attribute("height").as_float();
|
||||
if (pugi::xml_node polygon = object.child("polygon")) {
|
||||
out.polygon = ParsePoints(polygon.attribute("points").as_string());
|
||||
} else if (pugi::xml_node polyline = object.child("polyline")) {
|
||||
out.polygon = ParsePoints(polyline.attribute("points").as_string());
|
||||
}
|
||||
out.properties = ReadProperties(object);
|
||||
map.objects.push_back(std::move(out));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(map.tilesets.begin(), map.tilesets.end(), [](const TmxTileset& a, const TmxTileset& b) {
|
||||
return a.firstGid < b.firstGid;
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
const TmxMap& LoadTmxMapCached(const std::filesystem::path& path)
|
||||
{
|
||||
const std::filesystem::path canonical = std::filesystem::weakly_canonical(path);
|
||||
const std::string key = canonical.string();
|
||||
std::map<std::string, TmxMap>& cache = TmxMapCache();
|
||||
auto [it, inserted] = cache.try_emplace(key);
|
||||
if (inserted) {
|
||||
it->second = LoadTmxMap(canonical);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void ClearTmxMapCache()
|
||||
{
|
||||
TmxMapCache().clear();
|
||||
}
|
||||
|
||||
const TmxLayer* FindLayer(const TmxMap& map, const std::string& name)
|
||||
{
|
||||
auto it = std::find_if(map.layers.begin(), map.layers.end(), [&](const TmxLayer& layer) {
|
||||
return layer.name == name;
|
||||
});
|
||||
return it == map.layers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
const TmxObject* FindObject(const TmxMap& map, const std::string& name)
|
||||
{
|
||||
auto it = std::find_if(map.objects.begin(), map.objects.end(), [&](const TmxObject& object) {
|
||||
return object.name == name;
|
||||
});
|
||||
return it == map.objects.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
const TmxTileset* FindTilesetForGid(const TmxMap& map, std::uint32_t gid)
|
||||
{
|
||||
gid = CleanGid(gid);
|
||||
const TmxTileset* found = nullptr;
|
||||
for (const TmxTileset& tileset : map.tilesets) {
|
||||
if (gid >= tileset.firstGid) {
|
||||
found = &tileset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t LayerGidAt(const TmxLayer& layer, int x, int y)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= layer.width || y >= layer.height) {
|
||||
return 0;
|
||||
}
|
||||
const std::size_t index = static_cast<std::size_t>(y * layer.width + x);
|
||||
return index < layer.gids.size() ? CleanGid(layer.gids[index]) : 0;
|
||||
}
|
||||
|
||||
bool PointInRect(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
return x >= shape.x
|
||||
&& y >= shape.y
|
||||
&& x < shape.x + shape.width
|
||||
&& y < shape.y + shape.height;
|
||||
}
|
||||
|
||||
bool PointOnSegment(float px, float py, TmxPoint a, TmxPoint b)
|
||||
{
|
||||
const float cross = (px - a.x) * (b.y - a.y) - (py - a.y) * (b.x - a.x);
|
||||
if (std::abs(cross) > 0.001f) {
|
||||
return false;
|
||||
}
|
||||
return px >= std::min(a.x, b.x) - 0.001f
|
||||
&& px <= std::max(a.x, b.x) + 0.001f
|
||||
&& py >= std::min(a.y, b.y) - 0.001f
|
||||
&& py <= std::max(a.y, b.y) + 0.001f;
|
||||
}
|
||||
|
||||
bool PointInPolygon(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
if (shape.polygon.size() < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> points;
|
||||
points.reserve(shape.polygon.size());
|
||||
for (const TmxPoint& point : shape.polygon) {
|
||||
points.push_back({shape.x + point.x, shape.y + point.y});
|
||||
}
|
||||
|
||||
bool inside = false;
|
||||
for (std::size_t i = 0, j = points.size() - 1; i < points.size(); j = i++) {
|
||||
const TmxPoint a = points[i];
|
||||
const TmxPoint b = points[j];
|
||||
if (PointOnSegment(x, y, a, b)) {
|
||||
return true;
|
||||
}
|
||||
const bool crosses = ((a.y > y) != (b.y > y))
|
||||
&& (x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x);
|
||||
if (crosses) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
bool PointInCollisionShape(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
if (!shape.polygon.empty()) {
|
||||
return PointInPolygon(x, y, shape);
|
||||
}
|
||||
return PointInRect(x, y, shape);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsPointBlockedByCollision(const TmxMap& map, float worldX, float worldY)
|
||||
{
|
||||
if (worldX < 0.0f || worldY < 0.0f
|
||||
|| worldX >= static_cast<float>(map.width * map.tileWidth)
|
||||
|| worldY >= static_cast<float>(map.height * map.tileHeight)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (map.tileWidth <= 0 || map.tileHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int tileX = static_cast<int>(std::floor(worldX / static_cast<float>(map.tileWidth)));
|
||||
const int tileY = static_cast<int>(std::floor(worldY / static_cast<float>(map.tileHeight)));
|
||||
for (const TmxLayer& layer : map.layers) {
|
||||
if (!layer.visible || !IsTmxCollisionLayer(layer.name)) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t gid = LayerGidAt(layer, tileX, tileY);
|
||||
if (gid == 0) {
|
||||
continue;
|
||||
}
|
||||
const TmxTileset* tileset = FindTilesetForGid(map, gid);
|
||||
if (!tileset) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t localTileId = gid - tileset->firstGid;
|
||||
const auto shapes = tileset->collisionShapes.find(localTileId);
|
||||
if (shapes == tileset->collisionShapes.end() || shapes->second.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const float localX = (worldX - static_cast<float>(tileX * map.tileWidth))
|
||||
* static_cast<float>(std::max(1, tileset->tileWidth)) / static_cast<float>(map.tileWidth);
|
||||
const float localY = (worldY - static_cast<float>(tileY * map.tileHeight))
|
||||
* static_cast<float>(std::max(1, tileset->tileHeight)) / static_cast<float>(map.tileHeight);
|
||||
for (const TmxCollisionShape& shape : shapes->second) {
|
||||
if (PointInCollisionShape(localX, localY, shape)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsTmxCollisionLayer(const std::string& layerName)
|
||||
{
|
||||
return LowerCopy(layerName).find("collision") != std::string::npos;
|
||||
}
|
||||
|
||||
bool IsTmxOverlayLayer(const std::string& layerName)
|
||||
{
|
||||
const std::string lower = LowerCopy(layerName);
|
||||
return lower.find("fringe") != std::string::npos || lower.find("over") != std::string::npos;
|
||||
}
|
||||
|
||||
std::uint32_t AnimatedLocalTileId(const TmxTileset& tileset, std::uint32_t localTileId, int elapsedMs)
|
||||
{
|
||||
const auto it = tileset.animations.find(localTileId);
|
||||
if (it == tileset.animations.end() || it->second.empty()) {
|
||||
return localTileId;
|
||||
}
|
||||
|
||||
int totalDuration = 0;
|
||||
for (const TmxAnimationFrame& frame : it->second) {
|
||||
totalDuration += std::max(0, frame.durationMs);
|
||||
}
|
||||
if (totalDuration <= 0) {
|
||||
return localTileId;
|
||||
}
|
||||
|
||||
int cursor = std::max(0, elapsedMs) % totalDuration;
|
||||
for (const TmxAnimationFrame& frame : it->second) {
|
||||
const int duration = std::max(0, frame.durationMs);
|
||||
if (cursor < duration) {
|
||||
return frame.tileId;
|
||||
}
|
||||
cursor -= duration;
|
||||
}
|
||||
return it->second.back().tileId;
|
||||
}
|
||||
|
||||
TileBounds VisibleTileBounds(
|
||||
const TmxMap& map,
|
||||
float cameraTargetX,
|
||||
float cameraTargetY,
|
||||
float screenWidth,
|
||||
float screenHeight,
|
||||
float zoom,
|
||||
int paddingTiles)
|
||||
{
|
||||
const float safeZoom = std::max(0.1f, zoom);
|
||||
const float halfWorldWidth = screenWidth / safeZoom * 0.5f;
|
||||
const float halfWorldHeight = screenHeight / safeZoom * 0.5f;
|
||||
const float minWorldX = cameraTargetX - halfWorldWidth;
|
||||
const float maxWorldX = cameraTargetX + halfWorldWidth;
|
||||
const float minWorldY = cameraTargetY - halfWorldHeight;
|
||||
const float maxWorldY = cameraTargetY + halfWorldHeight;
|
||||
|
||||
TileBounds bounds;
|
||||
bounds.minX = std::clamp(static_cast<int>(std::floor(minWorldX / static_cast<float>(map.tileWidth))) - paddingTiles, 0, std::max(0, map.width - 1));
|
||||
bounds.maxX = std::clamp(static_cast<int>(std::ceil(maxWorldX / static_cast<float>(map.tileWidth))) + paddingTiles, 0, std::max(0, map.width - 1));
|
||||
bounds.minY = std::clamp(static_cast<int>(std::floor(minWorldY / static_cast<float>(map.tileHeight))) - paddingTiles, 0, std::max(0, map.height - 1));
|
||||
bounds.maxY = std::clamp(static_cast<int>(std::ceil(maxWorldY / static_cast<float>(map.tileHeight))) + paddingTiles, 0, std::max(0, map.height - 1));
|
||||
return bounds;
|
||||
}
|
||||
Reference in New Issue
Block a user