“意外”的乐趣:随机化在游戏中的价值
![Hero image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Fb4244389c0f3139092a19657b2aadf7c5420c07b-1230x410.jpg&w=3840&q=75)
来学学怎样为游戏添加随机元素,让玩家保持兴趣与期待。参与编写了《The Unity Game Designer Playbook》的Christo Nobbs将在这个游戏系统设计的博文系列中讲解更多的拓展知识,本篇为系列第二篇。请在我们的电子书中了解更多关于制作原型、打造并测试游戏的知识。
在上一期博客中,Christo简单地介绍了怎样借助多个相互作用的游戏系统来产生有趣且不可预测的玩法。而在这篇博文中,他将以例子深入说明怎样添加随机元素。
![Image with light purple background with the GamePlay & Design Ebook cover](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Fe7a3427474c05b24a7d7358c959e5881ed024953-1200x675.jpg&w=3840&q=75)
你可以借助显眼的图像元素来吸引玩家进一步探索游戏系统,并催生独特的游戏体验。上一篇Systems That Create Ecosystems: Emergent Game Design着重介绍了一个由大量树木组成的沙盒世界,这些木头在特定条件下可能会着火并让火势蔓延。我们在此基础上再给玩家一把斧子,让他们可以砍树。
假设玩家所在之处完全平整,那他们将难以预测树倒下的方向。如果有些树是“死树”,那种死了但还矗立着的树呢?这些枯木说不定什么时候就会倒下。这种不确定元素可以为游戏环境带来刺激感和紧张感。
你可以用画面来告诉玩家这些倒下的树十分危险,让他们保持警觉。玩家则能通过图像细节来分辨危险的枯木和健康的普通树木,然后自行权衡利弊:如果直接从倒地的树木上采集柴火,从而避免被这些“死树”伤到会不会更明智点?
作为一名设计师,我们一定要罗列出这些可能的连锁反应,再围绕着它设计、保证它能顺利发生,比如树木向随机方向倒下、偶尔着火这些元素可以产生混乱但有趣的状况。
Unity的Random脚本类是一种生成随机数据的静态类。它与.NET框架下的System.Random有着同样的名称与相似的功能,但两者之间有着几种关键的不同——比如前者要比System.Random快20%-40%。
Random类所包含的静态属性和方法如下:
静态属性
- insideUnitCircle:返回圆半径1.0以内或边缘上的某一点(只读)
- insideUnitSphere:返回球半径1.0以内或边缘上的某一点(只读)
- onUnitSphere:返回球半径为1.0的球面上的某一点(只读)
- Rotation:返回一个随机旋转角(只读)
- rotationUniform:返回一个均匀分布的随机旋转角(只读)
- state:取得或设定随机数字生成器的内部状态
- value:返回一个(包括)0.0到1.0之间的随机浮点数(只读)
静态方法
- ColorHSV:从HSV和alpha数值范围中生成一种随机色彩
- InitState:以一串种子值初始化随机数字生成器
- Range:返回一个(包括)[minInclusive..maxInclusive]之间的随机浮点数
我们在上一篇博文里探讨了“操纵杆”(调试键)的作用,以及如何使用ScriptableObjects来储存调试数值。你可以用Unity的Random.Range设定一个数值范围,用随机生成的浮点数来取代固定的调试数值。此时,包括最大最小值在内的任意浮点数会在每1千万个随机样本中出现一次。
你可以使用这种方法从范围中抽取出一个用于游戏的值。只要多实验几次,你就能找到最适合游戏的数值范围,然后再围绕着这个范围进行设计。
![Image of a white robot watching tree branch fires in a forest](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Ffa1ec8da73916a9e357cad89a52637e1c95c9178-1020x574.jpg&w=3840&q=75)
随机元素让游戏更吸引人。打个比方,假设树有着100点的固定血量,而玩家每次挥砍斧子可造成25点伤害。那砍树这件事很快会变得可预测且无聊。即便树木的血量在76到100点间浮动,每棵树仍是砍四下就倒。不过若血量设在了75到76之间,则游戏玩法会非常不同,这时每棵树需要被砍三到四下才会倒。
另一种给砍树添加趣味的方法是隐藏血条,用视觉线索来暗示血量变化。这样一来,玩家将从游戏过程中逐渐学习到砍倒一棵树要多久。视觉线索可以增添有限的不确定性,这些不确定性可根据游戏玩法进行平衡和调整。而放弃固定数值、善用Random类可让原本单调的任务变成一种有趣的体验。
要想更进一步,你可以让每次挥砍减少15到25点的伤害。这样一来玩家将很难准确判断砍倒一棵树要多久。他们只得依靠视觉线索来揣测这棵树何时会被砍倒;这些线索可以是砍飞的碎块大小、树桩的切口大小、掉落的树枝多少、树干断裂的音效等等。
玩家不能准确预测树木倒下的时机,但砍得越多,玩家就能猜得越准,并最终提高自己的生存几率。
![Screenshot of Random Hit Damage settings in the Inspector](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2F01417e5050cabf8f20d67b4485c80a66d5cba228-900x278.png&w=3840&q=75)
随机的目的是给玩家带来意料之外的挑战,促使他们权衡风险、争取最好的结果。
接下来我们再来看看几个使用Random类的例子。
假设某款卡牌游戏里的AI对手只会针对玩家的操作而出牌。如果没有随机性,对手每次出牌都会是固定的,因而也变得容易预测。
即便变成了二选一,这样的随机还是太简单,玩家会很快总结出一套规律。因此,我们可以试着在AI的每一步反击上增添一层随机性。如此一来,AI不会仅仅只从卡池里选两张牌之一打出,或一直优先打出某张牌而放弃其他牌,让整个系统更加复杂、游戏更具动态。
你可以设定几种难度,让不同难度下的对手优先出特定几张牌;比方说,根据预设的值或攻击前的手牌来预测出需要出的牌。如果有一张王牌和其他强力卡牌搭配时可以造成数倍伤害,那对手就可以等到手牌中凑齐了这些卡牌后再打出,给玩家增加难度。你可以通过提高或减少某张王牌与其他强力卡牌一起打出的可能性来给这种出牌组合设置“权重”。
![Image of Heartstone card game](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Febd35b5c479364e40e3f6b564aa67640bbde60a8-1759x894.png&w=3840&q=75)
随机性能以不同的形式出现在游戏中。比如,柏林噪声(Perlin noise)就能根据一串种子值来生成自然的递进噪声。它可以在Cinemachine中产生更加自然的第三人称跟随镜头。
![Screenshot of the Perlin noise algorithm in Cinemachine](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2F558f570390cfc8a3d8b67b3be8374d460c097e81-786x377.png&w=3840&q=75)
要想尝试Perlin noise,可以在资源商店上打开Starter Assets - Third-person Character Controller或Gaia资源包,或参考Mathf.PerlinNoise文档。
![Image of the Perlin noise sampled texture (blurred black and white spots)](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2F0eef6e566cc66d5f6aaef86b0c48c3895c3b8c1a-270x270.png&w=3840&q=75)
在一次采访中,《光环2》的首席工程师Chris Butcher在讨论游戏的AI时表示:“我们的目标并不是创造出那种不可预测的东西。这里的人工智能必须有高度的连贯性,可以识别特定的玩家输入。让玩家在做出某些动作后可以预测到AI的反应。“
![Image of a green robot standing in the middle of a lowpoly ground outside of cubed buildings](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Fde0d8279d41477039d06dbc0c4bf78acef74848b-1929x1123.png&w=3840&q=75)
在这个前提下,我们该怎样建立AI代理同时保持游戏的不确定性和活力呢?
一种方法是用资源商店的Starter Assets和AI工具进行实验,比如能让AI代理向某个位置移动的A* Pathfinding Project Pro。
当AI代理走向玩家时,玩家觉得自己马上会遭到攻击。但如果它走过来只是为了对话呢?如果我们想要加入更多四处走动、融入环境的NPC,让周围的环境更有活力呢?这些NPC应该线性地收到可以前往的地点,或者根据一套规则并用Random类来选取符合逻辑的地点。
假设有一个AI代理能用一张较弱的弓向玩家射箭。不幸的是,代理必须站在一定的距离内才能射击,因为它的最大射程是十米。当AI站到了玩家面前10米处时,便会开火。这样的安排算不上理想,尤其是有第二个弓箭手也想在NavMesh上争夺同一个位置时。
要让这个情景更为有趣,我们可以划定一块区域让敌人来靠近玩家。我们能用Random.insideUnitCircle来做到这点,将输出的vector2传入到vector3空间坐标的X和Z轴上,然后利用RandomRange在玩家周围划出一块最小与最大值之间的圆。
![Screenshot of script used for an AI agent selecting an attack point near the main character](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2F5e93208f3958290fa690159134d0c873ebe517c3-945x1036.png&w=3840&q=75)
![screenshot of design levers in the Inspector](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2Fd58068bf7a3a60e5d35d77c501484c4020019fbb-645x378.png&w=3840&q=75)
AI代理应该在玩家附近选取一个位置再射击,而不是过于靠近玩家。为了用最少的代码带来更多的刺激,所有AI代理都能应用这个脚本,并从多个角度攻击玩家。AI的行为在一定程度上是可以预判的,代理会在一个固定的距离展开攻击,但你并不能预测攻击的角度。
![image of a lowpoly space with a white crosshair over a grey and green circle](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffuvbjjlp%2Fproduction%2F7e7ef1cd1c3e377f9dd2104f562cca635421e134-1519x1294.png&w=3840&q=75)
你也可以在此基础上为代理添加近战攻击的能力。用同样的方法在玩家的近身距离内随机取得地点。让代理能在适当的时机选择自己的攻击方式。
玩家知道自己有可能会遭受近战攻击,但从哪个方向来呢?会不会有举过头顶的高位攻击?或者从左到右的横扫攻击?玩家只能通过观察动画上的文字来确定即将来临的攻击。
这时如果有多个敌对NPC意图攻击玩家,则整个状况会立即变得复杂。不过在育碧的《Far Cry 2》里,玩家并不会同时遭受所有敌人的进攻。不同的敌人有着各自的攻击时机与方式。这段来自Game Maker Toolkit的视频很好地解释了以上例子以及其他AI应用场景。
如果你希望在游戏中真实地还原现实动作所产生的结果,你就必须应用复杂、多方面的等式或一个训练好的ML代理。最终,这种效果很可能仍然无法契合游戏,并且实行的耗时长、技术要求高,且如果架构得差,要理解、平衡或控制起来会非常困难。
不过在Unity Random类的帮助下,游戏设计师们也可以为场景创造逼真的效果、控制游戏的刺激程度,相比于其他方法,这样也能节省不少时间。当然,随机性不一定处处适用。确定性仍然十分重要。但只要合适的地方、合适的时机善加利用,你就能给玩家带去非常独特的游戏体验,让玩家一次又一次地游玩。