Skip to content

ADR-0004: Canvas APIの採用

ゲーム描画技術としてHTML5 Canvas APIを採用する

日付: 2025-07-31

ステータス

2025-07-31 提案されました 2025-07-31 受け入れられました

コンテキスト

ぷよぷよゲーム開発において、リアルタイムなゲーム描画を実現するための技術選択が必要でした:

ゲーム描画要件

  • リアルタイム描画: 60FPSでの滑らかな描画
  • 複雑な図形: ぷよの円形、連鎖エフェクト、UI要素
  • 動的更新: ゲーム状態に応じた即座な画面更新
  • ブラウザ対応: 主要ブラウザでの安定動作

技術的制約

  • パフォーマンス: フレームレート維持
  • 保守性: 開発・デバッグの容易性
  • ブラウザ互換性: 幅広い環境での動作
  • 学習コスト: チームの習得容易性

比較対象

DOM + CSS

  • 利点: 宣言的な記述、CSSアニメーション活用
  • 欠点: 大量要素での性能劣化、細かい制御の困難

SVG

  • 利点: ベクターグラフィックス、拡大縮小対応
  • 欠点: 複雑なアニメーションでの性能問題

WebGL

  • 利点: GPU活用による高性能
  • 欠点: 学習コストの高さ、複雑性

Canvas API

  • 利点: 細かい描画制御、良好なパフォーマンス
  • 欠点: 低レベルAPI、イベント処理の複雑性

決定

HTML5 Canvas APIをメイン描画技術として採用する

採用理由

  1. 適切なパフォーマンス
  2. 60FPSゲームに十分な性能
  3. ブラウザ最適化による安定性
  4. メモリ使用量の効率性

  5. 細かい描画制御

  6. ピクセルレベルの制御
  7. カスタム図形の自由度
  8. アニメーション実装の柔軟性

  9. 学習コストの適正性

  10. 2D描画APIの理解しやすさ
  11. 豊富なドキュメントと事例
  12. デバッグツールの充実

  13. ブラウザ互換性

  14. 主要ブラウザでの安定サポート
  15. プログレッシブエンハンスメント対応
  16. モバイルブラウザでの動作

  17. エコシステム

  18. TypeScriptとの良好な統合
  19. 豊富なライブラリとツール
  20. アクティブなコミュニティ

実装方針

// 基本的な描画アーキテクチャ
export class GameRenderer {
  private canvas: HTMLCanvasElement
  private context: CanvasRenderingContext2D

  render(game: Game): void {
    this.clearCanvas()
    this.renderField(game.getField())
    this.renderCurrentPuyo(game.getCurrentPuyo())
    this.renderUI(game.getScore(), game.getState())
  }

  private clearCanvas(): void {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }
}

描画戦略

  1. レンダリングパイプライン
  2. 画面クリア → フィールド描画 → UI描画
  3. フレームベースの更新
  4. 部分更新の最適化

  5. 座標系設計

  6. ゲーム座標 ↔ 画面座標の変換
  7. レスポンシブ対応
  8. 高DPI対応

  9. パフォーマンス最適化

  10. 必要部分のみ再描画
  11. オフスクリーンCanvasの活用
  12. 描画コールの最小化

影響

ポジティブな影響

  1. ゲーム体験の向上
  2. 滑らかなアニメーション
  3. 即座のレスポンス
  4. 視覚的な魅力

  5. 開発効率

  6. 直感的な描画API
  7. デバッグの容易性
  8. 段階的な実装

  9. 保守性

  10. 明確な描画責務分離
  11. テスタブルな描画ロジック
  12. モジュール化された構造

  13. 拡張性

  14. エフェクト追加の容易性
  15. UI要素の柔軟な配置
  16. 将来機能への対応

ネガティブな影響

  1. 複雑性の増加
  2. 低レベルAPI操作
  3. 座標計算の複雑性
  4. イベント処理の実装

  5. パフォーマンス制約

  6. CPU描画による限界
  7. 大量オブジェクト時の性能劣化
  8. バッテリー消費の増加

  9. アクセシビリティ

  10. スクリーンリーダー対応の困難
  11. キーボードナビゲーション
  12. 代替テキストの実装

コンプライアンス

この決定の遵守を確認する方法:

技術的検証

  1. 描画性能測定

    # フレームレート測定
    # ブラウザDevToolsのPerformanceタブ
    # 60FPS維持の確認
    

  2. ブラウザ互換性テスト

  3. Chrome, Firefox, Safari, Edge
  4. デスクトップ・モバイル環境
  5. Canvas APIサポート状況

  6. パフォーマンスベンチマーク

  7. 描画時間測定: 16ms以内(60FPS)
  8. メモリ使用量監視
  9. CPU使用率の確認

実装品質検証

  1. コード構造
  2. 描画ロジックの分離
  3. レンダラークラスの責務明確化
  4. 座標変換の一元化

  5. 描画品質

  6. アンチエイリアス設定
  7. 高DPI対応
  8. レスポンシブデザイン

  9. デバッグ支援

  10. 描画情報の可視化
  11. パフォーマンス計測
  12. エラーハンドリング

運用検証

  1. ユーザビリティ
  2. 操作レスポンス性
  3. 視覚的フィードバック
  4. ゲーム体験の品質

  5. アクセシビリティ対応

  6. 代替操作手段の提供
  7. 色覚対応
  8. コントラスト確保

備考

著者

プロジェクトチーム

関連決定

  • ADR-0001: TypeScriptの採用(型安全な描画処理)
  • ADR-0003: DDDアーキテクチャの採用(描画層の分離)
  • ADR-0005: Vercelデプロイの採用(ブラウザ実行環境)

参考資料

実装統計

描画パフォーマンス

指標 目標値 実績値
フレームレート 60FPS 58-60FPS
描画時間 <16ms 8-12ms
メモリ使用量 <50MB 25-35MB

描画要素数

  • フィールドセル: 78個(6×13)
  • 現在のぷよ: 2個
  • UI要素: 5個(スコア、状態等)
  • エフェクト: 可変(連鎖時)

将来の検討事項

短期改善

  • 部分更新の最適化
  • エフェクトシステムの強化
  • レスポンシブ対応の改善

長期検討

  • WebGL移行の評価
  • WebAssembly活用
  • オフスクリーン描画の導入

実装例

基本描画パターン

export class PuyoRenderer {
  static renderPuyo(
    context: CanvasRenderingContext2D,
    x: number,
    y: number,
    color: PuyoColor,
    cellSize: number
  ): void {
    const centerX = x * cellSize + cellSize / 2
    const centerY = y * cellSize + cellSize / 2
    const radius = cellSize * 0.4

    // 影の描画
    context.fillStyle = 'rgba(0, 0, 0, 0.2)'
    context.beginPath()
    context.arc(centerX + 2, centerY + 2, radius, 0, Math.PI * 2)
    context.fill()

    // メインの描画
    context.fillStyle = this.getColorString(color)
    context.beginPath()
    context.arc(centerX, centerY, radius, 0, Math.PI * 2)
    context.fill()

    // ハイライト
    context.fillStyle = 'rgba(255, 255, 255, 0.3)'
    context.beginPath()
    context.arc(centerX - radius * 0.3, centerY - radius * 0.3, radius * 0.3, 0, Math.PI * 2)
    context.fill()
  }
}

最適化された描画ループ

export class GameRenderer {
  private animationFrameId: number | null = null

  startRenderLoop(game: Game): void {
    const renderFrame = () => {
      this.render(game)
      this.animationFrameId = requestAnimationFrame(renderFrame)
    }
    renderFrame()
  }

  stopRenderLoop(): void {
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId)
      this.animationFrameId = null
    }
  }
}