零、效果展示
丧尸围城演示视频
一、需求分析

二、UI相关
1、UI面板淡入淡出效果实现
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Panel : MonoBehaviour
{
// Start is called before the first frame update
public 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 frame
void 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管理器
csharp
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、单例模式制作数据管理器
csharp
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() 中更新了位置,摄像机可能会比角色"慢一拍",出现轻微抖动。
csharp
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 frame
void 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)使用范围检测进行伤害判断
csharp
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)使用射线检测进行伤害判断
csharp
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)举例实现
子弹类:
csharp
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);
}
}
}
子弹对象池:
csharp
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);
}
}
子弹工厂:
csharp
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;
}
}
武器类:
csharp
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);
}
}
}