quantum | v2 switch to v1  

애니메이션

개요

Quantum에는 애니메이션을 처리하는 2가지의 방식이 존재합니다:

  • 유니티에서 게임 상태를 폴링, 그리고
  • 사용자 지정 애니메이터를 사용한 결정론적 애니메이션.

메인 화면으로
 

폴링 기반 애니메이션

대부분의 게임은 애니메이션으로 객체의 상태를 플레이어에게 전달합니다. 예를 들어, 플레이 가능한 캐릭터가 걷거나 점프할 때 애니메이션은 실제로 제자리에서의 애니메이션이 되며 인식된 이동은 코드에 의해 이루어집니다.

즉, 캐릭터의 각각의 (유니티) 애니메이터를 관리하는 스크립트는 상태가 없으며 게임 시뮬레이션에서 폴링 된 데이터를 기반으로 하여 애니메이션 매개 변수로 전달할 값을 도출합니다(Quantum).

주의: 게임 플레이 시스템이 루트 모션에 의존하거나 애니메이션 상태를 인지해야 하는 경우 다음 섹션으로 건너뜁니다.

폴링 기반 애니메이션 개념은 API 샘플에서 구현되었습니다. 다음 코드는 PlayerAnimations 스크립트에서 발췌하여 이동 애니메이션을 구동합니다.

  1. 엔터티가 인스턴스화되면 엔터티의 EntityRef와 현재의 QuantumGame을 캐시 하는 Initialize() 메소드가 호출되며, 후자는 전적으로 편의를 위한 것입니다.
  2. 모든 유니티 Update()에서는 MovementAnimation() 함수가 호출됩니다.
  3. MovementAnimation() 함수는 이전에 캐시 된 EntityRef를 사용하여CharacterController3D의 데이터를 폴링 합니다.
  4. 관련 애니메이터 파라미터는 폴링 된 데이터에서 파생됩니다.
  5. 계산 된 데이터는 유니티 애니메이터로 전달됩니다.
// This snippet is extracted from the Quantum API Sample.

public unsafe class PlayerAnimation : MonoBehaviour
{
    [SerializeField] private Animator _animator = null;
    
    private EntityRef _entityRef = default;
    private QuantumGame _game = null;
    
    // Animator Parameters
    private const string FLOAT_MOVEMENT_SPEED = "floatMovementSpeed";
    private const string FLOAT_MOVEMENT_VERTICAL = "floatVerticalMovement";
    private const string BOOL_IS_MOVING = "boolIsMoving";

    // This method is called from the PlayerSetup.cs Initialize() method which is registered 
    // to the EntityView's OnEntityInstantiated event located on the parent GameObject
    public void Initialize(PlayerRef playerRef, EntityRef entityRef){
        _playerRef = playerRef;
        _entityRef = entityRef;
        _game = QuantumRunner.Default.Game;
    }
    
    // Update is called once per frame
    void Update(){
        MovementAnimation();
    }

    private void MovementAnimation() {
        var kcc = _game.Frames.Verified.Unsafe.GetPointer<CharacterController3D>(_entityRef);
        bool isMoving = kcc->Velocity.Magnitude.AsFloat > 0.2f;
        
        _animator.SetBool(BOOL_IS_MOVING, isMoving);
        
        if (isMoving) {
            _animator.SetFloat(FLOAT_MOVEMENT_SPEED, kcc->Velocity.Magnitude.AsFloat);
            _animator.SetFloat(FLOAT_MOVEMENT_VERTICAL, kcc->Velocity.Z.AsFloat);
        }
        else {
            _animator.SetFloat(FLOAT_MOVEMENT_SPEED, 0.0f);
            _animator.SetFloat(FLOAT_MOVEMENT_VERTICAL, 0.0f);
        }
    }

메인 화면으로
 

이벤트 트리거

일부 애니메이션은 플레이어가 점프를 누르거나 적에게 맞는 등 게임에서 발생하는 특정 이벤트를 기반으로 합니다. 이러한 경우에는 시뮬레이션에서 이벤트를 발생시키고 뷰가 이를 리슨 하는 것이 좋습니다. 이를 통해 디커플링을 보장하고 폴링 기반 애니메이션 접근 방식과 함께 잘 작동합니다.

이벤트 및 콜백에 대한 자세한 설명은 매뉴얼의 Quantum ECS > 게임 콜백 페이지를 참조하세요.

API 샘플은 이벤트를 사용하여 시각적 반응을 나타내야 하는 시간 엄수 동작(예: 플레이 가능한 캐릭터 점프)을 전달합니다.

Quantum의 MovementSystem은 플레이어 입력을 읽고 이동 값을 계산한 후 CharacterController3D로 전달합니다. 시스템은 점프 키를 누르는 소리도 들립니다. 키가 WasPressed이면 PlayerJump 이벤트를 발생시키고 CharacterController3DJump() 메소드를 호출합니다.

using Photon.Deterministic;
namespace Quantum
{
    public unsafe struct PlayerMovementFilter
    {
        public EntityRef EntityRef;
        public PlayerID* PlayerID;
        public Transform3D* Transform;
        public CharacterController3D* Kcc;
    }
    
    unsafe class MovementSystem : SystemMainThreadFilter<PlayerMovementFilter>
    {
        public override void Update(Frame f, ref PlayerMovementFilter filter)
        {
            var input = f.GetPlayerInput(filter.PlayerID->PlayerRef);
            
            // Other Logic

            if (input->Jump.WasPressed)
            {
                f.Events.PlayerJump(filter.PlayerID->PlayerRef);
                filter.Kcc->Jump(f);
            }

            // Other Logic
        }
    }
}

유니티 쪽에서는 PlayerAnimation 스크립트가 PlayerJump 이벤트를 듣고 반응합니다. 이를 위해 다음 단계가 필요합니다.

  1. 이벤트를 수신할 수 있는 메소드를 정의합니다- void Jump(EventPlayerJump e).
  2. 해당 이벤트를 구독합니다.
  3. 이벤트가 수신되면 이벤트에 포함된 PlayerRef와 이전에 캐시 된 PlayerRef를 비교하여 스크립트가 위치한 GameObject 인지 확인합니다.
  4. 유니티 애니메이터에서 파라미터를 트리거 / 설정합니다.
// This snippet is extracted from the Quantum API Sample.

public unsafe class PlayerAnimation : MonoBehaviour
{
    [SerializeField] private Animator _animator = null;
    
    private PlayerRef _playerRef = default;
    private QuantumGame _game = null;
    
    // Animator Parameters
    private const string TRIGGER_JUMP = "triggerJump";
    
    public void Initialize(PlayerRef playerRef, EntityRef entityRef)
    {
        _playerRef = playerRef;
        
        // Other Logic
        
        QuantumEvent.Subscribe<EventPlayerJump>(this, Jump);
    }
    
    private void Jump(EventPlayerJump e)
    {
        if (e.PlayerRef != _playerRef) return;
        _animator.SetTrigger(TRIGGER_JUMP);
    }

메인 화면으로
 

  • 모델과 모델 애니메이터 컴포넌트를 하위 객체에 배치합니다.
  • 이벤트는 게임 상태의 일부가 아니므로 늦게/다시 조인하는 플레이어들은 사용할 수 없습니다. 따라서 먼저 게임이 이미 시작된 경우 최신 게임 상태를 폴링 하여 애니메이션 상태를 초기화하는 것이 좋습니다.
  • 100% 정확도로 트리거 해야 하는 애니메이션(예: 승리 축하)에 동기화된 이벤트를 사용합니다.
  • 히트 등 신속해야 하는 애니메이션에는 동기화되지 않은 정기 이벤트를 사용합니다.
  • 취소된 비동기화 이벤트로 인해 트리거 된 애니메이션을 종료하려면 EventCanceled 콜백을 사용하세요. 이 문제는 이벤트가 예측의 일부로 제기되었지만 확인된 프레임 중에 롤백된 경우에 발생할 수 있습니다.

메인 화면으로
 

결정론적 애니메이션

결정론적 애니메이션 시스템을 사용하는 주요 이점은 모든 클라이언트에서 100% 동기화되며 롤백 시 올바른 상태로 스냅 되는 틱 정밀한 애니메이션입니다. 이상적으로 들릴 수 있지만 애니메이션과 애니메이션 상태는 이제 시뮬레이션 게임 상태의 일부이기 때문에 성능에 영향을 미칩니다. 현실에서 결정론적 애니메이션 시스템을 필요로 하고 이점을 얻는 게임은 거의 없습니다. 그중에는 파이팅 게임과 스포츠 게임도 있습니다.

사용자 지정 애니메이터는 결정론적 애니메이션을 가능하게 하는 도구입니다. 유니티의 메카님 컨트롤러에서 정보의 베이킹, 상태, 상태 간 전환, 모션 클립 등 모든 구성을 가져오는 방식으로 작동합니다.

유니티의 메카님과 함께 만든 의존성 때문에 사용자 지정 애니메이터의 개발이 중단되었습니다. 그러나 코드는 오픈 소스로 제공되며 애드온 > 사용자 지정 애니메이터 페이지에서 다운로드할 수 있습니다. 또한 이 페이지에서는 사용자 지정 애니메이터를 가져오고 사용하는 방법에 대한 개요와 빠른 안내를 제공합니다.

이 기능은 제한적이며 사용자의 필요에 따라 조정해야 할 수 있습니다.

메인 화면으로
 

  • 사용자 지정 애니메이터를 사용하기 전에 애니메이션이 게임 플레이와 연결되어 있는지 아니면 단지 시각적 표현에 불과한지 고려해야 합니다. 전자의 경우 사용자 지정 애니메이터가 적합한 솔루션이고, 그렇지 않은 경우 폴링 기반 애니메이션을 사용하는 것이 좋습니다.

기술문서 TOP으로 돌아가기