설명
요즘 세상에 AI라고 하면 머신러닝을 먼저 떠올리지만, pong game에서 구현한 것은 그런 것들과는 거리가 있다. pong game에서 사용된 AI구현은 수학적 모델에 따라서 공의 궤적을 예측하고, 해당 위치로 이동하도록 동작하도록 되어있다. 공의 움직임과 패들의 움직임을 모두 vector를 사용해서 계산하기에, 패들과 공을 vector로 정의하고, vector 간의 충돌을 계산함으로써, 자신이 질 경우(line을 넘어갈 경우), 패들의 위치를 조정한다. 해당 과정을 초당 60번 이상 계산함으로써, 공이 자신의 line을 넘어가지 않도록 패들이 계속해서 움직이게 된다.
충돌을 예상하는 로직은 충돌 로직에서 대부분 구현했기 때문에, 해당 코드를 조금 수정하는 방식으로 구현했다.
구현
코드를 호출 하는 부분 부터 설명한다.
AIManager.getInstance().GuaranteeConflict(data.ball.clone(), 10000);
전역에서 싱글톤으로 구현된 AIManager를 가져와서 위에서 설명한 것과 같이 2D 직선 충돌 logic에서 구현한 GuaranteeConflict를 조금 수정해서 충돌지점에 대한 시뮬레이션을 하게 된다. 첫 번째, 인자는 ball class를 그대로 복사해서 넣은 값이다. 두 번째, 인자는 값이 10000이 들어가는데, 이 부분의 수치 자체는 큰 의미는 없고, 그냥 큰 숫자를 넣었다. delta을 큰 값을 줌으로써, 현재 데이터를 기반으로 한, 게임을 미리 시뮬레이션 하기 위함이다.
if (depth > 10) {
data.paddle[1].keyPress.up = false;
data.paddle[1].keyPress.down = false;
return;
}충돌로직은 충돌하고 남은 delta 값 만큼 다시 충돌을 계산하게 된다. 인자로 큰 delta값을 넣었음에 따라서 사실상 무수히 많은 충돌이 생기게 되고, 이를 제한해줄 필요성을 느껴서 넣은 부분이다. 해당 코드에 따르면 AI는 10번의 충돌을 시뮬레이션하고, 패들을 멈추게 된다. pong game의 특성상 10번 정도면 line을 넘어가는 경우가 대부분이지만 무한히 rallly 하는 경우를 위해 넣어줬다.
/* collisionResult.pos에서 사용되는 PaddlePos */
export enum PaddlePos{
LeftFront,
LeftUp,
LeftDown,
RightFront,
RightUp,
RightDown
}private AIcheckWithPaddleCollision(copy: Ball, delta: number, depth: number) {
let collisionResult = copy.checkWithPaddleCollision(delta);
if (collisionResult !== undefined) {
copy.move(collisionResult.p);
if (collisionResult.pos >= 3) {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = false;
return true;
}
copy.handleWithPaddleCollision(collisionResult.pos);
copy.move(0.000001);
this.GuaranteeConflict(copy, delta, depth + 1);
return true;
}
return false;
}let collisionResult = copy.checkWithPaddleCollision(delta);
먼저 공과 패들간의 충돌을 계산한다. checkWithPaddleCollision에서는 delta 값에 따라서 충돌을 계산하고, 충돌했다면, 어디에 충돌했는지 pos와 언제 충돌하는지 p를 return하게 된다. 충돌하지 않았을 경우에는 pos가 undefine의 값을 가지게 된다.
이후 아래에서 AI측 패들(오른쪽)이 충돌한 경우 적절한 위치에 도달한 것이므로 패들의 움직임을 멈추고, 그렇지 않은 경우에는 Player의 패들에 맞은 경우이므로, 다시 이에 대한 시뮬레이션을 재귀적으로 진행한다.
/* collisionResult.pos에서 사용되는 CanvasPosition */
export enum CanvasPosition {
Top,
Bottom,
Right,
Left,
}private AIcheckWithWallCollision(copy: Ball, delta: number, depth: number) {
let collisionResult = copy.checkWithWallCollision(delta);
if (collisionResult !== undefined) {
copy.move(collisionResult.p);
if (collisionResult.pos < 2) {
copy.direction[1] *= -1;
copy.move(0.000001);
this.GuaranteeConflict(copy, delta, depth + 1);
return true;
}
if (collisionResult.pos === CanvasPosition.Right) {
if (copy.position[1] < data.paddle[1].position[1]) {
data.paddle[1].keyPress.up = false;
data.paddle[1].keyPress.down = true;
}
else {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = true;
}
} else {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = false;
}
return true;
}
return false;
}let collisionResult = copy.checkWithWallCollision(delta);
패들과의 충돌을 체크한 후, 이제 벽에대한 충돌을 확인하게 된다. checkWithWallCollision은 패들과의 충돌을 확인하는 logic과 같이 충돌한 위치 pos와 시간인 p를 반환한다. 마찬가지로 충돌하지 않는 경우 pos가 undefined를 가진다.
상단, 하단 벽에 충돌한 경우, 공을 정반사 후, 다시 시뮬레이션을 진행하고,
AI가 위치한 벽을 넘어갈 경우, 이를 막기 위해서 충돌한 공의 위치와 패들의 위치를 비교해서, 위 또는 아래로 움직이게 된다.
공이 player의 벽을 넘어갈 경우, 움직임을 정지한다.
구현한 Pong game에서 매우 큰 delta값을 줬을 때, 10번 이하의 충돌이 일어나는 경우는 없어야 하는데, 아닌 경우가 생길 경우 확인을 위해 false를 return 하고 이를 가지고 error를 출력하도록 했다.
전체적인 코드의 구현은 아래와 같다.
코드
private AIcheckWithPaddleCollision(copy: Ball, delta: number, depth: number) {
let collisionResult = copy.checkWithPaddleCollision(delta);
if (collisionResult !== undefined) {
copy.move(collisionResult.p);
if (collisionResult.pos >= 3) {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = false;
return true;
}
copy.handleWithPaddleCollision(collisionResult.pos);
copy.move(0.000001);
this.GuaranteeConflict(copy, delta, depth + 1);
return true;
}
return false;
}
private AIcheckWithWallCollision(copy: Ball, delta: number, depth: number) {
let collisionResult = copy.checkWithWallCollision(delta);
if (collisionResult !== undefined) {
copy.move(collisionResult.p);
if (collisionResult.pos < 2) {
copy.direction[1] *= -1;
copy.move(0.000001);
this.GuaranteeConflict(copy, delta, depth + 1);
return true;
}
if (collisionResult.pos === CanvasPosition.Right) {
if (copy.position[1] < data.paddle[1].position[1]) {
data.paddle[1].keyPress.up = false;
data.paddle[1].keyPress.down = true;
}
else {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = true;
}
} else {
data.paddle[1].keyPress.down = false;
data.paddle[1].keyPress.up = false;
}
return true;
}
return false;
}
public GuaranteeConflict(copy: Ball, delta: number, depth: number = 0) {
if (depth > 10) {
data.paddle[1].keyPress.up = false;
data.paddle[1].keyPress.down = false;
return;
}
if (this.AIcheckWithPaddleCollision(copy, delta, depth)) return;
if (this.AIcheckWithWallCollision(copy, delta, depth)) return;
console.error("AIManager: GuaranteeConflict: Something wrong");
}