add tilemap

This commit is contained in:
Rokas Puzonas 2025-12-14 14:56:30 +02:00
parent 5be299ad4c
commit 61e5edb8cf
5 changed files with 166 additions and 393 deletions

View File

@ -0,0 +1,23 @@
Micro Roguelike (1.4)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 01-11-2021
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -23,7 +23,7 @@ pub const Input = struct {
move_right: Window.KeyState,
};
const tile_size = Vec2.init(10, 10);
const tile_size = Vec2.init(8, 8);
canvas_size: Vec2,
entities: Entity.List,
@ -37,7 +37,7 @@ last_right_repeat_at: ?f64 = null,
pub fn init(gpa: Allocator) !Game {
var game = Game{
.canvas_size = .init(320, 180),
.canvas_size = (Vec2.init(20, 15)).multiply(tile_size),
.entities = .empty,
.timers = .empty,
.last_move = .init(0, 0)
@ -129,7 +129,7 @@ pub fn tick(self: *Game, input: Input) !void {
// entity.position = entity.position.add(velocity.multiplyScalar(input.dt));
entity.position = entity.position.add(move.multiply(tile_size));
Gfx.drawRectangle(entity.position, .init(10, 10), rgb(255, 0, 0));
Gfx.drawTileById(.player, entity.position, tile_size, rgb(255, 255, 255));
}
}
}

View File

@ -451,8 +451,11 @@ pub const Font = struct {
};
pub const ImageId = enum {
// package,
// trash
tilemap
};
pub const TileId = enum {
player
};
pub const Borders = struct {
@ -659,7 +662,13 @@ var font_id_map: std.EnumArray(FontVariant, c_int) = .initFill(c.FONS_INVALID);
var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{});
var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{});
var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{};
var tile_coords: std.EnumArray(TileId, Vec2) = .initUndefined();
const tile_size: Vec2 = .init(8, 8);
var tilemap_size: Vec2 = .init(0, 0);
const Options = struct {
allocator: std.mem.Allocator,
@ -679,38 +688,47 @@ fn loadEmbededFont(fs: ?*c.FONScontext, name: [*c]const u8, comptime path: []co
return font_id;
}
fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image {
var stbi_images_buffer: [16][*c]u8 = undefined;
var stbi_images: std.ArrayListUnmanaged([*c]u8) = .initBuffer(&stbi_images_buffer);
defer {
for (stbi_images.items) |stbi_image| {
c.stbi_image_free(stbi_image);
const ImageData = struct {
rgba8_pixels: [*c]u8,
width: u32,
height: u32,
fn load(png_data: []const u8) !ImageData {
var width: c_int = undefined;
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
return ImageData{
.rgba8_pixels = pixels,
.width = @intCast(width),
.height = @intCast(height)
};
}
fn deinit(self: *const ImageData) void {
c.stbi_image_free(self.rgba8_pixels);
}
};
fn makeImageWithMipMaps(image_datas: []const ImageData) !sg.Image {
var mip_levels_buffer = [_]sg.Range{.{}} ** 16;
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&mip_levels_buffer);
var image_width: c_int = -1;
var image_height: c_int = -1;
for (image_datas) |image_data| {
var width: c_int = undefined;
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(image_data.ptr, @intCast(image_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
for (image_datas) |mipmap_image| {
if (image_height == -1) {
image_width = width;
image_height = height;
image_width = @intCast(mipmap_image.width);
image_height = @intCast(mipmap_image.height);
}
try stbi_images.appendBounded(pixels);
try mip_levels.appendBounded(.{
.ptr = pixels,
.size = @intCast(width * height * 4)
.ptr = mipmap_image.rgba8_pixels,
.size = mipmap_image.width * mipmap_image.height * 4
});
}
@ -735,6 +753,31 @@ fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image {
return image;
}
fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image {
var stbi_images_buffer: [16]ImageData = undefined;
var stbi_images: std.ArrayListUnmanaged(ImageData) = .initBuffer(&stbi_images_buffer);
defer {
for (stbi_images.items) |image| {
image.deinit();
}
}
if (image_datas.len > stbi_images.capacity) {
return error.OutOfMemory;
}
for (image_datas) |image_data| {
const mipmap_image = try ImageData.load(image_data);
stbi_images.appendAssumeCapacity(mipmap_image);
}
return try makeImageWithMipMaps(stbi_images.items);
}
fn tileCoordToQuad(coord: Vec2) Rect {
_ = coord; // autofix
}
pub fn init(options: Options) !void {
dpi_scale = sapp.dpiScale();
@ -792,32 +835,30 @@ pub fn init(options: Options) !void {
.bold = loadEmbededFont(g_fons_context, "bold", "./assets/roboto-font/Roboto-Bold.ttf"),
});
// TODO: Generate mipmap from SVG.
// image_map = .init(.{
// .package = try makeImageFromMemory(&.{
// // https://www.iconfinder.com/icons/9026684/package_thin_icon
// @embedFile("../assets/icons/package-512x512.png"),
// @embedFile("../assets/icons/package-256x256.png"),
// @embedFile("../assets/icons/package-128x128.png"),
// @embedFile("../assets/icons/package-64x64.png"),
// @embedFile("../assets/icons/package-32x32.png"),
// }),
// .trash = try makeImageFromMemory(&.{
// // TODO: Provide attribution for image
// // https://www.iconfinder.com/icons/9027022/trash_thin_icon
// @embedFile("../assets/icons/trash-32x32.png"),
// })
// });
const tilemap = try ImageData.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
defer tilemap.deinit();
// var image_iter = image_map.iterator();
// while (image_iter.next()) |entry| {
// const image_view = sg.makeView(.{
// .texture = .{ .image = entry.value.* }
// });
// assert(image_view.id != sg.invalid_id);
//
// image_view_map.set(entry.key, image_view);
// }
image_map = .init(.{
.tilemap = try makeImageWithMipMaps(&.{
tilemap
})
});
tilemap_size = Vec2.init(@floatFromInt(tilemap.width), @floatFromInt(tilemap.height));
tile_coords = .init(.{
.player = .init(4, 0)
});
var image_iter = image_map.iterator();
while (image_iter.next()) |entry| {
const image_view = sg.makeView(.{
.texture = .{ .image = entry.value.* }
});
assert(image_view.id != sg.invalid_id);
image_view_map.set(entry.key, image_view);
}
linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR,
@ -825,6 +866,13 @@ pub fn init(options: Options) !void {
.mipmap_filter = .LINEAR,
.label = "linear-sampler",
});
nearest_sampler = sg.makeSampler(.{
.min_filter = .NEAREST,
.mag_filter = .NEAREST,
.mipmap_filter = .NEAREST,
.label = "nearest-sampler",
});
}
pub fn deinit() void {
@ -883,16 +931,17 @@ pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void {
);
}
pub fn drawImage(image_id: ImageId, pos: Vec2, size: Vec2, tint: Vec4) void {
pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void {
sgl.enableTexture();
defer sgl.disableTexture();
sgl.texture(
image_view_map.get(image_id),
linear_sampler
image_view_map.get(.tilemap),
nearest_sampler
);
const 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 });
const bottom_right = pos.add(size);
@ -901,352 +950,39 @@ pub fn drawImage(image_id: ImageId, pos: Vec2, size: Vec2, tint: Vec4) void {
sgl.beginQuads();
defer sgl.end();
v2fT2Color(top_left.x, top_left.y, 0, 0, tint);
v2fT2Color(top_right.x, top_right.y, 1, 0, tint);
v2fT2Color(bottom_right.x, bottom_right.y, 1, 1, tint);
v2fT2Color(bottom_left.x, bottom_left.y, 0, 1, tint);
}
fn getCircleSegmentCount(radius: f32, from_angle: f32, to_angle: f32) usize {
const pi2 = 2 * std.math.pi;
const C = pi2 * radius;
const C_segment = C * (@abs(to_angle - from_angle) / pi2);
return @intFromFloat(C_segment / circle_quality);
}
fn drawCorner(pos: Vec2, radius: f32, color: Vec4, from_angle: f32, to_angle: f32) void {
sgl.beginTriangles();
defer sgl.end();
const detail = getCircleSegmentCount(radius, from_angle, to_angle);
// TODO: Use precomputed angles
for (0..detail) |i| {
const angle = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(detail)));
const angle1 = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(detail)));
const x = @cos(angle);
const y = @sin(angle);
const x1 = @cos(angle1);
const y1 = @sin(angle1);
v2fColor(pos.x , pos.y , color);
v2fColor(pos.x + x * radius, pos.y + y * radius, color);
v2fColor(pos.x + x1 * radius, pos.y + y1 * radius, color);
}
}
fn drawOutlineCornerSegment(
pos: Vec2,
color: Vec4,
outer_radius: f32, inner_radius: f32,
from_angle: f32, to_angle: f32
) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
var detail = getCircleSegmentCount(outer_radius, from_angle, to_angle);
detail = @max(detail, 1);
// TODO: Use precomputed angles
for (0..detail) |i| {
const angle = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(detail)));
const angle1 = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(detail)));
const circle_point0 = Vec2.init(@cos(angle), @sin(angle));
const circle_point1 = Vec2.init(@cos(angle1), @sin(angle1));
drawQuad(
.{
pos.add(circle_point1.multiplyScalar(outer_radius)),
pos.add(circle_point0.multiplyScalar(outer_radius)),
pos.add(circle_point0.multiplyScalar(@max(0, inner_radius))),
pos.add(circle_point1.multiplyScalar(@max(0, inner_radius))),
},
color
);
}
}
pub fn drawRoundedRectangle(pos: Vec2, size: Vec2, color: Vec4, corners: Corners) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const left = pos.x;
const right = pos.x + size.x;
const top = pos.y;
const bottom = pos.y + size.y;
const tr = corners.top_right;
const tl = corners.top_left;
const bl = corners.bottom_left;
const br = corners.bottom_right;
{
sgl.beginQuads();
defer sgl.end();
vertexesQuad(
Vec2.init(left + tl, top),
Vec2.init(right - tr, top),
Vec2.init(right - tr, top + tr),
Vec2.init(left + tl, top + tl),
color
);
vertexesQuad(
Vec2.init(right - tr, top + tr),
Vec2.init(right, top + tr),
Vec2.init(right, bottom - br),
Vec2.init(right - br, bottom - br),
color
);
vertexesQuad(
Vec2.init(left + bl, bottom - bl),
Vec2.init(right - br, bottom - br),
Vec2.init(right - br, bottom),
Vec2.init(left + bl, bottom),
color
);
vertexesQuad(
Vec2.init(left, top + tl),
Vec2.init(left + tl, top + tl),
Vec2.init(left + bl, bottom - bl),
Vec2.init(left, bottom - bl),
color
);
vertexesQuad(
Vec2.init(left + tl, top + tl),
Vec2.init(right - tr, top + tr),
Vec2.init(right - br, bottom - br),
Vec2.init(left + bl, bottom - bl),
color
);
}
drawCorner(
Vec2.init(right - tr, top + tr),
tr,
color,
std.math.pi * (3.0 / 2.0),
std.math.pi * 2.0,
v2fT2Color(
top_left.x,
top_left.y,
tile_quad.left(),
tile_quad.top(),
tint
);
drawCorner(
Vec2.init(left + tl, top + tl),
tl,
color,
std.math.pi,
std.math.pi * (3.0 / 2.0)
v2fT2Color(
top_right.x,
top_right.y,
tile_quad.right(),
tile_quad.top(),
tint
);
drawCorner(
Vec2.init(left + bl, bottom - bl),
bl,
color,
std.math.pi,
std.math.pi * (1.0 / 2.0),
v2fT2Color(
bottom_right.x,
bottom_right.y,
tile_quad.right(),
tile_quad.bottom(),
tint
);
drawCorner(
Vec2.init(right - br, bottom - br),
br,
color,
0,
std.math.pi * (1.0 / 2.0)
v2fT2Color(
bottom_left.x,
bottom_left.y,
tile_quad.left(),
tile_quad.bottom(),
tint
);
}
const OutlineCorner = struct {
point: Vec2,
corner_radius: f32,
dir_to_center: Vec2,
pub fn getCircleCenter(self: OutlineCorner) Vec2 {
return self.point.add(self.dir_to_center.multiplyScalar(self.corner_radius));
}
pub fn getEdge(self: OutlineCorner, segment: OutlineSegment, border: f32) [2]Vec2 {
const inward_sign = switch (segment.axis) {
.X => self.dir_to_center.x,
.Y => self.dir_to_center.y,
};
var bottom_left = self.point;
if (segment.axis == .X) {
bottom_left.x += self.corner_radius * inward_sign;
} else {
bottom_left.y += self.corner_radius * inward_sign;
}
var top_left = bottom_left.add(segment.inward_direction.multiplyScalar(border));
if (segment.axis == .X) {
top_left.x += @max(border - self.corner_radius, 0) * inward_sign;
} else {
top_left.y += @max(border - self.corner_radius, 0) * inward_sign;
}
return .{ bottom_left, top_left };
}
fn draw(self: OutlineCorner, segment: OutlineSegment, border_size: f32, corner_angle_sign: f32, edge: [2]Vec2,) void {
if (self.corner_radius > 0) {
const center = self.getCircleCenter();
const inner_radius = self.corner_radius - border_size;
if (inner_radius < 0) {
drawTriangle(
.{
edge[0],
edge[1],
center
},
segment.color
);
}
drawOutlineCornerSegment(
center,
segment.color,
self.corner_radius,
inner_radius,
segment.outward_angle,
segment.outward_angle + corner_angle_sign * std.math.pi / 4.0,
);
}
}
};
const OutlineSegment = struct {
// TODO: Figure out a way to remove `lower_side_angle_dir` and `upper_side_angle_dir`?
lower_side: OutlineCorner,
lower_side_angle_dir: f32,
upper_side: OutlineCorner,
upper_side_angle_dir: f32,
outward_angle: f32,
color: Vec4,
axis: enum { X, Y },
inward_direction: Vec2,
pub fn getLowerEdge(self: OutlineSegment, border: f32) [2]Vec2 {
return self.lower_side.getEdge(self, border);
}
pub fn getUpperEdge(self: OutlineSegment, border: f32) [2]Vec2 {
return self.upper_side.getEdge(self, border);
}
fn draw(self: OutlineSegment, border: f32) void {
if (self.color.w != 0) {
const lower_edge = self.getLowerEdge(border);
const upper_edge = self.getUpperEdge(border);
drawQuad(
.{
lower_edge[0],
lower_edge[1],
upper_edge[1],
upper_edge[0],
},
self.color
);
self.lower_side.draw(self, border, self.lower_side_angle_dir, lower_edge);
self.upper_side.draw(self, border, self.upper_side_angle_dir, upper_edge);
}
}
};
pub fn drawRectangleOutlineRounded(pos: Vec2, size: Vec2, corners: Corners, borders: Borders) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
if (borders.size == 0) {
return;
}
const left = pos.x;
const right = pos.x+size.x;
const top = pos.y;
const bottom = pos.y+size.y;
const bottom_left_corner = OutlineCorner{
.point = Vec2.init(left, bottom),
.corner_radius = corners.bottom_left,
.dir_to_center = Vec2.init(1, -1),
};
const bottom_right_corner = OutlineCorner{
.point = Vec2.init(right, bottom),
.corner_radius = corners.bottom_right,
.dir_to_center = Vec2.init(-1, -1),
};
const top_right_corner = OutlineCorner{
.point = Vec2.init(right, top),
.corner_radius = corners.top_right,
.dir_to_center = Vec2.init(-1, 1),
};
const top_left_corner = OutlineCorner{
.point = Vec2.init(left, top),
.corner_radius = corners.top_left,
.dir_to_center = Vec2.init(1, 1),
};
const segments = .{
OutlineSegment{
.lower_side = bottom_left_corner,
.lower_side_angle_dir = 1,
.upper_side = bottom_right_corner,
.upper_side_angle_dir = -1,
.outward_angle = std.math.pi * 1.0 / 2.0,
.color = borders.bottom,
.axis = .X,
.inward_direction = Vec2.init(0, -1)
},
OutlineSegment{
.lower_side = top_left_corner,
.lower_side_angle_dir = -1,
.upper_side = top_right_corner,
.upper_side_angle_dir = 1,
.outward_angle = std.math.pi * 3.0 / 2.0,
.color = borders.top,
.axis = .X,
.inward_direction = Vec2.init(0, 1)
},
OutlineSegment{
.lower_side = top_left_corner,
.lower_side_angle_dir = 1,
.upper_side = bottom_left_corner,
.upper_side_angle_dir = -1,
.outward_angle = std.math.pi,
.color = borders.left,
.axis = .Y,
.inward_direction = Vec2.init(1, 0)
},
OutlineSegment{
.lower_side = top_right_corner,
.lower_side_angle_dir = -1,
.upper_side = bottom_right_corner,
.upper_side_angle_dir = 1,
.outward_angle = 0,
.color = borders.right,
.axis = .Y,
.inward_direction = Vec2.init(-1, 0)
}
};
inline for (segments) |segment| {
segment.draw(borders.size);
}
pub fn drawTileById(tile_id: TileId, pos: Vec2, size: Vec2, tint: Vec4) void {
const tile_coord = tile_coords.get(tile_id);
drawTile(tile_coord, pos, size, tint);
}
pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void {

View File

@ -351,6 +351,20 @@ pub const Rect = struct {
return self.pos.y + self.size.y;
}
pub fn multiply(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.multiply(xy),
.size = self.size.multiply(xy),
};
}
pub fn divide(self: Rect, xy: Vec2) Rect {
return Rect{
.pos = self.pos.divide(xy),
.size = self.size.divide(xy),
};
}
pub fn isInside(self: Rect, pos: Vec2) bool {
const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x;
const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;