Unity2D和3D的物理引擎是基于PhyX的,内置的碰撞检测也是基于PhyX的。PhyX可以模拟很真实的物理效果。但是对于游戏来说太过真实的物理效果反而看起来很假,游戏需要的是可配置性的“物理效果”,例如按帧或者时间线的方式来编辑产生类似物理的效果,所以目前大量的游戏几乎都是不使用物理引擎的。在Unity中可以关闭物理效果,只用它的碰撞功能,或者整理的碰撞功能都自己编写代码来完成

Collider 2D

任何的碰撞现象都有两个载体,一个是发起碰撞的,另一个是接受碰撞的,所以我们首先要明确哪些物体是可以接受碰撞的。Collider2D并不依赖Sprite组件,就好比一个空气墙

普通的Collider 2D对象

capture Collider一般用于主角,其他形状用于场景或者动态阻挡等。composite Collider表示将Box Collider和Polygon Collider的外形合并,

Tilemap Collider 2D专用于tile,它经常与composite Collider合并使用,来消除无用的tile碰撞边界

Rigidbody 2d

表示2D刚体组件,表示当前物体启动物理引擎。如果希望操控的角色被其他碰撞体组件挡住,就必须使用刚体组件

Body Type介绍:

  • Dynamic:表示动态刚体,完全模拟物理效果,碰到Collider 2D会被挡住,碰到任意Rigidbody 2D都会产生物理效果。它在空中会根据重力自动落下来,它的效率是最低的,仅适合给主角使用
  • Kinematic:它只能和选中Dynamic复选框的Rigidbody发生碰撞效果。如果需要碰撞事件,比如OnCollisionEnter2D,或者Kinematic与Kinematic碰撞,两者必须要有一个选中USE Full Kinematic Contacts复选框。Kinematic与Static碰撞,Kinematic必须选中USE Full Kinematic Contacts复选框,否则碰撞事件也没有了。Kinematic适合做主角被攻击的碰撞检测。比如主角被别的物体击飞,发生击飞的物体可以设置Kinematic。因为主角已经是Dynamic了,可以正常触发碰撞效果,如果使用Kinematic效率比Dynamic好
  • Static:只能和Dynamic发生碰撞物理效果,和Kinematic只能发生碰撞事件(需要保证Kinematic必须勾选USE Full Kinematic Contact复选框)它的效率是最高的

**如果需要移动或者旋转带Rigidbody 2D的组件时,不能直接修改它的Transform.position,而是要使用Rigidbody.positionRigidbody.rotation**否则无法发生正确物理现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class Script_06_10 : MonoBehaviour{
public SpriteRenderer heroRenderer;
public RigidBody2D heroRigidbody2D;

void Update(){
//处理方向键
if(Input.GetKey(KeyCode.W)){
Run(Vector3.up);
}else if(Input.GetKey(KeyCode.S)){
Run(Vector3.down);
}else if(Input.GetKey(KeyCode.A)){
Run(Vector3.left,true);
}else if(Input.GetKey(KeyCode.D)){
Run(Vector3.right,false);
}
}
void Run(Vector2 position,bool flipx = false)//通过预先赋值名参,实现参数的可隐藏
{
//控制人物左右移动时镜像
heroRenderer.flipx = flipx;
//绑定rigidbody以后,不能再使用transform.position赋值
heroRigidbody2D.position += (position * 0.1f);
}
}

碰撞事件

碰撞事件和碰撞效果是两个不同的概念。

碰撞事件表示Collider2D被RIgidbody2D碰撞后发生的事件,碰撞事件会被碰撞者和碰撞者同时接收到。

碰撞效果指的是碰撞事件触发的效果,比如主角被空气墙挡住

主角碰到墙,给主角或者墙任意一方绑定脚本都可以收到事件。如果是在主角这里监听碰到什么东西,代码可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
private void OnCollisionEnter2D(Collision2D collision)
{
Debug.LogFormat("开始碰到:{0}", collision.collider.name);
}
private void OnCollisionStay2D(Collision2D collision)
{
Debug.LogFormat("持续碰到:{0}", collision.collider.name);
}
private void OnCollisionExit2D(Collision2D collision)
{
Debug.LogFormat("结束碰到:{0}", collision.collider.name);
}

游戏中需要监听碰撞的事件可能比较多,并非一定要将其写在监听它的脚本中,可以将它抛出去,这样就可以在与它有关的地方统一处理。

首先新建CollisionPublisher脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;
using UnityEngine.Events;

public class CollisionEvent : UnityEvent<GameObject, GameObject> { }

public class CollisionPublisher : MonoBehaviour
{
public static CollisionEvent onCollisionEnter2D = new CollisionEvent();
public static CollisionEvent onCollisionStay2D = new CollisionEvent();
public static CollisionEvent onCollisionExit2D = new CollisionEvent();

private void OnCollisionEnter2D(Collision2D collision)
{
onCollisionEnter2D.Invoke(gameObject, collision.collider.gameObject);
}

private void OnCollisionStay2D(Collision2D collision)
{
onCollisionStay2D.Invoke(gameObject, collision.collider.gameObject);
}

private void OnCollisionExit2D(Collision2D collision)
{
onCollisionExit2D.Invoke(gameObject, collision.collider.gameObject);
}
}

将这个脚本挂载在某个具有Collider的对象上(发布者)

新建两个Square,全部挂载Box Collider和Rigidbody,其中一个是dynamic,一个是static,将CollisionListener脚本和下面的RigidbodyMove脚本全都挂载在Dynamic的Suqare上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RigidbodyMove : MonoBehaviour
{
private Rigidbody2D rb;
public float force = 10f;
// Start is called before the first frame update
void Start()
{
rb = gameObject.GetComponent<Rigidbody2D>();
CollisionListener.onCollisionEnter2D.AddListener((g1, g2) =>
{
Debug.LogFormat("{0}开始碰撞{1}", g1.name, g2.name);
});
CollisionListener.onCollisionStay2D.AddListener((g1, g2) =>
{
Debug.LogFormat("{0}正在接触{1}", g1.name, g2.name);
});
CollisionListener.onCollisionExit2D.AddListener((g1, g2) =>
{
Debug.LogFormat("{0}结束碰撞{1}", g1.name, g2.name);
});
}
private void FixedUpdate()
{
float horizontalmove = Input.GetAxis("Horizontal");
rb.velocity = new Vector2(horizontalmove * force*Time.deltaTime, rb.velocity.y);
float verticlemove = Input.GetAxis("Vertical");
rb.velocity = new Vector2(rb.velocity.x,verticlemove*force*Time.deltaTime);
}
}

将匿名方法加进去之后,碰撞就会执行匿名方法

控制台输出

碰撞方向

碰撞通常会有4个方向,跳起来脑袋碰到房顶,掉下去脚碰到地面,还有就是左右两边的碰撞了。

Unity2D目前并没有提供方法来判断方向,但是提供了碰撞发生的坐标点,这样就可以计算碰撞方向了。

将上面的Square挂载的Box Collider改为Circle Collider,并在RigidbodyMove脚本中添加下面几行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void OnCollisionStay2D(Collision2D collision)
{
foreach (var contact in collision.contacts)
{
Debug.DrawLine(contact.point, transform.position, new Color(0f, 211f, 187f),5f);
var direction = transform.InverseTransformPoint(contact.point);//将世界坐标转换为本地坐标,表示的是接触点相对当前物体的相对位置
Debug.Log(direction);

if(direction.x > 0f) { print("右碰撞"); }
if(direction.x < 0f) { print("左碰撞"); }
if(direction.y > 0f) { print("上碰撞"); }
if(direction.y < 0f) { print("下碰撞"); }
}

}

Circle碰撞

在playing模式下,打开gizmos就可以在game窗口中看到Debug.DrawLine画出的线

Debug显示

触发器Publisher

将Collider的is trigger勾选后,就变成了没有物理阻挡的触发器(Trigger)

新建TriggerPublisher脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using UnityEngine.Events;

public class TriggerEvent : UnityEvent<GameObject> { }

public class TriggerPublisher : MonoBehaviour
{
public static TriggerEvent onCollisionEnter2D = new TriggerEvent();
public static TriggerEvent onCollisionStay2D = new TriggerEvent();
public static TriggerEvent onCollisionExit2D = new TriggerEvent();

private void OnTriggerEnter2D(Collider2D collision)
{
onCollisionEnter2D.Invoke(gameObject);
}
private void OnTriggerStay2D(Collider2D collision)
{
onCollisionStay2D.Invoke(gameObject);
}
private void OnTriggerExit2D(Collider2D collision)
{
onCollisionExit2D.Invoke(gameObject);
}
}

Effectors 2D

具有特殊物理效果的Collider 2D对象,Effector需要在Collider的基础上勾选used by effector才可以使用

  • Platform Effector 2D:一种特殊的地面,称为单向平台,可以从下面跳上去。
  • Surface Effector 2D:像传送带一样带摩擦地缓慢移动(如果在主角移动代码中设定了Velocity,此效果器会失效)
  • Point Effector 2D:圆形引力场效果器,需要对应的Collider设置为Trigger,可以制作吸引或爆炸效果
  • Buoyancy Effector 2D:浮力效果器,模拟水中的浮力效果。
  • Area Effector 2D:区域力,例如物体从空中掉下来,进入某个区域互相弹跳的效果。(类似Point Effector,但是在碰撞区域内部起效)

优化

如果碰撞效果必须通过物理引擎,那么必须在RIgidbody 2D中选中Dynamic复选框了,这样功能虽然是最全面的,但是效率也是最低的。

另一种做法是不依赖物理引擎。没有碰撞效果(例如被墙挡住行走),仅选中运动学Kinematic,那么只能监听到OnCollisionEnter2D()OnTriggerEnter2D()一类的碰撞事件,无法自动处理类似被墙挡住的效果。

最后就是完全自己实现一套碰撞物理。更加灵活富有效率

计算区域

获取SpriteRenderer中4个点的世界绝对坐标,这样就可以判断相交、重合和计算距离等,这种方法类似于UGUI的RectTransform.GetWorldCorners

新建DrawSpriteRegin脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

public class DrawSpriteRegin : MonoBehaviour
{
public SpriteRenderer spriteRenderer;

private void Update()
{
Vector3 min = spriteRenderer.bounds.min;
Vector3 max = spriteRenderer.bounds.max;

Debug.DrawLine(min, new Vector3(max.x, min.y, 0f), Color.red);
Debug.DrawLine(new Vector3(max.x, min.y, 0f), max, Color.red);
Debug.DrawLine(max, new Vector3(min.x, max.y, 0f), Color.red);
Debug.DrawLine(new Vector3(min.x, max.y, 0f), min, Color.red);
}
}

挂载后,将spriteRenderer拉取进来,运行就可以看到边框

一个sprite的原点默认在中心点,也就是transform.position相对整个图片的坐标。这个原点可以在Sprite Editor中编辑,很多游戏会将这个点放在脚底板的位置上。防止角色高低不同。