抛射体运动在游戏开发中的实践

前言

Hi,我是陈皮皮。

概述

主线任务

本文的主线任务(核心主题)是「抛射体运动(Projectile Motion)」。

抛射体运动常常出现在我们的日常生活中,例如篮球运动员投篮后篮球在空中的运动。

在各种电子游戏中也可以看到抛射体运动的身影,例如《坦克世界》中坦克射击后炮弹在空中的运动。

如你所见,本文的标题是《抛射体运动在游戏开发中的实践》,我们将一起推导抛射体运动的各种公式,并在游戏中应用这些公式,实现各种有趣的功能。

支线任务

除了主线任务,本文还包括一些支线任务:

  • 3D 游戏中的对象交互与射线投射
  • 三维空间中矢量的点乘与叉乘(线性代数)
  • 三维空间中矢量在平面上的投影计算
  • 三维空间中矢量的有向角计算
  • 实时预绘制炮弹的运动轨迹

成果

效果展示

先给大家看下我们要实现的效果。

在线预览:https://app.chenpipi.cn/cocos-case-projectile/

动图:

固定角度和速度

固定角度

固定速度

不固定角度和速度

示例项目

与本文一同出现的示例项目「炮弹投射(Projectile)」为开源项目。

为了避免文章又臭又长,项目中的一些功能特性没有在文章中介绍,同时我会对项目代码进行整合再插入文内,所以实际的项目结构与代码和文章中出现的会有些许差异。

下载源码:

本项目使用的游戏引擎为 Cocos Creator 3.4.2。

正文

场景搭建

先来一段轻松愉快的搭积木体验。

环境

快速搭建一个简单的场景,放置一些五颜六色且高矮不一的障碍物(或者说是平台)。

记得给这些东西都加上合适的碰撞器(Collider),这样我们才能够通过射线和它们进行交互。

儿童乐园

🤪 实际上调整各种形状的尺寸和颜色花了大半天时间…

大炮

使用内置的几个基本形状组装了一门大炮(Cannon),尽管它看起来像个手电筒(🔦),但是我还是愿意称它为大炮!

大炮

大号手电筒

🤪 我真的尽力了,花了老半天时间呢,不过看久了其实还挺顺眼的…

简单介绍一下这门大炮的几个关键部件:

  • 让炮身可以左右旋转的偏航轴(Yaw Axis)

    大炮的偏航轴

  • 让炮管可以上下调整的俯仰轴(Pitch Axis)

    大炮的俯仰轴

  • 决定炮弹发射位置和方向的发射点(Fire Point)

    大炮的炮弹发射点

炮弹

使用内置的球体制作一个简单的炮弹(Bullet),保存为单独的预制体(Prefab)。

并且给炮弹添加一个球体碰撞器(Sphere Collider)和刚体(RigidBody)组件,这样炮弹就能拥有物理特性了。

另外有一点要注意的是,刚体组件的**线性速度衰减(LinearDamping)**项默认值为 0.1,我们需要将其设为 0,否则炮弹在飞行的时候会自动减速。

炮弹

基础功能

用代码实现一些基础的功能。

瞄准与射击

Cannon

创建一个名为 Cannon 的组件脚本,用来实现大炮的各种功能。

我们先来简单实现「瞄准(Aim)」和「射击(Shoot)」的功能:

大炮的基础函数

目前大炮的瞄准函数内只实现了俯仰角的更新。

GameController

创建一个名为 GameController 的组件脚本,用于实现游戏的控制逻辑。

实现「点击鼠标发射炮弹」的逻辑:

游戏控制器

🎯 运行效果:

点击鼠标发射炮弹

左右朝向

目前这门大炮只能朝一个固定的方向射击,呆头呆脑的,不如我们根据点击的位置来控制大炮的左右朝向吧。

点击交互与射线投射

我们先来简单了解下如何通过点击屏幕来指定大炮的目标位置。

一般在 2D 游戏中,想要通过鼠标或触摸屏与二维世界中的物体进行交互,只需要通过一个屏幕上的二维坐标就可以判断是否“击中”物体。

而在 3D 游戏中,想要与三维世界中的物体进行交互,则需要通过「射线投射(Raycast)」的方式实现。

简单来说就是从摄像机(Camera)的近裁剪面(Near Plane)发射一条射线(Ray),射线的长度等于近裁剪面到远裁剪面(Far Plane)的距离,这条射线与场景中的物体进行相交运算,如果相交则算作击中物体。

射线投射

大多游戏引擎都会在摄像机上提供「通过屏幕空间上的二维坐标创建射线」的功能。例如 Cocos Creator 的 Camera 组件就提供了 screenPointToRay 函数。

具体的点击功能实现起来非常简单:

射线投射的代码

需要注意的是,场景中的物体要有任意类型的碰撞器(Collider)才能够参与射线检测。

直接看向目标

现在我们已经可以得到一个目标位置了,但是在多数情况下,我们的目标位置与炮身不在同一水平面上,如果让炮身的节点直接看向目标位置,会得到一个不太正常的表现。

另外,我偷偷制作了一个简单的光标(白色箭头和十字准星)来突出当前的目标位置。

大炮直接看向目标位置

注:由于在 Cocos Creator 3.4.2 中,节点的正前方指向的是 -z 方向,所以现在是大炮的屁股看向目标节点。

偏航角

实际上我们只想改变炮身的左右朝向,这里所说的“左右朝向”其实有更专业的术语,称为“偏航(Yaw)”。

🛩 偏航(Yaw)

偏航描述的是物体基于的偏航轴(在游戏开发中通常为物体自身坐标系中的 y 轴)的运动,改变了它指向的方向,到其运动方向的左侧或右侧。

偏航(维基百科)

注:在旋转中,除了“偏航(Yaw)”,通常与之一同出现的还有“俯仰(Pitch)”和“翻滚(Roll)”,分别描述基于另外两个主轴的运动。

想让大炮朝向指定的位置,我们需要改变的正是炮身的偏航角,正确的做法是:

  1. 创建一个从炮身偏航轴节点位置指向目标位置的方向矢量
  2. 将该方向矢量投影到偏航轴节点所在的水平面上
  3. 通过「矢量点乘(Dot Product)」计算方向矢量与大炮的向前矢量之间的夹角

偏航角

注:“偏航轴节点”大炮的子节点,同时它承载了炮身的所有节点,改变偏航轴节点的偏航角就可以改变炮身的朝向了。

“失去方向”

慢着,通过矢量点乘来计算夹角得到的值永远都在 0~180 度之间,也就是说点乘能告诉你两个矢量的最小夹角度数,无法告诉你一个矢量在另一个矢量的左侧还是右侧,因为对于点乘来说“左侧 45 度”和“右侧 45 度”都是“45 度”。

矢量点乘(维基百科)

举个栗子,下图中两个紫色矢量与蓝色矢量的夹角都是 45 度,但是它们分别在蓝色矢量的两侧:

度数相同但是方向不同的夹角

有向角

但是在游戏引擎中,物体的旋转有效度数范围是 0~360,或者说是 -180~180 度(我们都知道“左转 90 度”和“右转 -90 度”、“右转 270 度”所表达的意思是一样的)。

说到底,其实我们就是需要一个带正负符号的夹角,也就是「有向角(Directed Angle)」。

当我们提到“方向”时,线性代数学得溜的同学很快就能想到,我们可以通过「矢量叉乘(Cross Product)」来判断一个矢量在另一个矢量的左侧还是右侧。

矢量叉乘(维基百科)

另外,在三维空间中矢量的有向角计算还需要有一个参照平面才有意义。

具体的计算步骤:

  1. 将两个矢量投影到同一参照平面上
  2. 通过叉乘求出两个矢量的法矢量
  3. 通过该法矢量在参照平面法矢量上的投影长度得到方向
  4. 使用点乘计算两个矢量的夹角
  5. 对夹角应用方向(符号)

计算有向角的函数

关于「将矢量投影到平面上」的函数:

将矢量投影到平面上的函数

注:在三维世界中,使用一个方向矢量来充当平面法线就能够表示一类朝向相同的平面,因为“平面法线”意味着该方向矢量一定垂直于其所表示的平面。

代码实践

解决了偏航角的问题后,现在就给大炮加上「朝向目标位置」的代码:

大炮的瞄准函数

注:在 Cocos Creator 3.4.2 中节点的旋转符合右手法则,即逆时针方向为正方向。

然后在 GameController 中实现「移动鼠标控制大炮瞄准」的逻辑:

游戏控制器

🎯 阶段性成果:

大炮的朝向

射击模式

现在我们的大炮已经可以跟着鼠标 360 度旋转和发射炮弹了,但是炮弹发射的俯仰角度和速度都是固定的,这看起来一点都不高级。

而我们期望是:给出一个目标位置,大炮能够自行计算发射角度和速度。

针对这个目标,我们可以设计 3 种不同的模式:

  • 固定发射角度,动态计算炮弹的初始速度
  • 固定炮弹的初始速度,动态计算发射角度
  • 动态计算发射角度和炮弹的初始速度

再加上目前「固定发射角度和炮弹的初始速度」的模式,我们总共有 4 种模式。

接下来我们将一一实现它们。

固定角度和速度

由于新增了模式的概念,为了让后面的编码更方便更优雅,我们把目前的代码结构稍作调整。

Cannon 组件的 aim 函数中根据模式来计算炮弹的发射角度和速度:

大炮的瞄准函数

抛射体运动

虽然前面阿巴阿巴了这么多,但是似乎都和我们的主题抛射体运动没啥关系。

简单介绍

我们先来说说什么是抛射体运动?

抛射体运动是以任意初速抛出的物体在地球重力作用下的运动。

抛射体运动可以分为平抛运动和斜抛运动:

  • 平抛运动(Horizontal Projectile Motion):物体的出射方向与水平面的夹角为 0,所以物体只有水平方向上的初速度(也可以看作是垂直方向上的初速度为 0)。

    平抛运动

  • 斜抛运动(Oblique Projectile Motion):物体的出射方向与水平面的有一定的夹角,所以物体不但有水平方向上的初速度,也有垂直方向上的初速度。

    斜抛运动

一般情况下,当我们提到抛射体运动时,指的都是斜抛运动,因为斜抛运动“兼容”平抛运动~

另外,抛射体运动也常被称作「抛物运动」,我觉得抛物运动这名字更顺口一点。

基本公式

👾 欢迎来到主线前置任务!

在深入之前,让我们先来学习(复习)一些简单的前置知识。

首先,我们来认识一下将会在各种公式中出现的成员们:

  • s s s - 位移(Displacement)
  • x x x - 水平位移(Horizontal Displacement)
  • y y y - 垂直位移(Vertical Displacement)
  • h h h - 最大高度(Max Height)
  • θ θ θ - 初始角度(Initial Angle)
  • v v v - 初始速度(Initial Velocity)
  • t t t - 时间(Time)
  • g g g - 重力加速度(Gravitational Acceleration)

注:我们默认认为初始速度 v v v 是有方向的,对应的是初始角度 θ θ θ

注:标准重力加速度 g g g 的值为 9.80665,但在游戏引擎中一般默认为 9.8 或 10。在 Cocos Creator 3.4.2 中重力加速度的默认值为 10。

注:本文所讨论的抛射体运动均为「理想的抛射体运动」。

自由落体位移公式

当物体只受重力影响时的位移计算公式。

s = 1 2 ∗ g ∗ t 2 (自由落体位移公式) s = \frac{1}{2} * g * t^2 \tag{自由落体位移公式} s=21gt2()

水平位移公式

在抛射体运动中,物体在水平方向上只受初速度影响。

也就是说物体水平速度恒等于初速度的水平分量,即 v ∗ cos ⁡ θ v * \cos{θ} vcosθ

x = v ∗ cos ⁡ θ ∗ t (水平位移公式) x = v * \cos{θ} * t \tag{水平位移公式} x=vcosθt()

注:在水平位移的计算中我们以“右”为正方向。

垂直位移公式

在抛射体运动中,物体在垂直方向上受初速度和重力加速度影响。

也就是说,物体其实同时存在有两种垂直方向位移:

  • 受初速度垂直分量影响的位移
  • 受重力影响的自由落体位移

y = v ∗ sin ⁡ θ ∗ t − 1 2 ∗ g ∗ t 2 (垂直位移公式) y = v * \sin{θ} * t - \frac{1}{2} * g * t^2 \tag{垂直位移公式} y=vsinθt21gt2()

注:我们在垂直位移的计算中以“上”为正方向,所以自由落体位移是负值。

高级模式

😈 欢迎来到主线任务!

固定角度

现在来实现第二种模式:固定一个发射角度,根据目标位置动态计算炮弹的初始速度。

在该模式下,大炮的发射角度固定,给出一个三维空间中的目标点,我们需要计算出炮弹发射的初速度。

计算水平和垂直位移

有了目标点的位置,并且我们本就知道发射点的位置,那我们就可以计算出从发射点到目标点的水平位移和垂直位移。

抛射体运动的位移

将两个点都投影到同一水平面上后,两个点的距离就是水平距离;而发射点和目标点之间的 y 值之差就是它们的垂直距离。

位移

实际的计算过程如下:

  1. 使用矢量减法得到从发射点到目标点的方向矢量
  2. 方向矢量的 y 值就是垂直距离
  3. 将方向矢量投影到水平面上后,其长度就是水平距离

计算位移的函数

注:在游戏开发中,想要表示水平面,最简单的方式就是使用一个方向垂直向上的方向矢量作为平面法线,比如一个值为 { x: 0, y: 1, z: 0 } 的方向矢量。

注:在 Cocos Creator 3.4.2 中 Vec3.UP = new Vec3(0, 1, 0)

初始速度公式

咳咳,回到正题。

来看下现在我们拥有的信息:

  • 水平位移( x x x
  • 垂直位移( y y y
  • 初始角度( θ θ θ

而我们想要求的是:

  • 初始速度( v v v

再看一眼上文中的「水平位移公式」和「垂直位移公式」,我们似乎还缺少一个关键的信息:

  • 时间( t t t

问题不大!我们可以试着消除掉这个时间项( t t t)。

首先,我们可以轻易地从「水平位移公式」得到「水平位移时间公式」:

x = v ∗ cos ⁡ θ ∗ t (水平位移公式) x = v * \cos{θ} * t \tag{水平位移公式} x=vcosθt()

↓ \downarrow

t = x v ∗ cos ⁡ θ (水平位移时间公式) t = \frac{x}{v * \cos{θ}} \tag{水平位移时间公式} t=vcosθx()

然后将这个「水平位移时间公式」代入「垂直位移公式」:

y = v ∗ sin ⁡ θ ∗ x v ∗ cos ⁡ θ − 1 2 ∗ g ∗ ( x v ∗ cos ⁡ θ ) 2 y = v * \sin{θ} * \frac{x}{v * \cos{θ}} - \frac{1}{2} * g * \left({\frac{x}{v * \cos{θ}}}\right)^2 y=vsinθvcosθx21g(vcosθx)2

⇓ \Downarrow

y = x ∗ v ∗ sin ⁡ θ v ∗ cos ⁡ θ − 1 2 ∗ g ∗ ( x v ∗ cos ⁡ θ ) 2 y = \frac{x * v * \sin{θ}}{v * \cos{θ}} - \frac{1}{2} * g * \left({\frac{x}{v * \cos{θ}}}\right)^2 y=vcosθxvsinθ21g(vcosθx)2

⇓ \Downarrow

y = x ∗ tan ⁡ θ − 1 2 ∗ g ∗ x 2 v 2 ∗ cos ⁡ 2 θ (垂直位移公式 2) y = x * \tan{θ} - \frac{1}{2} * g * \frac{x^2}{v^2 * \cos^2{θ}} \tag{垂直位移公式 2} y=xtanθ21gv2cos2θx2( 2)

好家伙,成功消除掉时间项( t t t)了,得到了一个新的垂直位移公式,为了区分我们把它命名为「垂直位移公式 2」吧。

最后我们再试着“孤立”公式中的初始速度项( v v v):

y = x ∗ tan ⁡ θ − 1 2 ∗ g ∗ x 2 v 2 ∗ cos ⁡ 2 θ (垂直位移公式 2) y = x * \tan{θ} - \frac{1}{2} * g * \frac{x^2}{v^2 * \cos^2{θ}} \tag{垂直位移公式 2} y=xtanθ21gv2cos2θx2( 2)

↓ \downarrow

0 = x ∗ tan ⁡ θ − g ∗ x 2 2 ∗ v 2 ∗ cos ⁡ 2 θ − y 0 = x * \tan{θ} - \frac{g * x^2}{2 * v^2 * \cos^2{θ}} - y 0=xtanθ2v2cos2θgx2y

↓ \downarrow

g ∗ x 2 2 ∗ v 2 ∗ cos ⁡ 2 θ = x ∗ tan ⁡ θ − y \frac{g * x^2}{2 * v^2 * \cos^2{θ}} = x * \tan{θ} - y 2v2cos2θgx2=xtanθy

↓ \downarrow

1 2 ∗ v 2 ∗ cos ⁡ 2 θ = x ∗ tan ⁡ θ − y g ∗ x 2 \frac{1}{2 * v^2 * \cos^2{θ}} = \frac{x * \tan{θ} - y}{g * x^2} 2v2cos2θ1=gx2

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值