integrate tiled

This commit is contained in:
Rokas Puzonas 2025-12-14 20:21:36 +02:00
parent 61e5edb8cf
commit e5e4e429b6
12 changed files with 356 additions and 23 deletions

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="20" height="15" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="20" height="15">
<data encoding="csv">
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
</data>
</layer>
</map>

View File

@ -0,0 +1,14 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"properties": [
],
"propertyTypes": [
]
}

View File

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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="tileset" tilewidth="8" tileheight="8" tilecount="160" columns="16">
<image source="../kenney-micro-roguelike/colored_tilemap_packed.png" width="128" height="80"/>
</tileset>

View File

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

View File

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

View File

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

View File

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

155
src/tiled.zig Normal file
View File

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

View File

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