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 CircleCollisionResolver extends CollisionResolver {
/**
* 주 객체를 등록하여 충돌체크를 진행한다.
*
* @param {GameObject} circle
*/
constructor(circle) {
super(circle);
this.circle = circle;
}
/**
* 원과 상자가 충돌했다면 true를 반환한다.
*
* @param {GameObject} box - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
isCollideWithBox(box) {
// 원의 중심과 상자의 중심간 거리의 차를 구한다.
const distance = this.circle
.getColliderWorldPosition()
.minus(box.getColliderWorldPosition());
distance.x = Math.abs(distance.x);
distance.y = Math.abs(distance.y);
// 중심간 차의 절대값이 상자의 주변에 원이 접했을 때의 거리보다 크다면
// 충돌하지 않은 것이다.
if (
distance.x >
box.getWorldBoundary().x / 2 + this.circle.getWorldBoundary() ||
distance.y > box.getWorldBoundary().y / 2 + this.circle.getWorldBoundary()
) {
return false;
}
// 중심간 차의 절대값이 상자의 크기의 절반보다 작다면
// 원이 상자 안에 있는 셈이므로 충돌한 것이다.
if (
distance.x <= box.getWorldBoundary().x / 2 ||
distance.y <= box.getWorldBoundary().y / 2
) {
return true;
}
// 꼭짓점부분에서 충돌이 될 가능성을 검사한다.
const d = distance.minus(box.getWorldBoundary().multiply(0.5));
return (
d.squareLength() <=
this.circle.getWorldBoundary() * this.circle.getWorldBoundary()
);
}
/**
* 원과 원이 충돌했다면 true를 반환한다.
*
* *******
* ***** ** **
* * * * *
* * x * * x *
* * * * *
* ***** ** **
* *******
* +--+ +---+ <-- 원의 반지름
* +------------+ <-- 중심간의 거리
*
* 두 원의 반지름의 합이 중심간의 거리보다 작다면 충돌하지 않은 셈이다.
*
* @param {GameObject} circle - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
isCollideWithCircle(circle) {
const distance = this.circle
.getColliderWorldPosition()
.minus(circle.getColliderWorldPosition());
return (
(this.circle.getWorldBoundary() + circle.getWorldBoundary()) *
(this.circle.getWorldBoundary() + circle.getWorldBoundary()) >
distance.squareLength()
);
}
/**
* 원이 상자와 충돌했을 때 충돌깊이와 반작용방향을 반환한다.
*
* @param {GameObject} box - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
resolveBoxCollision(box) {
const rectCenter = box.getColliderWorldPosition();
const distance = this.circle.getColliderWorldPosition().minus(rectCenter);
const closest = new Vector(
clamp(
distance.x,
-box.getWorldBoundary().x / 2,
box.getWorldBoundary().x / 2
),
clamp(
distance.y,
-box.getWorldBoundary().y / 2,
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 = box.getWorldBoundary().x / 2;
} else {
closest.x = -box.getWorldBoundary().x / 2;
}
} else {
if (closest.y > 0) {
closest.y = box.getWorldBoundary().y / 2;
} else {
closest.y = -box.getWorldBoundary().y / 2;
}
}
}
let penetrationDepth = 0;
let normal = distance.minus(closest);
const d = normal.squareLength();
if (
d > this.circle.getWorldBoundary() * this.circle.getWorldBoundary() &&
!inside
) {
return;
}
if (inside) {
normal = normal.multiply(-1).normalize();
// 원이 사각형 안에 있다면 단순하게 충돌 깊이를 반지름 * 2로 설정한다.
penetrationDepth = 2 * this.circle.getWorldBoundary();
} else {
normal = normal.multiply(1).normalize();
// 원이 사각형 밖에 있다면 충돌 깊이를 반지름에서 충돌한 거리를 뺀 값으로 설정한다.
penetrationDepth = this.circle.getWorldBoundary() - Math.sqrt(d);
}
return new Manifold(box, this.circle, normal, penetrationDepth);
}
/**
* 원과 원이 충돌했을 때 충돌깊이와 반작용방향을 반환한다.
*
* @param {GameObject} circle - 이 객체와 충돌인지 확인할 객체
* @returns {boolean}
*/
resolveCircleCollision(circle) {
const distance = circle
.getColliderWorldPosition()
.minus(this.circle.getColliderWorldPosition());
// 두 원의 반지름을 더한 값을 제곱하되 정확한 값을 위해서
// 제곱근을 씌우진 않는다.
const sumOfRadius =
this.circle.getWorldBoundary() + circle.getWorldBoundary();
const squareOfRadius = sumOfRadius * sumOfRadius;
// 두 원의 중심간 거리가 두 원의 반지름을 더한 값의 제곱보다 크면
// 충돌하지 않았음을 의미한다.
if (distance.squareLength() > squareOfRadius) {
return;
}
const d = distance.length();
// 두 원의 중심이 같은 경우를 생각해 임의로 방향과 충돌깊이를 설정한다.
let penetrationDepth = this.circle.getWorldBoundary();
let normal = new Vector(-1, 0);
if (d != 0) {
// 두 원의 중심간 거리가 0이 아니라면
// 충돌했으되 중심이 일치하지 않은 상황이다.
// 반지름을 더한 값 - 중심간의 거리가 충돌한 깊이를 의미한다.
penetrationDepth = sumOfRadius - d;
// 중심간의 거리를 단위벡터화하면 힘(반작용)이 작용할 방향이 된다.
normal = distance.multiply(1 / d).normalize();
}
return new Manifold(this.circle, circle, normal, penetrationDepth);
}
}
export default CircleCollisionResolver;