游戏开发论坛

 找回密码
 立即注册
搜索
查看: 7588|回复: 3

用 Unity 探究 2D 游戏的打击感

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2019-6-24 11:10:16 | 显示全部楼层 |阅读模式
引言

这是我毕业设计的一部分emmm……我的毕设和格斗游戏相关,而对于打击感的研究算是其中我比较在意的一环。现在临近毕业,我将毕设中开发部分的一些内容整理出来分享,希望能通过这样学习到更多的东西。

打击感为何物?

字面意思,“打到了的感觉”;好的打击感是易读的,包含信息充足的;它可以让玩家感受到这次的攻击奏效了、这次攻击的轻重程度、感受到这是怎样的攻击。在电子游戏中,则通过视觉和听觉呈现这些。

实现方式

市面上已经有很多作品供我们参考,让我自己想出一个独特的实现方式如同天方夜谭,不过我喜欢参照已有作品,去探究他们如何实现。

1)帧冻结(顿帧)

顿帧已经是司空见惯的手段,也是目前最常见的表现打击感的方式。它会让角色动画停止在那里,让玩家意识到发生了什么,接着再继续播放动画。我认为所有的打击感几乎都有这个东西,只不过可能有些帧冻结的时间设置,让人很难用肉眼察觉到。在《街霸4》中,击中时普遍的顿帧时间为:攻击角色顿14/60帧≈0.23秒,受击角色顿16/60帧≈0.26秒,是我见过最长的。

一般情况下,顿帧的时间不会超过0.3秒,除非是做特殊的效果处理,因为超过0.3秒这个图像在玩家眼里会变得非常突出,显得较为奇怪。通常程度越重的攻击顿帧的时间也越长,而这也需要适当的动作设计,比如较长时间的出手准备动作能让玩家对这次攻击有一个“很重”的预期。

2)击退距离

除非有特殊需求,不然大多数游戏在攻击后,双方角色之间的距离一定会产生变化。这是非常容易表现一个攻击轻重程度的方式。

顿帧+击退是我认为最主要的两个方式,运用这两者,即可实现最基本的打击感。那么在这基础上,可以添加一些“特效”来使它更加充实。

3)特效

可以有很多种,我认为这些就像是在打击感上加花,使它更加丰满。我通过总结,列举了一些常见手段:

1.打击火花(HitFire):攻击奏效时在特定位置创建。需要注意画风适当,风格对应;斩击有斩击的样式,拳击有拳击的样式,重攻击应该比轻攻击的火花更大更饱和等等。

2.精灵(Sprite)抖动:我们在很多游戏里可能都会看到,当角色受到攻击时,帧冻结期间角色的图片(Sprite)也在颤动,颤动的方式有多种,水平及垂直方向的,或是虚影向外扩张的(我不知道那种形式该怎么表述)。但这仅限于视觉上的抖动,该抖动不会有任何逻辑上的影响,比如角色的物理坐标,判定框这些均不会随着精灵的抖动而改变。

3.屏幕震动:很多游戏都会通过震屏来彰显一次攻击的冲击力,震屏的方式以及怎样去表现不同的攻击,这值得研究。

4.颜色变化:有些动作游戏会让角色在受击的时候,身体颜色产生变化,大多是在那一瞬间,身体闪一下相应颜色,红色和白色较为常见。这会让玩家更容易读出角色正在挨打这件事。

本次毕设我主要采取了帧冻结+击退+精灵抖动+打击火花的组合进行实现。

下面是在Unity中的应用:

1.HitBox与HurtBox的搭建

先简单说下我对游戏中攻击和受击的实现吧。

image001.png

我的角色总共有3种攻击方式,而我为每一种攻击方式在角色下面都创建了一个子物体;

image002.jpg

image003.jpg

每一个攻击子物体,都有他们对应的BoxCollider2D(IsTriiger)来代表他们的攻击判定,同理,HurtBox也一样。

我用Animator的帧事件控制每个攻击动作开启攻击判定的时机和位置。

  1. private void OnTriggerEnter2D(Collider2D collision)
  2. {      
  3.   if(collision.tag == "Hurt2")
  4.   {      
  5.   }   
  6. }
复制代码

我会在OnTriggerEnter2D函数里面实现攻击需要发生的事情,若攻击物体碰到了标签为“Hurt2”(2P的受击判定框)的物体时,攻击便发生了。

2.逻辑流程

在讨论实现之前,我想先梳理一下逻辑流程,先是攻击角色的流程:

  • 首先,在Hitbox与HurtBox重叠时,我们会判定为攻击奏效,此时我会优先处理的是关闭HitBox,因为我不希望攻击事件重复发生,避免一些麻烦;
  • 之后是“顿帧”,我通过控制动画播放速度,使速度为0来实现这一功能;
  • 接着我们需要记录下角色当前的速度(X轴和Y轴),因为接下来我们要锁定角色的位移了,在帧冻结期间,我们不希望角色的位置仍在变化;
  • 创建火花特效,创建的位置我希望是在判定框相交区间的中心位置;
  • 顿帧结束,恢复动画播放;
  • 恢复角色之前被记录的速度,若本次攻击是空中攻击,那么这之后角色会继续下落了;


•对角色施加对应方向的力,以达到击退的效果,也可以是垂直方向的力,达到浮空或强DOWN之类的效果。

那么总结下来,流程就是:

关闭HitBox→停止动画播放→记录当前速度→锁定角色的X轴Y轴→创建火花→恢复动画播放→恢复角色速度→施加力(击退)

对于受击角色来说也是类似,不过会多出一个抖动的效果,并且不用记录角色速度:

进入受击动画→停止动画播放(此时应处于受击动画的第一帧)→处理精灵抖动(抖动时间应小于顿帧时间)→恢复动画播放→施加力

好的,整理完了流程,可能在讲实现方式的时候思路会更清晰一些。

3.帧冻结的实现

我通过控制Animator的播放速度和Invoke函数这两者结合来实现帧冻结这一功能。

  1. public float HitStop_AS = 1;
  2. public float HitStop_AO = 1;
  3. public float HitStop_DS = 1;
  4. public float HitStop_DO = 1;

  5. void Start () {      
  6.   HitStop_AS = HitStop_AS / 60;        
  7.   HitStop_AO = HitStop_AO / 60;      
  8.   HitStop_DS = HitStop_DS / 60;      
  9.   HitStop_DO = HitStop_DO / 60;
  10. }

  11. private void OnTriggerEnter2D(Collider2D collision){   
  12.   if(collision.tag == "Hurt2")
  13.   {     
  14.     gameObject.GetComponent<Animator>().speed = 0;   
  15.     Invoke("AnimPlay", HitStop_AS);      
  16.   }
  17. }

  18. void AnimPlay() {
  19.   gameObject.GetComponent<Animator>().speed = 1;
  20. }
复制代码

Invoke可以让规定的函数在规定时间后启动,而我在Invoke函数中使用的HitStop_AS参数就是攻击击中后攻击角色自身的帧冻结时间,这里的参数需要以秒为单位。

在定义变量时,HitStop这一类参数是以帧为单位定义的,在public之后我可以在Unity界面中很方便的以帧的概念进行调节,在游戏开始时,便会通过Start()里面的指令转化为秒单位。

我创建了名为HitStop的脚本,这些都是写在那个脚本里的,而我会给每个攻击子物体都套上这个脚本,这样每一个攻击招式都具备这些模板一样的变量了。对此我进行了一系列的总结。

表1攻击招式应具备的模板属性

属性备注

动画总帧/anim完整攻击动画的帧数;不可调整

发生/A概念变量,攻击判定产生前的帧数;可调整

攻击持续/KA概念变量,攻击判定持续存在的帧数;可调整

击中时的顿帧/HitStop_AS击中时自身动画停止播放的帧数;可调整

被击中者的顿帧/HitStop_AO被击中者动画停止播放的帧数;可调整

被防时的顿帧/HitStop_DS被防御时自身动画停止播放的帧数;可调整

防御者的顿帧/HitStop_DO防御者动画停止播放的帧数;可调整

表2受击方相关属性

属性备注

受击动画总帧/anim_o完整受击动画的总帧数;不可调整

防御动画帧/anim_d完整防御动画的总帧数;大多情况下防御动画都是一张同样的图持续多帧;不可调整

表3其他相关属性

属性备注

黄金受击帧/zhexue受击顿帧结束后,受击动画至少要播放的帧数。即这个帧数会让玩家明确的感受到第二次攻击生效了;想达到这个效果,夸张的受击动画设计是必要的,动作幅度越大,感受就越直观;目前还没有一个明确的答案能解释多少帧合适,但动作改变幅度明显即可。

连招间歇帧/ComboBreak攻击者的第一招命中后,输入可以形成连招的第二招之前允许玩家间隔的最大帧

image004.jpg

这里有很多是我自己总结的一些概念,像是一些公式也只是出于玩家视角理解的知识进行总结;格斗游戏现在已经发展成了“电子竞技”,对于玩家来说,“有利帧”“不利帧”这些都已成为了必修功课,所以我认为这些也有必要列入到设计工作当中,而这些公式或许能帮助我在开发中提供更多的便利,仅供参考。

4.打击火花的创建

先前我使用了从不同游戏中的拿过来的美术素材进行实验,画风的不统一会让人觉得很不舒服,因为这是最直观的感受,所以我后来挑了一整套同一个游戏的素材,效果好多了。

我认为,合适的火花特效非常重要。同时,特效创建的位置也应合理,我希望火花创建在HitBox与HurtBox相交区间的中心处。

为此,我自定义了一个二维向量,用来计算相交区间的中心坐标:

  1. Vector2 hit(BoxCollider2D self, BoxCollider2D oppo)
  2. {
  3.   Vector2 hit = new Vector2(1,1);

  4.   //===============判定框的中心坐标=====================================
  5.   float self_pos_x = transform.position.x + self.offset.x;
  6.   float self_pos_y = transform.position.y + self.offset.y;

  7.   float oppo_pos_x = Player2_Hurt.transform.position.x + oppo.offset.x;
  8.   float oppo_pos_y = Player2_Hurt.transform.position.y + oppo.offset.y;

  9.   if (self_pos_y + self.size.y/2 >= oppo_pos_y + oppo.size.y / 2)
  10.   {
  11.     if(self_pos_y - self.size.y/2 <= oppo_pos_y - oppo.size.y / 2)
  12.     {
  13.       hit = Player2.transform.GetChild(0).gameObject.transform.position;
  14.     }
  15.     else
  16.     {
  17.       hit = new Vector2(Player2.transform.GetChild(0).gameObject.transform.position.x, (self_pos_y - self.size.y / 2) + ((oppo_pos_y + oppo.size.y / 2) - (self_pos_y - self.size.y / 2)) / 2);
  18.     }
  19.   }
  20.   else if (self_pos_y + self.size.y / 2 < oppo_pos_y + oppo.size.y / 2)
  21.   {
  22.     if(self_pos_y - self.size.y / 2 >= oppo_pos_y - oppo.size.y / 2)
  23.     {
  24.       hit = new Vector2(Player2.transform.GetChild(0).gameObject.transform.position.x, self_pos_y);
  25.     }
  26.     else if (self_pos_y - self.size.y / 2 < oppo_pos_y - oppo.size.y / 2)
  27.     {
  28.       hit = new Vector2(Player2.transform.GetChild(0).gameObject.transform.position.x, (self_pos_y + self.size.y / 2) - ((self_pos_y + self.size.y / 2) - (oppo_pos_y - oppo.size.y / 2)) / 2);
  29.     }
  30.   }

  31.   return hit;
  32. }
复制代码


之后通过Instantiate()函数在OnTriggerEnter2D()中实现创建的功能。

5.精灵抖动

角色sprite的抖动仅限于视觉上,角色的物理坐标不会随着抖动而变化。为了实现这一逻辑,我在sprite物体上创建了一个父物体,而角色的物理BoxCollider以及RigidBody等组件都会套在这个父物体上。

image005.jpg

父物体的运动会影响到子物体,但子物体的运动不会影响父物体,这样在Sprite抖动的时候就不会影响到物理判定框了。

  1. public float A = 1;
  2. public float speed = 1;
  3. float x = 0;
  4. bool swag = false;

  5. if (swag){
  6.   x = x + speed * Time.deltaTime;
  7.   this.transform.position = new Vector3(transform.parent.gameObject.transform.position.x + A * Mathf.Cos(x), transform.position.y, transform.position.z);
  8. }
复制代码

我为此定义了3个变量,A为抖动的振幅,speed为抖动的速度,即抖动频率的控制,x会按照speed定义的速度持续增加。

若判定抖动触发(bool==true),则让角色Sprite的X坐标等于A*Mathf.Cos(x)的值,借由三角函数的函数图像可知,若x一直递增,坐标便会呈现波动态。

连击预输入的实现

image006.jpg

我将每一个攻击动画都分为了三个区间,并且定义了一个bool值;

当进入到第二个(图中黄色)区间时,便会开始检测玩家是否按下了攻击键,若按下攻击键,则给bool变量赋值true;到第三个区间时,便会开始检测bool值的真假,若为真,则允许动画切换到下一个攻击动作。而在每个动画的第一帧,将bool值重新置为false。

一般我会在允许动画切换的关键帧前面留2-3帧作为预输入的区间。

总结

那么,这是最终实现出来的效果:录像软件和GIF制作出来的效果不能很好的表达出来……

image007.gif

本次毕设研究主要以视觉表现为主;一个合适的音效的确能给打击感带来更好的体验,但一个好的打击感主要还是通过视觉反馈造就的。玩家在按下按键的一瞬间或是按键之前心里就已经有了相应的预期,而产生的结果如果符合或超过玩家的预期则证明该反馈是优秀的。

不难理解为什么有人只通过看视频就想要对打击感评头论足,因为人们更在意感官表现,因为那很直观,尽管操作手感也包括在打击感的一环里,但并不是没有仅凭视觉就让人称赞的作品。

通过这次毕设的实验,我发现也许打击感更多要靠美术去表现。程序上的实现手段总结了下来大多都能理解,运用得当即可。但我在任何参数都没改变的情况下,仅仅换了一整套更高质量的美术资源,呈现出来的效果却让我感觉好了很多。上面在说明“帧冻结”的时候提到过,一次好的打击感也需要有好的动作设计和上乘的美术表现。通过实验,我对此有了较为深刻的印象。

我希望能通过分享和交流学习到更多的东西。

以上这些主要为我的毕业设计中系统开发的一部分,有很多地方没有说明为何要这样做。希望有机会能将更多的东西整理出来吧。

作者:Determine
来源:indienova
原地址:https://indienova.com/indie-game-development/2d-strike-feeling-in-unity/

0

主题

5

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2019-8-12 16:46:17 | 显示全部楼层
大神!能教教我怎么学Unity吗?入门的那种

0

主题

3

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2019-8-14 15:40:39 | 显示全部楼层
想学游戏开发 发表于 2019-8-12 16:46
大神!能教教我怎么学Unity吗?入门的那种

建议购买马瑶老师的入门书籍
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-11-24 12:54

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表