From e5e4e429b6530a61460c7ae2b227b3cda1eb2933 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 14 Dec 2025 20:21:36 +0200 Subject: [PATCH] integrate tiled --- build.zig | 37 +++++++ build.zig.zon | 8 ++ src/assets/tiled/first.tmx | 23 +++++ src/assets/tiled/main.tiled-project | 14 +++ src/assets/tiled/main.tiled-session | 36 +++++++ src/assets/tiled/tileset.tsx | 4 + src/entity.zig | 9 +- src/game.zig | 54 +++++++++- src/graphics.zig | 24 ++--- src/main.zig | 2 - src/tiled.zig | 155 ++++++++++++++++++++++++++++ src/window.zig | 13 ++- 12 files changed, 356 insertions(+), 23 deletions(-) create mode 100644 src/assets/tiled/first.tmx create mode 100644 src/assets/tiled/main.tiled-project create mode 100644 src/assets/tiled/main.tiled-session create mode 100644 src/assets/tiled/tileset.tsx create mode 100644 src/tiled.zig diff --git a/build.zig b/build.zig index e65327c..f7764dc 100644 --- a/build.zig +++ b/build.zig @@ -14,6 +14,7 @@ pub fn build(b: *std.Build) !void { .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, + .link_libc = true }), }); const exe_mod = exe.root_module; @@ -36,6 +37,42 @@ pub fn build(b: *std.Build) !void { const fontstash_dependency = b.dependency("fontstash", .{}); exe_mod.addIncludePath(fontstash_dependency.path("src")); + const libxml2_dependency = b.dependency("libxml2", .{ + .target = target, + .optimize = optimize, + .linkage = .static, + }); + + { + const libtmx_dependency = b.dependency("libtmx", .{}); + const libtmx = b.addLibrary(.{ + .name = "tmx", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true + }), + }); + libtmx.installHeader(libtmx_dependency.path("src/tmx.h"), "tmx.h"); + libtmx.root_module.addCSourceFiles(.{ + .root = libtmx_dependency.path("src"), + .files = &.{ + "tmx.c", + "tmx_utils.c", + "tmx_err.c", + "tmx_xml.c", + "tmx_mem.c", + "tmx_hash.c" + }, + .flags = &.{ + "-fno-delete-null-pointer-checks" + } + }); + libtmx.linkLibrary(libxml2_dependency.artifact("xml")); + + exe_mod.linkLibrary(libtmx); + } + const sokol_dependency = b.dependency("sokol", .{ .target = target, .optimize = optimize, diff --git a/build.zig.zon b/build.zig.zon index 81b3adb..8d9bb06 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -29,6 +29,14 @@ .url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be", .hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6", }, + .libtmx = .{ + .url = "git+https://github.com/baylej/tmx.git#11ffdcdc9bd65669f1a8dbd3a0362a324dda2e0c", + .hash = "N-V-__8AAKQvBQCTT3Q6_we7vTVX-MkAWDZ91YkUev040IRo", + }, + .libxml2 = .{ + .url = "git+https://github.com/allyourcodebase/libxml2.git?ref=2.14.3-4#86c4742a9becd6c86dc79180f806ed344fd2a727", + .hash = "libxml2-2.14.3-4-qHdjhn9FAACpyisv_5DDFVQlegox6QE3mTpdlr44RcbT", + }, }, .paths = .{ "build.zig", diff --git a/src/assets/tiled/first.tmx b/src/assets/tiled/first.tmx new file mode 100644 index 0000000..12ed9a9 --- /dev/null +++ b/src/assets/tiled/first.tmx @@ -0,0 +1,23 @@ + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + diff --git a/src/assets/tiled/main.tiled-project b/src/assets/tiled/main.tiled-project new file mode 100644 index 0000000..d0eb592 --- /dev/null +++ b/src/assets/tiled/main.tiled-project @@ -0,0 +1,14 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "compatibilityVersion": 1100, + "extensionsPath": "extensions", + "folders": [ + "." + ], + "properties": [ + ], + "propertyTypes": [ + ] +} diff --git a/src/assets/tiled/main.tiled-session b/src/assets/tiled/main.tiled-session new file mode 100644 index 0000000..ebbdeba --- /dev/null +++ b/src/assets/tiled/main.tiled-session @@ -0,0 +1,36 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "", + "expandedProjectPaths": [ + ], + "fileStates": { + "first.tmx": { + "scale": 3, + "selectedLayer": 0, + "viewCenter": { + "x": 141.49999999999997, + "y": 68 + } + } + }, + "last.imagePath": "/home/rokas/code/games/game-2025-12-13/src/assets/kenney-micro-roguelike", + "map.height": 15, + "map.lastUsedFormat": "tmx", + "map.tileHeight": 8, + "map.tileWidth": 8, + "map.width": 20, + "openFiles": [ + ], + "project": "main.tiled-project", + "recentFiles": [ + "first.tmx" + ], + "tileset.lastUsedFormat": "tsx", + "tileset.tileSize": { + "height": 8, + "width": 8 + } +} diff --git a/src/assets/tiled/tileset.tsx b/src/assets/tiled/tileset.tsx new file mode 100644 index 0000000..70adfea --- /dev/null +++ b/src/assets/tiled/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/src/entity.zig b/src/entity.zig index b6f307e..b3b3aa5 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -1,5 +1,7 @@ const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; +const Gfx = @import("./graphics.zig"); + const Math = @import("./math.zig"); const Vec2 = Math.Vec2; @@ -9,9 +11,14 @@ pub const List = GenerationalArrayList(Entity); pub const Id = List.Id; pub const Type = enum { + nil, player }; type: Type, -position: Vec2 +position: Vec2, +render_tile: ?union(enum) { + position: Vec2, + id: Gfx.TileId +} = null diff --git a/src/game.zig b/src/game.zig index 4ffa4c9..4390668 100644 --- a/src/game.zig +++ b/src/game.zig @@ -1,10 +1,12 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const assert = std.debug.assert; const Math = @import("./math.zig"); const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const rgb = Math.rgb; +const rgb_hex = Math.rgb_hex; const Timer = @import("./timer.zig"); const Window = @import("./window.zig"); @@ -12,6 +14,8 @@ const imgui = @import("./imgui.zig"); const Gfx = @import("./graphics.zig"); const Entity = @import("./entity.zig"); +const tiled = @import("./tiled.zig"); + const Game = @This(); pub const Input = struct { @@ -46,9 +50,46 @@ pub fn init(gpa: Allocator) !Game { _ = try game.entities.insert(gpa, .{ .type = .player, - .position = .init(0, 0) + .position = .init(0, 0), + .render_tile = .{ .id = .player }, }); + const manager = try tiled.ResourceManager.init(); + defer manager.deinit(); + + try manager.loadTilesetFromBuffer(@embedFile("assets/tiled/tileset.tsx"), "tileset.tsx"); + + const map = try manager.loadMapFromBuffer(@embedFile("assets/tiled/first.tmx")); + defer map.deinit(); + + var layer_iter = map.iterLayers(); + while (layer_iter.next()) |layer| { + if (layer.layer.visible == 0) { + continue; + } + if (layer.layer.type != @intFromEnum(tiled.Layer.Type.layer)) { + continue; + } + + const map_width = map.map.width; + for (0..map.map.height) |y| { + for (0..map_width) |x| { + + const tile = map.getTile(layer, x, y) orelse continue; + + if (tile.getPropertyString("type")) |tile_type| { + _ = tile_type; // autofix + } + + _ = try game.entities.insert(gpa, .{ + .type = .nil, + .position = Vec2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size), + .render_tile = .{ .position = tile.getUpperLeft().divide(tile_size) }, + }); + } + } + } + return game; } @@ -96,8 +137,8 @@ fn drawGrid(self: *Game, size: Vec2, color: Vec4) void { } pub fn tick(self: *Game, input: Input) !void { - Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), rgb(255, 255, 255)); - self.drawGrid(tile_size, rgb(200, 200, 200)); + Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), rgb_hex("#222323").?); + self.drawGrid(tile_size, rgb(20, 20, 20)); self.timers.now += input.dt; @@ -128,8 +169,13 @@ pub fn tick(self: *Game, input: Input) !void { // const velocity = input.move.multiplyScalar(100); // entity.position = entity.position.add(velocity.multiplyScalar(input.dt)); entity.position = entity.position.add(move.multiply(tile_size)); + } - Gfx.drawTileById(.player, entity.position, tile_size, rgb(255, 255, 255)); + if (entity.render_tile) |render_tile| { + switch (render_tile) { + .id => |tile_id| Gfx.drawTileById(tile_id, entity.position, tile_size, rgb(255, 255, 255)), + .position => |position| Gfx.drawTile(position, entity.position, tile_size, rgb(255, 255, 255)), + } } } } diff --git a/src/graphics.zig b/src/graphics.zig index fa89429..629c667 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -774,10 +774,6 @@ fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image { return try makeImageWithMipMaps(stbi_images.items); } -fn tileCoordToQuad(coord: Vec2) Rect { - _ = coord; // autofix -} - pub fn init(options: Options) !void { dpi_scale = sapp.dpiScale(); @@ -940,7 +936,12 @@ pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void { nearest_sampler ); - const tile_quad = Rect.init(tile_coord.x, tile_coord.y, 1, 1).multiply(tile_size).divide(tilemap_size); + var tile_quad = Rect.init( + tile_coord.x, + tile_coord.y, + 1, + 1 + ).multiply(tile_size).divide(tilemap_size); const top_left = pos; const top_right = pos.add(.{ .x = size.x, .y = 0 }); @@ -950,33 +951,30 @@ pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void { sgl.beginQuads(); defer sgl.end(); - v2fT2Color( + sgl.c4f(tint.x, tint.y, tint.z, tint.w); + sgl.v2fT2f( top_left.x, top_left.y, tile_quad.left(), tile_quad.top(), - tint ); - v2fT2Color( + sgl.v2fT2f( top_right.x, top_right.y, tile_quad.right(), tile_quad.top(), - tint ); - v2fT2Color( + sgl.v2fT2f( bottom_right.x, bottom_right.y, tile_quad.right(), tile_quad.bottom(), - tint ); - v2fT2Color( + sgl.v2fT2f( bottom_left.x, bottom_left.y, tile_quad.left(), tile_quad.bottom(), - tint ); } diff --git a/src/main.zig b/src/main.zig index 736a337..926e236 100644 --- a/src/main.zig +++ b/src/main.zig @@ -109,8 +109,6 @@ pub fn main() !void { .width = 640, .height = 480, .icon = .{ .sokol_default = true }, - .high_dpi = true, - .sample_count = 4, .window_title = "Game", .logger = .{ .func = Window.sokolLogCallback }, }); diff --git a/src/tiled.zig b/src/tiled.zig new file mode 100644 index 0000000..616058d --- /dev/null +++ b/src/tiled.zig @@ -0,0 +1,155 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("stdlib.h"); + @cInclude("tmx.h"); +}); + +const math = @import("math.zig"); +const Vec2 = math.Vec2; + +const log = std.log.scoped(.tiled); + +pub const TmxError = error { + MakeResourceManager, + LoadTileset, + LoadMap, +}; + +pub fn init() void { + c.tmx_alloc_func = c.realloc; + c.tmx_free_func = c.free; +} + +pub const ResourceManager = struct { + manager: *anyopaque, + + pub fn init() !ResourceManager { + const manager = c.tmx_make_resource_manager(); + if (manager == null) { + log.err("tmx_make_resource_manager: {s}", .{c.tmx_strerr()}); + return TmxError.MakeResourceManager; + } + + return ResourceManager{ + .manager = manager.?, + }; + } + + pub fn loadTilesetFromBuffer(self: *const ResourceManager, tileset: []const u8, key: [*:0]const u8) !void { + const success = c.tmx_load_tileset_buffer(self.manager, tileset.ptr, @intCast(tileset.len), key); + if (success != 1) { + log.err("tmx_load_tileset_buffer: {s}", .{c.tmx_strerr()}); + return TmxError.LoadTileset; + } + } + + pub fn loadMapFromBuffer(self: *const ResourceManager, map: []const u8) !Map { + const map_handle = c.tmx_rcmgr_load_buffer(self.manager, map.ptr, @intCast(map.len)); + if (map_handle == null) { + log.err("tmx_rcmgr_load_buffer: {s}", .{c.tmx_strerr()}); + return TmxError.LoadMap; + } + + return Map{ + .map = map_handle.? + }; + } + + pub fn deinit(self: *const ResourceManager) void { + c.tmx_free_resource_manager(self.manager); + } +}; + +pub const Tile = struct { + tile: *c.tmx_tile, + + pub fn getPropertyString(self: Tile, key: [*:0]const u8) ?[*:0]const u8 { + const maybe_prop = c.tmx_get_property(self.tile.properties, key); + if (maybe_prop == null) { + return null; + } + const prop: *c.tmx_property = maybe_prop.?; + if (prop.type != c.PT_STRING) { + return null; + } + return prop.value.string; + } + + pub fn getUpperLeft(self: Tile) Vec2 { + return Vec2.init( + @floatFromInt(self.tile.ul_x), + @floatFromInt(self.tile.ul_y), + ); + } +}; + +pub const TileWithFlags = struct { + tile: Tile, + flags: u32 +}; + +pub const Map = struct { + map: *c.tmx_map, + + pub fn deinit(self: *const Map) void { + c.tmx_map_free(self.map); + } + + pub fn iterLayers(self: *const Map) Layer.Iterator { + return Layer.Iterator{ + .current = self.map.ly_head + }; + } + + pub fn getTile(self: *const Map, layer: Layer, x: usize, y: usize) ?Tile { + if (self.getTileWithFlags(layer, x, y)) |tile_with_flags| { + return tile_with_flags.tile; + } + + return null; + } + + pub fn getTileWithFlags(self: *const Map, layer: Layer, x: usize, y: usize) ?TileWithFlags { + if (layer.layer.type != @intFromEnum(Layer.Type.layer)) { + return null; + } + + const gid = layer.layer.content.gids[(y*self.map.width) + x]; + const flags = gid & ~FLIP_BITS_REMOVAL; + const maybe_tile = self.map.tiles[gid & FLIP_BITS_REMOVAL]; + if (maybe_tile == null) { + return null; + } + + return TileWithFlags{ + .tile = Tile{ .tile = maybe_tile.? }, + .flags = flags + }; + } +}; + +pub const Layer = struct { + layer: *c.tmx_layer, + + pub const Type = enum(c_uint) { + none = c.L_NONE, + layer = c.L_LAYER, + object_group = c.L_OBJGR, + image = c.L_IMAGE, + group = c.L_GROUP + }; + + pub const Iterator = struct { + current: ?*c.tmx_layer, + + pub fn next(self: *Iterator) ?Layer { + if (self.current) |current| { + self.current = current.next; + return Layer{ .layer = current }; + } + return null; + } + }; +}; + +pub const FLIP_BITS_REMOVAL: u32 = c.TMX_FLIP_BITS_REMOVAL; diff --git a/src/window.zig b/src/window.zig index 2079678..8fbbb05 100644 --- a/src/window.zig +++ b/src/window.zig @@ -5,6 +5,7 @@ const sokol = @import("sokol"); const sapp = sokol.app; const Gfx = @import("./graphics.zig"); +const Tiled = @import("./tiled.zig"); const Math = @import("./math.zig"); const Vec2 = Math.Vec2; @@ -227,6 +228,8 @@ released_keys: std.EnumSet(KeyCode) = .initEmpty(), pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}), pub fn init(self: *Window, gpa: Allocator) !void { + Tiled.init(); + var events: std.ArrayList(Event) = .empty; errdefer events.deinit(gpa); try events.ensureTotalCapacityPrecise(gpa, 50); @@ -296,13 +299,17 @@ pub fn frame(self: *Window) !void { } self.events.clearRetainingCapacity(); + // TODO: Render to a lower resolution instead of scaling. + // To avoid pixel bleeding in spritesheet artifacts const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); - const scale = @min( + const scale = @floor(@min( window_size.x / self.game.canvas_size.x, window_size.y / self.game.canvas_size.y, - ); + )); - const filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5); + var filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5); + filler_size.x = @round(filler_size.x); + filler_size.y = @round(filler_size.y); const input = self.game.getInput(self);