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"?>
<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"/>
<layer id="2" name="Ground" width="30" height="20">
<data encoding="csv">
@ -85,14 +85,17 @@
<object id="1" name="spawn" x="484" y="555">
<point/>
</object>
<object id="17" name="spawn" x="1381.33" y="497.333">
<point/>
</object>
</objectgroup>
<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>
<property name="level" type="int" value="1"/>
</properties>
</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>
<property name="level" type="int" value="2"/>
</properties>

View File

@ -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
};
}

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 {
for (self.items) |item| {
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 {
if (self.get(name)) |value| {
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;
}
};

View File

@ -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) {
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(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)
.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 = Bounds{
.min_x = x,
.min_y = y,
.max_x = x,
.max_y = y
};
}
}
}
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();
}

View File

@ -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,

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 {
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,35 +296,94 @@ 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,
};
{
var xml_parser = try Tiled.xml.Parser.initCapacity(plt.gpa, .{});
defer xml_parser.deinit(plt.gpa);
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(
plt.gpa,
try xml_parser.parse(plt.assets.readFile("tiled/map.tmx")),
gpa,
try xml_parser.parse(assets.readFile("tiled/map.tmx")),
.root
);
defer tilemap.deinit();
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(
plt.gpa,
try xml_parser.parse(plt.assets.readFile("tiled/tileset.tsx")),
try tilesets.add(gpa, "tileset.tsx", try Tiled.Tileset.initFromXml(
gpa,
try xml_parser.parse(assets.readFile("tiled/tileset.tsx")),
.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 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 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| {
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| {
@ -326,36 +418,44 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
}
if (entity_type != null) {
_ = self.world.spawnEntity(Entity{
_ = level.world.spawnEntity(Entity{
.type = entity_type.?,
.layer = layer,
.pos_x = @intCast(ox),
.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;
const spawn = markers.getByName("spawn").?;
for (tilemap.getLayer("Markers").?.variant.object.items) |obj| {
if (obj.shape != .point) {
continue;
}
if (!std.mem.eql(u8, obj.name, "spawn")) {
continue;
}
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);
var position = obj.shape.point;
position.x /= @floatFromInt(tilemap.tile_width);
position.y /= @floatFromInt(tilemap.tile_height);
_ = 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,
.layer = .surface,
.pos_x = @intFromFloat(player_pos.x),
.pos_y = @intFromFloat(player_pos.y)
.pos_x = pos_x - bounds.min_x,
.pos_y = pos_y - bounds.min_y
});
}
}
return null;
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);
}