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 @@
+
+
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);