加入 SpriteRegion 与基础 Tilemap 绘制

DrawContext 现在支持从 atlas 子区域绘制 SpriteRegion,并在 draw_sprite_ex 中加入屏幕裁剪,避免边缘 sprite 逐像素依赖 FrameBuffer 越界保护。新增基础 Tilemap 数据结构和 draw_tilemap,按视口可见范围遍历 tile,并在视口边缘进行像素级裁剪,支持 camera 像素级滚动时显示半个 tile。

Demo 增加小型滚动 tilemap 测试,复用现有测试 sprite 作为单 tile atlas。文档同步记录 SpriteRegion、Tilemap、viewport 裁剪语义和后续优化方向。

Constraint: IMX6U 热路径应避免全地图扫描和运行时资源解码
Constraint: 当前 tilemap 先保持单 atlas、固定 tile 尺寸的简单模型
Rejected: 直接每帧遍历整张 tilemap | 大地图上会浪费 CPU
Rejected: 立即引入复杂地图资源系统 | 当前阶段只需要验证绘制链路
Confidence: high
Scope-risk: moderate
Directive: 扩展 Tilemap 时保持 tile 范围遍历 + 像素级边缘裁剪的语义
Tested: cmake --build build-win --config Release
Not-tested: 尚未在 IMX6U 真机上验证 framebuffer/SDL 后端性能
This commit is contained in:
SepComet 2026-06-07 09:23:26 +08:00
parent 213fa7e961
commit 20d2422650
8 changed files with 286 additions and 32 deletions

View File

@ -35,7 +35,7 @@ IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_G
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、输入/时间源、基础绘制能力例如点、线、矩形、四边形、sprite、tile。
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、输入/时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
@ -244,7 +244,9 @@ IMX6U-Game/
## 模块说明
### Draw2D
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer对外提供 `clear`、`draw_line`、`draw_triangle`、`draw_sprite`、`draw_text`、`present` 接口
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer对外提供 `clear`、`draw_line`、`draw_triangle`、`draw_sprite`、`draw_sprite_region`、`draw_text`、`draw_tilemap`、`present` 接口
- **SpriteRegion**:描述 atlas 中的子区域,`draw_sprite_region` / `draw_sprite_region_ex` 可直接绘制子图,底层复用 `draw_sprite_ex`
- **Tilemap**:使用 `uint16_t` tile id 引用 atlas 中的固定大小 tile`draw_tilemap` 按视口可见范围遍历 tile并在视口边缘做像素级裁剪
### Core
- **FrameBuffer**CPU 侧颜色缓冲,渲染结果先写在这里
@ -269,7 +271,7 @@ IMX6U-Game/
- 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试)
- 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite 与 bitmap font 文本绘制,当前 demo 显示 FPS 文本和测试 sprite
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
- C++11 兼容代码
@ -279,7 +281,7 @@ IMX6U-Game/
1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON
2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
4. Gfx 基础 2D 绘制接口(矩形、四边形、Tilemap继续完善 Sprite/Text 的裁剪和批处理)
4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径
5. 纹理贴图、OBJ 模型加载与完整光照
## 说明

View File

@ -52,7 +52,7 @@ IMX6U-Game/
职责:
- 管理 framebuffer、depthbuffer、渲染上下文。
- 提供基础绘制接口点、线、矩形、四边形、三角形、sprite、tile、简单文本等。
- 提供基础绘制接口点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
- 封装 SDL2 / framebuffer 显示提交、输入轮询、时间源。
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas运行时不直接依赖 PNG/TTF 解码。
@ -186,16 +186,20 @@ namespace Gfx
void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image);
void draw_sprite_region(int32_t x, int32_t y,
const RenderData::SpriteRegion& region);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
RenderData::Color color, const char* text);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
};
}
```
后续再扩展:
- `draw_sprite_region`
- `draw_tilemap`
- `set_clip_rect`
- `set_camera_2d`
- `batch sprite/tile`
@ -218,7 +222,7 @@ namespace Gfx
1. ~~先抽出统一 `IApp``AppManager`,让当前 demo 成为一个 app。~~
2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_text、present
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present
4. ~~底层代码迁移到 `src/Gfx/`Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
5. 新增 Launcher app只做最小菜单和应用切换。
6. 新增 GameA/GameB 空壳,验证三应用切换。
@ -241,3 +245,15 @@ namespace Gfx
- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas并输出同名 PNG 预览和 C++ 头文件。
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。
## 10. SpriteRegion 与 Tilemap 约定
- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。
- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口负责源区域检查、目标屏幕裁剪、scale 和 flip`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。
- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。
- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。
- `DrawContext::draw_tilemap` 的裁剪分两层:
- tilemap 层按 tile 计算需要尝试绘制的可见范围;
- sprite 层按像素裁剪每个 tile支持 camera 像素级滚动时显示半个 tile。
- 带 `viewport_w` / `viewport_h``draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。
- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。

View File

@ -121,7 +121,8 @@
- `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header。
- 生成像素格式统一为 `(R << 24) | (G << 16) | (B << 8) | A`,与 `RenderData::Color::to_rgba()``Core::FrameBuffer` 当前格式保持一致。
- 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。
- 运行时绘制 sprite/font 时只做裁剪、透明判断、颜色替换或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
- 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
- Tilemap 绘制应按视口可见范围遍历 tile不能每帧无条件扫描整张地图视口边缘允许通过 sprite 像素裁剪显示半个 tile。
- 字体资源当前走像素风路径,生成端会把 alpha 阈值化为 0/255如果未来要恢复抗锯齿字体必须同步设计 framebuffer alpha blending而不能只在绘制端把所有非 0 alpha 当作实心像素。
## 9. 新代码提交前检查清单

View File

@ -126,6 +126,16 @@ int main(int argc, char *argv[])
font.columns = font_columns;
font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = {
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
@ -253,11 +263,10 @@ int main(int argc, char *argv[])
}
// sprite 测试
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
ctx.draw_sprite(10, 10, sprite_img);
ctx.draw_sprite_ex(30, 10, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 2, false, false);
ctx.draw_sprite_ex(10, 30, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 3, true, false);
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(display->get_time_ms() / 20) % 32, 0);
// FPS 计数
++frame_count;

View File

@ -60,52 +60,197 @@ namespace Gfx
draw_sprite_region(dst_x, dst_y, img, 0, 0, img.width, img.height);
}
void DrawContext::draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region(dst_x, dst_y, region);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h)
{
draw_sprite_ex(dst_x, dst_y, img, src_x, src_y, src_w, src_h, 1, false, false);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region_ex(dst_x, dst_y, region, 1, false, false);
}
void DrawContext::draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v)
{
if (!region.atlas) return;
draw_sprite_ex(
dst_x,
dst_y,
*region.atlas,
region.x,
region.y,
region.width,
region.height,
scale,
flip_h,
flip_v);
}
void DrawContext::draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v)
{
if (scale < 1 || !img.pixels) return;
if (scale < 1 || !img.pixels || src_w <= 0 || src_h <= 0) return;
if (src_x < 0 || src_y < 0 || src_x + src_w > img.width || src_y + src_h > img.height) return;
const int32_t img_w = img.width;
const int32_t screen_w = frameBuffer->get_width();
const int32_t screen_h = frameBuffer->get_height();
const int32_t draw_w = src_w * scale;
const int32_t draw_h = src_h * scale;
if (dst_x >= screen_w || dst_y >= screen_h || dst_x + draw_w <= 0 || dst_y + draw_h <= 0) return;
int32_t start_dx = 0;
int32_t start_dy = 0;
int32_t end_dx = draw_w;
int32_t end_dy = draw_h;
if (dst_x < 0) start_dx = -dst_x;
if (dst_y < 0) start_dy = -dst_y;
if (dst_x + end_dx > screen_w) end_dx = screen_w - dst_x;
if (dst_y + end_dy > screen_h) end_dy = screen_h - dst_y;
const uint32_t* src = img.pixels;
const bool has_key = img.has_color_key;
const uint32_t key = img.color_key;
for (int32_t sy = 0; sy < src_h; ++sy)
if (scale == 1)
{
for (int32_t sy = start_dy; sy < end_dy; ++sy)
{
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + sy;
for (int32_t sx = start_dx; sx < end_dx; ++sx)
{
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
frameBuffer->set_pixel(dst_x + sx, dst_y_abs, pixel);
}
}
return;
}
for (int32_t dy_abs = start_dy; dy_abs < end_dy; ++dy_abs)
{
const int32_t sy = dy_abs / scale;
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + dy_abs;
for (int32_t sx = 0; sx < src_w; ++sx)
for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs)
{
const int32_t sx = dx_abs / scale;
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
const int32_t base_x = dst_x + sx * scale;
const int32_t base_y = dst_y + sy * scale;
frameBuffer->set_pixel(dst_x + dx_abs, dst_y_abs, pixel);
}
}
}
if (scale == 1)
{
frameBuffer->set_pixel(base_x, base_y, pixel);
}
else
{
for (int32_t dy = 0; dy < scale; ++dy)
{
for (int32_t dx = 0; dx < scale; ++dx)
{
frameBuffer->set_pixel(base_x + dx, base_y + dy, pixel);
}
}
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y)
{
draw_tilemap(tilemap,
screen_x,
screen_y,
frameBuffer->get_width() - screen_x,
frameBuffer->get_height() - screen_y,
camera_x,
camera_y);
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y)
{
if (!tilemap.tiles || !tilemap.atlas || !tilemap.atlas->pixels) return;
if (tilemap.width <= 0 || tilemap.height <= 0) return;
if (tilemap.tile_w <= 0 || tilemap.tile_h <= 0 || tilemap.atlas_columns <= 0) return;
if (viewport_w <= 0 || viewport_h <= 0) return;
const int32_t viewport_left = screen_x;
const int32_t viewport_top = screen_y;
const int32_t viewport_right = screen_x + viewport_w;
const int32_t viewport_bottom = screen_y + viewport_h;
int32_t start_tile_x = camera_x / tilemap.tile_w;
int32_t start_tile_y = camera_y / tilemap.tile_h;
int32_t offset_x = -(camera_x % tilemap.tile_w);
int32_t offset_y = -(camera_y % tilemap.tile_h);
if (camera_x < 0 && camera_x % tilemap.tile_w != 0)
{
--start_tile_x;
offset_x = -camera_x - (-start_tile_x * tilemap.tile_w);
}
if (camera_y < 0 && camera_y % tilemap.tile_h != 0)
{
--start_tile_y;
offset_y = -camera_y - (-start_tile_y * tilemap.tile_h);
}
const int32_t visible_cols = viewport_w / tilemap.tile_w + 2;
const int32_t visible_rows = viewport_h / tilemap.tile_h + 2;
for (int32_t row = 0; row < visible_rows; ++row)
{
const int32_t map_y = start_tile_y + row;
if (map_y < 0 || map_y >= tilemap.height) continue;
const int32_t dst_y = screen_y + offset_y + row * tilemap.tile_h;
for (int32_t col = 0; col < visible_cols; ++col)
{
const int32_t map_x = start_tile_x + col;
if (map_x < 0 || map_x >= tilemap.width) continue;
const uint16_t tile_id = tilemap.get_tile(map_x, map_y);
if (tile_id == RenderData::Tilemap::EmptyTile) continue;
const int32_t src_x = (tile_id % tilemap.atlas_columns) * tilemap.tile_w;
const int32_t src_y = (tile_id / tilemap.atlas_columns) * tilemap.tile_h;
const int32_t dst_x = screen_x + offset_x + col * tilemap.tile_w;
const int32_t tile_right = dst_x + tilemap.tile_w;
const int32_t tile_bottom = dst_y + tilemap.tile_h;
int32_t clipped_left = dst_x;
int32_t clipped_top = dst_y;
int32_t clipped_right = tile_right;
int32_t clipped_bottom = tile_bottom;
if (clipped_left < viewport_left) clipped_left = viewport_left;
if (clipped_top < viewport_top) clipped_top = viewport_top;
if (clipped_right > viewport_right) clipped_right = viewport_right;
if (clipped_bottom > viewport_bottom) clipped_bottom = viewport_bottom;
if (clipped_left >= clipped_right || clipped_top >= clipped_bottom) continue;
const int32_t clipped_src_x = src_x + (clipped_left - dst_x);
const int32_t clipped_src_y = src_y + (clipped_top - dst_y);
const int32_t clipped_w = clipped_right - clipped_left;
const int32_t clipped_h = clipped_bottom - clipped_top;
draw_sprite_ex(clipped_left, clipped_top, *tilemap.atlas,
clipped_src_x, clipped_src_y, clipped_w, clipped_h,
1, false, false);
}
}
}

View File

@ -3,6 +3,8 @@
#include "Vector2.h"
#include "Triangle.h"
#include "Image.h"
#include "SpriteRegion.h"
#include "Tilemap.h"
#include "BitmapFont.h"
#include <cstdint>
@ -50,11 +52,22 @@ namespace Gfx
void draw_triangle(const RenderData::Triangle& triangle, const RenderData::Color& color);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v);
void draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,

View File

@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include "Image.h"
namespace RenderData
{
struct SpriteRegion
{
const Image* atlas;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
SpriteRegion()
: atlas(nullptr), x(0), y(0), width(0), height(0) {}
SpriteRegion(const Image* atlas, int32_t x, int32_t y, int32_t width, int32_t height)
: atlas(atlas), x(x), y(y), width(width), height(height) {}
};
}

View File

@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include "Image.h"
namespace RenderData
{
struct Tilemap
{
static const uint16_t EmptyTile = 0xFFFF;
const uint16_t* tiles;
int32_t width;
int32_t height;
const Image* atlas;
int32_t tile_w;
int32_t tile_h;
int32_t atlas_columns;
Tilemap()
: tiles(nullptr),
width(0),
height(0),
atlas(nullptr),
tile_w(0),
tile_h(0),
atlas_columns(0)
{
}
Tilemap(const uint16_t* tiles, int32_t width, int32_t height,
const Image* atlas, int32_t tile_w, int32_t tile_h, int32_t atlas_columns)
: tiles(tiles),
width(width),
height(height),
atlas(atlas),
tile_w(tile_w),
tile_h(tile_h),
atlas_columns(atlas_columns)
{
}
uint16_t get_tile(int32_t x, int32_t y) const
{
return tiles[y * width + x];
}
};
}