19.关卡管理器逻辑

  1. 19.游戏场景-关卡管理器逻辑
    1. 19.1 知识点
      1. 创建GameLevelMgr脚本,搞成单例
      2. 需要在SceneInfo表中添加每个场景初始钱和保护区域主塔血量字段,在游戏中要使用。修改excel表,重新转成json,并且要在SceneInfo脚本增加对应变量。
      3. 给各个角色添加PlayeObject脚本并绑定开火点
      4. 在GameLevelMgr脚本创建PlayerObject对象和初始化游戏场景信息方法InitInfo。调用UIManager实例的ShowPanel方法来显示游戏界面显示游戏界面。根据之前记录的当前选中的玩家数据,创建玩家角色。首先从GameDataMgr实例中获取当前选中角色的信息(RoleInfo),然后在场景中找到玩家的出生位置(通过名称为”HeroBornPos”的GameObject找到对应Transform),接着使用玩家角色的资源路径(roleInfo.res)加载预设体资源,并实例化出玩家对象(heroObj)。最后,对玩家对象进行初始化,并设置玩家的基础属性(攻击力和初始金币数)。让主摄像机(Camera.main)跟随玩家移动。通过调用CameraMove组件的SetTarget方法,将摄像机的目标位置设置为动态创建的玩家对象的位置,使摄像机始终跟随玩家。初始化中央保护区域血量:获取MainTowerObject实例,并调用其UpdateHp方法,将中央保护区域的血量进行初始化设置。
      5. 在ChooseScenePanel脚本的点击开始方法后应该要切换场景。开始按钮监听中补充切换场景逻辑。假如直接使用同步加载的话可能会因为场景没初始化完就调用GameLevelMgr初始化导致报错。可以使用异步加载回调的形式进行优化。
      6. 在ChooseHeroPanel脚本的更新角色方法中,要移除角色预制体上的PlayerObject 脚本。否则选角时也会点击开火或者移动。
      7. 在BuildSetting添加开始场景和游戏场景,测试切换场景逻辑
      8. 用关卡管理器总控出怪点和每波怪物相关。创建出怪点列表,当前怪物波数和最大波数,当前怪物变量。提供出怪点列表添加出怪点方法。定义更新有多少波怪的方法。增加最大波数和当前有多少怪没出变量。调用游戏面板进行更新。添加改变当前波数方法,减少未出波变量,调用游戏面板更新方法进行更新。提供检测胜利方法,遍历出怪点列表调用每个出怪点时候有怪物没出完,有的话返回false,检测当前场景怪物变量时候大于0,有点话也返回false,都不满足返回true代表游戏胜利。定义改变当前怪物数量方法。
      9. 在MonsterPoint脚本一开始就把自己添加到关卡管理器的出怪点列表中,调用关卡管理器更新最大波数方法,传入当前自身的最大波数。创建怪物方法中调用关卡管理器减波数方法通知关卡管理器出了一波怪。创建怪物方法中调用关卡管理器的改变怪物数量方法,让怪物数量加1。
      10. 在MonsterObject脚本的死亡事件方法中。调用关卡管理器的改变怪物数量方法,让怪物数量加-1。并且删除以死亡的怪物对象。每次怪物死亡时检测游戏是否胜利。
      11. 在PlayerObject脚本的刀枪攻击时间中,遍历怪物碰撞器数组得到第一个怪物对象,只要怪物不为空且没有死就执行怪物的受伤方法,传入攻击力,跳出循环。
    2. 19.2 知识点代码

19.游戏场景-关卡管理器逻辑


19.1 知识点

创建GameLevelMgr脚本,搞成单例

public class GameLevelMgr
{
    private static GameLevelMgr instance = new GameLevelMgr();
    public static GameLevelMgr Instance => instance;

    private GameLevelMgr()
    {
        // 私有构造函数,避免外部创建实例
    }
}

需要在SceneInfo表中添加每个场景初始钱和保护区域主塔血量字段,在游戏中要使用。修改excel表,重新转成json,并且要在SceneInfo脚本增加对应变量。

[
    {"id":1,"imgRes":"SceneImg/GameScene1","name":"疯狂树林","tips":"啦啦啦啦","sceneName":"GameScene1","money":200,"towerHp":100},
    {"id":2,"imgRes":"SceneImg/GameScene2","name":"极寒雪地","tips":"嘻嘻嘻嘻","sceneName":"GameScene2","money":200,"towerHp":100},
    {"id":3,"imgRes":"SceneImg/GameScene3","name":"熔岩旱土","tips":"哈哈哈哈","sceneName":"GameScene3","money":200,"towerHp":100}
]

/// <summary>
/// 场景数据结构类
/// </summary>
public class SceneInfo
{
    public int id;
    public string imgRes;
    public string name;
    public string tips;
    public string sceneName;
    public int money;
    public int towerHp;
}

给各个角色添加PlayeObject脚本并绑定开火点

在GameLevelMgr脚本创建PlayerObject对象和初始化游戏场景信息方法InitInfo。调用UIManager实例的ShowPanel方法来显示游戏界面显示游戏界面。根据之前记录的当前选中的玩家数据,创建玩家角色。首先从GameDataMgr实例中获取当前选中角色的信息(RoleInfo),然后在场景中找到玩家的出生位置(通过名称为”HeroBornPos”的GameObject找到对应Transform),接着使用玩家角色的资源路径(roleInfo.res)加载预设体资源,并实例化出玩家对象(heroObj)。最后,对玩家对象进行初始化,并设置玩家的基础属性(攻击力和初始金币数)。让主摄像机(Camera.main)跟随玩家移动。通过调用CameraMove组件的SetTarget方法,将摄像机的目标位置设置为动态创建的玩家对象的位置,使摄像机始终跟随玩家。初始化中央保护区域血量:获取MainTowerObject实例,并调用其UpdateHp方法,将中央保护区域的血量进行初始化设置。

public PlayerObject player; // 玩家对象    

// 初始化游戏场景信息
// 在切换到游戏场景时调用,用于动态创建玩家并初始化相关信息
public void InitInfo(SceneInfo info)
{
    // 显示游戏界面
    UIManager.Instance.ShowPanel<GamePanel>();

    // 创建玩家
    // 获取之前记录的当前选中的玩家数据
    RoleInfo roleInfo = GameDataMgr.Instance.nowSelRole;
    // 获取场景中玩家的出生位置
    Transform heroPos = GameObject.Find("HeroBornPos").transform;
    // 实例化玩家预设体,并将位置和角度设置为与场景中的出生点一致
    GameObject heroObj = GameObject.Instantiate(Resources.Load<GameObject>(roleInfo.res), heroPos.position, heroPos.rotation);
    // 对玩家对象进行初始化
    player = heroObj.GetComponent<PlayerObject>();
    // 初始化玩家的基础属性
    player.InitPlayerInfo(roleInfo.atk, info.money);

    // 让摄像机看向动态创建出来的玩家
    Camera.main.GetComponent<CameraMove>().SetTarget(heroObj.transform);

    // 初始化中央保护区域的血量
    MainTowerObject.Instance.UpdateHp(info.towerHp, info.towerHp);
}

在ChooseScenePanel脚本的点击开始方法后应该要切换场景。开始按钮监听中补充切换场景逻辑。假如直接使用同步加载的话可能会因为场景没初始化完就调用GameLevelMgr初始化导致报错。可以使用异步加载回调的形式进行优化。

btnStart.onClick.AddListener(() =>
{
    //隐藏当前面板 
    UIManager.Instance.HidePanel<ChooseScenePanel>();
    //切换场景
    AsyncOperation ao = SceneManager.LoadSceneAsync(nowSceneInfo.sceneName);
    //进行关卡初始化
    ao.completed += (obj) =>
    {
        GameLevelMgr.Instance.InitInfo(nowSceneInfo);
    };
});

在ChooseHeroPanel脚本的更新角色方法中,要移除角色预制体上的PlayerObject 脚本。否则选角时也会点击开火或者移动。

/// <summary>
/// 更新场景上要显示的模型的
/// </summary>
private void ChangeHero()
{
    if(heroObj != null)
    {
        Destroy(heroObj);
        heroObj = null;
    }
    
    //取出数据的一条 根据索引值
    nowRoleData = GameDataMgr.Instance.roleInfoList[nowIndex];
    //实例化对象 并记录下来 用于下次切换时 删除
    heroObj = Instantiate(Resources.Load<GameObject>(nowRoleData.res), heroPos.position, heroPos.rotation);
    //由于我们现在在对象上挂载了PlayerObject 但是在开始场景 不需要
    Destroy(heroObj.GetComponent<PlayerObject>());
    
    //更新上方显示的 描述信息
    txtName.text = nowRoleData.tips;
    
    //根据解锁相关数据 来决定是否显示解锁按钮
    UpdateLockBtn();
}

在BuildSetting添加开始场景和游戏场景,测试切换场景逻辑

用关卡管理器总控出怪点和每波怪物相关。创建出怪点列表,当前怪物波数和最大波数,当前怪物变量。提供出怪点列表添加出怪点方法。定义更新有多少波怪的方法。增加最大波数和当前有多少怪没出变量。调用游戏面板进行更新。添加改变当前波数方法,减少未出波变量,调用游戏面板更新方法进行更新。提供检测胜利方法,遍历出怪点列表调用每个出怪点时候有怪物没出完,有的话返回false,检测当前场景怪物变量时候大于0,有点话也返回false,都不满足返回true代表游戏胜利。定义改变当前怪物数量方法。

private List<MonsterPoint> points = new List<MonsterPoint>(); // 所有的出怪点
private int nowWaveNum = 0; // 记录当前还有多少波怪物没出
private int maxWaveNum = 0; // 记录一共有多少波怪物
private int nowMonsterNum = 0; // 记录当前场景上的怪物数量


// 添加怪物出怪点的方法
public void AddMonsterPoint(MonsterPoint point)
{
    points.Add(point);
}

/// <summary>
/// 更新一共有多少波怪
/// </summary>
/// <param name="num">新增的波数</param>
public void UpdatgeMaxNum(int num)
{
    // 更新最大波数和当前波数,并更新界面显示
    maxWaveNum += num;
    nowWaveNum = maxWaveNum;
    UIManager.Instance.GetPanel<GamePanel>().UpdateWaveNum(nowWaveNum, maxWaveNum);
}

// 改变当前波数的方法
public void ChangeNowWaveNum(int num)
{
    // 减去指定数量的怪物波数,并更新界面显示
    nowWaveNum -= num;
    UIManager.Instance.GetPanel<GamePanel>().UpdateWaveNum(nowWaveNum, maxWaveNum);
}

/// <summary>
/// 检测是否胜利
/// </summary>
/// <returns>是否胜利</returns>
public bool CheckOver()
{
    // 检查每个出怪点是否还有怪物没有出完,如果有则返回false
    for (int i = 0; i < points.Count; i++)
    {
        if (!points[i].CheckOver())
            return false;
    }

    // 检查场景中是否还有未死亡的怪物,如果有则返回false
    if (nowMonsterNum > 0)
        return false;

    Debug.Log("游戏胜利");
    return true;    // 所有条件满足,游戏胜利
}

/// <summary>
/// 改变当前场景上怪物的数量
/// </summary>
/// <param name="num">改变的数量</param>
public void ChangeMonsterNum(int num)
{
    // 改变当前场景上的怪物数量
    nowMonsterNum += num;
}

在MonsterPoint脚本一开始就把自己添加到关卡管理器的出怪点列表中,调用关卡管理器更新最大波数方法,传入当前自身的最大波数。创建怪物方法中调用关卡管理器减波数方法通知关卡管理器出了一波怪。创建怪物方法中调用关卡管理器的改变怪物数量方法,让怪物数量加1。

void Start()
{
    Invoke("CreateWave", firstDelayTime);

    // 记录出怪点
    GameLevelMgr.Instance.AddMonsterPoint(this);
    // 更新最大波数
    GameLevelMgr.Instance.UpdatgeMaxNum(maxWave);
}

/// <summary>
/// 开始创建一波的怪物
/// </summary>
private void CreateWave()
{
    // 得到当前波怪物的ID是什么
    nowID = monsterIDs[Random.Range(0, monsterIDs.Count)];
    // 当前波怪物有多少只
    nowNum = monsterNumOneWave;
    // 创建怪物
    CreateMonster();
    // 减少波数
    --maxWave;
    // 通知关卡管理器出了一波怪
    GameLevelMgr.Instance.ChangeNowWaveNum(1);
}

/// <summary>
/// 创建怪物
/// </summary>
private void CreateMonster()
{
    // 直接创建怪物
    // 取出怪物数据
    MonsterInfo info = GameDataMgr.Instance.monsterInfoList[nowID - 1];

    // 创建怪物预设体
    GameObject obj = Instantiate(Resources.Load<GameObject>(info.res), this.transform.position, Quaternion.identity);
    // 为我们创建出的怪物预设体添加怪物脚本进行初始化
    MonsterObject monsterObj = obj.AddComponent<MonsterObject>();
    monsterObj.InitInfo(info);

    // 告诉管理器怪物数量加1
    GameLevelMgr.Instance.ChangeMonsterNum(1);

    // 创建完一只怪物后减去要创建的怪物数量1
    --nowNum;
    if (nowNum == 0)
    {
        if (maxWave > 0)
            Invoke("CreateWave", delayTime);
    }
    else
    {
        Invoke("CreateMonster", createOffsetTime);
    }
}

在MonsterObject脚本的死亡事件方法中。调用关卡管理器的改变怪物数量方法,让怪物数量加-1。并且删除以死亡的怪物对象。每次怪物死亡时检测游戏是否胜利。

// 死亡动画播放完毕后会调用的事件方法
public void DeadEvent()
{
    // 死亡动画播放完毕后移除对象
    // 之后有了关卡管理器再来处理

    // GameLevelMgr.Instance.ChangeMonsterNum(-1);

    // 在场景中移除已经死亡的对象
    Destroy(this.gameObject);

    // 怪物死亡时检测游戏是否胜利
    if (GameLevelMgr.Instance.CheckOver())
    {

    }
}

在PlayerObject脚本的刀枪攻击时间中,遍历怪物碰撞器数组得到第一个怪物对象,只要怪物不为空且没有死就执行怪物的受伤方法,传入攻击力,跳出循环。

/// <summary>
/// 专门用于处理刀武器攻击动作的伤害检测事件
/// </summary>
public void KnifeEvent()
{
    // 进行伤害检测
    Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("Monster"));

    // 播放音效
    GameDataMgr.Instance.PlaySound("Music/Knife");

    // 暂时无法继续写逻辑了 因为 我们没有怪物对应的脚本
    for (int i = 0; i < colliders.Length; i++)
    {
        // 得到碰撞到的对象上的怪物脚本 让其受伤
        MonsterObject monster = colliders[i].gameObject.GetComponent<MonsterObject>();
        if (monster != null && !monster.isDead)
        {
            monster.Wound(this.atk);
            break;
        }
    }
}

public void ShootEvent()
{
    // 进行摄像检测 
    // 前提是需要有开火点
    RaycastHit[] hits = Physics.RaycastAll(new Ray(gunPoint.position, this.transform.forward), 1000, 1 << LayerMask.NameToLayer("Monster"));

    // 播放开枪音效
    GameDataMgr.Instance.PlaySound("Music/Gun");

    for (int i = 0; i < hits.Length; i++)
    {
        // 得到对象上的怪物脚本 让其受伤
        // 得到碰撞到的对象上的怪物脚本 让其受伤
        MonsterObject monster = hits[i].collider.gameObject.GetComponent<MonsterObject>();
        if (monster != null && !monster.isDead)
        {
            // 进行打击特效的创建
            GameObject effObj = Instantiate(Resources.Load<GameObject>(GameDataMgr.Instance.nowSelRole.hitEff));
            effObj.transform.position = hits[i].point;
            effObj.transform.rotation = Quaternion.LookRotation(hits[i].normal);
            Destroy(effObj, 1);

            monster.Wound(this.atk);
            break;
        }
    }
}

19.2 知识点代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameLevelMgr
{
    private static GameLevelMgr instance = new GameLevelMgr();
    public static GameLevelMgr Instance => instance;

    private GameLevelMgr()
    {
        // 私有构造函数,避免外部创建实例
    }

    public PlayerObject player; // 玩家对象

    private List<MonsterPoint> points = new List<MonsterPoint>(); // 所有的出怪点
    private int nowWaveNum = 0; // 记录当前还有多少波怪物未出
    private int maxWaveNum = 0; // 记录一共有多少波怪物
    private int nowMonsterNum = 0; // 记录当前场景上的怪物数量

    // 初始化游戏场景信息
    // 在切换到游戏场景时调用,用于动态创建玩家并初始化相关信息
    public void InitInfo(SceneInfo info)
    {
        // 显示游戏界面
        UIManager.Instance.ShowPanel<GamePanel>();

        // 创建玩家
        // 获取之前记录的当前选中的玩家数据
        RoleInfo roleInfo = GameDataMgr.Instance.nowSelRole;
        // 获取场景中玩家的出生位置
        Transform heroPos = GameObject.Find("HeroBornPos").transform;
        // 实例化玩家预设体,并将位置和角度设置为与场景中的出生点一致
        GameObject heroObj = GameObject.Instantiate(Resources.Load<GameObject>(roleInfo.res), heroPos.position, heroPos.rotation);
        // 对玩家对象进行初始化
        player = heroObj.GetComponent<PlayerObject>();
        // 初始化玩家的基础属性
        player.InitPlayerInfo(roleInfo.atk, info.money);

        // 让摄像机看向动态创建出来的玩家
        Camera.main.GetComponent<CameraMove>().SetTarget(heroObj.transform);

        // 初始化中央保护区域的血量
        MainTowerObject.Instance.UpdateHp(info.towerHp, info.towerHp);
    }

    // 添加怪物出怪点的方法
    public void AddMonsterPoint(MonsterPoint point)
    {
        points.Add(point);
    }

    /// <summary>
    /// 更新一共有多少波怪
    /// </summary>
    /// <param name="num">新增的波数</param>
    public void UpdatgeMaxNum(int num)
    {
        // 更新最大波数和当前波数,并更新界面显示
        maxWaveNum += num;
        nowWaveNum = maxWaveNum;
        UIManager.Instance.GetPanel<GamePanel>().UpdateWaveNum(nowWaveNum, maxWaveNum);
    }

    // 改变当前波数的方法
    public void ChangeNowWaveNum(int num)
    {
        // 减去指定数量的怪物波数,并更新界面显示
        nowWaveNum -= num;
        UIManager.Instance.GetPanel<GamePanel>().UpdateWaveNum(nowWaveNum, maxWaveNum);
    }

    /// <summary>
    /// 检测是否胜利
    /// </summary>
    /// <returns>是否胜利</returns>
    public bool CheckOver()
    {
        // 检查每个出怪点是否还有怪物没有出完,如果有则返回false
        for (int i = 0; i < points.Count; i++)
        {
            if (!points[i].CheckOver())
                return false;
        }

        // 检查场景中是否还有未死亡的怪物,如果有则返回false
        if (nowMonsterNum > 0)
            return false;

        Debug.Log("游戏胜利");
        return true;    // 所有条件满足,游戏胜利
    }

    /// <summary>
    /// 改变当前场景上怪物的数量
    /// </summary>
    /// <param name="num">改变的数量</param>
    public void ChangeMonsterNum(int num)
    {
        // 改变当前场景上的怪物数量
        nowMonsterNum += num;
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏