내일배움캠프 Node.js 트랙 50일차
추측항법을 어떻게 구현할까?
레이턴시
레이턴시란 한 지점에서 다른 지점으로 이동하는 데 걸리는 시간을 의미한다.
게임 서버를 예로 들어보자. 클라이언트가 본인의 위치를 서버로 보내어 동기화를 시도할 때, 레이턴시가 길어진다면 어떨까. 아마 게임 속 캐릭터가 뚝뚝 끊기면서 이동하게 될 테고 사용자 경험은 곤두박질칠 것이다.
추측항법
추측항법의 기본 원리는 현재 상태를 바탕으로 미래 상태를 예측하는 것이다. 이를 테면, 서버가 클라이언트의 레이턴시가 100ms 인 상황에서 속도가 v인 오브젝트의 미래를 예측하여 클라이언트에게 미리 공지하는 것이다.
거리 = 속도 x 시간이므로, 다음과 같은 정보가 필요하다.
- 초기 위치 (Initial Position): 계산의 시작점이 되는 위치.
- 이동 속도 (Speed): 단위 시간당 이동 거리.
- 이동 방향 (Direction): 이동의 각도나 경로.
- 시간 (Time): 이동이 시작된 이후 경과된 시간.
플레이어가 초속 60px로 우측을 향해 달렸다고 가정해 보자. 초기 위치가 (0, 0)이라면, 2초 후에는 원점에서 120px만큼 떨어진 곳에 도착했을 것이라고 "추측"할 수 있다.
다만, 이 계산은 정확한 도착 위치를 보장하지는 않는다. 중간에 교통 상황이나 바람, 도로 상황 등 예측하지 못한 변수가 존재할 수 있기 때문이다. 하지만 기본적인 방향성과 거리를 추측할 수 있다.
추측 항법 코드로 구현해보기
클라이언트: 플레이어의 속도와 위치를 계산하여 서버로 전송하기
using UnityEngine;
public class Player : MonoBehaviour
{
private Vector2 lastPosition; // 이전 프레임의 위치를 저장
private Vector2 velocity; // 속도 계산용 벡터
void Update()
{
if (!GameManager.instance.isLive) return;
// 입력 기반으로 위치 업데이트
inputVec.x = Input.GetAxisRaw("Horizontal");
inputVec.y = Input.GetAxisRaw("Vertical");
Vector2 currentPosition = rigid.position; // 현재 위치
velocity = (currentPosition - lastPosition) / Time.deltaTime; // 속도 계산
// 위치 및 속도 패킷 전송
NetworkManager.instance.SendPositionAndVelocityPacket(currentPosition.x, currentPosition.y, velocity.x, velocity.y);
lastPosition = currentPosition; // 현재 위치를 저장
}
}
클라이언트: 패킷 전송 함수 추가하기
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
// 위치 및 속도 데이터를 서버로 전송하는 함수
public void SendPositionAndVelocityPacket(float posX, float posY, float velX, float velY)
{
PositionVelocityPayload payload = new PositionVelocityPayload
{
x = posX,
y = posY,
velocityX = velX,
velocityY = velY,
};
SendPacket(payload, (uint)Packets.HandlerIds.PositionVelocity);
}
}
클라이언트: 페이로드를 정의, 핸들러 ID를 추가
[ProtoContract]
public class PositionVelocityPayload
{
[ProtoMember(1, IsRequired = true)]
public float x { get; set; }
[ProtoMember(2, IsRequired = true)]
public float y { get; set; }
[ProtoMember(3, IsRequired = true)]
public float velocityX { get; set; }
[ProtoMember(4, IsRequired = true)]
public float velocityY { get; set; }
}
public enum HandlerIds
{
Init = 0,
LocationUpdate = 2,
PositionVelocity = 3 // 새로운 핸들러 ID 추가
}
서버: 핸들러 함수 추가
export const positionVelocityHandler = ({ socket, userId, payload }) => {
try {
const { x, y, velocityX, velocityY } = payload;
const user = getUserById(userId);
const targetLocation = user.calculatePosition(
user.latency,
velocityX,
velocityY,
);
console.log('다음 좌표는 여기! : ', targetLocation);
// 클라이언트로 다시 보내주기
const packet = targetLocationPacket(targetLocation);
socket.write(packet);
} catch (err) {
handleError(socket, err);
}
};
서버: 추측항법을 사용하여 위치를 추정하는 메서드
class User {
calculatePosition(latency, velocityX, velocityY) {
// 유저의 레이턴시를 인자로 받는다.
const timeDiff = latency / 1000; // 레이턴시를 초 단위로 계산
const distanceX = velocityX * timeDiff;
const distanceY = velocityY * timeDiff;
// x, y 축에서 이동한 거리 계산
return {
x: this.x + distanceX,
y: this.y + distanceY,
};
}
}
더 보완해야 하는 점
가속도 고려하기
현재 사용하는 방식은 마지막 속도가 일정하게 유지된다고 가정하고 있다. 그러나 게임 스타일에 따라서는 가속도를 반영하는 걸 고려할 수 있을 터.
출처 : 등가속도 운동 공식 : 네이버 블로그
등가속도 운동에서 변화한 위치는 위 그래프의 넓이와 같다.(거리 = 속도 x 시간이니까!)
코드로는 아래와 같이 옮겨 적을 수 있을 것이다.
const distanceX = velocityX * timeDiff + 0.5 * this.accelerationX * Math.pow(timeDiff, 2);
const distanceY = velocityY * timeDiff + 0.5 * this.accelerationY * Math.pow(timeDiff, 2);
가속도가 더해진 추측항법은 우리가 흔히 아는 미분의 개념으로 이해할 수도 있다. 전체 속도를 평균 내는 것이 아니라, 마지막 시점의 속도 변화율(가속도)을 기준으로 미래를 예측하기 때문이다.
등가속도 운동 공식을 다시 한번 살펴 보면, 실제로 속도-시간 그래프의 기울기는 가속도(a)를 의미한다.
'개발일지 > TIL(Today I Learned)' 카테고리의 다른 글
2025-01-10 <추측항법을 어떻게 구현할까? (2)> (0) | 2025.01.10 |
---|---|
2025-01-09 <스트림이란 무엇일까> (1) | 2025.01.09 |
2025-01-07 <라디안과 삼각함수를 이용해 게임 로직 구현하기> (1) | 2025.01.07 |
2025-01-06 <프로토콜 버퍼를 이용한 TCP 서버 통신> (1) | 2025.01.06 |
2025-01-03 (0) | 2025.01.03 |
댓글