从0开始,用Unity写一个“健壮”的贪吃蛇游戏

2,850 阅读12分钟

1.前言

本人自己刚转入C#编程学Unity不久,工作过程中也是在别人已经搭好的框架下进行软件功能的开发,时间稍久便会发现自己进步太慢,于是在网上找了一个Unity实战项目。发现贪吃蛇这个比较简单,也有较多前辈大佬分享了项目源码以及Unity资源,于是萌生了自己从头写一个贪吃蛇的想法,一方面巩固自己C#编程基础功底,一方面巩固Unity的相关知识。所以这个贪吃蛇项目就这么诞生了,其中的很多UI资源都是借鉴之前很多前辈大佬的资源,在此表达感谢!

当然相比于之前的贪吃蛇,加上自己的想法,进行了很多优化以及功能设置的添加,比如加了一些常见的设置功能、日志记录功能、游戏数据的保存功能等等。除此之外之前较多Unity实战项目大佬们力求代码精简及效率,很多代码都是挂载在UI资源上,对于初学者理解及学习比较吃力,自己也是生啃,花了较多时间去构建这个新的贪吃蛇项目,留一个程序入口脚本,其余通过代码驱动资源。当然本人也是菜鸡一枚,项目中错误难免,分享的本意是帮助每一个像我这样刚入行的朋友,较快的学习理解Unity及C#编程,欢迎大家批评指正,共同进步!

2.界面

2.1游戏主界面

游戏主界面提供三个按钮,一个开始游戏,点击后进入游戏界面,游戏设置按钮,点击后进入游戏设置页面,进行一些常规的设置,还可以查看游戏记录。 image.png

2.2游戏设置界面

常规设置界面,常规设置中提供中英文切换,主题(深色、浅色)切换,食物皮肤(植物、冰淇淋)切换,小蛇皮肤(蓝色、黄色)切换,游戏模式(经典模式、自由模式)切换。 image.png

游戏记录页面,游戏记录界面提供玩家所有游戏记录,包括编号、模式、游戏时间、分数、长度及单局时长。 image.png

2.3游戏说明界面

游戏说明界面,主要介绍游戏玩法及规则。 image.png

2.4游戏界面

游戏界面左侧提供当局的实时数据,包括模式、历史最高分、得分、速度、长度、得分加成、当局时长记录。另外提供三个按钮,暂停游戏、继续游戏、返回主界面。 image.png 哈哈哈哈是不是觉得界面Low爆了,没办法,自己美术功底太差...

2.5Unity界面

Unity界面,有项目工程资源目录、各层级节点,可以调整布局,启动代码后初始化界面。 image.png

3.主要功能

项目工程概览,主要有三个解决方啊,第一个解决方案Assembly-CSharp,主要是本工程的脚本,另外两个解决方案主要是MessagePack的解决方案,导入MessagePack的Unity包后会自动生成。 image.png

3.1程序入口

程序入口脚本AppStart.cs挂载在UICamera下,将需要初始化的类放在Start()函数中,需要每帧更新的类放在Update()方法中, 函数OnTriggerEnter2D(UnityEngine.Collider2D collision)用来检测碰撞事件的发生,程序结束时调用OnDisable()方法。

using Assets.Libs.Config;
using Assets.Scripts.Language;
using Assets.Scripts.LogManager;
using Assets.Scripts.Module.ScreenManager;
using Assets.Scripts.Module.Snake;
using Assets.Scripts.SpriteManager;
using Assets.Scripts.ThemeModel;
using Assets.Scripts.View.GameCaption;
using Assets.Scripts.View.GameRuningView;
using Assets.Scripts.View.GameSetting;
using Assets.Scripts.View.HomeView;

using UnityEngine;

/// <summary>
/// 程序入口,挂载在UICamera下
/// </summary>
public class APPStart : MonoBehaviour
{
    /// <summary>
    /// Unity��Ϣ
    /// </summary>
    private void Awake()
    {
        QualitySettings.vSyncCount = 0;

        //帧率定为60帧
        Application.targetFrameRate = 60;
    }
    
    /// <summary>
    /// Start is called before the first frame update
    /// </summary>
    void Start()
    {
        var uiRoot = GameObject.Find("UIRoot").transform;
        ScreenManager.instance.Init();
        LogManager.instance.Init();
        LanguageModel.instance.Init();
        ThemeModel.instance.Init();
        SpriteManager.instance.Init();
        FontManager.instance.Init();
        GlobalSetting.instance.Init();
        HomeView.instance.Init(uiRoot.Find("HomeView"));
        GameRunningView.instance.Init(uiRoot.Find("GameRunningView"));
        ConfigManager.instance.LoadLastConfig();
        GameSettingView.instance.Init(uiRoot.Find("GameSettingView"));
        GameCaptionView.instance.Init(uiRoot.Find("GameCaptionView"));
    }

    /// <summary>
    /// Update is called once per frame
    /// </summary>
    void Update()
    {
        GameRunningView.instance.Update();
        ScreenManager.instance.Update();
    }

    /// <summary>
    /// The on trigger enter 2 d.
    /// </summary>
    /// <param name="collision">
    /// The collision.
    /// </param>
    void OnTriggerEnter2D(UnityEngine.Collider2D collision)
    {
        Snake.instance.OnTriggerEnter2D(collision);
    }

    /// <summary>
    /// The on disable. 
    /// </summary`>`
    void OnDisable()
    {
        ConfigManager.instance.SaveLastConfig();
        LogManager.instance.Quit();
    }
}

3.2事件管理

事件管理器统一调度事件的注册及删除,通过EventConfig类中的key值来注册事件及删除事件,可以不带参数及带参数,可以根据需求添加相应的方法。

using System;
using System.Collections.Generic;

using UnityEngine;

namespace Assets.Scripts.EventManager
{
    /// <summary>
    /// 事件控制
    /// </summary>
    public class EventController
    {
        /// <summary>
        /// 永久性的消息  
        /// </summary>
        private readonly List<string> m_PermanentEvents = new List<string>();

        /// <summary>
        /// 事件路由    
        /// </summary>
        private readonly Dictionary<string, Delegate> m_Router = new Dictionary<string, Delegate>();

        /// <summary>
        /// 公开字段获取事件路由
        /// </summary>
        /// <value>The router.</value>
        public Dictionary<string, Delegate> theRouter => this.m_Router;

        /// <summary>
        /// 添加事件(不带参数)
        /// </summary>
        /// <param name="eventType">
        /// 事件类型
        /// </param>
        /// <param name="handle">
        /// 事件方法对象
        /// </param>
        public void AddEventListener(string eventType, Action handle)
        {
            this.OnListenerAdding(eventType, handle);
            this.m_Router[eventType] = (Action)Delegate.Combine((Action)this.m_Router[eventType], handle);
        }

        /// <summary>
        /// 添加事件(带一个参数)
        /// </summary>
        /// <typeparam name="T">类参数</typeparam>
        /// <param name="eventType">Type of the event.</param>
        /// <param name="handler">The handler.</param>
        public void AddEventListener<T>(string eventType, Action<T> handler)
        {
            this.OnListenerAdding(eventType, handler);
            this.m_Router[eventType] = (Action<T>)Delegate.Combine((Action<T>)this.m_Router[eventType], handler);
        }

        /// <summary>
        /// 删除事件(不带参数)
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        /// <param name="handle">
        /// The handle.
        /// </param>
        public void RemoveEventListener(string eventType, Action handle)
        {
            if (this.OnListenerRemoving(eventType, handle))
            {
                this.m_Router[eventType] = (Action)Delegate.Remove((Action)this.m_Router[eventType], handle);
            }

            this.OnListenerRemoved(eventType);
        }

        /// <summary>
        /// 删除事件(带一个参数)
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        /// <param name="handle">
        /// The handle.
        /// </param>
        /// <typeparam name="T">
        /// 类参数
        /// </typeparam>
        public void RemoveEventListener<T>(string eventType, Action<T> handle)
        {
            if (this.OnListenerRemoving(eventType, handle))
            {
                this.m_Router[eventType] = (Action)Delegate.Remove((Action)this.m_Router[eventType], handle);
            }
        
            this.OnListenerRemoved(eventType);
        }

        /// <summary>
        /// 触发事件(不带参数)
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        public void TriggerEvent(string eventType)
        {
            if (this.m_Router.TryGetValue(eventType, out var delegateHandle))
            {
                var invocationList = delegateHandle.GetInvocationList();
                for (int invocationIndex = 0; invocationIndex < invocationList.Length; invocationIndex++)
                {
                    var action = invocationList[invocationIndex] as Action;
                    if (action == null)
                    {
                        throw new EventException($"TriggerEvent {eventType} error: types of parameters are not match.");
                    }

                    try
                    {
                        action();
                    }
                    catch (Exception exception)
                    {   
                        Debug.LogError($"msg:{exception.Message} \nstacktrace:{exception.StackTrace}");
                    }
                }
            }
        }

        /// <summary>
        /// 触发事件(带一个参数)
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        /// <param name="args1">
        /// The args 1.
        /// </param>
        /// <typeparam name="T">
        ///
        /// </typeparam>
        public void TriggerEvent<T>(string eventType, T args1)
        {
            if (this.m_Router.TryGetValue(eventType, out var delegateHandle))
            {
                var invocationList = delegateHandle.GetInvocationList();
                for (int invocationIndex = 0; invocationIndex < invocationList.Length; invocationIndex++)
                {
                    var action = invocationList[invocationIndex] as Action<T>;
                    if (action == null)
                    {
                        throw new EventException($"TriggerEvent {eventType} error: types of parameters are not match.");
                    }

                    try
                    {
                        action(args1);
                    }
                    catch (Exception exception) 
                    {
                        Debug.LogError($"msg:{exception.Message} \nstacktrace:{exception.StackTrace}");
                    }
                }
            }
        }

        /// <summary>
        /// 清理事件
        /// </summary>
        public void CleanUp()
        {
            List<string> list = new List<string>();
            foreach (var pair in this.m_Router)
            {
                var flag = false;
                foreach (var str in this.m_PermanentEvents)
                {
                    if (pair.Key == str)
                    {
                        flag = true;
                        break;
                    }
                }

                if (!flag)
                {
                    list.Add(pair.Key);
                }
            }

            foreach (var str in list)
            {
                this.m_Router.Remove(str);
            }
        }

        /// <summary>
        /// Marks as permanent.
        /// </summary>
        /// <param name="eventType">Type of the event.</param>
        public void MarkAsPermanent(string eventType)
        {
            this.m_PermanentEvents.Add(eventType);
        }

        /// <summary>
        /// 添加事件
        /// </summary>
        /// <param name="eventType">
        /// 事件类型
        /// </param>
        /// <param name="listenerBeingAdded">
        /// The listener Being Added.
        /// </param>
        private void OnListenerAdding(string eventType, Delegate listenerBeingAdded)    
        {
            if (!m_Router.ContainsKey(eventType))
            {
                m_Router.Add(eventType, null);
            }

            var delegateHandle = this.m_Router[eventType];
            if (delegateHandle != null && delegateHandle.GetType() != listenerBeingAdded.GetType())
            {
                throw new EventException(
                    $"Try to add not correct event {eventType}. Current type is {delegateHandle.GetType().Name}, adding type is {listenerBeingAdded.GetType().Name}.");
            }
        }

        /// <summary>
        /// 删除事件
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        /// <param name="listenerBeingRemoved">
        /// The listener being removed.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        /// <exception cref="EventException">
        /// 异常消息
        /// </exception>
        private bool OnListenerRemoving(string eventType, Delegate listenerBeingRemoved)
        {
            if (!this.m_Router.ContainsKey(eventType))
            {
                return false;
            }

            var delegateHandle = this.m_Router[eventType];
            if (delegateHandle != null && delegateHandle.GetType() != listenerBeingRemoved.GetType())
            {
                throw new EventException(
                    $"Remove listener {eventType}\" failed, Current type is {delegateHandle.GetType()}, adding type is {listenerBeingRemoved.GetType()}.");
            }

            return true;
        }

        /// <summary>
        /// 移除该类型的所有事件
        /// </summary>
        /// <param name="eventType">
        /// The event type.
        /// </param>
        private void OnListenerRemoved(string eventType)
        {
            if (this.m_Router.ContainsKey(eventType) && this.m_Router[eventType] == null)
            {
                this.m_Router.Remove(eventType);
            }
        }
    }
}

3.3小蛇运动

小蛇运行类控制蛇的移动、分数记录、计时器等相关。小蛇的运动通过遍历蛇的节点位置来实现,每一次移动一个小蛇body的长度,这里需要算一下固定的移动刷新时间=小蛇身体长度除以速度。

using System.Collections.Generic;

namespace Assets.Scripts.Module.Snake
{
    using System;
    using System.Diagnostics;

    using Assets.Libs.Config;
    using Assets.MessageBox.Scripts;
    using Assets.Scripts.Base;
    using Assets.Scripts.EventManager;
    using Assets.Scripts.Language;
    using Assets.Scripts.Module.GameParam;
    using Assets.Scripts.SpriteManager;
    using Assets.Scripts.View.GameRuningView;

    using UnityEngine;
    using UnityEngine.UI;

    using PlayMode = Assets.Libs.Enum.PlayMode;
    using Random = UnityEngine.Random;

    /// <summary>
    /// 蛇类
    /// </summary>
    public class Snake : Singleton<Snake>
    {
        /// <summary>
        /// Y值范围
        /// </summary>
        public const int kYLimited = 760;

        /// <summary>
        /// X值范围
        /// </summary>
        public const int kXLimited = 540;

        /// <summary>
        /// 偏移
        /// </summary>
        public const int kXOffset = 50;

        /// <summary>
        /// The m_ parent.
        /// </summary>
        public Transform parent;

        /// <summary>
        /// The collider 2 d.
        /// </summary>
        public BoxCollider2D boxCollider2D;

        /// <summary>
        /// The head transform.
        /// </summary>
        public Transform headTransform;

        /// <summary>
        /// The image.
        /// </summary>
        public Image image;
            
        /// <summary>
        /// 蛇身体
        /// </summary>
        public List<Transform> bodyList = new List<Transform>();

        /// <summary>
        /// The body.   
        /// </summary>
        public Transform bodyContainer; 

        /// <summary>
        /// The body.
        /// </summary>
        public GameObject body;

        /// <summary>
        /// The config.
        /// </summary>
        public Config config;

        /// <summary>
        /// 历史记录
        /// </summary>
        public List<Record> records;

        /// <summary>
        /// The m_ move time.
        /// </summary>
        private float m_MoveTime;

        /// <summary>
        /// 是否吃到奖励
        /// </summary>
        private bool m_IsReward;

        /// <summary>   
        /// 蛇身图集
        /// </summary>
        private Sprite[] m_Sprites = new Sprite[2];

        /// <summary>
        /// 吃到食物音频
        /// </summary>
        private AudioSource m_EatAudioSource;

        /// <summary>
        /// 碰撞死亡音频
        /// </summary>
        private AudioSource m_DieAudioSource;

        /// <summary>
        /// 蛇头X坐标
        /// </summary>
        private float m_X;

        /// <summary>
        /// 蛇头Y坐标
        /// </summary>
        private float m_Y;

        /// <summary>   
        /// The m_ step.
        /// </summary>
        private float m_Step;

        /// <summary>
        /// 蛇头坐标.
        /// </summary>
        private Vector3 m_Head;

        /// <summary>
        /// 计时器
        /// </summary>
        private Stopwatch m_Stopwatch = new Stopwatch();

        /// <summary>
        /// 当前得分    
        /// </summary>
        public float score { get; set; }

        /// <summary>
        /// 得分加成    
        /// </summary>
        public float bonus { get; set; }

        /// <summary>
        /// Gets蛇的长度
        /// </summary>
        public int length { get; set; }

        /// <summary>
        /// Gets当前速度
        /// </summary>
        public float Speed { get; set; }


        /// <summary>
        /// Gets 是否停止   
        /// </summary>
        public bool IsStop { get; set; }

        /// <summary>
        /// The m_ time str.
        /// </summary>
        public  string TimeStr { get; set; }

        /// <summary>
        /// The init.
        /// </summary>
        /// <param name="parenTransform">
        /// The paren transform.
        /// </param>
        public void Init(Transform parenTransform)
        {
            parent = parenTransform;
            headTransform = parent.Find("Head").transform;
            headTransform.GetComponent<BoxCollider2D>().isTrigger = true;
            image = headTransform.GetComponent<Image>();
            image.sprite = SpriteManager.instance.GetSnakeSkinSprite(0);
            m_Sprites[0] = SpriteManager.instance.GetSnakeSkinSprite(1);
            m_Sprites[1] = SpriteManager.instance.GetSnakeSkinSprite(2);

            m_EatAudioSource = parent.Find("EatAudio").GetComponent<AudioSource>();
            m_EatAudioSource.clip = Resources.Load<AudioClip>("Audios/Success");
            m_EatAudioSource.playOnAwake = false;
            m_DieAudioSource = parent.Find("DieAudi").GetComponent<AudioSource>();
            m_DieAudioSource.clip = Resources.Load<AudioClip>("Audios/notification");
            m_DieAudioSource.playOnAwake = false;

            bodyContainer = parent.Find("Body").transform;
            boxCollider2D = headTransform.GetComponent<BoxCollider2D>();
            boxCollider2D.isTrigger = true;

            this.config = new Config();
            this.records = new List<Record>(1000);

            InitValue();
            AddEvent();
        }

        /// <summary>
        /// 初始化值
        /// </summary>
        public void InitValue()
        {
            Speed = 100;
            m_Step = 1;
            score = 0;
            bonus = 10;
            m_X = 0;
            m_Y = m_Step;
            IsStop = false;
            m_MoveTime = 0;
            headTransform.localPosition = new Vector3(0, 0, 0);
            ClassicModeItem.instance.InitValue();
        }

        /// <summary>
        /// 注册事件
        /// </summary>
        public void AddEvent()
        {
            EventDispatcher.AddEventListener(ConfigEvent.kGamePauseChange, OnGamePauseChang);
            EventDispatcher.AddEventListener(ConfigEvent.kGameContinueChange, OnGameContinueChang);
            EventDispatcher.AddEventListener(ConfigEvent.kLengthChange, OnSnakeLengthChange);
            EventDispatcher.AddEventListener(ConfigEvent.kSnakeSkinChange, OnSnakeSkinChange);
        }

        /// <summary>
        /// 是否向上移动
        /// </summary>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool MovingUp()
        {
            return this.m_Y > 0;
        }

        /// <summary>
        /// 是否向下移动
        /// </summary>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool MovingDown()
        {
            return this.m_Y < 0;
        }

        /// <summary>
        /// 是否向左移动
        /// </summary>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool MovingLeft()
        {
            return this.m_X < 0;
        }

        /// <summary>
        /// 是否向右移动
        /// </summary>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool MovingRight()
        {
            return this.m_X > 0;
        }

        /// <summary>
        /// 每帧更新
        /// </summary>
        public void Update()
        {
            //没有打开界面则不更新
            if (!GameRunningView.instance.isOpen || this.IsStop)
            {
                return;
            }
            
            if (Input.GetKeyDown(KeyCode.W))
            {
                if (this.MovingDown())
                {
                    return;
                }

                headTransform.localRotation = Quaternion.Euler(0, 0, 0);
                m_Y = m_Step;
                m_X = 0;
            }

            if (Input.GetKeyDown(KeyCode.A))
            {
                if (this.MovingRight())
                {
                    return;
                }

                headTransform.localRotation = Quaternion.Euler(0, 0, -90);
                m_Y = 0;
                m_X = -m_Step;
            }

            if (Input.GetKeyDown(KeyCode.S))
            {
                if (this.MovingUp())
                {
                    return;
                }

                headTransform.localRotation = Quaternion.Euler(0, 0, 180);
                m_Y = -m_Step;
                m_X = 0;
            }

            if (Input.GetKeyDown(KeyCode.D))
            {
                if (this.MovingLeft())
                {
                    return;
                }

                headTransform.localRotation = Quaternion.Euler(0, 0, 90);
                m_Y = 0;
                m_X = m_Step; 
            }

            if (Input.GetKey(KeyCode.Space))
            {
                this.Speed += 10f;
                if (Speed > 500)
                {
                    Speed = 500f;
                }
            }
            
            if (Input.GetKeyUp(KeyCode.Space))
            {
                this.Speed = 100;
            }

            //更新移动间隔时间
            this.m_MoveTime += Time.deltaTime;
            var time = 23 / this.Speed;
            if (this.m_MoveTime > time)
            {
                this.Move();
                this.m_MoveTime = 0;
            }

            this.UpdateValue();

            if (this.m_Stopwatch != null && m_Stopwatch.IsRunning)
            {
                TimeSpan elapsedTime = m_Stopwatch.Elapsed;
                string timeString = string.Format(
                    "{0:00}:{1:00}:{2:00}",
                    elapsedTime.Hours,
                    elapsedTime.Minutes,
                    elapsedTime.Seconds);
                TimeStr = timeString;
            }
        }

        /// <summary>
        /// 蛇的移动
        /// </summary>
        public void Move()
        {
            //记录蛇头位置
            m_Head = headTransform.localPosition;

            //蛇头要移动到的位置
            headTransform.localPosition = new Vector3(
                m_Head.x + (m_X * Speed * m_MoveTime),
                m_Head.y + (m_Y * Speed * m_MoveTime),
                0);

            if (bodyList.Count > 0)
            {
                for (int i = bodyList.Count - 2; i >= 0; i--)
                {
                    bodyList[i + 1].localPosition = bodyList[i].localPosition;
                }

                bodyList[0].localPosition = m_Head;
            }
        }

        /// <summary>
        /// 更新值
        /// </summary>
        public void UpdateValue()
        {
            ClassicModeItem.instance.curSpeed = this.Speed;
            ClassicModeItem.instance.curScore = this.score;
            EventDispatcher.TriggerEvent(ConfigEvent.kCurSpeedChange);
        }

        /// <summary>
        /// 碰撞触发函数
        /// </summary>
        /// <param name="collision">
        /// The collision.
        /// </param>
        public void OnTriggerEnter2D(Collider2D collision)
        {
            if (collision.CompareTag("food"))
            {
                //吃到食物
                m_IsReward = false;

                //销毁碰撞到的食物
                UnityEngine.Object.Destroy(collision.gameObject);
                m_EatAudioSource.Play();

                //变长
                Grow();
                EventDispatcher.TriggerEvent(ConfigEvent.kLengthChange);

                //生成食物
                FoodMaker.instance.CreateFood(Random.Range(0, 100) < 20);
            }
            else if (collision.tag == "reward")
            {
                //吃到奖励
                m_IsReward = true;

                //销毁碰撞到的奖励
                UnityEngine.Object.Destroy(collision.gameObject);
                m_EatAudioSource.Play();

                //变长
                Grow();
                EventDispatcher.TriggerEvent(ConfigEvent.kLengthChange);

                //生成食物
                FoodMaker.instance.CreateFood(Random.Range(0, 100) < 20);
            }
            else if (collision.tag == "body")
            {
                //撞到自己身体
                m_IsReward = false;
                m_DieAudioSource.Play();
                TimeStop();
                OnGamePauseChang();
                Record();

                //由消息盒子实现弹窗
                var msg = LanguageModel.instance.GetCurLanguageValue(LanguageKey.kRestart);
                var btnArray = new[]
                              {
                                  LanguageModel.instance.GetCurLanguageValue(LanguageKey.kConfirm), 
                                  LanguageModel.instance.GetCurLanguageValue(LanguageKey.kReturn)
                              };
                MessageBox.Show(
                    msg,
                    btnArray,
                    RestartItem.Restart,
                    RestartItem.ReturnHome);

                //TODO 待实现死亡窗口,重新开始......
            }
            else
            {
                this.m_IsReward = false;
                var mode = GlobalSetting.instance.curPlayMode;
                var isBorder = collision.tag == "Left" ||
                              collision.tag == "Right" ||
                              collision.tag == "Top" || collision.tag == "Bottom";
                if (mode == PlayMode.Classic)
                {
                    if (isBorder)
                    {
                        m_DieAudioSource.Play();
                        this.TimeStop();
                        OnGamePauseChang();
                        this.Record();
                        var msg = LanguageModel.instance.GetCurLanguageValue(LanguageKey.kRestart);
                        var btnArray = new[]
                                           {
                                               LanguageModel.instance.GetCurLanguageValue(LanguageKey.kConfirm),
                                               LanguageModel.instance.GetCurLanguageValue(LanguageKey.kReturn)
                                           };
                        MessageBox.Show(
                            msg,
                            btnArray,
                            RestartItem.Restart,
                            RestartItem.ReturnHome);
                    }
                }
                else if (mode == PlayMode.Freedom)
                {
                    switch (collision.gameObject.name)
                    {
                        case "Top":
                            this.headTransform.localPosition = new Vector3(headTransform.localPosition.x, -headTransform.localPosition.y + 30, 0);
                            break;
                        case "Bottom":
                            headTransform.localPosition = new Vector3(headTransform.localPosition.x, -headTransform.localPosition.y - 30, 0);
                            break;
                        case "Left":
                            headTransform.localPosition = new Vector3(-headTransform.localPosition.x - 30, headTransform.localPosition.y, 0);
                            break;
                        case "Right":
                            headTransform.localPosition = new Vector3(-headTransform.localPosition.x + 30, headTransform.localPosition.y, 0);
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// 身体增加
        /// </summary>
        public void Grow()
        {
            body = Resources.Load<GameObject>("Prefab/SnakeBody");

            //实例化长的身体
            body = UnityEngine.Object.Instantiate(body, new Vector3(2000, 2000, 0), Quaternion.identity);
            body.transform.localScale = new Vector3(1, 1, 1);
            body.transform.GetComponent<BoxCollider2D>().isTrigger = true;
            body.transform.SetParent(this.parent, false);

            //身体的sprite
            int index = bodyList.Count % 2 == 0 ? 0 : 1;
            body.GetComponent<Image>().sprite = this.m_Sprites[index];

            //加入身体链表
            bodyList.Add(body.transform);
        }

        /// <summary>
        /// 退出、清理
        /// </summary>
        public void Clear()
        {
            Speed = 0;
            score = 0;
            length = 0;
            InitValue();

            //销毁小蛇身体
            foreach (var bodyTransform in this.bodyList)
            {
                UnityEngine.Object.Destroy(bodyTransform.gameObject);
            }

            bodyList.Clear();
        }

        /// <summary>
        /// 开始计时
        /// </summary>
        public void TimeStart()
        {
            this.m_Stopwatch.Start();
        }

        /// <summary>
        /// 计时停止
        /// </summary>
        public void TimeStop()
        {
            this.m_Stopwatch.Stop();
        }

        /// <summary>
        /// 重新开始计时
        /// </summary>
        public void TimeRestart()
        {
            this.m_Stopwatch.Restart();
        }

        /// <summary>
        /// The record.
        /// </summary>
        public void Record()
        {
            if (score < 10)
            {
                return;
            }

            var record = new Record
                             {
                                 playMode = GameRunningView.instance.GetPlayMode(),
                                 score = this.score,
                                 length = this.length,
                                 duration = this.TimeStr,
                                 timeOver = DateTime.Now.ToString("yyyyMMddHHmmss")
                             };
            config.records.Add(record);
        }

        /// <summary>
        /// 游戏暂停
        /// </summary>
        private void OnGamePauseChang()
        {
            Speed = 0;
            IsStop = true;
        }

        /// <summary>
        /// 游戏继续
        /// </summary>
        private void OnGameContinueChang()
        {
            Speed = 100;
            IsStop = false;
        }

        /// <summary>
        /// 小蛇长度改变
        /// </summary>
        private void OnSnakeLengthChange()
        {
            this.length = this.bodyList.Count;

            //吃到奖励随机加分0-100
            if (this.m_IsReward)
            {
                var random = Random.Range(0, 100);
                score += random;
                bonus = random;
                return;
            }

            score += 10;
            bonus = 10;
        }

        /// <summary>
        /// 小蛇皮肤改变
        /// </summary>
        private void OnSnakeSkinChange()
        {
            image.sprite = SpriteManager.instance.GetSnakeSkinSprite(0);
            m_Sprites[0] = SpriteManager.instance.GetSnakeSkinSprite(1);
            m_Sprites[1] = SpriteManager.instance.GetSnakeSkinSprite(2);
        }
    }
}

3.4食物制作类

小蛇食物采用预制体,小蛇吃到食物后销毁预制体,身体长度加一,并实例化下一个食物预制体,食物分为普通食物和奖励,吃到普通食物+10分,吃到奖励随机+0-100分。

namespace Assets.Scripts.Module.Snake
{
    using Assets.Scripts.Base;
    using Assets.Scripts.SpriteManager;

    using UnityEngine;
    using UnityEngine.UI;

    /// <summary>
    /// 食物生成类
    /// </summary>
    public class FoodMaker : Singleton<FoodMaker>
    {
        /// <summary>
        /// 食物
        /// </summary>
        public GameObject food;

        /// <summary>
        /// 食物图集
        /// </summary>
        public Sprite[] foodSprites = new Sprite[10];

        /// <summary>
        /// 奖励图集
        /// </summary>
        public Sprite rewardSprite;

        /// <summary>
        /// 奖励
        /// </summary>
        public GameObject reward;

        /// <summary>
        /// 食物容器
        /// </summary>
        private Transform m_FoodContainer;

        /// <summary>
        /// 食物X坐标
        /// </summary>
        private int m_X;

        /// <summary>
        /// 食物Y坐标
        /// </summary>
        private int m_Y;

        /// <summary>
        /// Y值范围
        /// </summary>
        private int m_YLimited; 

        /// <summary>
        /// X值范围
        /// </summary>
        private int m_XLimited; 

        /// <summary>
        /// 偏移
        /// </summary>
        private int m_XOffset;  

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="parenTransform">
        /// The paren transform.
        /// </param>
        public void Init(Transform parenTransform)     
        {
            m_FoodContainer = parenTransform;

            //加载奖励图集
            rewardSprite = Resources.Load<Sprite>("Sprites/Food/IceCream/reward");
        }

        /// <summary>
        /// The init value.
        /// </summary>
        public void InitValue()
        {
            this.m_XLimited = 760;
            this.m_YLimited = 540;
            this.m_XOffset = 30;
            CreateFood(false);
        }

        /// <summary>
        /// 制作食物
        /// </summary>
        /// <param name="isReward">
        /// The isnoreward.
        /// </param>
        public void CreateFood(bool isReward)   
        {
            if (isReward)
            {
                this.m_X = Random.Range(-(this.m_XLimited - this.m_XOffset), this.m_XLimited - this.m_XOffset);
                this.m_Y = Random.Range(-(this.m_YLimited - this.m_XOffset), this.m_YLimited - this.m_XOffset);
                var rewardGameObject = Resources.Load<GameObject>("Prefab/Reward");
                reward = UnityEngine.Object.Instantiate(rewardGameObject);
                reward.transform.SetParent(m_FoodContainer);
                var rewardImage = this.reward.transform.GetComponent<Image>();
                rewardImage.enabled = true;
                rewardImage.sprite = this.rewardSprite;
                reward.transform.localScale = new Vector3(1, 1, 1);
                reward.transform.localPosition = new Vector3(this.m_X, this.m_Y, 0);
                reward.transform.GetComponent<BoxCollider2D>().enabled = true;
                reward.transform.GetComponent<BoxCollider2D>().isTrigger = true;

                //创建了奖励直接返回,不在创建食物
                return;
            }

            int index = Random.Range(0, 9);
            this.m_X = Random.Range(-(this.m_XLimited - this.m_XOffset), this.m_XLimited - this.m_XOffset);
            this.m_Y = Random.Range(-(this.m_YLimited - this.m_XOffset), this.m_YLimited - this.m_XOffset);
            var foodGameObject = Resources.Load<GameObject>("Prefab/EatFood");
            food = UnityEngine.Object.Instantiate(foodGameObject);
            food.transform.SetParent(m_FoodContainer);
            var image = food.transform.GetComponent<Image>();
            image.enabled = true;
            image.sprite = SpriteManager.instance.GetFoodSprite(index);
            food.transform.localScale = new Vector3(1, 1, 1);
            food.transform.localPosition = new Vector3(this.m_X, this.m_Y, 0);
            food.transform.GetComponent<BoxCollider2D>().enabled = true;
            food.transform.GetComponent<BoxCollider2D>().isTrigger = true;
        }

        /// <summary>
        /// 立即销毁食物或奖励,重新开始后重新生成
        /// </summary>
        public void Destroy()
        {
            if (this.food != null)
            {
                UnityEngine.Object.Destroy(this.food.gameObject);
            }

            if (this.reward != null)
            {
                UnityEngine.Object.Destroy(this.reward.gameObject);
            }
        }
    }
}

3.5弹窗管理类

所有的弹窗可重复性高,这里统一采用预制体,脚本挂载在预制体上,通过方法的传入实现不同的消息弹窗及按钮事件。

namespace Assets.MessageBox.Scripts
{
    using System;

    using UnityEngine;
    using UnityEngine.UI;

    /// <summary>
    /// 消息弹窗类
    /// Implements the <see cref="UnityEngine.MonoBehaviour" />
    /// </summary>
    /// <seealso cref="UnityEngine.MonoBehaviour" />
    public class MessageBox : MonoBehaviour
    {
        /// <summary>
        /// 预制体
        /// </summary>
        public static MessageBox instance;

        /// <summary>
        /// 按钮1
        /// </summary>
        public Button btn1;

        /// <summary>
        /// 按钮2
        /// </summary>
        public Button btn2;

        /// <summary>
        /// 按钮3
        /// </summary>
        public Button btn3;

        /// <summary>
        /// 遮罩
        /// </summary>
        public RectTransform mask;

        /// <summary>
        /// 弹窗消息
        /// </summary>
        public Text textInfo;

        /// <summary>
        /// 按钮1点击事件
        /// </summary>
        private Action m_Action1;

        /// <summary>
        /// 按钮2点击事件
        /// </summary>
        private Action m_Action2;

        /// <summary>
        /// 按钮3点击事件
        /// </summary>
        private Action m_Action3;

        /// <summary>
        /// 打开事件
        /// </summary>
        public static event Action OpenedCallback;

        /// <summary>
        /// 关闭事件
        /// </summary>
        public static event Action ClosedCallback;

        /// <summary>
        /// 关闭弹窗预制体
        /// </summary>
        public static void Close()
        {
            if (instance == null)
            {
                return;
            }

            ClosedCallback?.Invoke();
            Destroy(instance.gameObject);
            instance = null;
        }

        /// <summary>
        /// 隐藏弹窗
        /// </summary>
        public static void Hide()
        {
            if (instance == null)
            {
                return;
            }

            ClosedCallback?.Invoke();
            instance.gameObject.SetActive(false);
        }

        /// <summary>
        /// 打开弹窗预制体
        /// </summary>
        /// <returns>MessageBox.</returns>
        public static MessageBox Open()
        {
            if (instance != null)
            {
                return instance;
            }

            instance = Instantiate(Resources.Load<GameObject>("MessageBox")).GetComponent<MessageBox>();
            var objs = GameObject.FindGameObjectsWithTag("Root");
            var currentCanvas = objs[objs.Length - 1];
            instance.transform.SetParent(currentCanvas.transform);
            instance.transform.localPosition = Vector3.zero;
            instance.transform.localScale = Vector3.one;
            instance.transform.localRotation = Quaternion.identity;
            return instance;
        }

        /// <summary>
        /// 显示消息窗口
        /// </summary>
        /// <param name="info">The information.</param>
        /// <param name="btnName">Name of the BTN.</param>
        /// <param name="btn1">The BTN1.</param>
        /// <param name="btn2">The BTN2.</param>
        /// <param name="btn3">The BTN3.</param>
        public static void Show(
            string info,
            string[] btnName = null,
            Action btn1 = null,
            Action btn2 = null,
            Action btn3 = null)
        {
            Open();
            if (btnName != null)
            {
                // Debug.Log(btnName[0]);
            }

            instance.gameObject.SetActive(true);
            instance.mask.localScale = Vector3.one;
            instance.textInfo.text = info;
            if (btnName == null || btnName.Length == 1)
            {
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName == null ? "OK" : btnName[0];
                instance.btn1.gameObject.transform.localPosition = new Vector3(0, -70, 0);
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(false);
                instance.btn3.gameObject.SetActive(false);
            }

            if (btnName != null && btnName.Length == 2)
            {
                instance.btn1.transform.localPosition = new Vector3(-120, -70, 0);
                instance.btn2.transform.localPosition = new Vector3(120, -70, 0);
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName[0];
                instance.btn2.transform.Find("Text").GetComponent<Text>().text = btnName[1];
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(true);
                instance.btn3.gameObject.SetActive(false);
            }

            if (btnName != null && btnName.Length == 3)
            {
                instance.btn1.transform.localPosition = new Vector3(-120, -70, 0);
                instance.btn2.transform.localPosition = new Vector3(0, -70, 0);
                instance.btn3.transform.localPosition = new Vector3(120, -70, 0);
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName[0];
                instance.btn2.transform.Find("Text").GetComponent<Text>().text = btnName[1];
                instance.btn3.transform.Find("Text").GetComponent<Text>().text = btnName[2];
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(true);
                instance.btn3.gameObject.SetActive(true);
            }

            instance.m_Action1 = btn1;
            instance.m_Action2 = btn2;
            instance.m_Action3 = btn3;

            OpenedCallback?.Invoke();
        }

        /// <summary>
        /// 按钮1点击
        /// </summary>
        private void OnBtn1Click()
        {
            Hide();
            this.m_Action1?.Invoke();
            this.m_Action1 = null;
        }

        /// <summary>
        /// 按钮2点击
        /// </summary>
        private void OnBtn2Click()
        {
            Hide();
            this.m_Action2?.Invoke();
            this.m_Action2 = null;
        }

        /// <summary>
        /// 按钮3点击
        /// </summary>
        private void OnBtnMClick()
        {
            Hide();
            this.m_Action3?.Invoke();
            this.m_Action3 = null;
        }

        /// <summary>
        /// 销毁
        /// </summary>
        private void OnDestroy()
        {
            instance = null;
        }

        /// <summary>
        /// 开始
        /// </summary>
        private void Start()
        {
            this.btn1.onClick.AddListener(this.OnBtn1Click);
            this.btn2.onClick.AddListener(this.OnBtn2Click);
            this.btn3.onClick.AddListener(this.OnBtnMClick);
        }
    }
}

3.6中、英文语言实现类

这里采用XML语言文档储存中文、英文,通过脚本读取XML文档中的语言信息,储存在对应的中英文字典中,通过不同的键值对来查找字典。

using System.Collections.Generic;

namespace Assets.Scripts.Language
{
    using System.IO;
    using System.Xml;
    using Assets.Libs.Config;
    using Assets.Libs.Enum;
    using Assets.Scripts.Base;
    using Assets.Scripts.LogManager;
    using JetBrains.Annotations;
    using UnityEngine;

    /// <summary>
    /// 语言模型
    /// </summary>
    public class LanguageModel : Singleton<LanguageModel>
    {
        /// <summary>
        /// 中文字典
        /// </summary>
        [NotNull]
        private readonly Dictionary<int, string> m_ChDictionary = new Dictionary<int, string>();

        /// <summary>
        /// 英文字典
        /// </summary>
        private readonly Dictionary<int, string> m_EhDictionary = new Dictionary<int, string>();

        /// <summary>
        /// 获取语言
        /// </summary>
        /// <param name="value">    
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string GetCurLanguageValue(int value)    
        {
            if (GlobalSetting.instance.curLanguage == Language.Ch)
            {
                return this.m_ChDictionary[value];
            }
            else if (GlobalSetting.instance.curLanguage == Language.En)
            {
                return this.m_EhDictionary[value];
            }
            else
            {
                Debug.LogError($"不支持的语言类型{GlobalSetting.instance.curLanguage}");
                return null;
            }
        }

        /// <summary>
        /// 初始化xml字典
        /// </summary>
        public void Init()
        {
            var path = Path.Combine(Application.streamingAssetsPath, "Language.xml");
            XmlDocument xmlDoc = new XmlDocument();
            try
            {
                xmlDoc.Load(path);
                XmlNodeList zhDictionaryNodes = xmlDoc.SelectNodes("/Snake/ZhDictinary/item");
                if (zhDictionaryNodes != null)
                {
                    foreach (XmlNode node in zhDictionaryNodes)
                    {
                        if (node.Attributes != null)
                        {
                            string idStr = node.Attributes["id"].Value;
                            string content = node.InnerText;
                            if (int.TryParse(idStr, out var id))
                            {
                                this.m_ChDictionary.Add(id, content);
                            }
                        }
                    }
                }

                XmlNodeList enDictionaryNodes = xmlDoc.SelectNodes("/Snake/EnDictinary/item");
                if (enDictionaryNodes != null)
                {
                    foreach (XmlNode node in enDictionaryNodes)
                    {
                        if (node.Attributes != null)
                        {
                            string idStr = node.Attributes["id"].Value;
                            string content = node.InnerText;
                            if (int.TryParse(idStr, out var id))
                            {
                                this.m_EhDictionary.Add(id, content);
                            }
                        }
                    }
                }

                LogManager.instance.LogToFile("语言文档加载成功", LogLevel.Information);
            }
            catch (System.Exception e)
            {
                Debug.LogError("Failed to load language XML file: " + e.Message);
            }
        }
    }
}

3.7日志管理

日志用Stream类写入text文档中,并使用Unity自带的Application.logMessageReceived来捕获全局异常事件并记录。

namespace Assets.Scripts.LogManager
{
    using System;
    using System.IO;
    using System.Text;
    using Assets.Libs.Enum;
    using Assets.Scripts.Base;
    using UnityEngine;

    /// <summary>
    /// 日志管理
    /// </summary>
    public class LogManager : Singleton<LogManager>
    {
        /// <summary>
        /// The writer.
        /// </summary>
        private StreamWriter m_Writer;

        /// <summary>
        /// 初始化
        /// </summary>
        public void Init()
        {
            // 获取exe文件所在目录的路径
            string path = Path.GetDirectoryName(Application.dataPath) ?? Application.dataPath;

            // 创建日志文件
            string filePath = Path.Combine(path, "log.txt");
            m_Writer = new StreamWriter(filePath, true, Encoding.UTF8);
            OnEnable();
        }

        /// <summary>
        /// 写入日志
        /// </summary>
        /// <param name="log">
        /// 日志信息
        /// </param>
        /// <param name="level">
        /// 日志等级
        /// </param>
        public void LogToFile(string log, LogLevel level)
        {
            var currentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            var logStr = "[" + level.ToString() + "] " + "[" + currentTime + "] " + log;

            // 将日志写入文件
            m_Writer.WriteLine(logStr);
        }

        /// <summary>
        /// 程序退出
        /// </summary>
        public void Quit()
        {
            OnDisable();
            if (m_Writer != null)
            {
                m_Writer.Close();
                m_Writer.Dispose();
                m_Writer = null;
            }
        }

        /// <summary>
        /// 注册全局异常捕获事件
        /// </summary>
        private void OnEnable()
        {
            Application.logMessageReceived += HandleLog;
        }

        /// <summary>
        /// 取消注册全局异常捕获事件
        /// </summary>
        private void OnDisable()
        {
            Application.logMessageReceived -= HandleLog;
        }

        /// <summary>
        /// The handle log.
        /// </summary>
        /// <param name="logString">
        /// The log string.
        /// </param>
        /// <param name="stackTrace">
        /// The stack trace.
        /// </param>
        /// <param name="type">
        /// The type.
        /// </param>
        private void HandleLog(string logString, string stackTrace, LogType type)
        {
            if (type == LogType.Error || type == LogType.Exception)
            {
                // 捕获到错误或异常时,记录日志
                LogToFile(logString, LogLevel.Error);
                LogToFile(stackTrace, LogLevel.Error);
            }
            else if (type == LogType.Warning)
            {
                // 捕获到警告时,记录日志
                LogToFile(stackTrace, LogLevel.Warning);
            }
        }
    }
}

3.8配置与游戏记录数据保存

该项目中游戏配置及数据的保存使用MessagePack进行,在github上开源,有专门的Unity包,直接导入Unity即可,写一个帮助类,更好的调用我们想要使用的方法。

namespace Assets.Libs.Config
{
    using System;
    using System.IO;
    using MessagePack;
    using MessagePack.Resolvers;

    /// <summary>
    /// 序列化帮助类
    /// </summary>
    public static class MsgHelper
    {
        /// <summary>
        /// 从bytes中反序列化对象
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="bytes">The bytes.</param>
        /// <param name="useCompression">是否LZ4压缩</param>
        /// <returns>T.</returns>
        public static T DeserializeFormBytes<T>(byte[] bytes, bool useCompression = false)
        {
            if (useCompression)
            {
                return MessagePackSerializer.Deserialize<T>(bytes, GetLz4BlockArray());
            }
            else
            {
                return MessagePackSerializer.Deserialize<T>(bytes);
            }
        }

        /// <summary>
        /// 从文件路径中,反序列化对象
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="path">The path.</param>
        /// <returns>T.</returns>
        public static T DeserializeFormPath<T>(string path)
        {
            using (var fileStream = new FileStream(path, FileMode.Open))
            {
                return DeserializeFormStream<T>(fileStream);
            }
        }

        /// <summary>
        /// 从Stream中反序列化对旬
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="stream">The stream.</param>
        /// <param name="useCompression">if set to <c>true</c> [use compression].</param>
        /// <returns>T.</returns>
        public static T DeserializeFormStream<T>(Stream stream, bool useCompression = false)
        {
            if (useCompression)
            {
                return MessagePackSerializer.Deserialize<T>(stream, GetLz4BlockArray());
            }
            else
            {
                return MessagePackSerializer.Deserialize<T>(stream);
            }
        }

        /// <summary>
        /// 读取序列化文件
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="filePath">The file path.</param>
        /// <returns>T.</returns>
        public static T ReadFile<T>(string filePath)
        {
            using (var fileStream = new FileStream(filePath, FileMode.Open))
            {
                return DeserializeFormStream<T>(fileStream);
            }
        }

        /// <summary>
        /// 序列化克隆
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <returns>T.</returns>
        public static T SerializeClone<T>(T target)
        {
            var configBytes = SerializeToBytes(target);
            return DeserializeFormBytes<T>(configBytes);
        }

        /// <summary>
        /// 将对象序列化成bytes
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <param name="useCompression">是否LZ4压缩</param>
        /// <returns>System.Byte[].</returns>
        public static byte[] SerializeToBytes<T>(T target, bool useCompression = false)
        {
            if (useCompression)
            {
                return MessagePackSerializer.Serialize(target, GetLz4BlockArray());
            }
            else
            {
                return MessagePackSerializer.Serialize(target);
            }
        }

        /// <summary>
        /// 将对象序列化到Stream里
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="stream">The stream.</param>
        /// <param name="target">The target.</param>
        /// <param name="useCompression">是否LZ4压缩</param>
        public static void SerializeToStream<T>(Stream stream, T target, bool useCompression = false)
        {
            if (useCompression)
            {
                MessagePackSerializer.Serialize(stream, target, GetLz4BlockArray());
            }
            else
            {
                MessagePackSerializer.Serialize(stream, target);
            }
        }

        /// <summary>
        /// 写入序列化文件
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <param name="filePath">The file path.</param>
        /// <param name="fileMode">The file mode.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        public static bool WriteFile<T>(T target, string filePath, FileMode fileMode)
        {
            FileStream fileStream = null;
            try
            {
                var bytes = SerializeToBytes(target);
                fileStream = new FileStream(filePath, fileMode);
                fileStream.Write(bytes, 0, bytes.Length);
            }
            catch (Exception ex)
            {
                UnityEngine.Debug.LogError(ex.Message);
                return false;
            }
            finally
            {
                if (fileStream != null)
                {
                    fileStream.Close();
                }
            }

            return true;
        }

        /// <summary>
        /// 获取数据压缩配置
        /// </summary>
        /// <returns>MessagePackSerializerOptions.</returns>
        private static MessagePackSerializerOptions GetLz4BlockArray()
        {
            // 添加解析器
            var resolver = CompositeResolver.Create(
                StandardResolver.Instance,
                MessagePack.Unity.UnityResolver.Instance,
                BuiltinResolver.Instance,
                AttributeFormatterResolver.Instance,
                GeneratedResolver.Instance,
               ContractlessStandardResolver.Instance);

            var options = StandardResolverAllowPrivate.Options.WithCompression(MessagePackCompression.Lz4BlockArray)
                .WithResolver(resolver);
            return options;
        }
    }
}

配置管理类主要管理配置的保存以及加载。

namespace Assets.Libs.Config
{
    using System;
    using System.IO;
    using Assets.Scripts.Base;
    using Assets.Scripts.Module.Snake;
    using UnityEngine;
    using FileMode = System.IO.FileMode;

    /// <summary>
    /// 配置管理器,加载、保存配置
    /// </summary>
    public class ConfigManager : Singleton<ConfigManager>
    {
        /// <summary>
        /// 配置文件的文件头编码,禁止修改
        /// </summary>
        private const long kConfigFileHeader = 15175621787216;

        /// <summary>
        /// 保存当前配置
        /// </summary>
        public void SaveLastConfig()
        {
            var path = this.GetLastConfigPath();
            this.SaveConfig(path);
        }

        /// <summary>
        /// 保存配置
        /// </summary>
        /// <param name="path">保存路径</param>
        public void SaveConfig(string path)
        {
            var dir = Path.GetDirectoryName(path);
            if (!Directory.Exists(dir))
            {
                if (dir != null)
                {
                    Directory.CreateDirectory(dir);
                }
            }

            //保存为cfg格式
            this.SaveConfigCfg(path);
        }

        /// <summary>
        /// 加载最新配置
        /// </summary>
        public void LoadLastConfig()
        {
            var path = this.GetLastConfigPath();

            if (!File.Exists(path))
            {
                //初始化全局设置
                GlobalSetting.instance.Init();
            }

            LoadConfig(path);
        }

        /// <summary>
        /// 加载默认配置
        /// </summary>
        public void LoadDefaultConfig()
        {
            TextAsset configBytes;
            var config = new GlobalSetting();
            configBytes = Resources.Load<TextAsset>("config/defaultConfig");

            this.LoadConfig(configBytes);
        }

        /// <summary>
        /// 加载配置
        /// </summary>
        /// <param name="path">配置所在文件路径</param>
        public void LoadConfig(string path)
        {
            try
            {
                using (var fileStream = new FileStream(path, FileMode.Open))
                {
                    this.LoadConfig(fileStream);
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"load config error,msg:{e.Message},stackTrace:{e.StackTrace}");
            }
        }

        /// <summary>
        /// 获取最新配置文件路径
        /// </summary>
        /// <returns>配置文件路径</returns>
        private string GetLastConfigPath()
        {
            if (Application.platform == RuntimePlatform.WindowsPlayer ||
                Application.platform == RuntimePlatform.WindowsEditor)
            {
                return $@"{Application.dataPath}\..\config\lastConfig.cfg";
            }

            return $@"{Application.persistentDataPath}/config/lastConfig.cfg";
        }

        /// <summary>
        /// 加载配置
        /// </summary>
        /// <param name="configBytes">配置内容</param>
        /// <param name="initData">是否初始化数据 true:初始化 false:不初始化</param>
        private void LoadConfig(TextAsset configBytes, bool initData = true)
        {
            using (var stream = new MemoryStream(configBytes.bytes))
            {
                this.LoadConfig(stream, initData);
            }
        }

        /// <summary>
        /// 加载配置
        /// </summary>
        /// <param name="stream">配置文件流</param>
        /// <param name="initData">是否初始化数据 true:初始化 false:不初始化</param>
        private void LoadConfig(Stream stream, bool initData = true)
        {
            using (BinaryReader br = new BinaryReader(stream))
            {
                long header = br.ReadInt64();
                if (header == kConfigFileHeader)
                {
                    // 读取新版本的配置文件
                    var configFile = MsgHelper.DeserializeFormStream<Config>(stream, true);
                    configFile.ApplySetting();
                }
                else
                {
                   Debug.LogError("加载配置错误!");
                }
            }
        }

        /// <summary>
        /// 保存为cfg格式
        /// </summary>
        /// <param name="path">保存路径</param>
        private void SaveConfigCfg(string path)
        {
            using (var fileStream = new FileStream(path, FileMode.Create))
            {
                using (BinaryWriter bw = new BinaryWriter(fileStream))
                {
                    bw.Write(kConfigFileHeader);         // 文件头,用于校验文件
                    var config = Snake.instance.config;
                    MsgHelper.SerializeToStream(fileStream, config, true);
                }
            }
        }
    }
}

3.9视图(单例模式)管理

使用单例模式及泛型,写一个视图基类,方便管理视图,其中打开视图和关系视图定为虚方法,由子类根据需要进行重写。

namespace Assets.Scripts.View.BaseView
{
    using UnityEngine;

    /// <summary>
    /// The base view.
    /// </summary>
    /// <typeparam name="T">
    /// 视图类
    /// </typeparam>
    public class BaseView<T>
        where T : new()
    {
        /// <summary>
        /// 视图是否打开
        /// </summary>
        public bool isOpen;

        /// <summary>
        /// 视图父节点
        /// </summary>
        protected Transform m_Parent;

        /// <summary>
        ///
        /// </summary>
        private static readonly object s_Lock = new object();   

        /// <summary>
        /// 视图单列
        /// </summary>
        private static T s_Instance;

        /// <summary>
        /// 构造函数
        /// </summary>
        protected BaseView()
        {
            System.Diagnostics.Debug.Assert(s_Instance == null, "视图为空");
        }

        /// <summary>
        /// Gets视图对象属性(视图单列全局访问点)
        /// </summary>
        public static T instance
        {
            get
            {
                if (s_Instance == null)
                {
                    lock (s_Lock)
                    {
                        if (s_Instance == null)
                        {
                            s_Instance = new T();
                        }
                    }
                }

                return s_Instance;
            }
        }

        /// <summary>
        /// 视图对象是否存在
        /// </summary>
        /// <value><c>true</c> if exists; otherwise, <c>false</c>.</value>
        public static bool exists => s_Instance != null;

        /// <summary>
        /// 打开视图
        /// </summary>
        public virtual void Open()
        {
            isOpen = true;
            m_Parent.gameObject.SetActive(true);
        }

        /// <summary>
        /// 隐藏视图
        /// </summary>
        public virtual void Close()
        {
            isOpen = false;
            m_Parent.gameObject.SetActive(false);
        }
    }
}

3.10文本、按钮管理

项目中的文本、按钮、Toggle等复用性高,建立相关基类通过继承使用,减小代码耦合和提高维护性。

namespace Assets.Scripts.Base.Param
{
    using Assets.Libs.Config;
    using Assets.Scripts.EventManager;
    using Assets.Scripts.Language;
    using Assets.Scripts.SpriteManager;
    using Assets.Scripts.ThemeModel;
    using UnityEngine;
    using UnityEngine.UI;

    /// <summary>
    /// 按钮基类
    /// </summary>
    public abstract class ButtonItem
    {
        /// <summary>
        /// 按钮
        /// </summary>
        protected Button m_Button;

        /// <summary>
        /// 按钮文本
        /// </summary>
        protected Text m_Text;

        /// <summary>
        /// 按钮图片
        /// </summary>
        protected Image m_Image;

        /// <summary>
        /// 父节点
        /// </summary>  
        protected Transform m_Parent;

        /// <summary>
        /// 按钮
        /// </summary>
        internal Button button => m_Button;

        /// <summary>
        /// Gets按钮名称,子类实现
        /// </summary>
        internal abstract string name { get; }

        /// <summary>
        /// Gets字典编号,子类实现
        /// </summary>
        internal abstract int titleLanguageKey { get; }

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="parenTransform">
        /// The paren transform.
        /// </param>
        public virtual void Init(Transform parenTransform)
        {
            m_Parent = parenTransform;
            m_Button = m_Parent.Find(name).GetComponent<Button>();
            m_Text = m_Button.transform.Find("Text").GetComponent<Text>();
            m_Text.text = LanguageModel.instance.GetCurLanguageValue(titleLanguageKey);
            m_Text.color = ThemeModel.instance.GetColorValue(ColorKey.kText);
            m_Text.fontSize = 35;
            m_Text.font = FontManager.instance.GeFont((int)GlobalSetting.instance.curFontStyle);
            m_Image = m_Button.transform.Find("Image").GetComponent<Image>();
            m_Image.color = ThemeModel.instance.GetColorValue(ColorKey.kLeftButton);
            m_Button.onClick.AddListener(BtnClick);
            AddEvent();
            InitValue();
        }

        /// <summary>
        /// 注册事件
        /// </summary>
        public virtual void AddEvent()
        {
            EventDispatcher.AddEventListener(ConfigEvent.kSystemLanguageChange, OnSystemLanguageChange);
            EventDispatcher.AddEventListener(ConfigEvent.kSystemThemeChange, OnSystemThemeChange);
        }

        /// <summary>
        /// 按钮点击,抽象方法由子类实现
        /// </summary>
        internal abstract void BtnClick();

        /// <summary>
        /// 显示
        /// </summary>
        internal virtual void Show()
        {
            this.m_Parent.gameObject.SetActive(true);
        }

        /// <summary>
        /// 初始化值
        /// </summary>
        protected virtual void InitValue()
        {
        }

        /// <summary>
        /// 取消注册事件
        /// </summary>
        protected virtual void RemoveEvent()
        {
            EventDispatcher.RemoveEventListener(ConfigEvent.kSystemLanguageChange, OnSystemLanguageChange);
            EventDispatcher.RemoveEventListener(ConfigEvent.kSystemThemeChange, OnSystemThemeChange);
        }

        /// <summary>
        /// 语言改变时调用
        /// </summary>
        private void OnSystemLanguageChange()
        {
            this.m_Text.text = LanguageModel.instance.GetCurLanguageValue(this.titleLanguageKey);
        }

        /// <summary>
        /// 主题改变时调用
        /// </summary>
        private void OnSystemThemeChange()
        {
            this.m_Image.color = ThemeModel.instance.GetColorValue(ColorKey.kLeftButton);
            this.m_Text.color = ThemeModel.instance.GetColorValue(ColorKey.kText);
        }
    }
}

4.项目资源及地址

4.1贪吃蛇下载地址

本项目所有资源、源码分享给到大家,如果觉得学到了东西,给颗星星支持一下,谢谢!github地址:github.com/2022Cyberpu…

存在不足之处,大家也可以评论区留言讨论,共同进步!

4.2MessagePack下载地址

序列化及反序列化MessagePack,有专门的Unity包,里面有详细的使用说明,github地址:github.com/MessagePack…