애니메이션
개요
애니메이션은 플레이어 입력에 의해 유도된 움직임 및 동작과 같은 중요한 게임 플레이 피드백을 제공합니다.
이 문서에서는 Fusion으로 멀티플레이어 게임을 제작할 때 애니메이션에 적합한 접근 방식을 선택하는 방법에 대해 설명합니다. 예제는 캐릭터 애니메이션에 중점을 두지만, 이 문서에 제시된 네트워크를 통해 애니메이션을 동기화하는 개념은 다른 애니메이션 객체에도 적용할 수 있습니다.
애니메이션 정확도
Fusion에서 애니메이션을 시작하기 전에 애니메이션 시스템이 필요로 하고 대상으로 삼는 기준 정확도를 설정하는 것이 중요합니다.
일반적으로 애니메이션 접근 방식에는 정확도에 따라 두 가지 유형이 있습니다.
- 정확한 애니메이션 체크
- 정확한 애니메이션 렌더링
노트: 대부분의 경우 정확한 애니메이션을 렌더링 하는 것으로 충분합니다.
틱 정확 애니메이션
틱 정확 애니메이션은 애니메이션은 지정된 틱에서 모든 클라이언트와 서버에서 정확히 동일한 상태에 있음을 의미합니다. 즉, 전체 애니메이션 상태는 네트워크 데이터의 일부이며 애니메이션은 재시뮬레이션 중에 다시 재생(또는 단계적 재생)할 수 있습니다. 모든 클라이언트에서 동일한 포즈를 취하는 캐릭터는 캐릭터 팔다리의 지연 보상 콜라이더에 특히 중요합니다. 예를 들어, 클라이언트에서 실행 애니메이션 도중에 다른 플레이어의 다리를 치면 서버에서 정확히 동일한 히트가 발생합니다.
✅ 장점:
- 캐릭터는 모든 클라이언트와 서버에서 항상 동일한 포즈를 취하며 모든 히트 계산을 정확하게 합니다.
- 손에서 발생하는 레이저 광선 또는 근거리 공격에 대한 피해 박스 계산과 같은 다른 계산을 위해 정확한 팔/다리의 위치를 구축할 수 있는 기능을 제공합니다.
❌ 단점:
- 사용자 지정 애니메이션 솔루션이 필요하기 때문에 성공하기 어렵습니다(유니티 애니메이터는 정확한 틱 애니메이션을 지원하지 않습니다).
- 타사 애니메이션 추가 기능이 작동하지 않을 수 있습니다(애니메이터에 대한 종속성, 재시뮬레이션을 처리할 수 없음 등).
- 애니메이션 솔루션은 코드 중심적일 수 있으므로 애니메이터 컨트롤러와 같은 다른 솔루션에 비해 애니메이션을 설정할 때 개발자의 경험이 더 나빠질 수 있습니다.
서버에서 가장 최근의 틱 데이터가 도착하고 클라이언트 측에서 재시뮬레이션이 시작되면 코드는 로컬 예측 틱에 다시 도달할 때까지 새로운 데이터를 기반으로 로컬 플레이어(= 입력 권한)의 애니메이션을 재시뮬레이션합니다. 프록시의 경우 스냅샷 보간 시간(=서버에서 이미 확인된 데이터를 기반)으로 에 애니메이션을 만들기 때문에 재시뮬레이션 중에 특별한 처리가 필요하지 않습니다. 따라서 프록시 애니메이션은 재시뮬레이션과 동일합니다.
정확한 지연 보상을 위해서는
HitboxManager
가 히트 박스의 위치를 저장하기 전에 FUN Forward
*호출에서 프록시 애니메이션을 한 번 평가하면 충분합니다. 재시뮬레이션 동안 프록시의 캐릭터 포즈는 실제로 변경되지 않으며 이미 저장된 히트 박스 위치가 올바른 지연 보상 캐스팅에 사용됩니다.
FUN Forward
= FixedUpdateNetwork
메소드는 Forward
시뮬레이션 스테이지 (Runner.IsForward == true
)로 호출
렌더링 정확한 애니메이션
정확한 애니메이션 렌더링은 시뮬레이션 밖에서 실행됩니다(Render
, MonoBehaviour.Update
, OnChanged
호출) 또는 FUN Forward
단계에. 애니메이션의 시작은 일반적으로 느슨하게 동기화될 뿐이고 캐릭터 포즈는 재시뮬레이션 동안 이전 상태로 다시 감기지 않습니다. 즉, 팔/다리 위치에 따른 계산이 상당히 빗나갈 수 있습니다. 정확하지 않은 정도는 애니메이션의 유형에 따라 크게 달라집니다. Idle 또는 Aim과 같은 정적 애니메이션은 충분히 정확할 수 있지만 실행 중이거나 빠른 동작은 훨씬 더 나쁠 수 있습니다.
✅ 장점:
- 일반적인 애니메이션 솔루션을 사용할 수 있음(Animator, Animancer).
- 스튜디오 애니메이션 파이프라인을 변경할 필요 없음.
- 타사 애니메이션 추가 기능을 즉시 사용 가능
❌ 단점:
- 애니메이션에 의존하는 히트 박스의 위치가 정확하지 않습니다. 일부 솔루션을 사용하면 완화될 수 있습니다.(Animations 및 래그 보상 확인)
- 애니메이션 계층의 변환을 정확한 시뮬레이션 계산에 사용할 수 없음
틱 정확 애니메이션이 필요한가요?
틱 표시 정확 애니메이션 솔루션을 구축하는 것은 복잡하고 시간이 많이 소요됩니다. 프로젝트에 꼭 필요한지 여부와 추가 작업량이 타당한지 여부를 고려하십시오. 대부분의 경우 틱 정확한 애니메이션이 필요하지 않으며 플레이어가 차이를 알아차리지 않고도 정확한 애니메이션을 렌더링 하여 프로젝트를 성공적으로 실행할 수 있습니다. 어떤 접근 방식을 선택할지 결정하는 것은 게임마다 다릅니다.
일반적으로 프로젝트가 다음 두 가지 경우 중 하나에 해당할 경우 정확한 틱 방법론이 필요합니다.
- 캐릭터의 애니메이션 부분에 배치된 히트 박스에 100% 정확한 히트 필요
- 애니메이션의 영향을 많이 받고 수동 계산으로 쉽게 대체할 수 없는 변환 위치를 사용한 계산 존재(예: 근거리 공격 중 주먹에 연결된 대미지 박스를 구동하는 경우).
캐릭터 위치 또는 방향에 기반한 정확한 계산은 렌더 정확한 접근 방식으로 달성될 수 있습니다. 즉, 애니메이션 변환 계층 구조(=골격)의 영향을 받는 변환을 기반으로 계산을 수행할 수 없습니다. 게임 상태에서 계산된 위치와 시각적 표현은 아래 예에서 설명한 대로 다를 수 있습니다.
예 1: FPS 게임에서는 애니메이션의 영향을 받는 총의 실제 총열이 아닌 카메라에서 사격 계산을 수행하는 것이 일반적인 접근 방식입니다. Fusion 발사체에서 발사체의 비주얼이 실제 경로에 어떻게 보간 되는지 확인합니다. 그러나 실제로 총의 총열에서 계산을 해야 하고 총의 위치와 회전이 플레이어 애니메이션의 영향을 받는다면, 틱 정확한 접근 방법이 필요합니다.
예 2: 근거리 공격을 수행할 때 공격 중에 대미지 박스는 미리 정의된 스크립트 경로를 따를 수 있습니다. 그러면 이 경로는 캐릭터 애니메이션에서 독립적으로 재시뮬레이션 중에 실행될 수 있습니다. 한편, 애니메이션에서 디자인된 것처럼 대미지 박스가 캐릭터의 주먹의 복잡한 경로를 따라가야 한다면, 틱 정확한 애니메이션이 필요합니다.
렌더링 정확 애니메이션 솔루션
아래는 몇 가지 권장되는 정확한 렌더링 애니메이션 솔루션 목록입니다.
노트: 다음 단락에서 유니티 메카님 애니메이터는 단순히 Animator라고 불립니다.
Animator/Animancer + 기존 네트워크 상태
렌더링 정확 접근법
일반적으로 이미 존재하는 네트워크 상태를 사용하여 애니메이션을 제어할 수 있습니다. 애니메이션은 Render
메소드의 네트워크 데이터나 OnChanged
콜백을 기반으로 설정됩니다. 이 솔루션은 구현이 합리적이고 네트워크 리소스를 낭비하지 않으며 게임 요건에 따라 보다 정확하게 만들 수 있으므로 대부분의 게임에 쉽게 권장할 수 있습니다.
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()
{
if (Object.IsProxy == true)
return;
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;
}
OnChanged 콜백을 대신 사용하는 유사한 기능은 다음과 같습니다:
C#
[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked(OnChanged = nameof(OnJumpChanged))]
private int _jumpCount { get; set; }
public override void FixedUpdateNetwork()
{
if (Object.IsProxy == true)
return;
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 static void OnJumpChanged(Changed<Player> changed)
{
changed.LoadOld();
int previousJumpCount = changed.Behaviour._jumpCount;
changed.LoadNew();
if (changed.Behaviour._jumpCount > previousJumpCount)
{
changed.Behaviour._animator.SetTrigger("Jump");
// Play jump sound/particle effect
}
}
OnChanged 콜백의 간단한 버전을 사용할 수 있습니다(아래 참조). 그러나 이러한 사용은 서버에서 점프가 발생하지 않을 수 있기 때문에 약간 오류가 발생하기 쉽습니다(예: 점프를 하기 전에 상대방이 실제로 정지한 경우). 그러면 로컬 플레이어에 대해 점프가 두 번 트리거 됩니다(로컬 예측 중에 값이 증가된 경우 한 번, 값이 서버로부터 데이터를 다시 수신 후 한 번 ).
C#
public static void OnJumpChanged(Changed<Player> changed)
{
changed.Behaviour._animator.SetTrigger("Jump");
}
Animator + NetworkMecanimAnimator
렌더링 정확 접근법
애니메이터 속성 동기화를 위해 Fusion에 있는 빌트인 NetworkMecanimAnimator
컴포넌트를 사용하세요. NetworkMecanimAnimator
컴포넌트에 애니메이터를 할당 후, 애니메이터 속성을 할당하면 NetworkMecanimAnimator
는 자동으로 모든 클라이언트와 동기화합니다. 예외는 NetworkMecanimAnimator
컴포넌트를 통해서도 설정해야 하는 트리거들입니다(상세 내용은 설명서의 사전 구축된 컴포넌트페이지를 확인하세요).
프록시 객체가 갑자기 나타날 때(예: 늦은 참여 후 또는 프록시 객체가 관심 영역에 진입할 때) 더 나은 애니메이션 동작을 위해 'StateRoot
(애니메이터 컨트롤러의 첫 번째 레이어)와 StateLayers
(다른 모든 레이어)의 동기화를 사용하도록 설정합니다.
노트: 이 옵션을 활성화하면 데이터 트래픽이 상당히 증가합니다.
Animator/Animancer + FSM 동기화
렌더링 정확 접근 방식
FSM(유한 상태 머신) 상태의 애니메이션을 플레이합니다(예: 점프 상태, 공격 상태, 이동 상태). 현재 상태는 네트워크를 통해 동기화되며 애니메이션에 필요한 추가 데이터를 표준 네트워크 속성에 저장할 수 있습니다.
C#
public class JumpState : PlayerState
{
protected override void OnEnterState()
{
DoJump();
}
protected override void OnEnterStateRender()
{
Animator.SetTrigger("Jump");
}
}
애니메이션 제어를 상태로 전환하면 복잡한 애니메이션 설정을 관리하는 데 도움이 되며 애니메이션과 함께 다른 시각적 효과를 쉽게 제어할 수 있습니다(예: 점프 상태로 전환 시 점프 사운드 플레이 및 점프 VFX 플레이). FSM이 재시뮬레이션 지원과 적절하게 동기화되면 다른 시뮬레이션 로직에 사용될 수 있습니다. 상태는 실제로 AI 상태 또는 플레이어 동작 상태가 될 수 있습니다.
틱 정확 애니메이션 솔루션
표준 유니티 Animator에는 애니메이션 상태를 제어하는 방법에 대한 옵션이 제한되어 있으므로 정확한 틱 애니메이션에 사용할 수 없습니다. 그러나 유니티의 하위 레벨 Playables API는 이러한 기능을 허용합니다. 따라서 프로젝트가 틱 정확한 애니메이션을 목표로 한다면, 구현은 Playables를 기반으로 해야 할 것입니다.
Animancer + 틱 정확 래퍼 어라운드 애니멘서 상태
틱 정확 접근 방식
Animancer가 Playables을 기반으로 하기 때문에, AnimancerState
속성은 동기화될 수 있으며, Animancer는 FixedUpdateNetwork
네트워크에서 수동으로 애니메이션을 단계화하여 틱 정확한 애니메이션을 만들 수 있습니다.
이 접근 방법은 프로젝트에 "충분하게 틱 정확"할 수 있는 경우에만 권장됩니다. Animancer는 백그라운드에서 마법을 부리기 때문에(예를 들어 특정 페이드 동안 무중력 상태 생성) 모든 Animancer 기능으로 100% 틱 정확도를 달성하는 것은 시간이 걸릴 수 있으며 Playables를 기반으로 하는 사용자 지정 솔루션을 즉시 사용하는 것이 장기적인 옵션이 될 수 있습니다.
1. 모든
AnimancerState
의 속성을 개별적으로 동기화2. 전체 상태 배열(
AnimancerLayer.States
)을 동기화합니다. 현재 상태솔루션 2를 사용하면 프로젝트에 필요한 애니메이션 솔루션의 전반적인 복잡성에 따라 더 쉽고 더 많은 방탄 효과를 얻을 수 있습니다.
Playables API 기반의 사용자 지정 솔루션
틱 정확 접근 방식
Playables API는 설명서의 범위 밖이지만 일반적인 아이디어는 애니메이션 상태를 나타내는 사용자 지정 데이터 구조체를 사용하여 네트워크를 통해 필요한 데이터를 동기화하고 PlayableGraph
를 구성하는 것입니다. 그런 다음 표준 Fusion 방법에서 프로젝트의 요건에 따라 PlayableGraph
를 평가합니다.
배틀 테스트된 코드 기반 솔루션에 대해서는 Fusion BR 샘플을 참고하세요.
이전 유니티 애니메이션 + 틱 정확 래퍼 AnimationStates
틱 정확 접근 방식
AnimationStates
플레이에 대한 데이터 동기화에 대해서는 사용자 지정 데이터 구조체를 사용하세요.
. 틱에 대하여 올바른 캐릭터 포즈를 달성하기 위해 Animation.Sample
을 참고하세요.
프로젝트의 애니메이션 파이프라인이 이미 이전 애니메이션 시스템을 사용하고 있지 않은 경우 이 솔루션은 권장되지 않습니다.
애니메이션 및 지연 보상
지연 보상은 한 클라이언트에 등록된 히트가 서버에서도 올바르게 인식됨을 보장합니다.
히트 박스가 애니메이션의 영향을 받거나 애니메이션에 의해 구동되는 경우, 히트 박스의 상태가 캡처되기 전에 캐릭터가 올바른 자세를 취하고 있는지 확인하는 것이 중요합니다. 히트 박스 데이터는 FUN Forward
호출의 HitboxManager
에 의해 자동으로 캡처됩니다. 재시뮬레이션을 위해 이미 저장된 히트 박스의 위치가 올바른 레이캐스트 계산에 사용되므로 프록시 캐릭터의 애니메이션은 시간을 거슬러 올라갈 필요가 없습니다. 이것이 바로 렌더 정확한 접근 방식조차도 정확하게 실행되면 충분한 지연 보정 적중률을 얻을 수 있는 정확한 이유입니다.
HitboxManager
전에 애니메이션을 평가해야 하는 경우, [OrderBefore(typeof(HitboxManger)]
속성을 먼저 실행해야 하는 SimulationBehaviour
또는 NetworkBehaviour
스크립트에서 사용할 수 있습니다.
서버는 클라이언트가 스냅샷 보간 시간에 프록시 캐릭터를 렌더링 한다고 가정하여 이 타이밍이 지연 보상된 캐스트에 대해 서버에서 사용됩니다. 따라서 클라이언트의 애니메이션은 프록시의 스냅샷 보간 타이밍도 준수해야 합니다. 즉, 클라이언트의 프록시는 정확한 지연 보정을 위해 보간 된 데이터를 기반으로 애니메이션화되어야 합니다.
보간 애니메이션은 모든 FUN Forward
와 Render
의 프록시에 데이터를 사용하여 주어진 틱 또는 렌더 시간에 캐릭터 포즈를 재구성할 수 있으므로 틱 정확 애니메이션으로 정확한 애니메이션 타이밍을 갖는 것이 더 좋고 더 정확하게 달성됩니다. 이 경우 프록시 캐릭터는 서버의 보간 된 데이터만 표시하며, 실제로 애니메이션을 "플레이"하지는 않습니다.
반면에 프록시 캐릭터에서 정확한 애니메이션을 완전히 실행합니다. 중요한 것은 매개 변수가 애니메이터로 설정되거나 애니메이션 클립이 시작되는 시간입니다. 간단한 접근 방식은 전체 정확도를 무시하고 최신 네트워크 데이터를 기반으로 행동하는 것입니다(아래 예 참조). 그러나 보다 정확한 애니메이션을 위해서는 애니메이션 파라미터를 설정하거나 보간 된 데이터를 기반으로 클립 플레이를 시작해야 합니다.
또한 주기적으로 또는 특별한 경우(예: 캐릭터가 클라이언트의 관심 영역에 들어가는 경우)에 플레이하는 애니메이션(애니메이션 상태 및 상태 시간)을 동기화해야 합니다. 자세한 내용은 렌더링 정확 상태 동기화 팁 섹션을 참조하십시오.
렌더링 정확 애니메이션은 최신의 네트워크 된 데이터 (_jumpCount
)를 기반으로 합니다:
C#
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
}
public override void FixedUpdateNetwork()
{
if (HasJumpInput() == true)
{
DoJump();
_jumpCount++;
}
}
public override void Render()
{
if (_lastVisibleJump < _jumpCount)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = _jumpCount;
}
보간 데이터(_jumpCountInterpolator
)를 기반으로 정확한 애니메이션을 렌더링 합니다. 보간기를 사용하면 복잡성이 증가하지만 애니메이션의 정확도를 높일 수 있습니다.
C#
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
private Interpolator<int> _jumpCountInterpolator;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
_jumpCountInterpolator = GetInterpolator<int>(nameof(_jumpCount));
}
public override void FixedUpdateNetwork()
{
if (HasJumpInput() == true)
{
DoJump();
_jumpCount++;
}
}
public override void Render()
{
if (_jumpCountInterpolator.Value > _lastVisibleJump)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = _jumpCountInterpolator.Value;
}
팁
정확한 상태 동기화를 렌더링 하기 위한 팁
렌더링 정확 접근 방식은 애니메이션 상태 동기화에 중점을 두지 않고 변경사항을 동기화하고 정확한 시간에 적용하는 데 중점을 둡니다. 그러나 렌더링 정확 접근 방식의 정밀도를 높이려면 적어도 일종의 상태 동기화를 구현하는 것이 좋습니다. 즉, 현재 플레이 상태, 특히 캐릭터 애니메이션의 현재 상태 시간을 주기적으로 또는 특정 이벤트에서 동기화합니다.
예를 들어 프록시 캐릭터가 달리는 중이고 그 속도가 어느 정도인지 알 수 있습니다. 그러나 애니메이터에서 이러한 값을 처음 설정하면(게임 참여 후, 프록시 캐릭터가 로컬 플레이어의 관심 영역에 들어가는 등), 원격 플레이어가 이미 20초 동안 실행 중임에도 불구하고 처음부터 이동 루프를 재생합니다. 이로 인해 클라이언트 간에 애니메이션 타이밍이 달라져 애니메이션 동기화가 부정확해집니다. 이 문제는 일반적으로 원격 플레이어가 점프와 같은 다른 작업을 수행할 때 해결되지만, 플레이어가 이러한 작업을 수행할 때까지 애니메이션 타이밍이 상당히 흐트러질 수 있습니다.
유니티 애니메이터 Animator.GetCurrentAnimatorStateInfo()
를 사용하여 현재 상태의 전체 경로 해시와 정규화된 시간을 가져오고 이를 표준 네트워크 속성으로 동기화한 후 Animator.Play(int stateNameHash, int layer, float normalizedTime)
를 호출하여 프록시 캐릭터에 적용할 수 있습니다. 그러나 원하지 않는 시각적 결함을 방지하기 위해 애니메이터가 전환되지 않은 경우에만 동기화가 수행되어야 합니다.
StateRoot
동기화를 사용할 때 NetworkMecanimAnimator
가 하는 것입니다. 첫 번째 레이어에서 현재 플레이 중인 애니메이션 상태와 해당 시간을 동기화합니다. 애니메이터가 상태 간에 전환되는 경우를 제외하고 지속적으로 동기화합니다. 정규화된 시간을 지속적으로 동기화하는 것은 값이 지속적으로 변경되고 네트워크 리소스가 소모되기 때문에 이상적이지 않습니다.
틱 정확 상태 동기화에 대한 팁
애니메이션 상태가 Playables
, AnimancerStates
또는 유니티의 AnimationStates
에 기반하여 구동되는지 여부는 정확한 재계산에 필요한 데이터만 동기화해야 하며 시간 경과에 따른 값 변경은 동기화하지 않는 것이 이상적입니다. 예를 들어 틱 표시마다 Time 및 Weight 변경 사항을 보낼 필요는 없지만 모든 클라이언트에서 현재 Weight 및 TargetWeight 값을 계산하려면 주의 StartTick, FadeSpeed 및 TargetWeight를 동기화하는 것이 중요합니다.