njin
Engine 2D gọn nhẹ để tự tay làm game theo ý mình. Vibe coding cũng ổn luôn
Loading...
Searching...
No Matches
Vẽ hình và chữ

Mọi hàm vẽ phải gọi trong ba phase vẽ. Không gian phụ thuộc phase (xem Vòng lặp game):

Phase Không gian Dùng cho
phase_render Thế giới, qua camera Nhân vật, bản đồ, hiệu ứng
phase_post_render Màn hình, pixel UI, điểm số, menu
#include <njin.h>
namespace {
njin::font_handle title_font;
void load(njin::context &ctx) {
// Font có tiếng Việt. Dựng ở cỡ 32 pixel: vẽ đúng cỡ này thì nét sắc nhất.
title_font = njin::font_load(ctx, "assets/fonts/roboto.ttf", 32);
}
// [pixel_text]
// Font pixel thiết kế ở 8 pixel: vẽ ở 8, 16, 24. Chữ luôn nằm trong ảnh ảo.
void load_pixel_font(njin::context &ctx) {
const njin::font_handle pixel =
njin::font_load(ctx, "assets/fonts/PressStart2P.ttf", 8, njin::font_pixel);
style.font = pixel;
style.font_size = 8.0f;
njin::ui_style_set(ctx, style);
// Hoặc đổi font mặc định: njin::font_set_style(ctx, {}, njin::font_pixel);
}
// [pixel_text]
// Trong không gian thế giới: đi qua camera.
void draw_world(njin::context &ctx) {
using namespace njin;
draw_rect(ctx, rect{{0, 0}, {200, 20}}, colors::gray); // mặt đất
draw_circle(ctx, {100, -30}, 20, colors::yellow); // quả bóng
draw_line(ctx, {0, -60}, {200, -60}, 2.0f, colors::white); // dây
draw_triangle(ctx, {150, 0}, {170, -40}, {190, 0}, colors::green);
draw_rect_rotated(ctx, {40, -30}, {30, 30}, 45.0f, colors::red);
// Blend cộng: các vòng sáng chồng lên nhau thì sáng hơn.
draw_circle(ctx, {80, -100}, 30, rgba{1.0f, 0.5f, 0.1f, 0.5f});
draw_circle(ctx, {110, -100}, 30, rgba{1.0f, 0.5f, 0.1f, 0.5f});
blend_end(ctx);
}
// Trong không gian màn hình: UI, không bị camera dịch hay phóng.
void draw_ui(njin::context &ctx) {
using namespace njin;
const vec2 screen = screen_size(ctx);
// Chữ căn giữa: đo trước rồi lùi một nửa.
const char *title = "Màn 1: Khởi đầu";
const vec2 size = text_measure(ctx, title, 32, title_font);
draw_text(ctx, title, {(screen.x - size.x) * 0.5f, 20}, 32, colors::white,
title_font);
// Font mặc định (JetBrains Mono, có tiếng Việt): không cần nạp.
draw_text(ctx, "FPS 60", {10, 10}, 20, colors::green);
// Khung cuộn: chỉ vẽ bên trong vùng 300x100.
const rect panel{{20, 80}, {300, 100}};
draw_rect(ctx, panel, rgba{0, 0, 0, 0.5f});
clip_begin(ctx, panel);
for (int i = 0; i < 10; i++)
draw_text(ctx, "Dong chu dai", {30, 90 + i * 24.0f}, 20, colors::white);
clip_end(ctx);
draw_rect_lines(ctx, panel, 2.0f, colors::yellow);
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc drawing_module() { return {.name = "drawing", .setup = setup}; }
vec2 screen_size(const context &ctx)
Trả về kích thước màn hình mà game vẽ lên, tính bằng pixel.
void blend_end(const context &ctx)
Trở lại cách trộn mặc định.
void draw_rect_lines(const context &ctx, rect r, f32 thickness, rgba color)
Vẽ viền hình chữ nhật.
void draw_triangle(const context &ctx, vec2 a, vec2 b, vec2 c, rgba color)
Vẽ tam giác đặc.
void draw_rect_rotated(const context &ctx, vec2 center, vec2 size, f32 rotation, rgba color)
Vẽ hình chữ nhật đặc xoay quanh tâm của nó.
void blend_begin(const context &ctx, blend_mode mode)
Đổi cách trộn màu cho mọi thứ vẽ sau đó, cho đến blend_end().
void draw_circle(const context &ctx, vec2 center, f32 radius, rgba color)
Vẽ hình tròn đặc.
void draw_rect(const context &ctx, rect r, rgba color)
Vẽ hình chữ nhật đặc.
void draw_line(const context &ctx, vec2 a, vec2 b, f32 thickness, rgba color)
Vẽ đoạn thẳng.
void clip_end(const context &ctx)
Bỏ giới hạn vùng vẽ.
void clip_begin(const context &ctx, rect area)
Chỉ vẽ bên trong một vùng của màn hình, cho đến clip_end().
@ blend_additive
Cộng màu: sáng lên. Dùng cho lửa, ánh sáng, tia lửa.
Definition njin_draw.h:273
void ecs_register(context &ctx, sys_phase phase, sys_fnc fnc, const char *name=nullptr)
Thêm một system vào lịch chạy của một phase.
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
@ phase_render
Mỗi frame, vẽ trong không gian thế giới.
Definition _mod.h:38
@ phase_post_render
Mỗi frame, vẽ trong không gian màn hình (UI).
Definition _mod.h:39
vec2 text_measure(const context &ctx, const char *text, f32 size, font_handle font={})
Kích thước một chuỗi khi vẽ bằng draw_text() với cùng tham số.
font_handle font_load(context &ctx, const char *path, i32 size=0, font_style style=font_smooth)
Nạp một font TrueType/OpenType, kèm sẵn các ký tự tiếng Việt.
void draw_text(const context &ctx, const char *text, vec2 pos, f32 size, rgba color, font_handle font={})
Vẽ chữ với góc trên trái tại pos.
@ font_pixel
Không khử răng cưa, lọc nearest: từng texel của glyph hoặc bật hoặc tắt.
Definition njin_draw.h:97
void ui_style_set(context &ctx, const ui_style &style)
Đặt style cho mọi widget vẽ sau đó.
ui_style ui_default_style()
Giao diện mặc định: panel tối trong suốt, nút bo góc, màu nhấn xanh.
constexpr rgba yellow
Vàng.
Definition _math.h:239
constexpr rgba white
Trắng.
Definition _math.h:234
constexpr rgba red
Đỏ.
Definition _math.h:236
constexpr rgba green
Xanh lá.
Definition _math.h:237
constexpr rgba gray
Xám.
Definition _math.h:240
Định danh của một font, tạo bởi font_load().
Definition _types.h:131
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Hình chữ nhật thẳng trục: pos là góc trên trái, size là rộng và cao.
Definition _math.h:13
Màu gồm đỏ, xanh lá, xanh dương và alpha.
Definition _types.h:64
Toàn bộ giao diện của UI: phông chữ, kích thước, diện mạo từng loại widget, âm thanh.
Definition njin_ui.h:54
f32 font_size
Cỡ chữ, pixel.
Definition njin_ui.h:56
font_handle font
Phông chữ. Mặc định là phông của engine.
Definition njin_ui.h:55
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39
f32 x
Thành phần ngang.
Definition _types.h:40

Hình khối

Hàm Vẽ
njin::draw_rect() Hình chữ nhật đặc
njin::draw_rect_lines() Viền hình chữ nhật, viền nằm bên trong
njin::draw_rect_rotated() Hình chữ nhật xoay quanh tâm
njin::draw_circle() / njin::draw_circle_lines() Hình tròn đặc / viền
njin::draw_line() Đoạn thẳng có độ dày
njin::draw_triangle() Tam giác. Thứ tự ba đỉnh không quan trọng

Màu là njin::rgba, mỗi kênh 0..1. Có sẵn vài màu trong njin::colors (white, black, red, green, blue, yellow, gray, transparent).

Chữ

njin::draw_text() vẽ chữ UTF-8 với góc trên trái tại vị trí cho trước, hỗ trợ xuống dòng bằng \n. njin::text_measure() cho kích thước chữ trước khi vẽ, dùng để căn giữa hay căn phải.

Font mặc định (không truyền font) là JetBrains Mono (giấy phép SIL OFL), nhúng sẵn trong engine nên game không cần file font nào. Nó có bảng chữ Latin và toàn bộ chữ tiếng Việt. Muốn font khác, nạp một font TrueType/OpenType bằng njin::font_load(); font nạp cũng có sẵn bảng chữ Latin và tiếng Việt.

Mỗi cỡ chữ được vẽ có một ảnh glyph riêng, dựng đúng cỡ đó ở lần vẽ đầu tiên. Cỡ làm tròn thành số nguyên pixel (từ 6 đến 256) và vị trí cũng làm tròn về pixel, vì nửa pixel lệch là nửa pixel nhòe trên một chữ cao mười pixel. njin::text_measure() đo bằng đúng ảnh glyph đó, nên căn giữa và căn phải khớp với chữ được vẽ.

Nạp thất bại (file thiếu, không đọc được) trả về handle id 0, nên chữ tự rơi về font mặc định thay vì biến mất.

Chữ kiểu pixel

njin::font_set_style() với njin::font_pixel (hoặc tham số style của njin::font_load()) tắt khử răng cưa và dùng lọc nearest: mỗi texel của glyph hoặc bật hoặc tắt, và chữ được phóng bằng cùng bộ lọc nearest với sprite. Handle rỗng {} đổi font mặc định.

// Font pixel thiết kế ở 8 pixel: vẽ ở 8, 16, 24. Chữ luôn nằm trong ảnh ảo.
void load_pixel_font(njin::context &ctx) {
const njin::font_handle pixel =
njin::font_load(ctx, "assets/fonts/PressStart2P.ttf", 8, njin::font_pixel);
style.font = pixel;
style.font_size = 8.0f;
njin::ui_style_set(ctx, style);
// Hoặc đổi font mặc định: njin::font_set_style(ctx, {}, njin::font_pixel);
}

Kiểu này chỉ nét ở đúng cỡ font được thiết kế: một font pixel như Press Start 2P thiết kế ở 8 pixel thì vẽ ở 8, 16, 24. Font vector thường (như JetBrains Mono) ở cỡ nhỏ sẽ răng cưa và khó đọc. Chữ pixel luôn vẽ trong ảnh ảo, không đi qua lớp chữ nét bên dưới, nên hãy bật integer_scale để mọi pixel chữ to bằng nhau.

UI mịn trên màn hình có độ phân giải ảo

Ngoài chữ, panel bo góc, nút, thanh trượt và mọi hình vẽ trong ảnh nhỏ cũng bị vỡ hạt khi phóng bằng lọc nearest. Đặt njin::config::smooth_ui thì world vẫn vẽ trong ảnh ảo (pixel art), còn phase_post_render (UI, HUD), hội thoại, toast, flash và fade được vẽ sau khi ảnh đã phóng, thẳng vào cửa sổ: engine đặt một phép biến đổi (dời và nhân với mức phóng) nên tọa độ vẫn theo pixel ảo, nhưng hình khối được rasterize ở độ phân giải cửa sổ, và chữ dựng ở cỡ chữ nhân với mức phóng. Thứ tự vẽ giữ nguyên hoàn toàn, không có đánh đổi về chồng lớp như lớp chữ bên dưới. Với chế độ này crisp_text không còn tác dụng.

Lưu ý:

  • njin::clip_begin() vẫn nhận tọa độ pixel ảo. Shader của UI skin nhận uiRect theo pixel cửa sổ.
  • Texture vẽ trong UI được lọc theo njin::texture_filter của nó ở độ phân giải cửa sổ, nên icon pixel art cần njin::filter_nearest để giữ vẻ pixel.
  • Font pixel vẫn được phóng bằng lọc nearest như sprite.
  • Không có tác dụng khi mức phóng bằng 1.

Chữ nét trên màn hình có độ phân giải ảo

Với njin::config::virtual_size, cả frame được vẽ vào một ảnh nhỏ (ví dụ 640x360) rồi phóng lên cửa sổ, nên chữ vẽ trong ảnh đó nhòe theo mức phóng. Khi máy có GPU thật và njin::config::crisp_text bật (mặc định), chữ trên màn hình (UI, HUD, hội thoại, thông báo) không vẽ vào ảnh nhỏ mà xếp hàng đợi, rồi vẽ sau khi ảnh đã phóng, thẳng vào cửa sổ, từ ảnh glyph dựng ở cỡ chữ nhân với mức phóng. Chữ nét ở mọi cỡ cửa sổ, kể cả mức phóng lẻ. Vị trí vẫn theo pixel ảo, và mỗi dòng được dãn khoảng cách chữ cho đúng bằng độ rộng njin::text_measure() đã đo.

Đánh đổi:

  • Chữ xếp hàng nằm trên ảnh ảo, nên engine ghi lại những gì được vẽ sau chữ và phủ lên nó: njin::draw_rect() và nền, nút, thanh trượt của UI. Phần chữ nằm dưới một hình mờ (fade, flash, lớp làm tối sau popup, panel bán trong suốt) được trộn về màu của hình đó; dưới hình đục thì bị bỏ. Nhờ vậy panel vẽ sau che được chữ vẽ trước, đúng thứ tự vẽ. Sprite và texture vẽ sau chữ thì không che được nó; chữ cần nằm dưới chúng thì vẽ chữ trước bằng font pixel hoặc trong world.
  • njin::clip_begin() được tôn trọng: chữ xếp hàng bị cắt theo vùng clip lúc nó được vẽ.
  • Chữ trong world (giữa phase_render với camera) và chữ vẽ vào render texture vẫn vẽ trong ảnh ảo, vì chúng thuộc về không gian và ảnh đó.
  • Máy chỉ có renderer phần mềm (llvmpipe, SwiftShader, GDI Generic) luôn vẽ chữ trong ảnh ảo như trước. Cửa sổ đúng bằng ảnh ảo (mức phóng 1) cũng vậy.

Khử răng cưa bằng supersampling

njin::config::render_scale vẽ cả world và UI ở độ phân giải gấp render_scale lần (2, 4 hay 8) rồi thu nhỏ lại bằng lọc mượt khi lên cửa sổ, làm mượt cạnh hình khối, sprite xoay và đường cong. 1 (mặc định) là tắt.

cfg.render_scale = 4; // đọc từ file cài đặt của game, không hardcode
njin::context *ctx = njin::create(cfg);
context * create(const config &cfg)
Mở cửa sổ và trả về context của engine.
Cấu hình cửa sổ và vòng lặp, truyền vào create().
Definition njin_cfg.h:11
i32 render_scale
Khử răng cưa bằng supersampling: thế giới và giao diện được vẽ ở độ phân giải gấp render_scale lần (t...
Definition njin_cfg.h:69

Đây không phải MSAA của cửa sổ (raylib chỉ có đúng một mức 4x qua GLFW, không chọn được 2x/8x): cách này chạy trên mọi GPU giống nhau và cho đúng số mức đã đặt, đổi lại tốn thêm bấy nhiêu lần pixel GPU phải vẽ (16 lần ở mức 4).

Lưu ý:

  • Không đổi tọa độ nào: njin::screen_size(), chuột, camera vẫn tính như virtual_size (nếu có) hay kích thước cửa sổ (nếu không) — game không biết gì về render_scale.
  • Không đổi được lúc đang chạy. create() tạo cửa sổ và các render texture theo đúng giá trị này một lần; đổi mức trong menu cài đặt cần lưu lựa chọn rồi khởi động lại game để áp dụng, như đổi độ phân giải ở hầu hết game khác.
  • Pixel art dùng virtual_size với njin::filter_nearest nên thường không cần; hợp game vẽ hình khối, sprite xoay hay chữ vector hơn.
  • Cửa sổ rất lớn ở mức cao có thể vượt kích thước texture GPU cho phép; engine tự giảm mức xuống mức cao nhất còn vừa và ghi vào log khi việc đó xảy ra.

Trộn màu

njin::blend_begin() đổi cách màu mới trộn với màu đã có, cho đến njin::blend_end():

Chế độ Hiệu ứng Dùng cho
njin::blend_alpha Trộn theo độ trong suốt (mặc định) Mọi thứ bình thường
njin::blend_additive Cộng màu, sáng lên Lửa, ánh sáng, tia lửa
njin::blend_multiply Nhân màu, tối đi Bóng đổ, lớp phủ màu

Cắt vùng vẽ

njin::clip_begin() chỉ cho vẽ trong một hình chữ nhật, tính bằng pixel màn hình, cho đến njin::clip_end(). Dùng cho khung cuộn trong UI.

Texture nâng cao

njin::texture_draw_ex() vẽ texture với vùng nguồn (cho sprite sheet), tỉ lệ, điểm neo, góc xoay và lật, qua njin::texture_draw_desc. Thường không cần gọi trực tiếp: component njin::sprite làm việc này cho bạn, xem Sprite và animation.

njin::texture_set_filter() chọn cách lấy mẫu khi texture bị phóng to: njin::filter_linear (mặc định, mượt) hoặc njin::filter_nearest (giữ nguyên pixel, cho pixel art).