
【プロジェクト:Neith】通しテストプレイ〜みんなで作るゲーム制作【4/24(金)8日目】
■今回やった事
今回は以下の処理を作り込みました!
ストーリー固定イベントの発生処理
育成メニュー画面の連動
アルカステラのステータス確認機能
「休憩」コマンドの実装
特訓失敗時のペナルティ処理追加
戦闘システムのGridMap生成ロジックテスト
■実装部分のテスト
■3ターン通しテスト
少し長めの動画ですが、
実際の動きを見ていただけるとゲームらしさが増してきたのが伝わるかと思います!
■戦闘マップ生成ロジックテスト

■募集中のアイディア
①「古代エジプト編」で発生しそうなイベント
(例:ピラミッドでの過酷な修行、ナイル川での予期せぬハプニング など)
※期限はありません!皆さんの自由でユニークな発想をお待ちしています。
② 主人公・ネイトの「スキル」(4/28(火)まで!)
(例:ファイアボール / 射程:2マス先の一体 / 効果:対象に小ダメージ)
※王道からちょっと笑えるネタスキルまで、
どんなアイディアでも大歓迎です!
こんばんは、まる。です!
前回は残業に追われながらも、
なんとか特訓システムの基礎を作り上げたネイト。
これでユニット成長の基盤は盤石に整った……かに思えました。😏
しかし、彼女が構築しなければならないシステムは、
まだまだ山の様にそびえ立っています。
ランダム発生イベント、体力回復システム、アイテムの実装、そして最難関の戦闘システム……!
想像するだけでも頭がパンクしそうなこの複雑なロジックの波に、
ネイトはどう立ち向かっていくのか?
白熱の開発ストーリー、第8日目がいよいよ開幕です!!!
二人の茶番
🎧今日の作業用BGM
Tipsy_Dance/作(編)曲 :ヒオ
【民族音楽風/酒場/踊り】Tipsy Dance【フリーBGM】

「よお、おまえら!
オレだよ、ポマイラ様だ!
みんな、朝飯はしっかり食ってるか?
朝からちゃんと飯を食わないと、元気に一日動けないからな!」

「うぅ…耳が痛くなる話ね。
さて、今日から開発もいよいよ2週間目に突入よ。
これからは一気に内容が難しくなると思うから、
みんなどうか振り落とされずについて来てね!」

「そうか、もう一週間経っちまったのか…早いもんだな。
それで、1か月の締め切りには間に合いそうなのか?」

「正直、分からないわ…。
『宴』のアセットを使っている関係で、
戦闘システムとかは多分ゼロから自作することになるだろうし。」

「…とはいえ、私もただ指をくわえて待ってる訳じゃないわ!
裏ではClaudeさんに頼んで、
戦闘システムのコードを組んでもらっているところよ。」

「おっ、一応準備は着々と進めてるってわけだな。」

「まぁね…。
でも、書いてもらったコードを読んでも正直さっぱりよ。
難しそうな技術がてんこ盛りで…。」

「シミュレーションRPGに関しては、
私もからきし開発の知識がないの。
だからこそ、みんなと一緒に勉強していければいいなと思ってるわ。」

「そうだな!
将来、画面の向こうのおまえがすっげぇシミュレーションRPGを作るかもしれないしな。
そのための道標として、俺たちはここにしっかりと軌跡を残していこうぜ!」
特訓システムの総仕上げ(20:00-20:10)

「さて、今日はいきなり昨日作った特訓システムの仕上げに取り掛かるわよ!」

「ストーリー執筆は今日は良いのか?」

「うん…昨日の様子を見る限りこの2時間はシステム開発に回さないととてもじゃないけど間に合わない気がしたの」

「…ちょっとズルいかもしれないけど、ストーリー執筆は私の趣味として裏で進めていくわ。
元々、お話を書くのは好きだしね」

「まぁ、企画的にはグレーだが…せっかくならみんなに楽しく遊んで欲しいしな!
オレ様はアリだと思うぜ!」

「ありがとう。
それじゃ、張り切って作業を進めるわよ。
まずは、特訓の分岐を整理していくわ!」


「おっ、なんかカラフルで読みやすいぞ!」

「ふふ、そうでしょう?
これはね、一つのIF文のブロックごとに網掛けをして区別するようにしてるの。
『宴』でExcel管理をしていると、
今どの分岐にいるのか迷子になる事が多いからね。
これも一つの開発の工夫よ!」

「いいじゃねぇか!
他人にも、数日後の自分にも読みやすいコードってのは大事だぜ!」
シーンの繋ぎ(20:10-20:12)

「次に、『シーンマネージャー』という、
全体の流れを管理するための仕組みを作っていくわ。
これは育成メニューで行動するたびに呼び出されて、
現在のターン数に応じて、
固定で発生するイベントシーンへの『橋渡し』をする役割になるの。」


「なるほどな。
プレイヤーが行動するたびにこいつを呼び出して、条件に合った次のシーンを出してもらうってわけか。」

「そうよ!
条件に合致して呼び出すシーンがない場合は、
そのまま『育成メニュー』をもう一度表示させるわ。
これで、育成パートの基本的なループ(循環)が出来上がるの。」


「あとは、行動が終わった後に『現在のターン数(TurnInSection)』に+1をして、
『このセクションの残りターン数(TurnRemain)』から-1を引く処理を入れるの。
これで、プレイヤーが行動するたびに、裏側でしっかりターンが進んでいくようになるわ!」
特訓失敗時の処理を作成(20:12-20:30)
・ロジックの作成(20:12-20:20)

「次に、特訓失敗時の処理を作成していくわ。
これは、Claudeさんと事前に数回やり取りをして追加してもらった機能を使うのだけど…一部抜粋して説明するわね」
■各特訓の定義
public static readonly TrainingTypeConfig[] All = new[]
{
// ────────────────────────────────────────────────────────
// 1. 基礎体力作り(過酷な走り込み)
// メイン: 最大HP大アップ サブ: スピード微増 / 運微増
// ────────────────────────────────────────────────────────
new TrainingTypeConfig
{
trainingType = TrainingType.Cardio,
label = "基礎体力作り",
description = "泥臭くひたすら走り込む。\nHPを大幅強化。\nスピードと運も微増する。",
primaryStat = "maxHp",
primaryBase = 10,
primaryYarukiBonus = 0.05f,
primaryRandom = 1,
subStat1 = "speed",
subBase1 = 1,
subStat2 = "luck",
subBase2 = 1,
genkiCost = 25,
baseFailureRate = 5,
yarukiCost = -15,
companionBonus = 2,
affinityGain = 3,
},
// ────────────────────────────────────────────────────────
// 2. 仮想敵との実戦稽古(薪割り・素振り)
// メイン: ATK大アップ サブ: 最大HP微増 / 器用さ微増
// ────────────────────────────────────────────────────────
new TrainingTypeConfig
{
trainingType = TrainingType.Combat,
label = "実戦稽古",
description = "武器の振り込みや\n実戦形式の稽古でATKを強化。\nHPもわずかに伸び、器用さも微増。",
primaryStat = "atk",
primaryBase = 4,
primaryYarukiBonus = 0.03f,
primaryRandom = 2,
subStat1 = "maxHp",
subBase1 = 2,
subStat2 = "dexterity",
subBase2 = 1,
genkiCost = 30,
baseFailureRate = 10,
yarukiCost = -20,
companionBonus = 1,
affinityGain = 4,
},
// ────────────────────────────────────────────────────────
// 3. 丸太受け止め特訓(防具の着こなし訓練)
// メイン: DEF大アップ サブ: 最大HP微増 / 耐魔力微増
// ────────────────────────────────────────────────────────
new TrainingTypeConfig
{
trainingType = TrainingType.Defense,
label = "丸太受け",
description = "飛んでくる丸太を\n受け止めてDEFを高める。\nHPが微増し、\n精神力向上で耐魔力も微増。",
primaryStat = "def",
primaryBase = 4,
primaryYarukiBonus = 0.03f,
primaryRandom = 2,
subStat1 = "maxHp",
subBase1 = 2,
subStat2 = "magicDef",
subBase2 = 1,
genkiCost = 30,
baseFailureRate = 10,
yarukiCost = -20,
companionBonus = 1,
affinityGain = 4,
},
// ────────────────────────────────────────────────────────
// 4. 魔導書の読解と瞑想
// メイン: 魔力大アップ サブ: 耐魔力微増 / 器用さ微増
// ────────────────────────────────────────────────────────
new TrainingTypeConfig
{
trainingType = TrainingType.Magic,
label = "瞑想",
description = "座学や瞑想で\n魔力を大きく引き上げる。\n耐魔力がつき、繊細な魔力操作で\n器用さも微増。",
primaryStat = "magic",
primaryBase = 4,
primaryYarukiBonus = 0.03f,
primaryRandom = 1,
subStat1 = "magicDef",
subBase1 = 1,
subStat2 = "dexterity",
subBase2 = 1,
genkiCost = 20,
baseFailureRate = 1,
yarukiCost = -10,
companionBonus = 1,
affinityGain = 3, // 座学は絆が深まりやすい
},
// ────────────────────────────────────────────────────────
// 5. 森の中の障害物ダッシュ
// メイン: スピード大アップ サブ: DEF微増 / 運微増
// ────────────────────────────────────────────────────────
new TrainingTypeConfig
{
trainingType = TrainingType.Agility,
label = "障害物\nダッシュ",
description = "足場の悪い森を\n障害物を避けながら走る。\nスピードを極限強化。\n受け身の技術でDEF微増、\n危機察知で運も微増。",
primaryStat = "speed",
primaryBase = 4,
primaryYarukiBonus = 0.03f,
primaryRandom = 1,
subStat1 = "def",
subBase1 = 1,
subStat2 = "luck",
subBase2 = 1,
genkiCost = 25,
baseFailureRate = 10,
yarukiCost = -15,
companionBonus = 2,
affinityGain = 3,
},
};
...続く
「いきなりコードが出てきて難しそうよね。
一応、この中には各特訓で上昇するステータスやその値、
やる気によるボーナス、体力の消費量など色々なデータが詰まっているのだけど……一番注目してほしいのはこの部分よ!」
baseFailureRate = 5,
「この値はなんだ?ネイト。」

「これは、この特訓にかかる『基本的な失敗率』よ。
たとえば基礎体力作りなら、
デフォルトで5%の失敗率が設定されているわ。
実際のゲームプレイでは、この5%をベースにして、
キャラクターの『やる気』や『残り体力』に応じて失敗率を減らしたり、逆にドカンと上げたりするのよ。」
■失敗率計算
public static int CalcFailureRate(TrainingTypeConfig config, int genki, int yaruki)
{
// 1. 練習の基礎失敗率をベースにする
float rate = config.baseFailureRate;
// 2. 元気(体力)によるペナルティとボーナス
// 【調整ポイントA】安全ラインの元気量(例: 60)
int genkiThreshold = 60;
if (genki < genkiThreshold)
{
// 元気が安全ラインを下回っている場合、足りない分だけ失敗率が急上昇する
// 例: 係数が 1.5 の場合、元気が20(40不足)なら 40 × 1.5 = +60% の失敗率増
int missingGenki = genkiThreshold - genki;
rate += missingGenki * 1.5f;
}
else
{
// 元気が十分にある場合は、少しだけ失敗率を下げる
// 例: 係数が 0.2 の場合、元気が100(40超過)なら 40 × 0.2 = -8% の失敗率減
int extraGenki = genki - genkiThreshold;
rate -= extraGenki * 0.2f;
}
// 3. やる気によるボーナス
// 【調整ポイントB】やる気が高いほど失敗率を軽減する
// 例: 係数が 0.15 の場合、やる気100で -15% の軽減
float yarukiBonus = yaruki * 0.15f;
rate -= yarukiBonus;
// 4. 最終的な失敗率を 0% 〜 99% の範囲に収める(100%確定失敗にはしない)
return Mathf.Clamp(Mathf.FloorToInt(rate), 0, 99);
}
「これがその計算式ね。
ポイントは、体力の残りが少ないと失敗率が一気に跳ね上がるところよ。
具体的には、体力が60%を下回ると危険信号!
『やる気』に関しては、その値によって最大15%まで失敗率を減らせるわ。
ただ、他の育成ゲームと違って、本作では特訓でも『やる気』が減っちゃうの。
だから、体力管理とやる気管理がよりシビアで重要なゲームになると思うわ。」

「良くできてるじゃねぇか!
……でもよ、この計算式、プレイヤーが使いこなすのは難しくないか?」

「ふふふ、別に使いこなす必要はないのよ。
プレイヤー的には『今の体力だと失敗率が高くて危険そうだ…!』という結果の情報だけが直感的に分かればいいの!
『内部の計算ロジックは奥深く、外側(プレイヤーに見せるUI)は簡潔に。』
それが、良いゲーム体験を作る上で大切なことだと思うわ!」

「なるほどな。
確かに、裏側の難しいことはよく分かんねぇけど、
画面の数字を見るだけでハラハラできそうだぜ!」
失敗率のUI表示(20:20-20:30)

「それじゃあ、今作った失敗率のロジックを
昨日作った育成画面のUIに組み込んでいくわね。」


「こんな感じでどうかしら?
各特訓カードの左上に、現在の失敗率が赤く表示されるようにしてみたわ。」

「おおっ!
トゲトゲした警告マークが追加されて、一気にスリルが上がったな!
俺サマ好みの、スリリングなゲームになってきたぜ!」
ステータス画面作成(20:30-21:15)

「次は、仲間である『アルカステラ』たちのステータス画面を一気に仕上げちゃうわね!
今回、私が画面設計でやりたい要件はこの2つよ」
【今回のUI実装要件】
イラストの連動: 選択ボタンや詳細画面に出てくるイラストを、対象のアルカステラのものに動的に切り替える。
画面の分離: 「ネイト(主人公)」と「友達(仲間のアルカステラ)」で、詳細画面を別々のものにする。

「イラストをそれぞれのアルカステラに変えるってのは分かるが……
何でネイトだけ別の画面にするんだ?」

「私自身が持つステータスと、他のアルカステラたちのステータスは仕組みが独立しているからよ。
例えば、私自身に『好感度』のステータスは要らないじゃない?
でも逆に、特訓で消費する『体力(元気)』や『やる気』のパラメーターは、私にしか必要ないでしょ?」

「なるほどな。
プレイヤー自身である『一人称(ネイト)』の管理画面と、
仲間である『三人称(アルカステラ)』の管理画面を明確に分けてるってことか。
理にかなってるぜ!」
アルカステラの画像登録(20:30-20:35)

「それじゃあ、まずはアルカステラたちの画像を登録していくわね。
ここでも、Claudeさんが作ってくれたアセット(スクリプト)を使って実装していくわ」

「Claude AI、大活躍じゃねぇか。」


「今回作ってもらったのはどんなコードなんだ?」

「ざっと見た感じ、画像管理用のデータベースを作るためのコードみたいね。
せっかくだから、一緒に中身を見てみましょうか!」
// CompanionIconTable.cs
// 仲間キャラクターの画像アセットを一元管理する ScriptableObject
// 仲間一覧ボタン用アイコンと、詳細画面用立ち絵の2種類を companionId で引ける
using System;
using System.Collections.Generic;
using UnityEngine;
namespace TrainingGame
{
/// <summary>
/// 仲間キャラクターの画像アセットを管理する ScriptableObject。
///
/// 【作成方法】
/// Unity メニュー → Assets → Create → TrainingGame → CompanionIconTable
/// 作成したアセットの entries に仲間IDと画像を登録する。
///
/// 【参照方法】
/// StatusPanel の Inspector フィールド "iconTable" にアセットをアサインする。
/// StatusPanel が CompanionListButton に渡すので、他のクラスは直接持たなくてよい。
///
/// 【画像サイズの目安】
/// iconSprite : 128×128px 程度(仲間一覧ボタン内のサムネイル)
/// portraitSprite: 512×512px 程度(詳細画面の立ち絵・バスト)
///
/// 【仲間を追加するときの手順】
/// 1. CompanionRoster.CreateDefault() に CompanionData を追加する
/// 2. CompanionProfileTable にプロフィールを追加する
/// 3. このテーブルの entries にエントリーを追加して画像をアサインする
/// </summary>
[CreateAssetMenu(menuName = "TrainingGame/CompanionIconTable", fileName = "CompanionIconTable")]
public class CompanionIconTable : ScriptableObject
{
// ================================================================
// エントリー定義
// ================================================================
/// <summary>仲間1人分の画像セット</summary>
[Serializable]
public class Entry
{
/// <summary>CompanionData.companionId と一致させる</summary>
public string companionId;
/// <summary>
/// 仲間一覧ボタン用のアイコン画像。
/// 未設定の場合はボタンの Image が非表示になる。
/// </summary>
public Sprite iconSprite;
/// <summary>
/// 詳細画面用の立ち絵(バストアップなど)。
/// 未設定の場合は詳細画面の Image が非表示になる。
/// </summary>
public Sprite portraitSprite;
}
// ================================================================
// Inspector フィールド
// ================================================================
[Tooltip("仲間IDと画像の対応テーブル。仲間を追加するたびにエントリーを追加すること。")]
public List<Entry> entries = new List<Entry>();
// ================================================================
// 取得ユーティリティ
// ================================================================
/// <summary>
/// companionId に対応するアイコン画像を返す。
/// 未登録または画像未設定の場合は null を返す。
/// </summary>
public Sprite GetIcon(string companionId)
{
var entry = Find(companionId);
return entry?.iconSprite;
}
/// <summary>
/// companionId に対応する立ち絵画像を返す。
/// 未登録または画像未設定の場合は null を返す。
/// </summary>
public Sprite GetPortrait(string companionId)
{
var entry = Find(companionId);
return entry?.portraitSprite;
}
// ================================================================
// 内部処理
// ================================================================
private Entry Find(string companionId)
{
if (string.IsNullOrEmpty(companionId)) return null;
foreach (var entry in entries)
{
if (entry.companionId == companionId)
return entry;
}
Debug.LogWarning($"[CompanionIconTable] 画像エントリー未登録: '{companionId}'");
return null;
}
}
}

「Unityでこういったデータ管理用のアセットを作るには『ScriptableObject』っていうのを使うのね。
各キャラクターのIDとSprite(画像データ)をインスペクター上でセットして、
IDで検索をかけて、それをボタンや詳細画面のUIに貼り付ける仕組みになってるわ。
エラーを回避するために、IDがnull(空っぽ)だったり存在しなかったりする場合のセーフティネットもちゃんと張ってある。
本当に優秀ね。」

「ネイトはこういうコード、自分でも書けそうか?」

「無理ね。
昔、自分でUnityのC#をゴリゴリ書いてた頃なら、頑張れば書けたのかもしれないけど……もう何も見ずにゼロからコードを組む力はほとんどなくなっちゃったわ。
今はこうして、書いてもらったコードを『査読(レビュー)』するので精一杯よ。
Qiitaで色んなコードの切れ端を探してはマネしてた、あの頃が懐かしいわ……。」

「そっか。
まぁ、それだけAIが頼もしい、便利な世の中になったってことだな!」


「ええ、そうね。
とりあえず、最初に仲間になる『ベンヌ』の画像を登録しておくわ。
今後アルカステラが増えるたびに、ここにデータを追加していくのを忘れないようにしないとね!」
ネイト専用ステータス画面のスクリプトを作成(20:35-20:40)

「次に、私専用のステータス画面を表示するためのスクリプトを作っていくわ。
ベースになる仲間用のスクリプトが既にあるから、それをちょっと改変するだけでパパッと終わらせちゃうわね!」
// NateStatusPanel.cs
using UnityEngine;
using TMPro;
namespace TrainingGame
{
/// <summary>
/// 主人公ネイト専用のステータス表示パネル
/// </summary>
public class NateStatusPanel : MonoBehaviour
{
[Header("基本情報")]
[SerializeField] private TMP_Text nameText;
[SerializeField] private TMP_Text genkiText;
[SerializeField] private TMP_Text yarukiText;
[Header("基礎ステータス")]
[SerializeField] private TMP_Text maxHpText;
[SerializeField] private TMP_Text atkText;
[SerializeField] private TMP_Text defText;
[SerializeField] private TMP_Text magicText;
[SerializeField] private TMP_Text magicDefText;
[SerializeField] private TMP_Text speedText;
[SerializeField] private TMP_Text moveRangeText;
[SerializeField] private TMP_Text dexterityText;
[SerializeField] private TMP_Text luckText;
/// <summary>
/// パネルを表示し、データを反映する
/// </summary>
public void Show(UnitBaseStats stats, int genki, int yaruki)
{
if (nameText != null) nameText.text = "ネイト";
if (genkiText != null) genkiText.text = $"元気: {genki}";
if (yarukiText != null) yarukiText.text = $"やる気: {yaruki}";
RefreshStats(stats);
gameObject.SetActive(true);
}
private void RefreshStats(UnitBaseStats s)
{
if (s == null) return;
SetStatText(maxHpText, "最大HP", s.maxHp);
SetStatText(atkText, "ATK", s.atk);
SetStatText(defText, "DEF", s.def);
SetStatText(magicText, "魔力", s.magic);
SetStatText(magicDefText, "耐魔力", s.magicDef);
SetStatText(speedText, "スピード", s.speed);
SetStatText(moveRangeText, "移動力", s.moveRange);
SetStatText(dexterityText, "器用さ", s.dexterity);
SetStatText(luckText, "運", s.luck);
}
private void SetStatText(TMP_Text target, string label, int value)
{
if (target != null) target.text = $"{label} {value}";
}
public void OnButtonClose()
{
gameObject.SetActive(false);
}
}
}
「画面上にある各ステータスのテキスト枠を取得して、
そこに『SetStatText』という処理で『ATK 〇〇』みたいに順番にデータを流し込んでいるだけのシンプルな作りね。」

「なるほどな!すでにある仕組みを上手く使い回して効率よく作るのは、開発の基本だぜ!」
仲間用の詳細画面の作成(20:40-21:00)

「次は、この前作りかけた『仲間用の詳細画面』を仕上げていくわ」


「ちょっと雑に配置したけど、まぁ※モックテストとしては上出来ね!」
💡開発Tips:モックテスト(モックアップ)とは?
本格的なデザインやグラフィックを作り込む前に、仮の枠組みや文字を使って「画面のレイアウト」や「大まかな動作」に問題がないかを確認するテストのことです。
ゲーム開発では、まずはこうして「見た目の骨組み」を作ってから中身を詰めていきます!

「おっ、いい感じじゃねぇか。
……ん?でも、右側の空白のスクロールビューは何だ?」

「あぁ、そこには将来キャラクターの『スキル情報』が一覧で載る予定なの。
でも、スキル自体の制作はまだまだ先になるから、今は空っぽのままで気にしないで良いわ!」
ネイト専用の詳細画面の作成(21:00-21:05)


「次は、私(ネイト)専用の詳細画面の作成ね。
これは、さっき作った仲間用のUIをそのままコピーして、少しだけ修正するだけで完成よ!」

「ほら、さっき説明した通り、
私の画面の方には『好感度』の代わりに『元気』と『やる気』のパラメーターが表示されているでしょ?」
各UIにアタッチ(21:05-21:15)

「それじゃあ、今作ったUIを動かすために各UIオブジェクトのコンポーネントを設定していくわ。」

「いよいよ、仕上げか!
結構、この作業でミスることが多いから気を付けろよな!」

「分かってるわ。
ええと、ここにこれを入れて…」
10分後

「出来たわ!これで完成よ!」

「よくやったぜ!ネイト!」
休憩システムの導入(21:15-21:45)

「まだ、時間があるからパパっと新しい機能を作っちゃうわね。」

「おっ、どんな機能だ?」

「『特訓』とくれば、次は『休憩』よ!
これさえ作っちゃえば、特訓して、疲れたら休むっていう育成ゲームの基本サイクル(ループ)が完成するわ!」

「それは絶対に必要な機能だが……
今から新しいシステムをイチから作ってる余裕なんてあるのか?」

「ふふん、安心して!
システムを動かす裏側のコード自体は、もう作ってもらってあるの!」

「…いつの間に」

「さっきの作業の合間に、ちょちょっとClaudeさんに頼んでおいたのよ!
それじゃあ、この出来立てホヤホヤのシステムを早速プロジェクトに導入していくわ!」
宴に設定

「裏側のスクリプトのセットアップも終わったし、
あとは『宴』のExcelファイルに表示の処理を追加するわね」


「なるほど。
回復量の計算とかは裏のシステムに任せて、
あくまで『宴』はシステムメッセージや結果を表示させることに特化させるんだな」

「ええ、その通りよ!
一番良く触るこのExcelファイルに、複雑な処理や情報を詰め込みすぎちゃうと、後から読みにくくなっちゃうからね。
役割分担が大事!
よし、これで『休憩』コマンドの実装も完了よ!」
戦闘システム(GridMapの確認)(20:45-21:00)

「残りの時間は、いよいよ戦闘システムの実装準備に取り掛かるわよ!」

「ついに来たか……!
そういえば、裏でClaudeさんに色々システムを作ってもらってたんだっけか?」

「そうよ。
本当は皆さんに全部お見せしたいのだけど……ごめんね。
ファイル数が多すぎて、今の段階ではとても解説しきれないの。
だから今回は、作ってもらった『マップ生成と移動範囲のロジック』が正しく動くか、テストしていくわね」

「どんなテストをするんだ?」

「Unityの『Gizmos(ギズモ)』っていう機能を使って、
開発者専用のシーンビュー(作業画面)にマス目のタイルを表示させるテストよ」

「ギズモ?」

「簡単に言うと、開発者だけが見える『補助線の可視化機能』ね。
文字だけのコンソール画面と違って、ゲーム画面に図形や色を重ねて視覚的にテストできる便利な機能みたいなの!」

「ほーん、それで今回はどんなテストをするんだ?」

「先にテスト用のコードをお見せするわね」
using System.Collections.Generic;
using UnityEngine;
using TrainingGame.Battle;
namespace TrainingGame.Test
{
/// <summary>
/// バトルロジックの動作確認用プロトタイプクラス。
/// 空のGameObjectにアタッチして実行します。
/// </summary>
public class BattlePrototype : MonoBehaviour
{
private GridMap _map;
private List<UnitBattleState> _players;
private List<UnitBattleState> _enemies;
private HashSet<TileData> _reachableTiles;
void Start()
{
// ==========================================
// 1. マップの生成 (例: 横10 x 縦8)
// ==========================================
_map = new GridMap(10, 8);
// ==========================================
// 2. 地形(障害物など)のセットアップ
// ==========================================
// 中央付近に山岳(通行コスト大)や通行不可タイルを配置
_map.SetTile(4, 3, TileType.Mountain);
_map.SetTile(5, 3, TileType.Mountain);
_map.SetTile(4, 4, TileType.Forest);
_map.SetTile(5, 4, TileType.Impassable); // 通行不可
// ==========================================
// 3. ユニットの配置データを作成
// ==========================================
var nateStats = UnitBaseStats.CreateNate();
// テスト用の敵ステータス
var enemyStats = new UnitBaseStats
{
unitName = "ゴブリン",
maxHp = 30,
atk = 10,
def = 5,
moveRange = 3
};
var entries = new List<UnitPlacer.PlacementEntry>
{
new UnitPlacer.PlacementEntry { BaseStats = nateStats, X = 1, Y = 1, IsPlayerUnit = true },
new UnitPlacer.PlacementEntry { BaseStats = enemyStats, X = 8, Y = 5, IsPlayerUnit = false }
};
// 配置前検証
if (UnitPlacer.ValidatePlacements(_map, entries))
{
// ==========================================
// 4. マップへの配置実行
// ==========================================
(_players, _enemies) = UnitPlacer.PlaceAll(_map, entries);
// コンソールにマップのAA(アスキーアート)を出力
_map.DebugPrint();
}
// ==========================================
// 5. 経路探索(PathFinder)のテスト
// ==========================================
if (_players.Count > 0)
{
var nate = _players[0];
_reachableTiles = PathFinder.GetReachableTiles(_map, nate, _players, _enemies);
Debug.Log($"[Test] {nate.BaseStats.unitName} の移動可能マス数: {_reachableTiles.Count}");
}
}
// ==========================================
// おまけ: Sceneビューにグリッドを可視化する
// ==========================================
private void OnDrawGizmos()
{
if (_map == null) return;
float tileSize = 1.0f;
Vector3 offset = new Vector3(tileSize / 2, tileSize / 2, 0);
for (int x = 0; x < _map.Width; x++)
{
for (int y = 0; y < _map.Height; y++)
{
var tile = _map.GetTile(x, y);
Vector3 pos = new Vector3(x * tileSize, y * tileSize, 0) + offset;
// 地形ごとの色設定
Gizmos.color = tile.TileType switch
{
TileType.Plain => Color.white,
TileType.Forest => Color.green,
TileType.Mountain => new Color(0.6f, 0.4f, 0.2f), // 茶色
TileType.Impassable => Color.black,
_ => Color.white
};
// ユニットがいる場合は色を上書き
if (tile.HasUnit)
{
Gizmos.color = _players.Contains(tile.OccupyingUnit) ? Color.blue : Color.red;
}
// ワイヤーフレームでマス目を描画
Gizmos.DrawWireCube(pos, new Vector3(tileSize, tileSize, 0.1f));
// 移動可能範囲なら半透明の青で塗りつぶし
if (_reachableTiles != null && _reachableTiles.Contains(tile))
{
Gizmos.color = new Color(0, 1, 1, 0.3f);
Gizmos.DrawCube(pos, new Vector3(tileSize, tileSize, 0.1f));
}
// ユニットがいる場所は小さく塗りつぶし
else if (tile.HasUnit)
{
Gizmos.DrawCube(pos, new Vector3(tileSize * 0.8f, tileSize * 0.8f, 0.1f));
}
else if (tile.TileType != TileType.Plain)
{
// 特殊地形も薄く塗りつぶし
Gizmos.color = new Color(Gizmos.color.r, Gizmos.color.g, Gizmos.color.b, 0.3f);
Gizmos.DrawCube(pos, new Vector3(tileSize, tileSize, 0.1f));
}
}
}
}
}
}
「今回は、10×8の大きさで『マス目状(Grid)』のフィールドを生成して、そこに山岳や森、通行不可の障害物を配置するの。
さらに、テスト用の敵と自陣のユニットを配置して、
『自陣のユニットがどこまで移動できるか』を視覚的に確認できればテストはOKよ!」

「なんだか難しそうだが……要するに、
簡易的なマップと陣取りが画面に表示されれば成功ってことか?」

「そうね!それじゃあ、早速実行してみましょうか!」


「出来たわ!成功よ!」

「……えーっと、なんかしょぼくないか?
ただの四角いブロックの集まりじゃねぇか」

「しょぼくないわよ!
これってゲーム開発においてはもの凄い進歩なのよ!?
ほら、この緑のマスがあるでしょう?
これは、プログラムが正しくここを『森』だと認識している証拠なの!
茶色は『山岳』。
赤は敵ユニットの『ゴブリン』。
そして広範囲に広がっている水色は、
『今の私が歩ける移動範囲』を正確に計算して表しているのよ!」

「えーっと、つまり……?」

「これはいわば、プログラムの頭の中(思考回路)を覗き見している状態なの!
裏側でこれだけ完璧に盤面を計算できているんだから、
あとはこの四角いマス目一つ一つに、
ちゃんとしたイラスト素材を入れてあげれば……どうなると思う?」

「!? そうか!
あとは中身の絵を差し替えるだけで、
シミュレーションRPGの本格的なバトルマップが完成するのか!!」

「ふふ、やっと分かってくれたみたいね!
……とはいえ、正直私もどうしてAIがこんなに完璧な動きをするコードを一瞬で作れたのか、完全に理解できているわけじゃないの。
空いてる時間に作ってもらったコードをしっかり勉強して、
後でみんなにも分かりやすく説明するわね!」
■いざ、3ターン通しテスト!

「さて、今回は色んな機能を一気に実装しちゃったから、せっかくだから3ターンだけ、実際のゲームの流れで遊んでテストしてみるわね」

「おっ、ついに通しテストか!ワクワクするぜ!」
■実装部分のテスト(3分くらい)
■3ターン通しテスト
少し長めの動画ですが、
実際の動きを見ていただけるとゲームらしさが増してきたのが伝わるかと思います!

「おぉーっ!
これ本当に一週間で作ったのか!?すげぇぜ、ネイト!」

「うーん…」

「なんだよ。
システムは上手く動いたんだから、もっと素直に喜べよ!」

「まず、文章が長すぎるわ。
無駄な描写やセリフが多い。
これだと、いくらスキップ機能があるとはいえプレイヤーが退屈しちゃう。
それに、日を跨いで少しずつ書いたせいで、
テキストのテンポや語尾のニュアンスが日によって若干違っているわね。
あとは演出面。
SEやエフェクトをもっと活用すべきだし、一部キャラクターの表示タイミングがおかしい箇所もある。
シーンに合わせてBGMももっと細かく切り替えないと……あとは……」

「ちょちょちょ、ネイトさん!
ストップ、ストップ!!
俺たち、『1か月の期限』で作ってるんだぜ!?
そんなに最初から完璧を求めてたら、一生ゲームが完成しないぞ!」

「……そうね。
ポマイラの言う通りだわ。つい熱が入っちゃった」

「気になるところは、
また手が空いた時のブラッシュアップ期間に検討するとして……
今日の開発目標としては、これで無事完了よ!」

「おうよ!これでまた一歩完成に近づいたな。今日もお疲れ、ネイト!」
みんな!力を貸して!ネイトのスキルを考えよう

「そうそう、最後にまたみんなの力を貸して欲しいの!」

「おっ、アンケートかアイディア出しか?
今回は何を募集するんだ?」

「私(ネイト)が戦闘で使う『スキル』を、みんなに考えて欲しいの!」

「戦闘スキルか…。
具体的にはどんなアイディアが欲しいんだ?」

「そうねぇ…攻撃、サポート、回復…基本的には何でもアリよ!」

「カッコいい王道の技から、クスッと笑える面白い名前や効果まで、
皆さんの自由なアイディアがどんどん欲しいの!
またいつものようにに、こんな感じで書いてもらえると嬉しいわ」
【スキル募集テンプレート】
・スキル名:ファイアボール
・射程:遠くまで(※「2マス先」などの表現でもOK!)
・効果:対象に小ダメージ

「なるほどな。書き込む時の期日はどうする?」

「今回は、4/28(火) を締め切りにさせてもらうわね」

「だそうだぜ!おまえら、遠慮は無用だ!
ドンドンすっげぇスキルをネイトに考えてやってくれよな!
アイディアは下のRunPenを押して書いてくれよな!
待ってるぜ!」
最後に
かくして、第2週目の初日から怒涛の勢いでシステムを積み上げていったネイト。
さまざまな機能を構築していく中で、
限られた「2時間」という開発枠にも徐々に順応し、確かな手応えを掴み始めている。
極めつけは、Claudeさんの力を借りて密かに仕込んでいた、戦闘システムのGridMap生成ロジック。
Gizmo上に浮かび上がった色とりどりのマス目は、一見するとただの四角い集合体かもしれない。
けれどそれは紛れもなく、シミュレーションRPGの心臓部が確かに脈打ち始めた証なのだ。
今日作り上げたGridMapという土台の上で、果たしてどんな熱きバトルが展開されるのか!?
そして、未知の領域である「シミュレーションRPGの開発」という最大の壁に、ネイトはどう挑んでいくのか!?
次回も、どうぞ乞うご期待!
#プロジェクトNeith #HorizonNotes #ゲーム制作 #個人開発 #Unity #開発日記 #読者参加型 #毎日note