17.怪物逻辑

  1. 17.游戏场景-怪物逻辑
    1. 17.1 状态机准备
      1. 创建怪物的Animator文件在基础动画层添加出生,待机,跑步死亡动画。注意选择的动画只是有动作而不要有实际的移动,因为实际的移动是之后通过导航代理实现的。设置好过渡和一些参数。记得要把死亡状态的要把能够切换自己取消。创建攻击和受伤层。人形遮罩可以复用之前的,只受到上半身影响。因为是新的层所以设置一个默认的空状态。创建攻击状态和受伤状态并添加过渡。攻击状态可使用子状态机。子状态机设置两个攻击动画,代表左右手各攻击一次后才算完整的攻击状态。攻击和受过渡条件要非死亡状态才行,不然死亡后也可能播放攻击动画。
      2. 对于其他怪物的动画使用动画覆盖器,拖拽对应怪物的动画
      3. 创建多个怪物预制体,在预制体添加Animator组件,不要关联。之后会准备配置文件数据通过代码关联各个怪物的Animator文件。这样四个怪物和四个动画就能产生16种排列组合。
    2. 17.2 数据准备
      1. 创建excel表,配置怪物的id,怪物预制体路径,怪物动画文件路径,攻击力,移动速度,选择速度,血量,攻击间隔。去网站上转成json。取名MonsterInfo.json存储到StreamAssets文件夹下。
      2. 创建怪物数据脚本MonsterInfo,变量名要和配置表一一对应
      3. 在GameDataMgr脚本中,关联一个MonsterInfo列表。在构造函数中用Json数据管理器初始化读取。
    3. 17.3 逻辑处理
      1. 创建一个怪物对象类MonsterObject脚本,继承MonoBehaviour。挂载到怪物预制体上。给各个怪物预制体添加寻路组件和胶囊碰撞器。添加胶囊碰撞器是因为之后要收到玩家伤害。
      2. 声明动画组件和寻路组件变量。声明当前怪物信息数据,血量,死亡标识,上次攻击时间变量。在Awake的时候拿到动画组件和寻路组件。
      3. 创建提供给外部初始化怪物的信息方法InitInfo(MonsterInfo info)。将传入的MonsterInfo对象赋值给私有变量monsterInfo,并根据怪物信息中的配置设置动画控制器、血量、移动速度和旋转速度。
      4. 创建受伤方法Wound(int dmg),外部传入伤害。对血量进行减少并设置受伤动画参数,设置受伤参数播放受伤动画。判断血量是否小于0,走死亡逻辑或者播放音效逻辑,这段逻辑待补充。
      5. 创建死亡方法Dead。修改死亡标识为true,代理组件中停止移动标识也为true。设置死亡动画参数播放死亡动画。播放音效和杀死怪物加钱逻辑待补充。
      6. 在各个怪物的死亡动画播放完之后,添加死亡后要执行的事件。在死亡动画播放完后应该移除怪物对象,这段逻辑之后有了关卡管理器再处理。
      7. 在各个怪物出生动画播放完之后,应该添加出生后要执行的事件。出生结束后,通过设置代理组件的目标位置让怪物朝目标点移动。设置跑步动画参数来播放跑步动画。
      8. Update中写攻击逻辑。假如怪物死亡就直接返回。只要代理组件的速度不为0,就设置跑步动画参数为true来播放跑步动画。检测离保护区域主塔点的位置小于5米并且当前时间减上次攻击时间大于攻击间隔,就重置上次攻击时间为当前时间,设置攻击动画参数来播放动画。
      9. 在各个怪物的攻击时添加攻击事件。通过范围检测,在怪物面前1m创建一个半径一米的球进行检测,只检测主塔层。主塔层MainTower要进行添加判断是否碰撞到了主塔对象,调用保护区受伤方法,传入怪物攻击力让保护区受伤。
      10. 注意寻路组件上的停止参数要设置的大一些,不然一直达不到保护区域点会一直跑
      11. 注意怪物预制体所有子物体都改成Monster层。
    4. 17.4 代码
      1. MonsterInfo
      2. GameDataMgr
      3. MonsterObject
      4. MonsterObject.json

17.游戏场景-怪物逻辑


17.1 状态机准备

创建怪物的Animator文件在基础动画层添加出生,待机,跑步死亡动画。注意选择的动画只是有动作而不要有实际的移动,因为实际的移动是之后通过导航代理实现的。设置好过渡和一些参数。记得要把死亡状态的要把能够切换自己取消。创建攻击和受伤层。人形遮罩可以复用之前的,只受到上半身影响。因为是新的层所以设置一个默认的空状态。创建攻击状态和受伤状态并添加过渡。攻击状态可使用子状态机。子状态机设置两个攻击动画,代表左右手各攻击一次后才算完整的攻击状态。攻击和受过渡条件要非死亡状态才行,不然死亡后也可能播放攻击动画。






对于其他怪物的动画使用动画覆盖器,拖拽对应怪物的动画

创建多个怪物预制体,在预制体添加Animator组件,不要关联。之后会准备配置文件数据通过代码关联各个怪物的Animator文件。这样四个怪物和四个动画就能产生16种排列组合。


17.2 数据准备

创建excel表,配置怪物的id,怪物预制体路径,怪物动画文件路径,攻击力,移动速度,选择速度,血量,攻击间隔。去网站上转成json。取名MonsterInfo.json存储到StreamAssets文件夹下。

[
    {"id":1,"res":"Monster/Z1","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8},
    {"id":2,"res":"Monster/Z2","animator":"Animator/Monster/Broken","atk":5,"moveSpeed":50,"roundSpeed":100,"hp":10,"atkOffset":1.2},
    {"id":3,"res":"Monster/Z3","animator":"Animator/Monster/Calm","atk":5,"moveSpeed":60,"roundSpeed":100,"hp":10,"atkOffset":1},
    {"id":4,"res":"Monster/Z4","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.5},
    {"id":5,"res":"Monster/Z2","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":110,"roundSpeed":100,"hp":10,"atkOffset":0.8},
    {"id":6,"res":"Monster/Z3","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8}
]

创建怪物数据脚本MonsterInfo,变量名要和配置表一一对应

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

public class MonsterInfo
{
    public int id;
    public string res;
    public string animator;
    public int atk;
    public int moveSpeed;
    public int roundSpeed;
    public int hp;
    public float atkOffset;
}

在GameDataMgr脚本中,关联一个MonsterInfo列表。在构造函数中用Json数据管理器初始化读取。

/// <summary>
/// 专门用来管理数据的类
/// </summary>
public class GameDataMgr
{
    private static GameDataMgr instance = new GameDataMgr();
    public static GameDataMgr Instance => instance;

    //所有的怪物数据
    public List<MonsterInfo> monsterInfoList;

    private GameDataMgr()
    {
        //读取怪物数据
        monsterInfoList = JsonMgr.Instance.LoadData<List<MonsterInfo>>("MonsterInfo");
    }
}

17.3 逻辑处理

创建一个怪物对象类MonsterObject脚本,继承MonoBehaviour。挂载到怪物预制体上。给各个怪物预制体添加寻路组件和胶囊碰撞器。添加胶囊碰撞器是因为之后要收到玩家伤害。

public class MonsterObject : MonoBehaviour

声明动画组件和寻路组件变量。声明当前怪物信息数据,血量,死亡标识,上次攻击时间变量。在Awake的时候拿到动画组件和寻路组件。

    // 动画相关
    private Animator animator;
    // 位移相关 寻路组件
    private NavMeshAgent agent;
    // 一些不变的基础数据
    private MonsterInfo monsterInfo;
    // 当前血量
    private int hp;
    // 怪物是否死亡
    public bool isDead = false;
    // 上一次攻击的时间
    private float frontTime;

    void Awake()
    {
        agent = this.GetComponent<NavMeshAgent>();  // 获取当前物体上的NavMeshAgent组件
        animator = this.GetComponent<Animator>();   // 获取当前物体上的Animator组件
    }

创建提供给外部初始化怪物的信息方法InitInfo(MonsterInfo info)。将传入的MonsterInfo对象赋值给私有变量monsterInfo,并根据怪物信息中的配置设置动画控制器、血量、移动速度和旋转速度。

    // 初始化
    public void InitInfo(MonsterInfo info)
    {
        monsterInfo = info;  // 将传入的怪物信息赋值给monsterInfo

        // 状态机加载
        animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(info.animator);
        // 要变的当前血量
        hp = info.hp;
        // 速度和加速度赋值 之所以赋值一样 是希望没有 明显的加速运动 而是一个匀速运动 初始化
        agent.speed = agent.acceleration = info.moveSpeed;
        // 旋转速度
        agent.angularSpeed = info.roundSpeed;
    }

创建受伤方法Wound(int dmg),外部传入伤害。对血量进行减少并设置受伤动画参数,设置受伤参数播放受伤动画。判断血量是否小于0,走死亡逻辑或者播放音效逻辑,这段逻辑待补充。

    // 受伤
    public void Wound(int dmg)
    {
        hp -= dmg;  // 减少血量
        animator.SetTrigger("Wound");  // 播放受伤动画

        if (hp <= 0)
        {
            // 死亡
        }
        else
        {
            // 播放音效
        }
    }

创建死亡方法Dead。修改死亡标识为true,代理组件中停止移动标识也为true。设置死亡动画参数播放死亡动画。播放音效和杀死怪物加钱逻辑待补充。

    // 死亡
    public void Dead()
    {
        isDead = true;
        agent.isStopped = true;  // 停止移动
        animator.SetBool("Dead", true);  // 播放死亡动画

        // 播放音效

        // 加钱——我们之后通过关卡管理类 来管理游戏中的对象 通过它来让玩家加钱 
    }

在各个怪物的死亡动画播放完之后,添加死亡后要执行的事件。在死亡动画播放完后应该移除怪物对象,这段逻辑之后有了关卡管理器再处理。

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

在各个怪物出生动画播放完之后,应该添加出生后要执行的事件。出生结束后,通过设置代理组件的目标位置让怪物朝目标点移动。设置跑步动画参数来播放跑步动画。

    // 出生过后再移动
    // 移动——寻路组件
    public void BornOver()
    {
        // 出生结束后 再让怪物朝目标点移动
        agent.SetDestination(MainTowerObject.Instance.transform.position);
        // 播放移动动画
        animator.SetBool("Run", true);
    }

Update中写攻击逻辑。假如怪物死亡就直接返回。只要代理组件的速度不为0,就设置跑步动画参数为true来播放跑步动画。检测离保护区域主塔点的位置小于5米并且当前时间减上次攻击时间大于攻击间隔,就重置上次攻击时间为当前时间,设置攻击动画参数来播放动画。

    // 攻击
    void Update()
    {
        if (isDead)
            return;

        // 根据速度 来决定动画播放什么
        animator.SetBool("Run", agent.velocity != Vector3.zero);

        // 检测和目标点达到移动条件时 就攻击
        if (Vector3.Distance(this.transform.position, MainTowerObject.Instance.transform.position) < 5 &&
            Time.time - frontTime >= monsterInfo.atkOffset)
        {
            // 记录这次攻击时的时间
            frontTime = Time.time;
            animator.SetTrigger("Atk");
        }
    }

在各个怪物的攻击时添加攻击事件。通过范围检测,在怪物面前1m创建一个半径一米的球进行检测,只检测主塔层。主塔层MainTower要进行添加判断是否碰撞到了主塔对象,调用保护区受伤方法,传入怪物攻击力让保护区受伤。

// 伤害检测
public void AtkEvent()
{
    // 范围检测 进行伤害判断
    Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("MainTower"));
    for (int i = 0; i < colliders.Length; i++)
    {
        if (MainTowerObject.Instance.gameObject == colliders[i].gameObject)
        {
            // 让保护区域受到伤害
            MainTowerObject.Instance.Wound(monsterInfo.atk);
        }
    }
}

注意寻路组件上的停止参数要设置的大一些,不然一直达不到保护区域点会一直跑

注意怪物预制体所有子物体都改成Monster层。


17.4 代码

MonsterInfo

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

public class MonsterInfo
{
    public int id;
    public string res;
    public string animator;
    public int atk;
    public int moveSpeed;
    public int roundSpeed;
    public int hp;
    public float atkOffset;
}

GameDataMgr

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

/// <summary>
/// 专门用来管理数据的类
/// </summary>
public class GameDataMgr
{
    private static GameDataMgr instance = new GameDataMgr();
    public static GameDataMgr Instance => instance;

    //记录选择的角色数据 用于之后在游戏场景中创建
    public RoleInfo nowSelRole;

    //音效相关数据
    public MusicData musicData;

    //玩家相关数据
    public PlayerData playerData;

    //所有的角色数据
    public List<RoleInfo> roleInfoList;

    //所有的场景数据
    public List<SceneInfo> sceneInfoList;

    //所有的怪物数据
    public List<MonsterInfo> monsterInfoList;

    private GameDataMgr()
    {
        //初始化一些默认数据
        musicData = JsonMgr.Instance.LoadData<MusicData>("MusicData");
        //获取初始化玩家数据
        playerData = JsonMgr.Instance.LoadData<PlayerData>("PlayerData");
        //读取角色数据
        roleInfoList = JsonMgr.Instance.LoadData<List<RoleInfo>>("RoleInfo");
        //读取场景数据
        sceneInfoList = JsonMgr.Instance.LoadData<List<SceneInfo>>("SceneInfo");
        //读取怪物数据
        monsterInfoList = JsonMgr.Instance.LoadData<List<MonsterInfo>>("MonsterInfo");
    }

    /// <summary>
    /// 存储音效数据
    /// </summary>
    public void SaveMusicData()
    {
        JsonMgr.Instance.SaveData(musicData, "MusicData");
    }

    /// <summary>
    /// 存储玩家数据
    /// </summary>
    public void SavePlayerData()
    {
        JsonMgr.Instance.SaveData(playerData, "PlayerData");
    }
}

MonsterObject

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

public class MonsterObject : MonoBehaviour
{
    // 动画相关
    private Animator animator;
    // 位移相关 寻路组件
    private NavMeshAgent agent;
    // 一些不变的基础数据
    private MonsterInfo monsterInfo;
    // 当前血量
    private int hp;
    // 怪物是否死亡
    public bool isDead = false;
    // 上一次攻击的时间
    private float frontTime;

    void Awake()
    {
        agent = this.GetComponent<NavMeshAgent>();  // 获取当前物体上的NavMeshAgent组件
        animator = this.GetComponent<Animator>();   // 获取当前物体上的Animator组件
    }

    // 初始化
    public void InitInfo(MonsterInfo info)
    {
        monsterInfo = info;  // 将传入的怪物信息赋值给monsterInfo

        // 状态机加载
        animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(info.animator);
        // 要变的当前血量
        hp = info.hp;
        // 速度和加速度赋值 之所以赋值一样 是希望没有 明显的加速运动 而是一个匀速运动 初始化
        agent.speed = agent.acceleration = info.moveSpeed;
        // 旋转速度
        agent.angularSpeed = info.roundSpeed;
    }

    // 受伤
    public void Wound(int dmg)
    {
        hp -= dmg;  // 减少血量
        animator.SetTrigger("Wound");  // 播放受伤动画

        if (hp <= 0)
        {
            // 死亡
        }
        else
        {
            // 播放音效
        }
    }

    // 死亡
    public void Dead()
    {
        isDead = true;
        agent.isStopped = true;  // 停止移动
        animator.SetBool("Dead", true);  // 播放死亡动画

        // 播放音效

        // 加钱——我们之后通过关卡管理类 来管理游戏中的对象 通过它来让玩家加钱 
    }

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

    // 出生过后再移动
    // 移动——寻路组件
    public void BornOver()
    {
        // 出生结束后 再让怪物朝目标点移动
        agent.SetDestination(MainTowerObject.Instance.transform.position);
        // 播放移动动画
        animator.SetBool("Run", true);
    }

    // 攻击
    void Update()
    {
        if (isDead)
            return;

        // 根据速度 来决定动画播放什么
        animator.SetBool("Run", agent.velocity != Vector3.zero);

        // 检测和目标点达到移动条件时 就攻击
        if (Vector3.Distance(this.transform.position, MainTowerObject.Instance.transform.position) < 5 &&
            Time.time - frontTime >= monsterInfo.atkOffset)
        {
            // 记录这次攻击时的时间
            frontTime = Time.time;
            animator.SetTrigger("Atk");
        }
    }

    // 伤害检测
    public void AtkEvent()
    {
        // 范围检测 进行伤害判断
        Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("MainTower"));
        for (int i = 0; i < colliders.Length; i++)
        {
            if (MainTowerObject.Instance.gameObject == colliders[i].gameObject)
            {
                // 让保护区域受到伤害
                MainTowerObject.Instance.Wound(monsterInfo.atk);
            }
        }
    }
}

MonsterObject.json

[
{"id":1,"res":"Monster/Z1","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":2,"res":"Monster/Z2","animator":"Animator/Monster/Broken","atk":5,"moveSpeed":50,"roundSpeed":100,"hp":10,"atkOffset":1.2},
{"id":3,"res":"Monster/Z3","animator":"Animator/Monster/Calm","atk":5,"moveSpeed":60,"roundSpeed":100,"hp":10,"atkOffset":1},
{"id":4,"res":"Monster/Z4","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.5},
{"id":5,"res":"Monster/Z2","animator":"Animator/Monster/Aggresive","atk":5,"moveSpeed":110,"roundSpeed":100,"hp":10,"atkOffset":0.8},
{"id":6,"res":"Monster/Z3","animator":"Animator/Monster/Crawl","atk":5,"moveSpeed":100,"roundSpeed":100,"hp":10,"atkOffset":0.8}
]


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

×

喜欢就点赞,疼爱就打赏