implement parsing multiple levels from map file

This commit is contained in:
Rokas Puzonas 2026-09-21 22:36:46 +03:00
parent b6d9f1664b
commit a6bf95c570
6 changed files with 296 additions and 115 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="1" nextlayerid="8" nextobjectid="17"> <map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="1" nextlayerid="8" nextobjectid="19">
<tileset firstgid="1" source="tileset.tsx"/> <tileset firstgid="1" source="tileset.tsx"/>
<layer id="2" name="Ground" width="30" height="20"> <layer id="2" name="Ground" width="30" height="20">
<data encoding="csv"> <data encoding="csv">
@ -85,14 +85,17 @@
<object id="1" name="spawn" x="484" y="555"> <object id="1" name="spawn" x="484" y="555">
<point/> <point/>
</object> </object>
<object id="17" name="spawn" x="1381.33" y="497.333">
<point/>
</object>
</objectgroup> </objectgroup>
<objectgroup id="7" name="Level bounds"> <objectgroup id="7" name="Level bounds">
<object id="15" x="345.333" y="218.667" width="633.333" height="566.667"> <object id="15" x="324" y="214.667" width="633.333" height="566.667">
<properties> <properties>
<property name="level" type="int" value="1"/> <property name="level" type="int" value="1"/>
</properties> </properties>
</object> </object>
<object id="16" x="1228" y="172" width="565.333" height="593.333"> <object id="18" x="1196.67" y="206" width="633.333" height="566.667">
<properties> <properties>
<property name="level" type="int" value="2"/> <property name="level" type="int" value="2"/>
</properties> </properties>

View File

@ -3,6 +3,7 @@ const std = @import("std");
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
const Position = @import("./position.zig"); const Position = @import("./position.zig");
const Color = @import("./color.zig"); const Color = @import("./color.zig");
const Property = @import("./property.zig");
const Object = @This(); const Object = @This();
@ -113,6 +114,7 @@ class: []const u8,
rotation: f32, // TODO: maybe this field should be moved to Shape struct rotation: f32, // TODO: maybe this field should be moved to Shape struct
visible: bool, visible: bool,
shape: Shape, shape: Shape,
properties: Property.List,
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
@ -255,13 +257,16 @@ pub fn initFromXml(
} }
} }
const properties = try Property.List.initFromXmlContainer(arena, doc, element_id);
return Object{ return Object{
.id = id, .id = id,
.name = name, .name = name,
.class = class, .class = class,
.rotation = rotation, .rotation = rotation,
.visible = visible, .visible = visible,
.shape = shape orelse return error.UnknownShapeType .shape = shape orelse return error.UnknownShapeType,
.properties = properties
}; };
} }

View File

@ -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 { pub fn get(self: List, name: []const u8) ?Value {
for (self.items) |item| { for (self.items) |item| {
if (std.mem.eql(u8, item.name, name)) { if (std.mem.eql(u8, item.name, name)) {
@ -68,8 +86,19 @@ pub const List = struct {
pub fn getString(self: List, name: []const u8) ?[]const u8 { pub fn getString(self: List, name: []const u8) ?[]const u8 {
if (self.get(name)) |value| { if (self.get(name)) |value| {
if (value == .string) {
return 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; return null;
} }
}; };

View File

@ -263,6 +263,10 @@ pub const Bounds = struct {
pub fn height(self: Bounds) u32 { pub fn height(self: Bounds) u32 {
return @intCast(self.max_y - self.min_y + 1); 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 { 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 (layer.variant.tile.chunks) |chunk| {
for (0..chunk.height) |oy| { const chunk_bounds = Bounds{
for (0..chunk.width) |ox| { .min_x = chunk.x,
const x = chunk.x + @as(i32, @intCast(ox)); .min_y = chunk.y,
const y = chunk.y + @as(i32, @intCast(oy)); .max_x = chunk.x + @as(i32, @intCast(chunk.width)),
const gid = layer.variant.tile.get(x, y).?; .max_y = chunk.y + @as(i32, @intCast(chunk.height))
if (gid != 0) { };
if (result) |bounds| { if (result) |bounds| {
result = Bounds{ result = Bounds{
.min_x = @min(x, bounds.min_x), .min_x = @min(chunk_bounds.min_x, bounds.min_x),
.min_y = @min(y, bounds.min_y), .min_y = @min(chunk_bounds.min_y, bounds.min_y),
.max_x = @max(x, bounds.max_x), .max_x = @max(chunk_bounds.max_x, bounds.max_x),
.max_y = @max(y, bounds.max_y) .max_y = @max(chunk_bounds.max_y, bounds.max_y)
}; };
} else { } else {
result = Bounds{ result = chunk_bounds;
.min_x = x,
.min_y = y,
.max_x = x,
.max_y = y
};
}
}
}
} }
} }
} }
@ -304,6 +300,59 @@ pub fn getTileBounds(self: *const Tilemap) Bounds {
return result orelse Bounds.zero; 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 { pub fn deinit(self: *const Tilemap) void {
self.arena.deinit(); self.arena.deinit();
} }

View File

@ -43,18 +43,7 @@ pub const Tile = struct {
const tile_attrs = tag.variant.tag.attributes; const tile_attrs = tag.variant.tag.attributes;
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
const properties = try Property.List.initFromXmlContainer(arena, doc, element_id);
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);
}
}
return Tile{ return Tile{
.id = id, .id = id,

View File

@ -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 { pub fn deinit(self: *Tilemap, gpa: Allocator) void {
gpa.free(self.tiles); gpa.free(self.tiles);
} }
@ -114,6 +125,22 @@ const World = struct {
tilemap: Tilemap, tilemap: Tilemap,
entities: Entity.SlotMap, 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 { fn spawnEntity(self: *World, opts: Entity) Entity.Id {
var e = opts; var e = opts;
e.pos_x = -1; e.pos_x = -1;
@ -186,7 +213,13 @@ const World = struct {
} }
}; };
const Level = struct {
number: i32,
world: World, world: World,
};
world: World,
levels: std.ArrayList(Level),
roboto_font: Font, roboto_font: Font,
tilesheet: Tilesheet, tilesheet: Tilesheet,
@ -263,35 +296,94 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.tilesheet = tilesheet, .tilesheet = tilesheet,
.footstep_buffer = footstep_sound, .footstep_buffer = footstep_sound,
.sfx_bus = Audio.addBus(.{ .label = "sfx" }), .sfx_bus = Audio.addBus(.{ .label = "sfx" }),
.world = .{ .world = undefined,
.tilemap = undefined, .levels = .empty,
.entities = try .initCapacity(plt.gpa, 256)
}
}; };
{ self.levels = try self.loadLevels(plt.gpa, plt.assets);
var xml_parser = try Tiled.xml.Parser.initCapacity(plt.gpa, .{}); self.world = try self.levels.items[0].world.clone(plt.gpa);
defer xml_parser.deinit(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( const tilemap = try Tiled.Tilemap.initFromXml(
plt.gpa, gpa,
try xml_parser.parse(plt.assets.readFile("tiled/map.tmx")), try xml_parser.parse(assets.readFile("tiled/map.tmx")),
.root .root
); );
defer tilemap.deinit(); defer tilemap.deinit();
var tilesets: Tiled.Tileset.List = .empty; var tilesets: Tiled.Tileset.List = .empty;
defer tilesets.deinit(plt.gpa); defer tilesets.deinit(gpa);
try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromXml( try tilesets.add(gpa, "tileset.tsx", try Tiled.Tileset.initFromXml(
plt.gpa, gpa,
try xml_parser.parse(plt.assets.readFile("tiled/tileset.tsx")), try xml_parser.parse(assets.readFile("tiled/tileset.tsx")),
.root .root
)); ));
const bounds = tilemap.getTileBounds(); var levels: std.ArrayList(Level) = .empty;
self.world.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height()); 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;
}
};
std.mem.sort(Level, levels.items, Context{}, Context.lessThan);
return levels;
}
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 ground = tilemap.getLayer("Ground").?; const ground = tilemap.getLayer("Ground").?;
const foreground = tilemap.getLayer("Foreground").?; const foreground = tilemap.getLayer("Foreground").?;
@ -300,11 +392,11 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
const x = bounds.min_x + @as(i32, @intCast(ox)); const x = bounds.min_x + @as(i32, @intCast(ox));
const y = bounds.min_y + @as(i32, @intCast(oy)); const y = bounds.min_y + @as(i32, @intCast(oy));
const tile = self.world.tilemap.get(ox, oy); const tile = level.world.tilemap.get(ox, oy);
if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| {
const pos = tiled_tile.getPosition(); const pos = tiled_tile.getPosition();
tile.ground_sprite = tilesheet.get(pos.x, pos.y); tile.ground_sprite = self.tilesheet.get(pos.x, pos.y);
} }
if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| { if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| {
@ -326,36 +418,44 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
} }
if (entity_type != null) { if (entity_type != null) {
_ = self.world.spawnEntity(Entity{ _ = level.world.spawnEntity(Entity{
.type = entity_type.?, .type = entity_type.?,
.layer = layer, .layer = layer,
.pos_x = @intCast(ox), .pos_x = @intCast(ox),
.pos_y = @intCast(oy), .pos_y = @intCast(oy),
.sprite = tilesheet.get(pos.x, pos.y) .sprite = self.tilesheet.get(pos.x, pos.y)
}); });
} }
} }
} }
} }
const markers = &tilemap.getLayer("Markers").?.variant.object; for (tilemap.getLayer("Markers").?.variant.object.items) |obj| {
const spawn = markers.getByName("spawn").?; if (obj.shape != .point) {
continue;
}
if (!std.mem.eql(u8, obj.name, "spawn")) {
continue;
}
var player_pos: Vec2 = .zero; var position = obj.shape.point;
player_pos.x = @divFloor(spawn.shape.point.x, @as(f32, @floatFromInt(tilemap.tile_width))); position.x /= @floatFromInt(tilemap.tile_width);
player_pos.x -= @floatFromInt(bounds.min_x); position.y /= @floatFromInt(tilemap.tile_height);
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{ 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, .type = .player,
.layer = .surface, .layer = .surface,
.pos_x = @intFromFloat(player_pos.x), .pos_x = pos_x - bounds.min_x,
.pos_y = @intFromFloat(player_pos.y) .pos_y = pos_y - bounds.min_y
}); });
} }
}
return null; return level;
} }
pub fn frame(self: *App, plt: Platform.Frame) !void { 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.tilemap.deinit(plt.gpa);
self.world.entities.slots.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);
} }