플레이어 설정과 제어

# 학습 과정 소개

Player 모델에는 캐릭터의 초기 속성을 만들 때도 유용하게 사용하지만, 캐릭터를 이동시키거나, 상태를 제어할 때 유용하게 사용할 수 있습니다. PlayerComponent와 PlayerControllerComponent의 개념을 알아보고, 액션과 상태 애니메이션의 관계를 파악해 크리에이터의 의도에 따라 플레이어가 동작할 수 있게 만들어봅시다.

# PlayerComponent 개념

PlayerComponent는 다른 엔티티와 다르게 플레이어만이 가지고 있는 특성을 모아 놓은 컴포넌트입니다. 대표적으로 플레이어끼리 타격을 줄 수 있는 PVP 모드 기능이 있습니다. PlayerComponent의 함수는 플레이어를 제어하기 위해 사용합니다. 플레이어를 죽게 만들거나, 플레이어를 다른 맵 혹은 특정 지점으로 옮길 수 있습니다. Player 모델에 PlayerComponent가 포함되어 있지 않으면, 플레이어로 인식하지 못합니다. Player에게만 사용하는 컴포넌트인 특성을 활용해 Player를 식별할 때 PlayerComponent를 기준으로 삼을 수 있습니다. 예를 들어, PlayerComponent가 있는 엔티티를 특정 위치로 이동시키거나, 추가 조건을 붙여 죽음 상태로 만들 수 있습니다.

#### 주요 프로퍼티 소개

[{"프로퍼티":"MaxHp","설명":"Player의 최대 체력을 정할 수 있습니다. 기본 값은 1000입니다. 이 값은 UI 상태 창 모델의 HP와 연동되어, 변경한 값이 UI 모델에 반영됩니다."},{"프로퍼티":"PVPMode","설명":"활성화 시 플레이어끼리 타격을 줄 수 있습니다."},{"프로퍼티":"RespawnDuration","설명":"플레이어가 죽은 후 다시 맵에 리스폰될 때까지 걸리는 시간을 정합니다. 기본값은 5초입니다."},{"프로퍼티":"Enable","설명":"컴포넌트의 활성\/비활성 상태를 설정할 수 있습니다."},{"프로퍼티":"Nickname","설명":"Player가 웹에서 정한 닉네임을 의미합니다. Player 모델에서 NameTagComponent가 있다면 플레이어의 닉네임이 보입니다. 프로퍼티 에디터 창에서 노출되지 않으며, 스크립트 작성 시 활용할 수 있습니다."},{"프로퍼티":"RespawnPosition","설명":"플레이어가 리스폰될 위치를 정할 수 있습니다. 위치를 지정하지 않은 경우, SpawnLocation의 위치, 맵 진입 시점의 위치를 순차적으로 따라 설정됩니다. 프로퍼티 에디터 창에서 노출되지 않으며, 스크립트 작성 시 활용할 수 있습니다."},{"프로퍼티":"RespawnTime","설명":"플레이어가 다시 맵에 진입하기까지 걸리는 시간을 의미합니다. 프로퍼티 에디터 창에서 노출되지 않으며, 스크립트 작성 시 활용할 수 있습니다."},{"프로퍼티":"UserId","설명":"플레이어의 고유 식별자입니다. Client 실행제어 함수의 targetUserId 매개 변수에 사용할 수 있습니다."}]

# PlayerControllerComponent 개념

PlayerControllerComponent는 입력에 따라 액션을 수행하고, 특정 조건을 처리합니다.예를 들어, 공격 키를 눌렀을 때 특정 애니메이션을 재생하는 액션을 연결하면 공격 키가 입력되었을 때마다 작동합니다. 그러나 사다리에 매달려 있을 때는 사다리에 매달린 애니메이션이 재생되는 중이므로 공격 애니메이션은 재생되지 않게 해야 오르는 동작이 어색하지 않습니다. 그러므로 사다리에 플레이어가 매달려 있을 때, '공격 키를 눌러도 공격 액션이 재생되지 않는다'란 조건을 추가할 수 있습니다.

기본 Player 모델에는 PlayerControllerComponent와 StateComponent 모두 기본으로 포함되어 있으나, 크리에이터의 필요에 따라 둘 중 하나를 지운다면 정상 동작하지 않습니다. PlayerControllerComponent가 StateComponent를 참조하고 있기 때문입니다. 그러므로 PlayerControllerComponent가 정상적으로 작동하기 위해선 Player 모델에 StateComponent가 함께 있어야 합니다.

> Tip. PlayerControllerComponent에서 입력, 액션, 조건을 처리했을 때 애니메이션 재생은 StateStringToAvatarActionComponent에서 처리하게 됩니다. 유저가 키를 눌러 어떤 'Input'이 발생하면, PlayerControllerComponent가 이를 감지하고 맞는 State로 변경하며 Action을 처리합니다. 이후 StateComponent가 State 변경과 동작을 처리하며, StateChangeEvent를 발생시킵니다. AvatarRendererCompoent와 같은 애니메이션 관련 컴포넌트가 이 이벤트를 받아 애니메이션을 재생시키게 됩니다. 

#### 프로퍼티 소개

[{"프로퍼티":"AlwaysMovingState","설명":"활성화 시 이동 애니메이션이 가만히 있더라도 계속 재생됩니다."},{"프로퍼티":"FixedLookAt","설명":"값을 변경하면 캐릭터가 방향을 바꾸어도, 캐릭터의 이동 모습은 바뀌지 않습니다. 한 방향만 바라보게 만들 수 있습니다."},{"프로퍼티":"LookDirectionX","설명":"양수로 값을 변경하면, 캐릭터가 오른쪽을 바라본 상태가 기본이 됩니다."},{"프로퍼티":"Enable","설명":"컴포넌트의 활성\/비활성 상태를 설정할 수 있습니다."}]

#### 기본 조작 key, value

메이플스토리 월드에서 기본으로 설정되어 있는 이동과 행동 키입니다. 특별한 조작을 부여하고 싶지 않다면 해당 키를 그대로 사용하면 되고, 특별한 키가 필요할 경우 추가할 수 있습니다.

[{"key":"W, UpArrow","value":"MoveUp"},{"key":"A, DownArrow","value":"MoveLeft"},{"key":"S, LeftArrow","value":"MoveDown"},{"key":"D, RightArrow","value":"MoveRight"},{"key":"Space","value":"Jump"},{"key":"LeftAlt","value":"Jump2"},{"key":"LeftControl","value":"Attack"},{"key":"C","value":"Sit"}]

> Tip. LeftAlt는 Jump로 통합 예정입니다. 제작에 참고해주세요.

# 활용 예시

#### 정해진 시간이 지나면 다른 맵으로 이동시키기

`MoveToMapPosition()`를 이용해 플레이어를 특정 위치로 이동시킬 수 있습니다. 이 기능을 활용해 여러 맵으로 모든 유저를 한 번에 옮겨 새로운 맵에서 새로운 게임을 할 수 있게 합니다.

  1. [png](1634600723422190820861d63456fa40e21031bd5ffa2) MyDesk에 새로운 PlayerManagement 컴포넌트를 생성하고, [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.
  2. `TimerService`를 이용해 10초를 셀 수 있게 만들고, 10초 뒤 이동할 map의 이름과 그 맵의 위치를 입력합니다.  
맵의 이름은 Hierarchy의 이름과 동일해야 합니다.



> 더 알아보기 `TeleportService:TeleportToEntityPath()` 모든 Entity를 옮길 수 있지만, `PlayerComponent:MoveToMapPosition()`는 플레이어에만 활용 가능합니다.
    
    
    @Component
    script GlobalPlayerRelocator extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
        _TimerService:SetTimerOnce(function()
            for userId, userEntity in pairs(_UserService.UserEntities) do
                userEntity.PlayerComponent:MoveToMapPosition('map02',Vector2(2,2)) 
            end
        end, 10)
        end
    end
    

[gif](1656586279970675fa3b0db73418d92fccc5d03da1a6e)

#### 죽은 자리에서 리스폰 시키기

Player의 리스폰 지점은 SpawnLocation와 맵 진입 시의 위치를 따르지만, 크리에이터가 임의로 플레이 중 죽은 위치에서 바로 리스폰 시킬 수 있습니다.

  1. [png](1634600723422190820861d63456fa40e21031bd5ffa2) MyDesk에 새로운 PlayerSetting 컴포넌트를 생성하고, [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.
  2. `ConnectEvent`를 이용해 죽었을 때의 위치를 가져와 리스폰 위치로 정합니다.


    
    
    @Component
    script RespawnAnchor extends Component
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
        --     self.Entity:ConnectEvent(DeadEvent, function()
        --         self.Entity.PlayerComponent.RespawnPosition = self.Entity.TransformComponent.Position
        --     end)
        -- [Commented: MLUA requires @EventSender + handler pattern]
        end
    end
    

[gif](16565863334284c08832fec434f4789427b1b5a33d6ef)

#### 플레이어만 특정 지점으로 옮기기

수많은 엔티티 중 PlayerComponent가 있는 엔티티를 플레이어로 인식해, 특정 지점으로 이동시킬 수 있습니다. 특정 엔티티에 붙은 TriggerComponent가 PlayerComponent를 인식할 수 있게 만듭니다. 이 엔티티는 플레이어만 알아차릴 수 있기 때문에 같은 공간에서 똑같이 움직이는 몬스터 같은 엔티티는 이동시킬 수 없습니다.

  1. 플레이어가 부딪칠 수 있는 엔티티를 맵에 배치하고 이름을 MagicStone으로 변경한 뒤 TriggerComponent를 추가합니다.
  2. 도착지는 특정 엔티티의 경로로 지정하기 위해 맵에 포탈 오브젝트를 추가합니다.
  3. [png](1634600723422190820861d63456fa40e21031bd5ffa2) MyDesk에 새로운 PlayerMangement 컴포넌트를 생성하고, [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.
  4. 특정 엔티티에 충돌한 엔티티에 PlayerComponent가 있다면, 입력한 경로('maps/map01/portal-3')로 플레이어를 이동시킵니다.


    
    
    @Component
    script MagicStoneTeleporter extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
             local MagicStone = _EntityService:GetEntityByPath('/maps/map01/MagicStone') -- 플레이어와 부딪힐 엔티티의 경로를 입력합니다. 
        --      MagicStone:ConnectEvent(TriggerEnterEvent, function(event)
        --         if event.TriggerBodyEntity.PlayerComponent then
        --             event.TriggerBodyEntity.PlayerComponent:MoveToEntityByPath('/maps/map01/portal-3')
        --         end
        --      end)
        -- [Commented: MLUA requires @EventSender + handler pattern]
        end
    end
    

[gif](1656586368440e1c78d968dba467b9b2d5cca978b4996)

#### 특정 조건을 만들어 액션 제한하기

플레이어의 기본 속성에 발동할 수 있게 하는 조건을 추가할 수 있습니다. `AddCondition()` 함수를 사용해 플레이어의 행동을 제약하기 위한 조건을 추가할 수 있습니다. 기본적으로 사용 가능한 공격, 점프 등을 일시적으로 특정 조건을 만족할 때 기능 발동을 제한하거나, 추가 발동 조건을 추가해 색다른 재미를 선사할 수 있습니다.

  1. [png](1634600723422190820861d63456fa40e21031bd5ffa2) MyDesk에 PlayerSetting 컴포넌트를 생성하고, [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.
  2. Jump 액션이 실행되는 조건을 작성합니다.  
10 이상은 Jump가 실행되게 작성하면 반대 조건 또한 성립합니다.
  3. space bar를 눌러 점프를 했음에도 점프 동작이 재생되지 않는 것을 확인할 수 있게 log를 추가합니다.


    
    
    @Component
    script HealthJumpLimiter extends Component
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
        local playerController = self.Entity.PlayerControllerComponent
        playerController:AddCondition("Jump", function()
            if self.Entity.PlayerComponent.Hp < 10 then
                log("Stop Jumping")
                return false
            end
            return true
        end)
        end
    end
    

[gif](1656586397424b751fb67c7974fe694521961c9a7ecab)

#### 기존 조작키 삭제, 새로운 키 만들기

크리에이터의 기획에 맞게 기존의 키를 삭제하고, 새로운 키로 대체할 수 있습니다. 이 기능을 사용해 W, A, S, D 키 이동을 삭제하고, 새로운 키로 만들 수 있습니다.

  1. [png](1634600723422190820861d63456fa40e21031bd5ffa2) MyDesk에 PlayerSetting 컴포넌트를 생성하고, [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.
  2. `RemoveActionKey`를 이용해 왼쪽 Ctrl 키에 연결된 액션을 삭제하고, `SetActionKey`를 이용해 F 키에 Attack을 연결하는 스크립트를 작성합니다.


    
    
    @Component
    script ActionKeyInitializer extends Component
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
        local playerController = self.Entity.PlayerControllerComponent
        playerController:RemoveActionKey(KeyboardKey.LeftControl)
        playerController:SetActionKey(KeyboardKey.F, "Attack")
        end
    end
    

##### 참고 가이드

  * [캐릭터 초기 속성 편집](/docs?postId=48)
  * [공격과 피격](/docs?postId=206)