嗟星症问卷RSQ-6|开发小结|BoooMGameJam2023UnbalancedDice


3楼猫 发布时间:2023-10-28 10:32:25 作者:_不行_ Language

前置条件

Jam结束后的第一个周二中午去喝酒,古巴程序员大哥同事问我:“最近你还有参加什么游戏比赛嘛?”
我说暂时没有了,“上次jam我已经把所有学会的本事都用上了!”
所以,这次Jam其实是一个练兵场,最核心的目的是把之前学会的技能用一用。
今年的第一个BooomGameJam我做了一个推箱子解谜小游戏,做完之后把源码发给狮子看过,然后被狠狠教育了哈哈,感受到了野路子编程的局限。这次Jam我主要是想试一试用状态机写角色控制器,试一试3D游戏,再试一试使用对话管理器写文本。
那现在我十分骄傲地宣布,三个目标都完美完成!虽然因为国庆假期跑出去玩导致最后没时间写文本,但是总算在最后一刻打包上传了一个完整的没有bug的游戏,了不起!
这次开发也同样受到了很多网络邻居的帮助,每次在搜索引擎中键入关键字的时候我都在感叹生活在这个时代真好,互联网上有这么多资源和好心人。希望全球各地各个方面的收紧可以再慢点到来。我接下来会尝试拆分这次开发所用到的一些模块,给出一点我自己的粗浅理解和参考来源,希望能给游戏开发新新手们一点指路作用。

使用到的主要模块

1. 有限状态机(Finite State Machine)

其实我分不太清楚有限还是无限还是其他类型的状态机的异同,总之都是状态机。
我自己在反复对比学习了很多教程以后,发现状态机是对于我来说最自然的一种编程方式(似乎大名鼎鼎的Unity第三方插件playmaker就是使用状态机的思路)。
对比其他原始的Unity教程中用到的把所有功能写到一个update里,然后用很多if去控制执行哪一部分代码来说,使用状态机相当于是把这些用来控制的if代码块拆分成一个个其他的脚本。这样的好处是单个脚本不会十分臃肿,在代码量增加的时候也更加容易维护。对于我个人来说,我觉得最好的一点是,使用状态机的编程方式会强迫我先思考一个物体有哪些状态,提前计划好才进行程序的编写,使我摆脱了脱离网络教程就不会编程的情况。(网络上的新手教程特别容易让人产生依赖性,刚刚开始学习的时候,经常会看着教程跟着来觉得一切OK,到自己上手写新功能的时候就抓瞎了。)
写这个部分代码的时候,我主要参考了两个人的视频:
主要的一位是油管上的iHeartGameDev,他有许多和状态机相关的视频教程,做得非常用心,通俗易懂;
另一位是Bilibili的up主M_Studio,当时看完上面大哥的视频以后,总觉得还需要再看看别人是怎么做的,碰巧麦扣老师就上传了关于状态机的新视频,非常幸运。
在反复参考两位的状态机写法之后,活用搜索引擎恶补其中遇到的零碎知识,凑出来了我自己的有限状态机。我觉得对于我来说,写状态机的最大难点在于代码新手看到这种陌生的代码结构和里面用到的陌生词语就十分害怕,经常看一半就会打退堂鼓(以前我也尝试学习过,但是放弃了)。这次能坚持下来,还是多亏了上次jam完成了从0到1的突破,真的独立做完了一个小游戏,给了我很大的勇气和信心。
我自己总结的状态机的样貌是由如下三个部分组成的:
  • StateManager(状态机管理器):
该管理器除了负责切换状态外,最重要的是给其他States提供Context。换句话说就是,当切换到某个具体状态后,在该状态下这个物体需要的参数都应当由该StateManager提供,这样就能保证在写抽象的State时代码中只包含和State相关的内容而不用操心获取具体的信息参数。
左侧:状态机管理器中包含的所有状态的变量;右侧:状态机收集参数,并且控制状态的切换。

左侧:状态机管理器中包含的所有状态的变量;右侧:状态机收集参数,并且控制状态的切换。

  • BaseState(抽象的基础状态)
该状态包含所有可能存在的实际状态,但是该状态不是一个实际会被用到的状态。打个比方,这个抽象的状态就像是一个模板,它规定了其余的状态大致的样子,剩下的实际状态一定会遵循这个模板,在模板允许的范围内,每个实际状态可以有自己的特例。
除了规定一共有哪些状态外,抽象的基础状态还可以包含会被其他具体状态频繁使用的方法,这样其他的具体状态通过继承抽象基础状态就可以直接使用。
抽象状态如果和状态机管理器解耦的足够好,那么此时应当只需要传递给每个状态一个参数,这个参数就是场景中存在的对应管理器。
只需要一个PlayerStateManager作为各个状态的参数。

只需要一个PlayerStateManager作为各个状态的参数。

  • State A, State B, State C, etc.(具象的特例状态)
具体状态其实就是在不使用状态机来写东西的时候,包含在各个if判断里的代码内容。唯一的不同是要注意多写一个判断何时转换状态的代码。
除了BaseState和Manager外,还包含了Idle,jump和walk三个状态。

除了BaseState和Manager外,还包含了Idle,jump和walk三个状态。

除此之外,你可能还需要直到什么是继承。一个题外话,我也是到此时才明白每个新建的脚本都会有的XXX:MonoBehaviour到底是啥意思。

2. 3D角色控制器

这个部分纯粹是因为个人爱好,我想试一试开发3D游戏(梦想是做Arkane那样的浸入式模拟!但是最好能再轻度一点,更有好一点!),那么自然先从角色控制器开始。
这里我主要参考了这些内容:
首先,也是最重要的,是Toyful Games,他们除了在自己的官方网站上更新DevLog之外还在油管上有视频Devlog分享。我一直想试试用rigidbody写一个角色控制器,这次我写的角色控制器思路就是来自于他们的分享,即用一个悬浮的胶囊体碰撞体作为角色本身。他们详细在开发日志中分享了为什么要用悬浮胶囊体,以及如何实现。
简单来说,他们的思路是在角色的悬浮碰撞体下方安装一个弹簧,使得角色始终和地面保持一定距离。我自己尝试下来觉得这样做最大的优点是不用考虑地面上的小高差变化,也不用像传统的控制器那样写在斜坡上让rigidbody停止不动的特殊代码。同时这样做的视觉表现也更加生动,因为角色从高处落下时会自然的抖动(脚底有弹簧),十分适合卡通风格的美术表现。
悬浮胶囊体。右侧通过改变弹簧参数影响动画表现。

悬浮胶囊体。右侧通过改变弹簧参数影响动画表现。

在学习Toyful Games的方法时,我发现他们的分享更多的是关于原理,很少涉及到代码,对我这个新手来说即使想逆向工程也难度很大。幸运的是我还找到了Joe Binns的油管频道,他的这一期视频就是在学习Toyful Games的方法重现该角色控制器。于是顺藤摸瓜我找到了Github上Joe Binns分享的源代码,也多亏了他,我才能顺利的进行逆向工程和学习,帮助非常之大。这次逆向工程难度正正好,基本每一段代码都有新的知识需要我去搜索或者参考Unity文档进行学习和消化,进度非常之慢,但还好我坚持下来了,曾经像是天数一样的代码慢慢地都变得可以理解了。
理解了代码之后,剩下的内容就十分简单了。我只要把这个巨大的角色控制器脚本拆开来,有序地分散到写好的状态机代码里就可以了。

3. 对话管理器

工作中,项目组有在使用一个对话管理器插件。在处理小体量的对话的时候,完全够用了:它需要在引擎内手动一个一个去按照对话ID配置对话文本和手动做跳转逻辑。虽然它能够在引擎内看到可视化的文本逻辑,但仍然,在处理多分支文本的时候显然力不从心了,包括在内容量增加之后,如何方便策划进行维护修改也是一个大难题。
正巧去年看到了机核默颜老师的这篇文章:游戏分支剧情创作中的挑战和工具里面十分详细的介绍了几个主流的专门用于有大量文本的项目的对话管理器,其中包括 Twine, Yarn Spinner, ink, articy:draft等,我就不在此赘述了。在今年的头几个月里我断断续续的尝试使用了Twine, Yarn Spinner, 还有ink三个工具。其中给我感受最好的是Yarn Spinner,我认为它更像是一个服务于引擎的插件,而不像其他两个工具更像是一个独立的多分支文本写作软件。而且在学习的过程中,我发现Twine有太多版本,实在不是一时半会能消化的;ink虽然写起来很爽,但是响应式的文本语法对于中文文本来说也不方便,而且还没有图形化的分支文本展示;对比之下,Yarn Spinner处在一个恰好的甜点,最重要的是,他们的开发者比较活跃,官方文档和范例也十分清晰好学。那么自然这次Jam我选择使用Yarn Spinner来管理文本内容。
那么,在选定完工具后,自然是改造工具以适应我的游戏设计目的。我不是很喜欢Galgame式的文字展示方式,我认为把文字显示在屏幕下方并且交互一次就播放下一段文字的显示方式,先天上就很难给我一种阅读的体验,更像是在进行对话(可不,这就是Galgame要的!):把长段文字切成短句,然后一句一句地显示给玩家。
在明确方向后,剩下的工程对于我来说仍然是让人畏惧的。在能够大致读懂并且使用 Yarn Spinner的官方文档后,我开始了缓慢的逆向工程。幸运的是,Yarn Spinner的官方示例中正好有我所希望的文本显示样式——一个模拟手机聊天的示例工程。
左侧为YarnSpinner示例工程中的文本显示;右侧为这次Jam的游戏文本显示。

左侧为YarnSpinner示例工程中的文本显示;右侧为这次Jam的游戏文本显示。

我自己对这个示例工程的理解是这样的:每次玩家希望显示下一个对话的时候,在显示对话的区域内(一个竖直的Scroll View区域,内部的元素排列由Vertical Layout Group控制,这些和Unity UI相关的东西也花了很长时间去学习,可以说是处处都是拦路虎了)不断地Instantiate新的对话气泡Prefab,每一个气泡内部的文字就是使用Yarn Spinner编写的文本中的一句Line,直到一个对话中的所有Line被显示完毕,那么停止Instantiate新的气泡。
于是,在反反复复地实验如何使用Unity的这些UI组件后,我总算是凑出来了一个期望的对话展示形式。(题外话,在打包完成之后的一次测试上我发现我的滑动条方向反了……)接下来就是……把故事放进游戏中!
把故事放进游戏这件事情本身的难度,大部分来自于玩家和游戏交互的方式上比较怪异——推动一个骰子而不是点击按钮。所以我简略描述一下游戏中是如何实现继续播放下一段故事这件事情的:
首先,骰子是一个物理骰子,它和玩家间的交互是通过物理碰撞实现的。玩家推动骰子,骰子受力发生旋转,所以骰子的质量、玩家的质量还有玩家运动给出的力是在几次手工调整后确定的——骰子被玩家撞击后不会有太大的平移,而是发生旋转,并且这个力也不会让骰子转的太多(这样骰子始终会有一个面非常稳定地触地)。
然后,我在骰子的六个面中心放置了一个球形的碰撞体,这样当一个碰撞体碰到地面时,就代表玩家推动骰子发生了一次旋转。并且由于骰子相对的两个面上的点数之和为7,那么自然的,当1点数的面触地,代表6向上,即玩家期望骰出的点数是6。以此类推,即可完成其余面的判断逻辑。但是这样还不够,因为玩家期望骰出的点数可能不能够一次转动就得到,那么我的方法是加入一个等待的时间——当一个面触地达到三秒之后才判断这个点数是玩家所期望得到的点数。
接下来,我设置了一个变量用来追踪这是玩家第几次骰出骰子,这样我就能够根据这个变量来判断故事发展的阶段,进而让我的对话系统给出不同阶段的文本。我没有使用Yarn Spinner内置的变量储存器,我也没有时间再去学习怎么自己写一个变量储存器了,于是这次我把变量全部用static的形式在Unity里声明,这样也不用管理引擎和对话管理器之间的变量同步问题了(事实上,Yarn Spinner的官方文档中建议把所有变量只储存在一边,避免出现Unity和 Yarn Spinner中都声明了变量的情况)。
最后就是在Unity里写一些让Yarn Spinner能够获取、改变变量的方法,方便编辑文本的时候可以编辑变量(能够写文本的时候直接使用方法控制Unity里的逻辑实在太方便了!)。至此,算是完成了播放对话的事情。
接下来就是一些交互上的优化:比如在播放对话的时候把骰子的质量调整到1000kg防止玩家在播放对话的时候转动了骰子。其实这里也可以做别的对话,但是我程序能力有限,实在是没有精力去研究怎么使用Yarn Spinner加速、终止对话的过程中还不出奇怪bug了,所以干脆不让骰子动。再比如给玩家和骰子的地面做成一个圆形,并且让这个圆形地面跟随玩家和骰子,避免骰子被推出边界(这里也能做别的有趣的事情,但是……没时间了!)。

其他模块:

其他用到的模块我都只是粗略跟着教程跑了一遍,然后自己做出来一个能用的版本。所以我就不展开多说了,实在是说不出来啥,我会把教程链接一起写上来:
  • New Input System:
How to use NEW Input System Package! (Unity Tutorial - Keyboard, Mouse, Touch, Gamepad) - by Code Monkey
NEW INPUT SYSTEM in Unity - by Brackeys
Character Movement in Unity 3D | New Input System + Root Motion Explained - by iHeartGameDev
  • 像素化渲染
Make a PIXELATED CAMERA in Unity (A Short Hike case study) - by whateep (通俗易懂,非常好教程,爱来自瓷器)
How to Get a Pixelated Look | Unity Tutorial - by Thomas Friday
  • 描边shader
How to make outline (Unity URP) - by Interdimensional Play (差不多的思路我在Blender里也做过,所以这次连连看起来没啥障碍,很轻松)
  • Cinemachine
THIRD PERSON MOVEMENT in Unity - by Brackeys (这个教程中主要是说如何做第三人称角色控制器,但是也提到了如何设置第三人称Cinemachine)

做这个怪游戏的原因

拿到题目以后我觉得很矛盾的地方是:一个有意义的骰子,本来代表的就是公平;而如果骰子不公平,那就失去了作为骰子的意义。而且前段时间LD Jam好像题目就是骰子?去年GMTK Jam的题目也是和骰子有关,我都去玩了玩大家提交的游戏,难免这次BoooM Jam逆反了!就是不想好好做个游戏。这导致很长一段时间我都不知道做啥,感谢公司的同事也帮忙想了很多点子,但是我觉得都太“游戏”了——这些点子难免会和数值设计/概率扯上关系,或者是之前Jam中出现过的玩法。如前文所说,主题是关于不公平的骰子(一个矛盾的命题),那玩法中又让玩家去影响概率制造不公平(太……顺着题目了?没有那个矛盾的感觉),总觉得缺点意思,不得劲!
正巧那段时间看了默颜老师的文章:浅谈电子游戏中“选择”的设计,还有身患威廉吉布森见登美彥症候群老师的文章:杂谈丨交互性消失的地方。我想到极乐迪斯科里找的那只隐形的虫——直到玩家玩到这个地方之前,奇怪动物学家夫妇俩都没能找到的虫——突然就出现在了玩家的屏幕上。我一直觉得这是很讽刺的事情,投骰子本身也似乎变成一种无意义行为——我记得极乐迪斯科的主创说过,他们希望无论玩家做了什么,骰出怎样的结果,是大成功还是大失败,玩家都能得到丰富的体验——于我而言,这是一种极致地控制下的不随机。但即使如此,得知真相后我仍然会再玩下去,再骰出一个骰子,再做一次选择。似乎人的感受是如此容易被糊弄,这样的精致的设计数量足够后,也让人觉得这是丰富多彩的世界了。
一切好像都是命中注定,那我就做一个关于不接受命运安排的故事吧,一个关于后悔的故事。
游戏的文本没什么实际含义,我尝试写了一些小故事,各种风格都有。然后我尝试把几个小故事中间的意向变成其他故事中也出现的符号,看看会有怎样的效果。非常恶趣味的是,我这么安排是想看看有没有我最讨厌的互联网懂哥来分析这些故事碎片是怎么样的关系。我多虑了,根本没多少人玩!

结语

有一段时间我觉得很厉害的人是吹哥,最近我的榜样变成了Lucas Pope。他在互联网上的Devlog分享实在是让人受益匪浅。感谢每一个愿意在网络世界分享知识的伟大的人!我很喜欢看开发故事和开发者的DevLog,有的时候我不期望能学到什么新知识,单纯的是我想看到不同的开发者们是如何开发,看他们描述开发中的困难,看他们分享开发中的喜悦。这些分享各不相同,但是他们的相同点是:他们告诉我创作是困难的。而我从他们的分享中得到了 决心(determination)和勇气。
如果你看到了这里,希望我的分享能帮助到你。也能带给你决心和勇气!

© 2022 3楼猫 下载APP 站点地图 广告合作:asmrly666@gmail.com