본문으로 건너뛰기

© 2026 Molayo

HN요약2026. 05. 23. 23:36

Show HN: 코드 및 에셋 라이브 리로드 기능이 있는 Rust 기반 2D Lua 게임 엔진을 만들었습니다

요약

Rust 기반의 2D Lua 게임 엔진인 Usagi를 소개합니다. 코드와 에셋의 라이브 리로드, 크로스 플랫폼 내보내기, 입력 리매핑 기능을 지원하여 픽셀 아트 게임의 빠른 프로토타이핑을 돕습니다.

핵심 포인트

  • Rust 기반의 고성능 2D 게임 엔진
  • 코드 및 에셋 변경 시 즉시 반영되는 라이브 리로드 지원
  • 단일 명령어로 Linux, macOS, Windows, Web 내보내기 가능
  • Lua 5.5를 사용한 간편한 스크립팅 환경 제공

Usagi는 Lua 5.5를 사용하여 픽셀 아트 게임을 제작하기 위한 2D 게임 엔진입니다. 라이브 리로드 (live reload), 단일 명령어를 통한 크로스 플랫폼 내보내기 (cross-platform export), 그리고 입력 리매핑 (input remapping) 기능이 내장된 일시정지 메뉴를 특징으로 합니다.

Usagi는 Brett Chalupa가 제작한 무료 소프트웨어이며 퍼블릭 도메인 (public domain)에 기증되었습니다. 커피 한 잔을 구매하여 엔진 개발을 지원해 주세요.

링크: usagiengine.com, Discord, r/UsagiEngine, Quickstart video, YouTube Playlist

Linux, macOS:

curl -fsSL https://usagiengine.com/install.sh | sh

Windows (PowerShell):

irm https://usagiengine.com/install.ps1 | iex

설치 프로그램은 GitHub에서 최신 릴리스를 가져오고, SHA-256 체크섬 (checksum)을 검증하며, usagi~/.usagi/bin/ (Windows의 경우 %USERPROFILE%\.usagi\bin\)에 설치하고 PATH에 추가합니다.

수동 다운로드: GitHub Releases 또는 itch.io

최신 릴리스: v1.0.0.

Rotating cube demo

usagi dev는 코드와 에셋을 감시하며, 게임 상태를 잃지 않고 저장된 변경 사항을 적용합니다. 에디터에서 스프라이트 (sprite)를 수정하면 즉시 업데이트되는 것을 확인할 수 있습니다.

usagi export는 게임을 Linux, macOS, Windows 및 웹용으로 패키징합니다.

텍스처 (textures)를 위한 sprites.png.
사운드 효과, 스프라이트 에디터, 음악 도구는 자유롭게 가져와 사용하세요.

Menu preview showing Continue, Settings, Clear Save Data, Reset Game, and Quit options

프로젝트를 부트스트랩 (Bootstrap)하고 개발 모드에서 시작하세요:

usagi init my_game
cd my_game
usagi dev

initmain.lua (_init / _update / _draw 스텁 (stubs) 포함), Lua LSP 지원을 위한 .luarc.json, .gitignore, meta/usagi.lua (API 타입 스텁 (type stubs)), 그리고 USAGI.md (이 문서의 복사본)를 작성합니다.

main.lua를 편집하고 저장하면, 실행 중인 게임이 재시작하거나 상태를 잃지 않고 변경 사항을 반영합니다. "Hello, Usagi!"를 그리는 코드는 다음과 같습니다:

function _draw(_dt)
gfx.clear(gfx.COLOR_BLACK)
gfx.text("Hello, Usagi!", 10, 10, gfx.COLOR_WHITE)
...

usagi 바이너리를 최신 릴리스로 교체하거나, usagi update를 실행하여 최신 버전을 가져오세요. 그 다음 프로젝트 내부에서 usagi refresh를 실행하면 LSP 타입 스텁 (meta/usagi.lua, .luarc.json, USAGI.md)과 내장된 문서들을 갱신합니다. 이 과정에서 main.lua는 건드리지 않습니다.

피드백, 요청 사항 및 버그 제보를 위해 GitHub Issue를 열어주세요. 중복을 피하기 위해 먼저 검색해 보시기 바랍니다.

Usagi는 Lua를 사용한 빠른 2D 픽셀 아트 프로토타이핑 (Prototyping)을 위한 도구입니다. 아이디어를 빠르게 테스트하고 싶거나, 게임 프로그래밍이 처음이거나, Pico-8의 토큰 제한에 부딪혔거나, 혹은 Love2D보다 더 단순한 것을 원한다면 매우 적합합니다.

이 엔진은 판타지 콘솔 (Fantasy console)이나 Love2D의 대체재가 아닙니다. 모바일이나 VR을 타겟으로 하지 않으며, 중간 규모 이상의 완성도 높은 게임을 위해 만들어지지도 않았습니다.

Lua를 사용하는 이유: 작고, 게임 툴링 (Game tooling)에서 널리 사용되며, 개발의 흐름을 방해하지 않을 만큼 충분히 강력합니다.

Usagi 게임은 단일 .lua 파일이거나, 내부에 main.lua가 포함된 디렉토리 형태입니다. 프로젝트 루트 하위 어디에든 있는 추가 .lua 파일들은 표준 Lua의 require를 통해 로드할 수 있습니다. 선택 사항인 에셋 (Assets)들은 소스 코드와 함께 위치합니다. 여러 파일로 구성된 프로젝트의 폴더 구조는 다음과 같을 수 있습니다:

my_game/
main.lua -- 필수: 게임의 진입점 (Entry point)
sprites.png -- 선택 사항: 16×16 스프라이트 시트 (알파 채널이 포함된 PNG)
...

require "name"은 프로젝트 루트의 name.lua를 찾아가며, 만약 없다면 name/init.lua로 대체됩니다. 점으로 구분된 이름(require "world.tiles")은 슬래시로 구분된 경로가 됩니다. 동일한 탐색 방식이 통합/내보내기 (Fused / Exported)된 빌드 내부에서도 작동하므로, 다중 파일 프로젝트는 별도의 설정 없이 단일 바이너리 또는 .usagi 파일로 배포됩니다.

실행 방법:

usagi init path/to/new_game: 프로젝트를 초기화합니다 (main.lua 스텁, .luarc.json, .gitignore, LSP 스텁, USAGI.md 문서 포함).

usagi dev path/to/my_game: 라이브 리로드 (Live-reload) 개발을 위해 사용합니다 (저장 시 스크립트, 스프라이트, SFX가 리로드되며, Reset 시 _init이 다시 실행됩니다).

usagi run path/to/my_game: 라이브 리로드 없이 실행합니다.

usagi tools [path]: Usagi 도구 창(쥬크박스, 타일 피커)을 엽니다.

usagi export path/to/my_game: 배포를 위해 게임을 패키징합니다. Linux, macOS, Windows 및 웹용 ZIP 파일과 휴대 가능한 .usagi 번들을 생성합니다.

또한 usagi devusagi export와 같이 경로 없이 Usagi 명령어를 실행하여 현재 디렉토리에서 실행할 수도 있습니다.

철학 (Philosophy): 단순함을 유지하고, 이름을 명확하게 지으며, 고정된 함수 시그니처 (fixed function signatures)를 선호합니다.

스타일 (Style): Lua의 경우, 2칸 들여쓰기를 사용하며 로컬 변수 (locals), 함수 이름, 그리고 테이블 필드 (table fields)에는 snake_case를 사용합니다. 파일 범위 상수 (file-scope constants)에는 SCREAMING_SNAKE_CASE를 사용합니다 (local TICK = 0.12, gfx.COLOR_*). 프레임 간 전역 변수 (Cross-frame globals)는 **대문자 (Capitalized)**로 표기합니다. 표준적인 게임 상태 컨테이너는 State이며, _init 내부에서 설정됩니다. 전역으로 유지되는 모듈 임포트 (Module imports) 방식은 Player = require("player")입니다. 함께 제공되는 .luarc.jsonlowercase-global을 활성화하므로, 파일 범위에서 보호되지 않은 소문자 할당은 실수로 local을 누락한 것으로 간주되어 플래그가 지정됩니다. 엔진 API (gfx, input, sfx, music, usagi)는 소문자를 유지하며, meta/usagi.lua를 통해 린트 (lint) 대상에서 제외됩니다.

-- Engine info / config
usagi.GAME_W
usagi.GAME_H
...

Usagi는 각 .lua 소스 코드를 Lua VM에 전달하기 전에 아주 작은 전처리기 (preprocessor)를 거치게 하여, 복합 할당 연산자 (compound assignment sugar)를 추가합니다:

연산자재작성 (rewrite)
+=x = x + y
-=x = x - y
*=x = x * y
/=x = x / y
%=x = x % y
State.score += 1
State.timer += dt

제한 사항: 재작성은 라인 기준 (line-anchored)이므로, if cond then x += 1 end는 그대로 유지됩니다 (풀어서 작성하세요). 좌항 (LHS)이 그대로 복제되므로, t[f()] += 1f()를 두 번 호출합니다.

usagi init에서 제공되는 .luarc.json은 이들을 비표준 심볼 (nonstandard symbols)로 선언하여, lua-language-server가 이를 구문 오류로 밑줄 긋지 않도록 합니다.

Usagi가 호출할 수 있도록 다음 중 하나를 전역 변수로 정의하세요:

_init() — 시작 시와 리셋 (Reset) 시에 한 번 호출됩니다. 여기서 State (및 기타 프레임 간 전역 변수)를 초기화합니다.
_update(dt) — 매 프레임, 그리기 (draw) 전에 호출됩니다. dt는 마지막 프레임 이후 경과된 초 단위 시간입니다.
_draw(dt) — 매 프레임, 업데이트 (update) 후에 호출됩니다. dt는 위와 동일합니다.
_config() — 선택 사항입니다. _config로 호출됩니다.

지원되는 필드:

name: 표시 이름. 윈도우 타이틀 바, macOS .app 번들 디렉토리 (Sprite Example.app), Info.plist의 CFBundleName / CFBundleDisplayName을 결정합니다.

, 그리고 (ASCII kebab-case로 슬러깅한 후) usagi export에 의해 생성된 아카이브 파일명 + Linux/Windows 바이너리 이름입니다.

프로젝트 디렉토리 이름(examples/spr/main.lua → "spr")이 기본값이며, 경로를 사용할 수 없는 경우 "Usagi"로 대체됩니다.

pixel_perfect (기본값 false): true일 때, 게임은 정수 배율(1×, 2×, 3×, ...)로만 렌더링되며, 남는 창 공간은 검은색 레터박스(letterbox bars)로 채워집니다. false일 때, 게임은 화면 비율을 유지하면서 창 크기에 맞는 임의의 배율로 스케일링되므로, 이미지를 왜곡하지 않고 여유 공간이 있는 축에만 바(bars)가 나타납니다. 일반적인 전체 화면 해상도(720p, 1080p, 4K)에서 게임의 네이티브 크기인 320×180은 어차피 정수 배수에 해당하며, 창 모드에서도 여전히 보기 좋기 때문에 기본값은 false입니다.

game_id: com.brettmakesgames.snake와 같은 역-DNS(reverse-DNS) 문자열로, 데이터를 저장하는 네임스페이스 및 macOS 번들 식별자(bundle identifier)로 사용됩니다. 선택 사항입니다.

icon: sprites.png 내의 1부터 시작하는 타일 인덱스로, 창 아이콘 및 (usagi export --target macos 실행 시) .app 아이콘으로 사용됩니다. 선택 사항이며, 기본값은 Usagi 토끼입니다.

sprite_size (기본값 16): sprites.png 내 한 셀의 픽셀 단위 한 변의 길이입니다. gfx.spr 인덱싱, 타일 피커(tilepicker) 도구의 그리드, 그리고 창 아이콘 슬라이서(slicer)를 제어합니다. sprites.png는 반드시 양쪽 축 모두 이 값의 배수를 사용해야 합니다. 레이아웃이 맞지 않을 경우 창 아이콘은 기본값으로 대체됩니다. 이 값은 usagi.SPRITE_SIZE로도 전달되어 Lua 코드에서 활성화된 셀 크기를 읽을 수 있습니다. 선택 사항입니다.

game_width (기본값 320) 및 game_height (기본값 180): 게임의 렌더링 해상도를 재정의합니다. 내부 렌더 타겟(render target)은 이 치수에 맞춰 크기가 지정되며, 창은 비율을 유지하며 이에 맞춰 업스케일링됩니다. 테스트된 범위는 대략 320x180에서 640x360 사이입니다. 이 범위를 벗어나면 일시정지 메뉴와 도구 UI가 픽셀 고정(pixel-fixed)되어 있어, 크기가 매우 작을 경우 넘칠 수 있고 매우 클 경우 빈약해 보일 수 있습니다. 스프라이트 크기 (usagi.SPRITE_SIZE

, 16) 및 번들로 포함된 폰트(5x7)는 해상도에 따라 스케일링되지 않으므로, 1280x720 게임은 화면 대비 스프라이트와 텍스트가 매우 작게 보입니다. 웹 내보내기(web export) 템플릿은 설정된 해상도로부터 캔버스 백킹 스토어(canvas backing-store)와 종횡비(aspect ratio)를 가져오므로, 16:9가 아니거나 기본값이 아닌 게임도 기본 셸(default shell)과 함께 올바르게 배포되며(별도의 --web-shell이 필요하지 않음), itch의 어떤 iframe 크기에서도 깔끔하게 임베드됩니다. 선택 사항입니다. pause_menu (기본값 true): true일 때, 엔진은 Esc / P / Enter / 게임패드 Start 입력을 가로채서 내장된 일시정지 오버레이(pause overlay)를 엽니다. false로 설정하면 해당 키들이 사용자 코드(user code)로 전달되어, 게임이 usagi.menu_item, usagi.toggle_fullscreen, usagi.quitinput.key_* API를 사용하여 자체 메뉴를 구성할 수 있습니다. 비활성화하면 키보드 리맵(keyboard remap) UI, 입력 테스터(Input Tester), 게임패드 기반 메뉴 탐색(동일한 오버레이의 하위 뷰)도 꺼지며, usagi.menu_item 등록 항목도 더 이상 렌더링되지 않습니다. 키보드 중심의 프로토타입 제작에 적합합니다. 선택 사항입니다.

function _config()
return {
name = "Snake",
...

icon (선택 사항)은 gfx.spr과 동일한 인덱싱 방식을 사용하는 sprites.png 내의 1부터 시작하는 타일 인덱스입니다. 생략하면 내장된 Usagi 토끼가 사용됩니다. 선택된 타일은 Linux/Windows의 게임 창에 적용됩니다. usagi export --target macos 실행 시, 동일한 타일이 확대되어 .app 내부의 Resources/AppIcon.icns에 패킹되며, 이는 macOS의 Dock/Finder에서 사용하는 아이콘이 됩니다.

_config()는 런타임(runtime)이 완전히 활성화되기 전(창이 아직 존재하지 않는 상태)에 실행되므로, 그 반환 값은 시작 시 한 번만 읽혀 캐시됩니다. 게임이 실행 중인 동안 _config()를 수정해도 저장 시 제목이나 향후 설정 필드가 업데이트되지 않습니다. 변경 사항을 적용하려면 세션을 재시작하십시오.

gfx 화면에 그립니다. 위치는 게임 공간 픽셀(game-space pixels, 320×180) 단위입니다. 색상은 팔레트 슬롯 인덱스(palette slot indices) 1..16을 사용하며, 명명된 상수(named constants)를 사용하십시오.

gfx.clear(color) — 화면을 채웁니다.
gfx.rect(x, y, w, h, color) — 1픽셀 사각형 외곽선을 그립니다.
gfx.rect_fill(x, y, w, h, color)

— 채워진 사각형 (filled rectangle).
gfx.rect_ex(x, y, w, h, thickness, color)

— 픽셀 단위의 사용자 정의 선 두께 (stroke thickness)를 가진 사각형 외곽선.
gfx.circ(x, y, r, color)

(x, y)를 중심으로 하는 1픽셀 원 외곽선.
gfx.circ_fill(x, y, r, color)

(x, y)를 중심으로 하는 채워진 원.
gfx.circ_ex(x, y, r, thickness, color)

— 사용자 정의 선 두께를 가진 원 외곽선. 선은 명목 반경 (nominal radius)을 중심으로 배치되므로, circ_ex(x, y, r, 1, c) / circ_ex(x, y, r-1, 1, c) / circ_ex(x, y, r-2, 1, c) 호출을 중첩하면 간격 없이 매끄러운 동심원을 생성합니다. 이는 인접한 반경에서 일반 gfx.circ 호출을 레이어링할 때 발생하는 반올림 간격 (rounding-gap) 문제를 해결합니다.
gfx.line(x1, y1, x2, y2, color)

(x1, y1)에서 (x2, y2)까지의 1픽셀 선.
gfx.line_ex(x1, y1, x2, y2, thickness, color)

— 픽셀 단위의 사용자 정의 두께를 가진 선.
gfx.tri(x1, y1, x2, y2, x3, y3, color)

— 세 점을 잇는 1픽셀 삼각형 외곽선. 더 두꺼운 외곽선을 그리려면 세 번의 gfx.line_ex 호출을 사용하세요.
gfx.tri_fill(x1, y1, x2, y2, x3, y3, color)

— 세 점으로 이루어진 채워진 삼각형. 정점 (Vertex) 순서는 상관없습니다. 와인딩 (winding)이 자동으로 교정되므로, 점을 어떻게 배치하든 화살표나 우주선 앞부분 등을 문제없이 그릴 수 있습니다.
gfx.px(x, y, color)

— 단일 픽셀 설정.
gfx.get_px(x, y)

가장 최근에 렌더링된 프레임의 (x, y) 위치에 있는 픽셀에 대해 (r, g, b, palette_index)를 반환합니다. palette_index는 정확한 RGB 일치에 대한 1부터 시작하는 슬롯 번호이며, 팔레트에 없는 색상의 경우 nil을 반환합니다. 화면 밖 좌표이거나 (아무것도 그려지기 전인) 첫 번째 프레임의 경우 네 가지 반환 값 모두 nil입니다. 읽기 작업은 이전 프레임의 완성된 이미지를 반영하므로, 현재 _draw 내부에서 진행 중인 드로잉은 감지하지 못합니다. 전형적인 용도는 색상 기반 충돌 감지 (collision-by-color)입니다. 알려진 색상으로 프레임버퍼 (framebuffer)에 벽을 그린 다음, _update에서 이동할 목적지의 gfx.get_px를 확인하는 방식입니다.
gfx.text(text, x, y, color)

— 번들로 포함된 모노그램 폰트 (5×7 픽셀 폰트, 12 px 라인 높이 (line height); 아래 Credits 참조). 엔진의 기본 Latin/Cyrillic/Greek 글리프 (glyph) 세트를 렌더링하거나, 프로젝트 루트에 font.png가 있는 경우 사용자 정의 폰트를 사용합니다 (아래 "Custom fonts" 참조). 텍스트 크기를 측정하려면 usagi.measure_text를 사용하세요.

measure_textgfx가 아닌 usagi에 포함되어 있습니다. 측정 작업은 순수 유틸리티 (pure utility, 렌더링 부작용 없음)이며 _init을 포함한 모든 콜백 (callback)에서 호출할 수 있기 때문입니다.

gfx.text_ex(text, x, y, scale, rotation, color, alpha)

— 확장된 text 함수

:scale (number) — 폰트 크기 배율. 1, 2, 3

AI 자동 생성 콘텐츠

본 콘텐츠는 HN OpenAI Codex의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0