• 사용한 Unity 버전 : 2022.3.62f
  • 사용한 Spine 버전 : spine-unity 4.2

스파인 패키지 적용하기

스파인 패키지 링크

  • 여기 들어가서 패키지를 다운받고, 유니티로 드래그&드롭해서 적용시키기

  • 이 사진처럼 나오면 적용 완료

예제 씬 살펴보기

  • Spine Examples 들어가보면 예제 씬이 있음
  • 하나씩 까보자

1. The Spine GameObject

  • 대강 보면, 스파인 게임 오브젝트는 SkeletonAnimation(또는 SkeletonRenderer, SkeletonGraphic, SkeletonAnimator) 를 포함하고 있는 게임 오브젝트를 의미한다는 것을 알 수 있음.
  • 그리고 SkeletonData Asset의 경우에는 스파인 프로그램에서 내보내기 한 json, png, atlas.txt 파일이 묶인다는 점을 알 수 있음.
  • 오른쪽 위에 있는 스파인 유니티 걸을 확인하면 아래의 컴포넌트가 포함되어 있음

  • 여기서 Spine Blink Player 코드를 살펴보자.
public class SpineBlinkPlayer : MonoBehaviour {  
    const int BlinkTrack = 1;  
  
    public AnimationReferenceAsset blinkAnimation;  
    public float minimumDelay = 0.15f;  
    public float maximumDelay = 3f;  
  
    IEnumerator Start () {  
       SkeletonAnimation skeletonAnimation = GetComponent<SkeletonAnimation>();  
       if (skeletonAnimation == null) yield break;  
       while (true) {  
          skeletonAnimation.AnimationState.SetAnimation(SpineBlinkPlayer.BlinkTrack, blinkAnimation, false);  
          yield return new WaitForSeconds(Random.Range(minimumDelay, maximumDelay));  
       }  
    }  
}
  • 대강 보면, 눈을 깜빡이는 모션을 취하고, 0.15초부터 3초 사이의 랜덤한 초 이후에 원래대로 돌아가는 것 처럼 보임.

2. Controlling Animation

  • 플레이하면 스파인 보이가 걷고, 뛰고, 반대방향으로 돎.

  • 스파인 보이는 아래의 Spine Beginner Two 컴포넌트를 가지고 있다. 한 번 까보자.

public class SpineBeginnerTwo : MonoBehaviour {  
  
    #region Inspector  
    // [SpineAnimation] attribute allows an Inspector dropdown of Spine animation names coming form SkeletonAnimation.  
    [SpineAnimation]  
    public string runAnimationName;  
  
    [SpineAnimation]  
    public string idleAnimationName;  
  
    [SpineAnimation]  
    public string walkAnimationName;  
  
    [SpineAnimation]  
    public string shootAnimationName;  
  
    [Header("Transitions")]  
    [SpineAnimation]  
    public string idleTurnAnimationName;  
  
    [SpineAnimation]  
    public string runToIdleAnimationName;  
  
    public float runWalkDuration = 1.5f;  
    #endregion  
  
    SkeletonAnimation skeletonAnimation;  
  
    // Spine.AnimationState and Spine.Skeleton are not Unity-serialized objects. You will not see them as fields in the inspector.  
    public Spine.AnimationState spineAnimationState;  
    public Spine.Skeleton skeleton;  
  
    void Start () {  
       // Make sure you get these AnimationState and Skeleton references in Start or Later.  
       // Getting and using them in Awake is not guaranteed by default execution order.       
       skeletonAnimation = GetComponent<SkeletonAnimation>();  
       spineAnimationState = skeletonAnimation.AnimationState;  
       skeleton = skeletonAnimation.Skeleton;  
  
       StartCoroutine(DoDemoRoutine());  
    }  
  
    /// This is an infinitely repeating Unity Coroutine. Read the Unity documentation on Coroutines to learn more.  
    IEnumerator DoDemoRoutine () {  
       while (true) {  
          // SetAnimation is the basic way to set an animation.  
          // SetAnimation sets the animation and starts playing it from the beginning.          // Common Mistake: If you keep calling it in Update, it will keep showing the first pose of the animation, do don't do that.  
          spineAnimationState.SetAnimation(0, walkAnimationName, true);  
          yield return new WaitForSeconds(runWalkDuration);  
  
          spineAnimationState.SetAnimation(0, runAnimationName, true);  
          yield return new WaitForSeconds(runWalkDuration);  
  
          // AddAnimation queues up an animation to play after the previous one ends.  
          spineAnimationState.SetAnimation(0, runToIdleAnimationName, false);  
          spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
          yield return new WaitForSeconds(1f);  
  
          skeleton.ScaleX = -1;       // skeleton allows you to flip the skeleton.  
          spineAnimationState.SetAnimation(0, idleTurnAnimationName, false);  
          spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
          yield return new WaitForSeconds(0.5f);  
          skeleton.ScaleX = 1;  
          spineAnimationState.SetAnimation(0, idleTurnAnimationName, false);  
          spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
          yield return new WaitForSeconds(0.5f);  
  
       }  
    }  
  
}
  • 위에서부터 살펴보자.
// [SpineAnimation] attribute allows an Inspector dropdown of Spine animation names coming form SkeletonAnimation.  
[SpineAnimation]  
public string runAnimationName;
 
  • SpineAnimation 속성을 붙이는 경우, 인스펙터창에서 드롭다운을 통해 애니메이션을 고를 수 있다.
// Spine.AnimationState and Spine.Skeleton are not Unity-serialized objects. You will not see them as fields in the inspector.  
public Spine.AnimationState spineAnimationState;  
public Spine.Skeleton skeleton;
  • Spine.AnimationState와 Spine.Skeleton의 경우, 직렬화 할 수 없기 때문에, 인스펙터에 보이지 않는다.
void Start () {  
    // Make sure you get these AnimationState and Skeleton references in Start or Later.  
    // Getting and using them in Awake is not guaranteed by default execution order.    
    skeletonAnimation = GetComponent<SkeletonAnimation>();  
    spineAnimationState = skeletonAnimation.AnimationState;  
    skeleton = skeletonAnimation.Skeleton;  
  
    StartCoroutine(DoDemoRoutine());  
}
  • AnimationState와 Skeleton 참조를 얻기 위해서는 Start나, 그 이후에 시도해야 한다.
  • Awake에서 하면 안됨!! 실행 순서를 보장할 수 없음!
IEnumerator DoDemoRoutine () {  
    while (true) {  
       // SetAnimation is the basic way to set an animation.  
       // SetAnimation sets the animation and starts playing it from the beginning.       // Common Mistake: If you keep calling it in Update, it will keep showing the first pose of the animation, do don't do that.  
       spineAnimationState.SetAnimation(0, walkAnimationName, true);  
       yield return new WaitForSeconds(runWalkDuration);  
  
       spineAnimationState.SetAnimation(0, runAnimationName, true);  
       yield return new WaitForSeconds(runWalkDuration);  
  
       // AddAnimation queues up an animation to play after the previous one ends.  
       spineAnimationState.SetAnimation(0, runToIdleAnimationName, false);  
       spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
       yield return new WaitForSeconds(1f);  
  
       skeleton.ScaleX = -1;       // skeleton allows you to flip the skeleton.  
       spineAnimationState.SetAnimation(0, idleTurnAnimationName, false);  
       spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
       yield return new WaitForSeconds(0.5f);  
       skeleton.ScaleX = 1;  
       spineAnimationState.SetAnimation(0, idleTurnAnimationName, false);  
       spineAnimationState.AddAnimation(0, idleAnimationName, true, 0);  
       yield return new WaitForSeconds(0.5f);  
  
    }  
}
  • SetAnimation이 가장 기본적인 애니메이션 변경 방법.
  • SetAnimation은 애니메이션을 지정하고, 그 애니메이션을 처음부터 송출함.
  • 가장 자주 하는 실수는 이걸 Update에서 부르는거임. 그렇게 되면 애니메이션의 맨 첫 번째 포즈만 계속 나타나게 됨. 그러니 그러지 말도록!
  • AddAnimation은 현재 애니메이션이 종료된 후에 실행될 수 있도록 뒤에 줄세움.
  • 그리고 ScaleX = -1 을 써서 뒤집을 수 있음!

3. Controlling Animation Continued

  • Raptor 스크립트가 애니메이션들이 어떻게 동시에 송출되는지 보여준다고 함.
  • 그리고 AnimationReferenceAssets 가 애니메이션의 이름 대신 어떻게 쓰이는지도 보여준다고 함.
  • walk 애니메이션이 반복되는 동안, gun grab과 gun keep 애니메이션이 동시에 나올 것임.

  • 위에서 봤던 드롭다운과 다르게, AnimationReferenceAsset을 사용하면 바로 넣을 수 있음!
public class Raptor : MonoBehaviour {  
  
    #region Inspector  
    public AnimationReferenceAsset walk;  
    public AnimationReferenceAsset gungrab;  
    public AnimationReferenceAsset gunkeep;  
    #endregion  
  
    SkeletonAnimation skeletonAnimation;  
  
    void Start () {  
       skeletonAnimation = GetComponent<SkeletonAnimation>();  
       StartCoroutine(GunGrabRoutine());  
    }  
  
    IEnumerator GunGrabRoutine () {  
       // Play the walk animation on track 0.  
       skeletonAnimation.AnimationState.SetAnimation(0, walk, true);  
  
       // Repeatedly play the gungrab and gunkeep animation on track 1.  
       while (true) {  
          yield return new WaitForSeconds(Random.Range(0.5f, 3f));  
          skeletonAnimation.AnimationState.SetAnimation(1, gungrab, false);  
  
          yield return new WaitForSeconds(Random.Range(0.5f, 3f));  
          skeletonAnimation.AnimationState.SetAnimation(1, gunkeep, false);  
       }  
  
    }  
  
}
  • 여기서 주의해서 봐야 할 부분은 이 부분
IEnumerator GunGrabRoutine () {  
       // Play the walk animation on track 0.  
       skeletonAnimation.AnimationState.SetAnimation(0, walk, true);  
  
       // Repeatedly play the gungrab and gunkeep animation on track 1.  
       while (true) {  
          yield return new WaitForSeconds(Random.Range(0.5f, 3f));  
          skeletonAnimation.AnimationState.SetAnimation(1, gungrab, false);  
  
          yield return new WaitForSeconds(Random.Range(0.5f, 3f));  
          skeletonAnimation.AnimationState.SetAnimation(1, gunkeep, false);  
       }  
  
    }  
  • 애니메이션의 “트랙”을 다르게 설정해서, 동시에 송출하도록 하는 방법인가봄!
  • 도마뱀이 걷는 애니메이션은 0번 트랙에, 스파인 보이가 총을 드는 부분은 1번 트랙에 두고 동시 송출을 하는 듯 함!

4. Object Oriented Sample

  • 여기서는 입력을 받고, 게임 모델을 조종하며, 애니메이션을 관리하는걸 서로 다른 컴포넌트들로 나눠서 진행함.
  • 이번에 주의해서 봐야 하는 것들
  1. Player Input
public class SpineboyBeginnerInput : MonoBehaviour {  
    #region Inspector  
    public string horizontalAxis = "Horizontal";  
    public string attackButton = "Fire1";  
    public string aimButton = "Fire2";  
    public string jumpButton = "Jump";  
  
    public SpineboyBeginnerModel model;  
  
    void OnValidate () {  
       if (model == null)  
          model = GetComponent<SpineboyBeginnerModel>();  
    }  
    #endregion  
  
    void Update () {  
       if (model == null) return;  
  
       float currentHorizontal = Input.GetAxisRaw(horizontalAxis);  
       model.TryMove(currentHorizontal);  
  
       if (Input.GetButton(attackButton))  
          model.TryShoot();  
  
       if (Input.GetButtonDown(aimButton))  
          model.StartAim();  
       if (Input.GetButtonUp(aimButton))  
          model.StopAim();  
  
       if (Input.GetButtonDown(jumpButton))  
          model.TryJump();  
    }  
}
  • 여기서는 보면 SpineboyBeginnerModel 안에 있는 TryMove, TryShoot, StartAnim, StopAim, TryJump 를 호출하는 것 밖에 하지 않음.
  1. Spineboy Beginner Model
[SelectionBase]  
public class SpineboyBeginnerModel : MonoBehaviour {  
  
    #region Inspector  
    [Header("Current State")]  
    public SpineBeginnerBodyState state;  
    public bool facingLeft;  
    [Range(-1f, 1f)]  
    public float currentSpeed;  
  
    [Header("Balance")]  
    public float shootInterval = 0.12f;  
    #endregion  
  
    float lastShootTime;  
    public event System.Action ShootEvent;  // Lets other scripts know when Spineboy is shooting. Check C# Documentation to learn more about events and delegates.  
    public event System.Action StartAimEvent;   // Lets other scripts know when Spineboy is aiming.  
    public event System.Action StopAimEvent;   // Lets other scripts know when Spineboy is no longer aiming.  
  
    #region API  
    public void TryJump () {  
       StartCoroutine(JumpRoutine());  
    }  
  
    public void TryShoot () {  
       float currentTime = Time.time;  
  
       if (currentTime - lastShootTime > shootInterval) {  
          lastShootTime = currentTime;  
          if (ShootEvent != null) ShootEvent();   // Fire the "ShootEvent" event.  
       }  
    }  
  
    public void StartAim () {  
       if (StartAimEvent != null) StartAimEvent();   // Fire the "StartAimEvent" event.  
    }  
  
    public void StopAim () {  
       if (StopAimEvent != null) StopAimEvent();   // Fire the "StopAimEvent" event.  
    }  
  
    public void TryMove (float speed) {  
       currentSpeed = speed; // show the "speed" in the Inspector.  
  
       if (speed != 0) {  
          bool speedIsNegative = (speed < 0f);  
          facingLeft = speedIsNegative; // Change facing direction whenever speed is not 0.  
       }  
  
       if (state != SpineBeginnerBodyState.Jumping) {  
          state = (speed == 0) ? SpineBeginnerBodyState.Idle : SpineBeginnerBodyState.Running;  
       }  
  
    }  
    #endregion  
  
    IEnumerator JumpRoutine () {  
       if (state == SpineBeginnerBodyState.Jumping) yield break;   // Don't jump when already jumping.  
  
       state = SpineBeginnerBodyState.Jumping;  
  
       // Fake jumping.  
       {  
          Vector3 pos = transform.localPosition;  
          const float jumpTime = 1.2f;  
          const float half = jumpTime * 0.5f;  
          const float jumpPower = 20f;  
          for (float t = 0; t < half; t += Time.deltaTime) {  
             float d = jumpPower * (half - t);  
             transform.Translate((d * Time.deltaTime) * Vector3.up);  
             yield return null;  
          }  
          for (float t = 0; t < half; t += Time.deltaTime) {  
             float d = jumpPower * t;  
             transform.Translate((d * Time.deltaTime) * Vector3.down);  
             yield return null;  
          }  
          transform.localPosition = pos;  
       }  
  
       state = SpineBeginnerBodyState.Idle;  
    }  
  
}  
  
public enum SpineBeginnerBodyState {  
    Idle,  
    Running,  
    Jumping  
}
  • 일단 이 코드를 보면 단순하지만 FSM으로 구성되어있음. 그리고 여기서는 상태 전환밖에 안함. 실제로 애니메이션을 돌리는 로직 자체는 여기 없음!
public enum SpineBeginnerBodyState {  
    Idle,  
    Running,  
    Jumping  
}
  • Idle, Running, Jumping의 세 상태로 구분
public void TryShoot () {  
       float currentTime = Time.time;  
  
       if (currentTime - lastShootTime > shootInterval) {  
          lastShootTime = currentTime;  
          if (ShootEvent != null) ShootEvent();   // Fire the "ShootEvent" event.  
       }  
    }  
  
    public void StartAim () {  
       if (StartAimEvent != null) StartAimEvent();   // Fire the "StartAimEvent" event.  
    }  
  
    public void StopAim () {  
       if (StopAimEvent != null) StopAimEvent();   // Fire the "StopAimEvent" event.  
    }  
  • Shoot 관련된 애들은 다 Action으로 빠져있고, 여기서는 Invoke만 함.
public void TryMove (float speed) {  
    currentSpeed = speed; // show the "speed" in the Inspector.  
  
    if (speed != 0) {  
       bool speedIsNegative = (speed < 0f);  
       facingLeft = speedIsNegative; // Change facing direction whenever speed is not 0.  
    }  
  
    if (state != SpineBeginnerBodyState.Jumping) {  
       state = (speed == 0) ? SpineBeginnerBodyState.Idle : SpineBeginnerBodyState.Running;  
    }  
  
}
  • TryMove에서는 좌우 속도를 받아다가 그걸로 상태를 바꿈.
public void TryJump () {  
    StartCoroutine(JumpRoutine());  
}
 
IEnumerator JumpRoutine () {  
    if (state == SpineBeginnerBodyState.Jumping) yield break;   // Don't jump when already jumping.  
  
    state = SpineBeginnerBodyState.Jumping;  
  
    // Fake jumping.  
    {  
       Vector3 pos = transform.localPosition;  
       const float jumpTime = 1.2f;  
       const float half = jumpTime * 0.5f;  
       const float jumpPower = 20f;  
       for (float t = 0; t < half; t += Time.deltaTime) {  
          float d = jumpPower * (half - t);  
          transform.Translate((d * Time.deltaTime) * Vector3.up);  
          yield return null;  
       }  
       for (float t = 0; t < half; t += Time.deltaTime) {  
          float d = jumpPower * t;  
          transform.Translate((d * Time.deltaTime) * Vector3.down);  
          yield return null;  
       }  
       transform.localPosition = pos;  
    }  
  
    state = SpineBeginnerBodyState.Idle;  
}
 
  • TryJump 에서는 위-아래 이동을 임의로 구현함.
  1. Spineboy Beginner View
public class SpineboyBeginnerView : MonoBehaviour {  
  
    #region Inspector  
    [Header("Components")]  
    public SpineboyBeginnerModel model;  
    public SkeletonAnimation skeletonAnimation;  
  
    public AnimationReferenceAsset run, idle, aim, shoot, jump;  
    public EventDataReferenceAsset footstepEvent;  
  
    [Header("Audio")]  
    public float footstepPitchOffset = 0.2f;  
    public float gunsoundPitchOffset = 0.13f;  
    public AudioSource footstepSource, gunSource, jumpSource;  
  
    [Header("Effects")]  
    public ParticleSystem gunParticles;  
    #endregion  
  
    SpineBeginnerBodyState previousViewState;  
  
    void Start () {  
       if (skeletonAnimation == null) return;  
       model.ShootEvent += PlayShoot;  
       model.StartAimEvent += StartPlayingAim;  
       model.StopAimEvent += StopPlayingAim;  
       skeletonAnimation.AnimationState.Event += HandleEvent;  
    }  
  
    void HandleEvent (Spine.TrackEntry trackEntry, Spine.Event e) {  
       if (e.Data == footstepEvent.EventData)  
          PlayFootstepSound();  
    }  
  
    void Update () {  
       if (skeletonAnimation == null) return;  
       if (model == null) return;  
  
       if ((skeletonAnimation.skeleton.ScaleX < 0) != model.facingLeft) {  // Detect changes in model.facingLeft  
          Turn(model.facingLeft);  
       }  
  
       // Detect changes in model.state  
       SpineBeginnerBodyState currentModelState = model.state;  
  
       if (previousViewState != currentModelState) {  
          PlayNewStableAnimation();  
       }  
  
       previousViewState = currentModelState;  
    }  
  
    void PlayNewStableAnimation () {  
       SpineBeginnerBodyState newModelState = model.state;  
       Animation nextAnimation;  
  
       // Add conditionals to not interrupt transient animations.  
  
       if (previousViewState == SpineBeginnerBodyState.Jumping && newModelState != SpineBeginnerBodyState.Jumping) {  
          PlayFootstepSound();  
       }  
  
       if (newModelState == SpineBeginnerBodyState.Jumping) {  
          jumpSource.Play();  
          nextAnimation = jump;  
       } else {  
          if (newModelState == SpineBeginnerBodyState.Running) {  
             nextAnimation = run;  
          } else {  
             nextAnimation = idle;  
          }  
       }  
  
       skeletonAnimation.AnimationState.SetAnimation(0, nextAnimation, true);  
    }  
  
    void PlayFootstepSound () {  
       footstepSource.Play();  
       footstepSource.pitch = GetRandomPitch(footstepPitchOffset);  
    }  
  
    [ContextMenu("Check Tracks")]  
    void CheckTracks () {  
       AnimationState state = skeletonAnimation.AnimationState;  
       Debug.Log(state.GetCurrent(0));  
       Debug.Log(state.GetCurrent(1));  
    }  
  
    #region Transient Actions  
    public void PlayShoot () {  
       // Play the shoot animation on track 1.  
       TrackEntry shootTrack = skeletonAnimation.AnimationState.SetAnimation(1, shoot, false);  
       shootTrack.MixAttachmentThreshold = 1f;  
       shootTrack.SetMixDuration(0f, 0f);  
       skeletonAnimation.state.AddEmptyAnimation(1, 0.5f, 0.1f);  
  
       // Play the aim animation on track 2 to aim at the mouse target.  
       TrackEntry aimTrack = skeletonAnimation.AnimationState.SetAnimation(2, aim, false);  
       aimTrack.MixAttachmentThreshold = 1f;  
       aimTrack.SetMixDuration(0f, 0f);  
       skeletonAnimation.state.AddEmptyAnimation(2, 0.5f, 0.1f);  
  
       gunSource.pitch = GetRandomPitch(gunsoundPitchOffset);  
       gunSource.Play();  
       //gunParticles.randomSeed = (uint)Random.Range(0, 100);  
       gunParticles.Play();  
    }  
  
    public void StartPlayingAim () {  
       // Play the aim animation on track 2 to aim at the mouse target.  
       TrackEntry aimTrack = skeletonAnimation.AnimationState.SetAnimation(2, aim, true);  
       aimTrack.MixAttachmentThreshold = 1f;  
       aimTrack.SetMixDuration(0f, 0f); // use SetMixDuration(mixDuration, delay) to update delay correctly  
    }  
  
    public void StopPlayingAim () {  
       skeletonAnimation.state.AddEmptyAnimation(2, 0.5f, 0.1f);  
    }  
  
    public void Turn (bool facingLeft) {  
       skeletonAnimation.Skeleton.ScaleX = facingLeft ? -1f : 1f;  
       // Maybe play a transient turning animation too, then call ChangeStableAnimation.  
    }  
    #endregion  
  
    #region Utility  
    public float GetRandomPitch (float maxPitchOffset) {  
       return 1f + Random.Range(-maxPitchOffset, maxPitchOffset);  
    }  
    #endregion  
}
void Start () {  
    if (skeletonAnimation == null) return;  
    model.ShootEvent += PlayShoot;  
    model.StartAimEvent += StartPlayingAim;  
    model.StopAimEvent += StopPlayingAim;  
    skeletonAnimation.AnimationState.Event += HandleEvent;  
}
  • 2번 Model에 있던 액션들에 함수를 넣어줌
void HandleEvent (Spine.TrackEntry trackEntry, Spine.Event e) {  
    if (e.Data == footstepEvent.EventData)  
       PlayFootstepSound();  
}
  • 이 부분은 잘 모르겠어서 gpt의 도움을 빌림

  • = spine 에서 애니메이션에 이벤트를 넣어두고, 그 이벤트가 발생될 때마다 HandleEvent가 실행됨!
void Update () {  
    if (skeletonAnimation == null) return;  
    if (model == null) return;  
  
    if ((skeletonAnimation.skeleton.ScaleX < 0) != model.facingLeft) {  // Detect changes in model.facingLeft  
       Turn(model.facingLeft);  
    }  
  
    // Detect changes in model.state  
    SpineBeginnerBodyState currentModelState = model.state;  
  
    if (previousViewState != currentModelState) {  
       PlayNewStableAnimation();  
    }  
  
    previousViewState = currentModelState;  
}
  • 좌우 보는 방향 다르면 제대로 보도록 돌려주기 Turn
public void Turn (bool facingLeft) {  
    skeletonAnimation.Skeleton.ScaleX = facingLeft ? -1f : 1f;  
    // Maybe play a transient turning animation too, then call ChangeStableAnimation.  
}
  • 현재 상태랑 이전 상태가 다르면 그거에 맞는 애니메이션 재생 PlayNewStableAnimation
void PlayNewStableAnimation () {  
    SpineBeginnerBodyState newModelState = model.state;  
    Animation nextAnimation;  
  
    // Add conditionals to not interrupt transient animations.  
  
    if (previousViewState == SpineBeginnerBodyState.Jumping && newModelState != SpineBeginnerBodyState.Jumping) {  
       PlayFootstepSound();  
    }  
  
    if (newModelState == SpineBeginnerBodyState.Jumping) {  
       jumpSource.Play();  
       nextAnimation = jump;  
    } else {  
       if (newModelState == SpineBeginnerBodyState.Running) {  
          nextAnimation = run;  
       } else {  
          nextAnimation = idle;  
       }  
    }  
  
    skeletonAnimation.AnimationState.SetAnimation(0, nextAnimation, true);  
}
public void PlayShoot () {  
    // Play the shoot animation on track 1.  
    TrackEntry shootTrack = skeletonAnimation.AnimationState.SetAnimation(1, shoot, false);  
    shootTrack.MixAttachmentThreshold = 1f;  
    shootTrack.SetMixDuration(0f, 0f);  
    skeletonAnimation.state.AddEmptyAnimation(1, 0.5f, 0.1f);  
  
    // Play the aim animation on track 2 to aim at the mouse target.  
    TrackEntry aimTrack = skeletonAnimation.AnimationState.SetAnimation(2, aim, false);  
    aimTrack.MixAttachmentThreshold = 1f;  
    aimTrack.SetMixDuration(0f, 0f);  
    skeletonAnimation.state.AddEmptyAnimation(2, 0.5f, 0.1f);  
  
    gunSource.pitch = GetRandomPitch(gunsoundPitchOffset);  
    gunSource.Play();  
    //gunParticles.randomSeed = (uint)Random.Range(0, 100);  
    gunParticles.Play();  
}
  • 여기서, 왜 AddEmptyAnimation을 썼는가? 에 대한 의문

  • 애니메이션을 블렌드 해서 자연스럽게 종료시키기 위함!

5. Basic Platformer

  • 여기서는 코드는 크게 안봐도 될 것 같음 (이전과 비슷)
  • SkeletonRenderer는 Unity Mesh를 만들어다가 함
  • 이 얘기는 우리가 직접 쉐이더를 만들어서 적용시킬 수 있음.

6. SkeletonGraphic

  • ScrollRect 안에 SkeletonGraphic 을 넣을 수 있음.