| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- multi-thread
- 독서
- System Programming
- PS
- Unity
- Photon
- team project
- BOJ
- git
- design pattern
- Toy Project
- c#
- Network Programming
- Data Structure
- C++
- Online Judge
- docker
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 21일차(Client). 서버 확정 위치 기반 클라이언트 이동 보정 본문
Unity 게임 개발 캠프 개인 프로젝트 21일차(Client). 서버 확정 위치 기반 클라이언트 이동 보정
FanJae 2026. 10. 5. 21:501. 시작에 앞서
- Server에서는 이번 작업을 통해 Player의 이동 요청을 그대로 적용하지 않고, Map 범위와 이동 속도를 검증한 뒤 최종 위치를 결정하도록 변경했다.
- 이에 따라 Client 역시 단순히 이동 요청을 전송한 뒤 자신의 위치를 바로 확정하는 방식으로는 동작할 수 없게 되었다.
- Client에서 화면을 자연스럽게 움직이는 것과 Server가 실제로 인정한 Player 위치를 구분할 필요가 생겼다.
- 따라서 이번 작업에서는 Client의 이동 예측 위치와 Server 확정 위치를 분리하고, MoveResponse 결과에 따라 위치를 보정하는 구조를 구현했다.
2. 기존 Client 이동 방식의 문제
- 기존에는 MoveRequest 전송이 정상적으로 끝나면 Client에서도 해당 좌표를 Player의 위치로 바로 반영했다.
이동 입력
│
▼
MoveRequest
│
▼
전송 성공
│
▼
Client 위치 변경
- 하지만 Server에서 이동 가능 여부를 검사하기 시작하면서 다음과 같은 상황이 발생할 수 있다.
Client
(120, 45) 이동 요청
│
▼
Server
SpeedExceeded
│
▼
실제 위치는 기존 위치 유지
- Client가 요청한 위치를 그대로 확정한다면 Server와 Client의 위치가 서로 달라지게 된다.
- 따라서 이동을 요청한 위치와 Server가 확정한 위치를 별도로 관리하도록 변경했다.
3. LocalMovementState 추가
- Local Player의 이동 요청 상태를 관리하기 위해 LocalMovementState를 추가했다.
public uint MapId { get; private set; }
public int ServerX { get; private set; }
public int ServerY { get; private set; }
public ulong PendingSequence { get; private set; }
public bool HasPendingMove
=> PendingSequence != 0;
- 이 클래스에서는 현재 Server가 확정한 좌표와 함께 현재 처리 중인 MoveRequest의 Sequence를 관리한다.
LocalMovementState
Current Map ID
Server X / Y
Pending Sequence
Next Sequence
- 즉 화면상 Player가 어디를 향하고 있는지와 관계없이 실제 게임 상태의 기준 좌표는 Server Response를 통해서만 변경하도록 했다.
4. 이동 요청에 Sequence와 Map ID 적용
- 변경된 Server Protocol에 맞춰 Client의 MoveRequest에도 현재 Map ID와 Sequence를 포함하도록 했다.
public struct MoveRequestData
{
public uint MapId;
public ulong Sequence;
public int X;
public int Y;
}
- 이동 요청을 시작할 때 Sequence를 증가시키고 현재 처리 중인 요청으로 기록한다.
PendingSequence = ++_sequence;
request = new MoveRequestData
{
MapId = MapId,
Sequence = PendingSequence,
X = x,
Y = y
};
- 현재는 하나의 이동 요청에 대한 응답이 오기 전에 새로운 요청을 계속 보내지 않도록 했다.
if (MapId == 0 ||
HasPendingMove ||
_sequence == ulong.MaxValue)
{
return false;
}
- 방향키 입력은 계속 받을 수 있지만 Network Request는 이전 응답을 확인한 뒤 다음 요청을 보내는 형태다.
5. 화면 이동과 Server 좌표 분리
- Local Player의 화면 표현 역시 변경했다.
- 기존 SetTargetPosition()은 Server 좌표와 화면 목표 위치를 동시에 변경했다.
- 이번에는 화면상의 이동만 먼저 수행할 수 있도록 PredictPosition()을 분리했다.
public void PredictPosition(int x, int y)
{
_targetPosition =
WorldManager.ToUnityPosition(x, y);
}
- 따라서 이동 요청 과정은 다음과 같다.
Direction Input
│
▼
MoveRequest 생성
│
├──────────────► Server
│
▼
화면상 이동 예측
- 이 시점에서 Player Sprite는 요청 위치 방향으로 이동하지만 ServerX, ServerY는 아직 바뀌지 않는다.
화면 위치
≠
Server 확정 위치
- Server에서 응답을 받은 이후에만 실제 Server 좌표를 갱신한다.
6. MoveResponse를 이용한 위치 확정
- Server에서 새롭게 전달하는 MoveResponse도 Client Protocol에 추가했다.
public struct MoveResponseData
{
public ulong Sequence;
public uint MapId;
public MoveResult Result;
public int X;
public int Y;
}
- 응답을 받으면 현재 기다리고 있던 요청과 동일한지 먼저 확인한다.
if (!HasPendingMove || data.Sequence != PendingSequence || data.MapId != MapId)
{
return false;
}
- 현재 요청과 관계없는 Response라면 Client 상태에 반영하지 않는다.
MoveResponse
│
▼
Map ID 확인
│
▼
Sequence 확인
│
├─ 현재 요청 → 적용
│
└─ 이전 요청 → 무시
- 정상적인 Response라면 Server가 보내준 좌표를 현재 확정 좌표로 저장한다.
ServerX = data.X;
ServerY = data.Y;
7. 이동 성공과 실패에 따른 처리
- 이동에 성공했다면 Server가 반환한 위치를 Player의 최종 목표 위치로 사용한다.
_worldManager.SetLocalTargetPosition(data.X,data.Y);
- 반면 이동 요청이 거절된 경우에는 입력 목표 역시 Server 좌표로 되돌린다.
if (data.Result != MoveResult.Success)
{
_moveTarget = new Vector2(data.X, data.Y);
}
- 결과적으로 다음과 같은 흐름이 만들어졌다.
Client Prediction
│
▼
MoveRequest
│
▼
Server Validation
│
┌───┴────┐
▼ ▼
Success Reject
│ │
└───┬────┘
▼
MoveResponse
Server Position
│
▼
Client Correction
- Client의 화면 예측이 틀렸더라도 최종적으로는 Server가 반환한 위치로 수렴하게 된다.
8. 이전 응답이 현재 이동에 영향을 주지 않도록 처리
- 비동기 Network 환경에서는 이전 요청의 응답이 늦게 도착할 가능성도 고려해야 한다.
- 특히 Map을 이동한 뒤 이전 Map의 MoveResponse가 도착한다면 현재 Player 위치를 잘못 변경할 수 있다.
- 이를 방지하기 위해 MapId와 Sequence가 모두 현재 상태와 일치하는 경우만 적용하도록 했다.
Map A
Sequence 10 이동
│
│ Response 지연
▼
Map B 이동
Sequence 11 요청
│
▼
Map A의 Sequence 10 응답 도착
│
▼
현재 Map / Sequence와 다름
│
▼
무시
- Map을 변경할 때는 현재 대기 중인 이동 요청도 취소한다.
_movementState.CancelMove(_movementState.PendingSequence);
9. 이동 응답 Timeout 처리
- MoveRequest를 전송했지만 Server 응답이 오지 않는 경우도 고려했다.
- 요청 이후 일정 시간 동안 MoveResponse가 도착하지 않으면 Pending 상태를 해제한다.
_moveResponseDeadline = Time.unscaledTime + 5f;
- 시간이 초과되면 마지막 Server 확정 위치로 화면과 이동 목표를 되돌린다.
_moveTarget = new Vector2(_movementState.ServerX,_movementState.ServerY);
_worldManager.SetLocalTargetPosition(_movementState.ServerX,_movementState.ServerY);
- 따라서 Packet 유실이나 연결 문제 때문에 Client가 영구적으로 이동 요청 대기 상태에 남지 않도록 했다.
10. MapInfo 기반 이동 속도 적용
- 이전 Client에서는 방향키 이동 속도를 다음과 같이 고정값으로 사용하고 있었다.
private const float ArrowMoveSpeed = 80f;
- 하지만 이번 Server 구조에서는 이동 속도가 Map 데이터에 포함되어 있다.
- 따라서 Client에서도 Server에서 받은 MapInfo 값을 사용하도록 변경했다.
_moveTarget += direction * _mapInfo.MoveSpeed * Time.deltaTime;
- Map에 입장하면 Server가 다음 정보를 전달한다.
MapInfo
Map ID
Min X / Max X
Min Y / Max Y
Move Speed
Move Burst
- Client 테스트 화면에서도 현재 Server 설정을 확인할 수 있도록 표시했다.
Bounds X: -400..400
Bounds Y: -200..200
Move Speed: 80
Burst: 12
- 이동 규칙의 기준값을 Client 코드에 중복해서 가지고 있지 않고 Server가 전달한 값을 사용하도록 한 것이다.
11. Map 변경 시 위치 상태 보정
- Map 변경 응답에서도 Server가 현재 Map ID와 좌표를 반환하기 때문에 Client는 해당 값을 기준으로 상태를 다시 설정한다.
_movementState.EnterMap(data.MapId,data.X,data.Y);
- 성공 여부와 관계없이 Response에 포함된 Server 기준 상태를 이용해 Local Player의 위치를 맞춘다.
_currentMapId = data.MapId;
_moveTarget =
new Vector2(data.X, data.Y);
_worldManager.SetLocalTargetPosition(
data.X,
data.Y);
- 특히 존재하지 않는 Map으로 이동을 요청했을 경우에도 기존 Map과 기존 Server 좌표가 그대로 유지된다.
12. Map-local 통합 테스트 추가
- 이번 변경에서는 Unity 화면 테스트뿐 아니라 실제 Network와 Protocol 코드를 이용하는 별도 통합 테스트도 추가했다.
- 두 Client를 구성하여 다음과 같은 흐름을 자동으로 확인하도록 했다.
Login
↓
Character Select
↓
GameServer Enter
↓
MapInfo
↓
Player / Monster 동기화
↓
MoveRequest
↓
MoveResponse
↓
PlayerMove
- 특히 이동 관련해서는 다음 항목을 검증했다.
정상 이동
Map 범위 밖 이동
속도 초과
극단적인 좌표
잘못된 Map ID
중복 Sequence
이동 거절 이후 정상 이동 복구
Map 변경 후 이전 Response 무시
고빈도 이동 요청
- 기존에 구현했던 Map-local Player, Monster, Chat 기능도 함께 테스트하여 이동 구조 변경 이후 기존 기능이 깨지지 않는지 확인했다.
13. 정리
- 이번 작업에서는 Server가 Player 이동 결과를 직접 검증하도록 변경됨에 따라 Client의 이동 처리 방식도 함께 변경했다.
- LocalMovementState를 추가하여 화면에서 예측하고 있는 위치와 Server가 실제로 확정한 좌표를 분리했다.
- 이동 요청에는 현재 Map ID와 Sequence를 포함하고, 해당 요청과 일치하는 MoveResponse만 Client 상태에 반영하도록 했다.
- Player는 입력 즉시 화면상 목표 위치로 이동할 수 있지만 실제 Server 좌표는 Response를 받은 이후에만 변경된다.
- 이동이 Server에서 거절된 경우에는 화면 위치와 다음 입력 목표를 Server가 반환한 좌표로 다시 보정하도록 했다.
- 또한 일정 시간 동안 이동 응답이 없거나 Map이 변경되는 경우 Pending Move 상태를 정리해 이전 요청이 이후 상태에 영향을 주지 않도록 했다.
- 방향키 이동 속도 역시 Client의 고정값이 아니라 MapInfo를 통해 Server가 전달한 설정을 사용하도록 변경했다.
- 이번 작업을 통해 Client의 이동 표현은 빠르게 반응시키면서도 실제 Player 위치의 기준은 Server가 확정하도록 이동 처리 흐름을 정리했다.
'Projects > MyToyMapleServer' 카테고리의 다른 글
| Unity 게임 개발 캠프 개인 프로젝트 22일차(Client). 서버 지형 기반 발판 이동 예측 구현 (0) | 2026.10.06 |
|---|---|
| Unity 게임 개발 캠프 개인 프로젝트 22일차(Server). 서버 기반 발판 이동 시뮬레이션 구현 (0) | 2026.10.05 |
| Unity 게임 개발 캠프 개인 프로젝트 21일차(Server). 서버 기준 Player 이동 검증 구현 (0) | 2026.10.04 |
| Unity 게임 개발 캠프 개인 프로젝트 20일차. 테스트 클라이언트 책임 분리 및 Map-local 채팅 구현 (0) | 2026.10.03 |
| Unity 게임 개발 캠프 개인 프로젝트 19일차. Unity 네트워크 테스트 클라이언트 보완 (0) | 2026.10.02 |