Week06_LevelUp - M-634/unity-game-dev-tutorial GitHub Wiki

📘 Week06 - 成長・レベルアップシステム(経験値アイテム吸収+スキル強化)


🎯 今週の目的

  • 敵を倒すと経験値アイテム(Gem)が出現
  • プレイヤーが近づくと自動で吸収され、経験値が加算される
  • 経験値がたまるとレベルアップ
  • レベルに応じてスキル性能(発射間隔や速度)が強化される
  • DOTweenを使って吸収演出をなめらかにする

🛠 授業の流れ


🔧 1. DOTweenの導入

DOTween は Unity 向けのアニメーション/Tween ライブラリで、
「移動・回転・色変更などをスムーズに実装できる」強力なツールです。
今回は経験値アイテムがプレイヤーに吸い寄せられる演出に使用します。


📥 インストール手順(初回のみ)

  1. 以下リンクからDOTweenのAssetをDownload
  1. Open UnityEditorとテキストが変わるので、クリックするとPackageManagerWindowがひらかられる
  2. DOTween (HOTween v2)項目でImport
  3. Unity メニューから
    Tools > Demigiant > DOTween Utility Panel を選択
  4. パネルが開いたら、右上の 「Setup DOTween...」 をクリック
  5. Unity が自動でライブラリを初期化・設定してくれます

🔸 パッケージ導入時に Assets > Plugins > DOTween フォルダが生成されます
🔸 もしメニューが表示されない場合は 公式サイト から再取得も可能


✏ スクリプトへの記述方法

DOTween を使うスクリプトの先頭で、以下のように名前空間を追加してください。

using DG.Tweening;

✅ 使用例(今回の授業で使う)

transform.DOMove(_player.position, 0.5f).SetEase(Ease.InOutSine);
  • DOMove():オブジェクトを指定位置に一定時間で移動させる
  • SetEase():加速や減速のカーブを設定(自然な動きになる)

🔗 参考リンク


💎 2. 経験値アイテムの作成(ExpPickup.cs)

using UnityEngine;
using DG.Tweening;

 public class ExpPickup : MonoBehaviour
    {
        [SerializeField]
        private int _expValue = 5;

        [SerializeField, Range(0f, 50f)]
        private float _distanceToMove = 10f;
        
        [SerializeField, Range(0f, 5f)]
        private float _moveTime = 0.3f;

        private Transform _player;
        private bool _isMoving;
        private Tweener _moveTween;

        private void Start()
        {
            _player = GameObject.FindGameObjectWithTag("Player").transform;
        }

        private void Update()
        {
            if (_isMoving || _player == null) return;
            
            float distance = Vector2.Distance(transform.position, _player.position);
            if (distance < _distanceToMove)
            {
                _isMoving = true;
                StartFollowingPlayer();
                Debug.Log($"{gameObject.name} start moving");
            }
        }

        private void StartFollowingPlayer()
        {
            _moveTween = transform
                .DOMove(_player.position, _moveTime)
                .SetEase(Ease.Linear)
                .SetLoops(-1, LoopType.Restart)
                .OnUpdate(() =>
                {
                    if (_moveTween.IsActive())
                    {
                        _moveTween.ChangeEndValue(_player.position, true);
                    }
                });
        }

        private void OnTriggerEnter2D(Collider2D other)
        {
            if (other.CompareTag("Player"))
            {
                if (_moveTween != null && _moveTween.IsActive())
                {
                    _moveTween.Kill(); // Tweenを明示的に終了させる
                }
                
                if (PlayerLevelManager.Instance != null)
                {
                    PlayerLevelManager.Instance.AddExp(_expValue);
                }
                Destroy(gameObject);
            }
        }
    }

💥 3. 敵を倒したときに経験値をドロップ(EnemyHealth.csを修正)

 [RequireComponent(typeof(Rigidbody2D))]
    public class EnemyHealth : MonoBehaviour
    {
        [SerializeField] 
        private int _maxHp = 100;
        
        [SerializeField]
        private SpriteRenderer _spriteRenderer;
        
        [SerializeField]
        private ExpPickup _expPickupItemPrefab;//ドロップアイテムPrefab
        
        private int _currentHp;
        private bool _isDead = false;

        private void Start()
        {
            _currentHp = _maxHp;
        }
        
        public void OnDamaged(int damageAmount)
        {
            if(_isDead) return;
            
            DamagePopup.Create(transform.position, damageAmount);   
            _currentHp -= damageAmount;
            StartCoroutine(Flash());
            
            if (_currentHp <= 0)
            {
                OnDead();
            }
        }

        private void OnDead()
        {
            _isDead = true;

            if (EnemyKillCounter.Instance != null)
            {
                EnemyKillCounter.Instance.AddKillCount();
            }

            //今回の追加部分
            if (_expPickupItemPrefab != null)
            {
               Instantiate(_expPickupItemPrefab, transform.position, Quaternion.identity);    
            }
            
            Destroy(gameObject);
        }
        
        private IEnumerator Flash()
        {
            var tempColor = _spriteRenderer.color;
            _spriteRenderer.color = Color.red;
            yield return new WaitForSeconds(0.2f);
            _spriteRenderer.color = tempColor;
        }
    }

📈 4. レベルと経験値の管理(LevelManager.cs)+ Singleton 化

レベル情報はゲーム中1つだけ存在し、常にどこからでも参照される必要があります。
このようなケースに適しているのが Singleton(シングルトン)パターン です。


❓ Singletonとは?

  • インスタンスが1つだけ存在することを保証する仕組み
  • 他のスクリプトから PlayerLevelManager.Instance のようにして簡単にアクセスできる
  • UIやスコア、ゲームマネージャーなどでもよく使われるパターン

✅ PlayerLevelManager.cs

using UnityEngine;
using TMPro;

public class PlayerLevelManager : MonoBehaviour
{
    public static PlayerLevelManager Instance { get; private set; }

    [SerializeField] private TextMeshProUGUI _levelText;

    private int _level = 1;
    private int _exp = 0;
    private int _expToNext = 10;

    public int CurrentLevel => _level;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject); // すでに存在している場合は削除
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject); // シーンを跨いでも消えないようにする(必要に応じて)
    }

    public void AddExp(int amount)
    {
        _exp += amount;
        if (_exp >= _expToNext)
        {
            LevelUp();
        }
        UpdateUI();
    }

    private void LevelUp()
    {
        _level++;
        _exp = 0;
        _expToNext += 10;
        Debug.Log("Level Up! 現在のレベル: " + _level);
    }

    private void UpdateUI()
    {
        if (_levelText != null)
        {
            _levelText.text = $"Lv. {_level}";
        }
    }
}

🧠 5. スキル成長データの設定(AutoAttackSkillData.csを修正)

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "NewSkill", menuName = "Skills/AutoAttackSkill")]
public class AutoAttackSkillData : ScriptableObject
{
   public string skillName;
   public GameObject bulletPrefab;
   public Vector2 direction;
   
   public List<float> intervalPerLevels;
   public List<float> speedPerLevels;
   
   public float GetInterval(int level)
   {
      return intervalPerLevels[Mathf.Clamp(level - 1, 0, intervalPerLevels.Count - 1)];
   }
   
   public float GetSpeed(int level)
   {
      return speedPerLevels[Mathf.Clamp(level - 1, 0, speedPerLevels.Count - 1)];
   }
}

🔫 6. スキルのレベル連動処理(PlayerAutoAttack.cs)

using UnityEngine;
using System.Collections.Generic;

public class PlayerAutoAttack : MonoBehaviour
{
   [SerializeField]
        private List<AutoAttackSkillData> _skills = new();
        
        [SerializeField]
        private Transform _muzzle;
        
        private List<float> _timers = new();
        
        private PlayerLevelManager _playerLevelManager;
        
        private void Start()
        {
            _playerLevelManager = PlayerLevelManager.Instance;
            foreach (var _ in _skills)
            {
                _timers.Add(0f);
            }
        }
        
        private void Update()
        {
            //PlayerLevelManager.Instanceがnullなら「level1」固定
            int currentLevel = _playerLevelManager?.CurrentLevel ?? 1;
            
            for (int i = 0; i < _skills.Count; i++)
            {
                _timers[i] += Time.deltaTime;
        
                if (_timers[i] >= _skills[i].GetInterval(currentLevel))
                {
                    Shoot(_skills[i], currentLevel);
                    _timers[i] = 0f;
                }
            }
        }
        
        private void Shoot(AutoAttackSkillData skill, int currentLevel)
        {
            GameObject bullet = Instantiate(skill.bulletPrefab, _muzzle.position, Quaternion.identity);
            Rigidbody2D rb = bullet.GetComponent<Rigidbody2D>();
            rb.velocity = skill.direction.normalized *skill.GetSpeed(currentLevel);
        }
    }    
}

💎 7. 経験値アイテムPrefabを作成

  1. 適当な画像からPrefabを作成する。名前は「ExpPickupItem」とか。
  2. そのPrefabにExpPickup.csをアタッチする。
  3. 当たり判定を付けるために画像に合うColliderコンポーネントをアタッチする。IsTriggerにチェックを入れることを忘れずに
  4. インスペクター上で値を設定。
  5. Enemy.prefabにアタッチされているEnemyHealth.csコンポーネントの「expPickupItemPrefab」にアタッチ

✅ 動作確認ガイド

  • ✅ 敵を倒すと宝石型の経験値アイテムが出現する
  • ✅ プレイヤーが近づくと DOTween で吸い寄せられるように動く
  • ✅ 接触すると経験値が加算され、アイテムは消える
  • ✅ レベルアップ時に Console に「Level Up!」が出力される
  • ✅ 発射間隔または弾速がレベルに応じて変化する
  • ✅ 画面上部に現在の Lv が表示されている

📝 課題

  • プレイヤーのレベルがわかるようにUIに表示してみよう。
  • レベルアップ時にスキルの性能が変化するように自分なりに調整してみよう。
  • プレイヤーの経験値テーブルを作成してレベルデザインをしてみよう。  L 現状次のレベルを上げるのに経験値5が固定で設定されているが、レベルが上昇するにつれて必要となる経験値を増やしてみる。  cf: 【ドラクエウォーク】経験値テーブル(特級職レベル80に対応)【DQウォーク】

🔗 参考リンク

⚠️ **GitHub.com Fallback** ⚠️