From a6bf95c5707fd4b51a00ddc24a90f954955cd18f Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 21 Sep 2026 22:36:46 +0300 Subject: [PATCH] implement parsing multiple levels from map file --- assets/tiled/map.tmx | 9 +- libs/tiled/src/object.zig | 7 +- libs/tiled/src/property.zig | 31 ++++- libs/tiled/src/tilemap.zig | 95 +++++++++---- libs/tiled/src/tileset.zig | 13 +- src/app.zig | 256 +++++++++++++++++++++++++----------- 6 files changed, 296 insertions(+), 115 deletions(-) diff --git a/assets/tiled/map.tmx b/assets/tiled/map.tmx index bdd1c49..865645c 100644 --- a/assets/tiled/map.tmx +++ b/assets/tiled/map.tmx @@ -1,5 +1,5 @@ - + @@ -85,14 +85,17 @@ + + + - + - + diff --git a/libs/tiled/src/object.zig b/libs/tiled/src/object.zig index 782fca0..35c7551 100644 --- a/libs/tiled/src/object.zig +++ b/libs/tiled/src/object.zig @@ -3,6 +3,7 @@ const std = @import("std"); const xml = @import("./xml.zig"); const Position = @import("./position.zig"); const Color = @import("./color.zig"); +const Property = @import("./property.zig"); const Object = @This(); @@ -113,6 +114,7 @@ class: []const u8, rotation: f32, // TODO: maybe this field should be moved to Shape struct visible: bool, shape: Shape, +properties: Property.List, pub fn initFromXml( arena: std.mem.Allocator, @@ -255,13 +257,16 @@ pub fn initFromXml( } } + const properties = try Property.List.initFromXmlContainer(arena, doc, element_id); + return Object{ .id = id, .name = name, .class = class, .rotation = rotation, .visible = visible, - .shape = shape orelse return error.UnknownShapeType + .shape = shape orelse return error.UnknownShapeType, + .properties = properties }; } diff --git a/libs/tiled/src/property.zig b/libs/tiled/src/property.zig index 7531509..8d412c3 100644 --- a/libs/tiled/src/property.zig +++ b/libs/tiled/src/property.zig @@ -57,6 +57,24 @@ pub const List = struct { }; } + pub fn initFromXmlContainer( + arena: std.mem.Allocator, + doc: xml.Document, + element_id: xml.Element.Id + ) !Property.List { + var iter = doc.iterChildren(element_id); + while (iter.next()) |child_id| { + const child = doc.getTag(child_id) orelse continue; + const child_name = child.variant.tag.name; + + if (std.mem.eql(u8, child_name, "properties")) { + return try Property.List.initFromXml(arena, doc, child_id); + } + } + + return .empty; + } + pub fn get(self: List, name: []const u8) ?Value { for (self.items) |item| { if (std.mem.eql(u8, item.name, name)) { @@ -68,7 +86,18 @@ pub const List = struct { pub fn getString(self: List, name: []const u8) ?[]const u8 { if (self.get(name)) |value| { - return value.string; + if (value == .string) { + return value.string; + } + } + return null; + } + + pub fn getInt(self: List, name: []const u8) ?i32 { + if (self.get(name)) |value| { + if (value == .int) { + return value.int; + } } return null; } diff --git a/libs/tiled/src/tilemap.zig b/libs/tiled/src/tilemap.zig index 9e0ca88..8788c32 100644 --- a/libs/tiled/src/tilemap.zig +++ b/libs/tiled/src/tilemap.zig @@ -263,6 +263,10 @@ pub const Bounds = struct { pub fn height(self: Bounds) u32 { return @intCast(self.max_y - self.min_y + 1); } + + pub fn isInside(self: Bounds, x: i32, y: i32) bool { + return (self.min_x <= x and x <= self.max_x) and (self.min_y <= y and y <= self.max_y); + } }; pub fn getTileBounds(self: *const Tilemap) Bounds { @@ -274,29 +278,21 @@ pub fn getTileBounds(self: *const Tilemap) Bounds { } for (layer.variant.tile.chunks) |chunk| { - for (0..chunk.height) |oy| { - for (0..chunk.width) |ox| { - const x = chunk.x + @as(i32, @intCast(ox)); - const y = chunk.y + @as(i32, @intCast(oy)); - const gid = layer.variant.tile.get(x, y).?; - if (gid != 0) { - if (result) |bounds| { - result = Bounds{ - .min_x = @min(x, bounds.min_x), - .min_y = @min(y, bounds.min_y), - .max_x = @max(x, bounds.max_x), - .max_y = @max(y, bounds.max_y) - }; - } else { - result = Bounds{ - .min_x = x, - .min_y = y, - .max_x = x, - .max_y = y - }; - } - } - } + const chunk_bounds = Bounds{ + .min_x = chunk.x, + .min_y = chunk.y, + .max_x = chunk.x + @as(i32, @intCast(chunk.width)), + .max_y = chunk.y + @as(i32, @intCast(chunk.height)) + }; + if (result) |bounds| { + result = Bounds{ + .min_x = @min(chunk_bounds.min_x, bounds.min_x), + .min_y = @min(chunk_bounds.min_y, bounds.min_y), + .max_x = @max(chunk_bounds.max_x, bounds.max_x), + .max_y = @max(chunk_bounds.max_y, bounds.max_y) + }; + } else { + result = chunk_bounds; } } } @@ -304,6 +300,59 @@ pub fn getTileBounds(self: *const Tilemap) Bounds { return result orelse Bounds.zero; } +fn isRowEmpty(tile_layer: Layer.TileVariant, x: i32, y: i32, length: u32) bool { + for (0..length) |i| { + const gid = tile_layer.get(x + @as(i32, @intCast(i)), y) orelse continue; + if (gid != 0) { + return false; + } + } + return true; +} + +fn isColumnEmpty(tile_layer: Layer.TileVariant, x: i32, y: i32, length: u32) bool { + for (0..length) |i| { + const gid = tile_layer.get(x, y + @as(i32, @intCast(i))) orelse continue; + if (gid != 0) { + return false; + } + } + return true; +} + +pub fn trimBoundsByEmptyTiles(self: *const Tilemap, bounds: Bounds) Bounds { + var result: Bounds = bounds; + + for (self.layers) |*layer| { + if (layer.variant != .tile) { + continue; + } + const tile_layer = layer.variant.tile; + + // Trim left + while (result.min_x < result.max_x and isColumnEmpty(tile_layer, result.min_x, result.min_y, result.height())) { + result.min_x += 1; + } + + // Trim right + while (result.min_x < result.max_x and isColumnEmpty(tile_layer, result.max_x, result.min_y, result.height())) { + result.max_x -= 1; + } + + // Trim top + while (result.min_y < result.max_y and isRowEmpty(tile_layer, result.min_x, result.min_y, result.width())) { + result.min_y += 1; + } + + // Trim bottom + while (result.min_y < result.max_y and isRowEmpty(tile_layer, result.min_x, result.max_y, result.width())) { + result.max_y -= 1; + } + } + + return result; +} + pub fn deinit(self: *const Tilemap) void { self.arena.deinit(); } diff --git a/libs/tiled/src/tileset.zig b/libs/tiled/src/tileset.zig index 3c2094e..c72c51b 100644 --- a/libs/tiled/src/tileset.zig +++ b/libs/tiled/src/tileset.zig @@ -43,18 +43,7 @@ pub const Tile = struct { const tile_attrs = tag.variant.tag.attributes; const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; - - var properties: Property.List = .empty; - - var iter = doc.iterChildren(element_id); - while (iter.next()) |child_id| { - const child = doc.getTag(child_id) orelse continue; - const child_name = child.variant.tag.name; - - if (std.mem.eql(u8, child_name, "properties")) { - properties = try Property.List.initFromXml(arena, doc, child_id); - } - } + const properties = try Property.List.initFromXmlContainer(arena, doc, element_id); return Tile{ .id = id, diff --git a/src/app.zig b/src/app.zig index 720bf0b..11aa1f2 100644 --- a/src/app.zig +++ b/src/app.zig @@ -71,6 +71,17 @@ const Tilemap = struct { }; } + pub fn clone(self: Tilemap, gpa: Allocator) !Tilemap { + const tiles = try gpa.dupe(Tile, self.tiles); + errdefer gpa.free(tiles); + + return Tilemap{ + .tiles = tiles, + .width = self.width, + .height = self.height, + }; + } + pub fn deinit(self: *Tilemap, gpa: Allocator) void { gpa.free(self.tiles); } @@ -114,6 +125,22 @@ const World = struct { tilemap: Tilemap, entities: Entity.SlotMap, + pub fn clone(self: World, gpa: Allocator) !World { + var tilemap = try self.tilemap.clone(gpa); + errdefer tilemap.deinit(gpa); + + var entity_slots = try self.entities.slots.clone(gpa); + errdefer entity_slots.deinit(gpa); + + var entities = self.entities; + entities.slots = entity_slots; + + return World{ + .tilemap = tilemap, + .entities = entities + }; + } + fn spawnEntity(self: *World, opts: Entity) Entity.Id { var e = opts; e.pos_x = -1; @@ -186,7 +213,13 @@ const World = struct { } }; +const Level = struct { + number: i32, + world: World, +}; + world: World, +levels: std.ArrayList(Level), roboto_font: Font, tilesheet: Tilesheet, @@ -263,99 +296,166 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .tilesheet = tilesheet, .footstep_buffer = footstep_sound, .sfx_bus = Audio.addBus(.{ .label = "sfx" }), - .world = .{ - .tilemap = undefined, - .entities = try .initCapacity(plt.gpa, 256) + .world = undefined, + .levels = .empty, + }; + + self.levels = try self.loadLevels(plt.gpa, plt.assets); + self.world = try self.levels.items[0].world.clone(plt.gpa); + + return null; +} + +pub fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayList(Level) { + var xml_parser = try Tiled.xml.Parser.initCapacity(gpa, .{}); + defer xml_parser.deinit(gpa); + + const tilemap = try Tiled.Tilemap.initFromXml( + gpa, + try xml_parser.parse(assets.readFile("tiled/map.tmx")), + .root + ); + defer tilemap.deinit(); + + var tilesets: Tiled.Tileset.List = .empty; + defer tilesets.deinit(gpa); + + try tilesets.add(gpa, "tileset.tsx", try Tiled.Tileset.initFromXml( + gpa, + try xml_parser.parse(assets.readFile("tiled/tileset.tsx")), + .root + )); + + var levels: std.ArrayList(Level) = .empty; + + const level_bounds = tilemap.getLayer("Level bounds").?; + for (level_bounds.variant.object.items) |obj| { + if (obj.shape != .rectangle) { + continue; + } + + const level_index = obj.properties.getInt("level") orelse { + log.warn("Level bounds doesn't have index", .{}); + continue; + }; + + var rect = obj.shape.rectangle; + rect.x /= @floatFromInt(tilemap.tile_width); + rect.y /= @floatFromInt(tilemap.tile_height); + rect.width /= @floatFromInt(tilemap.tile_width); + rect.height /= @floatFromInt(tilemap.tile_height); + + var level_bound = Tiled.Tilemap.Bounds{ + .min_x = @intFromFloat(@floor(rect.x)), + .min_y = @intFromFloat(@floor(rect.y)), + .max_x = @intFromFloat(@ceil(rect.x + rect.width)), + .max_y = @intFromFloat(@ceil(rect.y + rect.height)), + }; + level_bound = tilemap.trimBoundsByEmptyTiles(level_bound); + + var level = try self.loadLevel(gpa, tilemap, tilesets, level_bound); + level.number = level_index; + try levels.append(gpa, level); + } + + const Context = struct { + fn lessThan(_: @This(), lhs: Level, rhs: Level) bool { + return lhs.number < rhs.number; } }; - { - var xml_parser = try Tiled.xml.Parser.initCapacity(plt.gpa, .{}); - defer xml_parser.deinit(plt.gpa); + std.mem.sort(Level, levels.items, Context{}, Context.lessThan); - const tilemap = try Tiled.Tilemap.initFromXml( - plt.gpa, - try xml_parser.parse(plt.assets.readFile("tiled/map.tmx")), - .root - ); - defer tilemap.deinit(); - var tilesets: Tiled.Tileset.List = .empty; - defer tilesets.deinit(plt.gpa); + return levels; +} - try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromXml( - plt.gpa, - try xml_parser.parse(plt.assets.readFile("tiled/tileset.tsx")), - .root - )); +pub fn loadLevel( + self: *App, + gpa: Allocator, + tilemap: Tiled.Tilemap, + tilesets: Tiled.Tileset.List, + bounds: Tiled.Tilemap.Bounds +) !Level { + var level = Level{ + .number = 0, + .world = .{ + .tilemap = try .init(gpa, bounds.width(), bounds.height()), + .entities = try .initCapacity(gpa, 256) + } + }; - const bounds = tilemap.getTileBounds(); + const ground = tilemap.getLayer("Ground").?; + const foreground = tilemap.getLayer("Foreground").?; + for (0..bounds.height()) |oy| { + for (0..bounds.width()) |ox| { + const x = bounds.min_x + @as(i32, @intCast(ox)); + const y = bounds.min_y + @as(i32, @intCast(oy)); - self.world.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height()); + const tile = level.world.tilemap.get(ox, oy); - const ground = tilemap.getLayer("Ground").?; - const foreground = tilemap.getLayer("Foreground").?; - for (0..bounds.height()) |oy| { - for (0..bounds.width()) |ox| { - const x = bounds.min_x + @as(i32, @intCast(ox)); - const y = bounds.min_y + @as(i32, @intCast(oy)); + if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { + const pos = tiled_tile.getPosition(); + tile.ground_sprite = self.tilesheet.get(pos.x, pos.y); + } - const tile = self.world.tilemap.get(ox, oy); + if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| { + const pos = tiled_tile.getPosition(); + const props = tiled_tile.getProperties(); + const props_type = props.getString("type") orelse ""; - if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { - const pos = tiled_tile.getPosition(); - tile.ground_sprite = tilesheet.get(pos.x, pos.y); + var entity_type: ?Entity.Type = null; + var layer: Entity.Layer = .surface; + if (std.mem.eql(u8, props_type, "wall")) { + entity_type = .wall; + } else if (std.mem.eql(u8, props_type, "box")) { + entity_type = .box; + } else if (std.mem.eql(u8, props_type, "goal_marker")) { + entity_type = .goal_marker; + layer = .ground; + } else { + log.warn("Unknown tile type: '{s}'", .{props_type}); } - if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| { - const pos = tiled_tile.getPosition(); - const props = tiled_tile.getProperties(); - const props_type = props.getString("type") orelse ""; - - var entity_type: ?Entity.Type = null; - var layer: Entity.Layer = .surface; - if (std.mem.eql(u8, props_type, "wall")) { - entity_type = .wall; - } else if (std.mem.eql(u8, props_type, "box")) { - entity_type = .box; - } else if (std.mem.eql(u8, props_type, "goal_marker")) { - entity_type = .goal_marker; - layer = .ground; - } else { - log.warn("Unknown tile type: '{s}'", .{props_type}); - } - - if (entity_type != null) { - _ = self.world.spawnEntity(Entity{ - .type = entity_type.?, - .layer = layer, - .pos_x = @intCast(ox), - .pos_y = @intCast(oy), - .sprite = tilesheet.get(pos.x, pos.y) - }); - } + if (entity_type != null) { + _ = level.world.spawnEntity(Entity{ + .type = entity_type.?, + .layer = layer, + .pos_x = @intCast(ox), + .pos_y = @intCast(oy), + .sprite = self.tilesheet.get(pos.x, pos.y) + }); } } } - - const markers = &tilemap.getLayer("Markers").?.variant.object; - const spawn = markers.getByName("spawn").?; - - var player_pos: Vec2 = .zero; - player_pos.x = @divFloor(spawn.shape.point.x, @as(f32, @floatFromInt(tilemap.tile_width))); - player_pos.x -= @floatFromInt(bounds.min_x); - player_pos.y = @divFloor(spawn.shape.point.y, @as(f32, @floatFromInt(tilemap.tile_height))); - player_pos.y -= @floatFromInt(bounds.min_y); - - _ = self.world.spawnEntity(Entity{ - .type = .player, - .layer = .surface, - .pos_x = @intFromFloat(player_pos.x), - .pos_y = @intFromFloat(player_pos.y) - }); } - return null; + for (tilemap.getLayer("Markers").?.variant.object.items) |obj| { + if (obj.shape != .point) { + continue; + } + if (!std.mem.eql(u8, obj.name, "spawn")) { + continue; + } + + var position = obj.shape.point; + position.x /= @floatFromInt(tilemap.tile_width); + position.y /= @floatFromInt(tilemap.tile_height); + + const pos_x: i32 = @intFromFloat(position.x); + const pos_y: i32 = @intFromFloat(position.y); + + if (bounds.isInside(pos_x, pos_y)) { + _ = level.world.spawnEntity(Entity{ + .type = .player, + .layer = .surface, + .pos_x = pos_x - bounds.min_x, + .pos_y = pos_y - bounds.min_y + }); + } + } + + return level; } pub fn frame(self: *App, plt: Platform.Frame) !void { @@ -478,4 +578,10 @@ pub fn deinit(self: *App, plt: Platform.Deinit) void { self.world.tilemap.deinit(plt.gpa); self.world.entities.slots.deinit(plt.gpa); + + for (self.levels.items) |*level| { + level.world.entities.deinit(plt.gpa); + level.world.tilemap.deinit(plt.gpa); + } + self.levels.deinit(plt.gpa); }