애니메이션
개요
애니메이션은 플레이어 입력에 의해 유도되는 움직임 및 동작과 같은 중요한 게임 플레이 피드백을 플레이어에게 제공합니다.
이 문서에서는 Fusion으로 멀티플레이어 게임을 만들 때 적합한 애니메이션 방법을 선택하는 방법에 대해 설명합니다. 예제는 캐릭터 애니메이션에 초점을 맞출 것입니다. 그러나 이 문서에서 제시하는 네트워크를 통해 애니메이션을 동기화하는 개념은 다른 애니메이션 개체에도 적용됩니다.
실무 애니메이션 샘플은 Fusion 애니메이션 기술 예제를 참조하시기 바랍니다.
애니메이션 정확도
Fusion에서 애니메이션을 시작하기 전에 애니메이션 시스템에서 필요로 하고 목표로 하는 베이스라인 정확도를 설정하는 것이 중요합니다.
일반적으로 정확도에 따라 두 가지 유형의 애니메이션 접근 방식이 있습니다:
- 렌더링 정확 애니메이션
- 틱 정확 애니메이션
렌더링 정확 애니메이션
렌더링 정확 애니메이션은 시뮬레이션 외부(Render
또는 MonoBehaviour.Update
호출) 또는 FUN Forward
* 단계에서 실행됩니다. 애니메이션의 시작은 보통 느슨하게 동기화되며 캐릭터 포즈는 재시뮬레이션 중에 이전 상태로 되감기지 않습니다. 전체 애니메이션 상태(현재 재생 애니메이션 시간, 애니메이션 가중 등)는 네트워크 데이터의 일부가 아니므로 지정된 시간에 모든 클라이언트에서 정확하게 동일한 포즈로 캐릭터가 움직이는 것은 보장되지 않습니다.
렌더 정확한 접근 방식을 사용하는 것은 팔다리 위치에 따른 계산이 상당히 어긋날 수 있음을 의미합니다. 부정확한 정도는 주로 애니메이션의 유형에 따라 다릅니다. 멈춰있음 또는 조준과 같은 정적 애니메이션은 충분히 정확할 수 있는 반면, 실행 또는 빠른 동작은 훨씬 더 나쁠 수 있습니다. 시뮬레이션 계산(예: 손바닥으로부터의 레이저 광선 계산)을 위해 애니메이션 계층에서 변환을 사용하지 않는 것이 좋습니다. 그러나 이러한 사용은 거의 필요하지 않습니다. 자세한 정보는 틱 정확한 애니메이션이 필요한가? 섹션을 확인하십시오.
✅ 장점:
- 일반적인 애니메이션 솔루션(애니메이터, 애니멘서)에서의 사용 능력
- 스튜디오 애니메이션 파이프라인을 변경할 필요가 없음
- 타사 애니메이션 추가 기능 제공
❌ 단점:
- 애니메이션 계층의 변환은 정확한 시뮬레이션 계산에 사용할 수 없습니다
- 일부 솔루션을 통해 이를 완화할 수 있음에도 불구하고 애니메이션에 의존하는 히트 박스의 위치가 부정확합니다(애니메이션 및 지연 보상 확인)
FUN Forward
= Forward
시뮬레이션 단계의 FixedUpdateNetwork
호출 (Runner.IsForward == true
)
틱 정확 애니메이션
틱 정확 애니메이션은 애니메이션이 주어진 틱에서 모든 클라이언트와 서버에서 정확히 동일한 상태에 있다는 것을 의미합니다. 즉, 완전한 애니메이션 상태는 네트워크 데이터의 일부이며 애니메이션은 재시뮬레이션 동안 다시 되돌려 재생(또는 오히려 단계적으로 완료) 할 수 있습니다. 모든 클라이언트에서 동일한 포즈의 캐릭터를 갖는 것은 캐릭터 팔다리의 지연 보상 콜라이더에서 특히 중요합니다. 예를 들어, 클라이언트에서 실행 중인 애니메이션 중 다른 플레이어의 다리를 치는 것은 서버에서 정확히 동일한 히트를 생성합니다.
✅ 장점:
- 캐릭터는 모든 클라이언트와 서버에서 동일한 포즈로 언제든지 모든 히트 계산을 정확하게 합니다
- 손바닥에서 나오는 레이저 광선이나 근거리 공격에 대한 히트 박스 계산과 같은 기타 계산을 위한 정확한 팔다리 위치 설정을 기반으로 하는 기능 제공
❌ 단점:
- 맞춤형 애니메이션 솔루션이 필요하기 때문에 실행하기가 어렵습니다(유니티 Animator는 정확한 애니메이션을 지원하지 않습니다)
- 타사 애니메이션 추가 기능이 작동하지 않을 수 있습니다(애니메이터 의존성, 재시뮬레이션 처리 불가 등으로 인해)
- 애니메이션 솔루션은 코드 중심적이어서 애니메이터 컨트롤러와 같은 다른 솔루션에 비해 애니메이션을 설정할 때 개발자 경험이 저하될 수 있습니다
최신 틱 데이터가 서버에서 도착하고 클라이언트 측에서 재시뮬레이션이 시작되면 로컬 예측 틱에 다시 도달할 때까지 새 데이터를 기반으로 로컬 플레이어(= 입력 권한)의 애니메이션을 코드가 재시뮬레이션합니다. 프록시의 경우 원격 시간에서 애니메이션을 만들기 때문에(= 서버의 이미 검증된 데이터를 기반으로) 재시뮬레이션 중에 특별한 처리가 필요하지 않으므로 프록시의 애니메이션은 재시뮬레이션에서 동일합니다.
올바른 지연 보상을 위해서는
HitboxManager
가 히트 박스의 위치를 저장하기 전에 FUN Forward
호출에서 한 번만 프록시 애니메이션을 평가하면 됩니다. 재시뮬레이션 중에는 프록시의 캐릭터 포즈가 실제로 변경되지 않으며 이미 저장된 히트 박스 위치가 올바른 지연 보상 캐스트에 사용됩니다.
FUN Forward
=Forward
시뮬레이션 단계의 FixedUpdateNetwork
메소드 호출 (Runner.IsForward == true
)
틱 정확 애니메이션이 필요한가요?
틱 정확 애니메이션 솔루션을 구축하는 것은 복잡하고 시간이 많이 소요됩니다. 프로젝트에 반드시 필요한지, 추가 작업량이 정당한지 고려해야 합니다. 많은 경우 틱 정확 애니메이션은 불필요하며 플레이어가 차이를 인지하지 못한 채 렌더링 정확 애니메이션으로 프로젝트를 성공적으로 수행할 수 있습니다. 어떤 접근 방식을 선택할지 결정하는 것은 매우 게임에 따라 다릅니다.
일반적으로 프로젝트가 다음 두 가지 경우 중 하나에 해당하는 경우에는 틱 정확 접근 방식이 필요합니다:
- 캐릭터의 애니메이션 부분에 설치된 히트 박스에 100% 정확한 히트가 필요합니다
- 애니메이션의 영향을 많이 받아 수동 계산으로 쉽게 대체할 수 없는 변환 위치를 사용한 계산이 있습니다. 예를 들어 난투전 중 주먹에 연결된 상처 박스를 구동하는 것입니다
렌더링 정확 접근법으로 캐릭터 위치 또는 방향에 기초한 정확한 계산도 달성할 수 있습니다. 단지 애니메이션 변환 계층(= 뼈)에 의해 영향을 받는 변환에 기초하여 계산할 수 없다는 것을 의미합니다. 게임 상태에서 계산된 위치와 시각적 표현은 아래의 예시에서 설명된 바와 같이 다를 수 있습니다.
예1: FPS 게임에서는 애니메이션의 영향을 받는 총의 실제 총통이 아닌 카메라에서 사격 계산을 하는 것이 일반적인 접근 방식입니다. Fusion 발사체에서 발사체의 모습이 실제 경로에 어떻게 보간 되는지 확인하십시오. 그러나 실제로 총의 총통에서 계산을 해야 하고 총의 위치와 회전이 플레이어 애니메이션의 영향을 받는 경우에는 정확한 접근 방식이 필요합니다.
예2: 근거리 공격을 수행할 때, 상처 박스는 공격 중에 미리 정의된 스크립트 경로를 따를 수 있습니다. 그러면 이 경로는 캐릭터 애니메이션에서 독립적으로 재시뮬레이션을 수행할 수 있습니다. 반면, 상처 박스가 애니메이션에서 설계된 것처럼 캐릭터 주먹의 복잡한 경로를 따라야 한다면, 틱 정확 애니메이션이 필요합니다.
렌더링 정확 애니메이션 솔루션
아래는 몇 가지 권장되는 렌더링 정확 애니메이션 솔루션 목록입니다.
노트: 다음 단락에서는 Unity Mecanim Animator를 간단히 Animator라고 부릅니다.
Animator/Animancer + 기존 네트워크 상태
Render accurate approach
일반적으로 이미 존재하는 네트워크 상태를 사용하여 애니메이션을 제어할 수 있습니다. 애니메이션은 Render
메소드로 네트워크 데이터를 기반으로 설정됩니다. 이 솔루션은 구현이 비교적 쉽고 네트워크 자원을 낭비하지 않으며 게임 요건에 따라 더 정확하게 만들 수 있기 때문에 대부분의 게임에 쉽게 추천할 수 있습니다.
C#
public override void Render()
{
_animator.SetFloat("Speed", _kcc.Data.RealSpeed);
}
필요할 때 애니메이션의 추가 데이터를 표준 네트워크 속성을 통해 쉽게 동기화할 수 있습니다.
C#
[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
}
public override void FixedUpdateNetwork()
{
var input = GetInput<PlayerInput>();
if (input.HasValue == false)
return;
if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
{
DoJump();
_jumpCount++;
}
_lastButtonsInput = input.Value.Buttons;
}
public override void Render()
{
if (_jumpCount > _lastVisibleJump)
{
_animator.SetTrigger("Jump");
// Play jump sound/particle effect
}
_lastVisibleJump = _jumpCount;
}
ChangeDetector를 대신 사용하는 유사한 기능:
C#
[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }
private ChangeDetector _changes;
public override void Spawned()
{
_changes = GetChangeDetector(ChangeDetector.Source.SnapshotFrom);
}
public override void FixedUpdateNetwork()
{
var input = GetInput<PlayerInput>();
if (input.HasValue == false)
return;
if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
{
//DoJump();
_jumpCount++;
}
_lastButtonsInput = input.Value.Buttons;
}
public override void Render()
{
foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
{
switch (propertyName)
{
case nameof(_jumpCount):
var reader = GetPropertyReader<int>(nameof(_jumpCount));
var values = reader.Read(previousBuffer, currentBuffer);
if (values.Item2 > values.Item1)
{
_animator.SetTrigger("Jump");
// Play jump sound/particle effect
}
break;
}
}
}
더 간단한 버전의 DetectChanges 블록(아래 참조)을 사용할 수 있습니다. 그러나 이러한 사용은 서버에서 점프가 발생하지 않을 수 있기 때문에 약간 오류가 발생하기 쉽습니다(예: 점프 전에 플레이어가 실제로 상대에 의해 멈춰짐). 로컬 플레이어에 대해 점프를 두 번(로컬 예측 중 값이 증가할 때 한 번, 서버에서 데이터를 수신한 후 값이 이전 값으로 반환될 때 한 번).
C#
foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
{
switch (propertyName)
{
case nameof(_jumpCount):
_animator.SetTrigger("Jump");
}
}
실무 예는 퓨전 애니메이션 기술 예제(예 1-3)을 참조하기 바랍니다.
Animator + NetworkMecanimAnimator
렌더링 정확 접근법
Fusion과 함께 제공되는 내장 NetworkMecanimAnimator
컴포넌트를 사용하여 Animator 속성을 동기화합니다. NetworkMecanimAnimator
컴포넌트에 Animator 속성을 지정하면 모든 클라이언트에 자동으로 동기화됩니다. 예외는 NetworkMecanimAnimator
컴포넌트에서도 설정해야 하는 트리거입니다.(자세한 내용은 매뉴얼의 네트워크 메카님 애니메이터를 참고하세요)
프록시 객체가 갑자기 나타날 때(예: 늦게 참여한 후 또는 관심 지역에 프록시 객체가 들어갈 때) 더 나은 애니메이션 동작을 위해 StateRoot
(애니메이터 컨트롤러의 첫 번째 계층)와 StateLayers
(다른 모든 계층)의 동기화를 활성화합니다.
노트: 이 옵션을 활성화하면 데이터 트래픽이 크게 증가합니다.
실용적인 예는 Fusion 애니메이션 기술 예제(예 4)를 참조하시기 바랍니다.
Animator/Animancer + FSM 동기화
Render accurate approach
점프 상태, 공격 상태, 이동 상태 등 FSM(유한 상태 머신, Finite State Machine) 상태의 애니메이션을 재생합니다. 현재 상태는 네트워크를 통해 동기화되어 애니메이션에 필요한 추가 데이터를 표준 네트워크 속성에 저장할 수 있습니다.
C#
public class JumpState : PlayerState
{
protected override void OnEnterState()
{
DoJump();
}
protected override void OnEnterStateRender()
{
Animator.SetTrigger("Jump");
}
}
애니메이션 제어를 상태로 변경하면 복잡한 애니메이션 설정을 관리하는 데 도움이 되며 애니메이션과 함께 다른 시각 효과(예: 점프 사운드 재생 및 점프 VFX)를 쉽게 제어할 수 있습니다. FSM이 재시뮬레이션 지원과 적절하게 동기화되면 다른 시뮬레이션 로직에도 사용할 수 있습니다. 상태는 실제로 AI 상태 또는 플레이어 동작 상태가 될 수 있습니다.
실용적인 예는 Fusion 애니메이션 기술 예제(예5)를 참조하시기 바랍니다.
틱 정확 애니메이션 솔루션
표준 유니티 Animator는 애니메이션 상태를 제어하는 방법에 대한 옵션이 제한되어 있어 정확한 애니메이션에 사용할 수 없습니다. 그러나 저수준 유니티인 Playables API에서는 이러한 기능을 허용합니다. 따라서 프로젝트가 정확한 애니메이션을 대상으로 한다면 구현은 Playables에 기반해야 할 가능성이 높습니다.
Animancer + 애니멘서 상태 주변에 틱 정확 래퍼
틱 정확 방법
Animancer는 Playables 기반이므로, AnimancerState
속성들은 동기화될 수 있으며 Animancer는 틱 정확 애니메이션을 위해 FixedUpdateNetwork
에서 수동으로 Animancer 단계를 진행해 틱 정확한 애니메이션을 구현할 수 있습니다.
이 접근 방식은 "틱 정확으로 충분"이 프로젝트에 적합할 때만 권장됩니다. Animancer는 백그라운드에서 약간의 마법을 수행하고 있기 때문에 (특정 페이드를 진행하는 동안 무중력 상태의 생성과 같이) 모든 기능으로 100% 틱 정확도를 달성하는 것은 시간이 많이 걸릴 수 있으며 Playables을 기반으로 한 사용자 지정 솔루션을 즉시 사용하는 것이 바람직한 장기 옵션이 될 수 있습니다.
- 모든
AnimancerState
의 속성을 동기화합니다 - 현재 전체 상태 배열(
AnimancerLayer.States
)을 그대로 동기화합니다
솔루션 2를 사용하면 프로젝트에 필요한 애니메이션 솔루션의 전반적인 복잡성에 따라 더 쉽고 많은 문제를 해결할 수 있습니다.
Playables API 위의 커스텀 솔루션
틱 정확 방식
Playables API는 이 매뉴얼의 범위 밖에 있습니다. 일반적인 아이디어는 애니메이션 상태를 나타내는 맞춤형 데이터 구조체를 사용하여 네트워크를 통해 필요한 데이터를 동기화하고 PlayableGraph
를 구축하는 것입니다. 그런 다음 프로젝트의 필요에 따라 PlayableGraph
를 표준 Fusion 메소드로 평가합니다.
Fusion BR 샘플에서 배틀 테스트된 코드 기반 솔루션 또는 정제되고 간소화된 버전을 확인하세요(예 6).
레거시 유니티 애니메이션 + AnimationStates 주변에 틱 정확 래퍼
틱 정확 방식
사용자 지정 데이터 구조를 사용하여 AnimationStates
을 플레이하는 데이터를 동기화합니다. Animation.Sample
을 사용하여 틱에 맞는 올바른 캐릭터 포즈를 얻을 수 있습니다.
프로젝트의 애니메이션 파이프라인이 이미 레거시 애니메이션 시스템을 사용하지 않는 한 이 솔루션은 권장되지 않습니다.
애니메이션과 지연 보상
지연 보상은 한 클라이언트에 등록된 히트가 서버에서도 올바르게 인식되도록 합니다.
히트 박스가 애니메이션의 영향을 받거나 애니메이션에 의해 구동되는 경우, 히트 박스의 상태를 캡처하기 전에 캐릭터가 올바른 자세로 있는지 확인하는 것이 중요합니다. 히트 박스 데이터는 FUN Forward
호출에서 HitboxManager
에 의해 자동으로 캡처됩니다. 재시뮬레이션을 위해 이미 저장된 히트 박스의 위치가 올바른 레이캐스트 계산에 사용되므로 프록시 캐릭터의 애니메이션은 시간을 되돌릴 필요가 없습니다. 이것이 바로 렌더 정확한 접근 방식으로도 정확하게 실행될 경우 정확한 충분한 지연 보상 히트가 발생할 수 있는 이유입니다.
서버는 클라이언트가 원격 타임프레임에서 프록시 캐릭터를 렌더링하는 것으로 가정하므로 이 타이밍은 지연 보상된 캐스트에 대해 서버에서 사용됩니다. 따라서 클라이언트의 애니메이션도 프록시의 원격 타임프레임을 존중할 필요가 있습니다. 즉, 클라이언트의 프록시는 정확한 지연 보상을 위해 보간된 데이터를 기반으로 애니메이션 되어야 합니다.
보간 된 애니메이션 상태 데이터는 모든 FUN Forward
그리고 Render
의 프록시에 사용되어 주어진 틱 또는 렌더링 시간에 캐릭터 포즈를 재구성할 수 있으므로 정확한 틱 애니메이션은 정확한 타이밍을 갖는 것이 더 좋고 정확합니다. 이 경우 프록시 캐릭터는 서버에서 보간 된 데이터를 보여줄 뿐 애니메이션 자체를 플레이하는 것은 아닙니다.
반면에 틱 정확 애니메이션은 프록시 캐릭터에서 완전히 실행됩니다. 중요한 것은 파라미터를 Animator로 설정하거나 애니메이션 클립을 시작하는 시점입니다. 간단한 접근 방식은 전체 정확도를 무시하고 최신 네트워크 데이터를 기반으로 동작하는 것입니다(아래 예 참조). 그러나 더 정확한 애니메이션을 위해서는 보간 된 데이터를 기반으로 애니메이션 파라미터를 설정하거나 클립을 재생하기 시작해야 합니다.
또한 일반적으로 주기적으로 또는 특별한 경우(예: 클라이언트의 관심 영역에 입장하는 캐릭터)에 애니메이션 재생을 동기화해야 합니다. 자세한 내용은 렌더링 정확 상태 동기화를 위한 팁 섹션을 참조하십시오.
최신 네트워크 데이터를 기반으로 렌더링 정확 애니메이션(_speed
, _jumpCount
):
C#
[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
}
public override void FixedUpdateNetwork()
{
// _speed and _jumpCount is changing here
}
public override void Render()
{
_animator.SetFloat("Speed", _speed);
if (_lastVisibleJump < _jumpCount)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = _jumpCount;
}
보간 된 데이터를 기반으로 정확한 애니메이션을 렌더링 합니다. 보간기를 사용하면 복잡성이 약간 증가하는 대신 애니메이션이 더 정확해집니다:
C#
[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }
public override void FixedUpdateNetwork()
{
// _speed and _jumpCount is changing here
}
public override void Render()
{
var interpolator = new NetworkBehaviourBufferInterpolator(this);
_animator.SetFloat("Speed", interpolator.Float(nameof(_speed)));
int interpolatedJumpCount = interpolator.Int(nameof(_jumpCount));
if (_lastVisibleJump < interpolatedJumpCount)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = interpolatedJumpCount;
}
팁
정확한 상태 동기화를 위한 팁
렌더링 정확 접근 방식은 애니메이션 상태 동기화에 초점을 맞추는 것이 아니라 변경 사항을 동기화하고 변경 사항을 정확한 시간에 적용하는 데 중점을 둡니다. 그러나, 렌더링 정확 접근 방식의 정밀도를 높이기 위해서는 적어도 어떤 종류의 상태 동기화를 구현하는 것이 더 좋습니다. 이는 주기적으로 또는 특정 이벤트에서 캐릭터 애니메이션의 현재 플레이 상태 및 특히 현재 상태 시간을 동기화하는 것을 의미합니다.
예를 들어, 프록시 캐릭터가 실행 중이며 속도는 얼마인지 알 수 있습니다. 그러나 (게임에 참여한 후, 프록시 캐릭터가 로컬 플레이어의 관심 영역에 입장하는 등) 처음으로 애니메이터에 이러한 값이 설정되면 원격 플레이어가 이미 20초 동안 실행 중임에도 처음부터 이동 루프를 재생합니다. 이로 인해 클라이언트 간에 애니메이션 타이밍이 달라지므로 애니메이션 동기화가 부정확해집니다. 이 문제는 원격 플레이어가 점프와 같은 다른 동작을 수행할 때 보통 해결되지만 플레이어가 이러한 동작을 수행하기 전까지는 애니메이션 타이밍이 상당히 꺼질 수 있습니다.
유니티 Animator의 경우 Animator.GetCurrentAnimatorStateInfo()
를 사용하여 현재 상태의 전체 경로 해시 및 정규화된 시간을 가져와 표준 네트워크 속성으로 동기화한 후 Animator.Play(int stateNameHash, int layer, float normalizedTime)
을 호출하여 프록시 캐릭터에 적용할 수 있습니다. 그러나 동기화는 원하지 않는 시각적 결함을 방지하기 위해 Animator가 전이 상태일 때만 수행해야 합니다.
StateRoot
동기화를 사용하도록 설정한 경우에 NetworkMecanimAnimator
가 수행하는 작업입니다. 이것은 첫 번째 레이어에서 현재 재생 중인 애니메이션 상태와 시간을 동기화합니다. Animator가 상태 간에 전환할 때를 제외하고는 계속해서 동기화합니다. 값이 지속적으로 변하고 네트워크 리소스가 소모되므로 정규화된 시간을 연속적으로 동기화하는 것은 이상적이지 않습니다.
틱 정확 상태 동기화를 위한 팁
애니메이션 상태가 Playables
, AnimancerStates
또는 유니티의 AnimationStates
를 기반으로 구동될 때는 올바른 재계산에 필요한 데이터만 동기화해야 하며 시간에 따른 값 변경은 동기화하지 않는 것이 이상적입니다. 예를 들어, Time 및 Weight변경 사항을 모든 틱 표시마다 전송할 필요는 없지만, 모든 클라이언트의 현재 Weight 및 Time 값을 계산하려면 상태의 StartTick, FadeSpeed and TargetWeight를 동기화하는 것이 중요합니다.