엔티티의 상태 제어하기

# 학습 과정 소개

[StateComponent](/apiReference/Components/StateComponent)와 StateType의 개념을 알아보고, 엔티티의 상태에 따라 애니메이션이 재생되는 과정을 이해해봅시다.

# State, StateComponent 알아보기

State는 어떤 엔티티가 특정한 상태에 있는 것을 말합니다. 서 있는 상태, 엎드린 상태, 공격 상태 등 크리에이터에 따라 다양한 상태를 정의할 수 있습니다. 월드에서 내 캐릭터가 가만히 서 있는 것이 당연하게 느껴질 수 있지만 이는 캐릭터의 State가 "서 있는 애니메이션을 재생하는 상태"인 IDLE로 정의되어 있기에 가능한 것입니다. 이처럼 상태에 맞는 애니메이션을 재생하려면 [StateAnimationComponent](/apiReference?postId=367)를 활용해야 합니다.   
State 이름은 영어 대문자로 지어야 합니다. 물론 영어 소문자를 사용하더라도 `AddState()`와 같은 함수를 호출할 때 자동으로 대문자로 바뀌어 등록되지만, 동작이 예상과 달라질 수 있으므로 대문자로 짓도록 합니다.

> Tip. StateAnimationComponent는 monster, npc에게만 적용할 수 있습니다.

StateComponent는 엔티티에 상태를 부여하고, 상태끼리의 관계를 정의하는 역할을 합니다. 기본적으로 IDLE, DEAD 상태를 갖고 있으므로 크리에이터가 새로운 상태를 추가하려면 두 개의 기본 상태를 염두에 두고 활용해야 합니다. StateComponent는 다른 컴포넌트와 함께 있을 경우 자동으로 추가되는 State가 있습니다. 예를 들어 DefaultPlayer에는 여러 상태를 담당하는 PlayerControllerComponent가 기본으로 포함되어 있기 때문에 특정 키를 누르면 상태가 변하고, 그 상태와 연결된 애니메이션(앉기, 걷기 등)이 자동으로 재생됩니다.

#### StateComponent와 특정 컴포넌트의 관계

StateComponent는 특정 컴포넌트와 함께하면 자동으로 새로운 상태가 추가되거나 기본 상태들을 처리할 수 있게 됩니다. 자동으로 추가되는 상태들은 특정 키 입력 혹은 특정 조건을 만족할 때 해당하는 State로 전이되어 동작을 수행하거나 애니메이션을 재생할 수 있게 합니다. 엔티티가 특정 상태일 때 사용자가 정의한 행동을 수행하게 하고, StateAnimationComponent를 함께 사용해 애니메이션 클립을 재생할 수 있게 할 수 있습니다.

  * PlayerComponent와 함께 있을 때는 DEAD를 처리하거나 부활 처리를 할 수 있습니다.
  * PlayerControllerComponent가 함께 있을 때는 MOVE, CLIMB, LADDER, CROUCH, JUMP, FALL, ATTACK, ATTACK_WAIT, SIT이 추가됩니다.
  * AIChaseComponent 및 AIWanderComponent와 함께 있을 때는 MOVE가 추가됩니다. 엔티티를 움직이는 키 입력이 있을 때 MOVE 상태로 전이해 걷는 애니메이션이 재생됩니다.



#### 상태 전이와 이벤트 발생

어떤 상태에서 다음 상태로 변하는 것을 상태 전이(Transition)라 합니다. 상태 전이가 발생할 때 다양한 이벤트가 발생하고, 이를 활용해 특정 엔티티가 상태 전이를 감지할 수 있습니다. 이벤트는 상태 전이가 발생하기 이전의 상태와 현재 상태(바뀐 상태)의 정보를 갖고 있습니다. 아래와 같은 상황에서 이벤트가 발생합니다.

  * StateComponent의 상태가 전이되면, [StateChangeEvent](/apiReference/Events/StateChangeEvent)가 함께 발생합니다.

  * StateComponent가 DEAD 상태에 진입하면 [DeadEvent](/apiReference/Events/DeadEvent)가 발생합니다.

  * DefaultPlayer에 StateComponent와 PlayerComponent가 함께 있다면, `PlayerComponent:Respawn()` 함수를 호출할 때 [ReviveEvent](/apiReference/Events/ReviveEvent)가 발생합니다.

  * HitComponent는 HitEvent를 발생시키므로 StateComponent는 엔티티에서 발생한 HitEvent를 받아 HIT으로 상태 전이합니다. HIT 상태가 된 뒤 0.5초 후에 IDLE로 자동 전이됩니다.

  * AttackComponent는 피격 대상을 검색할 때 `IsAttackTarget()` 함수를 호출합니다. 이때 피격 대상에 해당하는 엔티티의 State가 DEAD라면 false를 반환합니다.




# StateType

사용자 정의 State를 만들 때 사용합니다. StateType에서 상태 진입 및 퇴장 시 사용자가 정의한 행동을 수행하게 하거나, 상태 종료 조건을 정의할 수 있습니다. 원하는 상태가 여러 개라면 그 상태마다 새로운  StateType을 만들어 활용하는 것을 권장합니다. 물론 If문을 사용해 StateType 하나에 여러 개의 상태를 추가해 사용할 수도 있습니다. 하지만 제작 중 다른 상태를 추가, 삭제하는 경우까지 고려한다면, 상태 별로 StateType을 만드는 것이 더 효율적입니다. 예를 들어 새로운 나만의 몬스터를 만들기 위해 StateType을 만든다면, StateComponent의 기본 상태인 IDLE, DEAD를 제외한 다수의 새로운 StateType을 만들어 관리하는 것이 좋습니다. 추가한 새로운 StateType의 이름은 크리에이터가 만든 StateType의 type이 됩니다.

StateType은 상태를 정의하는 타입이므로 스크립트 컴포넌트와 같이 엔티티에 직접 추가하는 형태로 사용할 수 없습니다. StateType은 StateComponent의 `AddState()` 함수 호출 시 자동으로 할당됩니다. 스크립트 에디터 창에서 상태의 동작에 대해 정의한 후 해당 StateType을 사용하고 싶은 엔티티의 스크립트 컴포넌트 중 하나에 `StateComponent:AddState()`를 사용해 해당 StateType을 추가해야 합니다.

`OnEnter()` `OnUpdate()` `OnExit()` `OnConditionCheck()`에서 크리에이터가 만드는 StateType이 어떻게 작동할 것인지 정의할 수 있습니다.

  * OnEnter(): 상태에 처음 진입할 때의 동작을 실행합니다.
  * OnUpdate(): 상태가 유지되는 동안의 동작을 실행합니다.
  * OnExit(): 상태가 변경될 때 실행됩니다.
  * OnConditionCheck(string nextStateName): 상태 전이 조건을 지정합니다. nextStateName 매개 변수를 사용해 연결된 상태에 따라 다른 결괏값을 반환할 수 있습니다. 
    * nextStateName으로 분기할 수 있고, true일 때 상태 전이가 이뤄집니다.



#### StateType 만들기

MyDesk - Create Scripts - Create StateType을 눌러 새로운  StateType을 생성하고, 상태를 정의합니다. `OnConditionCheck()`를 활용해 상태 전이 조건을 만들 수 있습니다. 아래와 같이 MoveDown StateType을 만들고 아래의 예시 코드처럼 활용할 수 있습니다.
    
    
    @Component
    script OrientedMover extends Component
        method void OnUpdate()
        self.ParentComponent.Entity.TransformComponent:Translate(0, -0.01)
        end
    
        method boolean OnConditionCheck(string nextStateName)
        local rotationAngle = self.ParentComponent.Entity.TransformComponent.Rotation.z
        if rotationAngle ~= 0 and rotationAngle % 90 == 0 then
            return true
        else
            return false
        end
        end
    end
    

#### 상태 추가하기

`StateComponent:AddState(string stateName, Type stateType)`를 사용해 아래의 예시 코드처럼 엔티티에 상태를 추가할 수 있습니다.
    
    
    @Component
    script MoveUpStateInitializer extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            self.Entity.StateComponent:AddState("MOVEUP", MoveUp)
        end
    end
    

#### 전이될 상태 정의하기

`StateComponent:AddCondition(string stateName, string nextStateName, boolean reverseResult = false)`를 사용해 현재 상태에서 넘어갈 다음 상태를 입력해 연결합니다. 아래의 예시 코드처럼 작성하면 MOVEDOWN에서 MOVEUP로의 상태 전이를 설정할 수 있습니다.
    
    
    @Component
    script MovementStateInitializer extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            self.Entity.StateComponent:AddCondition("MOVEDOWN", "MOVEUP", false)
        end
    end
    

#### 상태 전이 조건 삭제

`StateComponent:RemoveCondition(string stateName, string nextStateName)`을 사용해 현재 상태에서 다음 상태로 가는 조건을 삭제합니다. 아래와 같이 작성하면 위에서 `AddCondition()`으로 추가했던 조건(MOVEDOWN에서 MOVEUP로 상태 전이)을 삭제합니다.
    
    
    @Component
    script VerticalMovementDisabler extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            self.Entity.StateComponent:RemoveCondition("MOVEDOWN", "MOVEUP")
        end
    end
    

# 활용 예시

플레이어의 공격에 상태가 변하는 나무 상자를 만들어봅시다. 기본 상태인 IDLE, DEAD와 크리에이터가 만든 새로운 BROKEN 상태를 함께 활용합니다. 상태마다의 특정 애니메이션, 스프라이트를 추가해 상태 변화를 직관적으로 확인할 수 있습니다. 이 예시는 IDLE → BROKEN → DEAD → IDLE로 상태 전이하며, 상태마다 다른 스프라이트, 애니메이션을 연결해 재생하게 됩니다.

[gif](16559702783894245b9b6f57f41bc80bea36df066edd1)

> Tip. 만들기 - 새로 만들기 - 기본 템플릿을 사용해 플레이어의 기본 공격, 피격을 구성합니다. 해당 템플릿의 DefaultPlayer에  PlayerHit,  PlayerAttack 컴포넌트가 있는지 확인합니다. 

애니메이션, 스프라이트 RUID 목록

  * IDLE: b871399ae18d41c4b76c2ae79feefa7d
  * BROKEN: 76815f621e354f57b5adb5b9faaf9eb3
  * 사과: 014e7c98ad374cf49e7bf7ea72844241


  1. Workspace - MyDesk - Create Model을 눌러  Model_Box를 생성합니다. TransformComponent, SpriteRendererComponent, StateComponent, HitComponent를 추가합니다. 상자 RUID(b871399ae18d41c4b76c2ae79feefa7d)를 SpriteRendererComponent의 SpriteRUID에 추가합니다. [png](165596740975009ece0c49b59481fa2b86a0d736f770d)

  2. Workspace - MyDesk - Create Model을 눌러  Model_Apple을 생성합니다. TransformComponent, SpriteRenderComponent를 추가합니다. 사과 RUID(014e7c98ad374cf49e7bf7ea72844241)를 SpriteRendererComponent의 SpriteRUID에 추가합니다. [png](16559673922967908df7d77a04ff2b354bcb7740742da)

  3. MyDesk - Create Scripts - Create StateType를 눌러  BrokenBox를 생성합니다. 새로운 BrokenBox에서 Broken 상태에 진입했을 때의 동작을 아래와 같이 작성합니다. `SpawnService:SpawnByModelId()`에서 불러올 모델의 ID는 Model_Apple입니다.
         
         @Component
         script DelayedItemSpawner extends Component
         property boolean exitFlag = false
         property number timerId = 0
         
         method void OnEnter()
         self.exitFlag = false
         self.timerId = _TimerService:SetTimerOnce(function() self.exitFlag = true end, 1.1)
         
         local spriteRenderer = self.ParentComponent.Entity.SpriteRendererComponent
         spriteRenderer.SpriteRUID = "76815f621e354f57b5adb5b9faaf9eb3"  --BROKEN RUID
         
         _TimerService:SetTimerOnce(function() 
             _SpawnService:SpawnByModelId("model://4ac51a4c-ecf6-4ddf-b32a-516fc66234be", "Item", self.ParentComponent.Entity.TransformComponent.Position + Vector3(0,0.12,0), self.ParentComponent.Entity.Parent) 
         end, 0.21) 
         
         -- Model_Apple's model ID
         end
         end
         



    
    
    4. `OnConditionCheck()`에서 다음 상태로 넘어갈 때와 상태가 종료될 때를 아래와 같이 작성합니다. 
    
    

@Component script StateExitHandler extends Component method boolean OnConditionCheck(string nextStateName) return self.exitFlag end
    
    
    method void OnExit()
    self.ParentComponent.Entity.SpriteRendererComponent.SpriteRUID = "b871399ae18d41c4b76c2ae79feefa7d" --IDLE RUID
    _TimerService:ClearTimer(self.timerId)
    end
    

end
    
    
    5. 새로운 **ItemBoxComponent**를 생성하고, ** Model_Box**에 추가합니다.
    **BROKEN** 상태를 새롭게 추가하고, BROKEN에서 DEAD로 상태 전이를 추가합니다.
    
        ```lua
    @Component
    script BrokenStateConfigurator extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
        local state = self.Entity.StateComponent
    
        state:AddState("BROKEN", BrokenBox)
        state:AddCondition("BROKEN", "DEAD")
        end
    end
    

  6. ItemBoxComponent에서 상태가 변경될 때 발생하는 StateChangeEvent를 활용해 상자가 재등장하는 타이밍과 상태 전이 시의 엔티티 변화를 아래와 같이 작성합니다.
         
         @Component
         script EntityStatusHandler extends Component
         @EventSender("Self")
         handler HandleStateChangeEvent(StateChangeEvent event)
         --------------- Native Event Sender Info ----------------
         -- Sender: StateComponent
         -- Space: Server, Client
         ---------------------------------------------------------
         
         -- Parameters
         -- local CurrentStateName = event.CurrentStateName
         -- local PrevStateName = event.PrevStateName
         ---------------------------------------------------------
         
         -- There's no BROKEN state on client side of StateComponent.
         if self:IsServer() then
             if CurrentStateName == "HIT" then
                 self.Entity.StateComponent:ChangeState("BROKEN")
             elseif CurrentStateName == "DEAD" then
                 _TimerService:SetTimerOnce(function() self.Entity.StateComponent:ChangeState("IDLE") end, 5)
             end
         else
             if CurrentStateName == "IDLE" then
                 self.Entity.SpriteRendererComponent.Color.a = 1
             end
         end
         end
         end
         



    
    
    7. 애니메이션 재생이 끝난 후 보이지 않게 하고 싶다면 **ItemBoxComponent**에서 SpriteAnimPlayerEndEvent를 활용해 마지막 프레임을 투명하게 처리할 수 있습니다.
    
        ```lua
    @Component
    script SpriteCompletionHider extends Component
        @EventSender("Self")
        handler HandleSpriteAnimPlayerEndFrameEvent(SpriteAnimPlayerEndFrameEvent event)
        --------------- Native Event Sender Info ----------------
        -- Sender: ClimbableSpriteRendererComponent
        -- Space: Client
        ---------------------------------------------------------
        -- Sender: SpriteRendererComponent
        -- Space: Client
        ---------------------------------------------------------
    
        -- Parameters
        -- local FrameIndex = event.FrameIndex
        -- local ReversePlaying = event.ReversePlaying
        -- local TotalFrameCount = event.TotalFrameCount
        -- local AnimPlayer = event.AnimPlayer
        ---------------------------------------------------------
    
        if TotalFrameCount > 1 then
        	self.Entity.SpriteRendererComponent.Color.a = 0 
        end
        end
    end
    

> 더 알아보기 이 예제처럼 사과가 튀어 오르는 느낌을 주고 싶다면, Modle_Apple에 새로운 스크립트 컴포넌트를 추가하고, `TweenLogic:MakeTween()`을 활용해 보세요.

##### 참고 가이드

  * [아바타 애니메이션 제어하기](/docs/?postId=545)