Unity核心实践小项目

要源码包的私信我。

简介

衔接Unity核心学习后的实操小项目

需求分析

准备工作

面板基类

为了能够控制一画布整体的透明度,所以需要给每个面板都添加一个 CanvasGroup组件

UI管理器

UGUI方面的参数设置

开始场景

场景搭建

直接用资源包搭建好的场景:Demo1 (PC端)

Demo2_mobile 是移动端的

将场景Demo1 复制到 Scenes文件夹下

删除

调整好相机 这样即可

开始界面

拼界面

拖入两个僵尸,创建动画状态机,拖入啃食动画和倒下动画到状态机里(注意要拖入循环动画),再把动画状态机拖入僵尸模型的Animator 组件中

界面逻辑

Main主路口

设置界面

拼界面

背景音乐数据

创建Data 数据文件夹

创建 BkMusic 用于管理背景音乐

界面逻辑

摄像机动画逻辑

先为摄像机做四个动画:idle(上下缓动)、turnLeft(左转摄像机)、turnRight(右转摄像机)、leftIdle(左上下缓动)

创建 CameraAnimator 脚本

BeginPanel 代码添加

人物选择界面

拼界面

资源准备

1.准备好人物模型

2.给人物都配好相应的武器

3.创建人物的动画状态机

双击进入

设置一些参数

分析出有9个动作

设置值和匹配动画

拖入翻滚动画

用到动画遮罩知识点,创建人物攻击动画

创建人物蹲下动画

一个人物的证套动作就完成了

运用 动画状态机复用功能为其他人物创建动画状态机

绑定相应动画就可以

数据准备

创建人物数据

转Json

创建玩家数据类

界面逻辑

先添加一个购买按钮

ChooseHeroPanel 逻辑

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class ChooseHeroPanel : BasePanel
{
    //左右键
    public Button btnLeft;
    public Button btnRight;

    //购买按钮
    public Button btnUnLock;
    public Text txtUnLock;

    //开始和返回
    public Button btnStart;
    public Button btnBack;

    //左上角拥有的钱
    public Text txtMoney;

    //角色信息
    public Text txtName;

    //英雄预设体需要创建在的位置
    private Transform heroPos;

    //当前场景中显示的对象
    private GameObject heroObj;
    //当前使用的数据
    private RoleInfo nowRoleData;
    //当前使用数据的索引
    private int nowIndex;

    public override void Init()
    {
        //一开始就找到场景中 放置对象预设体的位置
        heroPos = GameObject.Find("HeroPos").transform;

        //更新左上角玩家拥有的钱
        txtMoney.text = GameDataMgr.Instance.playerData.haveMoney.ToString();

        btnLeft.onClick.AddListener(() =>
        {
            --nowIndex;
            if (nowIndex < 0)
                nowIndex = GameDataMgr.Instance.roleInfoList.Count - 1;
            //模型的更新
            ChangeHero();
        });

        btnRight.onClick.AddListener(() =>
        {
            ++nowIndex;
            if (nowIndex >= GameDataMgr.Instance.roleInfoList.Count)
                nowIndex = 0;
            //模型更新
            ChangeHero();
        });

        btnUnLock.onClick.AddListener(() =>
        {
            //点击解锁按钮的逻辑
            PlayerData data = GameDataMgr.Instance.playerData;
            //当有钱时
            if (data.haveMoney >= nowRoleData.lockMoney)
            {
                //购买逻辑
                //减去花费
                data.haveMoney -= nowRoleData.lockMoney;
                //更新界面显示
                txtMoney.text = data.haveMoney.ToString();
                //记录购买的id
                data.buyHero.Add(nowRoleData.id);
                //保存数据
                GameDataMgr.Instance.SavePlayerData();

                //更新解锁按钮
                UpdateLockBtn();

                //提示面板 显示购买成功
                print("购买成功");
            }
            else
            {
                //提示面板 显示 金钱不足
                print("金币不足!");
            }
        });

        btnStart.onClick.AddListener(() =>
        {
            //第一 是记录当前选择的角色
            //因为 GameDataMgr 是单例模式 所以就算切场景了数据也不会删除,它是唯一的
            //后面我们可以通过单例模式的对象去获取里面的信息,相当于将数据传递到了 GameDataMgr中,间接的帮我们存储数据
            GameDataMgr.Instance.nowSelRole = nowRoleData;

            //第二 是隐藏自己 显示场景选择界面
            UIManager.Instance.HidePanel<ChooseHeroPanel>();
        });

        btnBack.onClick.AddListener(() =>
        {
            //隐藏自己
            UIManager.Instance.HidePanel<ChooseHeroPanel>();

            //播放切换摄像机动画
            //先得到主摄像机
            Camera.main.GetComponent<CameraAnimator>().TurnRight(() =>
            {
                //动画播放完后 显示开始界面
                UIManager.Instance.ShowPanel<BeginPanel>();
            });
        });

        //更新模型显示
        ChangeHero();
    }

    /// <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);

        //根据解锁相关数据 来决定是否显示解锁按钮
        UpdateLockBtn();

    }

    /// <summary>
    ///更新解锁按钮显示情况
    /// </summary>
    private void UpdateLockBtn()
    {
        //如果该角色 需要解锁 并且没有解锁的话 就应该显示解锁按钮 并且隐藏开始按钮
        if (nowRoleData.lockMoney > 0 && !GameDataMgr.Instance.playerData.buyHero.Contains(nowRoleData.id))
        {
            //更新解锁按钮显示 并更新上面的钱
            btnUnLock.gameObject.SetActive(true); // 显示true 隐藏false
            txtUnLock.text = "¥ " + nowRoleData.lockMoney;
            //隐藏开始按钮 因为该角色没有解锁
            btnStart.gameObject.SetActive(false);
        }
        else
        {
            btnUnLock.gameObject.SetActive(false);
            btnStart.gameObject.SetActive(true);
        }
    }

    public override void HideMe(UnityAction callBack)
    {
        base.HideMe(callBack);

        //每次隐藏自己时 要把当前显示的3D模型角色 删除掉
        if (heroObj != null)
        {
            DestroyImmediate(heroObj);  //马上删除,不用等到下一帧
            heroObj = null;
        }
    }

}

其他补充逻辑

提示界面

拼界面

界面逻辑

场景选择界面

拼界面

数据准备

准备场景数据

创建Excel表格

转成 Json

创建图片数据

创建 场景数据类

GameDataMgr 中添加场景数据

界面逻辑

上节课遗留

要把图片资源的纹理类型(Texture Type)改为 精灵图片

ChooseScenePanel 面板逻辑

调用

小总结:所有的面板都是数据的体现。

游戏场景

场景搭建

遗留:ChooseHeroPanel 面板中 显示人物名字

自行添加对应的逻辑

场景搭建

因为丧尸要自动寻路,所以先烘焙地图

1.打开导航

2.烘焙前先设置一下------烘焙静态

不需要设置连接点,因为该地图没有断开的点 (Off Mesh Link Generation)

3.回到导航窗口(Navigation)-->打开 烘焙页签(Bake)-->点击 Back 烘焙

记得打开 辅助功能 -- Gizmos

调整

游戏界面

拼界面

界面逻辑

GamePanel 逻辑

创建组合控件的脚本

摄像机跟随逻辑

创建 CameraMove 脚本(挂载到主摄像机上)

玩家逻辑

玩家的控制其实就是调用动画的播放

分析 玩家的一些属性

给人物添加 角色碰撞器

添加怪物层

为每个武器添加一个开火点

给开火动画添加事件

PlayerObject 逻辑

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerObject : MonoBehaviour
{
    //为获得玩家身上的动画组件
    private Animator animator;

    //1.玩家属性的初始值
    //玩家攻击力
    private int atk;
    //玩家拥有的钱
    public int money;
    //旋转的速度
    private float roundSpeed = 50;

    //持枪对象才有的开火点
    public Transform gunPoint;

    //2.移动变化 动作变化

    //3.攻击动作的不同处理

    //4.金币变化的逻辑

    // Start is called before the first frame update
    void Start()
    {
        //得到自己依附的 Animator组件
        animator = this.GetComponent<Animator>();
    }

    /// <summary>
    /// 初始化玩家基础属性
    /// </summary>
    /// <param name="atk"></param>
    /// <param name="money"></param>
    public void InitPlayerInfo(int atk, int money)
    {
        this.atk = atk;
        this.money = money;

        //更新界面上金币的数量
        UpdateMoney();
    }

    // Update is called once per frame
    void Update()
    {
        //2.移动变化 动作变化
        //移动动作的变换 由于动作有位移 我们也应用了动作的位移 所以只要改变这两个值 就会有动作的变化 和 速度的变化
        animator.SetFloat("VSpeed", Input.GetAxis("Vertical"));
        animator.SetFloat("HSpeed", Input.GetAxis("Horizontal"));
        //旋转
        this.transform.Rotate(Vector3.up, Input.GetAxis("Mouse X") * roundSpeed * Time.deltaTime);

        //下蹲
        if (Input.GetKeyDown(KeyCode.LeftShift))
        {
            //当按下 Shift键时 把编号为1的动画层级权重改为1
            animator.SetLayerWeight(1, 1);
        }
        else if (Input.GetKeyUp(KeyCode.LeftShift))
        {
            //当抬起 Shift键时 把编号为1的动画层级权重改为0
            animator.SetLayerWeight(1, 0);
        }

        //按下R 播放打滚动画
        if (Input.GetKeyDown(KeyCode.R))
            animator.SetTrigger("Roll");

        //鼠标左键 开火
        if (Input.GetMouseButtonDown(0))
            animator.SetTrigger("Fire");
    }

    //3.攻击动作的不同处理
    /// <summary>
    /// 专门用于处理刀武器攻击动作的伤害检测事件
    /// </summary>
    public void KnifeEvent()
    {
        //伤害检测      返回一个碰撞器数组
        Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("Monster"));

        //暂时无法继续写逻辑了 因为 我们没有怪物对应的脚本
        for (int i = 0; i < colliders.Length; i++)
        {
            //得到碰撞到的对象上的怪物脚本 让其受伤

        }
    }

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

        for (int i = 0; i < hits.Length; i++)
        {
            //得到对象上的怪物脚本 让其受伤

        }
    }


    //4.金币变化的逻辑
    public void UpdateMoney()
    {
        //间接的更新界面上 钱的数量
        UIManager.Instance.GetPanel<GamePanel>().UpdateMoney(money);
    }

    /// <summary>
    /// 提供给外部加钱的方法
    /// </summary>
    /// <param name="money"></param>
    public void AddMoney(int money)
    {
        //加金币
        this.money += money;
        UpdateMoney();
    }
}

保护区域逻辑

选择地图上一片区域为保护区

加入一个合适的特效

注意:因为一些草的贴图丢失,导致画面有面片感

处理:点击地图里的 地形------Terrain

点地形组件 Terrain 中页签中的 花的图标

按住 Shift 点击地图上的区域消除即可

给特效区域添加球形碰撞,并勾选触发器

MainTowerObject 逻辑(主保护区域相关逻辑) 挂载到保护区特效上

怪物逻辑

状态机准备

创建动画状态机

创建丧尸模型

数据准备

创建Excel表

再转成 Json 数据

然后申明 对应的数据结构

在 GameDataMgr 中去读取它

逻辑处理

创建 MonsterObject 脚本

cs 复制代码
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;

    //出生过后再移动
    //移动------------寻路组件
    //攻击------------伤害检测
    //受伤
    //死亡
    //初始化

    // Start is called before the first frame update
    void Awake()
    {
        //得到丧尸对象上挂载的动画组件和寻路组件
        animator = this.GetComponent<Animator>();
        agent = this.GetComponent<NavMeshAgent>();
    }

    //初始化
    public void InitInfo(MonsterInfo info)
    {
        monsterInfo = info;
        //状态机加载
        animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(monsterInfo.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;

        //根据速度 来决定动画播放什么
        //agent.velocity 是指对象三个方向的速度
        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()
    {
        //范围检测 进行伤害判断 用圆形范围检测
        //Physics.OverlapSphere 第一个参数是: 位置 ,第二个参数是:半径,第三个参数是:能够攻击到的层级
        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 (colliders[i].gameObject == MainTowerObject.Instance.gameObject)
            {
                //让保护区受到伤害
                MainTowerObject.Instance.Wound(monsterInfo.atk);
            }
        }
    }
}

补充:1.给所有丧尸模型添加 寻路组件------ Nav Mesh Agent

2.添加事件:丧尸死亡后移除模型(自行给每个死亡动画都添加上)

3.添加层级 MainTower 层

4.给每个丧尸模型都调整好预设体(调到适当的大小)和 添加碰撞盒------胶囊碰撞盒(后面玩家要攻击)

出怪点逻辑

选择适合的出怪点特效

创建 MonsterPoint (挂载到出怪特效上)

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MonsterPoint : MonoBehaviour
{
    //怪物有多少波
    public int maxWave;

    //每波怪物有多少只
    public int monsterNumOneWave;
    //用于记录 当前波的怪物还有多少只没有创建
    private int nowNum;

    //怪物ID 允许有多个 这样就可以随机创建不同的怪物 更具多样性
    public List<int> monsterIDs;
    //用于记录 当前波 要创建什么ID的怪物
    private int nowID;

    //单只怪物创建间隔时间
    public float createOffsetTime;

    //波与波之间的间隔时间
    public float delayTime;

    //第一波怪物创建的间隔时间
    public float firstDelayTime;

    // Start is called before the first frame update
    void Start()
    {
        //利用延时函数 Invok
        //第一波 延时
        Invoke("CreateWave", firstDelayTime);
    }

    /// <summary>
    /// 开始创建一波的怪物
    /// </summary>
    private void CreateWave()
    {
        //得到当前波怪物的ID是什么  用到 Unity中的随机数
        nowID = monsterIDs[Random.Range(0, monsterIDs.Count)];
        //当前波怪物有多少只
        nowNum = monsterNumOneWave;

        //创建丧尸
        CreateMonster();

        //减少波数
        --maxWave;
    }

    /// <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);

        //创建完一只怪物后 减去要创建的怪物数量
        --nowNum;
        if (nowNum == 0)
        {
            //继续利用延时函数
            if (maxWave > 0)
                Invoke("CreateWave", delayTime);
        }
        else
        {
            //延时函数 间隔的创建丧尸
            Invoke("CreateMonster", createOffsetTime);
        }
    }

    /// <summary>
    /// 出怪点是否出怪结束
    /// </summary>
    /// <returns></returns>
    public bool CheckOver()
    {
        return nowNum == 0 && maxWave == 0;
    }
}

游戏关卡管理器

创建 GameLevelMgr 管理器

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    public static GameLevelMgr Instance => instance;

    public PlayerObject player;

    //所有的出怪点
    private List<MonsterPoint> points = new List<MonsterPoint>();

    //记录当前 还有多少波怪物
    private int nowWaveNum = 0;
    //记录 一共有多少波怪物
    private int maxWaveNum = 0;

    //记录当前场景上的怪物数量
    private int nowMonsterNum = 0;

    private GameLevelMgr()
    {

    }

    //通过该关卡脚本来连接 开始场景 和 游戏场景

    //1.切换到游戏场景时 我们需要动态的创建玩家
    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);
    }

    //2.我们需要通过游戏管理器 来判断 游戏是否胜利
    //要知道场景中 是否还有怪物没有出 以及 场景中 是否还有 没有死亡的怪物

    //用于记录出怪点的方法
    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()
    {
        for (int i = 0; i < points.Count; i++)
        {
            //只要有一个出怪点 还没有出完怪 那么就证明游戏还没有胜利
            if (!points[i].CheckOver())
                return false;
        }

        //要所有的怪都死亡了
        if (nowMonsterNum > 0)
            return false;

        Debug.Log("游戏胜利");
        return true;
    }

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

补充:1.报错

2.创建一个玩家出生位置

3.给玩家角色都绑定好开火点

4.更改 SceneInfo 数据参数

4.将两个场景添加到 Build Settings 中

5.MonsterPoint 脚本 逻辑添加

  1. MonsterObject 中 逻辑添加

6.PlayerObject 添加逻辑

游戏结束面板

1.拼面板

2. GameOverPanel 逻辑

补充:1.在游戏关卡管理器(GameLevelMgr)中 提供一个清除数据的方法

2.在MonsterObject 中检测是否胜利的逻辑,并给玩家获得金币奖励

3.在进入游戏后 锁定鼠标,显示 结束面板时 解除锁定

音效特效添加

1.在 GameDataMgr 中给外部提供一个播放音效的方法(因为GameDataMgr中数据多,所以在这里写这个方法方便些)

2.在 PlayerObject 中播放音效

3.怪物受伤音效

创建特效相关

1. PlayerObject 中创建打击特效

其他相关特效可以自行添加

防御塔逻辑

数据模型准备

1.创建模型

从资源包里取出炮台模型并重命名

图片资源也得准备(自己截图)

2.配置数据

先创 Excel 表 ------> 转Json 数据 ------> 创建对应结构体数据 ------> GameDataMgr 中读取出来

防御塔逻辑

创建 TowerObject 类

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TowerObject : MonoBehaviour
{
    //炮台头部 用于旋转 指向目标
    public Transform head;
    //开火点 用于释放攻击的位置
    public Transform gunPoint;
    //炮台头部旋转速度 可以写死 也可以配在表中
    private float roundSpeed = 20;

    //炮台关联的数据
    private TowerInfo info;

    //当前要攻击的目标
    private MonsterObject targetObj;
    //当前要攻击的群体目标
    private List<MonsterObject> targetObjs;

    //用于计时的 用于判断攻击间隔时间
    private float nowTime;

    //用于记录怪物位置
    private Vector3 monsterPos;

    //测试代码
    private void Start()
    {
        InitInfo(GameDataMgr.Instance.towerInfoList[10]);
    }

    /// <summary>
    /// 初始化炮台相关数据
    /// </summary>
    /// <param name="info"></param>
    public void InitInfo(TowerInfo info)
    {
        this.info = info;
    }

    // Update is called once per frame
    void Update()
    {
        //单体攻击逻辑
        if (info.atkType == 1)
        {
            //没有目标 或者 目标死亡 或者 目标超出攻击距离 就继续寻找其他目标
            if (targetObj == null || 
                targetObj.isDead || 
                Vector3.Distance(this.transform.position, targetObj.transform.position) > info.atkRange)
            {
                targetObj = GameLevelMgr.Instance.FindMonster(this.transform.position, info.atkRange);
            }

            //如果没有找到任何可以攻击的对象 那么炮台就不应该旋转
            if (targetObj == null)
                return;

            //得到怪物位置,偏移Y的目标位置是希望 炮台头部不要上下倾斜
            monsterPos = targetObj.transform.position;
            monsterPos.y = head.position.y;

            //让炮台头部旋转起来
            head.rotation = Quaternion.Slerp(head.rotation, Quaternion.LookRotation(monsterPos - head.position), roundSpeed * Time.deltaTime);

            //Vector3.Angle()  这个方法可以得到两个向量的夹角
            //判断 两个对象之间的夹角 小于一定范围时 才能让目标受伤 并且攻击间隔条件要满足
            if (Vector3.Angle(head.forward, monsterPos - head.position) < 5 && 
                Time.time - nowTime >= info.offsetTime)
            {
                //让目标受伤
                //提示:这里为什么不用射线检测?
                //因为当这些条件满足时,射线检测也一定是能够打中敌方的,所以就直接让敌方受伤就行
                targetObj.Wound(info.atk);

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

                //创建开火特效
                GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
                //延迟移除特效
                Destroy(effObj, 0.2f);

                //记录开火时间
                nowTime = Time.time;
            }
        }
        //群体攻击逻辑
        else
        {
            targetObjs = GameLevelMgr.Instance.FindMonsters(this.transform.position, info.atkRange);

            if (targetObjs.Count > 0 && 
                Time.time - nowTime >= info.offsetTime)
            {
                //创建开火特效
                GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
                //延迟移除特效
                Destroy(effObj, 0.2f);

                //让目标们受伤
                for (int i = 0; i < targetObjs.Count; i++)
                {
                    targetObjs[i].Wound(info.atk);
                }

                //记录开火时间
                nowTime = Time.time;
            }
        }
    }
}

补充:1.创建炮台预设体和图片资源

2.记录丧尸的数量,用于后面进行攻击

在 关卡管理器(GameLevelMgr)中去申明(要把原申明丧尸数量的数据替换了)

再添加两个方法:添加丧尸数量、减少丧尸数量

其他地方逻辑有错也要进行对应的修改

3.在 关卡管理器(GameLevelMgr)中添加 满足攻击条件的丧尸并传出去

4.创建一些开火、伤害特效

5.给所有炮台拖入 TowerObject 脚本,并添加开火点

最后把测试代码注释了

造塔点逻辑

1.创建 造塔点特效 (在资源包里找到合适的即可)

2.给造塔点 加上碰撞器(要勾选触发器)

3.创建 TowerPoint 类

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TowerPoint : MonoBehaviour
{
    //造塔点关联的 塔对象
    private GameObject towerObj = null;
    //造塔点关联的 塔的数据
    public TowerInfo nowTowerInfo = null;

    //可以建造的三个塔的ID是多少
    public List<int> chooseIDs;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    /// <summary>
    /// 建造一个塔
    /// </summary>
    /// <param name="id"></param>
    public void CreateTower(int id)
    {
        TowerInfo info = GameDataMgr.Instance.towerInfoList[id - 1];
        //如果钱不够 就不用建造
        if (info.money > GameLevelMgr.Instance.player.money)
            return;

        //扣钱
        GameLevelMgr.Instance.player.AddMoney(-info.money);

        //创建塔
        //先判断之前是否有塔 如果有 就删除
        if(towerObj != null)
        {
            Destroy(towerObj);
            towerObj = null;
        }
        //实例化塔对象
        towerObj = Instantiate(Resources.Load<GameObject>(info.res),this.transform.position, Quaternion.identity);
        towerObj.GetComponent<TowerObject>().InitInfo(info);

        //记录当前塔的数据
        nowTowerInfo = info;

        //塔建造完毕 更新游戏界面上的内容
        if (nowTowerInfo.nextLev != 0)
        {
            //如果 不等于0 代表还能升级,界面就更新成要升级的炮台图标
            UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
        }
        else
        {
            UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
        }

    }

    //一定会用到触发器进入检测函数
    private void OnTriggerEnter(Collider other)
    {
        //如果现在已经有塔了 就没有必要再显示升级界面 或者造塔界面了
        if (nowTowerInfo != null && nowTowerInfo.nextLev == 0)
            return;

        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
    }

    //触发器离开检测函数
    private void OnTriggerExit(Collider other)
    {
        //如果不希望游戏界面下方的造塔界面显示 直接传空
        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
    }

}

4.在 GamePanel 中添加更新炮塔图标类的界面方法

5.在 TowerBtn 中添加 初始化炮台的方法

6.记得把炮台的图片资源调为 Sprite图片

7.报错的一个要点:运行时为什么会不显示游戏界面

因为在 GamePanel 中我们重写了 Update() , 将 BasePanel 里的 Update()覆盖了,而BasePanel里的 Updata() 有界面的淡入淡出,覆盖后就没有了。

解决办法,将 BasePanel 里的 Update 写成虚函数,让GamePanel 去重写。

8.给每个人物都加上 角色碰撞器

9.在 GamePanel 中添加 检测输入造塔的逻辑

补充:这里要在 GamePanel 中添加一个 是否检测输入的标识

10.在 MonsterObject 中添加怪物死亡后加金币的逻辑

这里发现 丧尸会多次死亡,导致我们会多加好多钱,添加一些条件判断死亡加金币

PlayerObject 中的条件也得改一下

细节完善

1.丧尸死亡后还在向前移动

关闭寻路即可(这里是将寻路组件失活)

2.射线检测的改动

玩家、丧尸、炮台、保护区的参数数组都自己合理更改。

到这里,游戏的基本逻辑都已实现,接下来要自己把人物开枪、打击特效添加好,将三副地图设计好,还有各个数值设置合理。

完成展示

Unity核心实践项目

总结

相关推荐
咩咩觉主4 小时前
Unity实战案例全解析 :PVZ 植物脚本分析
unity·游戏引擎
拾忆丶夜4 小时前
Unity3d 以鼠标位置点为中心缩放视角(正交模式下)
unity
_oP_i11 小时前
Unity 编辑器设置中文
unity
zaizai100720 小时前
编辑器拓展(入门与实践)
unity
躺下睡觉~1 天前
Unity-Transform-坐标转换
linux·unity·游戏引擎
吾名招财2 天前
unity3d入门教程五
游戏引擎·unity3d
598866753@qq.com2 天前
URP 线性空间 ui资源制作规范
ui·unity
王维志2 天前
Unity实现自己的协程系统
unity·c#·游戏引擎
Yasin Chen2 天前
Unity 粒子系统参数说明
unity·游戏引擎
冰凌糕2 天前
Unity3D 小案例 像素贪吃蛇 02 蛇的觅食
unity