import Manifold from "/src/engine/data-structure/manifold.js";
import Vector from "/src/engine/data-structure/vector.js";
import CollisionResolver from "/src/engine/core/collision-resolver.js";
import { clamp } from "/src/engine/utils.js";
/**
* 상자와 상자 또는 상자와 원 사이의 충돌체크 및
* 충돌깊이와 반작용방향을 연산하는 책임을 맡는다.
*
* @extends CollisionResolver
*/
class BoxCollisionResolver extends CollisionResolver {
/**
* 주 객체를 등록하여 충돌체크를 진행한다.
*
* @param {GameObject} box
*/
constructor(box) {
super(box);
this.box = box;
}
/**
* 상자와 상자가 충돌했다면 true를 반환한다.
*
* @param {GameObject} box - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
isCollideWithBox(box) {
// 단순하게 AABB충돌체크 방식을 사용한다.
if (
this.box.getColliderWorldPosition().x -
this.box.getWorldBoundary().x / 2 >
box.getColliderWorldPosition().x + box.getWorldBoundary().x / 2 ||
this.box.getColliderWorldPosition().x +
this.box.getWorldBoundary().x / 2 <
box.getColliderWorldPosition().x - box.getWorldBoundary().x / 2 ||
this.box.getColliderWorldPosition().y -
this.box.getWorldBoundary().y / 2 >
box.getColliderWorldPosition().y + box.getWorldBoundary().y / 2 ||
this.box.getColliderWorldPosition().y +
this.box.getWorldBoundary().y / 2 <
box.getColliderWorldPosition().y - box.getWorldBoundary().y / 2
) {
return false;
}
return true;
}
/**
* 상자와 원이 충돌했다면 true를 반환한다.
*
* @param {GameObject} circle - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
isCollideWithCircle(circle) {
// 원의 중심과 상자의 중심간 거리의 차를 구한다.
const distance = circle
.getColliderWorldPosition()
.minus(this.box.getColliderWorldPosition());
distance.x = Math.abs(distance.x);
distance.y = Math.abs(distance.y);
// 중심간 차의 절대값이 상자의 주변에 원이 접했을 때의 거리보다 크다면
// 충돌하지 않은 것이다.
if (
distance.x >
this.box.getWorldBoundary().x / 2 + circle.getWorldBoundary() ||
distance.y > this.box.getWorldBoundary().y / 2 + circle.getWorldBoundary()
) {
return false;
}
// 중심간 차의 절대값이 상자의 크기의 절반보다 작다면
// 원이 상자 안에 있는 셈이므로 충돌한 것이다.
if (
distance.x <= this.box.getWorldBoundary().x / 2 ||
distance.y <= this.box.getWorldBoundary().y / 2
) {
return true;
}
// 꼭짓점부분에서 충돌이 될 가능성을 검사한다.
const d = distance.minus(this.box.getWorldBoundary().multiply(0.5));
return (
d.squareLength() <= circle.getWorldBoundary() * circle.getWorldBoundary()
);
}
/**
* 상자와 상자가 충돌했을 때 충돌깊이와 반작용방향을 반환한다.
*
* +-------+
* +-----+ | |
* | x | | x |
* +-----+ | |
* +-------+
*
* +--+ +---+ <-- 가로 길이의 절반
* +----------+ <-- 중심간의 거리
*
* 각 상자의 길이의 절반의 합이 중심간의 거리보다 작을 때에만 충돌이다.
* 이 때 충돌한 깊이는 각 길이의 절반의 합과 중심간의 거리의 차로 구해진다.
*
* @param {GameObject} box - 이 객체와 충돌한 다른 객체
* @returns {Manifold}
*/
resolveBoxCollision(box) {
const distance = box
.getColliderWorldPosition()
.minus(this.box.getColliderWorldPosition());
// 충돌된 영역의 가로 길이
const xOverlap =
this.box.getWorldBoundary().x / 2 +
box.getWorldBoundary().x / 2 -
Math.abs(distance.x);
// 충돌된 영역의 세로 길이
const yOverlap =
this.box.getWorldBoundary().y / 2 +
box.getWorldBoundary().y / 2 -
Math.abs(distance.y);
if (xOverlap < 0 || yOverlap < 0) {
return;
}
let normal = new Vector(0, -1);
let penetrationDepth = 0;
// 가로 길이가 세로 길이보다 크다면
// 위->아래방향 또는
// 아래->위방향으로 진행한 물체가 충돌한 것이다.
if (xOverlap > yOverlap) {
// obj이 other보다 아래에 있으면 위에서 아래로 충돌했다는 말이므로
// 힘(반작용)은 위로 작용해야한다.
// 그렇지 않으면 힘이 아래로 작용해야한다.
if (distance.y < 0) {
normal = new Vector(0, -1);
} else {
normal = new Vector(0, 1);
}
penetrationDepth = yOverlap;
} else {
// 세로 길이가 가로 길이보다 크다는 말은
// 왼쪽->오른쪽방향 또는
// 오른쪽->왼쪽방향으로 진행한 물체가 충돌한 것이다.
// obj이 other보다 왼쪽에 있으면
// 힘(반작용)은 왼쪽으로 작용해야한다.
// 반대의 경우 오른쪽으로 작용해야한다.
if (distance.x < 0) {
normal = new Vector(-1, 0);
} else {
normal = new Vector(1, 0);
}
penetrationDepth = xOverlap;
}
return new Manifold(this.box, box, normal, penetrationDepth);
}
/**
* 원이 상자와 충돌했을 때 충돌깊이와 반작용방향을 반환한다.
*
* @param {GameObject} circle - 이 객체와 충돌한 다른 객체
* @returns {Manifold}
*/
resolveCircleCollision(circle) {
const rectCenter = this.box.getColliderWorldPosition();
const distance = circle.getColliderWorldPosition().minus(rectCenter);
const closest = new Vector(
clamp(
distance.x,
-this.box.getWorldBoundary().x / 2,
this.box.getWorldBoundary().x / 2
),
clamp(
distance.y,
-this.box.getWorldBoundary().y / 2,
this.box.getWorldBoundary().y / 2
)
);
let inside = false;
// 만약 원의 중심이 사각형의 안에 들어와 있다면...
// closest는 항상 사각형 내로 clamp되어 있기 때문에
// distance + rectCenter와 똑같아지게 된다.
if (distance.isEquals(closest)) {
inside = true;
// 중심에서 어떤 축이 더 가까운지 찾는다.
if (Math.abs(distance.x) < Math.abs(distance.y)) {
// y편차가 더 작다는 말은?
// 사각형에서 원과 가장 가까운 점을 찾아야 하므로
// 가장 가까운 사각형의 경계를 점으로 선택한다.
if (closest.x > 0) {
closest.x = this.box.getWorldBoundary().x / 2;
} else {
closest.x = -this.box.getWorldBoundary().x / 2;
}
} else {
if (closest.y > 0) {
closest.y = this.box.getWorldBoundary().y / 2;
} else {
closest.y = -this.box.getWorldBoundary().y / 2;
}
}
}
let penetrationDepth = 0;
let normal = distance.minus(closest);
const d = normal.squareLength();
if (d > circle.getWorldBoundary() * circle.getWorldBoundary() && !inside) {
return;
}
if (inside) {
normal = normal.multiply(-1).normalize();
// 원이 사각형 안에 있다면 단순하게 충돌 깊이를 반지름 * 2로 설정한다.
penetrationDepth = 2 * circle.getWorldBoundary();
} else {
normal = normal.multiply(1).normalize();
// 원이 사각형 밖에 있다면 충돌 깊이를 반지름에서 충돌한 거리를 뺀 값으로 설정한다.
penetrationDepth = circle.getWorldBoundary() - Math.sqrt(d);
}
return new Manifold(this.box, circle, normal, penetrationDepth);
}
}
export default BoxCollisionResolver;