【Unity】RPGのレベルシステムの書き方

レベルシステム、RPG作っていれば必ず書くと言っていい処理ですが、自分流の書き方を残しておきます。

累計経験値と累計経験値に応じたレベルのテーブルがあれば、そこから現在Lvと次のLvまでの残り経験値を算出できます。
しかし、Lvを取得する度に都度計算していては流石に重たいので、累計経験値が加算される度に現Lvと残り経験値を更新するようにします。

累計経験値に応じたレベルのテーブルを関数の引数にとって外部から参照してますが、これはレベルテーブルは固有な値として`static readonly`で修飾して使うことが多いためです。

あるいは、UnityならScriptableObjectから参照する場合もあるでしょう。

いずれにせよ外部またはサブクラス上のstaticな値を汎用的に参照させたいので関数の引数に含めました。

```C#:ExpLevelClass.cs
using System.Linq;
using UnityEngine;

[System.Serializable]
public class ExpLevelClass
{
    [SerializeField] int _exp;
    [SerializeField] int _remainExp;
    [SerializeField] int _minLevel;
    [SerializeField] int _level = 1;

    public int Exp => _exp;
    public int RemainExp => _remainExp;
    public int MinLevel => _minLevel;
    public int Level => _level;
    
    // Expを加算してLvを初期化する
    public void AddExp(int exp, int[] expArray)
    {
        //カンストを考慮して加算
        _exp = Mathf.Clamp(_exp + exp, 0, expArray[expArray.Length - 1]);
        // 値の更新
        UpdateLevel(expArray);
        UpdateRemainExp(expArray);
    }

    void UpdateLevel(int[] expArray)
    {
        // 現Exp以下の値の中で最大の値のインデックスを取得
        var maxIdx = expArray.Where(x => x <= _exp).Select((val, idx) => new {V = val, I = idx})
            .Aggregate((max, working) => (max.V > working.V) ? max : working).I;
        _level = maxIdx + 1;
    }

    void UpdateRemainExp(int[] expArray)
    {
        // 現Expより大きい値の中で最小の値のインデックスを取得
        var minIdx = expArray.Where(x => x > _exp).Select((val, idx) => new {V = val, I = idx})
            .Aggregate((min, working) => (min.V < working.V) ? min : working).I;
        _remainExp = expArray[minIdx] - _exp;
    }
}
```

```C#:Player.cs
[System.Serializable]
public class Player
{
    public string Name = "主人公";
    public ExpLevelClass ExpLevel = new ExpLevelClass();
    
    static readonly int[] TOTAL_EXP_ARRAY = {0,100,300,600,1000};

    // 経験値獲得処理
    public void AddExp(int exp)
    {
        ExpLevel.AddExp(exp, TOTAL_EXP_ARRAY);
    }
}
```

###おまけ
関数でLevelとRemainExpの計算結果を返す

```C#:ExpLevelClass.cs
using System.Linq;
using UnityEngine;

[System.Serializable]
public class ExpLevelClass
{
    [SerializeField] int _exp;
    [SerializeField] int _remainExp;
    [SerializeField] int _minLevel;
    [SerializeField] int _level = 1;

    public int Exp => _exp;
    public int RemainExp => _remainExp;
    public int MinLevel => _minLevel;
    public int Level => _level;
    
    // Expを加算してLvを初期化する
    public (int afterLevel, int remainExp) AddExp(int exp, int[] expArray)
    {
        //カンストを考慮して加算
        _exp = Mathf.Clamp(_exp + exp, 0, expArray[expArray.Length - 1]);
        // 値の更新
        UpdateLevel(expArray);
        UpdateRemainExp(expArray);

        return (Level, RemainExp);
    }

    void UpdateLevel(int[] expArray)
    {
        // 現Exp以下の値の中で最大の値のインデックスを取得
        var maxIdx = expArray.Where(x => x <= _exp).Select((val, idx) => new {V = val, I = idx})
            .Aggregate((max, working) => (max.V > working.V) ? max : working).I;
        _level = maxIdx + 1;
    }

    void UpdateRemainExp(int[] expArray)
    {
        // 現Expより大きい値の中で最小の値のインデックスを取得
        var minIdx = expArray.Where(x => x > _exp).Select((val, idx) => new {V = val, I = idx})
            .Aggregate((min, working) => (min.V < working.V) ? min : working).I;
        _remainExp = expArray[minIdx] - _exp;
    }
}
```

```C#:Player.cs
[System.Serializable]
public class Player
{
    public string Name = "主人公";
    public ExpLevelClass ExpLevel = new ExpLevelClass();
    
    static readonly int[] TOTAL_EXP_ARRAY = {0,100,300,600,1000};

    // 経験値獲得処理
    public (int afterLevel, int remainExp) AddExp(int exp)
    {
        return ExpLevel.AddExp(exp, TOTAL_EXP_ARRAY);
    }
}
```


##参考
[Linqで快適な生活を](https://qiita.com/Go-zen-chu/items/b546d01fd14ca818d00d)
[[LINQ][C#] Aggregateの使い方(畳み込み, fold)](https://webbibouroku.com/Blog/Article/linq-aggregate)
[C# 7 の Tuple は戻り値が複数の時に便利](http://ryuichi111std.hatenablog.com/entry/2017/03/12/175357)