문제
지금 프로젝트는 격자 기반 이동이라서, 실제 게임 로직은 타일 단위로 맞아야 한다.
하지만 렌더는 부드럽게 보여주고 싶어서 PrevPosition -> CurrentPosition 보간을 사용했다.
렌더는 이동 주기동안 보간하면서 부드럽게 이동중인데 로직은 격자 기반이동 이므로
보간을 시작하기전 이미 이동할 위치로 이동한다.
맵에서는 이미 다음 칸으로 들어가 있거나 반대로 아직 이전 칸에 남아 있으면
충돌, 타겟 판정, 카메라 추적이 전부 어긋난다.
이 둘을 맞추기 위해 PrevPosition, Position, NextPosition을 분리하고,중간 렌더 좌표와 실제 맵 반영 시점을 나눴다.

그리고 PrevPosition과 NextPosition으로 구한 Ratio가 0.5를 넘어가면Position에 NextPosition을 대입한다.
그렇게 되면 렌더상 선을 넘어가는 시점에 몬스터가 타일 상에도 다음 위치로 이동한다.

해결
AObject
먼저 AObject에 이동 상태를 따로 들고 가게 바꿨다.
Vector GetPosition() const { return Position; }
Vector GetNextPosition() const { return NextPosition; }
bool IsMoving() const { return bIsMoving; }
bool HasCommittedMove() const { return bPositionCommitted; }
void SetPosition(Vector InPosition);
void BeginMoveTo(Vector InNextPosition);
bool CommitMoveIfNeeded(float Alpha);
void FinishMoveIfNeeded(float Alpha);
기존에는 Position 하나만 보면 됐는데,
이제는
PrevPosition: 출발한 칸Position: 실제 맵 판정에 반영된 칸NextPosition: 렌더가 향하고 있는 목표 칸
이 세 개를 따로 본다.
이동 시작
이동 입력이 들어오면 바로 SetPosition()으로 좌표를 바꾸지 않고,
먼저 BeginMoveTo()로 이동 예약만 한다.
void AObject::BeginMoveTo(Vector InNextPosition)
{
if (bIsDestroy || bIsMoving || (InNextPosition.X == Position.X && InNextPosition.Y == Position.Y))
{
return;
}
PrevPosition = Position;
NextPosition = InNextPosition;
bIsMoving = true;
bPositionCommitted = false;
}
이 단계에서는 아직 Position은 그대로다.
그래서 게임 로직 입장에서는 아직 원래 칸에 있고,
렌더만 Prev -> Next 사이를 향해 움직이기 시작하는 상태가 된다.
입력 처리
MoveComponent도 이 흐름에 맞춰 바뀌었다.
이동 중일 때는 새 입력을 받지 않고,
입력 검사가 끝나면 BeginMoveTo()만 호출한다.
void UMoveComponent::HandleMoveInput()
{
if (PlayerPtr->IsMoving())
{
return;
}
...
SetFacingDirection(NextDirection);
PlayerPtr->BeginMoveTo(NextPosition);
MoveElapsedTime = 0.0f;
}
이전처럼 입력이 들어오자마자 Position을 바꾸는 구조가 아니라,
이제는 다음 칸으로 이동 시작 상태만 만든다.
실제 맵 반영
맵 좌표는 이동 시작 직후가 아니라, Alpha가 절반을 넘긴 시점에 한 번만 반영한다.
그 역할을 하는 게 CommitMoveIfNeeded()다.
bool AObject::CommitMoveIfNeeded(float Alpha)
{
if (bIsDestroy || !bIsMoving || bPositionCommitted || Alpha < 0.5f)
{
return false;
}
if (MapManager::GetInstance()->IsMapInitSize())
{
if (!MapManager::GetInstance()->MoveObject(this, Position, NextPosition))
{
NextPosition = Position;
PrevPosition = Position;
bIsMoving = false;
bPositionCommitted = false;
return false;
}
}
Position = NextPosition;
bPositionCommitted = true;
return true;
}
Alpha < 0.5f일 때는 아직 맵 좌표를 안 바꾼다- 한 번 바꾼 뒤에는
bPositionCommitted로 중복 반영을 막는다
이동 완료
이동 애니메이션이 끝난 시점에는 FinishMoveIfNeeded()가 마무리를 한다.
void AObject::FinishMoveIfNeeded(float Alpha)
{
if (bIsDestroy || !bIsMoving || Alpha < 1.0f)
{
return;
}
if (!bPositionCommitted)
{
CommitMoveIfNeeded(1.0f);
}
PrevPosition = Position;
NextPosition = Position;
bIsMoving = false;
bPositionCommitted = false;
}
- 혹시 아직 반영되지 않았으면 마지막에 한 번 강제로 반영
- 끝났으면
Prev,Position,Next를 다시 같은 값으로 맞춤
이제 한 칸 이동이 완전히 끝난 뒤에는 다시 정지 상태가 된다.
기타 개선
MapManager
맵 갱신도 이 구조에 맞춰 MoveObject()가 직접 담당하게 했다.
예전처럼 전체 오브젝트를 순회해서 PrevPosition과 Position을 다시 훑는 방식보다,
이제는 이동 커밋 시점에 명시적으로 옮긴다.
- 이동 가능한 칸인지 검사
- 이전 칸 비우기
- 새 칸에 현재 오브젝트 배치
이 순서가 AObject 이동 커밋과 같은 순간에 일어난다.
렌더
렌더는 이제 Position만 보지 않는다.
이동 중이면 NextPosition을 목표점으로 보고,
아니면 현재 Position을 그대로 쓴다.
Vector GetRenderTargetPosition(AObject* Object)
{
if (Object == nullptr)
{
return {0, 0};
}
return Object->IsMoving() ? Object->GetNextPosition() : Object->GetPosition();
}
그 다음 PrevPosition과 이 목표 좌표를 Lerp한다.
FVector InterpolatePosition(const Vector& PrevPosition, const Vector& CurrentPosition, float Alpha)
{
Alpha = min(max(Alpha, 0.0f), 1.0f);
return {
static_cast(PrevPosition.X) + static_cast(CurrentPosition.X - PrevPosition.X) * Alpha + 0.5f,
static_cast(PrevPosition.Y) + static_cast(CurrentPosition.Y - PrevPosition.Y) * Alpha + 0.5f
};
}
이 구조가 들어가면서 화면에서는 캐릭터가 끊기지 않고 움직이고,
맵 판정은 중간 시점에 맞춰 넘어가도록 보정되었다.
'TIL' 카테고리의 다른 글
| TEXT RPG(DIABL5) 발표하며 회고... (0) | 2026.05.29 |
|---|---|
| C++ 프로그램에서 Cloudflare Worker로 R2에 GET, POST 하기 (0) | 2026.05.28 |
| Text RPG 업데이트 Pooling, Tick, 전투/이동, Render 분리 (0) | 2026.05.26 |
| Text RPG 협업 5/21~ (0) | 2026.05.22 |
| 일주일 게임잼 제작 ~ 발표 (0) | 2026.05.21 |