当前位置: 首页 > news >正文

【Unity开发】丧尸围城项目实现总结

零、效果展示

丧尸围城演示视频

一、需求分析

在这里插入图片描述

二、UI相关

1、UI面板淡入淡出效果实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Panel : MonoBehaviour
{// Start is called before the first frame updatepublic float speed = 10f;private CanvasGroup cg;public bool isShow = false;void Start(){cg = this.GetComponent<CanvasGroup>();if (cg==null){cg = this.gameObject.AddComponent<CanvasGroup>();}}// Update is called once per framevoid Update(){if (isShow && cg.alpha!=1){cg.alpha += speed * Time.deltaTime;if (cg.alpha>=1){cg.alpha = 1;}}else if (!isShow && cg.alpha != 0){cg.alpha -= speed * Time.deltaTime;if (cg.alpha <= 0){cg.alpha = 0;}}}
}

2、使用单例模式制作UI管理器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class UIManager
{private static UIManager instance = new UIManager();public static UIManager Instance => instance;//用于存储显示着的面板的 每显示一个面板 就会存入这个字典//隐藏面板时 直接获取字典中的对应面板 进行隐藏private Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();//场景中的 Canvas对象 用于设置为面板的父对象private Transform canvasTrans;private UIManager(){//得到场景中的Canvas对象GameObject canvas = GameObject.Instantiate(Resources.Load<GameObject>("UI/Canvas"));canvasTrans = canvas.transform;//通过过场景不移除该对象 保证这个游戏过程中 只有一个 canvas对象GameObject.DontDestroyOnLoad(canvas);}//显示面板public T ShowPanel<T>() where T:BasePanel{//我们只需要保证 泛型T的类型 和面板预设体名字 一样 定一个这样的规则 就可以非常方便的让我们使用了string panelName = typeof(T).Name;//判断 字典中 是否已经显示了这个面板if (panelDic.ContainsKey(panelName))return panelDic[panelName] as T;//显示面板 根据面板名字 动态的创建预设体 设置父对象GameObject panelObj = GameObject.Instantiate(Resources.Load<GameObject>("UI/" + panelName));//把这个对象 放到场景中的 Canvas下面panelObj.transform.SetParent(canvasTrans, false);//指向面板上 显示逻辑 并且应该把它保存起来T panel = panelObj.GetComponent<T>();//把这个面板脚本 存储到字典中 方便之后的 获取 和 隐藏panelDic.Add(panelName, panel);//调用自己的显示逻辑panel.ShowMe();return panel;}/// <summary>/// 隐藏面板/// </summary>/// <typeparam name="T">面板类名</typeparam>/// <param name="isFade">是否淡出完毕过后才删除面板 默认是ture</param>public void HidePanel<T>(bool isFade = true) where T:BasePanel{//根据泛型得名字string panelName = typeof(T).Name;//判断当前显示的面板 有没有你想要隐藏的if( panelDic.ContainsKey(panelName) ){if( isFade ){//就是让面板 淡出完毕过后 再删除它 panelDic[panelName].HideMe(() =>{//删除对象GameObject.Destroy(panelDic[panelName].gameObject);//删除字典里面存储的面板脚本panelDic.Remove(panelName);});}else{//删除对象GameObject.Destroy(panelDic[panelName].gameObject);//删除字典里面存储的面板脚本panelDic.Remove(panelName);}}}//得到面板public T GetPanel<T>() where T:BasePanel{string panelName = typeof(T).Name;if (panelDic.ContainsKey(panelName))return panelDic[panelName] as T;//如果没有对应面板显示 就返回空return null;}}

3、UI拼接

参考学习链接:https://blog.csdn.net/weixin_45972052/category_12863511.html

三、数据管理

1、单例模式制作数据管理器

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;//所有塔的数据public List<TowerInfo> towerInfoList;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");//读取塔的数据towerInfoList = JsonMgr.Instance.LoadData<List<TowerInfo>>("TowerInfo");}/// <summary>/// 存储音效数据/// </summary>public void SaveMusicData(){JsonMgr.Instance.SaveData(musicData, "MusicData");}/// <summary>/// 存储玩家数据/// </summary>public void SavePlayerData(){JsonMgr.Instance.SaveData(playerData, "PlayerData");}/// <summary>/// 播放音效方法/// </summary>/// <param name="resName"></param>public void PlaySound(string resName){GameObject musicObj = new GameObject();AudioSource a = musicObj.AddComponent<AudioSource>();a.clip = Resources.Load<AudioClip>(resName);a.volume = musicData.soundValue;a.mute = !musicData.soundOpen;a.Play();GameObject.Destroy(musicObj, 1);}
}

2、数据存储——Json

通过配置表对游戏数据进行设置修改
参考学习链接:https://blog.csdn.net/weixin_45972052/article/details/151049312

(1)不同存储方法的区别

(i)PlayerPrefs

存储位置:Windows: 注册表 Android/iOS: 沙盒目录下的本地文件
支持数据类型:int、float、string
适用场景:适合存 轻量级数据(分数、设置、关卡进度)
缺点:数据量小(通常几 KB),不能存复杂结构,安全性差(容易被修改)

(ii)Resources.Load

存储位置:Resources 文件夹
支持数据类型:几乎所有类型
适用场景:存放游戏内用到的 只读资源,比如预制体、贴图、材质
缺点:所有资源打包到一个大文件,体积大,内存占用高,无法动态更新(必须重新打包)

(iii)Application.streamingAssetsPath

存储位置:StreamingAssets文件夹
支持数据类型:JSON、视频、音频、模型
适用场景:配置文件、大文件、多媒体资源
缺点:只读(不能在运行时修改/写入),Android/iOS 下读取复杂

(iv)Application.persistentDataPath

存储位置:不同平台不一样
支持数据类型:存档、用户配置、缓存
适用场景:存放运行时生成的数据,可读可写,支持复杂结构
缺点:不能预置(需要运行时生成或从 StreamingAssets 拷贝过来)

(v)Json / XML / 二进制序列化

存储位置:存放在persistentDataPath路径中,不同平台不一样
支持数据类型:int、float、string等结构化数据
适用场景:复杂存档、配置文件
缺点:JSON:体积稍大。XML:解析慢。二进制:不直观,调试不方便。

(vi)总结

在这里插入图片描述

四、游戏逻辑

1、Animatior中bool和Trigger这两个参数的不同

在这里插入图片描述

  • Bool参数是持久性的,可以保持在true或false状态,而Trigger参数是一次性的,在被设置为true后会自动返回false。
  • Bool参数通常用于表示持久性状态,Trigger参数通常用于触发一次性事件或状态转换。
  • Bool参数适合用于表示角色的当前状态(如站立、行走),而Trigger参数适合用于表示瞬时事件(如攻击、跳跃)。

2、动画遮罩

通过使用动画分层和遮罩提升动画多样性,节约资源
在这里插入图片描述
红色部分为模型不受动画影响的部分
参考学习链接(第五点的第2小点):https://blog.csdn.net/weixin_45972052/article/details/150710805?spm=1001.2014.3001.5502

3、自动寻路

使用Unity中的导航寻路系统,实现简单的自动寻路功能
参考学习链接(第六点):https://blog.csdn.net/weixin_45972052/article/details/150710805?spm=1001.2014.3001.5502

4、摄像机跟随玩家

摄像机位置更新建议在**LateUpdate()中进行更新,可以在角色位置完全更新后再调整位置,避免抖动和延迟。如果在Update()**中进行更新,如果被跟随的对象在 Update() 或 FixedUpdate() 中更新了位置,摄像机可能会比角色“慢一拍”,出现轻微抖动。

using System.Collections;azai
using System.Collections.Generic;
using UnityEngine;public class CameraMove : MonoBehaviour
{//摄像机要看向的目标对象public Transform target;//摄像机相对目标对象 在xyz上的偏移位置public Vector3 offsetPos;//看向位置的y偏移值public float bodyHeight;//移动和旋转速度public float moveSpeed;public float rotationSpeed;private Vector3 targetPos;private Quaternion targetRotation;// Update is called once per framevoid FixedUpdate(){if (target == null)return;//根据目标对象 来计算 摄像机当前的位置和角度//位置的计算//向后偏移Z坐标targetPos = target.position + target.forward * offsetPos.z;//向上偏移Y坐标targetPos += Vector3.up * offsetPos.y;//左右偏移X坐标targetPos += target.right * offsetPos.x;//插值运算 让摄像机 不停向目标点靠拢this.transform.position = Vector3.Lerp(this.transform.position, targetPos, moveSpeed * Time.deltaTime);//旋转的计算//得到最终要看向某个点时的四元数targetRotation = Quaternion.LookRotation(target.position + Vector3.up * bodyHeight - this.transform.position);//让摄像机不停的向目标角度靠拢this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);}/// <summary>/// 设置摄像机看向的目标对象/// </summary>/// <param name="player"></param>public void SetTarget(Transform player){target = player;}
}

5、玩家使用不同武器的伤害检测

(1)刀

结合动画帧事件+范围检测进行伤害检测

(i)在动画指定帧位置添加伤害检测事件

在这里插入图片描述

(ii)使用范围检测进行伤害判断
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;}}}

(2)枪、子弹

结合动画帧事件+**射线检测 **进行伤害检测

(i)在动画指定帧位置添加伤害检测事件
(ii)使用射线检测进行伤害判断
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;}}}

如果想要更加精确的射线检测,可以选择SphereCast(球形射线)或CapsuleCast(胶囊射线)进行检测

五、优化

1、结合对象池和工厂模型进行优化

(1)介绍

存在问题:子弹的频繁生成与销毁容易造成性能过大开销
解决方法:使用对象池和工厂模型
工厂模式(Factory): 负责“创建”子弹对象,屏蔽具体的生成逻辑(Prefab 加载、初始化参数)
对象池: 负责“复用”子弹对象,避免频繁 Instantiate / Destroy 导致 GC 和性能开销。

(2)举例实现

子弹类:

using UnityEngine;public class Bullet : MonoBehaviour
{public float speed = 20f;public float lifeTime = 2f;private float timer;void OnEnable(){timer = 0f;}void Update(){transform.Translate(Vector3.forward * speed * Time.deltaTime);timer += Time.deltaTime;if (timer >= lifeTime){// 回收子弹BulletPool.Instance.RecycleBullet(this);}}
}

子弹对象池:

using System.Collections.Generic;
using UnityEngine;public class BulletPool : MonoBehaviour
{public static BulletPool Instance;public GameObject bulletPrefab;public int initialSize = 10;private Queue<Bullet> pool = new Queue<Bullet>();void Awake(){Instance = this;// 初始化对象池for (int i = 0; i < initialSize; i++){Bullet bullet = CreateNewBullet();pool.Enqueue(bullet);bullet.gameObject.SetActive(false);}}private Bullet CreateNewBullet(){GameObject obj = Instantiate(bulletPrefab);Bullet bullet = obj.GetComponent<Bullet>();obj.transform.SetParent(transform);return bullet;}public Bullet GetBullet(){if (pool.Count > 0){Bullet bullet = pool.Dequeue();bullet.gameObject.SetActive(true);return bullet;}else{return CreateNewBullet();}}public void RecycleBullet(Bullet bullet){bullet.gameObject.SetActive(false);pool.Enqueue(bullet);}
}

子弹工厂:

using UnityEngine;public class BulletFactory
{public Bullet CreateBullet(Vector3 position, Quaternion rotation){Bullet bullet = BulletPool.Instance.GetBullet();bullet.transform.position = position;bullet.transform.rotation = rotation;return bullet;}
}

武器类:

using UnityEngine;public class Gun : MonoBehaviour
{public Transform firePoint;private BulletFactory factory = new BulletFactory();void Update(){if (Input.GetButtonDown("Fire1")){factory.CreateBullet(firePoint.position, firePoint.rotation);}}
}
http://www.xdnf.cn/news/1433863.html

相关文章:

  • 双八无碳小车cad+三维图+仿真+设计说明书
  • 快速入门Vue3——基础语法
  • SpringBoot RestTemplate 设置http请求连接池
  • 一个真正跨平台可用的免费PDF解决方案
  • 同步整流芯片为何容易受损?如何应对呢?
  • 第十七讲:编译链接与函数栈帧
  • 电机控制(二)-控制理论基础
  • 互联网向无线通信发展的关键历史时期
  • 睿思芯科正式加入龙蜥社区,携手共建 RISC-V 服务器生态新标杆
  • thinkphp6通过workerman使用websocket
  • ArkUI核心功能组件使用(一)
  • 强化学习PPO/DDPG算法学习记录
  • 01 - 网页和web标准
  • Spring Boot数据脱敏方案
  • java-设计模式-5-创建型模式-建造
  • quant, 量化交易,合约,期货心得,短线交易心得
  • Vue3 + GSAP 动画库完全指南:从入门到精通,打造专业级网页动画
  • 人工智能与强化学习:使用OpenAI Gym进行项目开发
  • 【小白笔记】使用 robocopy 解决大文件复制难题:从踩坑到精通
  • 第四届可再生能源与电气科技国际学术会议(ICREET 2025)
  • 如何修改 Docker 默认网段(网络地址池)配置:以使用 10.x.x.x 网段为例
  • CH01-1.1 Exercise-Ordinary Differential Equation-by LiuChao
  • 【代码随想录day 22】 力扣 131.分割回文串
  • DevOps部署与监控
  • MATLAB矩阵及其运算(四)矩阵的运算及操作
  • 集群无法启动CRS-4124: Oracle High Availability Services startup failed
  • 数据库入门实战版
  • 基于YOLOv4的无人机视觉手势识别系统:从原理到实践
  • 货运系统源码 货运物流小程序 货运平台搭建 货运软件开发
  • C19T1