Jiahonzheng's Blog

物理系统与碰撞

字数统计: 2.4k阅读时长: 10 min
2019/10/17 Share

游戏世界运动分为两大类:运动学动力学

运动学运用几何学的方法研究物体的运动:

  • 不考虑外部力作用
  • 将一个物体作为几何部件,抽象为质点运动模型
  • 仅考虑物体位置、速度、角度等
  • 具体实现方法:线性代数的矩阵变换

动力学以牛顿运动定律为基础,研究运动速度远小于光速的宏观物体。在游戏物理引擎中,主要是刚体动力学,主要包括质点系动力学的基本定理,由动量定理、动量矩定理、动能定理以及由这三个基本定理推导出来的一些定理。动量动量矩动能是描述质点、质点系和刚体运动的基本物理量。

  • 考虑外部力对物体运动的影响
  • 将一个物体当作刚体
  • 考虑物体的重力、阻力、摩擦力、重量、形状以及弹性等
  • 具体实现方式:物理引擎

物理引擎是一个软件组件,将游戏世界对象赋予现实世界物理属性(重量、形状等),并抽象为刚体模型,使得游戏物体在力的作用下,仿真现实世界的运动及其之间的碰撞过程。

《打飞碟 V2》

项目地址:github.com/Jiahonzheng/Unity-3D-Learning

在线预览:demo.jiahonzheng.cn/UFO-V2

预览视频:Unity 打飞碟 V2

与游戏世界交互 文章中,我们实现了《打飞碟》游戏,在游戏中,我们已使用了动力学模型实现飞碟的运动。现在,我们使用 Adapter 设计模式,为游戏添加运动学模型的支持,实现二者模型的自由切换。

Adapter 接口

首先,我们定义 Adapter 接口 IActionManager ,其含有 SetAction 方法,用于设置游戏对象的运动学模型。

1
2
3
4
public interface IActionManager
{
void SetAction(GameObject ufo);
}

动力学模型

这里,我们对上一版本的动力学模型代码进行重构,具体代码位于 PhysicActionManager

1
2
3
4
5
6
7
8
9
10
11
public class PhysicActionManager : IActionManager
{
public void SetAction(GameObject ufo)
{
var model = ufo.GetComponent<UFOModel>();
var rigidbody = ufo.GetComponent<Rigidbody>();
// 对物体添加 Impulse 力。
rigidbody.AddForce(0.2f * model.GetSpeed(), ForceMode.Impulse);
rigidbody.useGravity = true;
}
}

运动学模型

我们在 CCActionManager 实现运动学模型,具体代码如下。在 SetAction 方法中,我们设置了刚体重力属性,同时对游戏对象添加了 CCAction 脚本。在此脚本中,我们调用 Vector3.MoveTowards 方法赋予游戏对象运动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CCActionManager : IActionManager
{
private class CCAction : MonoBehaviour
{
void Update()
{
// 关键!当飞碟被回收时,销毁该运动学模型。
if (transform.position == UFOFactory.invisible)
{
Destroy(this);
}
var model = gameObject.GetComponent<UFOModel>();
transform.position = Vector3.MoveTowards(transform.position, new Vector3(-3f + model.GetID() * 2f, 10f, -2f), 5 * Time.deltaTime);
}
}

public void SetAction(GameObject ufo)
{
// 由于预设使用了 Rigidbody ,故此处取消重力设置。
ufo.GetComponent<Rigidbody>().useGravity = false;
// 添加运动学(转换)运动。
ufo.AddComponent<CCAction>();
}
}

在游戏实现中,我们在 Ruler 类的 GetUFOs 方法中实现不同关卡下的飞碟对象生成功能,我们使用 actionManager 对飞碟对象的运动学模型进行设置。

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
30
31
// 在用户按下空格键后被触发,发射飞碟。
public List<GameObject> GetUFOs()
{
List<GameObject> ufos = new List<GameObject>();
// 随机生成飞碟颜色。
var index = random.Next(colors.Length);
var color = (UFOFactory.Color)colors.GetValue(index);
// 获取当前 Round 下的飞碟产生数。
var count = GetUFOCount();
for (int i = 0; i < count; ++i)
{
// 调用工厂方法,获取指定颜色的飞碟对象。
var ufo = UFOFactory.GetInstance().Get(color);
// 设置飞碟对象的分数。
var model = ufo.GetComponent<UFOModel>();
model.score = score[index] * (currentRound + 1);
// 设置飞碟对象的缩放比例。
model.SetLocalScale(scale[index], 1, scale[index]);
// 随机设置飞碟的初始位置(左边、右边)。
var leftOrRight = (random.Next() & 2) - 1; // 随机生成 1 或 -1 。
model.SetSide(leftOrRight);
// 设置飞碟的速度比例。
model.SetSpeedScale(speed[index]);
// 设置飞碟 ID 。
model.SetID(i);
// 设置飞碟对象的运动学属性。
actionManager.SetAction(ufo);
ufos.Add(ufo);
}
return ufos;
}

游戏演示

在线预览:

演示视频:

以下是使用运动学模型的游戏截屏。

以下是使用动力学模型的游戏截屏。

《射箭》

项目地址:github.com/Jiahonzheng/Unity-3D-Learning

在线预览:demo.jiahonzheng.cn/Archery/

演示视频:

游戏规则

  • 靶对象为 5 环,按环计分。
  • 按下空格键获取箭,鼠标控制射箭方向,点击鼠标左键发射箭。
  • 游戏只有 1 Round,但有无限 Trials
    • 增强要求:添加一个风向和强度标志,提高难度。

获取箭

我们实现了 ArrowFactory 工厂,用于生成和回收箭对象。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ArrowFactory
{
// 单例。
private static ArrowFactory factory;
// 维护正在使用的箭对象。
private List<GameObject> inUsed = new List<GameObject>();
// 维护未被使用的箭对象。
private List<GameObject> notUsed = new List<GameObject>();
// 空闲箭的空间位置。
public static Vector3 INITIAL_POSITION = new Vector3(0, 0, -19);

// 使用单例模式。
public static ArrowFactory GetInstance()
{
return factory ?? (factory = new ArrowFactory());
}

// 获取一支箭。
public GameObject Get()
{
GameObject arrow;
if (notUsed.Count == 0)
{
arrow = Object.Instantiate(Resources.Load<GameObject>("Prefabs/Arrow"));
inUsed.Add(arrow);
}
else
{
arrow = notUsed[0];
notUsed.RemoveAt(0);
arrow.SetActive(true);
inUsed.Add(arrow);
}
return arrow;
}

// 回收一支箭。
public void Put(GameObject arrow)
{
arrow.GetComponent<Rigidbody>().isKinematic = true;
arrow.SetActive(false);
arrow.transform.position = INITIAL_POSITION;
notUsed.Add(arrow);
inUsed.Remove(arrow);
}
}

控制箭方向

我们在 GameController 捕捉用户的鼠标位置,从而控制箭的方向。

1
2
3
var direction = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
// 控制箭的方向为鼠标指针方向。
MoveArrow(direction);

我们在 MoveArrow 方法中,对箭的方向进行调整。

1
2
3
4
5
// 控制箭的指向。
public void MoveArrow(Vector3 direction)
{
holdingArrow.transform.LookAt(30 * direction);
}

碰撞检测

当用户点击鼠标左键后,会触发 ShootArrow 方法,从而实现射箭的动作。在此方法中,我们设置了箭的 isKinematic 属性为 false ,同时为其添加属性为 Impulse 的力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 发射箭。
public void ShootArrow(Vector3 direction)
{
var collider = holdingArrow.GetComponentInChildren<ArrowCollider>();
// 重置箭的击中状态。
collider.Reset();
// 设置箭击中物体后的回调函数。
collider.onArrowHitObject = (sender, e) =>
{
OnArrowHitObject(e);
};
// 添加 Impulse 力。
var rigidbody = holdingArrow.GetComponent<Rigidbody>();
rigidbody.isKinematic = false;
rigidbody.AddForce(30 * direction, ForceMode.Impulse);
holdingArrow = null;
// 设置游戏状态。
model.scene = SceneState.Shooting;
}

我们的箭对象的层次结构如下,注意到 Head 添加了 ArrowCollider 脚本。

我们的箭靶对象的层次结构如下,由 5 个 Cylinder 构成,注意到它们都已设置了 target 标签。

ArrowCollider 脚本中,我们实现了 OnTriggerEnter 方法,用于判断与箭触碰的游戏对象类型。

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
30
public class ArrowCollider : MonoBehaviour
{
public EventHandler<ArrowHitObjectEvent> onArrowHitObject;
public bool isHitTarget = true;

public void Reset()
{
isHitTarget = true;
}

void OnTriggerEnter(Collider other)
{
var otherObject = other.gameObject;
var arrow = gameObject.transform.parent.gameObject;
// 当箭击中箭靶时。
if (otherObject.tag == "target")
{
arrow.GetComponent<Rigidbody>().isKinematic = true;
gameObject.SetActive(false);
int target = int.Parse(otherObject.name);
isHitTarget = true;
onArrowHitObject.Invoke(this, new ArrowHitObjectEvent(arrow, target));
}
else // 当箭击中其他物体时。
{
isHitTarget = false;
onArrowHitObject.Invoke(this, new ArrowHitObjectEvent(arrow, 0));
}
}
}

我们使用了 EventHandler 实现 ArrowColliderGameController 的通信。首先,我们实现了 ArrowHitObjectEvent 类,用于定义通讯内容,其具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
public class ArrowHitObjectEvent : EventArgs
{
public GameObject arrow;
public int target;

public ArrowHitObjectEvent(GameObject arrow, int target)
{
this.arrow = arrow;
this.target = target;
}
}

随后,我们在 GameController 实现对此事件的响应。

1
2
3
4
5
6
7
8
9
10
11
// 当箭命中物体时,此函数被触发执行。
void OnArrowHitObject(ArrowHitObjectEvent e)
{
model.AddScore(e.target);
// 只有当 target 不为零时,才是击中箭靶,否则只是击中箭。
if (e.target != 0)
{
arrows.Remove(e.arrow);
model.scene = SceneState.WaitToGetArrow;
}
}

风向与风速

为了实现随机风向和风速的生成,我们需要扩充现有的 GameModel 的逻辑,更改代码如下。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
public class GameModel
{
public SceneState scene = SceneState.WaitToGetArrow;
public int score = 0;
public EventHandler<GameModelChangedEvent> onGameModelChanged;
public Wind currentWind = new GameModel.Wind(Vector3.zero, 0, "");
// 风向。
private Vector3[] winds = new Vector3[8] { new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(1, 0, 0), new Vector3(1, -1, 0), new Vector3(0, -1, 0), new Vector3(-1, -1, 0), new Vector3(-1, 0, 0), new Vector3(-1, 1, 0) };
// 风向描述。
private string[] windsText = new string[8] { "↑", "↗", "→", "↘", "↓", "↙", "←", "↖" };

public class Wind
{
public Vector3 direction;
public int strength;
public string text;

public Wind(Vector3 d, int s, string t)
{
direction = d;
strength = s;
text = t;
}
}

// 添加分数
public void AddScore(int target)
{
score += target;
onGameModelChanged.Invoke(this, new GameModelChangedEvent(score, target));
}

// 添加随机风向。
public void AddWind()
{
var index = UnityEngine.Random.Range(0, 8);
var strength = UnityEngine.Random.Range(5, 10);
currentWind.direction = winds[index] * strength;
currentWind.strength = strength;
currentWind.text = windsText[index];
}
}

我们定义了 GameModel.Wind 的类,用于表示当前风向和风速。我们期望在玩家按下空格键获取时,风向得到改变,因此我们在 GetArrow 方法添加 AddWind 调用。

1
2
3
4
5
6
7
8
9
10
11
// 获取箭。
public void GetArrow()
{
holdingArrow = arrowFactory.Get();
arrows.Add(holdingArrow);
// 设置风向和风速。
model.AddWind();
view.ShowWind(model.currentWind);
// 设置游戏状态。
model.scene = SceneState.WaitToShootArrow;
}

在射出箭时,我们需要将风力反应到箭的运动上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 发射箭。
public void ShootArrow(Vector3 direction)
{
var collider = holdingArrow.GetComponentInChildren<ArrowCollider>();
// 重置箭的击中状态。
collider.Reset();
// 设置箭击中物体后的回调函数。
collider.onArrowHitObject = (sender, e) =>
{
OnArrowHitObject(e);
};
// 添加 Impulse 力。
var rigidbody = holdingArrow.GetComponent<Rigidbody>();
rigidbody.isKinematic = false;
rigidbody.AddForce(30 * direction, ForceMode.Impulse);
holdingArrow = null;
// 添加风力
rigidbody.AddForce(5 * model.currentWind.direction, ForceMode.Force);
// 设置游戏状态。
model.scene = SceneState.Shooting;
}

游戏演示

在线预览:demo.jiahonzheng.cn/Archery/

演示视频:www.bilibili.com/video/av71662069/

CATALOG
  1. 1. 《打飞碟 V2》
    1. 1.1. Adapter 接口
    2. 1.2. 动力学模型
    3. 1.3. 运动学模型
    4. 1.4. 游戏演示
  2. 2. 《射箭》
    1. 2.1. 游戏规则
    2. 2.2. 获取箭
    3. 2.3. 控制箭方向
    4. 2.4. 碰撞检测
    5. 2.5. 风向与风速
    6. 2.6. 游戏演示