很久之前就想要用Unity实现一个比较复古的碰撞效果。
但是由于Unity的刚体是基于物理运算的,在发生碰撞的时候,会出现反弹等我们不希望出现的效果。
所以通过查看了一些类似的插件和官方的一些项目作为参考,实现了一个没有力的概念的碰撞系统。

效果


可以看出手感已经很平滑了,而且对于较为边缘的碰撞,也会自动向外部偏移,这个主要利用了切线的方向,后面会详细提到。
这一节主要讨论碰撞的算法实现,后续可能会更新一套完整的自定义物理系统的架构,可以扩展出许多好玩的效果比如:



推箱子

减速带

减速区

自定义碰撞算法

1.核心函数

Rigidbody2D.Cast(Vector2 direction, ContactFilter2D contactFilter, List<RaycastHit2D> results, float distance = Mathf.Infinity);

该函数会预先检测移动范围内所有的碰撞体。
该函数并不会导致物体移动。
物体移动使用Rigidbody2D.position。

参数和返回值

该函数返回值为int类型,表示检测到的碰撞物体的数量。
direction:表示运动方向。
contactFilter:表示碰撞设置,是一个struct,可以设置碰撞的layer,是否忽略trigger等。
results:返回发生碰撞的信息,也是一个struct,包括碰撞点,碰撞法线,碰撞点到物体的距离等信息。
distance:移动的距离。

Collider2D也拥有Cast函数,与Rigidbody2D的Cast方法的参数和返回值是相同的,不过两个函数也有不同点。

Rigidbody2D.Cast应该在FixedUpdate中调用,同时最终的移动采用Rigidbody.position进行移动。
Collider2D.Cast应该在Update中调用,最终的移动采用transform.Translate进行移动。

Rigidbody2D.Cast会检测该物体下所有碰撞体集合的信息。
Collider2D.Cast只会检测当前碰撞体的碰撞信息。

2.初始化

[RequireComponent(typeof(Collider2D))]
public class PhysicalObject : MonoBehaviour
{private const float MIN_MOVE_DISTANCE = 0.001f;private new Rigidbody2D rigidbody2D;private ContactFilter2D contactFilter2D;private readonly List<RaycastHit2D> raycastHit2DList = new List<RaycastHit2D>();public LayerMask layerMask;public Vector2 velocity;void Start(){rigidbody2D = GetComponent<Rigidbody2D>();if (rigidbody2D == null)rigidbody2D = gameObject.AddComponent<Rigidbody2D>();rigidbody2D.hideFlags = HideFlags.NotEditable;rigidbody2D.bodyType = RigidbodyType2D.Kinematic;rigidbody2D.simulated = true;rigidbody2D.useFullKinematicContacts = false;rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;rigidbody2D.sleepMode = RigidbodySleepMode2D.NeverSleep;rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;rigidbody2D.gravityScale = 0;contactFilter2D = new ContactFilter2D{useLayerMask = true,useTriggers = false,layerMask = layerMask};}private void OnValidate(){contactFilter2D.layerMask = layerMask;}
}

这里我们将Rigidbody2D的bodyType设置为Kinematic类型,并且useFullKinematicContacts为false。
Kinematic类型的刚体会返回碰撞信息,但是并不会对刚体造成物理影响,这正是我们需要的。
useFullKinematicContacts之所以设置为false,是因为我们采用Cast的方式进行碰撞检测而不是采用OnCollisionXX函数,所以为了减少不必要的性能消耗。

3.碰撞检测

private void Update()
{velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
}private void FixedUpdate()
{Movement(velocity * Time.deltaTime * 5f);
}private void Movement(Vector2 deltaPosition)
{if (deltaPosition == Vector2.zero)return;Vector2 updateDeltaPosition = Vector2.zero;float distance = deltaPosition.magnitude;Vector2 direction = deltaPosition.normalized;if (distance <= MIN_MOVE_DISTANCE)distance = MIN_MOVE_DISTANCE;rigidbody2D.Cast(direction, contactFilter2D, raycastHit2DList, distance);Vector2 finalDirection = direction;float finalDistance = distance;foreach (var hit in raycastHit2DList){//DoSth}updateDeltaPosition += finalDirection * finalDistance;}

我们在FixedUpdate中调用Movement方法,并且检测所有的碰撞信息。
在执行完Cast方法后,该方法会将所有的碰撞信息保存在raycastHit2DList中。
在Unity5.6版本之前,使用Foreach遍历每一帧都会GC,所以5.6之前对List的遍历需要使用For循环或者枚举器。(详细:https://www.jianshu.com/p/03760933e2fa)
个人测试Foreach的效率比For要好,大概2:2.5的样子,所以这里用的Foreach,测试版本Unity2018.3和2019.1。

接下来就是重头戏了,碰撞检测的方法。
不过在那之前,我们先明确一些东西。
首先传入Cast方法的Direction和Distance是固定的,这就意味着这次检测的方向是固定的,同时也就说明,最终移动的方向一定是检测的方向,否则这次检测将失去意义。
所以对于最终移动的方向和距离,我们只需要关心它的距离就好了。
并且最终的距离一定会小于Distance。

foreach (var hit in raycastHit2DList)
{float moveDistance = hit.distance;if (moveDistance < finalDistance){finalDistance = moveDistance;}}

在距离上,我们遍历所有的碰撞信息,并且找出最短的那个,这说明这个物体是距离我们最近的,同时也是我们应该停止的地方。

但是当我们发生碰撞的时候,无论什么方向,都无法进行移动了。
Debug一下

foreach (var hit in raycastHit2DList)
{float moveDistance = hit.distance;Debug.DrawLine(hit.point, hit.point + hit.normal, Color.white);Debug.DrawLine(hit.point, hit.point + direction, Color.yellow);Debug.Log(hit.distance);if (moveDistance < finalDistance){finalDistance = moveDistance;}}



黄色线代表移动方向,白色线代表碰撞法线的方向。
这时候我们发现无论我们朝着哪个方向进行移动,碰撞的法线方向始终是固定的,并且发生碰撞时,两物体之间的距离始终是0。
所以我们得出的结论就是,碰撞法线和距离是固定,与我们要移动的方向无关。
既然碰撞法线和距离是一定的,也就是说,无论我们朝着什么方向移动,他都不会改变,所以此时我们需要判断移动的方向与碰撞法线的方向,来决定该如何移动。

foreach (var hit in raycastHit2DList)
{float moveDistance = hit.distance;Debug.DrawLine(hit.point, hit.point + hit.normal, Color.white);Debug.DrawLine(hit.point, hit.point + direction, Color.yellow);Debug.Log(hit.distance);float projection = Vector2.Dot(hit.normal, direction);if (projection >= 0){moveDistance = distance;}if (moveDistance < finalDistance){finalDistance = moveDistance;}}


此时,我们加入一个点乘判断,这个判断表示,如果我们想要移动的方向与碰撞法线的方向基本相反(projection < 0),moveDistance = hit.distance,也就是0,表示无法移动。
如果想要移动的方向与碰撞法线方向呈90度(projection == 0)或者基本相同(projection > 0)说明我们要移动的方向并不会收到碰撞的限制,所以此时将moveDistance = distance,也就是初始运动的距离。

但是还有一个问题就是,虽然我们能够正确的进行移动了,但是当我们靠近物体的时候,只有在两方向夹角小于等于90度的时候才能够进行移动,如果我希望即使靠近物体,仍旧能够取得移动距离在可移动方向的分量怎么办?
可能有点绕,直白一点可以表示为,贴着墙摩擦前进。

于是我们需要引入切线来达到这个目的。
那么如何获取正确的需要移动的切线方向呢?

首先我们需要明确的是,切线方向是碰撞法线的切线方向,而不是移动方向的
其次切线的方向要保持和移动方向基本相同,因为是移动方向的分量。
(切线用品红色表示)

foreach (var hit in raycastHit2DList)
{float moveDistance = hit.distance;Debug.DrawLine(hit.point, hit.point + hit.normal, Color.white);Debug.DrawLine(hit.point, hit.point + direction, Color.yellow);float projection = Vector2.Dot(hit.normal, direction);if (projection >= 0){moveDistance = distance;}else{Vector2 tangentDirection = new Vector2(hit.normal.y, -hit.normal.x);float tangentDot = Vector2.Dot(tangentDirection, direction);if (tangentDot < 0){tangentDirection = -tangentDirection;tangentDot = -tangentDot;}float tangentDistance = tangentDot * distance;Debug.DrawLine(hit.point, hit.point + tangentDirection, Color.magenta);}if (moveDistance < finalDistance){finalDistance = moveDistance;}
}

所以我们首先通过法线方向获取切线的方向,然后判断切线方向与移动方向点积,来确定切线的方向(实际上这里用切线表述并不准确,但是为了简单明了还是采用切线来表述,实际表述应该是移动方向在切线方向的分量)
并且确定移动距离在切线方向的分量,也就是切线方向需要移动的距离。

在获得这些信息之后,我们还需要做一次Cast检测,来确定切线方向的移动会不会碰撞到物体

//Class
private readonly List<RaycastHit2D> tangentRaycastHit2DList = new List<RaycastHit2D>();{//Addif (tangentDot != 0){rigidbody2D.Cast(tangentDirection, contactFilter2D, tangentRaycastHit2DList, tangentDistance);foreach (var tangentHit in tangentRaycastHit2DList){Debug.DrawLine(tangentHit.point, tangentHit.point + tangentDirection, Color.magenta);if (Vector2.Dot(tangentHit.normal, tangentDirection) >= 0)continue;if (tangentHit.distance < tangentDistance)tangentDistance = tangentHit.distance;}updateDeltaPosition += tangentDirection * tangentDistance;}
}

首先我们需要在Class中声明一个盛放切线碰撞信息的容器。
然后进行切线方向的碰撞检测。
同样切线方向的移动也需要判断与法线的点积来确定如何移动。

最终效果。
如果想要做边缘自动偏移的话,将BoxCollider2D的Edge Radius设置一下就好了,这样正方形会变成圆角的正方形。

至此,关于碰撞检测算法已经基本完成了。
在此之前,我曾经尝试过很多种实现方式,但是最后采用这种方式,并且这种方式足够优雅~
只用了为数不多的代码。
并且实际上很多问题已经解决掉了,所以对这些东西感兴趣的朋友也可以自己尝试一下,这里也是提供一个参考。

完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;[RequireComponent(typeof(Collider2D))]
public class PhysicalObject : MonoBehaviour
{private const float MIN_MOVE_DISTANCE = 0.001f;private new Collider2D collider2D;private new Rigidbody2D rigidbody2D;private ContactFilter2D contactFilter2D;private readonly List<RaycastHit2D> raycastHit2DList = new List<RaycastHit2D>();private readonly List<RaycastHit2D> tangentRaycastHit2DList = new List<RaycastHit2D>();public LayerMask layerMask;[HideInInspector]public Vector2 velocity;void Start(){collider2D = GetComponent<Collider2D>();rigidbody2D = GetComponent<Rigidbody2D>();if (rigidbody2D == null)rigidbody2D = gameObject.AddComponent<Rigidbody2D>();rigidbody2D.hideFlags = HideFlags.NotEditable;rigidbody2D.bodyType = RigidbodyType2D.Kinematic;rigidbody2D.simulated = true;rigidbody2D.useFullKinematicContacts = false;rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;rigidbody2D.sleepMode = RigidbodySleepMode2D.NeverSleep;rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;rigidbody2D.gravityScale = 0;contactFilter2D = new ContactFilter2D{useLayerMask = true,useTriggers = false,layerMask = layerMask};}private void OnValidate(){contactFilter2D.layerMask = layerMask;}private void Update(){velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));}private void FixedUpdate(){Movement(velocity * Time.deltaTime * 5f);}private void Movement(Vector2 deltaPosition){if (deltaPosition == Vector2.zero)return;Vector2 updateDeltaPosition = Vector2.zero;float distance = deltaPosition.magnitude;Vector2 direction = deltaPosition.normalized;if (distance <= MIN_MOVE_DISTANCE)distance = MIN_MOVE_DISTANCE;rigidbody2D.Cast(direction, contactFilter2D, raycastHit2DList, distance);Vector2 finalDirection = direction;float finalDistance = distance;foreach (var hit in raycastHit2DList){float moveDistance = hit.distance;Debug.DrawLine(hit.point, hit.point + hit.normal, Color.white);Debug.DrawLine(hit.point, hit.point + direction, Color.yellow);float projection = Vector2.Dot(hit.normal, direction);if (projection >= 0){moveDistance = distance;}else{Vector2 tangentDirection = new Vector2(hit.normal.y, -hit.normal.x);float tangentDot = Vector2.Dot(tangentDirection, direction);if (tangentDot < 0){tangentDirection = -tangentDirection;tangentDot = -tangentDot;}float tangentDistance = tangentDot * distance;if (tangentDot != 0){rigidbody2D.Cast(tangentDirection, contactFilter2D, tangentRaycastHit2DList, tangentDistance);foreach (var tangentHit in tangentRaycastHit2DList){Debug.DrawLine(tangentHit.point, tangentHit.point + tangentDirection, Color.magenta);if (Vector2.Dot(tangentHit.normal, tangentDirection) >= 0)continue;if (tangentHit.distance < tangentDistance)tangentDistance = tangentHit.distance;}updateDeltaPosition += tangentDirection * tangentDistance;}}if (moveDistance < finalDistance){finalDistance = moveDistance;}}updateDeltaPosition += finalDirection * finalDistance;rigidbody2D.position += updateDeltaPosition;}
}

Unity 2D 自定义碰撞系统(一)相关推荐

  1. [Unity][2D][物理碰撞]同一层级碰撞体忽略碰撞

    3D碰撞体和2D碰撞体的原理 ,代码差不多.参考资料3 参考资料2,一个2D碰撞体忽略另外一个碰撞体. 参考资料1,一个层级的 碰撞体 忽略 另外一个 层级的 碰撞体. Edit-ProjectSet ...

  2. UNITY 2D入门基础教程 (一)

    如果用以前版本的Unity做2D游戏,虽然能做,但是要费很多周折. 比如你可以将一张纹理赋予一个"面片"网格,然后用脚本控制它的动画调整它的位移.如果你要使用物理引擎,那么还要将这 ...

  3. Unity 2D入门基础教程

    作者:Christopher LaPollo 翻译:Xiaoke 写在前面的前面的话:首先感谢原作者和译者,这是一篇非常棒的文章! 写在前面的话:转载肯定会留原文链接,作者的署名,这是毋庸置疑的.而我 ...

  4. Unity 2D教程: 滚动,场景和音效

    http://www.tairan.com/archives/7074 原文地址:http://www.raywenderlich.com/71029/unity-4-3-2d-tutorial-sc ...

  5. 【Unity】Unity 2D游戏开发(一)U2D基础功能

    文章目录 Unity 2D游戏开发基础知识 Animation动画 SortingLayer层排序 Physics 2D物理2D Rigidbody 2D刚体 示例 1.点击鼠标发出子弹 2.子弹打中 ...

  6. BEPU物理引擎碰撞系统的架构与设计

    前面我们讲解了如何监听物理引擎的碰撞事件, 在物理引擎内核中如何架构与设计碰撞规则,使得物理Entity与周围的物理环境产生碰撞时,如何灵活的控制物理碰撞,本节給大家详细的讲解BEPUphysicsi ...

  7. Unity 2D 学习笔记:游戏实例Sunnyland

    Unity 2D 学习笔记:游戏实例Sunnyland 01安装软件&导入素材 02编辑素材&Tilemap 03图层layer&角色建立 04角色移动 05角色方向& ...

  8. Unity 2D游戏开发视频教程 Unity 2D Game Developer Course Farming RPG

    Unity 2D游戏开发视频教程 Unity 2D Game Developer Course Farming RPG Unity 2D游戏开发课程农业RPG MP4 |视频:h264,1280×72 ...

  9. unity ui 概述_通过此概述了解Unity 2D和Platformer基础知识

    unity ui 概述 If you're shopping around for a 2D game engine, you've undoubtedly come across Unity. Di ...

  10. 【Unity 2D AABB碰撞检测】铸梦之路

    作者介绍:铸梦xy.IT公司技术合伙人,IT高级讲师,资深Unity架构师,铸梦之路系列课程创始人. 目录 1.AABB 碰撞介绍 2.常用2D碰撞盒 3.为什么要学习如何编写碰撞检测 4.2D BO ...

最新文章

  1. Android修改包名
  2. IA-32系统编程指南 - 第三章 保护模式的内存管理【1】
  3. python 爬取直播弹幕视频_python爬取斗鱼B总直播弹幕
  4. CVPR 2018 FlowTrack:《End-to-end Flow Correlation Tracking with Spatial-temporal Attention》论文笔记
  5. es6 babel转码器使用
  6. AIX性能管理指南-luoqiangb@dc
  7. hadoop的序列化与java的序列化区别
  8. Tomcat9.0部署iot.war(环境mysql8.0,centos7.2)
  9. hexo init报错
  10. Visio 2003 Professional
  11. macOS Python安装教程
  12. JAVA抛出异常的三种形式
  13. 实验11:20220319 1+X 中级实操考试(id:3097)
  14. 第7章 使用RAID与LVM磁盘阵列技术
  15. STC12C5A60S2单片机-双串口通信
  16. Backdoor.Trojan专杀工具
  17. 牛客网:乘积为正数的最长连续子数组
  18. scala列表-List.tabulate方法
  19. 【人工智能】人工智能顶级研究所+人工智能顶级研究机构+人工智能顶级大学推荐!
  20. linux 动态库系统目录,Linux操作系统:指定动态库(.so)搜索路径(4)

热门文章

  1. python 百度cpc点击
  2. 常见路由器默认登录用户名和密码(大全)
  3. 王之泰201771010131《面向对象程序设计(java)》第十五周学习总结
  4. nod32半年升级id
  5. [转]XXX无法访问。你可能没有权限使用网络资源
  6. 使用linux时电脑突然蓝屏,Win7系统电脑突然蓝屏提示的解决方法
  7. linux中安装apr
  8. aptana php 调试,AptanaStudio3+PHP程序远程调试的方法和步骤
  9. 第五章第五题(千克与磅之间的互换)(Conversion from kilogram to pound and pound to kilogram)
  10. 第五章 社会存在发展的基础和基本结构