ゲームプログラミング独学ブログ

ゲームプログラミングを初心者・未経験から独学で身に着けるための情報をまとめていきます。Unityを使った2D・3Dゲームの開発方法やゲームクリエイターになるための情報もまとめていきます。

ノンフィールドRPGの作り方を“検証ログ”で固める:Unity最小構成→スマホ対応→ツクール比較まで

【必見】ゲームプログラミング初心者向けのUnityを使ったゲームプログラミング講座サイト「Unity入門の森」の講座ショップ。RPG、アクションゲーム、ノベルゲーム、タワーディフェンス、FPS、レースゲーム、ローグライクゲームと盛りだくさんの講座サイトです。ここで独学でゲーム開発できるようになりました。

この記事は「ノンフィールドRPG 作り方」を、感覚論ではなく“再現できる手順”で固めた検証ログです。
目的は3つ。

「ノンフィールドとは」を動く最小単位に落とす PC/スマホで破綻しないUIを、追加コード最少で確認する 同じ要件をRPGツクールで作る場合の判断材料を作る(定性比較)

結論の要約: ・コアは「前進→遭遇→コマンド→結果」の1ループで十分検証できる。
スマホは SafeArea と 物理サイズ(約48dp)を先に押さえると最短で安定する。
・「名作」らしさはリッチさよりテンポの管理(1タップ→反応まで<300ms)1 。
以下、環境・手順・コード・比較・不具合修正の順で記録します。

検証の前提と評価軸(「ノンフィールドとは」を実装観点に翻訳)

本稿ではノンフィールドRPGを「歩行マップを持たず、単一の“進行入力”で出現/戦闘/リソース消費が進むRPG」と定義します。
評価軸は以下。

・再現性:同手順で誰がやっても“遊べる”まで到達できるか ・最小性:スクリプト1〜2本、UI4ボタンでコア体験が成立するか ・スマホ適合:SafeArea/解像度/タップ性で致命的崩れがないか ・拡張余地:数値やスキルを後から外出しできる見通しがあるか

除外するもの(今回は検証対象外): ・広いメニュー階層、複雑な装備/属性、ストーリー演出。
・オンライン要素。
必要になったら後段に差し替える前提です。

再現手順:Unityで“最小でも遊べる”ところまで(PC→スマホ

環境:Unity(3D/2D混在でOK)。
テンプレートは3Dコア。
新規プロジェクトから開始。

手順A:シーン/空オブジェクト 空のシーンを開く → GameRoot(空のGameObject)を作成。
下記「GameLoop.cs」を GameRoot にアタッチ。
Canvas(Screen Space-Overlay)を作る。
Panelの上にButtonを4つ(Attack/Guard/Heal/Escape)。
各Buttonの OnClick に GameRoot をドラッグ → 関数 GameLoop.EnqueueCommand(string) を選択 → 引数に "Attack" など文字列を設定。

手順B:ログ表示(任意) ・TextMeshProのUI Textを1つ置き、GameLoop の OnGUI() で GUILayout.Label(...) か、TMPへ簡易表示(今回は省略可)。

手順C:スマホ基本対応 Canvas Scaler → “Scale With Screen Size”(基準 1080×1920、Match 0.5)。
ボタンサイズは最小48dp相当(だいたい縦横100〜120px目安)。
パネル親に「SafeAreaApplier.cs」をアタッチ(後述コード)。
Android/iOSビルドでノッチやホームバーに重ならないことを確認。

これでPC(A/G/H/Eキー)・スマホ(4ボタン)両方で、前進→遭遇→戦闘→結果→再開が回ります。

コアループ実装:1クラスで検証に十分な挙動をまとめる

実装の意図: ・1ファイルで「前進」「遭遇」「戦闘」「回復CD」「逃走」を成立させる。
・“経過”は Tick() に集約し、リソース消費の二重更新を防止。
・テスト容易性のため、入力はキーボードとUIの両方から受ける。

// GameLoop.cs
using UnityEngine;

public class GameLoop : MonoBehaviour
{
public enum GameState { Title, PowerUp, Dungeon, Battle, Result, GameOver }
public enum BattleCommand { Attack, Guard, Heal, Escape }

[SerializeField] GameState state = GameState.Title;
[SerializeField] int floor = 1;
[SerializeField] int food = 20;
[SerializeField] int hp = 20, hpMax = 20;

int healCooldown = 0;
BattleCommand? pending; // UIの一回分入力を溜める

// 仮の敵(検証用)
int enemyHp = 10, enemyDef = 2;

void Update()
{
    switch (state)
    {
        case GameState.Title:
            if (Input.anyKeyDown) state = GameState.PowerUp;
            break;

        case GameState.PowerUp:
            // メタ強化は後回し。今回は即ダンジョンへ。
            state = GameState.Dungeon;
            break;

        case GameState.Dungeon:
            if (Advance()) // 遭遇したら戦闘へ
            {
                SpawnEnemy();
                state = GameState.Battle;
            }
            break;

        case GameState.Battle:
            var cmd = ReadCommand();
            DoTurn(cmd);
            if (hp <= 0 || food <= 0) state = GameState.GameOver;
            else if (enemyHp <= 0) state = GameState.Result;
            break;

        case GameState.Result:
            floor++;
            state = GameState.Dungeon;
            break;

        case GameState.GameOver:
            if (Input.anyKeyDown) ResetAll();
            break;
    }
}

// --- UIボタンから "Attack" 等を渡す ---
public void EnqueueCommand(string command)
{
    if (System.Enum.TryParse(command, out BattleCommand cmd))
        pending = cmd;
}

BattleCommand ReadCommand()
{
    if (pending.HasValue) { var c = pending.Value; pending = null; return c; }
    if (Input.GetKeyDown(KeyCode.A)) return BattleCommand.Attack;
    if (Input.GetKeyDown(KeyCode.G)) return BattleCommand.Guard;
    if (Input.GetKeyDown(KeyCode.H)) return BattleCommand.Heal;
    if (Input.GetKeyDown(KeyCode.E)) return BattleCommand.Escape;
    return BattleCommand.Attack; // 入力なし時は既定で進行
}

bool Advance()
{
    if (food > 0) food--;           // 前進は食料を消費
    return Random.value < 0.4f;     // 40%で遭遇(検証用)
}

void SpawnEnemy()
{
    enemyHp = 10 + floor; // フロアで微増
    enemyDef = 2;
}

void DoTurn(BattleCommand cmd)
{
    switch (cmd)
    {
        case BattleCommand.Attack:
            enemyHp -= CalcDamage(5, enemyDef);
            Tick(consumesFood: true);
            break;

        case BattleCommand.Guard:
            // 今回は効果薄めでも可。先にループを回す。
            Tick(consumesFood: true);
            break;

        case BattleCommand.Heal:
            if (healCooldown == 0)
            {
                hp = Mathf.Min(hp + 8, hpMax);
                healCooldown = 3; // 3ターン再使用不可
                // 回復はターン/食料を消費しない
            }
            break;

        case BattleCommand.Escape:
            state = GameState.Dungeon;
            break;
    }

    if (healCooldown > 0 && cmd != BattleCommand.Heal)
        healCooldown--;
}

int CalcDamage(int atk, int def)
{
    var baseDmg = Mathf.Max(1, atk - def);
    return baseDmg + Random.Range(0, 2); // 0〜1の軽い揺らぎ
}

void Tick(bool consumesFood)
{
    if (consumesFood && food > 0) food--;
    // 敵の行動・DoT等を追加するならここに集約
}

void ResetAll()
{
    floor = 1; food = 20; hp = hpMax = 20;
    healCooldown = 0; enemyHp = 10; enemyDef = 2;
    state = GameState.Title;
}


}

ポイント: ・“回復は食料を消費しない/クールダウン有り”は、操作テンポの検証に有効(ボタン連打で破綻しがちな箇所の炙り出し)。
・遭遇率やダメージは「感じ」を掴むために固定/近似でOK。
定数は後でSOやJSONに外出しできます。

スマホ適合の検証:SafeAreaとタップ性だけ先に固める

初回ビルドで最も崩れやすいのが、ノッチ/ナビバー衝突とボタンの押下性です。
以下の2点だけ先に入れると失敗が激減。

SafeArea適用(親RectTransformのアンカーを書き換え) 48dp相当の最小ターゲット(指先9mm)+十分なPadding

// SafeAreaApplier.cs
using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class SafeAreaApplier : MonoBehaviour
{
void Start()
{
var rt = GetComponent<RectTransform>();
var safe = Screen.safeArea;
var min = safe.position;
var max = safe.position + safe.size;

    min.x /= Screen.width;  min.y /= Screen.height;
    max.x /= Screen.width;  max.y /= Screen.height;

    rt.anchorMin = min;
    rt.anchorMax = max;
}


}

検証方法(再現手順): ・解像度を 1080×1920(縦)に固定 → 端末上でノッチ右/左/上下のパターンを2機種以上で確認。
・ボタンの最小辺を100px前後に設定 → 右利き/左利きで親指タップの“外し”が出ないかをカウント(10回×2手)。
・期待結果:外し率5%未満、UIがノッチ/バーに重ならない。

UnityとRPGツクール、同要件の比較検証(定性・最小表)

同じ「前進→遭遇→コマンド→結果」を両者で作る前提で、初期ハードルと伸び代を比較。
あくまで今回の要件・私の再現に基づく所感です。

観点 Unity ツクール 最初の一歩 C#1ファイル+UI配線で自由。
雛形から空気抵抗が低い 既存の戦闘/DBが強い。
イベント駆動が早い UI自由度/スマホ SafeArea/Canvasでコントロールしやすい 既定UIが安心。
高度カスタムはプラグインに依存 ループの可視化 Update/Stateで一本化しやすい マップ/イベントの抽象化が必要になる 拡張(3D/2D混在) 背景3D×敵2Dなどの混在が自然 2D中心でスムーズ。
3Dは別アプローチ

判断の指針: ・“名作のテンポを踏襲して短編を早く出す”ならツクールの既成レイヤーが便利。
・“スマホの押し心地/3D背景を詰めたい”ならUnityが手早い。
どちらを選んでも、ノンフィールドrpg スマホの体験核心は「反応速度と手順の少なさ」。
ここを検証の主語にすると迷いづらいです。

不具合の再現と修正ログ(最小ケース→原因→対処)

ログ1:回復が無限に撃てる ・再現:Heal連打 → クールダウン不発。
・原因:CD更新が入力直後にだけ行われ、他コマンド経過で減らしていない。
・対処:healCooldown > 0 && cmd != Heal の時にデクリメント(上掲コード)。
・確認:連続Heal不可、3アクション後に再解放。

ログ2:食料の二重消費 ・再現:前進で1消費、Attackでもう1消費 → 期待通り。
ただしGuardでの消費が場当たり的に増減。
・原因:各所でfood--していた。
・対処:Tick(consumesFood: bool) に集約。
・確認:前進時1、戦闘1アクションで1(Attack/Guardのみ)に統一。

ログ3:スマホでボタンがノッチに潜る ・再現:上下セーフエリア端末で、最下ボタンがホームバーに重なる。
・原因:親コンテナのアンカーを固定比率にしていた。
・対処:SafeAreaApplier を親へ適用。
・確認:全端でUIが安全領域内に収まる。

ログ4:テンポが重く感じる ・再現:Attack→結果表示まで体感>300ms。
・原因:Update内で余計な処理(ログ構築/Find)をしていた。
・対処:ログ構築を必要時のみ、Find禁止(SerializeField参照)。
・確認:体感改善。

まとめ:まず“1ループ”を完走させる。次はデータ外出しとスキルへ

今回の検証で見えた作り方:

・「ノンフィールドとは」の芯は、前進とリソース消費、単純な戦闘の往復。
ここを1クラス/4ボタンで回す。
スマホは SafeArea と 48dp だけ先に。
デザインは後からで間に合う。
・“経過処理の集約(Tick)”がバグと計測を減らす。
・拡張は ScriptableObject 等で“後から”外出しで十分(敵・スキル・成長)。

次の検証計画: ダメージ式/遭遇率/回復量をSO化し、難易度プロファイルを差し替え実験。
Healと同じCD機構をスキルに横展開し、強スキルの再使用間隔でテンポ調整。
メタ進行(例:到達階層に応じた初期食料+1)を最小実装。
“名作”の空気は、派手さより“流速の気持ちよさ”。
ここを継続的に測るとブレません。
必要になったとき、教材や素材はピンポイントで補えば十分でした。
【Unity6対応】Unity 初心者向けノンフィールドRPGの作り方講座【全14回】 【スマホ化対応】が参考になったのでよかったらみて。

— 脚注 —

タップ→反応までの主観閾値は約100〜300ms。
演出を待たせる場合は“自発操作直後”に即時フィードバック(SE/小さな点滅)を返し、重い処理は後段非同期/短フェードに逃がすのが有効。