Jasontreks Blog

DM 보내기


Send

탄환 오브젝트 구현 및 포물선 운동 적용

탄환 위치 업데이트

대포가 발사될 때 탄환이 포물선 궤적을 그리며 비행하려면 포물선 운동 공식을 적용해야 한다.

매 프레임마다 조금씩 포물선 이동을 한 탄환의 위치를 업데이트하려면 두번째, 세번째 공식을 사용하면 된다. 처음 탄환이 운동을 시작한 위치(대포의 포구 좌표)에 매 프레임마다 계산된 X값, Y값을 더해주면 포물선 운동이 구현된다.

세타는 발사 순간 포신의 각도, V0는 대포에 설정할 포신의 각도. 이 둘은 플레이하면서 자연스럽게 발생하는 값이다. g는 중력가속도이므로 게임 엔진에 설정된 상수다.

문제는 t인데, 이는 누적 비행 시간이므로 탄환 객체의 속성에 deltaTime 값을 누적시켜 계산에 이용해야 한다.

## 포물선 운동
t += delta * timeScale
var x = v0 * cos(theta0) * t
var y = -v0 * sin(theta0) * t + 0.5 * game.G * pow(t, 2)
## 떨어지고 있는 중인지 판정
if ((p0.y + y) - global_position.y) < 0:
    isFalling = false
else:
    isFalling = true

t를 일정 시간 간격을 두어 배열을 만들고 한꺼번에 계산하면 아래와 같이 포물선 궤적을 예측할 수 있게 된다. FPS게임에서 자주 보여지는 수류탄 투척 궤적 UI가 바로 이런 윈리이다.

Abstract art Abstract art

조준 시스템

우리 팀은 조준 시스템을 어떤 방향으로 정할지 고민했다. 포물선 궤적으로 뭔가를 날리는 슈팅 게임들은 대부분 에측 궤적을 일부 보여주고 이를 이용해 플레이어가 탄착 위치를 예측하는 방식으로 조준한다.

하지만 이 게임 특성상 수풀, 연못 등의 전략적 요소들로 필드를 최대한 활용하도록 설계되어 있기 때문에 꽤나 넓은 월드가 필요했다. 앞서 설명한 조준 시스템은 적어도 적이 보이는 위치까지는 월드를 끊김없이 카메라에 투영해야 의미가 있는 시스템인데, 우리 게임은 그 넓은 범위를 전부 투영하기엔 한계가 있었다. 자신과 적까지의 위치를 전부 투영하게 되면 캐릭터와 대포가 너무 작아 가시성이 떨어지게 된다.

그래서 우리는 플레이어가 탄착 위치를 먼저 결정하고, 그에 따른 각도를 계산해 포물선 운동이 이루어지도록 구현하기로 방향을 잡았다. 이를 염두에 두고 위의 공식을 다시 한변 살펴본다면

여기서 첫 번째 공식의 좌변(R)이 먼저 주어지게 되며 세타값이 해가 된다.

var theta = asin(cannon.game.G * currentAimRange / pow(V0, 2)) / 2

조준 거리 보정

지금까지의 풀이에 한가지 오류가 있는데, R은 실제 탄착 위치가 아니라는 점이다.

Abstract art

대포의 발사 위치가 지면으로부터 일정 거리 떨어져있기 때문에, 탄환은 플레이어가 실제로 조준한 거리인 R보다 더 비행하게 된다. 이를 해결하려면 조준 모드에서 보여지는 조준점의 위치가 R만큼 떨어진 거리가 아닌 단차 h만큼 추가로 비행한 거리를 더한 위치로 보정해야 한다.

보정 방법은 아래와 같다.

a. 탄환이 A에 도달한 순간의 Y축 속력을 구한다.

T=2v0sinθgT = \frac{2 \cdot v_0 \cdot \sin\theta}{g}
vy,end=v0sinθgTv_{y,\text{end}} = v_0 \cdot \sin\theta - g \cdot T

b. a에서 구한 Y축 속력으로 지면에 닿기까지 걸리는 시간을 구한다.

tend=vy,end+vy,end22ghgt_{\text{end}} = \frac{v_{y,\text{end}} + \sqrt{v_{y,\text{end}}^2 - 2 \cdot g \cdot h}}{g}

c. A 위치에서 b에서 구한 시간만큼 수평으로 이동한 거리를 더한다.

Ractual=R+v0cosθtendR_{\text{actual}} = R + v_0 \cdot \cos\theta \cdot t_{\text{end}}

d. c에서 구한 최종 거리 (실제 탄착 위치)로 조준점이 오게 한다.

# 탄착 조준위치 보정
# 발사 위치와 지면과의 단차를 구함(h)
var h = breech.global_position.y

# 발사 각도를 구함
var theta = asin(cannon.game.G * currentAimRange / pow(V0, 2)) / 2

# 총 비행시간을 구함 (h높이에서 발사, 같은 h높이로 되돌아오는 시간)
var T = 2 * V0 * sin(theta) / cannon.game.G

# 그 순간의 y축 속도를 구함
var vy_end = V0 * sin(theta) - cannon.game.G *  T

# 그 순간의 y축 속도로 지면에 닿기까지 걸리는 찰나의 시간을 계산 (단차 h 만큼 추가로 떨어지는데 걸리는 시간)
var t_end = (vy_end + sqrt(pow(vy_end, 2) - 2 * cannon.game.G * h)) / cannon.game.G

# 총 비행시간 T + h만큼 떨어지는데 걸리는 시간 t_end를 더한 비행시간이 실제 탄환의 비행시간
# 그 시간만큼 X축 이동한 거리가 실제 탄이 떨어지는 위치. 그 위치로 조준경의 초점을 맞춤
var actual_aim_range = currentAimRange + V0 * cos(theta) * t_end

포격 이벤트

플레이어가 대포에 붙은 상태에서 조준 모드로 진입할 수 있으며, 조준 모드에서 입력이 들어오면 탄환 발사 위치에서 탄환 오브젝트가 생성되도록 이벤트를 구현하였다.

탄환 발사 위치는 처음엔 포구(Muzzel)로 할 생각이였는데, 발사 각도에 따라 발사 위치가 수시로 변하기 때문에 계산이 상당한 수준으로 복잡해지는 문제가 있었다.(탄착 위치를 먼저 정하는 방식때문에 일반적인 연산으로는 해를 구할 수 없는 풀이가 나와버린다.) 그래서 편의상 포미에 해당하는 부분(Breech)으로 타협을 봤다.

현실적으로는 포신 내부에서부터 포물선 운동이 시작되기에 말이 안되지만, 대포의 모양이나 탄환의 크기, 이펙트 등을 적절히 섞어 크게 어색한 부분이 없도록 눈속임을 주면 문제가 되지 않는다.

다음 포스트

리슨 서버(Listen Server) 멀티 플레이 구현