공격과 피격

# 학습 과정 소개

메이플스토리에서 플레이어는 모험가가 되어 메이플 세계를 탐험하고 몬스터를 만나 전투를 합니다. 전투 과정에서 다른 몬스터를 공격하거나 피격당합니다. 메이플스토리뿐만 아니라 다양한 게임에서 공격과 피격은 중요한 개념입니다. 이번 과정에서는 공격과 피격을 구현하는 방법을 학습합니다.

##### 참고 가이드/레퍼런스

[AttackComponent](/apiReference/Components/AttackComponent%7B%22target%22:%22_blank%22%7D) [HitComponent](/apiReference/Components/HitComponent%7B%22target%22:%22_blank%22%7D) [충돌 그룹 만들기](/docs?postId=754%7B%22target%22:%22_blank%22%7D) [엔티티의 충돌](/docs/?postId=175%7B%22target%22:%22_blank%22%7D) [엔티티의 상태 제어하기](/docs/?postId=686%7B%22target%22:%22_blank%22%7D) [플레이어 설정과 제어](/docs/?postId=547%7B%22target%22:%22_blank%22%7D)

# AttackComponent 알아보기

메이플스토리 월드에서는 기본적으로 `Ctrl` 키를 누르면 공격 애니메이션이 재생되지만, 직접적인 공격이 가해지지 않습니다. 이 애니메이션이 재생될 때 실제로 타격이 되게 하려면, AttackComponent를 활용해야 합니다. 게임에서는 한 플레이어가 다양한 공격을 할 수 있고, 공격 방식마다 공격 범위를 다르게 설정할 수 있습니다. 공격을 구현할 때는 `AttackComponent`를 활용해 제작합니다.

#### 공격 범위 설정

AttackComponent의 `Attack()`과 `AttackFrom()` 두 가지 함수를 활용해 공격 범위를 설정할 수 있습니다.

  * `Attack()`: 캐릭터 위치를 기준으로 공격 범위를 설정합니다. 공격 범위 형태는 shape 타입으로 설정합니다. [png](16621061020831776c9b26fd14098bed69cf25a44963f)

  * `AttackFrom()`: 월드 좌표를 기준으로 사각형 공격 범위를 설정합니다. [png](166210611645871cd92bfcd6640b6b83e6fce31be8829)

  * `AttackFast()`: 반환 값이 없는 Attack 함수입니다. 캐릭터 위치 기준 불필요한 table 객체 생성을 줄일 수 있습니다. 공격 범위 형태는 shape 타입으로 설정합니다.




#### 대미지 설정

대미지는 `CalcDamage()` 함수로 만들 수 있습니다. 크리티컬 공격의 대미지는 `CalcCritical()` 함수를 사용해 공식을 만들어 활용할 수 있습니다. 이때 `GetCriticalDamageRate()` 함수를 활용해 크리티컬 공격을 할 때 기본 대미지 대비 몇 배의 대미지를 줄 것인지 정할 수 있습니다.

# HitComponent 알아보기

엔티티가 피격 당하기 위해서는 `HitComponent`가 있어야 합니다. `HitComponent`는 엔티티의 충돌체 모양, 크기, 충돌체 그룹을 설정할 수 있습니다. `AttackComponent`의 함수를 활용해 공격 했을 때 유효한 공격 범위 안에 `HitComponent`를 가진 엔티티가 있다면, 그 엔티티는 피격 처리 대상이 됩니다. `HitComponent`가 엔티티가 피격 대상인지 판단하고, 엔티티가 받을 최종 대미지를 결정합니다.

# 공격과 피격 관계

공격과 피격을 유기적으로 구성하기 위해서는 `AttackComponent`, `HitComponent`를 모두 사용해야 합니다. 권투 경기에서도 상대가 공격을 완전히 회피하거나, 최소한의 타격만 받을 때가 있습니다. 이처럼 공격은 성공했지만, 실제 타격 결과와 다른 다양한 경우의 수를 처리하고 싶다면, 두 컴포넌트에서 각각 공격과 피격을 구현해야 합니다.

#### 공격 대상 판단하기

크리에이터의 공격 제작 방식에 따라 `IsAttackTarget()` 함수를 활용해 피격 대상을 선정할 수 있습니다. `IsAttackTarget()` 함수는 공격자가 공격 박스에 포함되지 않도록 처리되어 있으므로, 공격자가 타격을 받지 않게 처리할 필요가 없습니다. Entity 타입의 `defender` 매개변수는 충돌한 엔티티가 공격 대상인지 판단할 수 있는 엔티티 정보를 받습니다. 함수의 리턴값이 false면 AttackComponent에서 적합한 공격 대상으로 판별하고 이후 판단을 HitComponent에 넘깁니다. 만약 공격자도 공격 대상에 포함시키고 싶다면, 아래와 같이 리턴값을 true로 변경합니다.
    
    
    override boolean IsAttackTarget(Entity defender)
    {
        return true
    }
    

공격과 피격은 다음과 같이 실행됩니다. 먼저 공격한 엔티티의 `AttackComponent`의 `IsAttackTarget()` 함수에서 공격에 실패했을 경우 false, 성공했을 경우 true를 반환합니다. false는 공격 실패로 이후 절차를 중단합니다. 무조건 타격을 주고 싶은 경우 true로 반환하면 됩니다. 이 경우 피격자의 `IsHitTarget()`은 호출되지 않고 무조건 대미지를 줍니다.

피격자의 `HitComponent`의 `IsHitTarget()`이 피격 여부를 판단합니다. `HitComponent` 에서 공격 받은 대미지와 피격자의 회피율 등을 계산해 최종 대미지를 계산합니다. 피격자의 `IsHitTarget()`이 true를 반환하면 대미지를 피격자에게 적용합니다. [png](16621062233862d80a40f725a44b2bd383f597906dca9)

#### 대미지 계산

대미지 값은 공격자의 AttackComponent의 `CalcDamage()` 함수를 이용합니다. 이 함수로 공격 대미지를 반환합니다. 피격자의 HitComponent의 `OnHit()` 함수는 `CalcDamage()`의 값을 매개 변수로 전달받을 수 있습니다. 전달받은 값은 그대로 사용할 수도 있고, `OnHit()` 함수에서 추가 계산을 거쳐 최종 대미지 값을 계산할 수도 있습니다. 예를 들어 전달된 공격자의 대미지와 방어자의 회피율 값을 계산해 `OnHit()` 함수가 최종 대미지를 리턴 값으로 냅니다.

[png](1662106244457dbfb15dbce134b3c8acc56b4dd23dfc8)

#### HitEvent

`OnHit()` 함수는 호출될 때 HitEvent를 발생시킵니다. 만약 몬스터로부터 100번 공격 당했을 때의 특별한 처리를 하고 싶다면, 이벤트 발생 횟수를 확인해 구현할 수 있습니다. [png](166210626375732ffa55d90e0452ab96703bf0137e84a)

# 예제

`Ctrl`키를 눌러 몬스터를 공격하고, 몬스터는 공격 당할 때마다 일정 비율로 크기가 커질 수 있도록 만들어봅시다.

#### 플레이어 공격 구현

  1. Workspace - BaseEnvironment - NativeScripts - Component - AttackComponent를 선택합니다. [png](1687765469788eef28e43cf6e478aadce01fbf58c566b)

  2. 콘텍스트 메뉴에서 Extend를 눌러 컴포넌트를 확장합니다. 확장한 컴포넌트의 이름을 CharacterAttack으로 변경합니다.

  3. CharacterAttack 컴포넌트를 더블 클릭하여 스크립트 에디터를 엽니다.

  4. Method에서 새로운 함수를 추가하고, 이름을 NormalAttack 으로 변경합니다.

  5. `Attack` 함수에서 사용할 공격 범위를 정의하고, 매개변수로 전달받을 수 있도록 아래와 같이 작성합니다.
         
         @Component
         script NormalAttack extends Component
         method void NormalAttack()
         local attackSize = Vector2(1, 1)
         local attackOffset = Vector2(0,0)
         
         self:Attack(attackSize, attackOffset, nil)
         end
         end
         



    
    
    6. 플레이어의 행동 변화를 받을 수 있도록 **Event Handler**에 **PlayerActionEvent**를 추가하고 다음과 같이 작성합니다. 플레이어가 `ctrl` 키를 누를 때마다 `NormalAttack()` 함수를 호출합니다.
    
    	```lua
    @Component
    script PlayerAttackHandler extends Component
        @EventSender("Self")
        handler HandlePlayerActionEvent(PlayerActionEvent event)
        --------------- Native Event Sender Info ----------------
        -- Sender: PlayerControllerComponent
        -- Space: Server, Client
        ---------------------------------------------------------
    
        -- Parameters
        local ActionName = event.ActionName
        -- local PlayerEntity = event.PlayerEntity
        ---------------------------------------------------------
    
        if ActionName == "Attack" then
        	self:NormalAttack()
        end
        end
    end
    
    
    
    > **Tip**
    > Parameters의 `local ActionName = event.ActionName` 주석을 해제해 활용할 수 있습니다.
    

  7. DefaultPlayer에 CharacterAttack 컴포넌트를 추가합니다.
  8. 맵에 몬스터를 배치하고, [시작]을 눌러 몬스터를 공격할 수 있는지 확인합니다.



# 공격 대미지 계산

`AttackComponent`의 `CalcDamage()` 함수를 활용해 공격 받은 대미지 값을 계산하고 출력할 수 있습니다. 이 함수는 충돌이 발생했을 때 대미지를 반환합니다.

  1. CharacterAttack컴포넌트에 `CalcDamage()` 함수를 추가합니다. [png](1635473274032dd346b758c4743be834dcc36af950302)

  2. 공격 대미지 값을 아래와 같이 100으로 작성합니다.
         
         @Component
         script FixedDamageCalculator extends Component
         override method integer CalcDamage(Entity attacker, Entity defender, string attackInfo)
         return 100
         end
         end
         



    
    
    3. [시작]을 누르고 몬스터를 공격하면 **100**의 대미지가 발생하는지 확인합니다.
    
    # 몬스터 피격 구현
    
     **AttackComponent**를 가진 엔티티가 **HitComponent**를 가진 엔티티를 공격하면, 별다른 추가 처리를 하지 않아도 엔티티는 피격 당하게 됩니다. 이 경우 추가 처리가 없기 때문에 기본값인 1로 대미지 스킨이 재생됩니다.
    피격을 구현할 때는 공격량 그대로 대미지로 받을 수도 있지만, 공격과 다른 공식을 함께 사용해 피격량을 계산할 수도 있습니다. 예를 들어 100의 공격력으로 공격했지만 몬스터의 방어력이 높은 경우 절반의 공격만 유효하게 처리할 수 있습니다.
    이번에는 아래 두 가지를 구현해 피격 당하는지를 확인하고, 최종 피격량을 계산해봅시다.
    
    * 공격받은 대미지를 20으로 변경합니다.
    * 공격받을 때마다 사이즈가 커집니다.
    
    1. **Workspace - BaseEnvironment - NativeScripts - HitComponent**를 선택하고 **콘텍스트 메뉴- Extend**를 선택해 컴포넌트를 확장합니다.
    2. 이름을 **MonsterHit**으로 변경합니다.
    [png](16353330342954504d937277b4fada89371a5e3da7faa)
    
    3. **Workspace - BaseEnvironment - NativeModel**에서 **MoveMonster** 모델을 선택합니다.
    [png](1635473322027345d4d4d82df42d283fe2d7c9ef91747)
    
    4. 프로퍼티 에디터에서 **HitComponent**의 **콘텍스트 메뉴 - [png](163453706197553d527cb3ea34392bc2829f15976f3d8) - Remove Component**를 눌러 기본 **HitComponent**를 모델에서 삭제합니다. 기본 HitComponent와 확장한 HitComponent를 함께 사용하면 오류가 발생할 수 있기 때문입니다.
        [png](16353328810860b81ed55939d474aaaea5cd238480c92)
    
        > **Tip**
        > 이미 맵에 배치한 몬스터는 모델 프로퍼티 변경 사항이 적용되지 않으므로 삭제 후, 재배치해야 합니다.
    
    5. 배치한 몬스터에 **MonsterHit** 컴포넌트를 추가합니다.
    6. **MonsterHit**컴포넌트에  `OnHit()` 함수를 추가합니다.
        [png](163547333521453f7d5d7ff6a4e20bd2f5c20a78ea254)
    7. 함수 내 기본 문구 중 **damage** 값을 **20**으로 변경합니다. 공격 받는 대미지 값을 결정합니다.
    
    	```lua
    @Component
    script OnHitDamageFixer extends Component
        override method void OnHit(Entity attacker, integer damage, boolean isCritical, string attackInfo, integer hitCount)
        __base:OnHit(attacker,20,isCritical,attackInfo,hitCount)
        end
    end
    

  8. 피격 당한 몬스터의 사이즈가 커지도록 `OnHit()` 함수에 아래 내용을 추가합니다. HitEvent가 발생할 때마다 TransformComponent.Scale 값을 1.3배씩 증가시킵니다.
         
         @Component
         script SizeIncreaseOnHit extends Component
         override method void OnHit(Entity attacker, integer damage, boolean isCritical, string attackInfo, integer hitCount)
         __base:OnHit(attacker,20,isCritical,attackInfo,hitCount)
         
         local scale = self.Entity.TransformComponent.Scale
         scale = scale * 1.3
         self.Entity.TransformComponent.Scale = scale
         end
         end
         



    
    
    9. [시작]을 눌러, 공격 때마다 공격당한 몬스터의 크기가 커지면서 대미지 값이 **20**으로 출력되는지 확인합니다.
    [png](1662107300698572fbabb48b54d5d92f06e1ae6487794)