用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/ababup1192/frogger_new --skill engine-guide命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | engine-guide |
| description | ゲームエンジン(Scene[s], EngineNode[s], GameEngine)のコードを書く前に参照するお作法ガイド。GameData設計、trait実装、衝突応答、動的スポーンを含む |
| user-invocable | false |
エンジン層(汎用・変更不要) ゲーム層(開発者が書く)
───────────────────────── ─────────────────────
EngineNode[s] — ノード包装 GameData — 状態 enum 定義
Scene[s] — ツリー管理 Node instance — process
GameEngine — 更新パイプライン AreaHandler instance — 衝突応答
AreaEvent — 衝突検出 mod Game — buildState / gameLoop
Timer — カウントダウン GameState — ゲーム全体の状態レコード
重要な分離: EngineNode[s] がノードの描画・物理を持ち、型パラメータ s(= GameData)がゲーム固有の状態を持つ。開発者は GameData の設計と trait instance だけを行い、CanvasItem/Node2D 等の trait 委譲は不要。
| ファイル | 役割 |
|---|---|
src/Main.flix | EngineConfig 定義 + LwjglLayer 起動(エントリポイント専用) |
src/scenes/Game.flix | GameData enum + Node/AreaHandler instance + mod Game(状態構築・ゲームループ・ロジック) |
test/scenes/TestGame.flix | テスト |
src/engine/**/*.flix | エンジン層(拡張時のみ) |
開発者は直接定義しない。EngineNode が描画・物理・衝突を担当する。
pub enum EngineNode[state] {
case Sprite2DWithState(Sprite2D, state)
case Area2DWithState(Area2D, state)
case Body2DWithState(CharacterBody2D, state)
case AnimSprite2DWithState(AnimatedSprite2D, state)
case Label2DWithState(Label2D, state)
case Marker2DWithState(Marker2D, state)
}
EngineNode が既に実装する trait: CanvasItem, Node2D, CollisionObject2D, Engine.Renderable
→ ゲーム層での trait 委譲ボイラープレートは 不要
内部のエンジン型を変換する関数:
EngineNode.mapBody2D(f: CharacterBody2D -> CharacterBody2D, node) -> EngineNode[s]
EngineNode.mapSprite2D(f: Sprite2D -> Sprite2D, node) -> EngineNode[s]
EngineNode.mapAnimSprite2D(f: AnimatedSprite2D -> AnimatedSprite2D, node) -> EngineNode[s]
EngineNode.mapArea2D(f: Area2D -> Area2D, node) -> EngineNode[s]
EngineNode.mapLabel2D(f: Label2D -> Label2D, node) -> EngineNode[s]
EngineNode.getState(node) -> s
EngineNode.setState(state, node) -> EngineNode[s]
EngineNode.mapState(f: s -> s, node) -> EngineNode[s]
GameData は ゲーム固有の状態データだけ を持つ enum。Area2D や Sprite2D は含めない。
pub enum GameData {
case PlayerData({prevKeys = Set[Engine.Key], target = Vec2.Vec2})
case VehicleData({vx = Float32})
case PlatformData({vx = Float32, platformWidth = Float32})
case HomeSlotData({filled = Bool})
case HitboxData // データなし、match での識別用
case StaticData // データなし、スプライト・ラベル等
}
設計方針:
HitboxData, StaticData)Eq, ToString を derive(必要に応じて)process で毎フレーム更新buildState, start, gameLoop, ヘルパー、ゲーム固有ロジックinstance Node[GameData] {
redef process(delta, node, _path, scene) =
match node {
case EngineNode.Body2DWithState(body, GameData.PlayerData(r)) =>
// body を変更して返す
(EngineNode.Body2DWithState(updatedBody, GameData.PlayerData(r)), scene)
case EngineNode.Area2DWithState(_, GameData.VehicleData(r)) =>
// Node2D.setPosition で移動
(Node2D.setPosition(newPos, node), scene)
case _ => (node, scene)
}
}
process は (EngineNode[GameData], Scene[GameData]) を返すEngineNode のバリアントと GameData のバリアントの 二重 match で分岐case _ => (node, scene)instance AreaHandler[GameData] {
pub def onAreaEntered(_selfPath, selfState, otherPath, otherState, scene) =
match (selfState, otherState) {
case (GameData.HitboxData, GameData.VehicleData(_)) =>
Scene.mapEngineNode("player" :: "Sprite" :: Nil,
EngineNode.mapAnimSprite2D(sprite ->
CanvasItem.setModulate({r = 1.0f32, g = 0.2f32, b = 0.2f32}, sprite)),
scene)
case _ => scene
}
}
Scene.mapEngineNode / Scene.mapState を使う// ノード追加
Scene.addNode(name, engineNode, scene)
Scene.addChild(parentName, childName, engineNode, scene)
Scene.addChildAt(parentPath, childName, engineNode, scene)
// 状態の取得・変更
Scene.get(name, scene) -> Option[s] // ルートノードの状態
Scene.getState(path, scene) -> Option[s] // 任意パスの状態
Scene.getEngineNode(path, scene) -> Option[EngineNode[s]]
Scene.mapEngineNode(path, f: EngineNode[s] -> EngineNode[s], scene)
Scene.mapState(path, f: s -> s, scene)
// 位置
Scene.globalPosition(name, scene) -> Vec2.Vec2
Scene.globalPositionAt(path, scene) -> Vec2.Vec2
// 全状態マップ(検索用)
Scene.states(scene) -> Map[NodeName, s]
パスの型: NodePath = List[NodeName](例: "player" :: "Sprite" :: Nil)
Scene.empty()
|> Scene.addNode("player",
EngineNode.Body2DWithState(
CharacterBody2D.make(pos, moveSpeed = 512.0f32),
GameData.PlayerData({prevKeys = Set.empty(), target = pos})))
|> Scene.addChild("player", "Sprite",
EngineNode.AnimSprite2DWithState(
AnimatedSprite2D.make(animations, ...),
GameData.StaticData))
CanvasItem.hide で非表示にする(センサー専用)ゲーム全体の状態を1つのレコードで管理する:
pub type alias GameState = {
scene = Scene[GameData],
gameTimer = Timer,
deathTimer = Timer,
lives = Int32,
phase = GamePhase
}
scene 以外のフィールドでゲーム全体の状態を持つ{ scene = newScene, phase = GamePhase.Win | state }pub def start(fontAtlas: FontAtlas): Unit \ Engine.Game =
gameLoop(fontAtlas, buildState(fontAtlas))
def gameLoop(fontAtlas: FontAtlas, state: GameState): Unit \ Engine.Game =
if (Engine.Game.shouldClose()) ()
else {
let dt = Engine.Game.getDeltaTime();
match state#phase {
case GamePhase.Playing =>
{ scene = GameEngine.updateWithAreaHandler(dt, false, state#scene) | state }
|> handlePlayerInput
|> processTimers(dt)
|> checkDeath
|> updateHUD
|> gameLoop(fontAtlas)
case GamePhase.GameOver =>
if (Engine.Game.isKeyPressed(Engine.Key.Enter))
gameLoop(fontAtlas, buildState(fontAtlas))
else
{ scene = GameEngine.updateWithAreaHandler(dt, true, state#scene) | state }
|> updateHUD
|> gameLoop(fontAtlas)
}
}
GameEngine.updateWithAreaHandler は Scene[s] を返す(prevOverlaps は Scene 内部管理)match で処理パイプラインを切り替えるpaused = true で process を停止しつつ描画のみ行うgameLoop を呼ぶScene.readyAll は 不要(processAll が初回に自動実行)def main(): Unit \ IO =
if (Engine.ensureMainThread()) ()
else {
let config: Engine.EngineConfig = {
screenWidth = 784,
screenHeight = 1000,
title = "Frogger",
textureManifest = {name = "...", path = "...", hasAlpha = true} :: Nil,
fontManifest = {name = "default", path = "...", fontSize = 32.0f32} :: Nil,
clearColor = {r = 0.1f32, g = 0.1f32, b = 0.1f32}
};
LwjglLayer.withLwjgl(config, () -> {
let fontAtlas = Engine.Game.getFontAtlas("default");
Game.start(fontAtlas)
})
}
Game.start に委譲するGameEngine.updateWithAreaHandler で使うprocess や onAreaEntered 内で Scene.addNode / Scene.removeAt を呼ぶprocess は 次フレームからgetAt が None を返す)bug!() を到達不能パス以外に使わない — match の網羅性に任せるbuildState の結果のノード数・存在確認Scene.processAll 後の状態検証AreaHandler.onAreaEntered を直接呼び出して検証wrapX, inputDirection 等)を個別テスト