-
[TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-2)TIL-sparta 2024. 7. 6. 21:42
> 과제 진행 간 완료한 사항 및 문제점과 해결 과정을 정리해보았다.
학습 키워드: Node.js, net, TCP, socket, Buffer, Protobuf, latency, dead reckoning1. 완료한 과제 진행 사항
1) (도전 요구사항) Latency 기반 추측 항법:
- 추측 항법 연산에서 유저 개인 latency가 아닌 game 세션 유저 내 최대 latency 값을 사용하도록 변경
- 각종 버그 수정
2. 과제 진행 간 문제점
1) 접속 초기 LocationUpdate 패킷의 x, y값 연산에서 NaN이 발생하는 문제 (해결):
콘솔 로그를 몇 개 찍어보고 바로 알 수 있는 단순한 문제였다. Figure 2 를 보면 유저의 latency 값이 undefined 인데, 유저 생성 초기에는 ping 패킷을 보내지 않았기 때문에 처음 LocationUpdate 패킷을 보낼 때 추측 항법을 통한 x y 값의 연산에서 시간 값으로 undefined가 사용되면서 NaN이 출력되는 것이었다. 아래와 같이 User 클래스의 생성자에서 ping을 한 번 호출하여 문제를 해결했다.
class User { constructor(id, playerId, socket, speed) { this.socket = socket; // 소켓을 우선 등록 this.latency = 0; // latency initialization this.ping(); // ping 시도 this.id = id; this.playerId = playerId; this.speed = speed; this.x = 0; this.y = 0; this.inputX = 0; this.inputY = 0; this.sequence = 0; this.updatedAt = Date.now(); } ...
2) 유저 캐릭터를 움직여도 제자리로 복귀하는 현상 (해결):
const locationUpdateHandler = async ({ socket, userId, payload }) => { const { x, y, inputX, inputY } = payload; const user = userSessionsManager.getUserByUserId(userId); /* do some calculation for x, y validation here */ user.updatePosition(x, y, inputX, inputY); // 유저 위치를 업데이트 const data = new LocationUpdateData(MAIN_GAME_ID, userId); // 추측 항법 적용위치 반환 const serialized = serialize(protoTypeNames.gameNotification.LocationUpdate, data); const header = writeHeader(serialized.length, headerConstants.packetTypes.LOCATION); return Buffer.concat([header, serialized]); };
유저 캐릭터를 움직이면 캐릭터가 미세하게 움직이다가 다시 원위치로 돌아오는 현상이 있었는데, 추측 항법으로 위치를 연산하고 반환하는 사이에 새로운 location update payload가 기존의 위치를 가지고 들어와서 위치 정보를 갱신하면서 전후 상태가 꼬이는 현상이라고 판단했다.
// user.updatePosition(x, y, inputX, inputY); // 유저 위치를 업데이트 user.updateInputVector(inputX, inputY); // input X Y 값만 업데이트
변경 후에도 계속해서 같은 문제가 일어나서 추가로 조사하던 중 latency 값이 0이나 음수가 되는(음수는 무슨 경우인지 아직 확인하지 못했다) 경우가 있어 추측 항법 연산에서 time 값이 0이 되어 캐릭터가 멈추거나, 음수가 되어 반대 방향으로 움직이는 현상이 생긴다는 것을 알게 되었다. 1 미만의 latency 값을 가지는 경우를 동일하게 1로 처리해주도록 아래와 같이 변경하여 해결했다.
calculateNextPosition(latency) { // distance = speed * time const time = (latency <= 1 ? 1 : latency) / 1000; // latency in seconds const distance = this.speed * time; ...
3) 정지 시 우측으로 조금씩 이동하는 현상 (해결):아무런 input 없이 가만히 있는데도 캐릭터가 조금씩 오른쪽으로 이동하는 것을 확인했는데, 원인이 뭘까 하고 고민하다가 캐릭터가 실제로 우측으로 이동하려면 어떻게든 dx가 양수의 값을 반환해야 한다고 생각하고 다시 살펴본 결과 inputX와 inputY 값이 0일 때 atan2가 0 (각도가 0)이며, 단위원에서 cos(0) 값은 1이 되기 때문에 x 값에 변동이 생기는 것이었다. User 클래스의 추측 항법 연산에서 inputX와 inputY 값이 0인 경우를 아래와 같이 조건문으로 처리하여 해결했다.
calculateNextPosition(latency) { ... if (this.inputX === 0 && this.inputY === 0) { // 조건 추가 return { x: this.x, y: this.y }; } const theta = this.calculateTheta(); ... }
4) 일정 시간 이후 움직임이 느려지다가 아예 멈추는 문제 (해결):
게임 시작 초반에는 캐릭터가 제대로 움직이다가 일정 시간이 지나면 갑자기 움직임이 조금씩 느려지더니 다시 일정 시간이 지나면 아무리 input을 입력해도 움직이지 않게 되는 문제가 생겼다. Figure 5에서 처럼 서버는 정상적으로 dx를 보내주고 있지만 클라이언트의 nextVec 값이 갑자기 0으로 바뀌고 고정되어 버린다.
확인해보니 현재 서버의 추측 항법 코드가 유저 latency 값 변동에 따라 이동 거리가 바뀌는 상태여서 최초 latency 측정 때 약간 큰 latency 값을 도출하고 이후 두 번째 latency 측정 때 latency 값에 큰 drop이 있어서 (예: 24 -> 1) Figure 8에서 처럼 이동 거리(dx)가 훅 줄어들게 되어 중간에 갑자기 멈추는 듯한 현상이 생기는 것이었다. 실제로는 멈추는 것이 아닌 매우 작은 값으로 움직이고 있었다는 것을 알 수 있었다.
5) 접속 유저가 많을수록 모든 유저의 속도가 증가하는 문제 (해결):
// 변경 전 calculateNextPosition(t) { const distance = this.speed * t; if (this.inputX === 0 && this.inputY === 0) { return { x: this.x, y: this.y }; } const theta = this.calculateTheta(); const dx = Math.cos(theta) * distance; const dy = Math.sin(theta) * distance; this.x += dx; this.y += dy; return { x: this.x, y: this.y }; }
일단 원인은 getAllUserLocations 함수가 모든 유저의 LocationUpdate 요청에서 실행되기 때문에 dead reckoning을 통한 조정 작업이 같은 유저를 대상으로 여러번 호출되면서 각 유저가 실행한 모든 보정치 값이 합산되기 때문이었다. 이전과는 다르게 calculateNextPosition 함수가 두 곳에서 호출되는데, 하나는 유저에게서 LocationUpdatePayload를 받았을 때 deltaTime만큼 이동시켜주기 위함이고, 다른 하나는 기존처럼 게임 세션 내 유저들의 위치 정보에 latency 기반의 추측 항법을 적용하기 위해서 사용했다. 전자는 각 유저당 한 번만 호출되기 때문에 문제가 없다. 하지만 후자의 경우 배열을 순회하면서 적용해주기 때문에 위 코드처럼 매번 this.x와 this.y에 보정치를 더하게 되면 각 유저가 한 번씩 루프를 돌리면서 총 유저 수 만큼 보정 수치가 불어나게 된다. 이 문제를 해결하기 위해 전자의 경우 합산을, 후자의 경우 반환 값에만 보정치를 더해주도록 아래와 같이 조건을 추가했다.
// 변경 후 calculateNextPosition(t, save) { // save : boolean const distance = this.speed * t; if (this.inputX === 0 && this.inputY === 0) { return { x: this.x, y: this.y }; } const theta = this.calculateTheta(); const dx = Math.cos(theta) * distance; const dy = Math.sin(theta) * distance; if (save) { // 조건 추가 this.x += dx; this.y += dy; return { x: this.x, y: this.y }; } else { return { x: this.x + dx, y: this.y + dy }; } }
3. 기타 사항
1) 캐릭터 충돌 시 관통하는 문제 (미해결):
추측 항법을 구현하면서 클라이언트의 유저 x y좌표를 쓰지 않도록 변경했는데, 서버의 계산에만 의존하다 보니 캐릭터 간 충돌을 무시하고 밀어내거나 통과하는 문제가 발생했는데. 전자는 밀리는 입장의 클라이언트에서, 후자는 미는 입장의 클라이언트에서 발생하여 화면 정보가 불일치하는 현상도 같이 발생했다.
--
REFERENCES:> 과제 spec
> 과제 repo
> TheBook, "게임 서버 프로그래밍 교과서: 5.2.3 추측 항법", 배현직 저, accessed: 2024-07-06
728x90'TIL-sparta' 카테고리의 다른 글
강의 과제) CPU란 무엇인가? (1) 2024.07.08 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-1) (0) 2024.07.07 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-3) (0) 2024.07.05 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-4) (0) 2024.07.04 [TIL] 스파르타) Chapter 5 주특기 플러스 개인 과제 진행 (TCP 게임 서버, D-5) (2) 2024.07.03