用Unity创建街机游戏的一些经验分享
发表于2016-08-08
技术相关信息
使用引擎:unity3d
开发语言:C#(你有别的选择吗?)
Why Unity3D?
为啥使用U3D?
选择U3D有以下几条原因:
先前有经验,学习没曲线
多平台支持
开发周期短
Unity或许不是开发街机游戏的最好选择,但是我想尽快从游戏开发中获利,我有点积蓄,但是不多,因此我可没什么闲情逸致去弄个专门的游戏引擎。此外,俺的数学水平颇低,这也是我不敢搞自定义引擎的原因。(神呐!是我翻错了,还是你写错了?)
好处
快速搞定
Unity非常有利于快速原型建立,无需编写自己的物理引擎,精灵动画甚至状态机,全内置了。
编辑器
在编辑器中进行设置超级方便,你可以快速的修改各项设置,对正在进行的工作有一个总体把握,我总会创建一堆的自定义编辑器,后面我会谈到。
劣势
好处很少吧,以下是原因
不开源
Unity的源代码不公开,也就意味着如果想在Unity中干点出格的事总是辣么难。我会把这个问题分成几部分进行讲解;(您老对Unity就这么苦大仇深吗…)
物理引擎
SpiritSphere有着良好的街机游戏风格,结合了老式动作RPG和空气曲棍球的玩法,Unity的2D物理引擎是基于box2D,非常适合这样一款游戏。
物理预测:
游戏里有一个单人模式,AI需要知道球最后的落地点以免失分,但是没有内置的方法进行物理模拟,或预测。
最后我只好推算球的落点,尽管搞定了,但由于是建立在物理假设基础上, 最后我对预测的结果总是狐疑重重,看不到底层的处理机制就是这么悲催。
总的来说,感觉就像是我在和Unity的物理机制干架,这赶脚不是那么良好。
序列化
一般来说Unity编辑器都棒,而有时对我的折磨也是永无止境。
除了gameobject外,你还可以向编辑器中添加[System.Serializeable]属性
[System.Serializable] public class Loot { public UnityEngine.Object item; public Sprite thumbnail; public int probabillity = 1; public int quanity = 1; public int value = 1; } |
[System.Serializable] public class LootTable { private int maxProbabillity = -1; public bool debug = false ; public List public List< int > lootProbabbillities= new List< int >(); } int > int > |
这是一个森林关卡中草地地块的截图画面,我可以在编辑器中指定使用何种地块,不错吧?
现在假设我想创建一个继承自Loot的新类,我先暂将其命名为SpecialLoot。
草地物体中有一个List(Loot游戏物体的队列),在我向数组中添加SpecialLoot实例时,Unity竟然不认?SpecialLoot,是神马鬼?我只好把SpecialLoot又全部转换为普通的Loot实例,然后抛弃所有相关数据。Unity编辑器竟然不支持继承,这真是让人如坐针毡啊。
铺地面的网格
按照Unity的建议,铺地面的网格就是简单的游戏物体和精灵渲染器的集合。但众所周知,这是一个性能大忌,我相信Unity正在开发一个tilegrid系统,但是在他们完工之前,我只能自己写。
Tilegrid简单来说,就是用带有许多附加顶点的方形网格做成的地块。通过设置顶点的UV坐标,我们可以指定是否显示相应的地块。
我的tilesheet(这个词是作者拼出来的,可以参看下面的图)有16个地块宽,是我需要为每个角落设置特定的类型地块的总数,这样,我就可以根据当前地块来选择正确的UV坐标了。
float indexY = Mathf.Floor(tile.ID / 16 ); float indexX = Mathf.Floor(tile.ID - (indexY * 16 ) ); //using 16x16 tilesheet
|
上面的代码工作的很好,为了更轻松的创建场景,我添加了一些自定义的编辑器来直接在editor中绘制tilegrid。
我将tilegrid分块,这样当一个地块改变时,我无需调整整个的tilegrid,只要处理其中一小部分就OK了。
但是却存在着一个小小的例外,在一个特定的摄像机位置,地块之间的接缝可以被看出来,我认为这是由于摄像机处于半像素位置时渲染不准确造成的。
为了解决这个问题,我就得在每个地块周围挤出1个像素大小,这样,我的tilesheet就不再是2的幂了,这让我感到不甚恐惧。
后面我得看下会有多少玩家会碰到这个接缝问题,要是多的话,我就得返工一大堆的tilesheets。
(为啥不考虑修改摄像机的移动,不要让它落在你半像素位置上呢?)
输入
需要讨论这个吗?unity的输入管理系统实在是太糟糕了,无法支持游戏中自定义输入控制。我有些用户报告:一个说想在键盘上玩,还有一个想用游戏杆,为此我得补写一些很讨巧的补丁来处理这些需求,我觉得还是等新的Unity输入系统出来后再说吧。目前,我就简单将用户1的键盘输入和用户2的特定键位进行映射来解决这个问题。
优化像素显示
我努力让像素艺术看起来更优美,通过设置摄像机的正交视图大小为(screenheight/ 2 ) / pixelsToUnit,但是在摄像机运动时游戏画面看起来就有点怪怪的(幸运的是,游戏中大部分时间摄像机都是静止的)。(又是摄像机移动的问题…)
我可以用一个渲染材质按照其初始分辨率对整个屏幕进行渲染,然后再放大材质(这这,这是制造像素感吗?)但是这种方法也招致了一堆问题,最后我只好放弃。
UI
Unity的界面系统还是可以的,但就像Unity开发做的其他东西一样,感觉还是个半成品:他们希望建立界面元素根据焦点位置而自动滚动的输入系统。
但使用时却让人赶脚相当的恶心,每次我不小心从一个UI元素上移走鼠标的时候(你不是不用鼠标吗…),游戏手柄也会失去它在UI元素上的焦点。
在启用和禁用界面元素时,被选择的项就会混在一起,结果就是两个看起来都像是被选中了一样。
而且我无法将一些文本卷曲显示,导致游戏中至今还有大量的文本混做一团。
游戏中唯一使用第三方资源是用于本地化的,名字叫做 LeanLocalisation,做的很棒,我写了一个扩展可以方便的从导出的excel工作表中加载数据,这插件着实让我省了不少事。
状态机
Unity的状态机还是很棒的,其中有大量的控制可以设置动画播放的条件。
查看状态机,对游戏特性进行测试或裁剪还是挺有趣的,我删除了冲刺时进行攻击的技能,因为它看起来不正确,也就是状态间红色箭头的部分。
除了动画外的状态机的其他问题
Unity声称,状态机可以支持除了动画以外的别的工作实现。但我并不认同,动画控制器与游戏物体完全独立的,因此无法仅仅引用游戏物体上的任何属性或者组件,幸运的是,我使用了自建的消息系统,实现了这项功能。
在进入dashing状态时,俺的系统可以禁用movement组件,在退出时再次启用movement组件。相当方便。
我还是认为如果能直接引用Dash组件是最好的,但是,好吧。
General tips and tricks
一些小窍门和技巧
我想凭以上的内容足以给Unity打分了。
我学到的就是:用它,你得包容它的怪异之处,并努力按照它的工作流程工作(有时很奇怪)。
缓存常用组件
因为是获取器,使用gameobject.transform相当占用CPU时间。最好为其建立缓存,这对大部分你经常要访问的组件来说是很有必要的。
Animator.SetBool/SetInt 等函数会消耗大量CPU时间
不是每帧都会调用这些函数的,而出于一些原因,Unity也不会检查其中的值是否已经修改,因此调用这些函数前得自己检查下。
用单件模式,不要用静态类*
原因嘛,Unity倾向于将事物与游戏物体关联起来,以方便其进行监听
GameObject projectile =ResourcePool.instance.Fetch( projectilePrefab ); public void Pool( GameObject obj, int maxSize = 20 ) { if ( !pool.ContainsKey( obj.name ) ) { pool.Add( obj.name, newList } if ( pool[ obj.name ].Count > 0&& maxSize != -1 && pool[ obj.name ].Count >= maxSize ) { //Debug.Log( "can't fit "+ obj.name + " in pool, destroying " ); Destroy( obj ); return ; } if ( pool[ obj.name ].Contains( obj ) ) return ; obj.SetActive( false ); #if UNITY_EDITOR obj.transform.SetParent(gameObject.transform ); #endif } public GameObject Fetch( GameObject ob ) { if ( pool.ContainsKey( ob.name ) == false || pool[ ob.name ].Count < 1 ) { GameObject clone =GameObject.Instantiate( ob ) as GameObject; clone.name = ob.name; return clone; } else { GameObject obj = pool[ ob.name ][pool[ ob.name ].Count - 1 ]; int instanceID =obj.GetInstanceID(); pool[ ob.name ].Remove( obj ); obj.SetActive( true ); #if UNITY_EDITOR obj.transform.SetParent( null ); #endif return obj; } return null ; } |
创建自己的定制编辑器
通过建立tilegrid编辑器为我节省了不少时间,看起来编写编辑器工作量不小,但之后它也会为你节省大量的时间。
为啥下一个项目我还是坚持使用Unity?(这个问题问的好啊)
尽管我洋洋洒洒写了10多页,痛陈Unity的不是,但是它仍然是最好的项目实现工具,并支持一大堆不同平台,缩短开发周期对我来说十分重要,毕竟我不能将一整天时间都花在电脑前。
此外,如果可以的话,我也想在下一个项目尝试下别的平台(特别是Monogame).当然,只要乐意,我还可以重回Unity的怀抱。(你个朝三暮四的汉子 ;))
像素(编程员)艺术
我不是艺术设计师,但是我自个为Spiritsphere完成了所有的艺术创作,我想对那些雇不起或者不想雇艺设的程序员同僚们提点建议:
我能给的最好的建议就是一步一步来:开始别尝试太高的分辨率,从8x8的像素精灵开始,慢慢提高。
当然,有几百万的颜色需要你调配,这蛮吓人的,使用调色板吧,我自己用的是NES色板,此外每个精灵最多限用3种颜色(很少有例外)。
简单也能上档次!
营销
很幸运,有人愿意帮我做市场推广,这使得我在发布前两周还可以放松心情关注些别的东东。不过游戏刚刚发布,根据至今我对发行商的了解,也提出一些小的建议:
对于Key,别太慷慨
即使是对发行商,也别太大方,我散了一些Key给评测人员,但是给出的测评和试用都太差了,抹黑了俺的游戏,他们对游戏报以肯定的态度,但是他们制作的测评内容却相当差劲,所以别太不贪婪(注意不字哦,很多程序员都不会谈价,所以我就这么翻了),花点时间去了解下那些申请测试key的人。
别害怕使用掣肘
SpiritSphere是一个本地的多人游戏,即使这在发布的第一句话就予以强调,但是发行商还是贴出了大量单用户的测评和视频,让玩家对游戏产生了不正确的理解,在完整版发布时,我也会对注意好对多玩家支持的宣传。
总结
长话短说,Unity对快速实现游戏是非常棒的,但也别指望能随心所欲的使用它。