StateSet 제어하기

StateSet은 엔티티의 상태를 제어할 때 사용합니다. StateSet Editor를 활용하면 보다 간편하게 상태 머신의 흐름을 시각적으로 구성할 수 있습니다.

##### 참고 가이드

  * [타입의 이해](/docs?postId=1038%7B%22target%22:%22_blank%22%7D)
  * [엔티티의 상태 제어하기](/docs?postId=686%7B%22target%22:%22_blank%22%7D)



##### 참고 API Reference

  * [StateComponent](/apiReference/Components/StateComponent%7B%22target%22:%22_blank%22%7D)
  * [StateChangeEvent](/apiReference/Events/StateChangeEvent%7B%22target%22:%22_blank%22%7D)



## StateSet

StateSet은 상태를 추가하고, 상태 사이의 전이 조건을 정의할 수 있습니다. 크리에이터는 3가지 프리셋과 기본 StateSet들을 활용할 수 있습니다. 크리에이터가 정의한 StateSet을 StateComponent의 `StateSetId` 프로퍼티 값으로 설정하면, 플레이 시점에 로드됩니다.

[png](178055904339324b4eefc3424472fa3c74a32abd6f8f5)

[{"이름":"StateSet","설명":"크리에이터가 정의할 수 있는 기본 StateSet입니다."},{"이름":"Player Preset","설명":"플레이어 엔티티의 기본 상태 머신이 구현된 프리셋입니다. 기본 플레이어 동작을 확장할 때 사용할 수 있습니다."},{"이름":"ChaseAI Preset","설명":"플레이어를 추적하는 AIChaseComponent를 사용한 몬스터 프리셋의 상태 머신입니다."},{"이름":"WanderAI Preset","설명":"주변을 배회하는 AIWanderComponent를 사용한 몬스터 프리셋 모델 상태 머신입니다."}]

## StateSet Editor

StateSet Editor에서 상태 노드를 추가하고, 전이를 선으로 연결해 상태 머신을 제작할 수 있습니다. StateSet Editor를 여는 방법은 2가지입니다.

  * Panels - StateSet Editor
  * Workspace - MyDesk - 생성한 StateSet 더블 클릭
  * Workspace - MyDesk - 생성한 StateSet - Property 창 - Open StateSetEditor 버튼 클릭



[png](1780561473893c6b78a91482f49bfbafa740aa11082c6)

StateSet Editor의 주요 영역은 다음과 같습니다.

[{"번호":"!","이름":"StateSet 추가","설명":"새로운 StateSet을 생성할 수 있습니다."},{"번호":"!","이름":"StateSet 선택","설명":"편집할 StateSet을 선택할 수 있습니다. StateSet을 편집 중에 다른 StateSet으로 변경할 경우, 변경 사항을 저장한 뒤 선택해야 합니다."},{"번호":"!","이름":"+ Node 버튼","설명":"좌측 상단의 버튼으로 새로운 상태 노드를 추가합니다."},{"번호":"!","이름":"Scale 슬라이더","설명":"캔버스 영역의 확대\/축소 배율을 조절합니다."},{"번호":"!","이름":"캔버스","설명":"상태 노드를 배치하고, 노드들의 전이를 편집할 수 있는 편집 화면입니다."},{"번호":"!","이름":"편집 창","설명":"노드, 전이의 프로퍼티 창입니다."}]

## 상태 노드

상태 노드는 IsNative 값에 따라 NativeStateType 또는 ScriptStateType으로 지정할 수 있습니다.

### 상태 노드 생성

상태 노드는 두 가지 방법으로 생성할 수 있습니다. 추가한 노드의 상태를 설정창에서 변경할 수 있습니다.

  * 캔버스에서 마우스 우클릭 - Create State Node 선택
  * 좌측 상단의 [+ Node] 버튼 클릭



### 시작 노드 지정

State가 어떤 노드부터 시작할지를 결정하는 시작 노드를 반드시 지정해야 합니다. 시작 노드로 정할 노드를 선택하고, 컨텍스트 메뉴에서 Set Start Node를 선택합니다. 시작 노드는 색상이 파란색으로 변경됩니다. [png](1780620483740628f8b66540f45f4abee29967c925a39)

### 상태 노드 이름

모든 상태 노드는 StateSet 내에서 중복되지 않는 고유한 이름을 가져야 합니다. 이름은 대문자 알파벳, 숫자, `_` 조합만 가능합니다.

> 주의 다음 키워드는 네이티브 예약어로 사용되고 있어 주의가 필요합니다. 네이티브 예약어를 사용하면 `Name` 필드 좌측에 경고 아이콘이 표시됩니다. "IDLE", "ATTACK", "ATTACK_WAIT", "CROUCH", "DEAD", "FALL", "CLIMB", "LADDER", "HIT", "JUMP", "MOVE", "SIT"

### 상태 노드 종류 구분

#### NativeStateType

`IsNative`를 true로 설정하면 NativeStateType에 지정된 값들 중 하나를 선택할 수 있습니다. NativeStateType은 크리에이터가 동작을 수정할 수 없으므로 Player Preset처럼 플레이어의 상태를 확장하는 경우 외에 사용하지 않는 것을 권장합니다. NativeStateType의 값들은 아래와 같습니다.

[{"타입":"Idle","설명":"정해진 행동이 없는 상태"},{"타입":"Move","설명":"이동 상태"},{"타입":"Jump","설명":"점프 상태"},{"타입":"Fall","설명":"낙하 상태"},{"타입":"Climb","설명":"로프 등반 상태"},{"타입":"Ladder","설명":"사다리 등반 상태"},{"타입":"Crouch","설명":"엎드린 상태"},{"타입":"Sit","설명":"앉아 있는 상태"},{"타입":"Attack","설명":"공격 중인 상태"},{"타입":"AttackWait","설명":"공격 후 딜레이 상태"},{"타입":"Hit","설명":"피격 상태"},{"타입":"Dead","설명":"사망 상태"},{"타입":"DoNothing","설명":"기본값"}]

#### ScriptStateType

`IsNative`를 false로 설정하면 ScriptStateTypeId에서 크리에이터가 생성한 StateType을 선택할 수 있습니다. StateType 생성과 활용 방법은 [엔티티의 상태 제어하기](/docs?postId=686%7B%22target%22:%22_blank%22%7D)를 참고합니다. ScriptStateType은 `StateComponent.IsLegacy` 값에 따라 이벤트 실행 가능 여부가 결정됩니다. `StateComponent.IsLegacy`가 true인 경우 서버 플레이어 엔티티, 서버 스폰 엔티티, 클라이언트 스폰 엔티티의 이벤트를 `OnUpdate()`에서 호출할 수 있습니다.

[png](178056088918290b2acc1e0f543c1b516ae815da9c393)

## 전이

전이는 특정 상태에서 조건을 만족했을 때 다음 상태로 이동할 수 있게 상태와 상태를 연결합니다.

### 전이 생성

StateSet Editor에서 노드와 노드를 연결해 전이를 생성할 수 있습니다. 조건이 설정되면 전이를 나타내는 선이 흰색으로 표시되고, 조건이 없는 경우 노란색으로 표시됩니다.

  1. 전이를 생성할 State를 선택 - 컨텍스트 메뉴 - Make Transition을 선택합니다. [png](178097323776218d2468d24f54f5e9d81e75ee6e01073)
  2. 생성된 선을 연결할 도착 노드로 드래그하고, 도착 노드를 선택합니다. [png](1780973259996f0fa07896fec4d28bdd1276dc3e2942d)
  3. 연결된 선을 선택하고, 프로퍼티 창에서 전이의 프로퍼티 값과 조건을 설정합니다.



#### 프로퍼티

  * Previous Node Name: 전이가 시작되는 노드입니다. 전이를 생성하면 값이 자동으로 결정됩니다.
  * Next Node Name: 전이 도착 노드입니다. 생성 시 자동으로 결정됩니다.
  * OrderInTransit: 전이의 평가 순서를 결정합니다. 값이 높은 전이부터 먼저 평가됩니다. 같은 값을 가진 전이끼리는 순서가 보장되지 않으며, 전이 내부의 조건들은 작성된 순서대로 평가됩니다.
  * TransitIfAnyConditionIsTrue: 여러 조건을 어떻게 결합할지 결정합니다. 값이 true인 경우 조건 중 하나라도 참으로 평가되면 전이를 수행합니다. false인 경우 모든 조건이 참으로 평가되어야 전이를 수행합니다.
  * Conditions: 전이가 발생할 수 있는 조건을 추가합니다. NativeConditionType, ScriptConditionType이 있습니다.



#### NativeConditionType

Condition의 IsNative를 true로 설정하면 NativeConditionType으로 전이를 사용하게 됩니다. NativeCondition 전이는 조건이 이미 정의되어 있으므로 크리에이터가 동작을 수정할 수 없습니다. [png](1780562743256a5499757f58f4cfa838da5bc16ddc64e)

[{"타입":"Always","설명":"항상 참으로 평가되는 조건"},{"타입":"HasOnetimeAnimDone","설명":"애니메이션이 한 차례 재생을 마치면 참으로 평가"},{"타입":"HasTimePassed","설명":"Duration에 설정된 시간이 경과되면 참으로 평가"},{"타입":"IsOnGround","설명":"땅 위에 있을 경우 참으로 평가"},{"타입":"IsStopped","설명":"정지 상태일 경우 참으로 평가"},{"타입":"IsNotClimbing","설명":"등반 중이 아닐 경우 참으로 평가"},{"타입":"Never","설명":"절대 참으로 평가되지 않는 조건"}]

#### ScriptConditionType

Condition의 IsNative를 false로 설정하면, ScriptConditionTypeId에서 ConditionType를 선택할 수 있습니다. ConditionType 전이를 사용하면 크리에이터가 전이 발생 조건을 직접 정의할 수 있습니다. 또한 `ParentComponent` 프로퍼티를 통해 `StateComponent`에 접근할 수 있습니다.

[png](1780560831186086aa7c917ba4168bbd64208999d07e8)

## StateComponent와의 관계

크리에이터가 생성한 StateSet은 `StateComponent.StateSetId`에 지정되어야 사용이 가능합니다. 또한, StateComponent의 `IsLegacy` 값에 따라 StateSet에 정의된 StateType과 ConditionType의 이벤트 실행 가능 공간이 달라집니다.

### StateType과 StateComponent

각 실행 공간에서 StateType의 `OnEnter(), OnUpdate(), OnExit()` 이벤트의 실행 여부는 상태 머신을 실행하는 엔티티의 속성과 `StateComponent.IsLegacy` 프로퍼티에 의해 결정됩니다. StateType은 `ParentComponent` 프로퍼티를 통해 상태 머신을 구동 중인 StateComponent 인스턴스에 접근하여 로직에 필요한 다양한 정보를 참조할 수 있습니다.

`StateComponent.IsLegacy`가 `true`일 때

[{"구분":"PlayerControllerComponent가 있는 엔티티(서버)","OnEnter 호출":"⭕","OnUpdate 호출":"❌","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(플레이어의 클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(타플레이어의 클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(서버)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"❌","OnExit 호출":"⭕"},{"구분":"로컬화된 엔티티","OnEnter 호출":"⭕","OnUpdate 호출":"❌","OnExit 호출":"⭕"}]

`StateComponent.IsLegacy`가 `false`일 때

[{"구분":"PlayerControllerComponent가 있는 엔티티(서버)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(플레이어의 클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(타플레이어의 클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(서버)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(클라이언트)","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"},{"구분":"로컬화된 엔티티","OnEnter 호출":"⭕","OnUpdate 호출":"⭕","OnExit 호출":"⭕"}]

### ConditionType과 StateComponent

`OnConditionCheck()`는 StateComponent 내부의 상태 머신이 현재 상태에서 다른 상태로의 전이를 시도할 때 호출되는 실제 조건 검사용 이벤트입니다. 상태 머신을 실행하는 엔티티의 속성과 `StateComponent.IsLegacy` 프로퍼티에 따라 어느 하나의 실행 공간에서만 호출됩니다. ConditionType은 `ParentComponent` 프로퍼티를 통해 상태 머신을 구동 중인 StateComponent 인스턴스에 접근하여 로직에 필요한 다양한 정보를 참조할 수 있습니다.

`StateComponent.IsLegacy`가 `true`일 때

[{"구분":"PlayerControllerComponent가 있는 엔티티(서버)","OnConditionCheck 호출":"❌"},{"구분":"PlayerControllerComponent가 있는 엔티티(플레이어의 클라이언트)","OnConditionCheck 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(타플레이어의 클라이언트)","OnConditionCheck 호출":"❌"},{"구분":"PlayerControllerComponent가 없는 엔티티(서버)","OnConditionCheck 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(클라이언트)","OnConditionCheck 호출":"❌"},{"구분":"로컬화된 엔티티","OnConditionCheck 호출":"❌"}]

`StateComponent.IsLegacy`가 `false`일 때

[{"구분":"PlayerControllerComponent가 있는 엔티티(서버)","OnConditionCheck 호출":"❌"},{"구분":"PlayerControllerComponent가 있는 엔티티(플레이어의 클라이언트)","OnConditionCheck 호출":"⭕"},{"구분":"PlayerControllerComponent가 있는 엔티티(타플레이어의 클라이언트)","OnConditionCheck 호출":"❌"},{"구분":"PlayerControllerComponent가 없는 엔티티(서버)","OnConditionCheck 호출":"⭕"},{"구분":"PlayerControllerComponent가 없는 엔티티(클라이언트)","OnConditionCheck 호출":"❌"},{"구분":"로컬화된 엔티티","OnConditionCheck 호출":"⭕"}]

## 활용 예제

일정 확률로 두 가지 아이템 중 하나가 나타나는 상태 머신을 만들어봅시다. Panels - Package Storage - MSW 패키지 - Example-StateSet을 설치하면 완성된 결과를 확인할 수 있습니다. [gif](1780994162378d83217584dc845af9c7aff095ab9b8fa)

### Gacha StateSet 만들기

2가지 확률이 나오기 위해서는 준비, 실행, 결과 1, 결과 2 총 4가지 노드가 필요합니다.

  1. 새로운 Gacha StateSet을 생성하고, 시작 노드의 이름을 READY로 변경합니다.

  2. 새로운 노드 3개를 생성하고 이름을 각각 TRY, SPAWN_SCROLL, SPAWN_POTION으로 설정합니다. [png](1780973868908afc2c223e85947a89235dbb3ac1d4c4a)

  3. Workspace - MyDesk - Create Scripts - Create Logic을 눌러 NetworkLogic을 생성하고 아래와 같이 작성합니다.
         
         @Component
         script TemplateScript_1398_1764 extends Component
         -- [Original MLUA template/example preserved as comment]
         -- Method
         --     boolean HasAuthority(Entity entity)
         --     {
         --     		local hasAuthority = false
         -- 
         --     		if entity ~= nil then
         --     			local entityOwnerId = entity.OwnerId
         --     			if _UtilLogic:IsNilorEmptyString(entityOwnerId) then
         --     				if entity.Localize then
         --     					hasAuthority = self:IsClient()
         --     				else
         --     					hasAuthority = self:IsServer()
         --     				end
         --     			else
         --     				local localPlayerEntity = _UserService.LocalPlayer
         --     				if localPlayerEntity ~= nil then
         --     					hasAuthority = (entityOwnerId == localPlayerEntity.OwnerId)
         --     				end
         --     			end
         --     		end
         -- 
         --     		return hasAuthority
         -- 
         --     }
         end
         



    
    
    4. **Workspace - MyDesk - Create Scripts - Create StateType**을 눌러 새로운 **SpawnModel**을 생성합니다.
    
        ```lua
    @Component
    script ChildSpawner extends Component
        property string ModelId = ""
        property string Name = ""
        property Vector3 SpawnPosition = Vector3(0,0,0)
        property Entity Parent = nil
        property Entity Spawned = nil
    
        method void OnEnter()
        local parent = self.Parent
        if parent == nil then
            parent = self:GetParentEntity()
            if parent == nil then
                return
            end
        end
    
        if parent.Localize then
            if _NetworkLogic:IsServer() then
                return
            end
        else
            if _NetworkLogic:IsClient() then
                return
            end
        end
    
        self.Spawned = _SpawnService:SpawnByModelId(self.ModelId, self.Name, self.SpawnPosition, parent)
        end
    
        method Entity GetParentEntity()
        local parentEntity = nil
    
        local parentStateComponent = self.ParentComponent
        if parentStateComponent ~= nil then
            parentEntity = parentStateComponent.Entity
        end
    
        return parentEntity
        end
    end
    

### Gacha 모델 생성

생성한 Gacha StateSet을 사용하기 위해서 StateComponent가 있는 엔티티를 생성합니다.

  1. Workspace - MyDesk - Create Model을 눌러 새로운 Gacha 모델을 생성합니다.
  2. 모델에 TransformComponent, RigidbodyComponent, SpriteRendererComponent, StateComponent를 추가합니다. 
     * SpriteRUID: 939597de7475400ab64231404c764e4c
  3. StateComponent의 StateSetId에 Gacha를 지정합니다.
  4. 맵에 모델을 배치합니다.



### 전이 생성

전이 조건으로 활용할 ConditionType을 만들어 봅시다. 특정 시간이 될 때마다 상태를 변경해줄 HasTimePassed, 아이템이 나타나는 확률을 결정해 줄 BasicRandom 두 가지가 필요합니다.

  1. Workspace - MyDesk - Create Scripts - Create ConditionType을 눌러 HasTimePassed를 생성합니다.

  2. 생성한 HasTimePassed에 아래와 같이 작성합니다.
         
         @Component
         script TransitionTimer extends Component
         property number Duration = 1
         property number OnPreviousStateEnteredTime = 0
         
         method void OnPreviousStateEnter()
         self.OnPreviousStateEnteredTime = _UtilLogic.ServerElapsedSeconds
         end
         
         method boolean OnConditionCheck(string nextStateName)
         return (_UtilLogic.ServerElapsedSeconds - self.OnPreviousStateEnteredTime) >= self.Duration
         end
         end
         



    
    
    3. **Workspace - MyDesk - Create Scripts - Create ConditionType**을 눌러 **BasicRandom**을 생성합니다.
    4. 생성한 **BasicRandom**에 아래와 같이 작성합니다.
        
        ```lua
    @Component
    script RandomChanceCondition extends Component
        property number Chance = 0.5
        property number Result = 0.5
    
        method void OnPreviousStateEnter()
        self.Result = _UtilLogic:RandomDouble()
        end
    
        method boolean OnConditionCheck(string nextStateName)
        return self.Result <= self.Chance
        end
    end
    

### Reward StateSet 만들기

  1. 새로운 Reward StateSet을 생성하고, 시작 노드의 이름을 POPOUT으로 변경합니다.

  2. Workspace - MyDesk - Create Scripts - Create StateType을 눌러 새로운 AddForce를 생성합니다.

  3. 아이템이 나타나는 방향과 속도를 설정할 수 있도록 아래와 같이 작성합니다.
         
         @Component
         script ImpulseApplier extends Component
         property Vector2 Force = Vector2.zero
         
         method void OnEnter()
         local parentRigidbodyComponent = self:GetParentRigidbodyComponent()
         if parentRigidbodyComponent == nil then
             return
         end
         
         if _NetworkLogic:HasAuthority(parentRigidbodyComponent.Entity) == false then
             return
         end
         
         parentRigidbodyComponent:AddForce(self.Force)
         end
         
         method Entity GetParentEntity()
         local parentEntity = nil
         
         local parentStateComponent = self.ParentComponent
         if parentStateComponent ~= nil then
             parentEntity = parentStateComponent.Entity
         end
         
         return parentEntity
         end
         end
         



    
    
    ### Potion, Scroll 모델 생성
    Gacha 모델의 상태가 SPAWN_POTION, SPAWN_SCROLL로 변경되었을 때 맵에 나타날 아이템들을 생성해 봅시다.
    
    1. **Workspace - MyDesk - Create Model**을 눌러 새로운 **Potion, Scroll** 모델을 생성합니다. 
    2. [모델 에디터](/docs?postId=1324{"target":"_blank"})에서 **TransformComponent, RigidbodyComponent, SpriteRendererComponent, StateComponent**를 추가합니다.
        * Potion SpriteRUID: c1f920047b71463e87232c4f42cf8241
        * Scroll SpriteRUID: 071abdd0474e4933ab44d53eb7d349f9
    3. **StateComponent**의 **StateSetId**에 **Reward**를 지정합니다.
        
    ### Gacha 상태 머신 완성
    1. **Gacha** StateSet의 노드들에 아래와 같이 값을 설정합니다.
    
        | 이름 | IsNative | ScriptStateTypeId  | 프로퍼티 | 
        | --- | --- | --- | --- |
        | READY |  true |  -   |  NativeStateType: DoNothing |
        | TRY |  true|  -   | NativeStateType: DoNothing | 
        | SPAWN_POTION |  false|  SpawnModel   |  ModelId: Potion 모델의 Entry ID  
    Name:Potion  
    SpawnPosition -0.3, 0,0  
    Parent: nil  | 
        | SPAWN_SCROLL | false|  SpawnModel   | ModelId: Scroll 모델의 Entry ID  
    Name:Scroll  
    SpawnPosition -0.3, 0,0  
    Parent: nil | 
    
    2. **각 노드를 선택 - 컨텍스트 메뉴 Make Transition**을 선택하고, 아래 그림과 같이 연결합니다. 
        [png](1780904022260bb3735df142e49639501396c387d5779)
    
        * READY → TRY: 3초의 대기 상태 이후에 TRY로 상태를 변환합니다.
        * TRY → READY: TRY 상태에서 READY로 변환합니다.
        * TRY → SPAWN_SCROLL: 50%의 확률로 TRY에서 SPAWN_SCROLL로 전환합니다.
        * TRY → SPAWN_POTION: 50%의 확률로 TRY에서 SPAWN_POTION으로 전환합니다.
        * SPAWN_POTION → READY: 아이템이 나타나면 READY 상태로 돌아갑니다.
        * SPAWN_SCROLL → READY: 아이템이 나타나면 READY 상태로 돌아갑니다.
          
    3. 연결한 전이들에 아래와 같이 값을 입력합니다.
        [png](1780904022260bb3735df142e49639501396c387d5779)
    
        | 방향 | OrderInTransit | Condition  |
        | --- | --- | --- | 
        | READY → TRY |  0 |  IsNative: false  
     ScriptConditionType: HasTimePassed  
    Duration:3 |
        | TRY → READY | 0  | IsNative: true  
    NativeConditionType: Always   |
        | TRY → SPAWN_POTION |  1  | IsNative: false  
     ScriptConditionType: BasicRandom  
    Chance:0.5  |
        | TRY → SPAWN_SCROLL |  2 |  IsNative: false  
     ScriptConditionType: BasicRandom  
    Chance:0.5   |
        | SPAWN_POTION → READY | 0  |   IsNative: true  
    NativeConditionType: Always  |
        | SPAWN_SCROLL → READY  | 0  |   IsNative: true  
    NativeConditionType: Always  |
    
    
    #### Reward 상태 머신 완성
    1. **Reward** StateSet의 **ScriptStateTypeId**에 **AddForce**를 지정하고, **Force** 값을 입력합니다.
        [png](17809920829660c9bed9ca4ba4a81a0010850b106f6dd)
    
        | 이름 | IsNative | ScriptStateTypeId  | 프로퍼티 | 
        | --- | --- | --- | --- |
        | POPOUT  |  false |  AddForce  |  Force: -2.5, 3.5 |
    
    2. Gacha 모델을 맵에 배치하고, 3초마다 두 가지 모델 중 하나가 나타나는지 확인합니다.