게임을 만들다 보면 하나의 Entity만 애니메이션을 재생해서 처리하기 어려운 상호작용이 있다.
개를 쓰다듬거나, 말에 올라타거나, NPC와 악수하는 경우가 그렇다.
플레이어와 대상이 각각 애니메이션을 가지고 있고, 두 애니메이션이 서로 맞물려야 한다. 단순히 두 Animator에서 애니메이션을 동시에 재생한다고 해결되는 문제가 아니다.
플레이어가 정해진 위치에 있어야 하고, 대상과 바라보는 방향도 맞아야 한다. 애니메이션이 시작된 이후에는 두 Entity의 움직임이 어긋나지 않아야 하고, 종료된 이후에는 다시 기존 상태로 돌아가야 한다.
처음 이 문제를 생각했을 때는 애니메이션을 동시에 재생하는 정도로 접근할 수 있지만, 실제로 구현하려고 하면 애니메이션보다 상호작용을 시작하기 위한 위치와 상태를 어떻게 관리할 것인가가 더 큰 문제가 된다.
Interaction Point
Pair Interaction에서 먼저 필요한 것은 두 Entity가 만나는 기준점이다.
예를 들어 개를 쓰다듬는 애니메이션을 만들었다고 하면 플레이어가 개의 아무 위치에서나 애니메이션을 시작할 수는 없다.
애니메이션에서 플레이어의 손이 개의 머리 쪽으로 움직이도록 만들었다면 실제 게임에서도 애니메이션이 시작되는 순간 플레이어와 개의 위치가 그에 맞아야 한다.
그래서 Target에 Interaction Point를 둔다.
Dog -> InteractionPoints -> Pet
Pet Point는 플레이어가 접근해야 하는 위치와 방향을 가지고 있다.
말타기라면 하나의 Point만 사용하는 것보다 상호작용에 필요한 위치를 나눌 수 있다.
Horse -> MountPoint / DismountPoint / HandPoint / FootPoint
이렇게 만들어두면 애니메이션 자체가 월드 좌표를 알고 있을 필요가 없다.
Entity의 크기나 Pivot이 변경되더라도 Interaction Point를 조정해서 대응할 수 있다.
멀리 있는 경우
플레이어가 개 바로 앞에 있다면 Interaction Point에 맞춘 뒤 애니메이션을 실행하면 된다.
문제는 플레이어가 개에서 멀리 떨어져 있을 때다.
플레이어가 5m 떨어진 곳에서 개를 클릭했다고 해서 그 자리에서 Pet 애니메이션을 실행할 수는 없다.
따라서 상호작용은 단순히 Animation을 실행하는 것이 아니라 접근 과정까지 포함해야 한다.
Interaction Request -> Target 검증 / Interaction Point 선택 / Point까지 이동 / Position·Rotation 정렬 / Animation 실행
여기서 자동으로 플레이어를 이동시키는 방식은 프로젝트의 이동 시스템에 따라 달라질 수 있다.
NavMesh를 사용하는 게임이라면 NavMesh 기반으로 이동시킬 수 있고, 직접 만든 Character Controller가 있다면 기존 이동 로직을 활용할 수도 있다.
중요한 것은 상호작용을 위해 이동하는 동안 일반적인 Player Input과 자동 이동이 서로 싸우지 않도록 만드는 것이다.
그래서 Interaction 중에는 Player Movement의 제어권을 Interaction System이 가져가는 구조를 생각할 수 있다.
Normal -> Moving -> Playing -> Ending -> Normal
Position과 Rotation
Interaction Point를 사용할 때 Position만 맞추면 부족하다.
애니메이션이 특정 방향을 기준으로 제작되어 있다면 플레이어의 Rotation도 같이 맞춰야 한다.
Vector3 position = interactionPoint.position;
Quaternion rotation = interactionPoint.rotation;
플레이어가 목표 위치에 충분히 가까워지고 Rotation도 일정 범위 안에 들어오면 Animation을 시작한다.
float distance =
Vector3.Distance(player.position, targetPosition);
float angle =
Quaternion.Angle(player.rotation, targetRotation);
bool aligned =
distance < positionThreshold &&
angle < rotationThreshold;
이렇게 허용 오차를 두는 이유는 모든 프레임에서 정확하게 동일한 Transform을 만들 필요는 없기 때문이다.
실제 게임에서는 몇 cm 정도의 위치 차이나 몇 도 정도의 방향 차이는 눈에 띄지 않을 수 있다.
오히려 중요한 것은 애니메이션이 시작됐을 때 플레이어와 Target의 상대적인 위치가 일정하게 유지되는 것이다.
Root Motion
여기서 Root Motion을 사용하면 이동 책임이 조금 복잡해진다.
플레이어를 Interaction Point까지 이동시키는 코드가 Transform을 수정하고 있는데 Animator에서도 Root Motion으로 이동을 적용한다면 두 시스템이 동시에 플레이어의 위치를 변경한다.
Interaction Movement -> Player Transform <- Root Motion
이 구조에서는 캐릭터가 예상보다 많이 움직이거나 순간적으로 튀는 문제가 생길 수 있다.
그래서 단계별로 이동을 담당하는 시스템을 분리하는 편이 낫다.
접근 -> Character Movement / Navigation
정렬 -> Interaction Movement
Animation -> Root Motion
종료 -> Character Movement
반대로 Pair Animation에서 Root Motion을 사용하지 않고 애니메이션은 자세만 담당하게 만들 수도 있다.
어느 쪽이 더 좋은지는 게임의 이동 구조와 애니메이션 제작 방식에 따라 결정하면 된다.
중요한 것은 같은 순간에 여러 시스템이 캐릭터의 Transform을 동시에 제어하지 않는 것이다.
두 Animator의 동기화
Player와 Target이 각각 Animator를 가지고 있다면 두 애니메이션의 실행 시점도 관리해야 한다.
간단한 개 쓰다듬기라면 다음 정도로 끝날 수 있다.
Player -> Pet
Dog -> PetReceive
하지만 말타기처럼 여러 단계로 구성된 상호작용에서는 조금 다르다.
Player -> Mount Start / Mount / Mounted
Horse -> Mount Start / Reaction / Mounted
두 Animator가 서로 독립적으로 상태를 관리하면 한쪽만 먼저 종료되는 상황이 생길 수 있다.
그래서 Pair Interaction 자체가 두 Entity의 상태를 관리하도록 만드는 방법이 있다.
Pair Interaction -> Actor / Target / Actor Animation / Target Animation / State
각 Entity가 상대방의 Animator를 직접 제어하는 것이 아니라 Interaction이 두 Entity를 묶고 각각에게 필요한 Animation을 전달한다.
Interaction -> Player Animation / Target Animation
이렇게 하면 Player는 Dog의 내부 Animation State를 알 필요가 없고, Dog 역시 Player가 어떻게 움직이는지 알 필요가 없다.
IK
애니메이션만으로 두 Entity의 접촉 위치를 전부 맞추는 것은 생각보다 번거롭다.
개를 쓰다듬는 상황을 보면 플레이어의 손이 개의 머리에 닿아야 한다.
그런데 개의 크기가 달라지거나 모델의 구조가 변경되면 기존 애니메이션의 손 위치가 맞지 않을 수 있다.
이런 부분은 IK를 사용하는 편이 효율적이다.
Animation -> 전체적인 몸의 움직임 / 자세
IK -> 손 / 발 / 접촉 위치 보정
예를 들어 개의 머리에 IK Target을 두고 플레이어의 손을 해당 위치로 맞추면 애니메이션 자체에서 손 위치를 완벽하게 맞출 필요가 없다.
말타기에서도 비슷한 방법을 사용할 수 있다.
Horse -> MountPoint / HandPoint / FootPoint
Player -> Animation / IK
애니메이션이 전체적인 움직임을 만들고 IK가 실제 접촉 지점을 보정하는 구조다.
Interaction을 데이터로 분리하기
상호작용이 많아지면 코드에 직접 Animation 이름이나 위치 조건을 넣는 것도 문제가 된다.
예를 들어 Pet, Ride, Talk, Handshake마다 Player 코드에 조건문을 추가하기 시작하면 상호작용이 늘어날수록 Player가 모든 Interaction을 알아야 한다.
그래서 Interaction 자체를 데이터로 분리할 수 있다.
[CreateAssetMenu]
public class InteractionDefinition : ScriptableObject
{
public string id;
public string actorAnimation;
public string targetAnimation;
public float positionThreshold;
public float rotationThreshold;
public bool useRootMotion;
public bool useIK;
}
그러면 실제 흐름은 다음과 같이 단순해진다.
Interaction Request -> Definition 조회 / Target 확인 / Point 확인 / 정렬 / Pair Animation / 종료
새로운 상호작용을 추가하더라도 Player의 코드를 수정하는 대신 새로운 Definition과 Interaction Point를 추가하는 방식으로 확장할 수 있다.
Interaction의 책임
최종적으로 Interaction System이 담당하는 영역은 상당히 명확하다.
Interaction System -> Target 검증 / Point 선택 / Actor 이동 / 정렬 / Animation 동기화 / IK / 종료 처리
반면 Entity는 자신의 역할만 가지고 있는 것이 좋다.
Player -> Input / Movement / Animator
Dog -> AI / Movement / Animator
Horse -> Movement / Animator
Interaction -> Entity 사이의 상호작용 관리
이렇게 분리하면 Player가 Dog의 내부 구현을 직접 알아야 하는 상황을 피할 수 있다.
특히 새로운 동물이 추가됐을 때도 Interaction Point와 Animation Definition만 추가해서 기존 시스템을 재사용할 수 있다.
마무리
Pair Interaction Animation을 구현하면서 중요한 것은 두 개의 애니메이션을 동시에 재생하는 방법이 아니다.
실제로 구현할 때 먼저 해결해야 하는 문제는 두 Entity를 어떤 위치와 상태로 만들고, 어느 시점부터 하나의 행동으로 묶을 것인가다.
Target 탐색 -> Interaction Point 선택 -> 접근 -> 정렬 -> Pair Animation -> IK 보정 -> 종료
이 흐름을 하나의 Interaction으로 묶어두면 개 쓰다듬기뿐만 아니라 말타기, NPC 대화, 악수, 물건 전달 같은 기능도 같은 구조에서 처리할 수 있다.
결국 Pair Interaction은 Animation System의 기능이라기보다 Entity와 Entity 사이의 관계를 관리하는 시스템에 가깝다.
Animation은 그 관계를 표현하는 수단이고, 실제 구현의 중심은 Interaction Point, Alignment, Movement Control, Animation Sync, IK, State Management에 있다.