はじめに
前回は、プレハブの作成を行いました。
今回は、スクリプトを作成します。
BattleManager.csの作成
新しく「BattleManager」という名前のスクリプトを作成してください。
その後、長めのコードですが以下をコピペしてください。
//BattleManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;
using static UnityEngine.GraphicsBuffer;
public class BattleManager : MonoBehaviour
{
public static BattleManager Instance { get; private set; }
[Header("UI (assign in inspector)")]
public GameObject battleRoot;
public Text messageText;
public RectTransform playerStatusPanel;
public GameObject playerStatusEntryPrefab;
public RectTransform enemiesContainer;
public RectTransform alliesContainer;
public GameObject actionMenuPanel;
public Text[] actionOptions;
public GameObject magicMenuPanel;
public RectTransform magicGridContainer;
public GameObject itemMenuPanel;
public RectTransform itemGridContainer;
public GameObject targetMenuPanel;
public RectTransform targetListContainer;
[Header("Prefabs")]
public GameObject enemyPortraitPrefab;
public GameObject allyPortraitPrefab;
public GameObject menuOptionPrefab;
public GameObject magicArrowPrefab;
[Header("Settings")]
public float defaultWaitAfterMessage = 0.25f;
bool inBattle = false;
public bool InBattle => inBattle;
bool fledThisBattle = false;
List<BattleEnemy> enemies = new List<BattleEnemy>();
List<PlayerStats> allies = new List<PlayerStats>();
Dictionary<PlayerStats, PlayerAction> pendingActions = new Dictionary<PlayerStats, PlayerAction>();
Dictionary<PlayerStats, PlayerStatusUI> statusUIs = new Dictionary<PlayerStats, PlayerStatusUI>();
Coroutine hudCoroutine = null;
class BattleEnemy
{
public EnemyData data;
public int hp;
public int mp;
public int id;
public string displayName;
public GameObject visualGO;
}
public enum ActionType { Attack, Magic, Item, Flee, None }
public class PlayerAction
{
public ActionType type = ActionType.None;
public MagicData magic = null;
public ItemData item = null;
public object target = null;
}
void Awake()
{
if (Instance != null && Instance != this) Destroy(this.gameObject);
else Instance = this;
if (battleRoot != null) battleRoot.SetActive(false);
}
public void StartBattleWithEnemyDatas(List<EnemyData> enemyDatas)
{
if (inBattle) return;
StartCoroutine(BattleRoutine(enemyDatas));
}
IEnumerator BattleRoutine(List<EnemyData> enemyDatas)
{
inBattle = true;
if (battleRoot != null) battleRoot.SetActive(true);
if (playerStatusPanel != null) playerStatusPanel.gameObject.SetActive(true);
if (enemiesContainer != null) enemiesContainer.gameObject.SetActive(true);
if (alliesContainer != null) alliesContainer.gameObject.SetActive(true);
if (magicMenuPanel != null) magicMenuPanel.SetActive(false);
if (itemMenuPanel != null) itemMenuPanel.SetActive(false);
if (targetMenuPanel != null) targetMenuPanel.SetActive(false);
if (actionMenuPanel != null) actionMenuPanel.SetActive(false);
fledThisBattle = false;
allies = (PartyManager.Instance != null)
? PartyManager.Instance.partyMembers.Where(p => p != null).ToList()
: new List<PlayerStats>();
enemies.Clear();
if (enemiesContainer != null) foreach (Transform t in enemiesContainer) Destroy(t.gameObject);
if (alliesContainer != null) foreach (Transform t in alliesContainer) Destroy(t.gameObject);
Dictionary<string, int> nameCounter = new Dictionary<string, int>();
int id = 0;
foreach (var ed in enemyDatas)
{
if (ed == null) continue;
string baseName = ed.enemyName;
if (!nameCounter.ContainsKey(baseName))
nameCounter[baseName] = 0;
int index = nameCounter[baseName]++;
char suffix = (char)('A' + index);
string displayName = $"{baseName}{suffix}";
var be = new BattleEnemy()
{
data = ed,
hp = ed.maxHP,
mp = ed.maxMP,
id = id++,
displayName = displayName
};
enemies.Add(be);
if (enemiesContainer == null) continue;
GameObject go = null;
if (ed.portraitPrefab != null)
{
go = Instantiate(ed.portraitPrefab, enemiesContainer, false);
}
else if (enemyPortraitPrefab != null)
{
go = Instantiate(enemyPortraitPrefab, enemiesContainer, false);
var img = go.GetComponentInChildren<Image>();
if (img != null && ed.portraitSprite != null)
img.sprite = ed.portraitSprite;
}
if (go != null)
{
go.name = $"Enemy_{displayName}";
var txt = go.GetComponentInChildren<Text>();
if (txt != null)
txt.text = displayName;
ForceFixUI(go, new Vector2(180, 180));
LayoutRebuilder.ForceRebuildLayoutImmediate(enemiesContainer);
be.visualGO = go;
}
}
if (alliesContainer != null)
{
alliesContainer.gameObject.SetActive(true);
var layout = alliesContainer.GetComponent<HorizontalLayoutGroup>();
if (layout != null) layout.enabled = false;
foreach (Transform t in alliesContainer)
Destroy(t.gameObject);
yield return null;
if (allyPortraitPrefab != null)
{
foreach (var a in allies)
{
var go = Instantiate(allyPortraitPrefab, alliesContainer, false);
go.name = $"Ally_{a.stats.charName}";
var txt = go.GetComponentInChildren<Text>();
if (txt != null)
txt.text = a.stats.charName;
Image img = null;
var portraitTf = go.transform.Find("PortraitImage");
if (portraitTf != null) img = portraitTf.GetComponent<Image>();
if (img == null) img = go.GetComponentInChildren<Image>();
if (img != null && a.stats.portrait != null)
{
img.sprite = a.stats.portrait;
img.SetNativeSize();
}
var rt = go.GetComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.localScale = Vector3.one;
rt.sizeDelta = new Vector2(160, 160);
}
}
yield return null;
if (layout != null) layout.enabled = true;
LayoutRebuilder.ForceRebuildLayoutImmediate(
alliesContainer.GetComponent<RectTransform>()
);
}
if (playerStatusPanel != null && playerStatusEntryPrefab != null)
{
foreach (Transform t in playerStatusPanel) Destroy(t.gameObject);
statusUIs.Clear();
foreach (var ally in allies)
{
var go = Instantiate(playerStatusEntryPrefab, playerStatusPanel, false);
ForceFixUI(go, new Vector2(320, 48));
var ui = go.GetComponent<PlayerStatusUI>();
if (ui != null)
{
ui.UpdateFrom(ally);
statusUIs[ally] = ui;
}
}
LayoutRebuilder.ForceRebuildLayoutImmediate(playerStatusPanel);
if (hudCoroutine != null) StopCoroutine(hudCoroutine);
hudCoroutine = StartCoroutine(UpdateHUDCoroutine());
}
yield return ShowMessage("敵と遭遇した!");
while (true)
{
if (fledThisBattle)
{
var sm = FindObjectOfType<SceneManager>();
if (sm != null)
{
var f = sm.GetType().GetField("suppressNextEncounter");
if (f != null) f.SetValue(sm, true);
}
yield return ShowMessage("うまくにげられた!");
break;
}
if (enemies.All(e => e.hp <= 0))
{
yield return ShowMessage("てきをたおした!");
int totalExp = enemies.Sum(e => e.data.giveExp);
int totalMoney = enemies.Sum(e => e.data.giveMoney);
foreach (var a in allies) a.GainExp(totalExp);
if (PartyManager.Instance != null) PartyManager.Instance.partyMoney += totalMoney;
foreach (var e in enemies)
{
foreach (var d in e.data.drops)
{
if (d.item != null && Random.value <= d.dropRate)
{
if (PartyManager.Instance != null) PartyManager.Instance.AddToBag(d.item, 1);
}
}
}
yield return ShowMessage($"戦闘終了。獲得経験値: {totalExp} お金: {totalMoney}");
break;
}
if (allies.All(a => !a.stats.IsAlive))
{
yield return ShowMessage("まけた...");
if (PartyManager.Instance != null) PartyManager.Instance.partyMoney /= 2;
foreach (var a in allies)
{
a.stats.hp = a.stats.maxHP;
a.stats.mp = a.stats.maxMP;
}
yield return ShowMessage("拠点へ戻された...");
break;
}
pendingActions.Clear();
yield return SelectActionsForAllAllies();
var order = new List<(int speed, bool isAlly, object actor)>();
foreach (var a in allies) if (a.stats.IsAlive) order.Add((a.stats.speed, true, (object)a));
foreach (var e in enemies) if (e.hp > 0) order.Add((e.data.speed, false, (object)e));
order = order.OrderByDescending(x => x.speed).ToList();
foreach (var entry in order)
{
if (entry.isAlly)
{
var actor = (PlayerStats)entry.actor;
if (actor.stats.IsAlive && pendingActions.ContainsKey(actor))
{
var act = pendingActions[actor];
yield return ExecuteAllyAction(actor, act);
}
}
else
{
var be = (BattleEnemy)entry.actor;
if (be.hp > 0)
yield return ExecuteEnemyAction(be);
}
if (enemies.All(e => e.hp <= 0) || allies.All(a => !a.stats.IsAlive)) break;
}
}
if (hudCoroutine != null) { StopCoroutine(hudCoroutine); hudCoroutine = null; }
statusUIs.Clear();
if (playerStatusPanel != null)
{
foreach (Transform t in playerStatusPanel) Destroy(t.gameObject);
}
if (battleRoot != null) battleRoot.SetActive(false);
inBattle = false;
yield break;
}
IEnumerator ShowMessage(string msg)
{
if (messageText != null) messageText.text = msg;
bool advanced = false;
while (!advanced)
{
if (Input.GetKeyDown(KeyCode.Space)) advanced = true;
yield return null;
}
yield return new WaitForSeconds(defaultWaitAfterMessage);
}
IEnumerator ShowMessageTimed(string msg, float minSeconds)
{
if (messageText != null) messageText.text = msg;
float t = 0f;
while (t < minSeconds)
{
if (Input.GetKeyDown(KeyCode.Space)) break;
t += Time.deltaTime;
yield return null;
}
yield break;
}
IEnumerator SelectActionsForAllAllies()
{
foreach (var ally in allies)
{
if (!ally.stats.IsAlive)
{
pendingActions[ally] = new PlayerAction() { type = ActionType.None };
continue;
}
yield return ShowActionMenuAndWait(ally);
}
}
IEnumerator UpdateHUDCoroutine()
{
while (inBattle)
{
// update each mapped UI
foreach (var kv in new List<KeyValuePair<PlayerStats, PlayerStatusUI>>(statusUIs))
{
var ps = kv.Key;
var ui = kv.Value;
if (ps == null || ui == null) continue;
ui.UpdateFrom(ps);
}
yield return null;
}
}
void ForceFixUI(GameObject go, Vector2 size)
{
if (go == null) return;
var rt = go.GetComponent<RectTransform>();
if (rt == null) return;
rt.localScale = Vector3.one;
rt.localRotation = Quaternion.identity;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
if (size.x > 0 && size.y > 0) rt.sizeDelta = size;
else rt.sizeDelta = Vector2.zero;
var img = go.GetComponent<Image>();
if (img != null) img.enabled = true;
foreach (Transform c in go.transform)
{
var crt = c.GetComponent<RectTransform>();
if (crt != null)
{
crt.localScale = Vector3.one;
crt.localRotation = Quaternion.identity;
}
}
}
IEnumerator ShowActionMenuAndWait(PlayerStats actor)
{
actionMenuPanel.SetActive(true);
if (actionOptions != null)
{
for (int i = 0; i < actionOptions.Length; i++)
{
var txt = actionOptions[i];
if (txt == null) continue;
var parent = txt.transform.parent;
if (parent == null) continue;
var dot = parent.Find("Dot");
if (dot != null) dot.gameObject.SetActive(false);
var parentImg = parent.GetComponent<Image>();
if (parentImg != null) parentImg.color = Color.clear;
}
}
int idx = 0;
UpdateActionMenuSelection(idx);
bool decided = false;
PlayerAction selectedAction = new PlayerAction();
while (!decided)
{
if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.LeftArrow))
{
idx = (idx - 1 + actionOptions.Length) % actionOptions.Length;
UpdateActionMenuSelection(idx);
}
else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.RightArrow))
{
idx = (idx + 1) % actionOptions.Length;
UpdateActionMenuSelection(idx);
}
else if (Input.GetKeyDown(KeyCode.Space))
{
yield return null;
string choice = actionOptions[idx].text;
if (choice.Contains("こうげき"))
{
actionMenuPanel.SetActive(false);
PlayerAction result = null;
yield return StartCoroutine(ShowTargetMenuForActor(actor, new PlayerAction() { type = ActionType.Attack }, true, (pa) => { result = pa; }));
if (result == null)
{
actionMenuPanel.SetActive(true);
UpdateActionMenuSelection(idx);
}
else
{
selectedAction = result;
decided = true;
}
}
else if (choice.Contains("まほう"))
{
PlayerAction result = null;
yield return StartCoroutine(ShowMagicMenuAndTarget(actor, (pa) => { result = pa; }));
if (result == null)
{
actionMenuPanel.SetActive(true);
UpdateActionMenuSelection(idx);
}
else
{
selectedAction = result;
decided = true;
actionMenuPanel.SetActive(false);
}
}
else if (choice.Contains("アイテム"))
{
PlayerAction result = null;
yield return StartCoroutine(ShowItemMenuAndTarget(actor, (pa) => { result = pa; }));
if (result == null)
{
actionMenuPanel.SetActive(true);
UpdateActionMenuSelection(idx);
}
else
{
selectedAction = result;
decided = true;
actionMenuPanel.SetActive(false);
}
}
else if (choice.Contains("にげる") || choice.Contains("逃げる"))
{
bool success = AttemptEscape(actor);
if (success)
{
selectedAction = new PlayerAction() { type = ActionType.Flee };
decided = true;
actionMenuPanel.SetActive(false);
}
else
{
yield return ShowMessage("にげられなかった!");
// remain in action selection
actionMenuPanel.SetActive(true);
UpdateActionMenuSelection(idx);
}
}
}
else if (Input.GetKeyDown(KeyCode.Escape))
{
selectedAction = new PlayerAction() { type = ActionType.None };
decided = true;
actionMenuPanel.SetActive(false);
}
yield return null;
}
pendingActions[actor] = selectedAction;
actionMenuPanel.SetActive(false);
yield return null;
}
void UpdateActionMenuSelection(int idx)
{
if (actionOptions == null || actionOptions.Length == 0) return;
for (int i = 0; i < actionOptions.Length; i++)
{
var txt = actionOptions[i];
if (txt == null) continue;
var parent = txt.transform.parent;
if (parent != null)
{
var parentImg = parent.GetComponent<Image>();
if (parentImg != null) parentImg.color = (i == idx) ? new Color(1f, 1f, 1f, 0.6f) : Color.clear;
var dotTf = parent.Find("Dot");
if (dotTf != null)
{
dotTf.gameObject.SetActive(i == idx);
}
}
}
}
IEnumerator ShowMagicMenuAndTarget(PlayerStats actor, System.Action<PlayerAction> onComplete)
{
if (magicArrowPrefab == null || menuOptionPrefab == null || magicGridContainer == null)
{
Debug.LogWarning("ShowMagicMenuAndTarget: missing prefab or container assignments.");
onComplete(null);
yield break;
}
magicMenuPanel.SetActive(true);
foreach (Transform t in magicGridContainer) Destroy(t.gameObject);
var known = actor.stats.knownMagic;
const int COLS = 3;
const int ROWS = 4;
const int EXTRA_ROWS = 1;
const int TOTAL_ROWS = ROWS + EXTRA_ROWS;
const int PAGE_ITEMS = COLS * ROWS;
int totalItems = known != null ? known.Length : 0;
int itemsPerPage = PAGE_ITEMS;
int totalPages = Mathf.Max(1, Mathf.CeilToInt((float)totalItems / itemsPerPage));
int page = 0;
System.Func<int, bool> SlotIsActive = (i) =>
{
if (i < 0 || i >= magicGridContainer.childCount) return false;
var txt = magicGridContainer.GetChild(i).GetComponentInChildren<UnityEngine.UI.Text>();
return txt != null && txt.gameObject.activeSelf;
};
System.Action<int> buildPage = (p) =>
{
foreach (Transform t in magicGridContainer) Destroy(t.gameObject);
int startItemIndex = p * itemsPerPage;
int placed = 0;
for (int r = 0; r < ROWS; r++)
{
for (int c = 0; c < COLS; c++)
{
int globalIdx = startItemIndex + placed;
var go = Instantiate(menuOptionPrefab, magicGridContainer);
var txt = go.GetComponentInChildren<UnityEngine.UI.Text>();
if (globalIdx < totalItems)
{
if (txt != null)
{
txt.gameObject.SetActive(true);
txt.text = $"{known[globalIdx].magicName} (MP {known[globalIdx].mpCost})";
}
}
else
{
if (txt != null) txt.gameObject.SetActive(false);
}
var dot = go.transform.Find("Dot");
if (dot != null) dot.gameObject.SetActive(false);
placed++;
}
}
for (int c = 0; c < COLS; c++)
{
if (c == 0 && p > 0)
{
var go = Instantiate(magicArrowPrefab, magicGridContainer);
var txt = go.GetComponentInChildren<UnityEngine.UI.Text>();
if (txt != null) { txt.gameObject.SetActive(true); txt.text = "↩"; }
var marker = go.GetComponent<MagicPageArrow>();
if (marker != null) marker.isNext = false;
}
else if (c == COLS - 1 && p < totalPages - 1)
{
var go = Instantiate(magicArrowPrefab, magicGridContainer);
var txt = go.GetComponentInChildren<UnityEngine.UI.Text>();
if (txt != null) { txt.gameObject.SetActive(true); txt.text = "↪"; }
var marker = go.GetComponent<MagicPageArrow>();
if (marker != null) marker.isNext = true;
}
else
{
var go = Instantiate(menuOptionPrefab, magicGridContainer);
var txt = go.GetComponentInChildren<UnityEngine.UI.Text>();
if (txt != null) txt.gameObject.SetActive(false);
var dot = go.transform.Find("Dot");
if (dot != null) dot.gameObject.SetActive(false);
}
}
};
buildPage(page);
yield return null;
int gridCount = TOTAL_ROWS * COLS;
int idx = 0;
{
int found = -1;
for (int i = 0; i < gridCount; i++) if (SlotIsActive(i)) { found = i; break; }
idx = (found >= 0) ? found : 0;
}
bool decided = false;
PlayerAction selected = null;
System.Func<int, int> GetRow = (i) => i / COLS;
System.Func<int, int> GetCol = (i) => i % COLS;
while (!decided)
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
int r = GetRow(idx), c = GetCol(idx);
int nr = Mathf.Max(0, r - 1);
int cand = nr * COLS + c;
while (cand >= 0 && !SlotIsActive(cand))
{
nr--;
if (nr < 0) break;
cand = nr * COLS + c;
}
if (cand >= 0 && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
int r = GetRow(idx), c = GetCol(idx);
if (r == ROWS - 1)
{
int arrowRowStart = ROWS * COLS;
if (c == 0)
{
int prevIdx = arrowRowStart + 0;
if (prevIdx < magicGridContainer.childCount && SlotIsActive(prevIdx))
{
idx = prevIdx;
goto postNav;
}
}
if (c == COLS - 1)
{
int nextIdx = arrowRowStart + (COLS - 1);
if (nextIdx < magicGridContainer.childCount && SlotIsActive(nextIdx))
{
idx = nextIdx;
goto postNav;
}
}
}
else
{
int nr = Mathf.Min(TOTAL_ROWS - 1, r + 1);
int cand = nr * COLS + c;
while (cand < TOTAL_ROWS * COLS && !SlotIsActive(cand))
{
nr++;
if (nr >= TOTAL_ROWS) break;
cand = nr * COLS + c;
}
if (cand < TOTAL_ROWS * COLS && SlotIsActive(cand)) idx = cand;
}
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
int row = GetRow(idx);
int startRow = row * COLS;
int cand = idx - 1;
while (cand >= startRow && !SlotIsActive(cand)) cand--;
if (cand >= startRow && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
int row = GetRow(idx);
int startRow = row * COLS;
int endRow = startRow + (COLS - 1);
int cand = idx + 1;
while (cand <= endRow && cand < TOTAL_ROWS * COLS && !SlotIsActive(cand)) cand++;
if (cand <= endRow && cand < TOTAL_ROWS * COLS && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.Space))
{
yield return null;
var selGo = magicGridContainer.GetChild(idx);
var arrow = selGo.GetComponent<MagicPageArrow>();
var txt = selGo.GetComponentInChildren<UnityEngine.UI.Text>();
if (arrow != null)
{
if (arrow.isNext && page < totalPages - 1)
{
page++;
buildPage(page);
yield return null;
int found = -1;
for (int i = 0; i < TOTAL_ROWS * COLS; i++) if (SlotIsActive(i)) { found = i; break; }
idx = (found >= 0) ? found : 0;
}
else if (!arrow.isNext && page > 0)
{
page--;
buildPage(page);
yield return null;
int found = -1;
for (int i = 0; i < TOTAL_ROWS * COLS; i++) if (SlotIsActive(i)) { found = i; break; }
idx = (found >= 0) ? found : 0;
}
}
else
{
int topAreaIdx = idx;
if (topAreaIdx >= ROWS * COLS) { /* bottom row or placeholder clicked */ }
int globalIdx = page * itemsPerPage + topAreaIdx;
if (globalIdx >= 0 && globalIdx < totalItems)
{
var chosen = known[globalIdx];
if (actor.stats.mp < chosen.mpCost)
{
StartCoroutine(ShowMessage("MPが足りない!"));
}
else
{
var pa = new PlayerAction() { type = ActionType.Magic, magic = chosen };
PlayerAction targetResult = null;
yield return StartCoroutine(ShowTargetMenuForActor(actor, pa, true, (res) => { targetResult = res; }));
if (targetResult == null)
{
buildPage(page);
yield return null;
int found = -1;
for (int i = 0; i < TOTAL_ROWS * COLS; i++) if (SlotIsActive(i)) { found = i; break; }
idx = (found >= 0) ? found : 0;
}
else
{
selected = targetResult;
decided = true;
magicMenuPanel.SetActive(false);
}
}
}
}
}
else if (Input.GetKeyDown(KeyCode.Escape))
{
magicMenuPanel.SetActive(false);
onComplete(null);
yield break;
}
postNav:
for (int i = 0; i < magicGridContainer.childCount; i++)
{
var child = magicGridContainer.GetChild(i);
var t = child.GetComponentInChildren<UnityEngine.UI.Text>();
if (t != null) t.color = (i == idx && t.gameObject.activeSelf) ? Color.yellow : Color.white;
var dotTf = child.Find("Dot");
if (dotTf != null) dotTf.gameObject.SetActive(i == idx && t != null && t.gameObject.activeSelf);
}
yield return null;
}
onComplete(selected);
}
IEnumerator ShowItemMenuAndTarget(PlayerStats actor, System.Action<PlayerAction> onComplete)
{
itemMenuPanel.SetActive(true);
foreach (Transform t in itemGridContainer) Destroy(t.gameObject);
const int COLS = 2;
const int ROWS = 7;
const int SLOTS = COLS * ROWS; // 14
var inv = actor?.Inventory;
if (inv == null)
{
inv = actor.GetComponent<CharacterInventory>();
if (inv == null)
{
Debug.LogWarning($"ShowItemMenuAndTarget: actor {actor?.name ?? "null"} に CharacterInventory が見つかりません。");
inv = new CharacterInventory();
}
}
int invCount = (inv != null) ? inv.slots.Count : 0;
for (int i = 0; i < SLOTS; i++)
{
var go = Instantiate(menuOptionPrefab, itemGridContainer);
go.name = $"ItemSlot_{i}";
var txt = go.GetComponentInChildren<Text>();
InventorySlot slot = (i < invCount) ? inv.slots[i] : null;
if (slot != null && slot.item != null && slot.item.usableInBattle && slot.count > 0)
{
if (txt != null)
{
txt.gameObject.SetActive(true);
txt.text = $"{slot.item.itemName} x{slot.count}";
}
}
else
{
if (txt != null) txt.gameObject.SetActive(false);
}
var dot = go.transform.Find("Dot");
if (dot != null) dot.gameObject.SetActive(false);
}
bool any = false;
if (inv != null)
{
foreach (var s in inv.slots)
{
if (s != null && s.item != null && s.item.usableInBattle && s.count > 0) { any = true; break; }
}
}
if (!any)
{
yield return ShowMessageTimed("戦闘で使えるアイテムがない。", 1.0f);
itemMenuPanel.SetActive(false);
onComplete(new PlayerAction() { type = ActionType.None });
yield break;
}
int idx = 0;
System.Func<int, bool> SlotIsActive = (i) =>
{
if (i < 0 || i >= itemGridContainer.childCount) return false;
var t = itemGridContainer.GetChild(i).GetComponentInChildren<Text>();
return t != null && t.gameObject.activeSelf;
};
if (!SlotIsActive(idx))
{
int f = -1;
for (int i = 0; i < SLOTS; i++) if (SlotIsActive(i)) { f = i; break; }
if (f >= 0) idx = f;
}
bool decided = false;
PlayerAction selected = null;
while (!decided)
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
int r = idx / COLS;
int c = idx % COLS;
int nr = Mathf.Max(0, r - 1);
int cand = nr * COLS + c;
while (cand >= 0 && !SlotIsActive(cand))
{
nr--;
if (nr < 0) break;
cand = nr * COLS + c;
}
if (cand >= 0 && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
int r = idx / COLS;
int c = idx % COLS;
int nr = Mathf.Min(ROWS - 1, r + 1);
int cand = nr * COLS + c;
while (cand < ROWS * COLS && !SlotIsActive(cand))
{
nr++;
if (nr >= ROWS) break;
cand = nr * COLS + c;
}
if (cand < ROWS * COLS && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
int startRow = (idx / COLS) * COLS;
int cand = idx - 1;
while (cand >= startRow && !SlotIsActive(cand)) cand--;
if (cand >= startRow && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
int startRow = (idx / COLS) * COLS;
int endRow = startRow + (COLS - 1);
int cand = idx + 1;
while (cand <= endRow && cand < ROWS * COLS && !SlotIsActive(cand)) cand++;
if (cand <= endRow && cand < ROWS * COLS && SlotIsActive(cand)) idx = cand;
}
else if (Input.GetKeyDown(KeyCode.Space))
{
if (!SlotIsActive(idx)) { /* nothing */ }
else
{
int slotIndex = idx;
var slot = (slotIndex < invCount) ? inv.slots[slotIndex] : null;
if (slot == null || slot.item == null || slot.count <= 0)
{
}
else
{
var item = slot.item;
yield return null;
itemMenuPanel.SetActive(false);
var pa = new PlayerAction() { type = ActionType.Item, item = item };
PlayerAction targetResult = null;
yield return StartCoroutine(ShowTargetMenuForActor(actor, pa, true, (res) => { targetResult = res; }));
if (targetResult == null)
{
itemMenuPanel.SetActive(true);
if (!SlotIsActive(idx))
{
int f = -1;
for (int i = 0; i < SLOTS; i++) if (SlotIsActive(i)) { f = i; break; }
if (f >= 0) idx = f;
}
}
else
{
if (targetResult.type != ActionType.None)
{
inv.RemoveItem(item, 1);
}
selected = targetResult;
decided = true;
}
}
}
}
else if (Input.GetKeyDown(KeyCode.Escape))
{
itemMenuPanel.SetActive(false);
decided = true;
selected = new PlayerAction() { type = ActionType.None };
}
for (int i = 0; i < itemGridContainer.childCount; i++)
{
var child = itemGridContainer.GetChild(i);
var txt = child.GetComponentInChildren<Text>();
if (txt != null) txt.color = (i == idx && txt.gameObject.activeSelf) ? Color.yellow : Color.white;
var dot = child.Find("Dot");
if (dot != null) dot.gameObject.SetActive(i == idx && txt != null && txt.gameObject.activeSelf);
}
yield return null;
}
onComplete(selected);
}
IEnumerator ShowTargetMenuForActor(PlayerStats actor, PlayerAction baseAction, bool includeAlliesOption, System.Action<PlayerAction> onComplete)
{
targetMenuPanel.SetActive(true);
foreach (Transform t in targetListContainer) Destroy(t.gameObject);
List<object> targets = new List<object>();
foreach (var e in enemies)
{
if (e.hp <= 0) continue;
var go = Instantiate(menuOptionPrefab, targetListContainer);
var txt = go.GetComponentInChildren<Text>();
txt.text = e.displayName;
targets.Add(e);
}
if (includeAlliesOption)
{
var go = Instantiate(menuOptionPrefab, targetListContainer);
go.GetComponentInChildren<Text>().text = "みかた";
targets.Add("みかた");
}
yield return null;
int idx = 0;
bool decided = false;
PlayerAction finalAction = null;
while (!decided)
{
if (Input.GetKeyDown(KeyCode.UpArrow)) idx = Mathf.Max(0, idx - 1);
else if (Input.GetKeyDown(KeyCode.DownArrow)) idx = Mathf.Min(targets.Count - 1, idx + 1);
else if (Input.GetKeyDown(KeyCode.Space))
{
var sel = targets[idx];
if (sel is string && (string)sel == "みかた")
{
foreach (Transform t in targetListContainer) Destroy(t.gameObject);
List<PlayerStats> aliveAllies = allies.Where(a => a.stats.IsAlive).ToList();
for (int i = 0; i < aliveAllies.Count; i++)
{
var go2 = Instantiate(menuOptionPrefab, targetListContainer);
go2.GetComponentInChildren<Text>().text = aliveAllies[i].stats.charName;
}
yield return null;
int aidx = 0;
bool adec = false;
while (!adec)
{
if (Input.GetKeyDown(KeyCode.UpArrow)) aidx = Mathf.Max(0, aidx - 1);
else if (Input.GetKeyDown(KeyCode.DownArrow)) aidx = Mathf.Min(aliveAllies.Count - 1, aidx + 1);
else if (Input.GetKeyDown(KeyCode.Space))
{
finalAction = baseAction;
finalAction.target = aliveAllies[aidx];
adec = true;
decided = true;
}
else if (Input.GetKeyDown(KeyCode.Escape))
{
decided = false;
break;
}
for (int i = 0; i < targetListContainer.childCount; i++)
{
var txt = targetListContainer.GetChild(i).GetComponentInChildren<Text>();
txt.color = (i == aidx) ? Color.yellow : Color.white;
var dotTf = targetListContainer.GetChild(i).Find("Dot");
if (dotTf != null) dotTf.gameObject.SetActive(i == aidx);
}
yield return null;
}
}
else if (sel is BattleEnemy)
{
finalAction = baseAction;
finalAction.target = sel;
decided = true;
}
}
else if (Input.GetKeyDown(KeyCode.Escape))
{
targetMenuPanel.SetActive(false);
onComplete(null);
yield break;
}
for (int i = 0; i < targetListContainer.childCount; i++)
{
var txt = targetListContainer.GetChild(i).GetComponentInChildren<Text>();
if (txt != null) txt.color = (i == idx) ? Color.yellow : Color.white;
var dotTf = targetListContainer.GetChild(i).Find("Dot");
if (dotTf != null) dotTf.gameObject.SetActive(i == idx);
}
yield return null;
}
targetMenuPanel.SetActive(false);
onComplete(finalAction);
}
bool AttemptEscape(PlayerStats actor)
{
int enemySpeed = 0;
int count = 0;
foreach (var e in enemies) if (e.hp > 0) { enemySpeed += e.data.speed; count++; }
int avgEnemySpeed = (count > 0) ? Mathf.CeilToInt((float)enemySpeed / count) : 0;
float chance = (actor.stats.speed >= avgEnemySpeed) ? 0.8f : 0.4f;
return Random.value <= chance;
}
IEnumerator ExecuteAllyAction(PlayerStats actor, PlayerAction act)
{
if (act == null || act.type == ActionType.None)
{
yield return ShowMessage($"{actor.stats.charName} は何もしなかった。");
yield break;
}
if (act.type == ActionType.Flee)
{
fledThisBattle = true;
yield return ShowMessage($"{actor.stats.charName}は逃げ出した!");
yield break;
}
if (act.type == ActionType.Attack)
{
var target = act.target as BattleEnemy;
if (target == null)
{
yield return ShowMessage($"{actor.stats.charName} はターゲットを見失った!");
yield break;
}
int damage = CalculateDamageWithElement(actor.stats.atk, target.data.def, actor.stats.attribute, target.data.attribute, false, actor.stats.atk, target.data.def);
target.hp = Mathf.Max(0, target.hp - damage);
yield return ShowMessage($"{actor.stats.charName} の こうげき! {target.displayName} に {damage} のダメージ!");
if (target.hp <= 0)
{
yield return StartCoroutine(HandleEnemyDeath(target));
}
yield break;
}
else if (act.type == ActionType.Magic)
{
var magic = act.magic;
if (magic == null) { yield return ShowMessage("魔法が選択されていない。"); yield break; }
actor.UseMP(magic.mpCost);
if (magic.effect == MagicEffect.Damage)
{
if (act.target is BattleEnemy btarget)
{
int damage = CalculateMagicDamage(actor, btarget.data, magic);
btarget.hp = Mathf.Max(0, btarget.hp - damage);
yield return ShowMessage($"{actor.stats.charName} は {magic.magicName} をとなえた! {btarget.displayName} に {damage} のダメージ!");
if (btarget.hp <= 0)
{
yield return StartCoroutine(HandleEnemyDeath(btarget));
}
}
else if (magic.target == MagicTarget.AllEnemies)
{
foreach (var e in enemies.Where(x => x.hp > 0))
{
int damage = CalculateMagicDamage(actor, e.data, magic);
e.hp = Mathf.Max(0, e.hp - damage);
if (e.hp <= 0)
{
yield return StartCoroutine(HandleEnemyDeath(e));
}
}
yield return ShowMessage($"{actor.stats.charName} の {magic.magicName}! 敵全体にダメージ!");
}
}
else if (magic.effect == MagicEffect.Heal)
{
if (act.target is PlayerStats pt)
{
pt.Heal(magic.basePower + actor.stats.matk);
yield return ShowMessage($"{actor.stats.charName} は {magic.magicName} を使った! {pt.stats.charName} を回復した。");
}
}
yield return null;
}
else if (act.type == ActionType.Item)
{
var item = act.item;
if (item == null) { yield return ShowMessage("アイテムが選択されていない。"); yield break; }
if (act.target is BattleEnemy be)
{
be.hp = Mathf.Max(0, be.hp - item.effectValue);
yield return ShowMessage($"{item.itemName} が {be.displayName} に {item.effectValue} のダメージ!");
if (be.hp <= 0)
{
yield return StartCoroutine(HandleEnemyDeath(be));
}
}
else if (act.target is PlayerStats pt)
{
if (item.effect == ItemEffect.Heal)
{
pt.Heal(item.effectValue);
yield return ShowMessage($"{pt.stats.charName} は {item.itemName} を使って回復した。");
}
}
yield return null;
}
}
IEnumerator ExecuteEnemyAction(BattleEnemy enemy)
{
var aliveAllies = allies.Where(a => a.stats.IsAlive).ToList();
if (aliveAllies.Count == 0) yield break;
bool useMagic = enemy.data.learnedMagic != null && enemy.data.learnedMagic.Count > 0 && Random.value < 0.3f;
if (useMagic)
{
var m = enemy.data.learnedMagic[Random.Range(0, enemy.data.learnedMagic.Count)];
var target = aliveAllies[Random.Range(0, aliveAllies.Count)];
int damage = Mathf.Max(1, m.basePower + enemy.data.matk - target.stats.mdef);
target.TakeDamage(damage);
yield return ShowMessage($"{enemy.displayName} は {m.magicName} を使った! {target.stats.charName} に {damage} のダメージ!");
}
else
{
var target = aliveAllies[Random.Range(0, aliveAllies.Count)];
int damage = Mathf.Max(1, enemy.data.atk - target.stats.def);
target.TakeDamage(damage);
yield return ShowMessage($"{enemy.displayName} の こうげき! {target.stats.charName} に {damage} のダメージ!");
}
}
IEnumerator HandleEnemyDeath(BattleEnemy be)
{
if (be == null) yield break;
if (be.visualGO != null)
{
be.visualGO.SetActive(false);
}
if (enemiesContainer != null)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(enemiesContainer);
}
be.visualGO = null;
yield break;
}
float ElementalMultiplier(Element atkElem, Element defElem)
{
// simple relation:
// Fire > Ice, Fire < Water
// Water > Fire, Water < Thunder
// Ice > Wind, Ice < Fire
// Wind > Thunder, Wind < Ice
// Thunder > Water, Thunder < Wind
// Light strong vs Dark, Dark strong vs Light (just invert)
if (atkElem == Element.None || defElem == Element.None) return 1f;
if (atkElem == defElem) return 1f;
// mapping:
if (atkElem == Element.Fire && defElem == Element.Ice) return 2f;
if (atkElem == Element.Fire && defElem == Element.Water) return 0.5f;
if (atkElem == Element.Water && defElem == Element.Fire) return 2f;
if (atkElem == Element.Water && defElem == Element.Thunder) return 0.5f;
if (atkElem == Element.Ice && defElem == Element.Wind) return 2f;
if (atkElem == Element.Ice && defElem == Element.Fire) return 0.5f;
if (atkElem == Element.Wind && defElem == Element.Thunder) return 2f;
if (atkElem == Element.Wind && defElem == Element.Ice) return 0.5f;
if (atkElem == Element.Thunder && defElem == Element.Water) return 2f;
if (atkElem == Element.Thunder && defElem == Element.Wind) return 0.5f;
if (atkElem == Element.Light && defElem == Element.Dark) return 2f;
if (atkElem == Element.Dark && defElem == Element.Light) return 2f;
return 1f;
}
int CalculateDamageWithElement(int atk, int def, Element atkElem, Element defElem, bool isMagic, int atkStat, int defStat)
{
float baseDamage = Mathf.Max(1, atk - def);
float mult = ElementalMultiplier(atkElem, defElem);
int final = Mathf.Max(1, Mathf.RoundToInt(baseDamage * mult));
return final;
}
int CalculateMagicDamage(PlayerStats actor, EnemyData target, MagicData magic)
{
float mult = ElementalMultiplier(actor.stats.attribute, target.attribute);
int damage = Mathf.Max(1, magic.basePower + actor.stats.matk - target.mdef);
damage = Mathf.RoundToInt(damage * mult);
return Mathf.Max(1, damage);
}
}
PartyManager.csの作成
新しく「PartyManager」という名前のスクリプトを作成してください。
その後、以下をコピペしてください。
//PartyManager.cs
using System.Collections.Generic;
using UnityEngine;
public class PartyManager : MonoBehaviour
{
public static PartyManager Instance { get; private set; }
[Header("Party")]
public PlayerStats[] partyMembers = new PlayerStats[0];
[Header("Party shared")]
public int partyMoney = 0;
[Header("Bag (ふくろ) - up to 100 unique items")]
public int bagCapacity = 100;
public List<ItemData> bagItems = new List<ItemData>();
public List<int> bagCounts = new List<int>();
private void Awake()
{
if (Instance != null && Instance != this) Destroy(this.gameObject);
else Instance = this;
}
public bool AddToBag(ItemData item, int count)
{
if (item == null || count <= 0) return false;
int idx = bagItems.IndexOf(item);
if (idx >= 0)
{
bagCounts[idx] += count;
return true;
}
else
{
if (bagItems.Count >= bagCapacity) return false;
bagItems.Add(item);
bagCounts.Add(count);
return true;
}
}
public bool UseBagItem(ItemData item)
{
int idx = bagItems.IndexOf(item);
if (idx < 0) return false;
if (bagCounts[idx] <= 0) return false;
bagCounts[idx]--;
if (bagCounts[idx] == 0)
{
bagCounts.RemoveAt(idx);
bagItems.RemoveAt(idx);
}
return true;
}
public int GetBagCount(ItemData item)
{
int idx = bagItems.IndexOf(item);
if (idx < 0) return 0;
return bagCounts[idx];
}
}
おわり
今回は、スクリプトの作成を行いました。
次回は、UIの作成を行います。