【制作100个unity游戏之30】使用unity实现一款2d俯视角RPG战斗游戏,主要使用InputSystem+有限状态机+A*寻路实现(完结,附带项目源码)

人物素材

人物环境素材
https://game-endeavor.itch.io/
在这里插入图片描述

敌人素材
https://bdragon1727.itch.io/pixel-character-16x16
请添加图片描述
敌人
在这里插入图片描述
阴影
在这里插入图片描述
攻击特效
https://v-ktor.itch.io/pixelated-attackhit-animations
在这里插入图片描述
UI
https://mounirtohami.itch.io/pixel-art-gui-elements
在这里插入图片描述
物品
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

简单绘制环境

参考:【推荐100个unity插件之14】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)

环境按自己喜欢的绘制即可,效果
在这里插入图片描述

玩家状态机控制

参考:【unity实战】使用unity的新输入系统InputSystem+有限状态机设计一个玩家状态机控制——实现玩家的待机 移动 闪避 连击 受击 死亡状态切换

效果
在这里插入图片描述

虚拟相机跟随和区域限制

参考:【推荐100个unity插件之10】Unity最全的最详细的Cinemachine(虚拟相机系统)介绍,详细案例讲解,快速上手

效果
在这里插入图片描述
我希望虚拟摄像机跟随玩家,并且之后的每个场景,都能自动跟随玩家,我们就需要添加一个脚本给它里面

public class AutoSetupCamera : MonoBehaviour
{
    private PlayerController player; // 玩家对象的引用
    private CinemachineVirtualCamera cc; // Cinemachine虚拟摄像机的引用

    private void Awake()
    {
        // 获取Cinemachine虚拟摄像机组件的引用
        cc = GetComponent<CinemachineVirtualCamera>();

        // 在场景中查找并获取Player类型的对象实例
        player = FindObjectOfType<PlayerController>();

        // 如果找到了Player对象,则设置摄像机跟随Player的目标
        if (player != null)
        {
            cc.Follow = player.transform;
        }
        else
        {
            Debug.LogWarning("未找到Player对象");
        }
    }
}

有限状态机敌人AI

使用A星插件制作敌人自动寻路功能、避障,参考:【unity实战】Unity中使用A*寻路+有限状态机制作一个俯视角敌人AI

效果
在这里插入图片描述

树木排序问题

参考:【unity小技巧】2d人物和树木或者物品遮挡关系

效果
在这里插入图片描述

脚步灰尘

探究

要实现走路灰尘效果,Unity的粒子系统(Particle System)中有属性RateOverDistance:根据移动距离发射粒子,不移动时不发射。恰好可满足当前需求

实际使用时发现,不管怎么移动都不发射粒子,但RateOverTime(随时间推移发射粒子)的功能是正常的

解决方案
粒子系统有一属性:EmitterVelocity(发射器速度模式),它有2种模式

Transform:通过Transform中Position的变化计算粒子系统的移动速度
Rigidbody:将刚体(若有)的速度作为粒子系统的移动速度

看了上述解释即可想到,若EmitterVelocity设置为Rigidbody模式,当该粒子系统没有刚体时,系统会认为该发射器是不动的,因此移动速度为0,因此移动距离为0:因此RateOverDistance不会发射粒子

所以将EmitterVelocity(发射器速度模式)设置为Transform(变换)即可

实现

素材图片
在这里插入图片描述
添加粒子效果
在这里插入图片描述
在这里插入图片描述
配置
在这里插入图片描述
效果
在这里插入图片描述

生命系统

新增Character属性基类,敌人和玩家甚至可破坏的物品只需要继承这个脚本,就不用再次编写一次生命系统的脚本,来实现相同代码的复用

public class Character : MonoBehaviour
{
    [Header("生命值")]
    public float maxHealth; // 最大生命值
    protected float _currentHealth; // 当前生命值
    //生命值修改器
    public virtual float CurrentHealth{
        get { return _currentHealth; }
        set{ _currentHealth = value; }
    }

    [Header("无敌")]
    public float invulnerableDuration; // 无敌持续时间
    protected bool invulnerable; // 是否处于无敌状态

    private void Start()
    {
        CurrentHealth = maxHealth;
    }

    // 受到伤害
    public virtual void TakeDamage(Attack attack)
    {
        if (invulnerable) return; // 如果处于无敌状态,直接返回
        SetHealth(-attack.damage);
        Hit(attack);// 触发受伤逻辑
        StartCoroutine(nameof(InvulnerableCoroutine)); // 启动无敌状态的协程
    }

    // 设置生命值
    public virtual void SetHealth(float value)
    {
        CurrentHealth += value;
        if (CurrentHealth >= maxHealth)//超过最大生命值
        {
            CurrentHealth = maxHealth;
        }

        if (CurrentHealth <= 0)// 小于等于0
        {
            CurrentHealth = 0; 
            Die(); // 触发死亡逻辑
        }
    }

    // 受伤逻辑
    protected virtual void Hit(Attack attack)
    {
    }

    // 死亡逻辑
    protected virtual void Die()
    {
    }

    // 无敌状态的协程
    private IEnumerator InvulnerableCoroutine()
    {
        invulnerable = true; // 设置为无敌状态

        yield return new WaitForSeconds(invulnerableDuration); // 等待无敌持续时间

        invulnerable = false; // 结束无敌状态
    }
}

玩家生命系统脚本

public class PlayerCharacter : Character {
    // 受伤逻辑
    protected override void Hit(Attack attack)
    {
        GetComponent<PlayerController>().parameter.isHurt = true;
    }

    // 死亡逻辑
    protected override void Die()
    {
        if(GetComponent<PlayerController>().parameter.isDead) return;
        GetComponent<PlayerController>().parameter.isDead = true;
    }
}

敌人生命系统脚本

public class EnemyCharacter : Character {
    [HideInInspector] public Collider2D coll; 
    private void Awake() {
        coll = GetComponent<Collider2D>();
    }

    // 受伤逻辑
    protected override void Hit(Attack attack)
    {
        GetComponent<Enemy.FSM>().parameter.isHurt = true;
    }

    // 死亡逻辑
    protected override void Die()
    {
        GetComponent<Enemy.FSM>().parameter.isDead = true;
        //取消碰撞
        coll.enabled = false;
        Destroy(gameObject, 2f);
    }
}

配置,玩家和敌人分别挂载对应的生命系统脚本,并配置对应参数
在这里插入图片描述
在这里插入图片描述

玩家敌人攻击

新增Attack攻击脚本

public class Attack : MonoBehaviour {

    public int damage;
    
    private void OnTriggerStay2D(Collider2D other)
    {
        other.GetComponent<Character>()?.TakeDamage(this);
    }
}

玩家和敌人分别挂载对应的攻击脚本
在这里插入图片描述
配置敌人和玩家的不同攻击层
在这里插入图片描述

配置玩家只能攻击敌人二号可破坏物(后面会加),敌人只能攻击玩家
在这里插入图片描述
配置攻击动画的攻击区域
在这里插入图片描述
可以敌人死亡动画,透明度慢慢变为0,效果更好
在这里插入图片描述
效果,记得关闭敌人死亡动画循环播放
在这里插入图片描述

打击感

参考:【unity实战】使用Unity实现动作游戏的攻击 连击 轻重攻击和打击感

轻重攻击

可以给攻击动画配置不同的播放速度,以达到不同 轻重攻击效果
在这里插入图片描述
效果
在这里插入图片描述

时间管理器 时停效果

public class TimeManager : MonoBehaviour
{
    // 单例模式
    public static TimeManager Instance { get; private set; }

    [Header("默认游戏时间缩放")]
    [Range(0f, 2f)] public float defaultTimeScale = 1;

    [Header("时间缩放程度")]
    [SerializeField, Range(0f, 2f)] private float bulletTimeScale;
    [Header("多久恢复到默认游戏时间")]
    [SerializeField] private float timeRecoveryDuration;

    private void Awake()
    {
        Instance = this;
        Time.timeScale = defaultTimeScale;
    }

    // 进入减速模式
    public void BulletTime()
    {
        Time.timeScale = bulletTimeScale;
        StartCoroutine(nameof(TimeRecoveryCoroutine));
    }

    // 恢复到默认时间缩放的协程
    IEnumerator TimeRecoveryCoroutine()
    {
        float ratio = 0f;
        while (ratio < 1f)
        {
            ratio += Time.unscaledDeltaTime / timeRecoveryDuration;
            // 插值时间缩放从减速时间缩放到默认时间缩放
            Time.timeScale = Mathf.Lerp(bulletTimeScale, defaultTimeScale, ratio);

            yield return null; // 等待下一帧继续执行
        }
    }
}

配置
在这里插入图片描述
调用,可以在敌人受伤时发起调用,达到短暂时停的效果

TimeManager.Instance.BulletTime();

效果,测试用的玩家受伤
在这里插入图片描述

击退

修改EnemyCharacter

[Header("击退")]
public float knokbackForce = 5f;//击退力
private Vector2 direction;//击退方向向量

// 受伤逻辑
protected override void Hit(Attack attack)
{
    GetComponent<Enemy.FSM>().parameter.isHurt = true;

    //击退
    rb.velocity = Vector2.zero;
    direction = (transform.position - attack.transform.parent.position).normalized;
    rb.AddForce(direction * knokbackForce, ForceMode2D.Impulse);
}

效果
在这里插入图片描述

击中特效

敌人上配置特效
在这里插入图片描述
特效动画,播放100%自动退出
在这里插入图片描述
修改EnemyCharacter,代码控制

[Header("受击特效")]
private Animator hitAnimator; // 敌人的受击特效动画控制器

hitAnimator = transform.GetChild(0).GetComponent<Animator>();

// 播放受击特效动画
hitAnimator.SetTrigger("isHit"); 

效果
在这里插入图片描述

屏幕震动

玩家受伤可以发起屏幕震动,参考:unity实现简单的摄像机震动效果(包括普通摄像机和虚拟摄像机)

效果
在这里插入图片描述

敌人管理器控制敌人生成波次

public class EnemyManager : MonoBehaviour
{
    // 单例模式
    public static EnemyManager Instance { get; private set; }

    [Header("刷新点")]
    public Transform[] spawnPoints;  // 敌人生成点数组

    [Header("巡逻点数组")]
    public Transform[] patrolPoints; // 敌人巡逻点数组

    [Header("敌人波次")]
    public List<EnemyWave> enemyWaves;   // 敌人波次列表

    private int currentWaveIndex = 0;     // 当前波次索引
    private int enemyCount = 0;            // 敌人计数

    // 判断是否为最后一波
    public bool GetLastWave() => currentWaveIndex == enemyWaves.Count - 1;

    private void Awake()
    {
        Instance = this;
    }

    private void Start()
    {
        StartCoroutine(nameof(startNextWaveCoroutine));
    }
	
	//杀死敌人方法
    public void KillEnemy()
    {
        enemyCount--;
        if (enemyCount == 0)// 敌人死亡
        {
            //开始下一波敌人
            if (GetLastWave())
            {
                Debug.Log("游戏胜利!");
                return;
            }
            currentWaveIndex++; // 当前波次索引增加
            StartCoroutine(nameof(startNextWaveCoroutine));
        }
    }

    // 开始下一波的协程
    IEnumerator startNextWaveCoroutine()
    {
        List<EnemyData> enemies = enemyWaves[currentWaveIndex].enemies; // 获取当前波次对应的敌人列表
        foreach (EnemyData enemyData in enemies)
        {
            for (int i = 0; i < enemyData.waveEnemyCount; i++)
            {
                enemyCount++;
                // 实例化敌人预制体,并设置位置为随机的刷新点
                GameObject enemy = Instantiate(enemyData.enemyPrefab, GetRandomSpawnPoint(), Quaternion.identity);

                if (patrolPoints != null) // 如果巡逻点数组不为空,将巡逻点数组赋值给敌人的巡逻点数组
                {
                    enemy.GetComponent<Enemy.FSM>().parameter.patrolPoints = patrolPoints;
                }

                yield return new WaitForSeconds(enemyData.spawnInterval); // 等待生成下一个敌人的间隔时间
            }
        }
    }

    // 获取随机刷新点
    private Vector3 GetRandomSpawnPoint()
    {
        int randomIndex = Random.Range(0, spawnPoints.Length); // 随机选择一个刷新点的索引
        return spawnPoints[randomIndex].position; // 返回随机刷新点的位置
    }
}

// 由于没有继承MonoBehaviour,所以需要加上[System.Serializable]以在Unity编辑器中序列化
[System.Serializable]
public class EnemyData
{
    public GameObject enemyPrefab;  // 敌人预制体
    public float spawnInterval;     // 生成间隔
    public float waveEnemyCount;    // 波次敌人数量
}

[System.Serializable]
public class EnemyWave
{
    public List<EnemyData> enemies; // 每波的敌人列表
}

简单配置两波敌人
在这里插入图片描述
敌人死亡时调用杀死敌人方法

EnemyManager.Instance.KillEnemy();

效果,可以看到第一波敌人被全部杀死,又生成下一波新的敌人,两波敌人杀完提示游戏胜利
在这里插入图片描述

玩家血条

参考:unity 3种办法实现血条效果并实现3d世界血条一直看向摄像机

简单绘制UI

在这里插入图片描述

使用事件渲染UI数据

这里涉及事件的知识,参数:【unity实战】事件(Event)的基本实战使用

新增PlayerEvents,控制玩家事件

//玩家事件
public class PlayerEvents
{
    public static event Action<float, float> onUpdateHP;
    
	//更新血条UI事件
    public static void UpdateHP(float currentHealth, float maxHealth) 
    {
        onUpdateHP?.Invoke(currentHealth, maxHealth);
    }
}

新增PlayerHealthBar,控制玩家血条UI显示

public class PlayerHealthBar : MonoBehaviour
{
    public Image HP;        // 即时血量UI
    public Image slowHP;    // 缓冲血量UI

    // 注册事件监听器
    private void OnEnable()
    {
        PlayerEvents.onUpdateHP += UpdateHP;
    }

    // 注销事件监听器
    private void OnDisable()
    {
        PlayerEvents.onUpdateHP -= UpdateHP;
    }
	// 更新血条UI事件
    public void UpdateHP(float currentHP, float maxHP)
    {
        HP.fillAmount = currentHP / maxHP;
    }

    private void Update()
    {
        // 当缓冲血量大于即时血条时,缓冲血条持续减少
        if (slowHP.fillAmount > HP.fillAmount)
        {
            // 缓冲血量持续减少,Time.daltaTime可换成其他时间参数,如0.01f
            slowHP.fillAmount -= Time.deltaTime * 0.1f;
        }
        else
        {
            // 使缓冲血量与即时血量相等
            slowHP.fillAmount = HP.fillAmount;
        }
    }
}

修改PlayerCharacter调用血条变化

// 重写生命值属性字段,更改生命值时同步更新玩家血条
public override float CurrentHealth{
    get { return _currentHealth; }
    set{
        _currentHealth = value;
        //更新玩家血条UI
        PlayerEvents.UpdateHP(_currentHealth, maxHealth);
    }
}

配置
在这里插入图片描述
效果
在这里插入图片描述

翻滚cd条

修改PlayerEvents

public static event Action<float> onUpdateDodgeCD;

//更新血条UI事件
public static void UpdateDodgeCD(float dodgeCooldown) 
{
    onUpdateDodgeCD?.Invoke(dodgeCooldown);
}

新增PlayerDodgeCDBar ,订阅事件

public class PlayerDodgeCDBar : MonoBehaviour
{
    public Image CD;
    float dodgeCD;

    private void Start() {
        CD.fillAmount = 1;
    }

    // 注册事件监听器
    private void OnEnable()
    {
        PlayerEvents.onUpdateDodgeCD += UpdateDodgeCD;
    }

    // 注销事件监听器
    private void OnDisable()
    {
        PlayerEvents.onUpdateDodgeCD -= UpdateDodgeCD;
    }

    //更新CD UI
    public void UpdateDodgeCD(float dodgeCooldown)
    {
        CD.fillAmount = 0;
        dodgeCD = dodgeCooldown;
    }

    private void Update()
    {
        if(CD.fillAmount == 1) return;
        CD.fillAmount  += Time.deltaTime / dodgeCD;
    }
}

修改DodgeState,闪避时触发更新闪避cd UI

//更新闪避cd UI
PlayerEvents.UpdateDodgeCD(parameter.dodgeCooldown);

效果
在这里插入图片描述

伤害飘字

参考:
【推荐100个unity插件之2】DoTween动画插件的安装和使用整合(最全)
【unity造轮子】伤害飘字效果,封装代码+DoTween实现伤害飘字效果(2024/07/08补充)

效果
在这里插入图片描述

敌人血条

新增EnemyHealthBar ,控制敌人血条

//敌人血条
public class EnemyHealthBar : MonoBehaviour
{
    public Image HP;        // 即时血量UI
    public Image slowHP;    // 缓冲血量UI

    private void Start() {
        HP.fillAmount = 1;
        gameObject.SetActive(false);
    }

    //更新血条UI
    public void UpdateHP(float currentHP, float maxHP)
    {
        gameObject.SetActive(true);
        HP.fillAmount = currentHP / maxHP;
    }

    private void Update()
    {
        // 当缓冲血量大于即时血条时,缓冲血条持续减少
        if (slowHP.fillAmount > HP.fillAmount)
        {
            // 缓冲血量持续减少,Time.daltaTime可换成其他时间参数,如0.01f
            slowHP.fillAmount -= Time.deltaTime;
        }

        if(slowHP.fillAmount < HP.fillAmount)
        {
            // 使缓冲血量与即时血量相等
            slowHP.fillAmount = HP.fillAmount;
        }

        if(slowHP.fillAmount == 0){
            gameObject.SetActive(false);
        }
    }
}

修改EnemyCharacter调用

// 重写生命值属性字段,更改生命值时同步更新敌人血条
public override float CurrentHealth{
    get { return _currentHealth; }
    set{
        _currentHealth = value;
        //更新血条UI
        GetComponentInChildren<EnemyHealthBar>(true).UpdateHP(_currentHealth, maxHealth);
    }
}

配置
在这里插入图片描述

效果
在这里插入图片描述

可破坏物品

新增爆炸粒子特效
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

新增DestructibleCharacter

//可破坏物品
public class DestructibleCharacter : Character
{
    [Header("爆炸特效")]
    public GameObject explosiveEffects ; // 敌人的受击特效动画控制器

    // 死亡逻辑
    protected override void Die()
    {
        Destroy(gameObject);
        //爆炸特效
        Instantiate(explosiveEffects, transform.position, Quaternion.identity);
    }
}

配置
在这里插入图片描述

效果
在这里插入图片描述

随机掉落物品

新增ItemSpawner

using DG.Tweening;
using UnityEngine;

public class ItemSpawner : MonoBehaviour
{
    public PropPrefab[] propPrefabs; // 存储不同种类的物品预制体数组
    public float maxOffsetDistance = 1f;  // 随机偏移的最大距离

    // 初始化生成物品的方法
    public void DropItems()
    {
        foreach (var propPrefab in propPrefabs)
        {
            if (Random.Range(0f, 100f) <= propPrefab.dropPercentage) // 根据掉落概率生成物品
            {
                //生成随机的数量
                int Count = Random.Range(propPrefab.minCount, propPrefab.maxCount + 1);
                for (int i = 0; i < Count; i++){
                    Instantiate(propPrefab.prefab, transform.position, Quaternion.identity);
                }
                
            }
        }
    }
}

[System.Serializable] // Unity 可以序列化该类以在编辑器中显示
public class PropPrefab
{
    public GameObject prefab; // 物品的预制体

    [Range(0f, 100f)] public float dropPercentage; // 掉落的概率范围 0% 到 100%
    public int maxCount = 1;//生成最大数量
    public int minCount = 1;//生成最小数量
}

配置
在这里插入图片描述
调用生成战利品,敌人和可破坏物品都可以调用

//生成掉落物品
GetComponent<ItemSpawner>().DropItems();

效果
在这里插入图片描述

敌人巡逻绕过可破坏物品

放置可破坏物品记得重新生成寻路网格
在这里插入图片描述

拾取物品

修改GameManager

private int _coinCount;//金币数
public int CoinCount{
    get { return _coinCount; }
    set{
        _coinCount = value;
    }
}

//改变金币数量
public void ChangeCoins(int amount)
{
    if (CoinCount + amount < 0)
    {
        Debug.Log("金币不足");
        return;
    }
    CoinCount += amount;
}

新增Item 物品拾取类

/// <summary>
/// 物品拾取类
/// </summary>
public class Item : MonoBehaviour
{
    // 物品类型枚举
    public enum ItemType
    {
        Coin,           // 硬币
        HealingPotion   // 治疗药水
    }

    [Header("基本属性")]
    [SerializeField] private ItemType itemType;          // 物品类型
    [SerializeField] private int value;                      // 物品价值

    [Header("抛物线属性")]
    [SerializeField] private float throwHeight = 1f;         // 抛物线高度
    [SerializeField] private float throwDuration = 1f;       // 抛物时间
    public float maxOffsetDistance = 1f;  // 随机偏移的最大距离

    [Header("拾取范围")]
    [SerializeField] private float pickUpDistance = 3f;      // 自动拾取距离
    [SerializeField] private float moveSpeed = 5f;           // 自动拾取速度

    private bool canPickUp = false;                          // 是否可以拾取

    private PlayerCharacter playerCharacter;                 // 玩家对象的引用

    private void Awake()
    {
        playerCharacter = FindObjectOfType<PlayerCharacter>();
    }

    private void Start()
    {
        ThrowItem();
    }

    private void Update()
    {
        // 如果可以拾取并且玩家在拾取范围内,则向玩家位置移动
        if (canPickUp && Vector2.Distance(transform.position, playerCharacter.transform.position) < pickUpDistance)
        {
            Vector2 dir = (playerCharacter.transform.position - transform.position).normalized;
            transform.Translate(dir * moveSpeed * Time.deltaTime);
        }
    }

    // 抛物线动画
    private void ThrowItem()
    {
        // 使用DOJump方法实现物体的弹跳
        Vector3 randomOffset = new Vector3(Random.Range(-maxOffsetDistance, maxOffsetDistance), Random.Range(-maxOffsetDistance, maxOffsetDistance), 0f);
        transform.DOJump(transform.position + randomOffset, throwHeight, 1, throwDuration).SetEase(Ease.OutSine).OnComplete(() =>
        {
            canPickUp = true;  // 动画完成后可以拾取                                
        });
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        // 如果可以拾取并且碰撞对象是玩家
        if (canPickUp && collision.gameObject.GetComponent<PlayerController>())
        {
            CollectPickup();   // 执行拾取逻辑
        }
    }

    // 根据物品类型执行不同的拾取逻辑
    private void CollectPickup()
    {
        switch (itemType)
        {
            case ItemType.Coin:
                HandleCoinPickup();                            // 执行硬币拾取逻辑
                break;
            case ItemType.HealingPotion:
                HandleHealingPotionPickup();                   // 执行治疗药水拾取逻辑
                break;
        }

        Destroy(gameObject);                                   // 拾取完成后销毁物品对象
    }

    // 处理硬币拾取逻辑
    private void HandleCoinPickup()
    {
        GameManager.Instance.ChangeCoins(value);               // 修改游戏中的硬币数量

        // 在物品位置显示拾取文本效果
        GameManager.Instance.ShowText("+" + value, transform.position, Color.yellow);
    }

    // 处理治疗药水拾取逻辑
    private void HandleHealingPotionPickup()
    {
        playerCharacter.SetHealth(value);// 恢复玩家的生命值
    }
}

效果
在这里插入图片描述

显示金币数

修改PlayerEvents,新增金币变化更新金币UI事件

public static event Action<int> onUpdateCoin;

//更新金币事件
public static void UpdateCoin(int count) 
{
    onUpdateCoin?.Invoke(count);
}

新增CoinUI

//金币UI显示
public class CoinUI : MonoBehaviour
{
    public TextMeshProUGUI coinText;
    
    // 注册事件监听器
    private void OnEnable()
    {
        PlayerEvents.onUpdateCoin += UpdateCoin;
    }

    // 注销事件监听器
    private void OnDisable()
    {
        PlayerEvents.onUpdateCoin -= UpdateCoin;
    }

    public void UpdateCoin(int CoinCount){
        coinText.text = CoinCount.ToString();
    }
}

修改GameManager,调用

private int _coinCount;//金币数
public int CoinCount{
    get { return _coinCount; }
    set{
        _coinCount = value;
        //更新金币文本
        PlayerEvents.UpdateCoin(_coinCount);
    }
}

private void Start() {
	//初始化金币数
 	CoinCount = _coinCount;
}

配置
在这里插入图片描述

效果
在这里插入图片描述

添加更多敌人

新增动画器覆盖控制器即可,这里我加入新的骷髅怪
在这里插入图片描述
在这里插入图片描述
配置
在这里插入图片描述

效果
在这里插入图片描述

敌人全部死亡,生成传送门

修改EnemyManager

public GameObject portalPrefab; //传送门预制体

public void KillEnemy(Transform tf)
{
    enemyCount--;
    if (enemyCount == 0)// 敌人死亡
    {
        if (GetLastWave())
        {
            if (portalPrefab)
            {
                Debug.Log("通关");
                //生成传送门
                Instantiate(portalPrefab, tf.position, Quaternion.identity);
            }
            else
            {
                Debug.Log("游戏胜利");
            }

            return;
        }
        //开始下一波敌人
        currentWaveIndex++; // 当前波次索引增加
        StartCoroutine(nameof(startNextWaveCoroutine));
    }
}

效果
在这里插入图片描述

场景切换

配置新场景Scene2,把需要的组件都迁移过来
在这里插入图片描述

回到场景1,新增退出场景脚本,挂载在传送门上

/// <summary>
/// 场景退出逻辑
/// </summary>
public class SceneExit : MonoBehaviour
{
    [Tooltip("需要切换到的新场景名称")]
    public string newSceneName;
    public float timer = 1f; //多久生效
    private Collider2D coll;

    private void Start() {
        coll = GetComponent<Collider2D>();
        coll.enabled = false;
        Invoke(nameof(SetCollider), timer);
    }

    void SetCollider(){
        coll.enabled = true;
    }

    // 触发器碰撞检测
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            TransitionInternal();
        }
    }

    // 设置内部切换逻辑
    public void TransitionInternal()
    {
        SceneLoader.Instance.TransitionToScene(newSceneName);
    }
}

加载新场景操作

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

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }

        DontDestroyOnLoad(gameObject);
    }

    public void TransitionToScene(string sceneName)
    {
        StartCoroutine(TransitionCoroutine(sceneName));
    }

    // 切换场景的协程
    public IEnumerator TransitionCoroutine(string newSceneName)
    {
        // TODO加载前操作

        // 异步加载新场景
        yield return SceneManager.LoadSceneAsync(newSceneName);

        // TODO加载完成操作
    }
}

配置
在这里插入图片描述
在这里插入图片描述
效果
在这里插入图片描述

场景切换淡入淡出效果

新增画布组件,把没用的组件删除,添加CanvasGroup 组件,通过控制它的透明度控制场景当如淡出效果
在这里插入图片描述
记得还要在画布下添加一个占满屏幕的黑色图片
在这里插入图片描述

新增ScreenFader,使用DOTween实现淡入淡出效果

public class ScreenFader : MonoBehaviour
{
    // 单例模式
    public static ScreenFader Instance { get; private set; }

    public CanvasGroup faderCanvasGroup; // 控制淡入淡出的CanvasGroup组件

    public float fadeDuration = 1f; // 淡入淡出持续时间

    private void Awake()
    {
        // 实现单例模式
        if (Instance == null)
        {
            Instance = this;
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }
        DontDestroyOnLoad(gameObject);  // 切换场景时保持该对象不被销毁
    }

    // 淡入场景
    public IEnumerator FadeSceneIn()
    {
        yield return StartCoroutine(Fade(0f, faderCanvasGroup));
        // 将淡入遮罩的CanvasGroup对象禁用
        faderCanvasGroup.gameObject.SetActive(false);
    }

    // 淡出场景
    public IEnumerator FadeSceneOut()
    {
        // 启用淡出遮罩的CanvasGroup对象
        faderCanvasGroup.gameObject.SetActive(true);
        yield return StartCoroutine(Fade(1f, faderCanvasGroup));
    }

    // 淡入淡出实现
    public IEnumerator Fade(float finalAlpha, CanvasGroup canvasGroup)
    {
        // 使用DOTween实现淡入淡出效果
        yield return canvasGroup.DOFade(finalAlpha, fadeDuration).WaitForCompletion();
    }
}

配置
在这里插入图片描述
修改SceneLoader调用淡入淡出效果

// 切换场景的协程
public IEnumerator TransitionCoroutine(string newSceneName)
{
    // TODO加载前操作

    // 淡出当前场景
    yield return StartCoroutine(ScreenFader.Instance.FadeSceneOut());

    // 异步加载新场景
    yield return SceneManager.LoadSceneAsync(newSceneName);

    // TODO加载完成操作

    // 淡入新场景
    yield return StartCoroutine(ScreenFader.Instance.FadeSceneIn());
}

效果
在这里插入图片描述

切换场景后,保存金币和玩家血量

修改GameManager

private void Start() {
    CoinCount = _coinCount;
    currentHealth = PlayerCharacter.Instance.maxHealth;
}
    
public void Save()
{
    //保存玩家血量
    currentHealth = PlayerCharacter.Instance.CurrentHealth;
}

public void Load()
{
    //更新金币
    CoinCount = _coinCount;
}

修改SceneLoader,调用

// 切换场景的协程
public IEnumerator TransitionCoroutine(string newSceneName)
{
    // TODO加载前操作
    GameManager.Instance.Save();

    // 淡出当前场景
    yield return StartCoroutine(ScreenFader.Instance.FadeSceneOut());

    // 异步加载新场景
    yield return SceneManager.LoadSceneAsync(newSceneName);
	
	//清除 DOTween 库中当前正在进行的所有动画和补间
    DOTween.Clear();
        
    // TODO加载完成操作
    GameManager.Instance.Load();

    // 淡入新场景
    yield return StartCoroutine(ScreenFader.Instance.FadeSceneIn());
}

修改PlayerCharacter,默认读取GameManager的生命值

public override void Start()
{
    base.Start();
    CurrentHealth = GameManager.Instance.currentHealth;
}

效果
在这里插入图片描述

持久化存储数据

参考:【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解

音乐音效管理

参考:【unity小技巧】Unity音乐和音效管理器,持久化存储设置记录(2024/6/30补充)

有时候攻击不造成伤害的问题

可以把刚体碰撞检测设置为持续,提供检测的精度
在这里插入图片描述
设置从不休眠
在这里插入图片描述

源码

整理好我会放上来

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,以便我第一时间收到反馈,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

向宇it

创作不易,感谢你的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值