「ノンフィールドRPG 作り方」で迷子になって、戦闘ループ設計で足が止まった。
ちょうど『Unity 初心者向けノンフィールドRPGの作り方(スマホ対応)』講座を見つけて道筋が見えたので、実際にわたしの環境で“まず遊べる最小構成”を通した記録を置いておく。
再現手順→ハマり→比較の順で、テンプレじゃなく「こうしたら動いた」視点。
最初に“捨てる”を決める(ノンフィールドとは何か)
フィールド移動や広域マップを思い切って捨て、「前進→遭遇→コマンド戦闘→結果処理」を核心に。
迷走しないために、最初に境界線を紙に書いた。
- 残す:ターン制コマンド、HP/食料などのリソース、周回強化(メタ進行)
- 捨てる:自由移動、複雑な装備DB、重量インベントリ
- 後回し:状態異常・属性・難易度分岐・隊列・スキルツリー
ここが定まると、UIとデータ構造がスッと決まる。
環境準備
プロジェクトは「3D コア」で作成し、2DパッケージとTextMeshProを追加。
Portrait前提で最初から解像度を固定。
- Build ProfilesでAndroid/iOSにSwitch→Gameビューに「1080×1920 Portrait」を追加
- TMPの日本語フォントアセットを生成(Atlas 4096、Custom Charactersに日本語表)
- Assets/{Scenes, Scripts, Data, Images, Fonts, Prefabs, Materials, Audios} を先に切っておく
画像はTexture Type=Sprite(2D and UI)、PPUは統一(わたしは480)。
PPUがバラつくとボタン寸法が端末で崩れる。
まずは“遊べる最小ループ”から:タイトル→強化→ダンジョン→バトル→結果
最初は演出ゼロでOK。
MonoBehaviour 1本で通す。
キーボード(A/G/H/E)でも、UIボタンからでも動くようにした。
// GameLoop.cs
// 最小ノンフィールドRPGのゲームループ(タイトル→強化→ダンジョン→バトル→結果→…)
// ・A:Attack / G:Guard / H:Heal / E:Escape
// ・UIボタンからは BattleUI などで EnqueueCommand("Attack") のように呼ぶ
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;
[SerializeField] int hpMax = 20;
// ------ バトル補助 ------
int healCooldown = 0; // 0で使用可
BattleCommand? pendingCommand = null; // UIからの入力キュー
// デモ用の敵(ScriptableObject導入前の仮)
int enemyHp = 10;
int enemyDef = 2;
void Update()
{
switch (state)
{
case GameState.Title:
if (Input.anyKeyDown) state = GameState.PowerUp;
break;
case GameState.PowerUp:
// 本来はメタポイントで初期HP/食料を増やすなど
state = GameState.Dungeon;
break;
case GameState.Dungeon:
if (DoAdvance())
{
SpawnEnemy();
state = GameState.Battle;
}
break;
case GameState.Battle:
var cmd = GetCommand();
DoBattleTurn(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) ResetGame();
break;
}
}
// ---- UIから呼ぶ入口(Button OnClickで "Attack" など渡す)----
public void EnqueueCommand(string command)
{
if (System.Enum.TryParse(command, out BattleCommand cmd))
{
pendingCommand = cmd;
}
}
BattleCommand GetCommand()
{
if (pendingCommand.HasValue)
{
var cmd = pendingCommand.Value;
pendingCommand = null;
return cmd;
}
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 DoAdvance()
{
if (food > 0) food--;
return Random.value < 0.4f; // 40%で遭遇
}
void SpawnEnemy()
{
enemyHp = 10 + floor; // ゆるく強くする
enemyDef = 2;
}
void DoBattleTurn(BattleCommand cmd)
{
switch (cmd)
{
case BattleCommand.Attack:
enemyHp -= CalcDamage(5, enemyDef);
TurnProgress(true);
break;
case BattleCommand.Guard:
// 実ダメ処理は未実装でもよい(最小形)
TurnProgress(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 or 1
}
void TurnProgress(bool consumesFood)
{
if (consumesFood && food > 0) food--;
// ここに敵行動/毒/バフ経過などを集約
}
void ResetGame()
{
floor = 1;
food = 20;
hp = hpMax = 20;
healCooldown = 0;
enemyHp = 10;
enemyDef = 2;
state = GameState.Title;
}
}
Safe Area(ノッチ回避):親RectTransformに一括適用
縦画面はノッチ差がつらい。
まずはSafe Areaを親に当ててUIズレを防ぐ。
// SafeAreaApplier.cs
using UnityEngine;
[RequireComponent(typeof(RectTransform))]
public class SafeAreaApplier : MonoBehaviour
{
void Start()
{
var rt = GetComponent<RectTransform>();
var area = Screen.safeArea;
Vector2 min = area.position;
Vector2 max = area.position + area.size;
min.x /= Screen.width;
min.y /= Screen.height;
max.x /= Screen.width;
max.y /= Screen.height;
rt.anchorMin = min;
rt.anchorMax = max;
}
}
データ駆動の導入
次の段階で、敵やスキルをScriptableObject化。
最小版が動いたあとで入れ替えると破綻しにくい。
// EnemyData.cs
using UnityEngine;
[CreateAssetMenu(menuName = "NonField/Enemy")]
public class EnemyData : ScriptableObject
{
public string enemyName;
public int hpMax;
public int atk;
public int def;
public Sprite artwork;
public SkillData[] skills;
}
// SkillData.cs
using UnityEngine;
[CreateAssetMenu(menuName = "NonField/Skill")]
public class SkillData : ScriptableObject
{
public string skillName;
public int power; // 与ダメ係数
public int cooldown; // 再使用までのターン
public int guardCut; // ガード時の軽減貫通など
}
SOにすると、再生中に値を触って手触りを詰められる。
配列の複製時に参照が連動することがあるので、想定外に動いたら新規SOを作り直すのが早かった。
UIの実務メモ
- Canvas Scaler:Scale With Screen Size、Reference=1080×1920、Match=0.5
- 重要UIはAnchorsで四隅固定を避け、LayoutGroupで伸縮を任せる
- ボタンは48dp相当(物理約9mm)を目安に。TMPはアウトライン細め
RPGツクール系とUnityの比較)
- ツクール:短編を高速で出す、戦闘DB編集が強い、最初の1本が圧倒的に速い
- Unity:スマホ最適UI/3D背景×2D立ち絵/アセット連携/状態機械が素直
- 結論:短距離走はツクール、長距離&スマホUI自由度はUnity、で住み分けるのがラク
ハマり→対策ログ
- TMPが豆腐:日本語フォントアセット未生成。Atlas4096/Custom Charactersに日本語表
- UIが端末でズレる:PPU統一+LayoutGroup+SafeAreaを親に適用
- 回復クールダウンが働かない:減算タイミングを「ターン進行」に集約
- 逃走後の食料が不正:食料減算をTurnProgressに一元化して分岐を減らす
- BGM切替でプチノイズ:Stop→Playでなく、Volumeフェード or Mixer Snapshot
まとめ
ノンフィールドRPGは“捨てる決断”で難易度が一気に下がる。
今回は、状態遷移→最小戦闘→SafeArea→データ駆動の順に固めて、まず“遊べる原型”を1日で通した。
そこから演出やスキルツリーを足していけば良い。
ジャンル選びで迷っているなら、最小ループを先に通してから判断するのが結局いちばん早かった。
必要になったら、ゲーム制作をまとめて学べる講座群もあるので、詰まった箇所だけピンポイントで参照するのがおすすめ。
ちなみに今回参考にしたのはこちらの
【Unity6対応】Unity 初心者向けノンフィールドRPGの作り方講座【全14回】 【スマホ化対応
本1冊分の料金で完走できるのがうれしい。
ゲームを完成させたいなどの人にもおすすめ。