从零点五开始用Unity做半个2D战棋小游戏(五)
发表于2019-01-15
好久不见。
这是第21篇与游戏开发有关的文章。
| 写在最前
这次想要一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方依据体力在地图上轮流行动并向对方发动攻击,先消灭掉所有敌人的一方将获得胜利。
预计将分为以下几篇(未完成前可能会调整...):
1、创建战场(已完成)
根据预定尺寸生成战场地图,并随机一些障碍物。
2、添加地图功能 (已完成)
实现战场格子点击反馈,地图导航及范围选定。
3、添加对战双方 (已完成)
向战场中添加作战单位,作战单位轮流行动,可进行移动、攻击。
4、加入玩家控制 (已完成)
玩家可控制一个战斗单位,手动选择移动目标及攻击单位。
5、添加常用的界面
建立界面管理器,加入一些常用的界面。
6、添加常用的战场显示
为战斗单位添加血条,加入伤害文字显示。
7、扩展作战单位
丰富作战单位的类型,添加职业,并加入若干不同类型的技能。
8、扩展战场地图
丰富战场地图,加入地形及道具等元素。
9、规范战斗配置
可以通过规范化的数据结构配置战场、职业、技能、道具等。
本次的主题是:添加常用的界面。
| 目标
使用Unity自带的UGUI替换之前的GUI来实现一些常用界面:
1、包含开始战斗按钮及战斗结束时提示文字的主界面。
2、玩家手动操作时,辅助选择移动、攻击及待命的弹出面板。
3、点选地图、战斗单位时,弹出的详情展示面板。
实现后的效果如下图:
战斗结束时显示的友情提醒
行动选择菜单
战场单位信息面板
不难看出,当UGUI碰撞上专业的素材后,一个个绝美的界面瞬间跃然屏上。这也给了那些常说“程序员不懂美”的家伙们一记响亮的耳光。
| 关于如何制作界面
这些界面都是用UGUI制作的,并没有什么难度,相信上手过UGUI或NGUI的同学,只要碰到精美的纹理贴图,都能轻松完成。
使用的是一套高雅灰主题的专业UI纹理素材
本文不会对UGUI的使用做详细介绍,我们将重点聊聊对界面的管理。
因为只要找到了方法,做出漂亮的界面就只是时间问题罢了。
而至于证明“程序员也能凭自己的能力作出专业界面”这件事,相信上面已经做到了。
受项目大小及时间所限,这里会使用一套轻量级的界面管理方式。而在此之前,让我们先做一些前期准备工作。
| 分配相机
为界面的绘制单独分配一个相机(界面相机),并调整界面相机和战场相机上的Clear Flags、CullingMask和Depth设置。
界面相机与战场相机的设置
整个界面系统的根画布(ScreenCanvas),是一个类型为ScreenSpace-Camera的Canvas,它的父节点ScreenUIRoot上有专门渲染界面的界面相机。
| 设置层级
ScreenCanvas下设5个节点,分别对应5个层级:背景层、基础层、弹出层、顶层和Debug层.
界面的层级结构
各层的功能为:
背景层:装饰性的、非功能性的界面。
基础层:常驻的界面(主界面、角色头像、快捷操作栏等)。
弹出层:点击后弹出的界面(各功能界面)。
顶层:强制显示在最上层的界面(Tips界面或走马灯等)。
Debug层:开发时辅助调试用。
这些层级从下到上放置,遮挡关系是上层遮挡下层。当然,其顺序、层数和名称可根据实际需求进行调整。
需要注意的是,这5个节点(Transform)并非是必须的,它们只是为了Debug时能更直观的查看层级间的关系,真正用于用于区分层级的是Canvas上的SortingLayer和OrderInLayer属性。
SortingLayers设置中的层级与对应的节点
不同界面上Canvas所设置的Layer和Order值
| 界面管理流程
比起代码,我觉得还是看图来的更直观些。
界面管理流程
整个界面管理可以简单拆解为四个部分:打开界面、关闭界面、层级刷新及界面刷新,下面我们依次介绍。
| 打开界面
打开界面的逻辑很简单,但需要注意的是,为了更好的使用内存,界面管理器维护了两个界面缓存区:常驻缓存和临时缓存。打开界面时如果需要加载新的界面,先去这两个缓存区中查看一下是否有缓存过的界面,从缓存区加载比重新读取新的界面效率要高。
| 关闭界面
当我们要关闭界面时,界面管理器会根据它的“存储策略”决定其被关闭后的去留,有些会被放入到常驻缓存区,有些会被放到临时缓存区,而有些则会被直接移除。
这就好比两性交往中女生犯错通常当时就会被原谅;男生犯错通常需要好好表现一段时间才有可能被原谅;而单身狗连犯错的机会都没有。
| 层级刷新
上面介绍了打开和关闭界面。其实它们都可以被看成是在“显示界面”。因为关闭界面也可以理解为“被这个界面遮挡覆盖的界面,可能需要显示了”。
因此,当有界面被打开或关闭后,界面管理器会从上到下的让各层刷新自己的显示状态及对屏幕的遮挡状态,并将这个遮挡状态向下传递,用作后面层级的显示判断。
| 界面刷新
我们知道界面的刷新和显示是有代价的,因为它们会对CPU及GPU的性能造成开销。因此我为每个界面设置了是否遮挡了屏幕、不可见时是否仍然刷新及Dirty属性。
如果一个界面遮挡了屏幕,那么它下面的界面首先应该被“隐藏”以减小渲染的压力;其次如果不可见的界面没有被设置为“不可见时仍然刷新”,则当需要刷新它时(界面数据发生了变化),也只是被打上Dirty标记,并在下次需要显示的时候再刷新。
| 界面的生命周期函数
上面流程图中的蓝色部分,是界面的生命周期函数,它们会在适当的时间被界面管理器、层级或界面自身调用。
Init:初始化,界面首次被加载后调用。
OnPush:界面被显示前,加入层级时被调用。
OnShow:界面被显示时调用。
UpdateView:界面需要被刷新时调用。
OnHide:界面被隐藏时调用。
OnPopup:界面被关闭,从层级中移除时调用。
OnExit:界面不需要被缓存,被Destroy前调用。
一次完整的界面打开、刷新、关闭过程
| 界面的存储策略
界面被关闭后会根据预设的存储策略决定去留,这里定义的存储策略有以下三种。
自动移除:很少用到的界面,每次关闭时会被直接Destroy掉。
自动移除策略
临时缓存区:较为常用的界面,在关闭时我们把它放入一个有深度设置的缓存区,这个缓存区当收入一个新的界面时,会判断缓存量是否已超过预设深度;如果超过了预设深度,会将最早缓存的界面弹出并Destroy掉。
临时缓存区策略(深度为2)
常驻缓存区:需要频繁开关的界面,在关闭时我们会把它们放入一个没有深度设置(缓存个数限制)的缓存区。
常驻缓存区策略(仅唯一存在界面可被设置为常驻)
| 界面配置
我使用ScriptableObject对象作为界面配置的载体。每次创建新的界面时,需要同样创建一个配置对象,并将两者进行关联。界面自身及使用者可以通过读取这个对象获取界面的配置信息。
界面配置文件的属性构成
当然我们可以稍微修改Editor,添加一些辅助工具帮助我们快速生成界面配置文件。
一个简单的配置文件刷新器
| 写在最后
至此,添加常用的界面篇就介绍到这里,详细代码可以移步Github:
感谢您能读到这里。
愿不忘初心。
下回见。