2D game engine com scene graph estilo Godot, escrita em Kotlin, com backends de renderização e linguagens de scripting agnósticas.
A nengine existe para aprender arquitetura de engine — clareza didática acima de performance prematura, evolução incremental guiada por jogos de exemplo que viram prova viva de cada capacidade. Cada decisão fundamental nasce como uma change OpenSpec (proposal + design + specs) antes do código.
A meta de longo prazo é cobrir o ciclo completo: do scene graph mínimo até um editor visual, passando por backends e linguagens de scripting trocáveis sem tocar no núcleo (:engine permanece Kotlin puro, sem dependência de UI/render).
| Backend | Status | Módulo |
|---|---|---|
| Skiko | default — todos os jogos shipped | :engine-skiko |
| LWJGL | segundo backend ativo (NanoVG + GLFW + OpenGL) | :engine-lwjgl |
| Linguagem | Status | Módulo | Notas |
|---|---|---|---|
| Kotlin | native | :engine |
biblioteca entra como dependência |
| Python | default | :engine-bundle-python |
via GraalPy 24.x; stubs .pyi inclusos |
| Lua | suportado | :engine-bundle-lua |
via LuaJ 3.0.x; stubs LuaCATS inclusos |
A SceneTree gerencia uma cena corrente (tree.currentScene) sob um root estável, resolvida por um SceneSource injetado pelo main do jogo em tree.scenes (a quarta SPI server-style, ao lado de audio/textures/textMeasurer). A troca é deferred — tree.changeScene("nome") agenda o swap para o fim do tick (onExit/_exit_tree da cena velha antes do onEnter/_ready da nova), é segura de qualquer hook (inclusive _draw), e falha fail-fast em cena desconhecida. Trocar para a própria cena recarrega-a fresh — "restart de cena" vem de graça.
Duas fontes de cena:
- Bundle — forma única:
manifest.jsonna raiz (descritor de projeto: blocoplatformopcional →GameConfig, blococontentobrigatório comlanguage+mainScene) +scenes/*.json(nome lógico = filename) +scripts/e assets compartilhados (parse de script 1×, instâncias fresh por troca). A cena de entrada é a declarada emcontent.mainScene(não há mais convençãomainnemscene.jsonna raiz).BundleLoader.launch("nome", scriptHosts)lê o manifesto e devolve umLaunch(config, tree)pronto;sceneSourceFromResources(...)/sceneSourceFromPath(...)seguem como API de baixo nível devolvendo oSceneSource. - Code-only —
FactorySceneSource("menu", mapOf("menu" to ::MenuScene, ...))sobre um map de factories Kotlin.
Scripts disparam a troca via self.tree.changeScene("match") (Python) / self.tree:changeScene("match") (Lua). Exemplos canônicos: Demos (code-only, menu ↔ demos), Pong (título → partida, Python) e Jogo da Velha (replay por recarga de cena, Lua).
| Jogo | Backend | Scripting | Função na engine |
|---|---|---|---|
| Pong | Skiko | Python | prova da fundação (loop, física, scripts, signals, Camera2D); título → partida via changeScene |
| Jogo da Velha | Skiko | Lua | sentinela do segundo backend de scripting; replay por recarga de cena (changeScene) |
| Demos | Skiko (+LWJGL) | Kotlin | 5 demos exercitando invariantes, navegadas por menu de UI (FactorySceneSource + changeScene); sentinela do segundo backend de render |
| Snake | Skiko | Python | gameplay discreto/tick-based; mutação dinâmica de scene graph |
| Hello World | Skiko | — | exemplo code-only mínimo (um Label centralizado) |
| Platformer | Skiko | Lua | platformer mínimo (gravidade, pulo, andar) — TileMap + CharacterBody2D + AnimatedSprite2D |
./gradlew :games:hello-world:run # exemplo code-only mínimo
./gradlew :games:pong:run # backend padrão: Skiko + Python
./gradlew :games:tictactoe:run # backend padrão: Skiko + Lua
./gradlew :games:demos:run # backend padrão: Skiko
./gradlew :games:demos:runLwjgl # segundo backend: LWJGL
./gradlew :games:snake:run
./gradlew :games:platformer:run # backend padrão: Skiko + Lua (assets Pixel Adventure 1)macOS — o backend LWJGL precisa rodar no main thread do processo (
-XstartOnFirstThread), pois GLFW liga em Cocoa viaNSApp. A taskrunLwjglinjeta essa flag automaticamente; quem invocarMainLwjglKtmanualmente viajava -cp ...precisa adicionar a flag à linha de comando. Linux e Windows não exigem.
A executável :games:demos expõe 5 demos navegadas por um menu de UI (um botão por demo; cada demo tem um botão de voltar (seta ←)) — não há mais teclas 1–0. Cada demo exercita um aspecto da engine. Detalhe completo em openspec/specs/demos-sample/.
- Transforms — composição aninhada de transform (Sol → órbita → planeta → lua) +
Camera2Dcom zoom (scroll) e pan (arrastar o mouse ou setas); o zoom escala a hierarquia em uníssono (escala-composição). Clicar num corpo trava a câmera nele (zoom suave + follow centrado), com as setas saltando para o corpo vizinho eEsc/clique no vazio destravando — seguir uma lua faz o universo girar em torno dela (a demonstração mais clara doworld()aninhado). - Spawn & Collide — clique/auto-spawn de bolinhas
RigidBody2DnumaBoundaryWalls; trap central interativo (arrastável, com clamp) alterna entreDespawn(sensorArea2Dque remove noonBodyEntered) eCollide(sólidoStaticBody2Dem que quicam), via umSpawnCollideWidgetde debug que o demo registra/des-registra; o auto-spawn pode ser desligado. - Rotating Frame — sweep
moveAndCollideem frame rotativo (CharacterBody2Ddentro de uma caixa que gira e translada). - Tumbling Swarm — quadrados
RigidBody2Dcom spin numaBoundaryWalls; OBB rotated sweep + fricção Coulomb tangencial. - Sprites & Tiles —
TileMap(chão) +AnimatedSprite2Dcorrendo; playerCharacterBody2DsobreStaticBody2D. Sentinela cross-backend (Skiko + LWJGL).
As demos com arena (Spawn & Collide, Tumbling Swarm) têm paredes resize-aware via BoundaryWalls — redimensionar a janela move as paredes em tempo real. O FPS sai do canto de cada demo; use o Profiler (F1) como fonte de verdade.
F1— abre/fecha a HUD de debug com checkboxes para cada widget registrado (Colliders, Log, Debug Draw, Velocity, Contacts, Time, Profiler, Inspector, e quaisquer widgets custom do jogo). O keybind é configurável viaGameConfig(debugHudKey = ...).- Com o Colliders ligado, um painel
Collidersaparece com um segmented controlAABB | REAL(defaultREAL: geometria real da forma;AABB: envelope do broad-phase) — clique no segmento para escolher o modo; o ativo fica destacado. Fechar o painel ([x]) desliga o gizmo. O Profiler mostrafpsno topo do painel, amostrado de forma barata (independente da instrumentação de fases), além das medições por fase. - Com o Inspector ligado, uma view tree navegável do scene graph aparece (filtrando o subtree
__debug): clique numa linha para selecionar o nó, ou clique direto no mundo. A linha selecionada fica destacada; um painel de detalhe (tipo,name, transform world, props@Inspect, read-only) e um gizmo world-space na seleção acompanham. Fechar a view tree ([x]) desliga a ferramenta inteira.
A HUD lista uma linha por DebugWidget ativo no tree.debug registry; clicar uma linha alterna o enabled do widget. Cada painel screen-space (ScreenDebugWidget) tem um header com controles de janela: o grip (grade de pontos) à esquerda arrasta o painel, e à direita ficam colapsar ([_], esconde o corpo mantendo só o header) e fechar ([x], soft close via enabled = false — reabre pela HUD). BACKSPACE restaura o layout default: devolve cada painel ao seu slot e expande os colapsados. Para plugar um gizmo novo num projeto-jogo basta criar uma classe estendendo ScreenDebugWidget (overlay 2D em pixels) ou WorldDebugWidget (gizmo em coordenadas de mundo, recebe a view transform da Camera2D automaticamente) e registrá-la após tree.start():
class MyAxes : WorldDebugWidget() {
override val title = "My axes"
override fun drawDebug(renderer: Renderer) { /* ... */ }
}
fun main() {
val tree = SceneTree(root = MyRoot())
tree.start()
tree.debug.register(MyAxes())
SkikoHost().run(tree, GameConfig())
}Controles específicos de cada jogo (teclas de movimento, mouse) vivem na spec do respectivo <jogo>-sample em openspec/specs/.
Para autocompletar e type-check em scripts:
-
Python (Pyright/Pylance) — adicione
engine-bundle-python/src/main/resources/stubsaoextraPaths:{ "extraPaths": ["engine-bundle-python/src/main/resources/stubs"] } -
Lua (sumneko-lua) — adicione
engine-bundle-lua/src/main/resources/stubsàworkspace.library:{ "workspace": { "library": ["engine-bundle-lua/src/main/resources/stubs"] } }
CLAUDE.md— invariantes arquiteturais, convenções de código, modelo de scripting, workflow OpenSpecROADMAP.md— changes ativas e planejadasopenspec/specs/— especificações por capability (jogos, físicas, renderers, scripting)openspec/changes/archive/— histórico de changes arquivadas