Unity核心实践小项目

news2024/9/24 1:26:03

要源码包的私信我。

简介

衔接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  逻辑

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 逻辑

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 脚本

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 (挂载到出怪特效上)

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 管理器

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 脚本 逻辑添加

5. 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 类

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 类

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核心实践项目

总结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2126696.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VD1013 DFN小封装芯片 适用于小电流的输出的电池保护芯片

VD1013内置高精度电压检测电路和延迟电路以及内置MOSFET&#xff0c;是用于单节锂离子/锂聚合物可再充电电池的保护IC。 本IC适合于对1节锂离子/锂聚合物可再充电电池的过充电、过放电和过电流进行保护 。 VD1013具备如下特点: 高精度电压检测电路 过充电检测电压…

chfsgui局域网共享局域网http服务 Cute HTTp File Server软件

Cute HTTp File Server https://wwaz.lanzouv.com/iGHIj29srj0b 密码:eaq3

OpenHarmony鸿蒙( Beta5.0)智能窗户通风设备开发详解

鸿蒙开发往期必看&#xff1a; 一分钟了解”纯血版&#xff01;鸿蒙HarmonyOS Next应用开发&#xff01; “非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线&#xff01;&#xff08;从零基础入门到精通&#xff09; “一杯冰美式的时间” 了解鸿蒙HarmonyOS Next应用开发路…

区块链积分系统:革新支付安全与用户体验的未来

在数字时代&#xff0c;确保交易过程中的安全性和风险控制变得至关重要。随着传统支付系统在处理大规模交易时面临的挑战&#xff0c;如繁琐的审核、严格的监管以及欺诈风险&#xff0c;这些问题不仅影响了交易效率&#xff0c;还可能给用户和企业带来经济损失。因此&#xff0…

【CanMV K230 AI视觉】 跌倒检测

【CanMV K230 AI视觉】 跌倒检测 跌倒检测 动态测试效果可以去下面网站自己看。 B站视频链接&#xff1a;已做成合集 抖音链接&#xff1a;已做成合集 跌倒检测 跌倒检测主要根据人体姿态来判断&#xff0c;可以用于老人、小孩跌倒监护。 实验名称&#xff1a;跌倒检测 实验…

基于单片机的电子药箱控制系统设计

本设计采用STM32F103C8T6单片机作为电子药箱的主控单元&#xff0c;组成模块包括时钟芯片DS1302、语音提醒模块WT588D、液晶显示模块LCD1602、红外避障传感器FC-51、电磁锁驱动电路和通信模块SIM800C。系统初始化结束&#xff0c;红外传感器检测药物的剩余情况并将信息上传到主…

虚拟机器配置固定IP地址

新安装的虚拟机&#xff0c;如何配置固定的ip地址&#xff0c;废话少说直接上干货 第一步&#xff1a;在VMarea中 选中你要固定IP的虚拟机器&#xff0c;点击上面的“编辑”按钮&#xff0c;然后找到“虚拟网络编辑器”&#xff0c;选中你要修改的ip VMnet8&#xff0c;然后是…

力扣题解2555

大家好&#xff0c;欢迎来到无限大的频道。 今日继续给大家带来力扣题解。 题目描述&#xff1a; 两个线段获得的最多奖品 在 X轴 上有一些奖品。给你一个整数数组 prizePositions &#xff0c;它按照 非递减 顺序排列&#xff0c;其中 prizePositions[i] 是第 i 件奖品的位…

01 Docker概念和部署

目录 1.1 Docker 概述 1.1.1 Docker 的优势 1.1.2 镜像 1.1.3 容器 1.1.4 仓库 1.2 安装 Docker 1.2.1 配置和安装依赖环境 1.3镜像操作 1.3.1 搜索镜像 1.3.2 获取镜像 1.3.3 查看镜像 1.3.4 给镜像重命名 1.3.5 存储&#xff0c;载入镜像和删除镜像 1.4 Doecker…

逆向基础一阶段检测-1

前言&#xff1a; 记录一下&#xff0c;最开始学习逆向时&#xff0c;rkvir所出的题目~&#xff08;当时不会&#xff09; 查壳。 32ida打开。 进入main函数&#xff1a; 对于这种情况&#xff0c;我们可以考虑直接在if cmp判断时&#xff0c;下断点去查看值&#xff0c;因为…

Unet改进31:添加Star_Block(2024最新改进方法)|紧凑的网络结构和高效的运算

本文内容:在不同位置添加Star_Block 目录 论文简介 1.步骤一 2.步骤二 3.步骤三 4.步骤四 论文简介 最近的研究引起了人们对网络设计中尚未开发的“星型操作”(元素智能乘法)潜力的关注。虽然有很多直观的解释,但其应用背后的基本原理在很大程度上仍未被探索。我们的研…

论文学习笔记 VMamba: Visual State Space Model

概览 这篇论文的动机源于在计算机视觉领域设计计算高效的网络架构的持续需求。当前的视觉模型如卷积神经网络&#xff08;CNNs&#xff09;和视觉Transformer&#xff08;ViTs&#xff09;在处理大规模视觉任务时展现出良好的表现&#xff0c;但都存在各自的局限性。特别是&am…

ROS入门教程(八)—— 路径规划导航实战

通过Gazebo仿真和RViz仿真的学习后,本文将通过Gazebo与RViz联合仿真实现机器人在Gazebo仿真环境中运动,通过远距搜索与近距搜索实现机器人路径规划导航。 目录 前言 实现思路 仿真模型 仿真源码 前言 前面的ROS入门教程提供ROS仿真的基础步骤,本文提供了实现思路,仿真…

Java面试篇基础部分-Java中常用的I/O模型

阻塞I/O模型 阻塞式的I/O模型是一种非常常见的I/O模型机制,在进行数据读写操作的时候,客户端会发生阻塞等待。 工作流程如图所示,该用户线程一直阻塞,等待内存中的数据就绪;内存中的数据就绪之后,内核态的数据将拷贝到用户线程中,并且返回I/O的执行结果到用户线程。这个…

视觉检测中的深度学习应用

引言 视觉检测是计算机视觉的一个重要领域&#xff0c;涉及到对图像或视频流进行分析和理解。随着深度学习技术的迅猛发展&#xff0c;视觉检测领域发生了革命性的变化。深度学习通过使用复杂的神经网络模型&#xff0c;尤其是卷积神经网络&#xff08;CNNs&#xff09;&#…

一些硬件知识(二十一)

高侧开关应该选用P-MOS还是N-MOS呢&#xff1f; 高侧开关就是负载是接地的&#xff0c;开关相对于负载处于高电位&#xff0c;如下图所示。如果将开关和负载的位置互换&#xff0c;就是低侧开关。 P-MOS作为高侧开关的示意图如下图所示。要想P-MOS管导通&#xff0c;则VgsVg-V…

express框架(二)

中间件 什么是中间件 中间件&#xff08;Middleware&#xff09;本质是一个回调函数&#xff0c;中间件函数可以像路由回调一样访问请求对象&#xff08;request&#xff09;&#xff0c;响应对象&#xff08;response&#xff09;。 中间件的作用 使用函数封装公共操作&am…

kitti数据深度图转点云坐标计算方法与教程(代码实现)

文章目录 前言一、kitti深度图官网介绍1、官网深度图介绍2、深度图读取官网代码(python)3、深度图解读1、数据格式内容2、深度图加工3、深度图转相机坐标深度二、kitti数据内参P矩阵解读1、P2矩阵举例2、内参矩阵 (3x3)3、特殊平移向量(第4列)4、kitti的bx与by解释三、kitti深…

【getshell】phpmyadmin后台getshell(4.8.5)

&#x1f3d8;️个人主页&#xff1a; 点燃银河尽头的篝火(●’◡’●) 如果文章有帮到你的话记得点赞&#x1f44d;收藏&#x1f497;支持一下哦 【getshell】phpmyadmin后台getshell&#xff08;4.8.5&#xff09; 一、进入sql命令输入界面二、上传代码三、getshell 一、进入…

LINQ 和 LINQ扩展方法 (1)

LINQ函数概念&#xff1a; LINQ&#xff08;Language Integrated Query&#xff09;是一种C#语言中的查询技术&#xff0c;它允许我们在代码中使用类似SQL的查询语句来操作各种数据源。这些数据源可以是集合、数组、数据库、XML文档等等。LINQ提供了一种统一的编程模型&#x…