Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック

Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック 1. 概要 Web ゲーム開発、特にローグライクやタクティカル RPG を開発する際、避けて通れないのが「マップの描画」だ。通常、これらは「タイルセット」と呼ばれる画像ファイルを読み込んで描画するが、開発の初期段階や、あえて外部アセットに頼りたくない場合、Canvas 2D API を使ってプログラムで直接タイルを描画する「手書き(プログラマティック描画)」の手法が非常に強力な武器になる。 本記事では、HTML5 Canvas の fillRect や beginPath などの基本命令のみを使い、擬似的なピクセルアート風のタイルを描画するテクニックを解説する。画像を用意する手間を省きつつ、動的に色や形状を変更できる柔軟な描画システムを構築しよう。 2. タイル描画の基本構造 まずは、どのようなタイルでも共通して利用できる描画の入り口を作る。ピクセル座標ではなく、タイル座標(x, y)とタイルサイズ(tileSize)を受け取る設計にすることで、グリッドベースのシステムと統合しやすくなる。 /** * タイルを描画するメイン関数 * @param {CanvasRenderingContext2D} ctx - Canvasコンテキスト * @param {string} tileType - タイルの種類 ('wall', 'floor', 'grass', etc.) * @param {number} x - タイルのX座標(グリッド単位) * @param {number} y - タイルのY座標(グリッド単位) * @param {number} tileSize - 1タイルのピクセルサイズ * @param {string} fieldType - フィールドの種類 ('meadow', 'forest', 'mountain') */ function drawTile(ctx, tileType, x, y, tileSize, fieldType = 'meadow') { const px = x * tileSize; const py = y * tileSize; ctx.save(); ctx.translate(px, py); switch (tileType) { case 'floor': drawFloor(ctx, tileSize, fieldType); break; case 'wall': drawWall(ctx, tileSize, fieldType); break; case 'object_grass': drawFloor(ctx, tileSize, fieldType); drawGrass(ctx, tileSize); break; case 'object_tree': drawFloor(ctx, tileSize, fieldType); drawTree(ctx, tileSize); break; case 'stairs_down': drawFloor(ctx, tileSize, fieldType); drawStairs(ctx, tileSize, false); break; default: ctx.fillStyle = '#333'; ctx.fillRect(0, 0, tileSize, tileSize); } ctx.restore(); } この設計のポイントは、ctx.translate を使ってタイルの左上を原点 (0, 0) に固定することだ。これにより、各タイルの描画ロジック内で座標計算を簡略化できる。 ...

March 31, 2026 · 4 min

TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する

TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する ゲーム開発、特に RPG やタクティカルゲームにおいて、スキルの「射程(レンジ)」や「効果範囲(AOE: Area of Effect)」の実装は非常に複雑になりがちです。単体攻撃、直線、周囲、円形、さらには自分自身を対象とするものまで、そのバリエーションは多岐にわたります。 これらを string 型の ID や、大量の if 文、あるいは複雑なクラス継承で管理しようとすると、いつか必ず「新しいレンジを追加したのに、判定処理を書き忘れた」あるいは「このスキルタイプには不要なはずのパラメータが混入している」といったバグに直面します。 TypeScript の Union 型(特に Discriminated Union / 判別可能な共用体) を活用すれば、こうしたロジックをコンパイルレベルで安全に保護し、設計意図をコードに焼き付けることができます。本記事では、架空のゲームを題材に、any や as を一切使わずに、メンテナンス性の高いスキルシステムを構築する手法を徹底解説します。 1. Union 型でスキル種別を表現する まず、スキルの「レンジ(範囲)」を型として定義します。ここでのポイントは、単に名前を列挙するのではなく、「そのレンジを計算するために最低限必要なデータ」をセットにすることです。 なぜこの設計にするのか(データ構造の純粋性) オブジェクト指向的な発想では、BaseRange クラスを継承して AreaRange クラスを作る、といった方法が取られがちです。しかし、ゲームデータ(特に JSON などでシリアライズされるデータ)を扱う場合、純粋なオブジェクト(POJO)として扱える方がシリアリアライズの相性が良く、ロジックを分離(疎結合)しやすくなります。 // 1. 各レンジの個別定義 type MeleeRange = { type: 'melee' }; // 隣接マス(1マス) type RangedRange = { type: 'ranged'; distance: number }; // 遠距離(単体指定)。最大射程が必要。 type LineRange = { type: 'line'; length: number; width?: number; // オプションで太さを持たせることも可能 }; // 直線。貫通距離が必要。 type AreaRange = { type: 'area'; radius: number; excludeCenter?: boolean }; // 円形または四角形範囲。半径が必要。 type SurroundRange = { type: 'surround' }; // 自分の周囲8マス。パラメータ不要。 type SelfRange = { type: 'self' }; // 自分自身。パラメータ不要。 // 2. これらを統合した Union 型(Discriminated Union) export type SkillRange = | MeleeRange | RangedRange | LineRange | AreaRange | SurroundRange | SelfRange; // 3. スキルの全体定義 export interface Skill { id: string; name: string; description: string; range: SkillRange; // 型安全なレンジ定義 baseDamage: number; scalingStat: 'STR' | 'INT' | 'DEX'; // どのステータスに依存するか manaCost: number; } このように type という「タグ(判別子)」を持たせることで、TypeScript は「type が 'area' なら radius プロパティが確実に存在する」と認識します。逆に、'melee' の時には radius にアクセスしようとするとコンパイルエラーになります。これにより、不必要なデータへの依存を完全に排除できます。 ...

March 28, 2026 · 4 min

ターン制戦闘をフィールドに統合する vs 専用画面に分ける:設計比較と判断基準

ターン制戦闘をフィールドに統合する vs 専用画面に分ける:設計比較と判断基準 ターン制RPGを開発する際、エンジニアが最初に直面する大きな設計判断の一つが「戦闘をどこで行うか」です。具体的には、不思議のダンジョンのように**フィールド上でそのまま戦う(フィールド統合型)のか、ドラゴンクエストのように専用の戦闘画面に遷移する(専用画面型)**のか、という選択です。 この選択は単なるビジュアルの違いに留まらず、状態管理(State Management)、当たり判定、AIの実装、そしてスケーラビリティに決定的な影響を与えます。本稿では、TypeScriptを用いた架空のゲームの実装例を交えながら、両アプローチの設計思想と判断基準を深く掘り下げます。 1. 2つのアプローチの比較 まずは、それぞれの特性をトレードオフの観点から整理します。 項目 フィールド統合型 (Seamless) 専用画面型 (Isolated) UXの印象 テンポが良い、空間の繋がりを感じる 演出が豪華、戦略に集中できる 状態管理 非常に複雑(フィールド+戦闘の混合) 比較的単純(画面ごとにStateを全入れ替え) 位置情報の意味 極めて重要(射程、視線、逃走経路) 抽象的(前衛・後衛、ターゲット選択) AIの実装コスト 高い(地形考慮、パス検索が必要) 低い(コマンド選択アルゴリズムに集中) 拡張性 難しい(新しいギミックが戦闘に影響する) 容易(戦闘専用の特殊ルールを作りやすい) 2. フィールド統合型の実装:空間と時間の同期 フィールド統合型(ローグライク方式)では、「移動」と「攻撃」が同じタイムライン上で扱われます。 なぜその設計にするか プレイヤーが「一歩動く」ことと「剣を振る」ことが同等のコスト(1ターン)を持つため、戦略が空間的になります。壁を背にする、通路に誘い込むといった地形利用が自然にゲームプレイに組み込まれるのが最大のメリットです。 アクション設計の例 TypeScriptでのアクション定義は以下のようになります。 type FieldAction = | { type: 'FIELD_PLAYER_MOVE'; direction: Vector2 } | { type: 'FIELD_PLAYER_ATTACK'; targetId: string } | { type: 'FIELD_MONSTER_TURN_START' } | { type: 'FIELD_DAMAGE_ENTITY'; entityId: string; amount: number } | { type: 'FIELD_KILL_MONSTER'; monsterId: string }; interface FieldState { player: Player; monsters: Record<string, Monster>; tiles: TileMap; turnOwner: 'PLAYER' | 'MONSTER'; animations: AnimationQueue; } 実装のポイント この形式では、Reducer が非常に巨大になりがちです。なぜなら、「移動した結果、トラップを踏み、そのダメージでHPが0になり、死亡処理が走る」という一連の連鎖(Side Effects)を、同一のグリッド座標系で計算しなければならないからです。 const fieldReducer = (state: FieldState, action: FieldAction): FieldState => { switch (action.type) { case 'FIELD_PLAYER_ATTACK': const monster = state.monsters[action.targetId]; if (!monster) return state; // 距離計算が必須 const dist = calculateDistance(state.player.pos, monster.pos); if (dist > state.player.range) return state; return { ...state, // 戦闘結果を直接フィールドの状態に反映 monsters: { ...state.monsters, [action.targetId]: { ...monster, hp: monster.hp - state.player.atk } }, animations: [...state.animations, { type: 'SLASH', pos: monster.pos }] }; // ... } }; 3. 専用画面型の実装:コンテキストの分離 専用画面型(エンカウント方式)では、戦闘が開始された瞬間にフィールドのコンテキストがシリアライズされ、独立した「戦闘エンジン」に制御が移ります。 ...

March 28, 2026 · 2 min

React + Canvas 2D API で作るターン制ローグライク:論理と描画を切り離す設計

React + Canvas 2D API で作るターン制ローグライク:論理と描画を切り離す設計 React でゲームを作る際、多くの開発者が最初に直面するのが「DOM で描画するか、Canvas で描画するか」という選択です。特に数千枚のタイルや多数のユニットが登場するローグライクゲームでは、DOM 要素の管理はすぐにパフォーマンスの限界に達します。 本稿では、React の強力な状態管理(useReducer)と、Canvas 2D API の命令的な描画を組み合わせ、滑らかなアニメーションを実現しつつ堅牢なゲームロジックを維持する設計手法について解説します。 1. 概要:なぜ React と Canvas を組み合わせるのか React は「宣言的」な UI 構築に長けていますが、毎秒 60 回の頻度で数千の DOM 要素を更新するような動的な描画には向いていません。一方で、Canvas は「命令的」であり、ピクセル単位での高速な描画が可能ですが、状態と描画の同期を自分で行う必要があります。 この二つの「いいとこ取り」をするのが、「ロジックは React(useReducer)で、描画は Canvas で」 という役割分担です。 本記事で構築するアーキテクチャ Core Logic (useReducer): ゲームの「真実の状態(State of Truth)」を管理。ターン単位で離散的に変化する座標などを扱う。 Render Loop (requestAnimationFrame): Canvas 上で毎フレーム実行される描画処理。 Interpolation (補完): 離散的な論理座標を、滑らかな描画座標へと変換するローカル状態管理。 2. 設計判断:論理座標と描画座標の分離 ローグライクゲームは基本的に「ターン制」です。プレイヤーが右に移動したとき、内部データ(Core State)では x: 10 から x: 11 へと一瞬で書き換わります。しかし、これをそのまま描画すると、キャラクターがワープしたように見えてしまいます。 滑らかな移動(アニメーション)を実現するためには、以下の二種類の状態を明確に分ける必要があります。 なぜ分離が必要か 種類 管理場所 特徴 役割 論理座標 (Logical Position) useReducer (Global) 整数(タイル単位)。 当たり判定、AI、クエスト進行など。 描画座標 (Visual Position) Canvas 内の Actor クラス (Local) 小数点を含むピクセル単位。 滑らかな移動、揺れ、エフェクト。 判断理由: Core State にアニメーションの「途中経過(x: 10.2 など)」を持たせてしまうと、ゲームロジックが描画の都合に汚染されます。例えば、「まだ移動アニメーション中だから攻撃はできない」といった判定をロジック層で書く必要が出てき、コードが複雑化します。ロジック層は常に「今は(論理的に)どこにいるか」だけを知っていれば良いのです。 ...

March 28, 2026 · 4 min

三竦(さんすくみ)要件定義書

1. 概要 1.1 ゲームコンセプト 「三竦(さんすくみ)」は、犬・猿・雉の三すくみ関係を利用した追跡型対戦ゲーム。プレイヤーは召喚獣を配置して相手を攻撃しつつ、敵の召喚獣から逃げ切る戦略性とアクション性を兼ね備えたリアルタイムバトルゲーム。 1.2 コアメカニクス 三すくみ関係: 犬 → 猿 → 雉 → 犬 追跡システム: 召喚獣は相手プレイヤーを自動追跡 相性バトル: 有利な召喚獣は相手を一方的に倒す 戦略的配置: 召喚位置とタイミングが勝敗を分ける 1.3 開発目標 シンプル: ルールが3分で理解できる 完成優先: 1ヶ月以内にプレイアブル版完成 Android専用: Expo使用、まずCPU対戦のみ 2. ゲーム仕様 2.1 基本ルール 勝利条件 HP制: 各プレイヤーHP 3 制限時間: 3分 勝敗判定: 相手のHPを0にした方が勝ち 3分経過時、HP多い方が勝ち 同点の場合は引き分け ゲームフロー graph TD A[ゲーム開始] --> B[3分タイマー開始] B --> C{ゲーム中} C --> D[プレイヤー移動] C --> E[召喚獣配置] C --> F[召喚獣追跡] F --> G{当たり判定} G -->|当たった| H[HP-1] G -->|外れた| C H --> I{HP=0?} I -->|Yes| J[ゲーム終了] I -->|No| C C --> K{3分経過?} K -->|Yes| J K -->|No| C J --> L[リザルト表示] 2.2 召喚獣仕様 三すくみ関係 graph LR A[犬] -->|勝つ| B[猿] B -->|勝つ| C[雉] C -->|勝つ| A パラメータ表 召喚獣 速度 寿命 クールダウン 特性 犬 🐕 速い 10秒 10秒 素早く追跡、短命 猿 🐒 中速 10秒 10秒 バランス型 雉 🐦 遅い 10秒 10秒 遅いが長持ち 共通ルール: ...

February 6, 2026 · 6 min