zoukankan      html  css  js  c++  java
  • 糍粑大叔的独游之旅-战斗!之弹道实现

    转自:http://www.gameres.com/677540.html

    今天谈谈一个有趣的内容——u3d实现子弹弹道。当然这个完整的说,也非常复杂,还是由浅入深,先说说最核心的原理。

      一、定义

      我将子弹分为至少两种:

      1、实体型。即发射后,生成一个带刚体的gameobject,可以通过u3d的物理引擎实现碰撞检测。比如机关枪子弹、弹道等等。

      2、射线型。即子弹的运动和碰撞不由刚体实现,弹道控制由代码实现,通过Physics2D下函数或自己计算物体检测。比如定向激光、AOE伤害。

      本文分别讲两种的实现。

      二、理解U3D的物理引擎

      知其所以然,才能更好、更快的进行代码实现。这个章节从运用和现象入手,说明一些有关u3d物理引擎的“坑”

      Collider和Rigidbody

      如果完全不了解U3D的Collider(碰撞体)、Rigidbody(刚体)可以先找下相关的文章看下,网上很多很好的文章。

      这里用自己的理解简单说下(针对2D):

      Collider描绘物体的碰撞形状,有Box、Cirlce等,是碰撞检测的基础,比如鼠标点击的碰撞判断,都是依赖于定义的Collider来知晓物体的“碰撞形状”

      Rigidbody是给物体赋予一个接受物理规律的属性,比如给一个小球加上刚体,默认的小球就会收到重力加速度往下掉。

      刚体必须有collider,不然就没有“形状”。

      在u3d的2D物理效果的设计理念是:

      不会动的物体,只设Collider,例如大地、树木;

      会动的,需要有物理运动效果的,设置Collider和Rigidbody,Collider说明“外形”,Rigidbody说明物理属性,如质量、碰撞属性等。

      注意这个理念,因为我首次使用2D物理到游戏中的时候,有很多地方很费解,不明白为什么要这么设计,如果理解这个理念了,很多东西就想明白了。

      碰撞检测的相关参数

      U3D的2D物理引擎,可以不用做任何编码实现刚体的运动碰撞效果。

      但如果需要代码获取碰撞信息,相关的属性有:

      Collider的Is Trigger,Rigidbody的Is Kinematic

    糍粑大叔的独游之旅-战斗!之弹道实现(上) ...


      至于其他刚体参数的作用,可以摆个场景逐个调整参数看看效果,就基本明白了。

      因为我主要是想通过u3d的2D物理引擎获得的碰撞信息,而不关注碰撞后的物理效果,所以其他参数都可以不管或设置为0。

      特别的,由于游戏是一个top-down视角的游戏,而u3d的2D物理引擎默认y轴是2D世界的上下方向,所以Gravity Scale要设置为0。

    糍粑大叔的独游之旅-战斗!之弹道实现(上) ...


      对应的相关的函数有:

      OnTriggerEnter2D、OnTriggerExit2D、OnTriggerStay2D(Collider发生碰撞时被调用)

      OnCollisionEnter2D、 OnCollisionExit2D、OnCollisionStay2D(Rigidbody发生碰撞时被调用)

      下面将解释这些属性和函数的作用。

      碰撞检测的推荐实现方式——Rigidbody的Collision

      需要用u3d的2d物理引擎实现碰撞检测时,需要这么设置(假定检测A和B之间的碰撞):

      1、A和B都添加collider,不勾选Is Trigger

      2、A或B至少一个添加rigidbody,不勾选IsKenematic

      3、对A或B添加脚本,添加OnCollisionEnter2D、 OnCollisionExit2D或OnCollisionStay2D函数获取碰撞信息。

      以本文的实体型子弹为例:

      1、  对游戏单位和子弹都添加collider

      2、  对子弹添加rigidbody

      3、  对子弹添加OnCollisionEnter2D方法,编写造成伤害的逻辑代码,并销毁子弹对象。

      关于Rigidbody的Is Kinematic的属性:勾选后,2D物理引擎对这个刚体不起作用,只能代码去实现物体的运动。同时,OnCollisionEnter2D也不会被触发。

      另外一种碰撞检测的实现——Collider的Trigger

      通过Collider的Is Trigger的,也能实现“碰撞检测”。

      Collider的Is Trigger:顾名思义,这个属性说明是否触发,勾选后,则会有“碰撞时”OnTriggerEnter2D、OnTriggerExit2D, OnTriggerStay2D函数。

      例如,检测A和B之间的碰撞:

      1、  A和B都添加collider,A勾选Is Trigger,B不勾选

      2、  A添加rigidbody

      3、  对A脚本添加OnTriggerEnter2D

      A和B的collider发生接触时,则A的OnTriggerEnter2D被调用。如果B的脚本也有OnTriggerEnter2D,也会被调用,尽管B没有勾选Is Trigger。

      这种“碰撞检测”,依靠Collider的trigger机制,在Collider层面就可以完成,其原理应该和鼠标点击事件的触发类似。但有以下问题:

      1、  这个触发机制的碰撞检测频率和Update一样,而上文中推荐方式(利用OnCollisionEnter2D)是和FixedUpdate一样,后者是专门是做刚体物理运算,其计算频率更好,碰撞检测更准确。如果使用OnTriggerEnter2D的方式,检测到碰撞发生时可能两个碰撞的物体已经相互嵌入很久了,如果其中一个物体运动速度过快,可能已经“穿”过去了

      2、  OnCollisionEnter2D的参数提供的碰撞信息更丰富,而OnTriggerEnter2D只有一个碰撞对方collider的信息,得不到更精确的点。

      3、  虽然碰撞是在Collider层面完成,感觉跟Rigidbody没有什么关系(1、2两点的想象也侧面印证了我这个想法),但A和B之间必须有一个是Rigidbody,不然碰撞事件触发不了。Physics2D中IsTouching等函数也有这样。

      4、  设置Trigger后,所有的碰撞事件被Trigger拦截,OnCollisionEnter2D不会再被调用。

      基于以上因素,这种碰撞检测,不能称之为有效的“碰撞检测”,在实际运用中要根据实际情况判断是否合适。

      作为游戏物体和物体的碰撞检测,不推荐使用Collider的Trigger方式。

      关于碰撞检测的总结

      1、如果想使用物理引擎实现碰撞,包括Collider的Trigger,rigidbody的Collision,Physics 2D的IsTouching等方法,除了碰撞双方都有Collider,必须有1个有rigidbody。(此点让我无力吐槽)。

      2、使用Collider的Trigger(勾选Is Trigger),可以使用OnTriggerEnter2D、OnTriggerExit2D, OnTriggerStay2D监听碰撞,但没有碰撞物理效果,rigidbody的collision无法使用。Collider的Trigger不是在物理引擎层面上工作的,不管是碰撞检测的更新频率、是碰撞结果都不好,且它直接“阻止”了物理引擎的对物体的作用。

      3、使用rigidbody的collision(不勾选Is Kinematic),使用OnCollisionEnter2D、 OnCollisionExit2D、OnCollisionStay2D监听碰撞,物理引擎会影响刚体的运动,会有碰撞反弹的物理效果。最好在OnCollisionEnter2D只获取状态而不更新物体运动,因为物理引擎这是也在控制它的运动。

      综上,u3d物理引擎的使用限制还是很多的,实现很多逻辑功能都有障碍。由于对于实体子弹的实现,子弹打击单位后,子弹自我销毁,和以上第3点正好满足,可以使用u3d的collision,而非实体子弹显然不能使用。

      三、实体型子弹

      如果认真阅读上面的分析且理解了原理,应该对u3d的2D碰撞(3D类似)的套路应该很清楚,实现实体子弹打击效果,仅仅是点点、配配的事。

      现实方式为:

      1、  对游戏单位和子弹都添加collider

      2、  对子弹添加rigidbody

      3、  对子弹添加OnCollisionEnter2D方法,编写造成伤害的逻辑代码,并销毁子弹对象。

      关键代码:

    1. void Update ()
    2.     {
    3.         if (Common.pause)
    4.             return;
    5.         m_Anim.OnUpdate (GetComponent<SpriteRenderer> ());
    6.         float l = Time.deltaTime * m_Info.speed;
    7.         transform.position += m_Direction * l;
    8.         m_LeftDistance -= l;
    9.         if (m_LeftDistance < 0 || m_DestroySelf)
    10.         {
    11.             FlyerManager.Free(gameObject);
    12.         }
    13.     }
    14.     public virtual void OnCollisionEnter2D (Collision2D coll)
    15.     {
    16.         if (m_DestroySelf)
    17.             return;
    18.         TargetPick pick = TargetPick.From (coll);
    19.         m_Info.AttackOn (pick,m_Direction, m_myUnit,hitEffectType);
    20.         m_DestroySelf = true;
    21.     }
    复制代码


      其中有很多类和函数已经封装,例如:

      FlyerManager.Free(),内部实现了子弹的回收,便于再利用。

      再如TargetPick和AttackOn,实现了拾取最合适的游戏单位和计算打击伤害的功能。

    上文说明在弹道实现中,关于u3d的物理引擎的一些相关要点,并跟给了实体性子弹的关键实现代码。

      本篇中,将继续说明射线型弹道的实现的。

      四、射线型子弹

      本章先讲子弹的碰撞逻辑实现,由于射线型子弹用u3d的sprite是绘制不出来的,所有需要特殊的技巧,绘制方法在下一章中说明。

      使用Physic2D库,进行非自动碰撞检测

      区别于实体子弹,在游戏中,我需要实现类似于激光射线、范围伤害(AOE)的攻击类型。

      这种情况下,就不能依靠rigidbody来实现碰撞的检测,前文中又说了,u3d的物理引擎不支持两个collider的碰撞检测。

      所以,没有办法“自动”做碰撞检测(这里所谓自动,就是去实现一个函数,然后等着u3d在碰撞发生时自动调用)

      我依然使用u3d的2D物理库,在每一帧(每个Update)中,对目标collider进行检测。

      例如,AOE伤害:

    1. RaycastHit2D[] hits = Physics2D.CircleCastAll(transform.position, m_Info.aoeRadius, Vector2.zero,
    2.      LayerManager.GetLayer(m_Faction).oppUnitMask );
    复制代码


      使用Physics2D的CircleCastAll、RaycastAll、BoxCastAll,对指定layer中的所有collider,做圆形、射线、长方形的碰撞检测。

      即,这里不再用collider和collider,而是判断指定的圆形、射线、长方形,和哪些collider碰撞(Cast),包括相交和包含。

      这些函数有一个All和非All的版本,返回所有检测到collider或者最近的collider。

      返回值RaycastHit包含collider、point(碰撞点)、normal(碰撞法线)等,具体请参考u3d api,这里不再累述。

      和上文说的Collider的碰撞检测一样,碰撞的代码实现放在Update里,可能出现“嵌入过多”或“穿过”的情况。

      但由于,需求本身是针对非实体子弹的,其面积、范围比子弹大很多,所有没有太严重的影响。

      (如果放在FixedUpdate会精确很多,但消耗太大)。

      几种弹道类型的攻击逻辑实现

      穿透激光

    糍粑大叔的独游之旅-战斗!之弹道实现(中) ...


      穿透激光,可以对射线上的单位造成伤害。

      其攻击的实现代码:

    1. IEnumerator _TakeAttack ()
    2. {
    3.         yield return new WaitForSeconds (RAY_TAKE_ATTACK); 
    4.         Vector3 v = Utils.Up (transform);
    5.         Vector3 worldCenter = transform.position + v * m_Info.attackDistance / 2;
    6.         Vector2 size = new Vector2 (width, m_Info.attackDistance);
    7.         RaycastHit2D[] hits = 
    8.             Physics2D.BoxCastAll (worldCenter, size, 
    9.                 Utils.DirToAngle(v), new Vector2 (0, 0),
    10.                 Mathf.Infinity,LayerManager.GetLayer(m_Faction).oppUnitMask);  
    11.       m_Filter.Clear();
    12.                       for (int s = 0; s < hits.Length; ++s)
    13.                       {
    14.                                 TargetPick pick = TargetPick.From (ref hits [s]);
    15.                                 pick.ToShield();
    16.                                 if( pick )
    17.                                           if( m_Filter.Test(ref pick) )
    18.                                         m_Info.AttackOn (pick, v, m_myUnit,hitEffectType,null);
    19.                       }
    20.         }
    复制代码


      注意,攻击使用StartCoroutine做一个延时,这是因为,为了动画效果更真实,在做激光的绘制时,有一个很快的激光射线变长的过程,所以攻击的实际效果要和增长时间配合。

      代码里面有很多游戏逻辑相关的东西,不用太关注,关键是要获取到一个box的形状描述,需要知道Up,Center,Size。

      这里把激光看成一个很长的box,长度是激光的最大攻击距离。

      最后,由于有些单位有多个Collider,所有需要过滤一下(即m_Filter),原理很简单:

      1、找hit或collider对应的单位(TargetPick.From)

      2、检查单位在过滤器中是否已经存在。存在就不在处理,不存在就继续,并添加到过滤器中(m_Filter.test)

      3、进行伤害的计算逻辑(m_Info.Attack)

      定向激光(持续)

    糍粑大叔的独游之旅-战斗!之弹道实现(中) ...


      定向激光对指定的单位进行攻击。

      定向激光不需要通过碰撞检测去“探测”激光与哪些collider相交的,因为定向激光是对已指定的单位进行持续攻击。

      需要解决的问题,激光与单位的碰撞点到底在哪。根据这个碰撞点,绘制激光的形状。

      其攻击的实现代码:

    1. public bool _TakeAttack (out Vector3 point)
    2.     {
    3.         point = Vector3.zero;
    4.         Vector3 v = Utils.Up (transform);
    5.         // The colliders in the array are sorted in order of distance from the origin point
    6.         RaycastHit2D[] hits = Physics2D.RaycastAll (transform.position, v, m_Info.attackDistance,
    7.             LayerManager.GetLayer(m_Faction).oppUnitMask);
    8.         // 是否有target的hit
    9.         TargetPick pick = TargetPick.none;
    10.         for (int s = 0; s < hits.Length; ++s)
    11.         {
    12.             pick = TargetPick.IsTarget (target, ref hits [s]);
    13.             if (pick)
    14.                 break;
    15.         }
    16.         if (pick)
    17.         {
    18.             point = pick.point;
    19.             m_Info.AttackOn (pick, v, m_myUnit, Const.NONE_EFFECT, this);
    20.             if (pick.unit && pick.unit.curHp <= 0)
    21.                 return false;
    22.             if (pick.part && pick.part.unit.curHp <= 0)
    23.                 return false;
    24.             return true;
    25.         }
    26.         return false;
    27.     }
    复制代码


      攻击函数返回是否攻击到对象单位(target),并返回攻击点。和穿透激光的区别,使用RayCastAll目的只是找到要攻击的对象是否在其中,并确定碰撞点。

      定向激光(单次攻击)

    糍粑大叔的独游之旅-战斗!之弹道实现(中) ...


      类似于图中的闪电效果。原理持续的定向激光基本一致,区别是单次攻击时播放闪电(或其他效果)动画。

      同持续定向激光一样,在攻击过程中不停的用RayCast判断闪电是否“打”到了目标上,

      如果没有需要立即中断攻击和攻击动画,否则攻击单位在出现突然转身的是否,闪电会随着攻击攻击单位移动,出现bug。

      不再给出代码。

      范围攻击

      典型的是喷火器或者爆炸,在一定范围内所有单位收到伤害。

    1. bool _TakeAttack ()
    2.     {
    3.         Vector3 dir = Utils.Up (transform);
    4.         m_Filter.Clear ();
    5.         RaycastHit2D[] hits = Physics2D.CircleCastAll (transform.position, m_Info.attackDistance, Vector2.zero,
    6.                                   Mathf.Infinity, LayerManager.GetLayer(m_Faction).oppUnitMask);
    7.         
    8.         bool f = false;
    9.         // 是否有target的hit
    10.         for (int s = 0; s < hits.Length; ++s)
    11.         {
    12.             Vector3 v = Utils.V2toV3 (hits [s].point) - transform.position;
    13.             if (Mathf.Abs (Vector3.Angle (dir, v)) < m_Info.attackArc / 2)
    14.             {
    15.                 f = true;
    16.                 TargetPick pick = TargetPick.From (ref hits [s]);
    17.                 // AOE CircleCastAll 可能选不到shield
    18.                 pick.ToShield ();
    19.                 if (pick)
    20.                 if (m_Filter.Test (ref pick))
    21.                     m_Info.AttackOn (pick, Vector3.zero, null, Const.NONE_EFFECT ,this);
    22.             }
    23.         }
    24.         return f;
    25.     }
    复制代码


      原理很简单,先找到圆形范围内的所有collider,再判断是否在喷火器的扇形角度内。

      攻击、弹道这块内容游戏逻辑是游戏的一个重点,其实很难写一辆篇文章说清,其实我很想把从最下层的u3d的物理、绘制到最上层代码逻辑架构 全部说清楚,

      但发现写一篇文博耗费的时间比我想象长,长到我写代码实现的一个功能的时间还没写一篇文章长。

      所以,我考虑了下,不能指望所有的东西全部说清楚,最要还是讲原理,不同于网上大部分的教程讲的是最基础的内容,甚至是解释api,

      而是建立读者有一定基础上,讲原理、讲结构,讲自认为的难点,有助于自己梳理游戏代码,也是对关键的技术点做一个备忘。

      下篇预告:弹道的图形效果实现、攻击逻辑的结构和要点(比如本文中展示代码中定义的类的意义)

  • 相关阅读:
    angular 项目 error TS2451: Cannot redeclare block-scoped variable 'ngDevMode'
    chrome 总崩溃的正确解决方法
    angular 学习日志
    mongodb 3.4 学习 (二)命令
    mongodb 3.4 学习 (一) 安装
    python中文入库
    [转贴] 流量统计脚本
    监控系统开发的一些参考
    nagios centos7 rpm打包
    collectd配置
  • 原文地址:https://www.cnblogs.com/sevenyuan/p/5804428.html
Copyright © 2011-2022 走看看