2d-collisions 中文文档教程
Detect Collisions
detect-collisions 是一个零依赖性 JavaScript 库,用于快速准确地检测多边形、圆和点之间的碰撞。 它结合了用于宽相搜索的Bounding Volume Hierarchy (BVH) 的效率和 分离轴定理 (SAT) 用于窄相碰撞测试。
- Credits
- Installation
- Documentation
- Demos
- Usage
- Getting Started
- Lines
- Concave Polygons
- Rendering
- Bounding Volume Padding
- Only using SAT
- FAQ
Credits
它最初是从 github.com/Sinova/Collisions 分叉而来
的变化不大,但该项目得到了维护。 演示已更新。
Installation
yarn add detect-collisions --save
# or
npm i detect-collisions --save
Documentation
查看文档(此自述文件也在那里)。
Demos
Usage
const { Collisions } = require('detect-collisions');
// other options:
// import { Collisions } from 'detect-collisions';
// Create the collision system
const system = new Collisions();
// Create a Result object for collecting information about the collisions
const result = system.createResult();
// Create the player (represented by a Circle)
const player = system.createCircle(100, 100, 10);
// Create some walls (represented by Polygons)
const wall1 = system.createPolygon(400, 500, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 1.7);
const wall2 = system.createPolygon(200, 100, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 2.2);
const wall3 = system.createPolygon(400, 50, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 0.7);
// Update the collision system
system.update();
// Get any potential collisions
// (this quickly rules out walls that have no chance of colliding with the player)
const potentials = player.potentials();
// Loop through the potential wall collisions
for (const wall of potentials) {
// Test if the player collides with the wall
if (player.collides(wall, result)) {
// Push the player out of the wall
player.x -= result.overlap * result.overlap_x;
player.y -= result.overlap * result.overlap_y;
}
}
Getting Started
1. Creating a Collision System
Collisions 提供执行宽相和窄相碰撞测试的函数。 为了充分利用这两个阶段,需要在碰撞系统中跟踪物体。
调用 Collisions 构造函数来创建碰撞系统。
const { Collisions } = require('detect-collisions');
const system = new Collisions();
2. Creating, Inserting, Updating, and Removing Bodies
Collisions 支持以下主体类型:
- Circle: A shape with infinite sides equidistant from a single point
- Polygon: A shape made up of line segments
- Point: A single coordinate
要使用它们,需要所需的主体类,调用其构造函数,并将其插入到碰撞中系统使用 insert()
。
const { Collisions, Circle, Polygon, Point } = require('detect-collisions');
const system = new Collisions();
const circle = new Circle(100, 100, 10);
const polygon = new Polygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const line = new Polygon(200, 5, [[-30, 0], [10, 20]]);
const point = new Point(10, 10);
system.insert(circle)
system.insert(polygon, line, point);
碰撞系统公开了几个方便的函数,用于创建物体并将它们一步插入到系统中。 这也避免了必须要求不同的主体类别。
const { Collisions } = require('detect-collisions');
const system = new Collisions();
const circle = system.createCircle(100, 100, 10);
const polygon = system.createPolygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const line = system.createPolygon(200, 5, [[-30, 0], [10, 20]]);
const point = system.createPoint(10, 10);
所有物体都具有可操作的 x
和 y
属性。 此外,Circle
主体有一个 scale
属性,可用于缩放它们的整体大小。 Polygon
主体具有 scale_x
和 scale_y
属性以沿特定轴缩放它们的点,以及一个 angle
属性以旋转它们当前位置周围的点(使用弧度)。
circle.x = 20;
circle.y = 30;
circle.scale = 1.5;
polygon.x = 40;
polygon.y = 100;
polygon.scale_x = 1.2;
polygon.scale_y = 3.4;
polygon.angle = 1.2;
当然,当不再需要尸体时,可以将其移走。
system.remove(polygon, point);
circle.remove();
3. Updating the Collision System
当其中的主体发生变化时,碰撞系统需要更新。 这包括何时插入、移除实体,或何时更改它们的属性(例如位置、角度、缩放比例等)。 更新碰撞系统是通过调用 update()
完成的,通常每帧发生一次。
system.update();
更新碰撞系统的最佳时间是之后它的主体发生变化并且之前测试碰撞。 例如,游戏循环可能使用以下事件顺序:
function gameLoop() {
handleInput();
processGameLogic();
system.update();
handleCollisions();
render();
}
4. Testing for Collisions
当测试物体碰撞时,通常建议执行广泛阶段搜索首先通过调用 potentials()
来快速排除距离太远而无法碰撞的物体。 Collisions 使用Bounding Volume Hierarchy (BVH) 进行宽相搜索。 在物体上调用 potentials()
遍历 BVH 并构建潜在碰撞候选者列表。
const potentials = polygon.potentials();
获取潜在碰撞列表后,遍历它们并使用 collides()
执行窄相碰撞测试。 碰撞 使用分离轴定理 (SAT) 进行窄相碰撞测试。
const potentials = polygon.potentials();
for (const body of potentials) {
if (polygon.collides(body)) {
console.log('Collision detected!');
}
}
也可以完全跳过 broad-phase 搜索并直接在两个物体上调用 collides()
。
注意:不建议跳过广义阶段搜索。 在针对大量物体测试碰撞时,使用 BVH 执行宽相搜索效率更高。
if (polygon.collides(line)) {
console.log('Collision detected!');
}
5. Getting Detailed Collision Information
通常需要有关碰撞的详细信息,以便对其做出适当的反应。 此信息使用 Result
对象存储。 Result
对象在发生碰撞时设置了几个属性,所有这些都在 文档< /a>。
为方便起见,有多种方法可以创建 Result
对象。 Result
对象不属于任何特定的碰撞系统,因此以下任何一种创建方法都可以互换使用。 这也意味着相同的 Result
对象可用于跨多个系统的冲突。
注意:强烈建议在执行多次碰撞测试时回收
Result
对象以节省内存。 以下示例创建多个Result
对象,严格作为演示。
const { Collisions, Result, Polygon } = require('detect-collisions');
const system = new Collisions();
const my_polygon = new Polygon(100, 100, 10);
const result1 = new Result();
const result2 = Collisions.createResult();
const result3 = system.createResult();
const result4 = Polygon.createResult();
const result5 = my_polygon.createResult();
要使用 Result
对象,请将其传递给 collides()
。 如果发生碰撞,它将填充有关碰撞的信息。 请注意,在以下示例中,每次迭代都会重复使用相同的 Result
对象。
const result = system.createResult();
const potentials = point.potentials();
for (const body of potentials) {
if (point.collides(body, result)) {
console.log(result);
}
}
6. Negating Overlap
碰撞检测中的一个常见用例是在发生碰撞时(例如玩家撞墙时)取消重叠。 这可以使用 Result
对象中的碰撞信息来完成(请参阅获取详细碰撞信息)。
Result
对象的三个最有用的属性是 overlap
、overlap_x
和 overlap_y
。 这些值一起描述了源体与目标体重叠的程度和方向。 更具体地说,overlap_x
和 overlap_y
描述方向向量,overlap
描述该向量的大小。
这些值可用于使用所需的最小距离将一个物体“推出”另一个物体。 更简单地说,从源物体的位置减去这个向量将导致物体不再发生碰撞。 这是一个示例:
if (player.collides(wall, result)) {
player.x -= result.overlap * result.overlap_x;
player.y -= result.overlap * result.overlap_y;
}
7. Detecting collision after insertion
const { Collisions } = require("detect-collisions")
const system = new Collisions();
const collider = system.createCircle(100, 100, 10);
const potentials = collider.potentials();
const obj = { name: 'coin', collider };
const done = console.log
let result = system.createResult();
let collided = false;
for (const wall of potentials) {
if (obj.collider.collides(wall, result)) {
collided = done(obj, wall, result) || collided;
}
}
if (collided) {
obj.collider.remove()
}
Lines
创建一条线只是创建一个单边多边形(即只有两个坐标对的多边形)。
const line = new Polygon(200, 5, [[-30, 0], [10, 20]]);
Concave Polygons
Collisions 使用分离轴定理< /a> (SAT) 的窄相碰撞测试。 SAT 的一个警告是它只适用于凸体。 然而,凹多边形可以通过使用一系列线来“伪造”。 请记住,使用 Lines 绘制的多边形是“空心的”。
处理真正的凹多边形需要将它们分解成它们的组件凸多边形(凸分解)并单独测试它们的碰撞。 有计划在未来将此功能集成到库中,但现在,请查看 poly-decomp.js。
Rendering
对于调试,能够可视化碰撞体通常很有用。 通过调用 draw()
并传入画布的 2D 上下文,可以将 Collision 系统中的所有物体绘制到 元素。
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// ...
context.strokeStyle = '#FFFFFF';
context.beginPath();
system.draw(context);
context.stroke();
身体也可以单独绘制。
context.strokeStyle = '#FFFFFF';
context.beginPath();
polygon.draw(context);
circle.draw(context);
context.stroke();
还可以绘制 BVH 来帮助测试 Bounding Volume Padding。
context.strokeStyle = '#FFFFFF';
context.beginPath();
system.drawBVH(context);
context.stroke();
Bounding Volume Padding
当物体在碰撞系统中四处移动时,内部 BVH 必须移除并重新插入物体以确定它在层次结构中的位置。 这是维护 BVH 成本最高的操作之一。 一般来说,大多数项目永远不会因此而出现性能问题,除非它们同时处理数千个移动物体。 在这些情况下,有时“填充”每个物体的边界体积是有益的,这样 BVH 就不需要移除和重新插入位置变化不大的物体。 换句话说,填充边界体积为其内部的主体提供了“呼吸空间”,可以四处移动而不会被标记为更新。
权衡是稍大的边界体积会在宽相 potentials()
搜索期间触发更多误报。 虽然窄阶段最终会使用轴对齐边界框测试将这些排除在外,但在拥挤的物体上放置过多的填充物会导致过多的误报和性能回报递减。 开发人员可以根据每个物体在单个帧内可以移动多少以及系统中物体的拥挤程度来确定每个物体需要多少填充。
可以在实例化主体时将填充添加到主体(请参阅每个主体的文档)或随时通过更改其填充
属性。
const padding = 5;
const circle = new Circle(100, 100, 10, 1, padding);
// ...
circle.padding = 10;
Only using SAT
某些项目可能只需要进行 SAT 碰撞测试,而无需进行广相搜索。 这可以通过完全避免碰撞系统并仅使用 collides()
函数来实现。
const { Circle, Polygon, Result } = require('collisions');
const circle = new Circle(45, 45, 20);
const polygon = new Polygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const result = new Result();
if (circle.collides(polygon, result)) {
console.log(result);
}
FAQ
Why shouldn't I just use a physics engine?
鼓励需要物理的项目使用其中的几种物理引擎(例如 Matter.js,Planck.js)。 然而,许多项目最终只使用物理引擎来进行碰撞检测,开发人员经常发现自己不得不绕过这些引擎所做的一些假设(重力、速度、摩擦力等)。 Collisions 的创建是为了提供可靠的碰撞检测,仅此而已。 事实上,以碰撞为核心的物理引擎很容易编写。
Why does the source code seem to have quite a bit of copy/paste?
Collisions 以性能为主要关注点。 有意识地做出牺牲可读性的决定,以避免不必要的函数调用或属性查找的开销。
Sometimes bodies can "squeeze" between two other bodies. What's going on?
这不是由错误的碰撞引起的,而是由项目如何处理其碰撞响应引起的。 有几种方法可以对碰撞做出响应,其中最常见的方法是循环遍历所有物体,找到它们的潜在碰撞,并一次否定任何发现的重叠。 由于重叠一次被否定,因此最后一个否定优先并且可能导致身体被推入另一个身体。
一种解决方法是解决每个碰撞,更新碰撞系统,然后重复直到没有发现碰撞。 请记住,如果两个碰撞体相互抵消,这可能会导致无限循环。 另一种解决方案是收集所有重叠并将它们组合成一个单一的合成向量,然后将主体推出,但这可能会变得相当复杂。
没有完美的解决方案。 如何处理冲突取决于项目。
Detect Collisions
detect-collisions is a zero dependency JavaScript library for quickly and accurately detecting collisions between Polygons, Circles, and Points. It combines the efficiency of a Bounding Volume Hierarchy (BVH) for broad-phase searching and the accuracy of the Separating Axis Theorem (SAT) for narrow-phase collision testing.
- Credits
- Installation
- Documentation
- Demos
- Usage
- Getting Started
- Lines
- Concave Polygons
- Rendering
- Bounding Volume Padding
- Only using SAT
- FAQ
Credits
It was originally forked from github.com/Sinova/Collisions
Since then not much has changed, but the project is maintained. The demos were updated.
Installation
yarn add detect-collisions --save
# or
npm i detect-collisions --save
Documentation
View the documentation (this README is also there).
Demos
Usage
const { Collisions } = require('detect-collisions');
// other options:
// import { Collisions } from 'detect-collisions';
// Create the collision system
const system = new Collisions();
// Create a Result object for collecting information about the collisions
const result = system.createResult();
// Create the player (represented by a Circle)
const player = system.createCircle(100, 100, 10);
// Create some walls (represented by Polygons)
const wall1 = system.createPolygon(400, 500, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 1.7);
const wall2 = system.createPolygon(200, 100, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 2.2);
const wall3 = system.createPolygon(400, 50, [[-60, -20], [60, -20], [60, 20], [-60, 20]], 0.7);
// Update the collision system
system.update();
// Get any potential collisions
// (this quickly rules out walls that have no chance of colliding with the player)
const potentials = player.potentials();
// Loop through the potential wall collisions
for (const wall of potentials) {
// Test if the player collides with the wall
if (player.collides(wall, result)) {
// Push the player out of the wall
player.x -= result.overlap * result.overlap_x;
player.y -= result.overlap * result.overlap_y;
}
}
Getting Started
1. Creating a Collision System
Collisions provides functions for performing both broad-phase and narrow-phase collision tests. In order to take full advantage of both phases, bodies need to be tracked within a collision system.
Call the Collisions constructor to create a collision system.
const { Collisions } = require('detect-collisions');
const system = new Collisions();
2. Creating, Inserting, Updating, and Removing Bodies
Collisions supports the following body types:
- Circle: A shape with infinite sides equidistant from a single point
- Polygon: A shape made up of line segments
- Point: A single coordinate
To use them, require the desired body class, call its constructor, and insert it into the collision system using insert()
.
const { Collisions, Circle, Polygon, Point } = require('detect-collisions');
const system = new Collisions();
const circle = new Circle(100, 100, 10);
const polygon = new Polygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const line = new Polygon(200, 5, [[-30, 0], [10, 20]]);
const point = new Point(10, 10);
system.insert(circle)
system.insert(polygon, line, point);
Collision systems expose several convenience functions for creating bodies and inserting them into the system in one step. This also avoids having to require the different body classes.
const { Collisions } = require('detect-collisions');
const system = new Collisions();
const circle = system.createCircle(100, 100, 10);
const polygon = system.createPolygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const line = system.createPolygon(200, 5, [[-30, 0], [10, 20]]);
const point = system.createPoint(10, 10);
All bodies have x
and y
properties that can be manipulated. Additionally, Circle
bodies have a scale
property that can be used to scale their overall size. Polygon
bodies have scale_x
and scale_y
properties to scale their points along a particular axis and an angle
property to rotate their points around their current position (using radians).
circle.x = 20;
circle.y = 30;
circle.scale = 1.5;
polygon.x = 40;
polygon.y = 100;
polygon.scale_x = 1.2;
polygon.scale_y = 3.4;
polygon.angle = 1.2;
And, of course, bodies can be removed when they are no longer needed.
system.remove(polygon, point);
circle.remove();
3. Updating the Collision System
Collision systems need to be updated when the bodies within them change. This includes when bodies are inserted, removed, or when their properties change (e.g. position, angle, scaling, etc.). Updating a collision system is done by calling update()
and should typically occur once per frame.
system.update();
The optimal time for updating a collision system is after its bodies have changed and before collisions are tested. For example, a game loop might use the following order of events:
function gameLoop() {
handleInput();
processGameLogic();
system.update();
handleCollisions();
render();
}
4. Testing for Collisions
When testing for collisions on a body, it is generally recommended that a broad-phase search be performed first by calling potentials()
in order to quickly rule out bodies that are too far away to collide. Collisions uses a Bounding Volume Hierarchy (BVH) for its broad-phase search. Calling potentials()
on a body traverses the BVH and builds a list of potential collision candidates.
const potentials = polygon.potentials();
Once a list of potential collisions is acquired, loop through them and perform a narrow-phase collision test using collides()
. Collisions uses the Separating Axis Theorem (SAT) for its narrow-phase collision tests.
const potentials = polygon.potentials();
for (const body of potentials) {
if (polygon.collides(body)) {
console.log('Collision detected!');
}
}
It is also possible to skip the broad-phase search entirely and call collides()
directly on two bodies.
Note: Skipping the broad-phase search is not recommended. When testing for collisions against large numbers of bodies, performing a broad-phase search using a BVH is much more efficient.
if (polygon.collides(line)) {
console.log('Collision detected!');
}
5. Getting Detailed Collision Information
There is often a need for detailed information about a collision in order to react to it appropriately. This information is stored using a Result
object. Result
objects have several properties set on them when a collision occurs, all of which are described in the documentation.
For convenience, there are several ways to create a Result
object. Result
objects do not belong to any particular collision system, so any of the following methods for creating one can be used interchangeably. This also means the same Result
object can be used for collisions across multiple systems.
Note: It is highly recommended that
Result
objects be recycled when performing multiple collision tests in order to save memory. The following example creates multipleResult
objects strictly as a demonstration.
const { Collisions, Result, Polygon } = require('detect-collisions');
const system = new Collisions();
const my_polygon = new Polygon(100, 100, 10);
const result1 = new Result();
const result2 = Collisions.createResult();
const result3 = system.createResult();
const result4 = Polygon.createResult();
const result5 = my_polygon.createResult();
To use a Result
object, pass it into collides()
. If a collision occurs, it will be populated with information about the collision. Take note in the following example that the same Result
object is being reused each iteration.
const result = system.createResult();
const potentials = point.potentials();
for (const body of potentials) {
if (point.collides(body, result)) {
console.log(result);
}
}
6. Negating Overlap
A common use-case in collision detection is negating overlap when a collision occurs (such as when a player hits a wall). This can be done using the collision information in a Result
object (see Getting Detailed Collision Information).
The three most useful properties on a Result
object are overlap
, overlap_x
, and overlap_y
. Together, these values describe how much and in what direction the source body is overlapping the target body. More specifically, overlap_x
and overlap_y
describe the direction vector, and overlap
describes the magnitude of that vector.
These values can be used to "push" one body out of another using the minimum distance required. More simply, subtracting this vector from the source body's position will cause the bodies to no longer collide. Here's an example:
if (player.collides(wall, result)) {
player.x -= result.overlap * result.overlap_x;
player.y -= result.overlap * result.overlap_y;
}
7. Detecting collision after insertion
const { Collisions } = require("detect-collisions")
const system = new Collisions();
const collider = system.createCircle(100, 100, 10);
const potentials = collider.potentials();
const obj = { name: 'coin', collider };
const done = console.log
let result = system.createResult();
let collided = false;
for (const wall of potentials) {
if (obj.collider.collides(wall, result)) {
collided = done(obj, wall, result) || collided;
}
}
if (collided) {
obj.collider.remove()
}
Lines
Creating a line is simply a matter of creating a single-sided polygon (i.e. a polygon with only two coordinate pairs).
const line = new Polygon(200, 5, [[-30, 0], [10, 20]]);
Concave Polygons
Collisions uses the Separating Axis Theorem (SAT) for its narrow-phase collision tests. One caveat to SAT is that it only works properly on convex bodies. However, concave polygons can be "faked" by using a series of Lines. Keep in mind that a polygon drawn using Lines is "hollow".
Handling true concave polygons requires breaking them down into their component convex polygons (Convex Decomposition) and testing them for collisions individually. There are plans to integrate this functionality into the library in the future, but for now, check out poly-decomp.js.
Rendering
For debugging, it is often useful to be able to visualize the collision bodies. All of the bodies in a Collision system can be drawn to a <canvas>
element by calling draw()
and passing in the canvas' 2D context.
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// ...
context.strokeStyle = '#FFFFFF';
context.beginPath();
system.draw(context);
context.stroke();
Bodies can be individually drawn as well.
context.strokeStyle = '#FFFFFF';
context.beginPath();
polygon.draw(context);
circle.draw(context);
context.stroke();
The BVH can also be drawn to help test Bounding Volume Padding.
context.strokeStyle = '#FFFFFF';
context.beginPath();
system.drawBVH(context);
context.stroke();
Bounding Volume Padding
When bodies move around within a collision system, the internal BVH has to remove and reinsert the body in order to determine where it belongs in the hierarchy. This is one of the most costly operations in maintaining a BVH. In general, most projects will never see a performance issue from this unless they are dealing with thousands of moving bodies at once. In these cases, it can sometimes be beneficial to "pad" the bounding volumes of each body so that the BVH doesn't need to remove and reinsert bodies that haven't changed position too much. In other words, padding the bounding volume allows "breathing room" for the body within it to move around without being flagged for an update.
The tradeoff is that the slightly larger bounding volumes can trigger more false-positives during the broad-phase potentials()
search. While the narrow phase will ultimately rule these out using Axis Aligned Bounding Box tests, putting too much padding on bodies that are crowded can lead to too many false positives and a diminishing return in performance. It is up to the developer to determine how much padding each body will need based on how much it can move within a single frame and how crowded the bodies in the system are.
Padding can be added to a body when instantiating it (see the documentation for each body) or at any time by changing its padding
property.
const padding = 5;
const circle = new Circle(100, 100, 10, 1, padding);
// ...
circle.padding = 10;
Only using SAT
Some projects may only have a need to perform SAT collision tests without broad-phase searching. This can be achieved by avoiding collision systems altogether and only using the collides()
function.
const { Circle, Polygon, Result } = require('collisions');
const circle = new Circle(45, 45, 20);
const polygon = new Polygon(50, 50, [[0, 0], [20, 20], [-10, 10]]);
const result = new Result();
if (circle.collides(polygon, result)) {
console.log(result);
}
FAQ
Why shouldn't I just use a physics engine?
Projects requiring physics are encouraged to use one of the several physics engines out there (e.g. Matter.js, Planck.js). However, many projects end up using physics engines solely for collision detection, and developers often find themselves having to work around some of the assumptions that these engines make (gravity, velocity, friction, etc.). Collisions was created to provide robust collision detection and nothing more. In fact, a physics engine could easily be written with Collisions at its core.
Why does the source code seem to have quite a bit of copy/paste?
Collisions was written with performance as its primary focus. Conscious decisions were made to sacrifice readability in order to avoid the overhead of unnecessary function calls or property lookups.
Sometimes bodies can "squeeze" between two other bodies. What's going on?
This isn't caused by faulty collisions, but rather how a project handles its collision responses. There are several ways to go about responding to collisions, the most common of which is to loop through all bodies, find their potential collisions, and negate any overlaps that are found one at a time. Since the overlaps are negated one at a time, the last negation takes precedence and can cause the body to be pushed into another body.
One workaround is to resolve each collision, update the collision system, and repeat until no collisions are found. Keep in mind that this can potentially lead to infinite loops if the two colliding bodies equally negate each other. Another solution is to collect all overlaps and combine them into a single resultant vector and then push the body out, but this can get rather complicated.
There is no perfect solution. How collisions are handled depends on the project.