아바타 애니메이션 제어하기

# 학습 과정 소개

아바타 애니메이션을 제어하는 3가지 방법을 살펴봅니다. 본 과정을 통해 크리에이터가 원하는 기능에 맞게 아바타 애니메이션을 제어해 봅시다.

# ActionStateChangedEvent로 제어

아바타는 AvatarRoot에 Body와 Face로 나뉘어 있습니다. Body는 말 그대로 아바타의 몸통이며 Face는 아바타의 얼굴을 뜻합니다. 아바타의 Body와 Face에 어떤 애니메이션을 재생할지는 [ActionStateChangedEvent](/apiReference/Events/ActionStateChangedEvent)로 설정할 수 있습니다.

  * Face는 아바타 얼굴 애니메이션, 즉 표정을 제어합니다.
  * Body는 아바타의 몸통 액션과 파츠 액션을 제어합니다.

  
**ActionStateChangedEvent**의 프로퍼티를 살펴봅시다. 

[{"프로퍼티":"CoreActionName","설명":"아바타 몸통의 액션 이름입니다."},{"프로퍼티":"PartsActionName","설명":"각 파츠의 액션 이름입니다. 얼굴 장식을 제외한 아바타 장착 아이템의 액션입니다."},{"프로퍼티":"PlayRate","설명":"재생 속도입니다."},{"프로퍼티":"PlayType","설명":"재생 타입입니다."},{"프로퍼티":"StartFrameIndex","설명":"애니메이션 시작 프레임입니다."},{"프로퍼티":"EndFrameIndex","설명":"애니메이션 종료 프레임입니다."}]

## ActionName

ActionStateChangedEvent는 ActionName으로 어떤 애니메이션을 재생할지 결정합니다. 아래는 ActionName의 목록입니다.

[{"ActionName1":"stand1","ActionName2":"swingO1","ActionName3":"stabO1","ActionName4":"shoot1"},{"ActionName1":"stand2","ActionName2":"swingO2","ActionName3":"stabO2","ActionName4":"shoot2"},{"ActionName1":"walk1","ActionName2":"swingO3","ActionName3":"stabOF","ActionName4":"shootF"},{"ActionName1":"walk2","ActionName2":"swingOF","ActionName3":"stabT1","ActionName4":"nan"},{"ActionName1":"alert","ActionName2":"swingT1","ActionName3":"stabT2","ActionName4":"nan"},{"ActionName1":"prone","ActionName2":"swingT2","ActionName3":"stabTF","ActionName4":"nan"},{"ActionName1":"proneStab","ActionName2":"swingT3","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"jump","ActionName2":"swingTF","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"fly","ActionName2":"swingP1","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"sit","ActionName2":"swingP2","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"ladder","ActionName2":"swingPF","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"rope","ActionName2":"nan","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"dead","ActionName2":"nan","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"heal","ActionName2":"nan","ActionName3":"nan","ActionName4":"nan"},{"ActionName1":"blink","ActionName2":"nan","ActionName3":"nan","ActionName4":"nan"}]

각 액션의 애니메이션은 메이커의 AvatarItem Editor에서 미리 볼 수 있으며, 방법은 아래와 같습니다.

  1. [뒤통수가 개방된 모자](/docs/?postId=684) 가이드에서 Avatar_Cap_A1.psd를 다운로드합니다. 

> Tip. [아바타 아이템 제작](/ko/docs/?postId=582) 가이드 하위 문서에서 제공하는 예시 리소스 파일(psd)은 모두 사용해도 됩니다. 반드시 Avatar_Cap_A1.psd를 사용할 필요는 없습니다.

  2. 메이커의 내비게이션 메뉴에서 Window - AvatarItem Editor를 클릭합니다.   

  3. [PSD 파일 선택] 버튼을 클릭한 뒤, 다운로드했던 Avatar_Cap_A1.psd를 선택합니다.   

  4. 조금 기다린 뒤, psd 파일이 적용되면 ActionName 목록에서 원하는 이름을 선택합니다. 선택한 애니메이션을 확인할 수 있습니다. [gif](1666762416657685d48785e8e4eec9deb8320f75bbd3a)



ActionName은 CoreActionName과 PartsActionName에서 함께 활용합니다. 위에서 설명했듯이 CoreActionName은 아바타의 맨몸 액션이고 PartsActionName은 얼굴 장식을 제외한 아바타 장착 아이템의 액션입니다. CoreActionName과 PartsActionName을 서로 다르게 설정한 아바타의 움직임을 보면 두 가지의 차이점을 쉽게 이해할 수 있습니다. 예를 들어 아래 이미지를 보면, CoreActionName을 walk1로 설정했기 때문에 아바타의 몸통은 걷고 있지만, PartsActionName은 stand1로 설정했기 때문에 옷이나 신발은 가만히 서 있습니다. [gif](1666850718269278086006fbe4b198a9c492c952bd8c4)

CoreActionName과 PartsActionName가 다르면 대부분 위와 같이 어색한 모습이 연출되기 때문에 일반적으로는 두 가지를 동일하게 설정합니다. 물론 죽음 액션처럼 CoreActionName를 dead, PartsActionName를 stand1로 서로 다르게 설정해야 자연스러운 경우도 있습니다. 크리에이터의 의도에 따라 CoreActionName과 PartsActionName를 설정하면 됩니다.

## PlayRate

애니메이션의 재생 속도를 제어합니다. 숫자가 커지면 재생 속도가 빨라집니다.

## PlayType

애니메이션의 재생 방식을 설정합니다. PlayType에는 OneTime, Loop, ZigzagLoop가 있습니다.

[{"SpriteAnimClipPlayType":"OneTime","설명":"한번 재생합니다."},{"SpriteAnimClipPlayType":"Loop","설명":"순행해서 반복 재생합니다."},{"SpriteAnimClipPlayType":"ZigzagLoop","설명":"순행 - 역행 순서로 반복 재생합니다."}]

## 시작/종료 프레임

각 애니메이션의 총 프레임은 AvatarItem Editor에서 확인할 수 있습니다.

[png](16667657292657df3e93d54f8437cb7a709131c0ab531)

위와 같이 총 프레임을 확인한 뒤, 크리에이터가 재생하기를 원하는 애니메이션 시작 프레임(StartFrameIndex)과 종료 프레임(EndFrameIndex)을 설정하면 됩니다. 프레임을 설정할 때는 다음 사항을 주의해야 합니다.

  * 프레임은 양수로 설정해야 합니다.
  * 시작 프레임은 종료 프레임보다 작거나 같아야 합니다.
  * 종료 프레임은 총 애니메이션 프레임보다 작거나 같아야 합니다.
  * 프레임값을 잘못 설정하면 보정됩니다.
  * 프레임값을 입력하지 않으면 기본값으로 설정됩니다.
  * CoreActionName(몸통 액션)과 PartsActionName(파츠 액션)을 입력해야 애니메이션이 정상 출력됩니다.   




## 예제

ActionStateChangedEvent는 아바타의 body를 통해 발생하기 때문에 AvatarRendererComponent의 GetBodyEntity로 이벤트를 보내야 합니다. 기본적으로는 아래와 같이 설정합니다.
    
    
    @Component
    script WalkActionInitializer extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
            local event = ActionStateChangedEvent("walk1", "walk1", 1, SpriteAnimClipPlayType.Loop, 1, 2)
            body:SendEvent(event)
        end
    end
    

  
일부 프로퍼티는 생략해도 정상 작동합니다. 하지만 **CoreActionName**과 **PartsActionName**은 반드시 입력해야 합니다. ```lua @Component script AvatarWalkStateTrigger extends Component @ExecSpace("ServerOnly") method void OnBeginPlay() local body = self.Entity.AvatarRendererComponent:GetBodyEntity() local event = ActionStateChangedEvent("walk1", "walk1") \-- 재생 속도, 재생 타입, 시작 프레임, 종료 프레임은 생략할 수 있습니다. \-- ActionStateChangedEvent("walk1", "walk1", 1, SpriteAnimClipPlayType.Loop, 0, 종료 프레임)과 같은 의미입니다. body:SendEvent(event) end end ```   
프레임값을 잘못 입력하면 자동으로 보정됩니다. ```lua @Component script AvatarAnimationStarter extends Component @ExecSpace("ServerOnly") method void OnBeginPlay() local body = self.Entity.AvatarRendererComponent:GetBodyEntity() local event = ActionStateChangedEvent("walk1", "walk1", 1, SpriteAnimClipPlayType.Loop, -1, 4) \-- 프레임값으로 양수를 넣어야 하는데 위와 같이 startFrameIndex로 음수를 넣으면 "0"으로 보정됩니다. \-- endFrame이 "3"인데 endFrameIndex로 "4"를 잘못 넣었다면 "3"으로 보정됩니다. \-- 결과적으로 ActionStateChangedEvent("walk1", "walk1", 1, SpriteAnimClipPlayType.Loop, 0, 3)으로 보정됩니다. body:SendEvent(event) end end ```   
이처럼 **ActionStateChangedEvent**를 사용하면 몸통과 파츠의 애니메이션을 각각 다르게 조절하고, 여러 액션을 세밀하게 설정할 수 있습니다. 하지만 매번 액션을 지정해야 하는 번거로움도 있습니다. '서기' 또는 '걷기'처럼 자주 사용하는 애니메이션을 매번 이렇게 설정한다면 쓸데없이 반복되는 코드도 늘어날 것입니다. 또한 아바타가 한 손 무기나 양손 무기를 들었을 때 각각 다른 애니메이션을 취해야 하는데 이러한 것도 별도로 설정해야 합니다. **BodyActionStateChangeEvent**를 사용하면 이러한 불편을 해소할 수 있습니다. 

# BodyActionStateChangeEvent로 제어

[BodyActionStateChangeEvent](/apiReference/Events/BodyActionStateChangeEvent)는 이름 그대로 아바타의 bodyActionState를 변경하는 이벤트입니다. 이 이벤트는 무기 타입에 따른 애니메이션을 별도로 지정할 필요가 없어 편리합니다. BodyActionStateChangeEvent는 MapleAvatarBodyActionState로 애니메이션을 제어합니다. MapleAvatarBodyActionState에는 각 상태별로 어떤 몸통 액션과 파츠 액션을 쓸 것인지, 재생 속도와 재생 타입은 무엇인지 미리 정의되어 있습니다. 자주 사용하는 애니메이션 프리셋이 정의되어 있다고 생각하면 이해하기 쉽습니다.   
MapleAvatarBodyActionState는 아래 표와 같이 ActionStateChangedEvent에 매칭됩니다.

[{"MapleAvatarBodyActionState":"Stand","CoreActionName":"stand1","PartsActionName":"stand1","PlayRate":"1","PlayType":"ZigzagLoop"},{"MapleAvatarBodyActionState":"Stand","CoreActionName":"stand2","PartsActionName":"stand2","PlayRate":"1","PlayType":"ZigzagLoop"},{"MapleAvatarBodyActionState":"Walk","CoreActionName":"walk1","PartsActionName":"walk1","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Walk","CoreActionName":"walk2","PartsActionName":"walk2","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Attack","CoreActionName":"alert","PartsActionName":"alert","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Crouch","CoreActionName":"prone","PartsActionName":"prone","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Fall","CoreActionName":"jump","PartsActionName":"jump","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Sit","CoreActionName":"sit","PartsActionName":"sit","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Rope","CoreActionName":"rope","PartsActionName":"rope","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Ladder","CoreActionName":"ladder","PartsActionName":"ladder","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Dead","CoreActionName":"dead","PartsActionName":"stand1","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Blink","CoreActionName":"blink","PartsActionName":"blink","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Fly","CoreActionName":"fly","PartsActionName":"fly","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Hit","CoreActionName":"alert","PartsActionName":"alert","PlayRate":"1","PlayType":"ZigzagLoop"},{"MapleAvatarBodyActionState":"Alert","CoreActionName":"alert","PartsActionName":"alert","PlayRate":"1","PlayType":"ZigzagLoop"},{"MapleAvatarBodyActionState":"Heal","CoreActionName":"heal","PartsActionName":"heal","PlayRate":"1","PlayType":"Loop"},{"MapleAvatarBodyActionState":"Invalid","CoreActionName":"-","PartsActionName":"-","PlayRate":"-","PlayType":"-"}]

## 예제

BodyActionStateChangeEvent가 MapleAvatarBodyActionState로 Fly 상태를 호출하는 예시를 살펴봅시다.
    
    
    @Component
    script BeginFlightAnimation extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            local event = BodyActionStateChangeEvent()
            event.ActionState = MapleAvatarBodyActionState.Fly
            event.needResetAction = true
            event.startFrameIndex = 1
            event.endFrameIndex = 2
            self.Entity:SendEvent(event)
        end
    end
    

이 이벤트는 위의 매칭 표에 나오는 것처럼 결과적으로 다음과 같이 ActionStateChangedEvent를 호출합니다.
    
    
    @Component
    script InitialFlightState extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            ActionStateChangedEvent("fly", "fly", 1, SpriteAnimClipPlayType.Loop, 1, 2)
        end
    end
    

물론 BodyActionStateChangeEvent에서 시작/종료 프레임을 잘못 입력했더라도 ActionStateChangedEvent에서 보정됩니다.

# ActionSheet와 StateComponent로 제어

이미 정의된, 혹은 새로 정의할 아바타의 상태가 바뀔 때마다 매번 이벤트를 보내서 애니메이션을 변경하는 것도 번거로울 수 있습니다. 이벤트를 보내지 않고도 아바타 상태가 바뀔 때마다 애니메이션도 함께 변경되면 매우 편리할 것입니다. [AvatarStateAnimationComponent](/apiReference/Components/AvatarStateAnimationComponent)의 ActionSheet에 아바타 상태와 연결될 애니메이션을 설정하고 [StateComponent](/apiReference/Components/StateComponent)로 원하는 타이밍에 아바타 상태를 바꾸면 애니메이션을 손쉽게 변경할 수 있습니다.

AvatarStateAnimationComponent에서 기본으로 제공되는 ActionSheet는 다음과 같습니다. [png](1666838653260b4a969d3d22a438990ede8add75f3edb)

[{"Key":"IDLE","Value":"stand"},{"Key":"MOVE","Value":"walk"},{"Key":"ATTACK","Value":"attack"},{"Key":"HIT","Value":"hit"},{"Key":"CROUCH","Value":"crouch"},{"Key":"FALL","Value":"fall"},{"Key":"JUMP","Value":"fall"},{"Key":"CLIMB","Value":"rope"},{"Key":"LADDER","Value":"ladder"},{"Key":"DEAD","Value":"dead"},{"Key":"SIT","Value":"sit"}]

ActionSheet에서

  * Key는 State를 의미합니다.
  * Value는 MapleAvatarBodyActionState를 의미합니다. 

[{"ActionSheet Value":"stand","MapleAvatarBodyActionState":"Stand"},{"ActionSheet Value":"walk","MapleAvatarBodyActionState":"Walk"},{"ActionSheet Value":"attack","MapleAvatarBodyActionState":"Attack"},{"ActionSheet Value":"crouch","MapleAvatarBodyActionState":"Crouch"},{"ActionSheet Value":"fall","MapleAvatarBodyActionState":"Fall"},{"ActionSheet Value":"sit","MapleAvatarBodyActionState":"Sit"},{"ActionSheet Value":"rope","MapleAvatarBodyActionState":"Rope"},{"ActionSheet Value":"ladder","MapleAvatarBodyActionState":"Ladder"},{"ActionSheet Value":"dead","MapleAvatarBodyActionState":"Dead"},{"ActionSheet Value":"blink","MapleAvatarBodyActionState":"Blink"},{"ActionSheet Value":"fly","MapleAvatarBodyActionState":"Fly"},{"ActionSheet Value":"hit","MapleAvatarBodyActionState":"Hit"},{"ActionSheet Value":"alert","MapleAvatarBodyActionState":"Alert"}]

  
아바타가 특정 **State**가 되면 **ActionSheet**에서는 해당 **State**의 **Value** 값에 매칭되는 **MapleAvatarBodyActionState**를 찾습니다. 위에서 살펴본 것처럼 **MapleAvatarBodyActionState**에는 어떤 몸통, 파츠 애니메이션을 재생할지 이미 정의되어 있습니다. 그러므로 **StateComponent**로 **State**만 바꾸면 아바타 애니메이션도 따라서 변경되는 것입니다.   
이처럼 **ActionSheet**는 상태에 따른 애니메이션을 쉽게 변경할 수 있어 편리합니다. 예를 들어, **IDLE** 상태에 **walk** 애니메이션을 재생하고 싶다면 **Value** 값을 **walk**로 바꾸기만 하면 됩니다. 크리에이터가 수동으로 새로운 상태를 추가하는 것도 손쉽게 할 수 있습니다. 



> 더 알아보기 ActionSheet와 StateComponent를 사용해 간단하게 애니메이션을 변경하는 예시는 [ActionSheet로 아바타 애니메이션 손쉽게 제어하기](/docs/?postId=545) 가이드를 참고합니다.

  
**ActionSheet**와 **StateComponent**로 애니메이션을 변경하는 방법은 간편하지만 단점도 있습니다. * 애니메이션의 시작과 끝 프레임은 선택할 수 없습니다. * 정의된 기본/사용자 상태 변경 조건이 있다면 이 조건에 의해서 상태가 자동으로 변경될 수 있습니다. 따라서 원하는 애니메이션이 출력되기 전에 바로 상태가 변경되어 버릴 수 있습니다. 

## 정리

애니메이션 변경 과정을 정리하면 다음과 같습니다. [png](16666629577385097ba49f896467aace2023dbb6bc7eb)

위에서 알아본 3가지 아바타 애니메이션 제어 방법의 장단점을 정리하면 아래 표와 같습니다. 각 방법은 장단점이 명확하므로 크리에이터의 의도에 맞는 방법을 적절히 선택하면 됩니다.

[{"아바타 애니메이션 제어 방법":"ActionStateChangedEvent","장점":"몸통과 파츠 애니메이션을 각각 다르게 조절할 수 있습니다. 재생 속도, 재생 타입, 애니메이션 시작\/종료 프레임을 세밀하게 설정할 수 있습니다.","단점":"자주 사용하는 액션을 매번 이렇게 설정하기는 번거롭습니다.무기에 따라 달라지는 애니메이션을 별도로 지정해야 합니다."},{"아바타 애니메이션 제어 방법":"BodyActionStateChangeEvent","장점":"무기에 따라 달라지는 애니메이션을 별도 지정할 필요가 없습니다.애니메이션 시작\/종료 프레임을 설정할 수 있습니다.","단점":"몸통과 파츠 애니메이션을 각각 다르게 조절할 수는 없습니다. MapleAvatarBodyActionState에서 정의되지 않은 상태로는 변경할 수 없습니다."},{"아바타 애니메이션 제어 방법":"ActionSheet와 StateComponent","장점":"프로퍼티 에디터의 ActionSheet에서 아바타 상태별 애니메이션을 간단하게 변경할 수 있습니다. 아바타 상태를 손쉽게 추가할 수 있습니다.StateComponent로 원하는 타이밍에 State만 바꿔주면 애니메이션도 따라서 바뀝니다.","단점":"애니메이션 시작\/종료 프레임을 설정할 수 없습니다.다른 상태 변경 조건에 의해 상태가 자동으로 변경되면 원하는 애니메이션이 출력되지 않을 수 있습니다."}]

# 애니메이션 프레임 이벤트

재생되는 애니메이션의 프레임 정보를 알 수 있는 3가지 이벤트를 살펴봅시다. 알아낸 프레임 정보를 활용해, 특정 프레임이 재생될 때 다른 애니메이션으로 변경하는 예제도 확인해 봅시다.

[{"이벤트":"[SpriteAnimPlayerChangeFrameEvent](\/apiReference\/Events\/SpriteAnimPlayerChangeFrameEvent)","설명":"스프라이트 애니메이션의 프레임이 바뀔 때 발생합니다."},{"이벤트":"[SpriteAnimPlayerStartFrameEvent](\/apiReference\/Events\/SpriteAnimPlayerStartFrameEvent)","설명":"스프라이트 애니메이션이 첫 번째 프레임을 재생할 때 발생합니다."},{"이벤트":"[SpriteAnimPlayerEndFrameEvent](\/apiReference\/Events\/SpriteAnimPlayerEndFrameEvent)","설명":"스프라이트 애니메이션이 마지막 프레임을 재생할 때 발생합니다."}]

각 이벤트는 프레임 번호(FrameIndex)와 역재생 여부(ReversePlaying), 총 프레임 수(TotalFrameCount)를 전달합니다. 이벤트는 ConnectEvent를 사용해 아바타의 Body 또는 Face에 연결해야 제대로 사용할 수 있습니다.

## 예제

다음의 예제 코드를 보면서 FrameEvent의 사용법을 알아봅시다.

  1. 어떤 아바타 애니메이션의 2번째 프레임이 재생될 때 다른 애니메이션으로 변경하고 싶다고 가정합니다. 그렇다면 SpriteAnimPlayerChangeFrameEvent를 활용할 수 있을 것입니다. `OnBeginPlay` 함수를 추가하고, 위에서 설명한 대로 이벤트를 Body와 연결합니다. 구독 취소를 대비해 연결한 이벤트의 핸들러는 프로퍼티로 가지고 있도록 합니다. 
         
         @Component
         script SpriteAnimationFrameHandler extends Component
         property any eventHandler = nil
         
         @ExecSpace("ClientOnly")
         method void OnBeginPlay()
                 local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
         --         self.eventHandler = body:ConnectEvent("SpriteAnimPlayerChangeFrameEvent", self.ChangeFrameEvent)  -- [Commented: MLUA requires handler pattern]
                 -- SpriteAnimPlayerChangeFrameEvent를 body와 연결합니다. 
                 -- SpriteAnimPlayerChangeFrameEvent가 발생하면 ChangeFrameEvent 함수를 호출합니다.
         end
         end
         



    
    
        이제 **SpriteAnimPlayerChangeFrameEvent**가 발생할 때 `ChangeFrameEvent` 함수를 호출하게 되었습니다.
          
    
    2. `ChangeFrameEvent` 함수를 작성합니다. 
    애니메이션 프레임이 2로 변경됐을 때 **ActionStateChangedEvent**를 통해 **swingOF** 애니메이션을 **0.5** 재생 속도로 **2~3프레임을 반복 재생**하도록 설정합니다.
        ```lua
    @Component
    script FrameActionTrigger extends Component
        @ExecSpace("ClientOnly")
        method void ChangeFrameEvent(any event)
        if event.FrameIndex == 2 then
            local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
            local actionEvent = ActionStateChangedEvent("swingOF", "swingOF", 0.5, SpriteAnimClipPlayType.Loop, 2,3)
            body:SendEvent(actionEvent)
        end
        end
    end
    
    
    
    위의 코드는 애니메이션 재생까지는 문제가 없습니다. 하지만 여전히 **SpriteAnimPlayerChangeFrameEvent**를 구독 중이기 때문에 애니메이션이 시작되자마자 다시 새로운 **ActionStateChangedEvent**를 보내는 과정이 반복될 것입니다. 그러면 원하는 결과를 얻을 수 없으므로 **SpriteAnimPlayerChangeFrameEvent**를 구독 취소해야 합니다.
      
    
    

  3. 아까 저장한 handler를 통해 SpriteAnimPlayerChangeFrameEvent 구독을 취소합니다. `ChangeFrameEvent` 함수를 수정해 봅시다. 
         
         @Component
         script FrameStateChanger extends Component
         @ExecSpace("ClientOnly")
         method void ChangeFrameEvent(any event)
                 if event.FrameIndex == 2 then
                     local body = self.Entity.AvatarRendererComponent:GetBodyEntity()
         --             body:DisconnectEvent("SpriteAnimPlayerChangeFrameEvent", self.eventHandler)   -- [Commented: Use handler pattern instead]
                     -- SpriteAnimPlayerChangeFrameEvent 구독 취소
                     local actionEvent = ActionStateChangedEvent("swingOF", "swingOF", 0.5, SpriteAnimClipPlayType.Loop, 2,3)
                     body:SendEvent(actionEvent)
                 end
         end
         end
         



    
    
        이제 플레이 후 처음 애니메이션의 2번째 프레임이 재생될 때 **swingOF** 애니메이션으로 변경됩니다.
      
    
    **SpriteAnimPlayerStartFrameEvent**나 **SpriteAnimPlayerEndFrameEvent**도 사용법은 같습니다. 크리에이터의 구현 의도에 따라 이벤트를 활용하여 원하는 애니메이션을 만들어봅시다.
    
    > **Tip.**
    > 아바타 애니메이션은 클라이언트를 통해 재생되므로 클라이언트로 제어해야 합니다.