原文地址:Collision detection and physics
使用JavaScript执行碰撞检测并产生物理反应。检查图形之间是否重叠,应用Hitbox并计算新速度。通过物体的质量,重力和恢复力使其更自然。 在本教程结束时,您将在游戏中运行基本的物理模拟。
创建一些移动的对象
如果您已经知道如何创建运动对象,并且只对检测碰撞或物理感兴趣,请向下滚动至下一部分。
在检测运动物体之间的碰撞之前,首先需要一些物体。 在上一教程中,您学习了如何移动单个矩形。让我们扩展这种逻辑,并创建一堆移动对象来填充游戏。首先,定义一种新型的游戏对象。这将是一个简单的正方形。
class Square extends GameObject
{
// Set default width and height
static width = 50;
static height = 50;
constructor (context, x, y, vx, vy){
super(context, x, y, vx, vy);
}
draw(){
// Draw a simple square
this.context.fillStyle = this.isColliding?‘#ff8080‘:‘#0099b0‘;
this.context.fillRect(this.x, this.y, Square.width, Square.height);
}
update(secondsPassed){
// Move with set velocity
this.x += this.vx * secondsPassed;
this.y += this.vy * secondsPassed;
}
}
这段代码可能看起来有点熟悉。 就像上一教程中一样,有一个draw()和update()函数。这次,它被提取到一个单独的正方形class中。这样,您可以创建一个正方形的许多实例,并且它们都使用相同的逻辑进行绘制和更新。您将很容易管理正方形的行为和外观。
对该新类中的fillStyle进行了一些调整。当该对象碰撞时,它将颜色从蓝色更改为红色。当检测到第一个碰撞时,您将看到此动作。目前,所有方块均为蓝色。
所有方块都继承自GameObject类。每个游戏对象都有一个位置和速度。这使您可以轻松创建新型的游戏对象。他们继承了GameObject类的属性和方法。正方形只是一个例子,但是您也可以通过这种方式为游戏制作敌人或玩家之类的对象。
class GameObject
{
constructor (context, x, y, vx, vy){
this.context = context;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.isColliding = false;
}
}
您可以使用new关键字创建类的新实例。使用此createWorld()函数创建一些正方形以填充您的游戏世界。
let gameObjects;
function createWorld(){
gameObjects = [
new Square(context, 250, 50, 0, 50),
new Square(context, 250, 300, 0, -50),
new Square(context, 150, 0, 50, 50),
new Square(context, 250, 150, 50, 50),
new Square(context, 350, 75, -50, 50),
new Square(context, 300, 300, 50, -50)
];
}
在函数中,创建了一堆正方形。他们将位置和速度作为参数传递给他们。目前,此函数是不会变化的,但是您可以轻松地对其进行修改以创建更多随机的正方形或使用某些生成算法。
现在一切就绪,可以绘制正方形。使用以下代码更新游戏循环,以遍历新创建的游戏对象并将它们绘制在屏幕上。
function gameLoop(timeStamp)
{
secondsPassed = (timeStamp - oldTimeStamp) / 1000;
oldTimeStamp = timeStamp;
// Loop over all game objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].update(secondsPassed);
}
clearCanvas();
// Do the same to draw
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].draw();
}
window.requestAnimationFrame(gameLoop);
}
如您所见,update()和draw()不再每次迭代调用一次。屏幕上的每个对象,每次迭代都调用一次。
这样,update()和draw()的实现是特定于对象的。对于游戏循环,您尝试绘制哪种对象都没有关系,只要它们具有update()和draw()函数即可。
对于您正在使用的正方形,它将绘制一个简单的正方形并将其沿直线移动。但是,请想象其他类型的对象,它们具有两个功能的自己的实现,并且具有自己的行为和外观。这个游戏循环可以处理它。
顺便说一句,您是否注意到这些新类中缺少“use strict”这一行?这是因为默认情况下,使用class关键字定义的类是严格的。因此,无需在这些类中专门添加“use strict”。
看一下结果:
[canvas]
您可以看到现在绘制了一堆矩形。他们每个人都有自己的出发位置,并朝着不同的方向前进。就像在createWorld()函数中定义的一样。您可以调整变量以创建新的正方形类型。
为什么需要碰撞检测?
正方形的运动可能重叠,但是目前并没有太大作用。如果正方形可以相互作用并表现得像实际的固体对象并彼此反弹,那将很酷。为了做到这一点,他们必须首先知道自己正在相互碰撞。这就是碰撞检测的用武之地。
碰撞检测是一种检测两个对象是否彼此碰撞的技术,或者是从现在到最后一帧之间是否发生碰撞。这是在游戏中实现物理的第一步。
检查物体之间的碰撞
正方形在屏幕上移动,但是没有相交的形态。就像他们没有注意到对方。 让我们为此做些事情。
您将检查运动对象之间是否存在碰撞。这就需要您遍历所有对象,并检查它们中的任何一个是否重叠。为此,您需要嵌套循环。 看一下示例:
function detectCollisions(){
let obj1;
let obj2;
// Reset collision state of all objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].isColliding = false;
}
// Start checking for collisions
for (let i = 0; i < gameObjects.length; i++)
{
obj1 = gameObjects[i];
for (let j = i + 1; j < gameObjects.length; j++)
{
obj2 = gameObjects[j];
// Compare object1 with object2
if (rectIntersect(obj1.x, obj1.y, obj1.width, obj1.height, obj2.x, obj2.y, obj2.width, obj2.height)){
obj1.isColliding = true;
obj2.isColliding = true;
}
}
}
}
检查所有对象是否彼此相交。第二个for循环更智能,并且跳过所有先前已经检查过的项。您不必检查对象两次。如果它们第一次重叠,那么第二次也会重叠。当然,您不必对照自身检查对象,因为它和自身始终会重叠。
该函数为每个对象组合调用rectIntersect()。当发现冲突时,它将两个相关对象的isColliding设置为true。
还记得正方形里的draw()函数吗?它将对isColliding做出反应,并以其他颜色绘制正方形。您可以轻松看到两个对象何时重叠。
您什么时候检查碰撞?
与draw()方法一样,您要在检查碰撞之前先更新所有游戏对象的位置。这样,您将始终检查处于最新状态的重叠对象。如果以其他方式进行操作并在更新之前检查冲突,则将检查前一帧的状态是否重叠。您将始终不能执行真实情况。
另一个选择是按照正确的顺序进行碰撞检查,但是要反复进行。您将更新对象-a,检查对象-a是否与所有其他对象重叠,更新对象-b,检查对象-b与所有其他对象重叠,依此类推。这不是进行碰撞检查的正确方法。想象一下,在更新对象a的位置后,对象a将与对象b发生碰撞。即使对象b也会先移动,系统也可能会检测到碰撞。因此,在进行碰撞检查之前,您必须先更新所有对象。
游戏循环的正确顺序是更新,碰撞检查,清除画布,绘制。因此,将catchCollisions()函数放在更新所有游戏对象的循环之后。您的总游戏循环现在看起来像这样:
矩形之间的碰撞检测
最后的难题是rectIntersect()方法。您可以使用它来检查两个矩形是否重叠。检查两个与轴对齐(未旋转)的矩形之间的重叠非常简单且直接。您可能会想出一种通过使用矩形的位置和大小来检查两个轴上是否重叠的方法。有很多方法可以执行此操作,但是下一个方法非常有效:
rectIntersect(x1, y1, w1, h1, x2, y2, w2, h2) {
// Check x and y for overlap
if (x2 > w1 + x1 || x1 > w2 + x2 || y2 > h1 + y1 || y1 > h2 + y2){
return false;
}
return true;
}
该代码可以检测到明显重叠一半的矩形,在一个小矩形完全落入一个大矩形的情况下也可以使用。
有了这段代码,您最终可以检查出结果。这里又是正方形,但是这次它们相互响应。
[canvas]
检测到碰撞后,将isColliding属性设置为true。这使正方形以红色绘制。 您现在可以清楚地看到两个对象何时重叠。
检查两个圆是否重叠
您现在有了一种检查未旋转矩形之间的碰撞的方法。但是,如果您想对圆做同样的事情怎么办?当然,那也不难。
假设您有两个圆,每个圆都有自己的半径。它们之间的距离很远。如果距离小于两个圆的半径之和,则这些圆将重叠。由于圆是圆形的,因此甚至在旋转对象时也可以使用,因此不必将其与轴对齐。
计算两点之间的距离
您可以使用以下公式计算两点之间的距离:
c = sqrt((x1 - x2)2 + (y1 - y2)2)
如果您将Δx和Δy视为三角形的两个边,则它基本上会应用勾股定理来计算点之间的直线距离, 为c。
因此,如果此距离小于或等于圆a的半径加圆b的半径,则这些圆会重叠或接触。下一个函数使用此原理:
circleIntersect(x1, y1, r1, x2, y2, r2) {
// Calculate the distance between the two circles
let squareDistance = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);
// When the distance is smaller or equal to the sum
// of the two radius, the circles touch or overlap
return squareDistance <= ((r1 + r2) * (r1 + r2))
}
如您所见,对公式进行了一些调整。数字直接相乘比使用Math.sqrt()获得平方根要快得多,因此,在不获取根的情况下计算距离,且半径之和与自身相乘。结果保持不变,但是性能更好。
这是与以前相同的示例,但是这次是圆:
[canvas]
其他图形呢?
在本文中,仅对两种形状进行了碰撞检测。但是,如果您的游戏对象由其他更复杂的形状甚至图像组成,并且您想在它们之间进行碰撞检查,该怎么办?
好吧,对于几何形状,您可以找到其他公式来检测两个对象何时重叠。 这是一个涵盖许多不同形状的碰撞检测的网站。总体而言,更复杂的形状使碰撞检测更加困难。对于图像,您可以应用像素完美碰撞检测。不利的一面是,这是一个占用大量CPU的操作。想象一下,必须将每个像素彼此匹配,这将是一项艰巨的工作。
这就是为什么为了使事情变得简单并减轻系统负担,开发人员经常使用Hitbox来检测形状复杂的游戏对象之间的碰撞。这是一种使碰撞检测更容易并且仅使用基本几何形状的方法,例如本教程中介绍的矩形和圆形。因此,在开始构建对各种复杂形状的支持之前,请尝试考虑一种使用基本形状和Hitbox实现相同效果的简单方法。
什么是Hitbox,如何使用它们?
Hitbox是游戏对象周围的虚构几何形状,用于确定碰撞检测。想象您有一个游戏角色。您无需检查其手臂和腿部是否发生碰撞,而只需检查围绕在玩家周围的大的假想的矩形即可。
您可以简单地将函数用于矩形碰撞检测(如前所述),以检查hitboxes是否存在碰撞。它占用的CPU少得多,并且使在游戏中支持复杂形状变得更加容易。在某些特殊情况下,每个游戏对象甚至可以使用多个Hitbox。它依然胜过像素的完美解决方案。
上图演示了冲突检测的不同类型。 它们各有优点和缺点:
- 完美的像素法-超精确的碰撞检测,但需要一些严肃的系统资源。在大多数情况下,这太过分了。
- Hitbox-更好的性能,但是碰撞检测可能非常不精确。不过,在许多游戏场景中,这并不重要。
- 多个Hitbox-比单个Hitbox效率低,但仍胜过像素完美变体。并且您可以支持复杂的形状。对于需要一些额外精度的重要游戏对象,例如上面提到的四肢玩家,这是一个不错的选择。您可以为核心创建一个Hitbox,为手臂,腿部和头部分离一个Hitbox。
示例图像是来自游戏Pixi Pop的实际游戏资源。在游戏中,当精确的碰撞检测很重要并且没有运行太多其他游戏任务时,才使用资源。因此,在这种情况下,可以选择与多个hitboxes一起使用。只需选择最适合您的游戏场景的选项即可。
对物理碰撞做出响应
现在,您有了可以检测碰撞并更改颜色的游戏对象。但是,如果物体像现实生活中的物体那样相互反弹,会不会更酷呢?现在是时候将一些物理学应用于您的游戏了。
要更改移动物体的速度,您需要找出碰撞发生的方向和速度。然后,您可以将速度更改应用于碰撞的对象。此原理适用于矩形和圆形。
找到碰撞的方向和速度
想象两个游戏对象之间的下一次碰撞。这两个对象都有自己的速度和方向。他们不会直接撞到对方,而是在自己的路线上碰巧会引起碰撞。
您需要找出碰撞的速度和方向,以便将其应用于游戏对象的速度。首先为发生的碰撞创建矢量。该矢量仅是两个碰撞对象之间的x和y之差。您可以将其视为带有长度和方向的箭头。对于矢量,长度也称为幅度。像这样计算碰撞矢量:
let vCollision = {x: obj2.x - obj1.x, y: obj2.y - obj1.y};
在两个游戏对象的示例中,碰撞矢量如下所示:
在这种情况下,大小等于两个碰撞对象之间的距离。它与速度无关。但是您可以使用向量的方向。要获得方向,您需要除去距离的因素。
我们首先计算碰撞矢量的距离。您可以使用之前相同的公式来计算两个碰撞圆之间的距离。因此,代码变为:
let distance = Math.sqrt((obj2.x-obj1.x)*(obj2.x-obj1.x) + (obj2.y-obj1.y)*(obj2.y-obj1.y));
现在使用距离来计算归一化的碰撞向量。您基本上删除了距离作为碰撞矢量中的一个因素,因此只剩下一个方向。碰撞范数与碰撞向量的方向相同,只是norm/magnitude/length与1比较大小。也将其称为单位向量。您可以像这样计算归一化向量:
let vCollisionNorm = {x: vCollision.x / distance, y: vCollision.y / distance};
基本上,这只会为您提供碰撞方向。在两个游戏对象的示例中,它将如下所示:
您现在有一个方向。这是发生碰撞的方向。现在,您所需要的只是碰撞速度,您将能够计算碰撞如何影响物体的速度。您可以像这样计算碰撞速度:
let vRelativeVelocity = {x: obj1.vx - obj2.vx, y: obj1.vy - obj2.vy};
let speed = vRelativeVelocity.x * vCollisionNorm.x + vRelativeVelocity.y * vCollisionNorm.y;
作为示例代码中的第一行,将使用对象的相对速度创建另一个矢量。就像您要使其中一个游戏对象静止时所要留下的向量。(您可以在此处详细了解相对速度。)在下一个示例中更容易理解。两个游戏对象的向量彼此重叠显示,因此您可以看见相对速度向量:
相对速度向量与碰撞法线一起用于计算两个向量的点积。点积是相对速度在碰撞法线上的投影长度。换句话说,就是速度向量在碰撞方向上的长度。在此处可详细了解dot products。在此处了解有关矢量操作的更多信息。
点积等于碰撞速度。就是这样,您有了两个对象之间碰撞的速度和方向。您可以将其应用于游戏对象的速度,并使它们彼此反弹。
改变运动物体的速度
碰撞速度可以为正或负。当它为正时,对象正在朝彼此移动。当结果为负数时,他们会离开。当物体移开时,无需执行任何其他操作。他们将自行摆脱冲突。
if (speed < 0){
break;
}
对于另一种情况,当对象彼此靠近时,请沿碰撞方向施加速度。两个物体从碰撞中获得相同的速度变化。将速度减去或加到两个碰撞对象的速度上。
obj1.vx -= (speed * vCollisionNorm.x);
obj1.vy -= (speed * vCollisionNorm.y);
obj2.vx += (speed * vCollisionNorm.x);
obj2.vy += (speed * vCollisionNorm.y);
就是这样,通过将速度应用于方向,您可以计算出碰撞速度。现在,该速度将以所涉及对象的速度进行处理。您的游戏对象应该以自然的方式反弹。
现在看一下结果:
[canvas]
增加质量,脉冲和动力
如果愿意,您可以进一步应用物理学,并根据速度计算碰撞脉冲,从而将质量纳入方程式。使用冲量来计算动量。重物会将轻物推开。
let impulse = 2 * speed / (obj1.mass + obj2.mass);
obj1.vx -= (impulse * obj2.mass * vCollisionNorm.x);
obj1.vy -= (impulse * obj2.mass * vCollisionNorm.y);
obj2.vx += (impulse * obj1.mass * vCollisionNorm.x);
obj2.vy += (impulse * obj1.mass * vCollisionNorm.y);
如果您有两个质量为1的物体,则脉冲刚好等于速度。在其他情况下,您基本上将速度分为许多小部分。重物从动量中吸收了一部分,轻物得到了很多。这使得较轻的物体受到碰撞的影响更大。
不要忘记为游戏对象增加质量。GameObject类是存储质量的好地方。您可以修改createWorld()函数以通过Circle和Rectangle类将质量作为参数传递。
这是一个经过修改的示例,可以创建许多小圆圈和两个大圆圈。(生成算法不是很聪明,因此对象可能在碰撞中开始)
[canvas]
在该示例中,与小圆圈相比,大圆圈的质量非常大。他们将一切推开。但是,当两个重物相互撞击时,它们也会弹起。
获取对象的运动方向上的顶部
物体不断碰撞并改变方向。对于游戏,准确地知道哪个方向会有所帮助,因此您可以添加旋转的纹理或基于该纹理构建游戏逻辑。让我们计算一下!
通过在x和y速度上使用Math.atan2(),您可以轻松地获得对象的角度。结果以弧度表示,请使用Math.PI将其转换为度。这是一个在update()函数中计算角度的示例:
update(secondsPassed){
// Move with set velocity
this.x += this.vx * secondsPassed;
this.y += this.vy * secondsPassed;
// Calculate the angle (vy before vx)
let radians = Math.atan2(this.vy, this.vx);
// Convert to degrees
let degrees = 180 * radians / Math.PI;
}
您可以稍后在游戏中使用该角度。在本系列的下一个教程中,您将学习如何使用它来绘制旋转的图像。现在,这是一个简单的示例实现,用一点线显示对象的移动方向。
[canvas]
添加重力的影响
本教程中显示的示例仅包含物理学的基本实现。您可以在游戏中添加更多方面,以使其看起来更加自然。引力或弹力之类的事情不太容易实现。现在开始添加重力到模拟中。
对于重力,只需使用重力加速度调整对象的y速度。在地球上,重力加速度大约为9.81。您可以将其应用到游戏对象的update()函数中。每秒将g添加到y速度,这将使对象掉落的速度越来越快。
// Set gravitational acceleration
const g = 9.81;
update(secondsPassed){
// Apply acceleration
this.vy += g * secondsPassed;
// Move with set velocity
this.x += this.vx * secondsPassed;
this.y += this.vy * secondsPassed;
}
更新速度,然后再更新位置。如本文中有关对运动方程的积分所述,这将提供更准确的结果。这种集成称为半隐式欧拉。
限制物体的运动空间
为了使重力效果很好地显示,可以将对象的移动限制在画布的边缘。它会像一个封闭的盒子一样在上面弹起物体。
您可以通过简单的调整使其实现。在主要碰撞检测功能之后立即执行下一个函数,因此将检查对象边缘碰撞以及对象-对象碰撞。
// Define the edges of the canvas
const canvasWidth = 750;
const canvasHeight = 400;
// Set a restitution, a lower value will lose more energy when colliding
const restitution = 0.90;
function detectEdgeCollisions()
{
let obj;
for (let i = 0; i < gameObjects.length; i++)
{
obj = gameObjects[i];
// Check for left and right
if (obj.x < obj.radius){
obj.vx = Math.abs(obj.vx) * restitution;
obj.x = obj.radius;
}else if (obj.x > canvasWidth - obj.radius){
obj.vx = -Math.abs(obj.vx) * restitution;
obj.x = canvasWidth - obj.radius;
}
// Check for bottom and top
if (obj.y < obj.radius){
obj.vy = Math.abs(obj.vy) * restitution;
obj.y = obj.radius;
} else if (obj.y > canvasHeight - obj.radius){
obj.vy = -Math.abs(obj.vy) * restitution;
obj.y = canvasHeight - obj.radius;
}
}
}
基本上,它检查是否位于边缘之外的对象,并将其位置重置为再次落入盒子内。 然后将对象的速度翻转以垂直于墙移动。
这是一个非常基本的实现,只能通过这种方式工作,因为画布的边缘是预先定义的直线。您可以对圆弧线碰撞和设置动态线执行相同的操作,但是比该简单的示例要复杂得多。
实现弹力损耗
如果您现在运行代码,您将看到游戏对象永远不会处于静止状态。他们会不断弹跳,并且永远不会失去任何能量。为了解决这个问题,您可以实施赔偿。
恢复基本上描述了每次碰撞后还剩下多少能量。它对对象的反弹具有影响。弹跳后的开始速度和结束速度之间的比率称为恢复系数或COR。
- COR为0的物体会在撞击时吸收所有能量,例如一袋沙子撞击地板。
- COR为1的物体将具有完美的弹性,例如超级弹力的弹跳球。
- COR > 1的对象完全是虚构的,每次碰撞后都会增加额外的能量。
在前面的编码示例中,COR被应用于与边缘的碰撞。这会使物体在每次反弹后仅损失一点能量。这将使模拟更加逼真,而忽略它会使对象永远反弹。
要完成弹力的实现,您还需要将其应用于涉及对象-对象冲突的对象。只需将它们的速度乘以COR(只需恢复代码即可)。现在,每次碰撞都会消耗一点能量。
当两个物体以不同的恢复设置碰撞时,例如,当弹跳球击中一袋沙子时,最低恢复将计算在内。在这种情况下,弹跳球或沙袋都不会反弹,它们的弹力都被袋子吸收了。
let speed = vRelativeVelocity.x * vecCollisionNorm.x + vRelativeVelocity.y * vecCollisionNorm.y;
speed *= Math.min(obj1.restitution, obj2.restitution);
下一个实时画布示例显示了重力,弹力和撞击的应用。
[canvas]
您可以轻松地调整变量以创建不同的方案。设置较高的重力以模拟在异物行星上的位置或降低其弹力,以使这些物体像吸收所有撞击的沙袋一样起作用。
改善表现的方法
您可能现在还没有真正注意到它,但是如果同时显示许多游戏对象或显示更复杂的形状,则碰撞检测和反应会给您的系统带来很大压力。以下是一些有助于提高性能的提示。它们似乎很明显,但是当游戏变得更加复杂时,很容易忽略其中一些概念。
-
仅比较足够近的对象以至于可能发生碰撞。您可以使用网格系统,或者仅在对象输入特定半径时才检测碰撞。这称为将碰撞检测分为宽相和窄相。在此处了解有关宽相碰撞检测的更多信息。
-
保持对象池清洁。在物体看不见或在游戏中被破坏时清理它们。
-
排除背景/固定物体。有些对象永远不会对碰撞做出反应,因此不要在迭代中包含它们。
-
使用Hitboxs。如前所述,命中盒是优化碰撞检测并简化复杂形状的好方法。
-
调整碰撞检测和物理的实现来适应您的游戏。当您要做的只是井字游戏时,您不需要完整的物理引擎。这是一个非常棒的例子,但是您明白了。剥离逻辑以仅支持所需的内容。
处理快速移动的物体
关于碰撞检测的最后一点。上面的示例通过检查两个对象是否重叠来检测冲突。在许多情况下,这是一个很好的解决方案。但是,当您的对象高速移动时,它将无法正常工作。当速度高于最小对象的大小时,对象就有机会跳过碰撞检查。 他们彼此穿越。
想象一下,您检查游戏中子弹与敌人之间是否有碰撞。第一帧子弹在敌人面前。没有重叠,因此没有碰到物体。下一帧子弹移动得如此之快,它现在位于敌人的后面。仍然没有重叠,因此没有碰撞。但是子弹确实穿过了敌人,应该受到了打击。
这是一张图像,用来演示快速移动的物体(例如子弹)的情况,该物体从未与另一个游戏物体真正重叠,而应该引起了碰撞:
您需要针对这种情况的另一种方法。最简单的方法是限制游戏对象的速度。简而言之,请确保速度永远不会大于最小的游戏对象,以使其无法通过。对于许多类型的游戏来说,这是一个很好的解决方案,并且投入的精力最少。
另一种解决方案是使用投影路径而不是两个对象的当前位置执行碰撞检测。尝试将子弹的路径可视化为一条线。线的长度等于子弹行进的距离。现在,您可以使用线到矩形或线到圆的碰撞检查来发现子弹是否会击中另一个物体。对于大的子弹,可以使用矩形而不是直线。
这是一个简化的解决方案。在此过程中,您可能会遇到其他问题,例如查找影响点或确定首先击中较大集合中的哪个对象。但是这里提到的步骤可能会帮助您指出正确的方向。目前,本教程仅涉及快速移动的对象。
下一步是什么?
碰撞和物理学的所有内容到此为止。您的碰撞检查已经到位,您的游戏对象现在正在以一种半自然的方式进行交互。如果您有任何意见或问题,请随时将其发布在下面的评论部分。
在本教程的下一步中,您将学习如何在游戏中使用图像并创建精灵动画。