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

零、效果展示

丧尸围城演示视频

一、需求分析

二、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);
        }
    }
}
相关推荐
NRatel3 小时前
Unity项目基本风格/规范
unity·c#·游戏引擎·代码规范·规范
JIQIU.YANG6 小时前
Unity切换平台资源重新编译缓慢
unity·游戏引擎
淡海水6 小时前
【光照】Unity中的[光照模型]概念辨析
unity·pbr·光照模型·phong·brdf
淡海水6 小时前
【光照】[自发光Emission]以UnityURP为例
unity·urp·光照模型·经验模型·自发光
寻水的鱼、、7 小时前
【Unity Shader学习笔记】(二)图形显示系统
unity·shader
NRatel13 小时前
Unity资源管理——操作一览(编辑器下 &运行时)
unity·资源管理·资源·打包ab·unity热更
一线灵14 小时前
跨平台游戏引擎 Axmol-2.8.0 发布
游戏引擎
1uther16 小时前
Unity核心概率④:MonoBehavior
开发语言·游戏·unity·c#·游戏引擎
猫猫的小茶馆1 天前
【STM32】贪吃蛇 [阶段 8] 嵌入式游戏引擎通用框架设计
stm32·单片机·嵌入式硬件·mcu·物联网·游戏引擎·智能硬件