
前回で、バトルを最後まで遊べるようになりました。勝つと「けいけんち 11ポイント」と出ますが、まだ出るだけです。今回は、もらった経験値でレベルが上がって、能力が伸びるようにします。レベルが上がった瞬間は、RPGで一番うれしいところ。光って、文字が飛び出して、伸びた数字が並ぶ…という演出も作ってみたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#11 勝ち負けと、にげる
今回作るもの
- 経験値の合計から、レベルを決める表
LevelTable - レベルごとの能力を、#2 キャラクターと敵のデータで入れておいた「Lv1の能力」と「1レベルごとの成長」から計算する
- 勝ったら、立っている仲間が経験値をもらう。レベルが上がったら、黄色く光って「LEVEL UP!」が飛び出し、ステータスのウインドのLvとHPが増え、伸びた能力をメッセージで出す
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| LevelTable | 経験値の表 | (付けない) |
| Stats | レベルごとの能力を計算する | (付けない) |
| BattleUnit | レベル・経験値の合計・経験値をもらう | - |
| DamagePopup・BattleEffects | 数字の代わりに文字も飛ばせるようにする | - |
| UnitView | レベルアップで光る | - |
| BattleManager | 勝ったら経験値を配って、レベルアップの演出を出す | - |
経験値の表を作ろう

この節では、「経験値の合計が、いくつになったらレベルいくつか」を決める表を作ります。
なぜこの数字か
| レベル | 必要な経験値の合計 | 前のレベルからの差 |
|---|---|---|
| Lv2 | 10 | 10 |
| Lv3 | 25 | 15 |
| Lv4 | 45 | 20 |
| Lv5 | 70 | 25 |
| Lv6 | 100 | 30 |
| Lv7 | 140 | 40 |
| Lv8 | 190 | 50 |
| Lv9 | 250 | 60 |
| Lv10 | 320 | 70 |
- 地下1階の3体(こぼれたインク3・紙ひこうき3・本の虫5)を倒すと、11ポイント。最初のバトルに勝つと、すぐLv2になるようにしています。最初のレベルアップが早いと、「強くなった!」がすぐ味わえます
- 差は5ずつ、Lv6からは10ずつ大きくしていきます。地下2階の敵は1体8〜11ポイントなので、下の階へ進むほど、同じペースでレベルが上がっていきます
- 最大はLv10。ボスの「禁じられた本」は200ポイントなので、ボスの手前でLv8〜9になっていれば、倒したあとにLv10に届きます(ボスは#23、バランスの見直しは#24)
LevelTable
- 「LevelTable」スクリプトを作る
// 経験値の表:そのレベルになるのに必要な、経験値の合計(最大 Lv10)
public static class LevelTable
{
// [0] が Lv1(0)、[1] が Lv2(10)、…、[9] が Lv10(320)
private static readonly int[] totalExp = { 0, 10, 25, 45, 70, 100, 140, 190, 250, 320 };
public static int MaxLevel => totalExp.Length;
// 経験値の合計から、レベルを出す
public static int LevelFor(int exp)
{
int level = 1;
while (level < MaxLevel && exp >= totalExp[level])
{
level++;
}
return level;
}
}
- LevelFor:Lv1から始めて、「次のレベルに必要な合計」に届いている間、レベルを1つずつ上げる。合計11なら、Lv2(10)には届いて、Lv3(25)には届かないのでLv2
totalExp[level]は「今のレベル+1」に必要な合計です。配列は0から数えるので、[1]がLv2になります
- 全員が同じ表を使い、ゲームの途中で変わることもないので、ScriptableObjectにせず、スクリプトに直接書いています
能力を伸ばそう

この節では、レベルに合わせて、能力(HP・こうげき など)を計算できるようにします。
Stats
#2のCharacterDataには、「Lv1の能力(Base Stats)」と「1レベル上がるごとに増える分(Growth)」を入れておきました。レベルnの能力は、Lv1の能力 + 成長 ×(n − 1)で計算できます。
| スライムちゃん | Lv1 | 成長 | Lv2 | Lv5 |
|---|---|---|---|---|
| HP | 38 | 5 | 43 | 58 |
| こうげき | 12 | 2 | 14 | 20 |
| ぼうぎょ | 11 | 2 | 13 | 19 |
| すばやさ | 9 | 1 | 10 | 13 |
Statsに、次のメソッドを足します(magicの下)。
// レベル level の能力:Lv1 の能力(baseStats)に、成長(growth)を(level − 1)回足したもの
public static Stats AtLevel(Stats baseStats, Stats growth, int level)
{
int times = level - 1;
Stats stats = new Stats();
stats.maxHp = baseStats.maxHp + growth.maxHp * times;
stats.maxMp = baseStats.maxMp + growth.maxMp * times;
stats.attack = baseStats.attack + growth.attack * times;
stats.defense = baseStats.defense + growth.defense * times;
stats.speed = baseStats.speed + growth.speed * times;
stats.magic = baseStats.magic + growth.magic * times;
return stats;
}
- データ(Base Stats・Growth)は書き換えず、新しいStatsを作って返します。ScriptableObjectのデータを書き換えると、再生をやめても元に戻らないからです
BattleUnitに、レベルと経験値を足す
BattleUnitのStatsの行を書き換えて、3つ足します。
public Stats Stats { get; private set; } // 今のレベルの能力(レベルが上がると変わる)
public int Level { get; private set; } = 1;
public int TotalExp { get; private set; } // もらった経験値の合計(仲間だけ)
- 敵の
Exp(倒されたときにくれる経験値)と区別するため、仲間の経験値の合計はTotalExpという名前にしています
Rowの下に、仲間のデータを覚えておく変数を足します。
private readonly CharacterData characterData; // 仲間のデータ(レベルが上がったときに、能力を計算し直す。敵は null)
仲間のコンストラクタのStats = data.BaseStats;を、次の2行に書き換えます。
characterData = data;
Stats = Stats.AtLevel(data.BaseStats, data.Growth, Level);
TakeDamageの下に、経験値をもらうメソッドを足します。
// 経験値をもらう。レベルが上がったら、能力を計算し直して true を返す
public bool GainExp(int amount)
{
TotalExp += amount;
int newLevel = LevelTable.LevelFor(TotalExp);
if (newLevel == Level)
{
return false;
}
int oldMaxHp = Stats.maxHp;
Level = newLevel;
Stats = Stats.AtLevel(characterData.BaseStats, characterData.Growth, Level);
Hp += Stats.maxHp - oldMaxHp; // 最大HPが増えた分だけ、今のHPも増やす
return true;
}
- 1回で2つ以上レベルが上がっても、
LevelForが合計から決めるので、そのまま正しいレベルになります - Hp += …:最大HPが5増えたら、今のHPも5増やす。減っていたHPが全回復するわけではありません
Stats.AtLevelのStatsは、プロパティではなくクラスの名前です。C#では、プロパティとクラスが同じ名前でも、Stats.AtLevelのように静的メソッドを呼べます
レベルアップの演出

この節では、レベルが上がった瞬間の演出を作ります。
- 黄色く3回光る
- 「LEVEL UP!」の文字が飛び出す
- ステータスのウインドのLvとHPが増える
- 伸びた能力を、数字でメッセージに出す
文字も飛ばせるようにする
#8 攻撃の手応えを作ろうで作ったDamagePopupは、ダメージの数字しか出せませんでした。Setupを、次の2つのメソッドに書き換えます。
// ダメージの数字を出す。大きいダメージほど大きな文字に
public void Setup(int damage)
{
SetupText(damage.ToString(), Mathf.Min(70f + damage * 3f, 140f), Color.white);
}
// 数字の代わりに、文字を出す(「LEVEL UP!」など)
public void SetupText(string message, float fontSize, Color color)
{
text = GetComponent<TextMeshProUGUI>();
rectTransform = (RectTransform)transform;
text.text = message;
text.fontSize = fontSize;
text.color = color;
velocity = new Vector2(Random.Range(-150f, 150f), jumpSpeed);
}
- 飛び出して消える動き(
Update)はそのまま。出す文字と大きさと色だけを、外から決められるようにしました
BattleEffectsに、文字を出すメソッドを足します(PlayHitAsyncの下)。
// 文字を飛び出させる(「LEVEL UP!」など)
public void ShowText(UnitView target, string message, Color color)
{
DamagePopup popup = Instantiate(damagePopupPrefab, popupLayer);
popup.transform.position = target.transform.position;
// 光っている体と重ならないように、頭の上から出す
((RectTransform)popup.transform).anchoredPosition += new Vector2(0f, 160f);
popup.SetupText(message, 64f, color);
}
- 文字の色は「LEVEL UP!」も光る色も黄色なので、体の真ん中から出すと、光っている体に重なって読めません。
anchoredPositionを160だけ上にずらして、頭の上から出しています
黄色く光る
UnitViewの一番下に、レベルアップで光るメソッドを足します。
// レベルアップ:黄色く3回光る
public async Awaitable PlayLevelUpAsync()
{
// 表示する前に、形と色を入れておく
flashImage.sprite = image.sprite;
Color color = flashColor;
color.a = 0.8f;
flashImage.color = color;
for (int i = 0; i < 3; i++)
{
flashImage.enabled = true;
await Awaitable.WaitForSecondsAsync(0.1f, destroyCancellationToken);
flashImage.enabled = false;
await Awaitable.WaitForSecondsAsync(0.1f, destroyCancellationToken);
}
}
- #5 だれの番か、ひと目で分かるようにしようの光る絵(Flash)を、今度は0.1秒ごとに出したり消したりします
- 色は、選んでいる人の点滅と同じ黄色(
flashColor)を濃くしたもの。喜ぶポーズのまま光ります
勝ったら経験値を配る
BattleManagerに、「LEVEL UP!」の色を足します(randomSeedの上)。
[SerializeField] private Color levelUpColor = new Color(1f, 0.88f, 0.4f); // 「LEVEL UP!」の色
ShowVictoryAsyncを、次のように書き換えます。
// 勝った:立っている仲間が喜んで、経験値をもらう。レベルが上がったら演出を出す
private async Awaitable ShowVictoryAsync()
{
foreach (BattleUnit unit in lineup.PartyUnits)
{
if (!unit.IsDead)
{
_ = unit.View.PlayWinAsync(); // 喜ぶのは待たずに、メッセージと一緒に進める
}
}
await messageWindow.ShowAsync("てきを ぜんぶ たおした!");
int exp = 0;
int gold = 0;
foreach (BattleUnit enemy in lineup.EnemyUnits)
{
exp += enemy.Exp;
gold += enemy.Gold;
}
await messageWindow.ShowAsync($"{exp}ポイントの けいけんちを かくとく!");
// 立っている仲間だけが、経験値をもらう
foreach (BattleUnit unit in lineup.PartyUnits)
{
if (unit.IsDead)
{
continue;
}
Stats before = unit.Stats;
if (unit.GainExp(exp))
{
await ShowLevelUpAsync(unit, before);
}
}
await resultWindow.ShowAsync("しょうり!", $"けいけんち {exp}ポイント\n{gold}ゴールドを てにいれた");
}
// レベルアップ:光って「LEVEL UP!」、ステータスの行を書きかえて、伸びた能力を出す
private async Awaitable ShowLevelUpAsync(BattleUnit unit, Stats before)
{
Stats after = unit.Stats;
unit.Row.SetLevel(unit.Level);
unit.Row.SetHp(unit.Hp, after.maxHp);
unit.Row.SetMp(after.maxMp, after.maxMp); // MP は #13 で使うようになるまで、満タンのまま
battleEffects.ShowText(unit.View, "LEVEL UP!", levelUpColor);
// ここで、レベルアップの効果音を鳴らす(#26 で足す)
Awaitable glow = unit.View.PlayLevelUpAsync();
await messageWindow.ShowAsync($"{unit.Name}は レベル{unit.Level}に あがった!");
await glow;
await messageWindow.ShowAsync(
$"HP+{after.maxHp - before.maxHp} MP+{after.maxMp - before.maxMp} " +
$"こうげき+{after.attack - before.attack} ぼうぎょ+{after.defense - before.defense} " +
$"すばやさ+{after.speed - before.speed}");
}
- Stats before = unit.Stats;:
GainExpはレベルが上がると新しいStatsに入れ替えるので、入れ替える前のStatsを取っておけば、あとで「いくつ伸びたか」を引き算で出せます - _ = と Awaitable glow:喜ぶポーズは待たずに進め、光るのはメッセージと同時に始めて、メッセージのあとで待っています(#8と同じ「始めておいて、あとで待つ」)
- 効果音は、#26でまとめて鳴らします。鳴らす場所だけ、コメントで印を付けておきます
- 倒れている仲間は経験値をもらいません。バトルに出ていない控えの1人に半分を配るのは、#20で作ります
「もういちど」を押すと、レベルは1に戻ります。今はシーンを読み込み直すと、全部が最初の状態になるからです。上がったレベルやお金を次のバトルに持ち越すのは、#17でGameSessionを作ってからです。
動かして確認!

再生して、3体とも倒してみましょう(Random Seed 42で、3人とも「たたかう」で左の敵から倒していきます)。
#12の動作確認
- 勝つと「11ポイントの けいけんちを かくとく!」
- 立っている仲間がLv2に上がり、黄色く光って「LEVEL UP!」が飛び出す
- ステータスのウインドのLvが2になり、HPの最大が増える(スライムちゃん 38→43、フェアリー 26→29、キョンシー 32→36)
- 伸びた能力が、メッセージで出る
あとがき
今回は、経験値の表を作って、勝つとレベルが上がるようにしました。数字が伸びるのを見ると、もっと戦いたくなりますね。次回は、MPを使う「まほう」を作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#12 経験値とレベルアップ […]