我觉得我大学四年一直都陷在一个怪圈里:一直在学习东西,但总觉得还学得不够火候以实操起来,又因为没有用起来而学完就忘,最终导致又觉得自己火候不够。最近认识到这个问题之后,就趁着最近毕设做完,还没回公司,抓紧把学的 Unity 用起来,做点小游戏练练手。经过一些调查,我选择像素级模仿 2048 原作做一款练手的游戏。
你可以点击这个链接来在线体验这个简单的小游戏,四个方向键可以控制方块的移动——由于需要使用键盘,请用电脑端打开。由于开启压缩后会有 Bug,文件较大,请耐心等候载入~
由于我水平还很差,该项目估计无法为各位带来什么阅读价值,所以发出这篇博客更多是出于进行自我记录以及总结。即便如此,我还是将项目的源码放在了 GitHub,如果真有人感兴趣(包括以后的自己),可供参考。
以下是我从这个小项目中收获的知识。
GameObject & Component
这是 Unity 里非常重要的两个类。其中,GameObject 代表着或具象或抽象的每一个游戏对象。如在本项目中,“2048”标题是一个 GameObject,“棋盘”是一个 GameObject,每个方块也是一个 GameObject。GameObject 不一定是在游戏中可视的对象,有的时候会用空的 GameObject 来挂载一些独立的脚本。
Component 则是 GameObject 里的“模组”。每个 GameObject 可以包含多个、各种种类的 Component。典型的 Component 有如:Rigidbody,用以参与物理规则(重力、施加力的作用等);Collider2D,用以参与碰撞检测。我们写的每个继承了 MonoBehaviour 的 C# 类也可以作为 Component 使用,挂载到 GameObject 上。
生命周期函数
Unity 有一套生命周期函数,如 Awake、Start、Update 等函数,会在适当的时候由 Unity 进行调用。Unity 官方提供了一个页面,详细阐述了各个函数的调用时机。这里仅总结一些比较常见的生命周期函数执行顺序:
- Awake:在类被实例化的时候就被立即调用;
- Start:在第一帧之前被执行,可能比 Awake 要迟得多;
- FixedUpdate:以固定的速率(默认每秒执行 50 次)独立于 Update 被调用。仅适合用于计算物理系统。
- yield WaitForFixedUpdate
- Update:每帧调用一次,用以更新游戏状态;
- LateUpdate
- OnGUI:用以绘制 GUI;
- yield WaitForEndOfFrame:该帧的工作结束时调用,似乎可以用于一些后处理效果;
- OnDisable
- OnDestroy
Prefab 预制体
可以视为 GameObject 模板,我们可以在编辑器中或在运行时将 Prefab “实例化”成为 GameObject。其中,运行时使用的是 Instantiate 函数。
在更新 Prefab 之后,所有基于该 Prefab 创建的 GameObject 都会自动得到更新。很智能地,从 Prefab 创建得到 GameObject 之后发生了变更的属性,不会被重新覆盖。
当一个场景中存在很多相似物体时,为其提供一个 Prefab 是一个不错的选择,典型例子如“子弹”。
代码与界面的有机融合
这是 Unity 的世界里,最让我惊讶的一个点:项目代码与 Unity 编辑器似乎完成了深度绑定,成为了一个整体。目前我发现了两个体现这种设计的地方:一是挂载为 Component 的 C# 类的公共对象,可以在 Unity 界面中完成值的覆盖、对象的绑定。比如此处,我们在代码中声明的一个 public GameObject tilePrefab
,可以直接在 Unity 编辑器中用拖动的方式进行绑定。二是,我们的代码一旦写了死循环,整个编辑器都会卡死、崩溃。似乎项目代码与编辑器同在一个 C# 虚拟机里运行,看起来像是项目代码被 Unity 动态注入到自己的进程直接运行,并用反射机制来获取类的成员信息。
不过这种设计会导致一个类的成员有两份默认取值。一份是在代码里的,一份是在 Unity 编辑器中绑定的,最终以编辑器的值为准。我觉得这是一个潜在的坑点:如果代码中对成员进行了初始化,则容易变成像“没有被及时更新的老旧注释”一样的坑。
C# 的 getter 与 setter
作为 Python 的忠实拥簇,说实话我更喜欢 C# 的 getter/setter 语法。在项目中,我用在了自动更新方块样式、自动更新“Score”的文本等。其他类里直接跟 int 值打交道,由 setter 进行其他自动更新。
小技巧:列“真值表”
在面对比较复杂的判断逻辑时,可以列一个“真值表”,来理清需要的逻辑。比如这几行代码,在列表之前我调试了无数遍,改完这里那里就又忘改了。在列表之后很快就理清了,写出了正确的代码。
但在事后回顾时,我觉得数据驱动编程会不会是一个更好的解决思路?