在昆明工作的日子,我虽对经理忽悠的作风极为不屑,但对他整天挂在嘴边的那些软件工程思想还是很尊崇的。无论是传统的瀑布流,还是敏捷开发,无论是大的系统,还是小的功能,先分析需求、再构思设计、再实现的流程是永恒真理。
事情还没弄清楚就开始动手做,是初学者的幼稚行径,越成熟的设计师,越是重视前期的需求分析,思路清晰后,编码也只是“唰”一下的事情了。
习惯总得从无到有养成。所以这次我刻意走一个完整的流程,即便不是那么适应,也要慢慢学会。主要就是在编码之前,先用Visio画UML图。
由于游戏工程的特殊性,大部分用于企业应用、网站设计、数据库设计的UML图并派不上用场,但描述一个软件工程的架构总是脱不开一款绘图软件,想法是不要求完全规范,仅仅是借用那些形状,也是一个路子。哪怕是最原始的用纯文字一条条描述需求的手段,也是聊胜于无。
并不是所有的游戏在被制作之前都有一个模仿对象,假如我是一个主策划,只会说“我们做一个dota传奇那样的”或是“我们做一个皇室战争那样的”,明显是没前途。迟早有一天我会面临原创,所以这里先简单交待一下《Besiege》是个怎样的游戏。
这是一个单机物理类沙盒游戏,玩家需要用游戏提供的各种零件,组装出自己的一台攻城器械,完成每一关卡的任务,任务基本都是摧毁某个建筑之类的了。它的玩点在于逼真的物理效果,以及可以无限自由组合的脑洞大开,你好不容易组装出可以通关的机械,抬头一看网上别的玩家做出的东西的时候,大概就会产生“我们玩的是同一个游戏吗?”的想法。由此奠定了此款游戏的神作地位。
然后我来山寨。
假如是正式的商业项目,肯定要先进行充分的可玩性探讨,但我的目的是练习技术,所以可能本来这个项目不太适合移植到手机上,比如有很多操作设计上的硬伤,我都忽略。
思路是尽量简化,抛弃关卡设计,直接进入主场景,可以组装,可以运行,就够了。
我是有把它做成联机版的想法的,考虑到自己的服务端经验尚浅,第一阶段的架构暂不考虑联机,先做出单机版,再研究如何改造成多人对战。
所以核心功能大概是这样:
组装阶段:
1.按各模块对应按钮,出现模块提示图标
2.拖动过程中图标随手指移动
3.触点进入已有模块可连结范围时,判断新模块是否可连结已有模块。若可以,出现新模块提示,此时若松手,则在结点创建新模块并连结,若不松手拖出范围外,新模块提示消失。
运行阶段:
1.激活所有模块
2.按控制按钮触发相应功能
注意,游戏和桌面应用软件的主循环结构,虽然本质上一样,但表现上还是有个主要的差别。
就是后者一般是消息驱动,比如鼠标点一下,就干什么,键盘按了下,再干什么,而无输入时,基本是个空循环啥都不干。
而游戏是个忙到死的程序,就算没有任何输入,它还是疯狂的往屏幕上渲染画面,基本只恨cpu不够用,鲜有说让cpu歇会的。
有没有特例?少数确实对性能要求不高的游戏可不可以利用这点做一些省电的优化?乃至于引擎限帧后是否已经帮我们做了类似优化?我都不敢乱说。
反正想起三年前刚学做游戏的时候为降cpu占用率主动在线程里加Sleep(),都只给三个字“拿衣服”。
然而,现在这个游戏,倒真是像极了应用软件,至少在组装模式下,除了响应各种操作外,没什么其他的事要做。所以,架构上,它自然也退化到消息驱动程序的样子。
主循环实际上隐藏在EasyTouch的Update()中,我们自己的代码只要给绑定一些On_TouchStart、On_TouchDown、On_TouchUp就好了,Update()中反而空空如也。代码外观上像极了我之前做的看图软件。
贴部分核心代码:
public void ChangeMode() //响应“运行/返回”按钮的方法 { switch (gameState) { case GameState.Assembling: ChangeToRunningState(); break; case GameState.Running: ChangeToAssemblingState(); break; default: break; } } private void ChangeToRunningState() { gameState = GameState.Running; //unbind ET events of Assembling state EasyTouch.On_TouchStart -= On_TouchStartAssembling; EasyTouch.On_TouchDown -= On_TouchDownAssembling; EasyTouch.On_TouchUp -= On_TouchUpAssembling; //create a new copy of machine for running, hide the old one runTimeMachine = Instantiate(machine); machine.SetActive(false); //activate all modules in machine Rigidbody[] rigs = runTimeMachine.GetComponentsInChildren<Rigidbody>(); foreach (Rigidbody r in rigs) { r.useGravity = true; r.isKinematic = false; } //bind ET events of Running state } private void ChangeToAssemblingState() { gameState = GameState.Assembling; //unbind ET events of Running state //destroy runTimeMachine, show the one for assembling Destroy(runTimeMachine); machine.SetActive(true); //unbind ET events of Assembling state EasyTouch.On_TouchStart += On_TouchStartAssembling; EasyTouch.On_TouchDown += On_TouchDownAssembling; EasyTouch.On_TouchUp += On_TouchUpAssembling; }
现在RunningMode的响应方法我还没添加,激活零件的方法也只针对小木块这一特殊类型,后面会改成调用不同类型零件的相应方法。
然后我又试着画了画各模块的类图:
这个设计以后肯定是会改的,可能做着做着发现某个方法不需要,而另外一些方法要加进来。该不该这么设计本身也有待商榷,比如动力轮是不是该从从动轮继承而来之类的,我们不要在意这些,错的预设计总是胜过不预设计。
另外由此图我看出来一些端倪,比如由于引擎所有的GameObject都自带Transform组件,而Unity提供的Rigidbody组件又已经包含了质量这一参数,实际上一些简单的类已经不需要自己设计了,只要包含基本的组件即可。另外像轮子、铰链这些类,可能引入Unity自带的一些相关组件后,也能省掉很多方法和成员。
摄像机控制模块相对比较独立,大体需求是这样:
a.组装阶段
1.始终以机器质心为目标点
2.滑动屏幕时摄像机围绕目标转动
3.双指拉伸调节摄像机距离目标远近
b.运行阶段
1.维持目标后上方跟随视角
由于我工作中搞摄像机控制搞得很多,所以对这块完全放心。
我对Unity物理引擎的轮子、铰链都不是太熟,接下来想做一些自己称之为“核心技术攻关”的工作,就是正式做项目之前,先对自己不太了解或者没什么把握的部分做一些试验。
已有的百来行代码已经把组装阶段的功能实现了个七七八八。
再直接做个可以控制开动、转向的小车,应该就能把运行阶段的主要工作都涵盖,再然后就是代码的整合工作了。
最后再说下这几天对UI代码这块的一些思考。
主要就是像UI按钮涉及的一些功能到底是分散在绑定在各UI元素上的脚本好,还是集中在某个主控类中好?
一开始我倾向于后者,这样代码不至于太过零碎,更易读,但后来再想一想,其实把功能代码就绑在它相关的触发源头也没那么难理解,反而更直观。
实际工作中再体会,见仁见智吧。