1.前言
本人自己刚转入C#编程学Unity不久,工作过程中也是在别人已经搭好的框架下进行软件功能的开发,时间稍久便会发现自己进步太慢,于是在网上找了一个Unity实战项目。发现贪吃蛇这个比较简单,也有较多前辈大佬分享了项目源码以及Unity资源,于是萌生了自己从头写一个贪吃蛇的想法,一方面巩固自己C#编程基础功底,一方面巩固Unity的相关知识。所以这个贪吃蛇项目就这么诞生了,其中的很多UI资源都是借鉴之前很多前辈大佬的资源,在此表达感谢!
当然相比于之前的贪吃蛇,加上自己的想法,进行了很多优化以及功能设置的添加,比如加了一些常见的设置功能、日志记录功能、游戏数据的保存功能等等。除此之外之前较多Unity实战项目大佬们力求代码精简及效率,很多代码都是挂载在UI资源上,对于初学者理解及学习比较吃力,自己也是生啃,花了较多时间去构建这个新的贪吃蛇项目,留一个程序入口脚本,其余通过代码驱动资源。当然本人也是菜鸡一枚,项目中错误难免,分享的本意是帮助每一个像我这样刚入行的朋友,较快的学习理解Unity及C#编程,欢迎大家批评指正,共同进步!
2.界面
2.1游戏主界面
游戏主界面提供三个按钮,一个开始游戏,点击后进入游戏界面,游戏设置按钮,点击后进入游戏设置页面,进行一些常规的设置,还可以查看游戏记录。
2.2游戏设置界面
常规设置界面,常规设置中提供中英文切换,主题(深色、浅色)切换,食物皮肤(植物、冰淇淋)切换,小蛇皮肤(蓝色、黄色)切换,游戏模式(经典模式、自由模式)切换。
游戏记录页面,游戏记录界面提供玩家所有游戏记录,包括编号、模式、游戏时间、分数、长度及单局时长。
2.3游戏说明界面
游戏说明界面,主要介绍游戏玩法及规则。
2.4游戏界面
游戏界面左侧提供当局的实时数据,包括模式、历史最高分、得分、速度、长度、得分加成、当局时长记录。另外提供三个按钮,暂停游戏、继续游戏、返回主界面。
哈哈哈哈是不是觉得界面Low爆了,没办法,自己美术功底太差...
2.5Unity界面
Unity界面,有项目工程资源目录、各层级节点,可以调整布局,启动代码后初始化界面。
3.主要功能
项目工程概览,主要有三个解决方啊,第一个解决方案Assembly-CSharp,主要是本工程的脚本,另外两个解决方案主要是MessagePack的解决方案,导入MessagePack的Unity包后会自动生成。
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…