Skip to content

LKG パズルゲーム開発 — 企画からWebGLデプロイまで

概要

Looking Glass(LKG)向けに、Nintendo 3DS のキューブパズルゲームを参考にしたパズルゲームを開発しました。 本記事では、企画立案からアセット選定、実装、WebGL ビルド・デプロイまでの全工程をまとめます。

項目内容
Unity バージョンUnity 2022.3.53f1(ビルドは Unity 6 でも実施)
LKG SDKv3(UI 表示方法が v4 では未確認のため)
アスペクト比16:9
デプロイ先Netlify(WebGL ビルド)

プロジェクトのポイントは「固定カメラ」の採用です。 LKG ではカメラ制御に制約があるため、Cinemachine に依存しないゲームジャンルを選定したことが開発をスムーズに進める鍵になりました。


ゲーム企画

Nintendo 3DS のキューブパズル(参考リンク)をベースにした、立体パズルゲームです。

  • 立方体の組み合わせでできたピースを組み合わせて、お題と同じ形を作ります
  • ピースは面を選択して 1 マス押し出すことができます
  • 押し出しの回数は決まっています
  • 展示会向けにレベルは 3 つ作成する予定です
ゲームコンセプトスケッチ

研修プロジェクトとして、以下の要件が定義されました。

  1. 3D モデルを複数同時に表示し、それぞれの正解位置を記憶する
  2. 近くに持っていくとピタッとハマる(スイープ処理)
  3. ユーザー自身でセットアップができる(コンポーネントをアタッチすれば動くイメージ)

LKG や LeapMotion は後回しにし、まずは純粋な Unity アプリとして開発する方針で進めました。 ユニパケ(Unity Package)で渡しやすいように、コンポーネントをアタッチすれば動く構成を目指しています。

タスク分担は以下のとおりです。

  • 松崎: ピース同士のくっつけ処理、面押し出し処理、ピース選択・移動
  • 村井: UI システム作成、問題作成、ピース回転アニメーション

アセット選定

LKG 向けゲーム開発では、カメラ制御との互換性が最重要課題です。 SDK に合わせて URP / HDRP は使用せず、ビルトインレンダーパイプラインで動作するアセットを調査しました。

アセット名価格URP/HDRPLKG 適性
Corgi Engine$82.50対応リスクあり(Cinemachine 依存)
Platformer Project 3D$49.50対応リスクあり(Cinemachine 依存)
Platformer Puzzle Project$38.50URP のみリスクほぼなし(固定カメラ)
Universal Fighting Engine 2無料不明Unity バージョンが古すぎる懸念
Platformer Engine 2D/2.5D$27.49対応やや注意(カメラ揺れあり)

Corgi Engine は最もグラフィックがきれいでしたが、Cinemachine のインパルス機能を使用しており LKG カメラとの競合リスクがありました。Platformer Puzzle Project は固定カメラのため Cinemachine 非依存と推測でき、LKG との相性が最もよいと判断しました。

結論: 固定カメラのパズルゲームが最も安全です。最終的には独自にキューブパズルを一から実装する方針に決定しました。


画面設計

ゲームは 4 つの画面で構成されます。

画面主要UI遷移先
タイトル開始ボタン / 終了ボタンレベルセレクト / アプリ終了
レベルセレクト各レベルボタン / x ボタンゲーム画面 / タイトル
ゲームリセットボタン(右上)---
ポーズ再開 / メニュー / 終了ゲーム / レベルセレクト / アプリ終了
タイトル画面スケッチゲーム画面スケッチ

操作方法

Xbox コントローラーを前提とした入力マッピングです。

操作入力
ポーズStart ボタン
プレイヤー回転右スティック
見本回転LT + 右スティック
ピース選択RT
ピース回転(選択中)左スティック
面選択B ボタン
面押し出し(面選択中)Y ボタン
カーソル操作左スティック
コントローラーマッピング図

実装の要点

PuzzlePiecesManager --- ピース管理クラス

パズルの進捗管理はオブザーバパターンで実装しています。Judge イベント発火後、各ピースが独立して setprogress を呼び出し、全ピースの報告が揃ったら進捗判定を行います。

csharp
public class PuzzlePiecesManager : MonoBehaviour
{
    public PuzzlePieceData[] _piecesAndAnswers;
    public static Action judgeStart;
    private int judgeCount = 0;
    public static PuzzlePiecesManager Instance { get; private set; }

    private void Awake() { Instance = this; }

    private void Start()
    {
        AttachAllPiecesScript();
        ButtonEvents.Save += saveAnswerPosition;
        ButtonEvents.Judge += prepareJudge;
    }

    public void AttachAllPiecesScript()
    {
        for (int i = 0; i < _piecesAndAnswers.Length; i++)
        {
            var pieceObj = _piecesAndAnswers[i].piece;
            var comp = pieceObj.GetComponent<Pieces>()
                       ?? pieceObj.AddComponent<Pieces>();
            comp.Setting(i);
        }
    }

    public void setprogress(int piecenum, bool iscorrect)
    {
        judgeCount++;
        _piecesAndAnswers[piecenum].iscorrect = iscorrect;
        if (judgeCount == _piecesAndAnswers.Length) { Check(); }
    }
}

SAColliderBuilder --- メッシュコライダーの自動生成

複雑な形状のピースには SAColliderBuilder を使用しました。MeshFilter 付きオブジェクトに SAMeshCollider をアタッチし、shapeType: カプセルfitType: inner に設定して Process を実行します。

UICursorInteractor --- カスタムカーソルによるボタン操作

OS カーソルではなく UI Image でカーソルを代用しているため、GraphicRaycaster でカーソル位置の UI 要素を検出しクリックイベントを発火させます。

csharp
public class UICursorInteractor : MonoBehaviour
{
    [SerializeField] private RectTransform cursorImage;
    [SerializeField] private GraphicRaycaster graphicRaycaster;
    [SerializeField] private InputActionProperty submitAction;
    private PointerEventData pointerEventData;

    private void Awake()
    {
        pointerEventData = new PointerEventData(EventSystem.current);
    }

    private void Update()
    {
        if (submitAction.action.WasPressedThisFrame()) { TryClickButton(); }
    }

    private void TryClickButton()
    {
        pointerEventData.position = GetCursorScreenPosition();
        var results = new List<RaycastResult>();
        graphicRaycaster.Raycast(pointerEventData, results);
        foreach (var result in results)
        {
            var btn = result.gameObject.GetComponent<Button>();
            if (btn != null && btn.interactable) { btn.onClick.Invoke(); return; }
            var handler = result.gameObject.GetComponent<IPointerClickHandler>();
            if (handler != null)
            {
                ExecuteEvents.Execute(result.gameObject,
                    pointerEventData, ExecuteEvents.pointerClickHandler);
                return;
            }
        }
    }

    private Vector2 GetCursorScreenPosition()
    {
        var canvasRect = graphicRaycaster.GetComponent<Canvas>()
                         .GetComponent<RectTransform>();
        var center = new Vector2(canvasRect.rect.width / 2f,
                                 canvasRect.rect.height / 2f);
        return cursorImage.anchoredPosition + center;
    }
}

カーソルの注意点

Canvas が Overlay の場合、座標計算方法が Camera スペースとは異なります。ビルド後にマウス操作で画面外を誤クリックする可能性があるため注意してください。


WebGL ビルドとデプロイ

Unity 6 での変更点

Unity 6 では Build Settings が廃止され Build Profiles に変更されています。

  1. Build Profiles から WebGL モジュールをインストールする
  2. プロジェクトを再起動する(Hub でダウンロード完了後も切り替え不可の場合がある)
  3. Switch Platform 実行後、エディタ上でテストプレイ確認してからビルドする
Build Profiles 画面

ビルドエラー

WebGL 切り替え直後にエラーが大量発生する場合は Reimport All を試してください。イベント呼び出しに ?.Invoke を付けることで NullReferenceException を防止できます。

Netlify へのデプロイ

ビルド出力のルートフォルダ(Build/, TemplateData/, index.html)を Netlify Drop にドラッグ&ドロップするだけでデプロイが完了します。

デプロイ先: https://puzzleforlkgdemo.netlify.app/


開発で遭遇した問題

OBJ モデルのマテリアル不一致

OBJ ファイルを外部からリネームした際に、内部の mtllib 参照が元のファイル名(3DModel.mtl)のままだったため、マテリアルが適用されませんでした。OBJ をテキストエディタで開き mtllib 行を正しいファイル名に修正する必要があります。

OBJ 内部の mtllib 参照

Canvas Overlay でのカーソル座標ズレ

Canvas Overlay 環境では、Camera スペース前提の座標計算だとカーソルが画面右下に固定されるバグが発生しました。canvasRect の幅・高さから中央座標を算出し、anchoredPosition ベースに修正して解消しました。

なお、ChatGPT は UI 系(Canvas 設定などインスペクタに依存する部分)が苦手な傾向があります。Canvas のスペース設定によって座標計算が変わるため、AI が提案するコードはスペース設定を確認してから採用してください。


AI 補完

AI 補完 --- Cinemachine と LKG カメラの競合

Cinemachine の CinemachineBrain と LKG SDK の HoloPlay Capture はどちらもカメラの Transform を毎フレーム制御するため、競合が発生します。特に CinemachineImpulse(画面揺れ)はキルト生成で視点ズレを引き起こします。

回避策: (1) 固定カメラのゲームジャンルを選ぶ、(2) Cinemachine をダミーオブジェクトに適用し必要な値だけ HoloPlay Capture に反映する、(3) 特定シーンで CinemachineBrain.enabled = false にして LKG 側に制御を渡す。

AI 補完 --- LKG ゲーム開発のベストプラクティス

  • 固定カメラまたは制限付きカメラ移動を推奨します。自由なカメラ移動は視差変化が大きく酔いの原因になります
  • オブジェクトは LKG のフォーカスプレーン付近に配置すると最も立体感が出ます
  • パズルのように「手前と奥に要素が分かれるゲーム」は LKG との相性が非常によいです
  • UI は手前側に浮かせて配置するとゲーム空間との区別がつきやすくなります
  • LKG は数十枚のビューをレンダリングするため、ポリゴン数とドローコールの最適化が重要です

AI 補完 --- Unity 6 WebGL ビルドと Netlify デプロイ

WebGL ビルド: Build SettingsBuild Profiles に移行しました。WebGL 2.0 がデフォルトで、Compression Format を Disabled にするとデプロイが最もシンプルになります。?.Invoke を使用してデリゲートの null チェックを行うことを推奨します。

Netlify デプロイ: ルートフォルダをドラッグ&ドロップするだけで完了します。Brotli / Gzip 圧縮を使う場合は netlify.toml で Content-Encoding ヘッダーの設定が必要です。

toml
[[headers]]
  for = "/Build/*.wasm.br"
  [headers.values]
    Content-Encoding = "br"
    Content-Type = "application/wasm"

統合元記事: lkg用ゲーム.md (松崎), lkg用ゲーム制作.md (松崎), lkg研修用のパズルゲームを作ってみる.md (松崎), lkg研修用のパズルゲームを作ってみる。2.md (松崎)