找回密码
 立即注册
查看: 1130|回复: 14

不考虑动作的话unity如何实现类似鬼泣的浮空连击?

[复制链接]
发表于 2021-1-8 18:04 | 显示全部楼层 |阅读模式
不考虑动作的话unity如何实现类似鬼泣的浮空连击?
发表于 2021-1-8 18:08 | 显示全部楼层
国内很少做真正动作游戏,对物理引擎了解不深
物理引擎功能上分为两部分,物理碰撞检测和物理响应解算
检测部分是通用的,而解算部分是可以根据需求来单独做的
物理引擎默认的解算是基于真实物理响应的,而游戏里面通常都是定制响应模型,即便是赛车,这类看起来像是真实物理游戏,也是走的定制响应,卡丁车类游戏,里面的车基本是四个轮胎物理射线,加弹簧反弹,来进行计算的
物理引擎基本都有一个character controller 就是只使用物理碰撞,但是物理resolve 响应单独计算的例子,这种是模拟FPS的,fps中角色撞墙会光滑的划过去,斜坡也可以轻松的爬上去
如果是模拟鬼泣之类也可以,不要用刚体,浮空,不是正常物理现象, 有一种物理运算是通过确定目标状态,来进行计算的,称为velvet  积分,可以用这种来稳定计算物理状态,而不是欧拉积分
而物理碰撞可以自己打射线来实现
浮空态,只是确定多少帧上升,上升多少高度,滞空多少帧,多少帧下降,连续浮空就是在滞空期间,又加浮空,确定这些,彻底抛弃刚体,自己计算transform 位置即可
当然要记得,物理计算要放到fixed update里面,否则物理不稳定
可以简单的移动之类基于charater controller
但是滞空时候自己计算位置,而不走controller的刷新
也就是多状态切换,不同状态下,应用不同的物理响应规则
可以看看unity的动作游戏资源,基本都是射线,加自己控制物理响应来实现的
参考资料  卡丁车物理实现
FPS 物理实现
hitman velvet 积分
旺达与巨像物理
发表于 2021-1-8 18:15 | 显示全部楼层
————————原回答——————————
浮空连击,主要问题不在攻击的人物A,而是靠被打到空中的人物B的配合。
包含浮空连击的游戏,被打者B至少支持站立、倒地和浮空三种状态。当进入浮空状态以后,再次被某些技能击中,就会再飞起一点以保持浮空状态。
当满足某些条件以后(比如落地,或空中被打太多次),就不再能被打飞,防止无限浮空。


武器碰撞盒的大小,是根据具体需求决定的。动作花哨的游戏武器碰撞盒一般比较大,写实类游戏就没那么大,根据效果决定大小即可。


总之,这些动作游戏的华丽效果背后是一些简单的机制,但是精品游戏往往是通过多种手段配合把它做到了极致的效果。这个有点像做大餐和炒小菜的区别。


——————2020-7-11补充—————————
看了 猴与花果山 的回答非常受启发,他描述的针对动作、格斗游戏的一种机制非常精巧、非常清晰,适用于绝大多数动作格斗游戏的需求。
我不敢说完全领会了这种思路,只是先简单总结一下方便以后参考:
1、关键是,角色当前的运动属性可抽象为 速度、重力。不要分状态讨论,这样运动就统一了。
2、技能命中时,具有吹飞力和推力,力量会影响受击者当前速度,这样就实现了击退、击飞等效果。
3、在该思路中,被打后“倒地”是通过一种广义的“buff”实现的。如何理解这种buff是另一个问题了,其实也可以换成其它思路。


这一思路让我对状态机有了更深入的思考,简单说一下:
1、状态机很好用很常用。特别是在一开始我们没有找到最优解法之前,只能把问题分解为一个一个独立的状态,将问题分而治之。
2、但是某些复杂的、自成体系的问题,用状态机是解决不了的。很简单的例子:物理系统本身就不是状态机实现的,一个刚体时时刻刻要计算碰撞、力、加速度、速度、角速度,不可能把刚体运动分解为一个一个独立的状态(比如平移状态、滚动状态、下落状态等等),这样分开考虑不可能模拟出真实的物理运动规律。
3、在格斗和动作游戏中应用状态机,也会碰到类似的问题。格斗游戏的状态比较复杂,越做状态越多,而且效果还不一定好。理论上所有效果肯定可以用状态机做出来,但是如果用更巧妙、更合适的系统代替状态机,就可以得到一套简明、自洽、且具有强大扩展性的系统。


需要注意的是,以上说明一定只有在充足实践经验的基础上才能领悟。简简单单一篇回答不可能直接教会你如何去做,实际问题都是要具体分析的。(初学者如果连状态机都不熟悉、如何抽象状态都没掌握的话,就更谈不上理解状态机的局限性了。)
实际中的情况更为多变。举个例子,如果要在动作游戏中加入一个“爬梯子”的需求,而且爬梯子时的输入方式、攻击方式又与平时完全不同,这时较好的办法是加一个“爬梯子”的状态,以便和战斗时的逻辑区分出来,防止与原有逻辑互相干扰。
发表于 2021-1-8 18:19 | 显示全部楼层
看了评论,发现不少人没有做动作游戏的经验,甚至是没有开发游戏的经验,我就在文末追加一些关于实际操作的说明和伪代码(TypeScript),用Entity Componet System,假如你觉得ECS的每个update,都是“状态”,那我觉得抬杠可以到此为止了,ECS的特点就是无状态。
原贴内容

动作游戏也好、格斗游戏也好,其实都不物理,任何正常的游戏都不会用真实物理,你却偏偏选择用Unity那玩了命也要仿真的物理引擎,这是问题关键之一。
动作游戏被浮空以后下落规则和跳跃等下落是一样的——y坐标的变化:y +=“体重”+f(tick)-“浮空力”(unity坐标系这个还得反反),这什么意思呢?就是首先每个角色有一个体重,这个体重决定了被吹飞、起跳、下落等的基础距离(注意,单位都是1帧,所以这是个距离=速度的“极端情况”),吹飞力则是对抗这个体重的,吹飞力和跳跃力差不多,当他们大于体重+f(tick)的时候角色是往上去的,反之就往下来。
而这里的妙处在于这个f(tick),这个Tick是指角色离开地面以后的连续帧数累积,下次碰“地面”的时候会被清0,这是个正比例函数。这个值的不断增加,才形成了角色起跳然后一帧的提高量不如前一帧,最后因为体重+f(tick) > 吹飞力,以至于开始往下落,并且会越落越快、越落越快(因此如果移动碰撞是Unity这种每帧判断2个矩形而不是两个矩形连成的平行四边形,那么从很高的地方往下跳是会掉到地下去的,除非地板足够厚)。
这个f(tick)提供了什么样的性质呢?除了下落越来越快,还包括浮空保护,因为浮空连击的时候一直没有碰到“地面”,只是不断的被追加吹飞力,所以“体重+f(tick)”不断地在加,直到动作来不及命中就已经掉出了被攻击的碰撞范围,浮空连击结束,实际上玩过DNF的人,尤其是喜欢pvp的应该是有很明显的感觉的(因为也就在dnf里面有机会夸张的不断挑空,毕竟韩国人做游戏毫无平衡性可言),敌人被挑空连击次数多了会变得“特别重”就是这个道理。
    所以,顺着我上面说的正确做法,你应该能打该看出问题了:
    Unity的很多插件碰撞判定是有问题的,尤其当你用他的碰撞做过快速子弹的Top Down Shooter游戏,会发现子弹会穿过人之后。Unity的很多处理都是特殊处理,也就是根据“大多情况”来写死的,比如这个cube落地之前很难命中等,都是人家辛苦写出来的代码,你还嫌不好?不好就别用(内行都不会用这个)。在空中的处理都是“加一点力”,不过看问题,题主理解的“加一点力”和我上面说的“加一点力”只是恰好相似了,happy accident。既然做游戏,逻辑层就没有物理,没有状态机,当然除非你想做搞笑游戏。状态机仅仅是在写UI的时候有可能会用到的东西,游戏逻辑里根本不该存在状态机这样的东西,尤其是动作游戏。比如你说浮空是个状态吧,那浮空的时候“爆气回复平衡”又是个什么状态呢?爆气回复平衡以后又被升龙打中继续浮空了又是什么状态呢?或者像侍魂3之后很多格斗游戏可以空中防御,爆气了空中防御又是什么状态呢?哇,状态机哎,状态机可是有限状态机,怎么能动不动就扩展呢?是吧,没听过无限状态机把?也只有外行做游戏会想着用有限状态机来解决无限多的情况。话说回来,“浮空”和“跳跃”在受到“下一次挑空攻击”的时候有什么区别呢?
接下来就是说好的代码时间

在没有状态机的思路里,怎么去做动作游戏?事实上动作游戏本来就没状态机,而我特地用ECS来写一遍,就是告诉你,做游戏核心玩法不需要状态机。
Structs

首先我们列出一些数据的结构,这些数据都是来自于设计师配置的:
  1. class FrameInfo{
  2.     //基础信息
  3.     public id:string;           //id就不解释了
  4.     public clipInfo:ClipInfo;   //贴图信息,其实就是哪张源图片的那个位置(矩形)以及是否需要旋转等信息,这里不岔开,各家各自设计,大同小异。
  5.    
  6.     //伤害相关碰撞框,伤害碰撞交给attackCollisionSystem处理,就是个aabb碰撞,然后判断动作是否可以连续hit,这种不在这里多说了
  7.     public attackRects:Array<Rect>; //矩形数组,攻击框
  8.     public defendRects:Array<Rect>; //受击框数组
  9.     public guardRects:Array<Rect>;  //防御框数组
  10.     //用于和地形碰撞的框,地形碰撞是terrainCollisionSystem的工作
  11.     public bodyRects:Array<Rect>;   //身体碰撞框
  12.     //用于处理逻辑的数据
  13.     public damageTimes:number;  //float 伤害倍率
  14.     public stable:number;   //稳固度,稳固度高于敌对动作的破坏力,就是“霸体”
  15.     public breakPower:number;   //破坏力
  16.     public upper:number;    //吹飞力(y),负数则是向地面打击的压力,为什么是向上吹飞?不能向下吹飞?如果你有这个疑惑,出门左转,做游戏不适合你。
  17.     public pushPower:number;     //推力(x),推向远方的力,负数则为拉力。
  18.     //用于Cancel
  19.     public cancelInfo:Array<CancelInfo>;   
  20.     //其他特殊游戏特殊玩法需要依赖的数据,这里略。
  21. }
  22. class CancelInfo{
  23.     public actionId:string; //切换到的动作id,必须是id,不能是模糊的,设计师应当设计的非常精致。
  24.     public frameIndex:number;   //切换过去以后从第几帧开始。
  25. }
  26. class DamageInfo{
  27.     public damage:number;
  28.     public attacker:CharacterComponent;
  29.     public defender:CharacterComponent;
  30.     public upper:number;    //应当被吹飞多少
  31.     public pushPower:number;     //应当被推开多少
  32. }
复制代码
这里有3个相关的数据:
FrameInfo是动作每一帧的数据,一个角色永远只可能停留在某一帧(这似乎是句废话……),一帧一定属于一个动作。所以一个动作的一帧,至少在动作游戏里得有这么一些数据。
CancelInfo是cancel需要的数据,所谓cancel,就是允许在这一帧的时候发生跳转,跳转到某个动作,并且从哪个动作的第几帧继续。这个概念玩动作游戏的人都应该熟悉,开发的人就更不该不知道了。
DamageInfo是一次伤害的信息,任何游戏,一次伤害都不会只是加减法的数字——buff机制——解决了游戏行业技能带来的一切问题的逻辑框架:
如何设计一个易扩展的游戏技能系统?当然这篇回复发了好多次(题外话:这篇有点老了,今年是buff机制10周岁,经历了上百个项目的大小项目、几乎所有游戏类型的实际运作之后已经更加成熟了,我会在年底的时候在“千猴马游戏设计之道”公众号发出一篇10年总结和buff机制完整抽象方式,以及游戏策划当如何设计游戏技能的文章)
DamageInfo在经过buff的“洗礼”之后完成其最终形态,用于做后续的一切逻辑处理。
Components

数据清楚之后,我们再来看,既然是ECS,那么首先是components,我们需要一些什么样的components呢?
  1. class CharacterComponent{
  2.     //角色相关的属性,这里就一个不列了,算了,还是了一个buff把,动作游戏还是可以有buff的
  3.     public hp:number;  
  4.     public buff:Array<BuffObj>;
  5. }
  6. class VelocityComponent{
  7.     public x:number;
  8.     public y:number;
  9. }
  10. class GravyComponent{
  11.     public ticked:number;      //向下持续了多少帧
  12.     public currentWeight:number;    //当前的重力(向下的力)
  13. }
  14. class BeHurtComponent{
  15.     public attacker:CharacterComponent; //攻击者角色的标记(含属性)
  16.     public defender:CharacterComponent; //挨打角色
  17.     public attackFrameInfo:FrameInfo;   //攻击者在命中时的“动作当前帧”,汉语叫“状态”,计算机程序不叫“状态”
  18.     public defenderFrameInfo:FrameInfo; //挨打的“当前动作帧”
  19. }
复制代码
CharacterComponent的作用是为了证明entity是一个“角色对象”数据,同时也会记录一些基础的属性,但是其中不包含“当前动作”相关的信息。在创建角色entity的时候应该被加上。
VelocityComponent是“移动力”数据,当然这个移动力不是SLG里面的角色移动力,而是汉语语义下的,移动所需要的力。在创建角色entity的时候应该被加上,因为这是个动作游戏,如果是个回合制游戏,可以不直接加上,这是因为业务带来的特殊操作。
GravyComponent是“重力”数据,这个数据当且仅当需要的时候加上,比如策划设计了“在普通世界里跳起来的时候要加上”,这是最常见的设计。这里的“跳起来”是有二义性的,策划要说的是逻辑上的跳起来,但是很多外行程序员会理解为“看起来跳起来”,事实上角色在地面上三连击之类的动作,其中有些也会“看起来是跳起来了”,所以这是需要策划明确定义规则的,那么真的“跳起来”就“跳起来”了吗?这个逻辑Maze绕不过去的话,ECS是掌握不了的。
BeHurtComponent是受到打击的时候才添加的component,处理完之后就会丢掉。这个component的存在代表着角色正在挨揍。
Systems

接下来就是一些相关的System了,也是这块的核心逻辑所在:
  1. //这是整个受到攻击的处理系统
  2. class DamageSystem{
  3.     /**
  4.      * 这里是对应被捕获的entity
  5.      * 捕获所有带有:
  6.      * BeHurtComponent、CharacterComponent、VelocityComponent的控件
  7.      */
  8.     private entities:Array<Entity>;
  9.    
  10.     public Update(){
  11.         this.entities.forEach((entity:Entity){
  12.             //拿出每个component
  13.             let beHurt:BeHurtComponent = entity.get(BeHurtComponent);
  14.             let chaObj:CharacterComponent = entity.get(CharacterComponent);
  15.             let velo:VelocityComponent = entity.get(VelocityComponent);
  16.             //对于每个被捕获的entity进行处理
  17.             let damageInfo = new DamageInfo();  //伤害什么的是各家规则,就不废话了
  18.             
  19.             //开始算推力等,注意是+=,因为影响velocity的system不是只有我一个
  20.             if (beHurt.attackFrameInfo.breakPower > beHurt.defenderFrameInfo.breakPower){
  21.                 //这里根据策划设计规则来变化推力、吹飞力,假设规则就是=攻击动作对应值
  22.                 damageInfo.pushPower += beHurt.attackFrameInfo.pushPower;
  23.                 damageInfo.upper += beHurt.attackFrameInfo.upper;
  24.             }else{
  25.                 //“霸体”,推不动,啥也不干,else其实都没必要写,但是不写没法注释
  26.             }
  27.             //之后开始处理buff
  28.             beHurt.attacker.buff.forEach((buff:BuffObj)=>{
  29.                 damageInfo = buff.model.beHurt(...);    //这里省略具体的buff参数等,各种游戏各自定义
  30.             },this);
  31.             beHurt.defender.buff.forEach((buff:BuffObj)=>{
  32.                 damageInfo = buff.model.OnHit(...);
  33.             },this);
  34.             if (chaObj.hp <= damageInfo.damage){
  35.                 //角色有可能会死
  36.                 beHurt.defender.buff.forEach((buff:BuffObj)=>{
  37.                     damageInfo = buff.model.BeforeKilled(...);
  38.                 });
  39.                 if (chaObj.hp <= damageInfo.damage){
  40.                     beHurt.attacker.buff.forEach((buff:BuffObj)=>{
  41.                         damageInfo = buff.model.OnKill(...);
  42.                     })
  43.                 }
  44.             }
  45.             //省略伤害值之类的处理,这里只管切题的处理击飞。
  46.             //之所以要运行完buff,是因为运行的过程中,可能产生“角色无视击飞”等等buff的情况
  47.             velo.x += damageInfo.pushPower;
  48.             velo.y += damageInfo.upper;
  49.             //丢掉我
  50.             entity.remove(BeHurtComponent);
  51.         },this);
  52.     }
  53. }
  54. //这是处理重力的系统
  55. class GravySystem{
  56.     /**
  57.      * 这里是对应被捕获的entity
  58.      * 捕获所有带有:
  59.      * GravyComponent、VelocityComponent的控件
  60.      */
  61.     private entities:Array<Entity>;
  62.    
  63.     public Update(){
  64.         this.entities.forEach((entity:Entity){
  65.             let gravy:GravyComponent = entity.get(GravyComponent);
  66.             let velo:VelocityComponent = entity.get(VelocityComponent);
  67.             gravy.currentWeight += (gravy.ticked += 1);    //假设每个Tick+1,规则当然未必非得这样,看设计了。
  68.             //毕竟,gravy只管向下。
  69.             velo.y += gravy.currentWeight;
  70.         }
  71.     }
  72. }
复制代码
我们首先来看,挨打处理的系统DamageSystem里面并没有特殊处理什么,角色是否正在浮空什么的我根本就不关心,我要做的就是,如果条件允许,就让角色的velocity发生变化,注意是velocity,而不是直接改变坐标,改变坐标则应该有moveSystem来承担,而且换动作做,则是由motionSystem根据策划设计的、逻辑业务所关心的数据来进行。
GravySystem是重力影响角色移动的系统,也就是说如果一个角色没有GravyComponent他是不受到重力影响的,比如小鸟之类的单位,在没有挨打的时候是不需要添加GravyComponent的(当然这也是得看策划设计的)。
总结

是不是?动作游戏杠呢本就不需要什么状态机。事实上所有的游戏都不需要状态机,状态机是什么?本质上就是Switch Case,也就是特殊处理,强行把一些东西关联到一起归类。很多人爱道德批判,说人家喜欢“贴标签”是坏事儿, 写代码的时候用状态机难道不是“贴标签”吗?
假如你理解一个单词,比如“状态机”,他可以用来解释任何你觉得可以解释的词,抬杠的时候特别灵光,诶,我说这个是状态机他就是,因为他有xx特征,诶,我现在说那个也是,因为他有oo特征。对不起,这个单词什么都解释不了,因为什么都能解释的词组,就什么都不是——这正是编程2大困难之一命名的要求之一——一个词组不能解释超过1种情况,不然就是二义性。
发表于 2021-1-8 18:29 | 显示全部楼层
物理引擎是模型刚体简单物理用的,人物不是刚体,也不是简单物体,所以不能用。管你是不是浮空什么的。
一般还会利用物理引擎是碰撞盒做碰撞检测。这属于物理引擎里包含的公共基础设施,虽然自己写也可以,但是不用白不用。


但是受物理影响的物体和不受影响的物体,受影响和不受影响的共存和切换就会存在各种问题,这些问题现在没有统一解法,也是动作相关业务逻辑的重点。
发表于 2021-1-8 18:29 | 显示全部楼层
动作游戏需要你自己实现一套物理规则,而非使用unity提供的物理引擎,至少需要满足:
1.可按需求随时取消各种力以及矢量速度,即你的浮空需求
2.百分百概率的运算结果重现,即在容差范围内输入的同一套连招,运动结果必须要一致,和格斗游戏帧同步类似的需求
发表于 2021-1-8 18:32 | 显示全部楼层
鬼泣的动作用的都不是物理引擎。你得自己写动作逻辑。
只要你用了物理引擎,那就不可控。
发表于 2021-1-8 18:41 | 显示全部楼层
一般做这种游戏都不用引擎的物理系统的,然后状态机是所有状态连所有状态,为了能用他的那个模型动画过渡,如果有其他插件当然也可以,然后具体要实现什么逻辑要自己写,
发表于 2021-1-8 18:42 | 显示全部楼层
简单来说就是动作编辑器里,该动作相关的动作帧,增加inair选项。
勾选改选项时候,普通物理(重力)不生效。
直到进入没有勾选的动作帧为止。(比如状态机切换)
我们做动作时候是这样实现的。
发表于 2021-1-8 18:48 | 显示全部楼层
击飞浮空其实是2D时代的表现手法,完全不遵循物理定律的,关掉重力比较好。
其实一种偷懒的办法,是让物体实际上并没有真正在高度轴移动,只有人物模型和碰撞框会动,让玩家看着觉得飞起来就行。
搞一个cube上,这个cube绑在主物体上,设置一个高度属性,控制cube相对主物体的高度,这个高度是恒定速度下降的,打中一下,固定上升XX高度。
或者像其他回答的,加一个连续吹飞次数,设置一个公式让上升幅度不断降低就好,或者把碰撞框拉高,那么可以产生站在地上打也可以把敌人连到升天的现象。
这样吹飞不涉及物理引擎,也不改变主物体的实际位置,仅仅是主物体下的cube相对主物体改变高度,cube位置随时可以瞬间归零,可以避免不少BUG,也可以更方便地参考2D格斗游戏的经验。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-22 13:10 , Processed in 0.097461 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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