目录
序.游戏感的介绍
1.加入细节的动画和音效(涉及知识:音频中间件)
2.增加敌人数量且让敌人更容易被打死(涉及知识:对象池)
3.增加开火特效,加强射击的表现力
4.让子弹出射角度随机变化(涉及知识:随机数)
5.增加击中特效(涉及知识:环境系统、编辑器扩展)
6.让敌人受伤变色(涉及知识:MaterialPropertyBlock)
7.敌人受伤后击退
8.敌人死亡后尸体残留
9.相机平滑跟随(涉及知识:临界阻尼弦模型、缓动函数)
10.相机焦点位置根据玩家方向偏移(涉及知识:双向正前聚焦,Procamera2D)
11.相机震动(涉及知识:相机震动)
12.攻击时的后坐力
13.击中敌人的停顿效果
14.倒退射击
15.开枪时弹出弹壳
16.更多的子弹
17.敌人死亡时随机爆炸
18.更多、更快的敌人
19.加强特效上的表现
20.死亡时的慢动作(涉及知识:时间系统)
Jan Willem Nijman 是《废土之王》的设计师。他在这个视频里Jan Willem Nijman - Vlambeer - "The art of screenshake"分享了他在 《废土之王》用到的用于提升游戏感(GameFeel)的手法 。
什么是游戏感?在这本 《Game Feel: A Game Designer’s Guide to Virtual Sensation》 (中文译名 《游戏感:虚拟感觉的游戏设计师指南》)定义为:在一个模拟空间中对虚拟对象进行实时控制,过程中通过润色来强化其交互效果。 我的理解的话就是玩家在玩游戏时的感受,感觉。其中,强化交互效果的方法可以分为:动画、视觉效果、声音效果、镜头效果和触觉效果。
之所以写这篇文章,是因为这也是凉屋游戏的面试题之一。具体的题目是,三天做出一个射击项目,要求包含视频里提到的所有设计技巧。所以我把这个视频提到的强化游戏感的手法总结一遍,同时梳理自己在之前工作中的开发经验。主要是编程方面的经验,非程序人员阅读可能会有门槛。
![](https://img-blog.csdnimg.cn/20200513003620464.gif#pic_center%29)
回到视频本身。Jan首先展示了一个游戏最原始的样子,有角色移动、跳跃、开枪的基本功能,看上去非常的简陋。接上来他会在不引入新玩法、新系统的情况下,通过一些细节上的改动一步一步提升游戏感。
第一步,Jan 加入了人物跳跃时头发会动的动画和跳跃时的音效。跳跃的头发摆动,感觉是非常细节的东西了,实际上玩家在玩的过程中也不容易察觉。但正是这种细节的堆积,最终决定了游戏的品质。
编程方面,音效可以提一下。Unity本身提供的音效功能非常有限不建议使用。 游戏开发中一般使用音频中间件。没有听说过的可以看这篇文章:什么是音频中间件?
随便举个例子,玩家在攻击时需要随机发出语音。如果不用音频中间件,这个功能是不是得程序员自己写。使用音频中间件后,一行代码都不用写。因为音频中间件在unity提供的接口是执行一个音频事件而不是播一个音频,这个音频事件可以是播一个声音,可以是播多个音频,可以是停止某个音频等等。音频事件要做什么事情完全由音频师或策划决定,程序员在代码里只需要执行这个事件即可,不需要关心这个事件是干嘛的。
目前世界级别的游戏音频中间件一共有三种:Criware,Wwise和Fmod。一般是前面两个用得比较多。我个人的话用过Wwise。这篇文章记录我用Wwsie遇到的一些问题:【音频篇】Unity中使用wwise的常见问题及解决方案。
接下来,Jan增加了敌人的数量,降低了敌人的血量和提升了子弹的射速,让敌人更容易被打死。这一步是降低了玩家输入到游戏给玩家反馈这个循环的时间。我个人很反感一些游戏增加游戏难度的做法就是单纯给怪堆血量堆攻击,这会让打怪这个过程变得很无聊。增加难度应该是用更聪明的AI,或者更有趣的游戏机制。
编程方面要说的话就是子弹记得用对象池。 关于对象池,我总结了一篇文章【功能开发篇】使用对象池的注意事项。
接下来,Jan给武器加上了开火的闪光特效,增加了武器射速,增加了子弹的大小。
说到特效,之前做的项目里特效类是一个很臃肿的类,但是到项目后期,花时间重构又会很影响项目进度。关于这种问题,我也写了一篇文章:【代码篇】如何解决函数参数过多的祖传代码。
这里Jan让子弹出射时有一个随机角度变化,方向不再是直接朝右。这样子的改动让游戏看起来更加有趣。
随机数在游戏里也是使用非常广的。随机数算法有Linear congruential generator(线性同余)、Mersenne Twister(马特赛特旋转演算法)、Xorshift等。 在图形学里面,上面这些算法反而用不太着,因为太随机了不符合自然规律。图形学里面被广泛使用的是各种各样的噪声算法,如Perlin噪声、Value噪声、Sample噪声等。关于噪声算法,冯乐乐曾经分享过一篇博文:【图形学】谈谈噪声。感兴趣的可以看看。
这里加入了击中环境和击中敌人的特效。这个也是常见的需求。不过我之前遇到的需求更加复杂一点。之前做的游戏里的角色和环境引入了材质这个概念,为了和渲染里的材质做区分,叫它 ”子弹击中材质“ 。子弹击中材质有很多种,比如金属、岩石、水、肉体、沙地等等。
策划的要求是:子弹击中不同的材质有不同的特效,并且游戏里的材质种类可以自由增加和删除。 首先想到的是写一个scriptablehuaobject,里面存一个list去存游戏里有几种材质。然后在角色和墙壁地板上挂一个类标明它是哪种材质。
再在子弹上挂一个特效管理类,上面可以配置打到哪种材质会播哪种特效。
这个需求分析下来,实际上没有什么难的地方,我就不挂代码了 (而且这都是公司项目源码) 。像这种需求,它唯一麻烦的地方在于如何让策划配置得更舒服一点。而这种需求策划肯定不会跟你说。比如你可以看到上图的 BulletHitEffectController上的列表是没有添加和删除按钮的,因为我通过写Editor让这个列表里有多少项元素是根据BulletHitMaterials这个scriptableobject上的配置进行自动变化的。这样的话策划就能看到哪些材质对应的特效是没有配置的。
此外,还有一个更进阶的要求:编辑器是否足够美观? 使用特殊的GUIStyle是一个不错的办法。关于GUIStyle的介绍,可以看这个【编辑器扩展篇】使用GUIStyle让编辑器更好看。
我们都知道,变色这个功能改material就能实现。但是,直接改material会导致内存里多复制一份material。所以,推荐做法是使用MaterialPropertyBlock来替换Material属性操作。具体可以参考【UWA】使用MaterialPropertyBlock来替换Material属性操作。我这里做简要介绍。MaterialPropertyBlock顾名思义,是材质属性块。可以从Render里拿出它里面的属性,进行修改,再赋值回去。用法如下:
_propertyBlock = new MaterialPropertyBlock();
_renderer = GetComponent<MeshRenderer>();
_renderer.GetPropertyBlock( _propertyBlock );
_propertyBlock.SetColor( "_Color", Color.yellow);
_renderer.SetPropertyBlock( _propertyBlock );
其中SetColor需要传入Shader里面的Properties定义的属性名。
根据那篇UWA的博客上来看,使用MaterialPropertyBlock比直接修改Material有5倍左右的耗时差距。
关于MaterialPropertyBlock,还有以下几点那篇博客没有提到:
- 同样的Mesh使用MaterialPropertyBlock设置不同的属性时,这些Mesh不会进行批处理。 参考MaterialPropertyBlock对性能的影响。
- 默认在Inspector上是看不到使用MaterialPropertyBlock修改后的属性的。需要给Properties里的属性加上 [PerRenderData] 标签才能看到。写法如下:
[PerRenderData] _MainTex ("Diffuse Texture", 2D) = "white" {
}
关于相机平滑,可以直接使用unity提供的Vector3.SmoothDamp。网上有很多讲这个函数的,但是关于里面的原理却没有人提。这里我简单说一下。Vector3.SmoothDamp用到了Mathf.SmoothDamp。【Unity的Mathf源码】。
public static float SmoothDamp(float current, float target, ref float currentVelocity, float smoothTime, [uei.DefaultValue("Mathf.Infinity")] float maxSpeed, [uei.DefaultValue("Time.deltaTime")] float deltaTime)
{
// Based on Game Programming Gems 4 Chapter 1.10
smoothTime = Mathf.Max(0.0001F, smoothTime);
float omega = 2F / smoothTime;
float x = omega * deltaTime;
float exp = 1F / (1F + x + 0.48F * x * x + 0.235F * x * x * x);
float change = current - target;
float originalTo = target;
// Clamp maximum speed
float maxChange = maxSpeed * smoothTime;
change = Mathf.Clamp(change, -maxChange, maxChange);
target = current - change;
float temp = (currentVelocity + omega * change) * deltaTime;
currentVelocity = (currentVelocity - omega * temp) * exp;
float output = target + (change + temp) * exp;
// Prevent overshooting
if (originalTo - current > 0.0F == output > originalTo)
{
output = originalTo;
currentVelocity = (output - originalTo) / deltaTime;
}
return output;
}
这里面提到了游戏编程精粹4的 《用临界阻尼实现慢入慢出的平滑》
- 这篇文章用了带阻力的胡克定律公式,然后对阻尼系数取一个特殊值从而让Y轴达到平滑的效果。这样的话这个公式画出来的曲线就是一个S形的曲线。
- 最后公式的解是一个带自然对数的结果:
- 然后对ex使用泰勒展开来进行近似计算。代码里面exp后面那一串就是取前三项的泰勒展开。
- 高数学得不好的人可能还是看不懂,别说泰勒展开,二阶导数可能都看不懂。看不懂就算了。弄懂这个对工作中意义不大,遇到实际使用的情况直接复制代码。感兴趣的可以自行翻阅游戏编程精粹4。
除了临界阻尼弦模型以外,还可以使用各种各样的缓动函数来达到平滑的效果,像我们经常使用的Lerp就是最基础的缓动函数。DoTween 里面自带了大量的缓动函数,如果没有使用DoTween插件,可以去网上找别人写好的代码,引入到工程。这里提供一个我之前找的代码:Easing.cs。
可以看到相机的焦点并不是玩家位置,而是玩家方向往前偏移了一点。这是为了能让玩家在正前方有更开阔的视野。这种手法并不是Jan首创,在2D游戏里面经常能看到。
专业术语叫双向正前聚焦(dual-forward-focus)。这篇专门介绍2D摄像机理论的文章提到了这个:Scroll Back: The Theory and Practice of Cameras in Side-Scrollers。或者看这篇译文:Scroll Back:2D 横版游戏摄像机运镜原理与实践。这篇文章非常牛逼,强烈建议大家看完。实现方面推荐使用Procamera2D插件。基本实现了上面文章提到的2D游戏里用到的所有摄像机机制,包括双向正前聚焦、平台捕捉、缩放适应、多焦点、摄像机窗格、震屏、相机边界等等。我之前项目也是用的这个插件。基本能搞定大部分相机相关的需求,不过有一些特殊的需求需要自己手写,比如你需要在游戏过程中移动相机边界,或者需要相机做指定路径的移动等等。
关于相机震动,可以看GDC上的视频:【GDC2016】Math for Game Programmers: Juicing Your Cameras With Math。里面提到的关于相机震动的要点总结如下:
- 相机震动的程度和玩家受伤程度不应该是线性关系。建议是trauma2 或者trauma3 。其中trauma是一个表示玩家受伤程度,范围在0到1的数。
- 2D游戏的相机震动使用位移+旋转。
- 3D游戏的相机震动使用旋转。因为使用位移有可能导致相机穿到墙壁里,而且会让玩家感觉远处的震动和近处的震动程度不一样,有违和感。
- 在VR游戏里慎用相机震动。因为会晕。
- 使用柏林噪声来制造随机数。能让相机震动更加真实,产生的随机数更加可控。
Jan在视频里演示的是,玩家在射击时按住反方向,并不会掉头,而是朝前开枪朝后移动。只有在松开射击时,才可以转向。
这里将枪变成了散弹。实际游戏里肯定不会只有散弹这么简单。枪的话,用好面向对象的思想,将不同枪拆成不同类去实现就可以了。
Jan让敌人死亡时有33%的概率会发生爆炸。
这一步加入了敌人死亡时的烟雾,把之前的爆炸效果调大,把武器射速提高。虽然玩法没有发生什么改变,但是看上去像那么回事了。
最后一个是慢动作,这个涉及到游戏中的时间系统,关于这个功能的开发,可以参考我写的文章【功能开发篇】游戏中的时间系统——Chronos插件源码解析。里面提到了时间系统如何设计,以及如何修改unity自带组件的运行速度。
以上就是这个视频里提到的所有内容了,因为不是策划,所以如果让我分析每个点为什么这么做我也分析不上来。这篇文章更多的是去探讨在编程上如何实现以及有什么注意事项。当然单纯实现这些功能很简单,但是从一个公司项目的角度去思考就没那么容易了。如果只是为了做这个小游戏,对象池也不需要了,相机震动更不要搞什么缓动函数,慢动作修改Time.Scale直接完事。但是正式项目肯定是不能这么做的,要充分考虑到健壮性和可扩展性,所以才有了这篇文章。
![](https://img-blog.csdnimg.cn/20200606215505251.gif)
关于作者:
- 水曜日鸡,简称水鸡,ACG宅。曾参与索尼中国之星项目研发,具有2D联网多人动作游戏开发经验。
CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
Q群:891809847