본문 바로가기
게임 개발/유니티 게임 개발 일지

#10 적 AI 구현하기

by FlowTree 2020. 3. 15.
반응형

1.구현 목적

적 AI를 왜 구현해야 할까?(적 AI 기획의도)

= 게임 플레이가 재미있었으면...!

>장르가 도대체 뭐냐? 슈팅? 생존? 아케이드? = 아케이드

>단순 조작으로 재미가 있어야 하니까

>전투가 재미있었으면!

>적을 쉽게 잡는 재미, 적의 공격을 피하는 재미, 적절한 긴장감을 주는 적 AI 필요

 

구현할 적 AI 타입 정리

  • 적을 쉽게 잡는 재미 제공 = 단순한 공격을 하고 잡기 쉬운 AI = 근접형 AI
  • 적의 공격을 피하는 재미 제공 = 피할 수 있는 원거리 공격을 하는 AI = 원거리형 AI
  • 적절한 긴장감 제공 = 강한 데미지를 가하고 예상치 못한 공격을 가하는 AI = 순간이동형 AI

 

#여담-추가적으로 생각할 요소, 나중에 마나 시스템 추가하기

마나 파밍 방법에 따라서 AI 난이도와 비율을 조절하기 

1.적 사망 후 마나 드롭만으로 마나 파밍을 할지

2.마나 파밍 + 일정 시간이 지나면 조금씩 차오르게 할지

3.마나 파밍 + 일정 시간 마다 플레이어 주변에 마나가 드롭되게 할지

 

고민 중...

 

우선 적 3가지 타입을 구현 후 마나 시스템을 추가하고 테스트하며 수정하기로 결정
(고민만 하는 것보다 시간 절약 & 직접 테스트하며 원하는 기획의도를 확인해보는 게 낫다)

 

마나 수급량에 대한 고민

적을 너무 쉽게 잡으면 마나 파밍이 쉬워지면서 스킬 난사가 가능해진다.

그러면 플레이가 너무 쉬워지고 지루해진다.

 

스킬 난사를 방지하기 위해서 적 AI 난이도에 따라서 드롭하는 마나량을 구분했다.

적 AI를 쉬움, 보통, 어려움으로 구분했고, 잡기 쉬울 수록 소량의 마나를 드롭한다.

 

적의 난이도와 스폰 비율에 따라서 플레이 난이도와 마나 수급량이 정해지므로

일단 약간 느린 템포로 스킬을 사용하도록 스폰 비율은 쉬움 7 : 보통 3 : 어려움 1로 정했다.

 

이러한 적 AI와 마나 수급량으로 플레이 시 적절한 전투의 재미와 긴장감, 스킬 사용의 재미 밸런스를 조절할 것이다.

 

2.적 AI 간단 소개

사용한 적 리소스

  • 비주얼 통일성: 적들의 외형과 분위기가 각각 다르면 이질감을 줄 것 같았다. 다른 타입의 적들이 같은 공간에서 등장해도 자연스럽게 느껴지도록 하나의 에셋에서(동일한 그래픽 분위기) 적 리소스를 선별했다.
  • SYNTY STUDIOS의 POLYGON - Dungeons Pack을 구매해서 이용했다.

 

근접형 AI

 

스켈레톤

외형

한손검을 장비한 스켈레톤

체력 적음
데미지 보통
공격 속도 빠름
마나 드롭량 소량
스폰 비율(10) 다수(7)
전투 난이도 쉬움
전투 스타일

1.플레이어를 추적하며 공격 사거리안에서 근접 공격을 한다.

2.공격 속도가 빨라서 한 번 붙으면 연속 공격을 받기 때문에 플레이어에게 일정 거리를 유지하게 한다.

설명

1.주 마나 파밍 요소: 거리만 유지한다면 쉽게 잡을 수 있어서 주 마나 파밍용 적이다.

2.노력형 요소: 적을 많이 잡아서 소량의 마나를 계속 저장하여 스킬을 사용하는 노력형 요소이다.

 

 

원거리형 AI

 

고블린 샤먼

외형

1.원시 주술을 사용하기 때문에 해골 지팡이를 장비함

2.원시 주술사 느낌으로 가면과 가죽 갑옷을 입은 고블린

원래는 맵 컨셉을 봉인된 던전으로 하여 스켈레톤 타입 적으로 통일성을 주려고 했는데 관련 리소스를 구하기 힘들어서(특히 애니메이션) 고블린 샤먼이 스켈레톤과 고스트를 조종한다는 설정으로 변경했다.

체력 보통
데미지 높음
공격 속도 보통
마나 드롭량 중간량
스폰 비율(10) 소수(2)
전투 난이도 보통
전투 스타일

공격 사거리 안에서 멈춰서 직선형 원거리 마법을 플레이어를 향해 발사한다.

설명

1.원거리 마법을 통해 플레이어의 이동과 공격을 견제한다.

2.원거리 마법이 플레이를 까다롭게 하기 때문에 플레이어에게 회피 또는 원거리 적 제거의 플레이 우선순위를 선택하게 한다.(플레이 우선순위 판단 요소)

3.회피(까다로운 공격 감수, 회피하는 재미) vs 원거리 적 제거(마나 보상 & 원활한 플레이)

4.적 제거를 한다면 괜찮은 마나 드롭량을 통해 보다 빠르게 스킬을 사용할 수 있게 한다. 

 

 

순간이동형 AI

 

고스트

외형 전체가 반투명한 맨손의 유령 병사

초기 컨셉으로 점프 도약 공격(눈으로 식별할 수 있는 공격을 통해 플레이어가 대응 & 회피할 수 있게)을 하는 스켈레톤으로 기획했으나 해당 그래픽 리소스와 애니메이션을 찾을 수 없어서 고민했으나 유령의 경우 순간이동, 투과하는 특징이 있어서 유령 타입으로 변경했다. 이 후 점프 도약과 비슷한 기능을 하는 순간이동 기능을 추가했다. 아쉽겠도 순간이동의 경우 눈으로 식별하기 어려운 단점이 있다.

순간이동을 이용하기 전에 준비 동작?준비 효과?를 (몸이 안개로 변하는?) 추가해서 플레이어가 식별해서 대응할 수 있게 하고 싶다.(미래의 일...)

체력 낮음
데미지 매우 높음
공격 속도 느림
마나 드롭량 대량
스폰 비율(10) 극소수(1)
전투 난이도 어려움
전투 스타일

1.느린 이동 속도로 적에게 접근하여 강한 공격을 한다.

2.일정 시간 마다 플레이어 근처로 순간이동을 할 수 있어 예상치 못한 공격을 한다.(긴장감 유발 & 회피 강요)

3.플레이어에게 적과 일정 거리를 유지하며 제거하는 플레이를 유도한다.

설명

1.느린 이동속도로 플레이어를 방심하게 하나 순식간에 접근하여 공격할 수 있기 때문에 주시해야할 적이다.

2.하이리스크 하이리턴: 예상치 못한 공격과 매우 높은 데미지를 감수하고 적을 제거한다면 대량의 마나를 쉽게 파밍할 수 있어서 이 후 스킬 사용을 통해 더 많은 이익을 얻을 수 있다.

 

 

3.구현된 영상

https://www.youtube.com/watch?v=PaFZavA5bgg

 

4.구현할 기능

LivingEntity 클래스

적과 플레이어는 모두 비슷한 기능을 가진다. 체력이 있다. 피격받는다. 사망한다. 이런 공통 기능들은 모아서 부모 클래스인 LivingEntity에 먼저 구현했다. 유니티 게임 프로그래밍 에센스책의 내용을 이용했다. 

 

필요한 데이터

  • 기본 체력: 해당 타입의 기본 체력
  • 기본 마나: 해당 타입의 기본 마나
  • 현재 체력: 해당 타입의 게임 플레이 시 현재 체력
  • 현재 마나: 해당 타입의 게임 플레이 시 현재 마나
  • 현재 사망 상태(bool): 해당 타입의 사망 상태를 결정하는 데이터

필요한 기능

  • 리셋 기능: 해당 타입이 활성화될 때 상태를 리셋, 사망하지 않은 상태로 시작, 시작 시 현재 체력을 기본 체력으로 설정
  • 피격 기능: 피격 시 효과와 데미지만큼 현재 체력을 감소하는 기능(데미지 계산식 X, 단순 계산) 
  • 사망 기능: 현재 체력이 0이하 시 해당 타입을 사망 처리, 제거하는 기능

 

근접형 AI

근접형 AI는 플레이어를 추적하고 AI는 공격 사거리까지 플레이어에게 접근하여 공격한다. AI에 필요한 기본적인 기능들은 근접형 AI에서 구현했고 다른 타입을 구현할 때 이용했다.

 

#여담-클래스를 구현 못해서...

기본 타입을 부모 클래스로 해서 파생 타입을 자식 클래스로 구현하고 싶었지만 C#을 잘 모르기 때문에 기본 타입 스크립트를 복제해서 일일이 파생 타입을 구현했다. 

 

구현하고 싶은 것들은 유튜브나 인터넷 검색으로 어느정도 따라해서 구현할 수 있지만 관련 자료가 없으면 구현이 어렵다. 정말 내 머릿 속 기획을 구현하고 싶다면 C#의 기본을 알아야함을 느낀다.

 

그래서 이번에 "이것이 C# 이다" 책을 구매했다. 진득하게 처음부터 공부하고싶지만 내 계획에 맞춰서 게임을 개발하고 포트폴리오를 준비하려면 불가능하다. 그래서 모르는 내용이 있을때 C# 책을 사전처럼 사용하기로 했다.

 

시중의 유니티 게임 개발 책은 유니티 게임 개발 입문으로 프로그래밍에 대한 진입 장벽을 낮춰줄 뿐이지, 그것만으로 부족하다. 누군가 개발한 것을 따라해서 만드는 것은 한계가 분명있다.

 

내 경험과 생각, 기획의도를 게임과 특정 기능으로 구현하고 싶다면 프로그래밍 지식(나는 C#)은 정말 필수인 것 같다. 만드는 방법을 알아야지 스스로 게임 개발이 가능하다.

 

근데, 유니티 게임 프로그래밍 에센스책은 설명이 정말 잘 되어있다. 잘 읽어보면 입문 실력한테 정말 큰 도움이 된다.(탈룰라 무엇?)

 

필요한 데이터

  • 추적 대상: 적이 플레이어를 추적 시 필요한 추적 대상
  • 기본 체력: 적의 기본 체력
  • 추적 대상과의 거리: 실시간으로 추적 대상과 적의 거리를 계산하여 추적 대상이 공격 사거리 안에 있는지 비교하기 위한 데이터
  • 공격력: 적의 공격력
  • 공격 사거리: 적이 공격할 수 있는 공격 사거리
  • 공격 속도(공격 딜레이): 적의 공격 속도
  • 마지막 공격 시점: 마지막 공격 시점을 갱신 및 검사하여 공격 딜레이를 적용
  • 이동 속도: 적의 이동 속도
  • 마나 드롭량: 적이 사망 시 드롭할 마나의 량(미구현)
  • 점수: 적이 사망 시 게임매니저에 제공할 점수(미구현)

필요한 기능

  • 탐지 기능: 플레이어를 탐지하고 추적 대상으로 설정하는 기능
  • 추적 기능: 추적 대상과의 거리를 계산하고 추적하는 기능
  • 공격 기능: 추적 대상과의 거리와 적의 공격 사거리를 검사하고 공격 + 데미지를 입히는 기능
  • 사망 기능: 적의 체력이 0 이하가 되면 적을 사망 처리하고 제거하는 기능(미구현)
  • 마나 드롭 기능(미구현)
  • 점수 업데이트 기능(미구현)

 

원거리형 AI

원거리형 AI는 공격 사거리 안에 플레이어가 있으면 멈춰서 원거리 마법을 발사한다. 근접형 AI에서 공격 기능을 수정하고 원거리 마법 발사 기능을 추가하면 된다. 근접형 AI를 기본으로 만들기 때문에 중복되는 정보는 제외했다.

 

필요한 데이터

  • 기본 체력
  • 공격 사거리
  • 데미지
  • 공격 속도(공격 딜레이)
  • 이동 속도
  • 마나 드롭량(미구현)
  • 점수(미구현)

필요한 기능

  • 공격 기능(마법구 발사)
  • 마법구 이동 기능

 

순간이동형 AI

순간이동형 AI는 느리게 걸으며 근접 공격을 한다. 순간이동은 일정 시간마다 이용할 수 있고 한 번씩 사용할 수 있다. 쿨타임이 지나면 다시 이용할 수 있다. AI의 순간이동 사거리 안에 플레이어가 있으면 AI는 순간이동을 할 수 있다. AI가 순간이동 시 플레이어 캐릭터 주변의 랜덤한 위치로 순간이동을 한다. 

 

필요한 데이터

  • 기본 체력
  • 데미지
  • 공격 사거리
  • 공격 속도(공격 딜레이)
  • 순간이동 거리
  • 순간이동 쿨타임
  • 순간이동 횟수
  • 마나 드롭량(미구현)
  • 점수(미구현)

필요한 기능

  • 순간이동 기능(쿨타임, 횟수 카운트, 순간이동 위치 계산, 순간 이동)

 

5.구현 과정

5.1.LivingEntity 스크립트

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;

//생명체로 동작할 게임 오브젝트들의 뼈대를 제공
//체력, 피해받음, 사망 기능, 사망 이벤트 제공

public class LivingEntity : MonoBehaviour//, IDamageable 적용 예정
{
    public float startingHealth = 100f; //시작 체력
    public float startingMana = 0f; //시작 마나
    public float health { get; protected set;} //현재 체력
    public float mana { get; protected set; } //현재 마나
    public bool dead { get; protected set;} //사망 상태

    public event Action onDeath; //사망 시 발동할 이벤트

    //생명체가 활성화될 떄 상태를 리셋
    protected virtual void OnEnable()
    {
        //사망하지 않은 상태로 시작
        dead = false;
        //체력을 시작 체력으로 초기화
        health = startingHealth;
        mana = startingMana;

    }


    //피해를 받는 기능
    public virtual void OnDamage(float damage)
    {
        //데미지만큼 체력 감소
        health -= damage; // health = health - damage;

        //체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
        if (health <= 0 && !dead)
        {
            Die();
        }
    }

    //체력을 회복 하는 기능은 책에 있는데 나는 플레이어 스텟 스크립트에서 적용할 것임

    //사망 처리
    public virtual void Die()
    {
        //onDeath 이벤트에 등록된 메서드가 있다면 실행
        if (onDeath != null)
        {
            onDeath();
        }

        dead = true;
    }

}

 

5.2.근접형 AI

5.2.1.오브젝트 준비하기

1.하이어라키 창에서 빈 게임 오브젝트를 생성 > Enemys로 이름을 변경

2.Enemys는 모든 AI 오브젝트를 모아둘 게임 오브젝트

3.Enemys에서 빈 게임 오브젝트를 생성하고 Enemy_Skeleton로 이름 변경

4.Enemy_Skeleton는 적 모델링과 타겟팅UI을 담는다.

5.Enemy_Skeleton 오브젝트에 프로젝트에 있는 적 모델링(Character_Skeleton_Solider_02)을 추가

6.스켈레톤이 검을 장비하도록 적 모델링 > 오른손 오브젝트 > 사용할 무기 오브젝트를 넣어준다.

(검 위치 미세조정은 애니메이션 설명에서)

7.플레이어에게 적이 타겟팅됐을 경우 타겟팅UI를 보여주기 위해서 이전에 사용한 타겟팅UI(Canvas)를 Enemy_Skeleton 오브젝트에 넣어준다.

8.타겟팅UI는 적 모델링 정중앙 바닥에 위치하도록 위치를 조정한다.

9.타겟팅UI의 인스펙터를 체크해제하고 FieldOfView 스크립트에서 컨트롤한다.(FieldOfView 글 참고)

 

5.2.2.컴포넌트 추가하기

1.AI의 움직임을 제어할 Animator 컴포넌트 추가하기

2.애니메이터 컴포넌트의 Apply Root Motion 체크해제하기

3.AI의 충돌을 감지할 Capsule Collider 컴포넌트 추가하기

4.캡슐 콜라이더의 크기를 AI의 크기게 맞게 변경한다. Center Y는 1, Height는 2로 변경

5.AI가 플레이어를 탐지하고 추적할 수 있게 해주는 Nav Mesh Agent 컴포턴트를 추가하기

 

5.2.3.애니메이션 설정하기

1.프로젝트 창 > 에셋 > 마우스 오른쪽 클릭 > Create > Animator Controller 클릭하여 애니메이터를 생성한다.

 

2.애니메이터의 이름을 Enemy Skeleton으로 변경하여 구분하기 쉽게한다.

 

3.애니메이터를 더블 클릭해서 애니메이터 창을 연다.

 

4.AI의 Idle, Move, Attack1 상태를 구현한다.

 

5.Base Layer에서 마우스 오른쪽 클릭 > Create State > Empty를 클릭하여 빈 상태를 생성한다.

 

6.상태 설명

  • Idle: 대기하고 있는 상태, 대기하고 있는 애니메이션을 실행
  • Move: 이동하는 상태, 이동 애니메이션을 실행
  • Attack1: 공격하는 상태, 공격 애니메이션을 실행

7.상태들을 만들고 상태별 전환을 위해 Bool 타입 Parameters들을 추가한다.

  • bool타입 파라미터 CanMove: Move 상태 전환 스위치
  • bool타입 파라미터 CanAttack: Attack 상태 전환 스위치

8.스위치 값에 상태가 전환될 수 있게 Make Transition생성하여 상태들을 연결한다.

 

9.생성된 Transition을 선택하고 Conditions에서 사용할 Parameters와 Parameters의 값을 설정한다.

 

10.상태 변환 조건

상태 전환 파라미터 값
Idle > Move CanMove true
Move > Idle false
Idle > Attack1 CanAttack true
Attack1 > Idle false

 

11.모든 Transitions들은 연결 후 Has Exit Time을 해제한다. Has Exit Time을 체크하면 실시간으로 애니메이션 변경이 안된다. 애니메이션이 완전히 실행되고 상태가 전환되어 부자연스러워서 체크해제한다.

 

#유니티 트랜지션

https://docs.unity3d.com/kr/530/Manual/class-Transition.html

 

 

12.Enemy Skeleton 애니메이터를 완성 후 Enemy Skeleton 게임 오브젝트의 애니메이터 컴포넌트에 할당한다.

 

13.플레이를 눌러서 애니메이션이 잘 작동하는 지 확인한다.

 

 

14.적에게 장비 시켜준 무기가 이상한 위치와 방향이라면 조절한다.

 

15.Play 버튼 옆에 있는 Pause 누르고 무기를 선택하고 E를 눌러서 Rotate Tool을 선택한다.

 

 

16. 무기의 위치와 방향을 자연스럽게 변경한다.

 

 

17.현재의 위치 정보를 복사한다. 인스펙터 창 > Transform 컴포넌트 > 톱니바퀴 > Copy Component

 

 

18. 플레이 정지 후 무기를 선택하고 Paste Component Values를 클릭하면 쉽게 무기 위치와 방향을 변경할 수 있다.

 

 

5.2.4.근접형 AI 플로우 차트

이 게임은 아케이드 게임처럼 단순한 조작으로 전투를 하기 때문에 AI 또한 단순하게 구현했다. 물론 짜임새 있는 AI는 전투를 즐겁게 해주지만 내가 그걸 구현할 실력도 안되고 시간도 부족해서 적 AI 한테 꼭 필요한 기능만 구현했다.

 

AI가 단순하다는 단점은 새로운 패턴/기능이 있는 파생형 타입 AI를 추가해서 보완했다. 만약에 게임을 지속적으로 업데이트를 하게 된다면 새로운 패턴/기능이 있는 파생형 AI들을 계속 추가할 것 같다.

 

근접형 AI는 탐지 > 추적 > 공격의 단계로 행동한다. AI의 행동이 끊김없이 자연스럽게 행동하길 원해서 코루틴으로 0.25초 간격으로 AI를 실행했다.

 

1.탐지

  • AI는 AI를 중심으로 탐지 범위가 있으며, 탐지 범위 안에서 살아 있는 플레이어를 탐지할 수 있다.
  • 플레이어를 탐지했을 경우 해당 플레이어를 추적 대상으로 지정한다.
  • 탐지 범위 안에 플레이어가 없을 경우 제자리에 정지하고 계속해서 탐지 범위를 체크한다.

2.추적

  • 추적 대상이 있으면 AI의 공격 사거리까지 대상을 추적한다.

3.공격

  • AI가 공격 사거리까지 추적 대상을 추적했을 경우 이동을 정지한다.
  • AI는 추적 대상 방향으로 바라본다. (공격 애니메이션을 실행 시 공격 방향을 맞추기 위해서)
  • 공격을 하기 전 공격 딜레이를 체크해서 공격이 가능한지 판단한다.
  • 공격을 할 수 있으면 공격 애니메이션을 실행한다.
  • 공격 애니메이션의 특정 프레임에서(유니티 애니메이션 이벤트 이용) 플레이어에게 데미지를 적용한다.

5.2.5.기능 구현

탐지 기능

1.읽기 전용 프로퍼티로 추적 대상 존재 유무를 구현한다.

2.탐지 기능은 Coroutine로 0.25초마다 실행한다. 최적화 + 끊김없는 행동을 위해서

3.while문을 이용해서 단순하고 특정 조건일 때만 탐지 기능을 실행한다.

 

추적 기능

1.Enemy_Skeleton 오브젝트에 Nav Mesh Agent 컴포넌트를 추가하기

2.Enemy 스크립트에서 Nav Mesh Agent를 제어하기

1.경로 계산 AI 에이전트 변수 선언
//AI 에이전트
private NavMeshAgent pathFinder;
//추적대상
private LivingEntity targetEntity;

2.AI 에이전트 정지 제어
pathFinder.isStopped = true/false;

3.AI 목적지 정하기
pathFinder.SetDestination(추적대상.transform.position);
pathFinder.SetDestination(targetEntity.transform.position);

 

공격 기능

1.추적 대상이 공격 사거리 안에 있으면 공격 메서드 실행하기

2.추적 대상이 공격 사거리 밖에 있으면 계속 추적하기

3.추적 대상 방향으로 바라보기 (transform.LookAt)

4.공격 딜레이: 최근 공격 시점+공격 딜레이 만큼 지나면 공격 가능

    //추적 대상과의 거리에 따라 공격 실행
    public virtual void Attack()
    {

        //자신이 사망X, 추적 대상과의 거리이 공격 사거리 안에 있다면
        if (!dead && dist < attackRange)
        {
            //공격 반경 안에 있으면 움직임을 멈춘다.
            canMove = false;

            //추적 대상 바라보기
            this.transform.LookAt(targetEntity.transform);

            //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능
            if (lastAttackTime + attackDelay <= Time.time)
            {
                canAttack = true;
            }

            //공격 반경 안에 있지만, 딜레이가 남아있을 경우
            else
            {
                canAttack = false;
            }
        }

        //공격 반경 밖에 있을 경우 추적하기
        else
        {
            canMove = true;
            canAttack = false;
            //계속 추적
            pathFinder.isStopped = false; //계속 이동
            pathFinder.SetDestination(targetEntity.transform.position);
        }
    }

 

공격 기능 - 데미지 적용하기 기능

대상을 공격하는 순간에 데미지를 적용하기 위해서 유니티 애니메이션 이벤트에서 사용할 메서드를 작성했다.

공격 애니메이션이 특정 프레임에서 데미지 적용 메서드가 실행되어 자연스럽게 데미지 적용을 할 수 있다.

 

    //유니티 애니메이션 이벤트로 공격하는 모션일 때 데미지 적용시키기
    public void OnDamageEvent()
    {
        //공격 대상을 지정할 추적 대상의 LivingEntity 컴포넌트 가져오기
        LivingEntity attackTarget = targetEntity.GetComponent<LivingEntity>();

        //공격 처리
        attackTarget.OnDamage(damage);

        //최근 공격 시간 갱신
        lastAttackTime = Time.time;
    }

 

5.2.6.스크립트

Enemy

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI; //AI, 네비게이션 시스템 관련 코드 가져오기

public class Enemy : LivingEntity
{
    public LayerMask whatIsTarget; //추적대상 레이어

    private LivingEntity targetEntity;//추적대상
    private NavMeshAgent pathFinder; //경로 계산 AI 에이전트

    /*public ParticleSystem hitEffect; //피격 이펙트
    public AudioClip deathSound;//사망 사운드
    public AudioClip hitSound; //피격 사운드
    */

    private Animator enemyAnimator;
    //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트

    public float damage = 20f; //공격력
    public float attackDelay = 1f; //공격 딜레이
    private float lastAttackTime; //마지막 공격 시점
    private float dist; //추적대상과의 거리

    public Transform tr;

    private float attackRange = 2.3f;

    //추적 대상이 존재하는지 알려주는 프로퍼티
    private bool hasTarget
    {
        get
        {
            //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
            if (targetEntity != null && !targetEntity.dead)
            {
                return true;
            }

            //그렇지 않다면 false
            return false;
        }
    }

    private bool canMove;
    private bool canAttack;

    private void Awake()
    {
        //게임 오브젝트에서 사용할 컴포넌트 가져오기
        pathFinder = GetComponent<NavMeshAgent>();
        enemyAnimator = GetComponent<Animator>();
        //enemyAudioPlayer = GetComponent<AudioSource>();
    }

    //적 AI의 초기 스펙을 결정하는 셋업 메서드
    public void Setup(float newHealth, float newDamage, float newSpeed)
    {
        //체력 설정
        startingHealth = newHealth;
        health = newHealth;
        //공격력 설정
        damage = newDamage;
        //네비메쉬 에이전트의 이동 속도 설정
        pathFinder.speed = newSpeed;
    }


    void Start()
    {
        //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작
        StartCoroutine(UpdatePath());
        tr = GetComponent<Transform>();
        
    }

    // Update is called once per frame
    void Update()
    {
        enemyAnimator.SetBool("CanMove", canMove);
        enemyAnimator.SetBool("CanAttack", canAttack);

        if (hasTarget)
        {
            //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update()
            dist = Vector3.Distance(tr.position, targetEntity.transform.position);
        }
    }

    //추적할 대상의 위치를 주기적으로 찾아 경로 갱신
    private IEnumerator UpdatePath()
    {
        //살아 있는 동안 무한 루프
        while(!dead)
        {
            if (hasTarget)
            {
                Attack();
            }
            else
            {
                //추적 대상이 없을 경우, AI 이동 정지
                pathFinder.isStopped = true;
                canAttack = false;
                canMove = false;

                //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기
                Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

                //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기
                for (int i = 0; i < colliders.Length; i++)
                {
                    //콜라이더로부터 LivingEntity 컴포넌트 가져오기
                    LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                    //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면
                    if (livingEntity != null && !livingEntity.dead)
                    {
                        //추적 대상을 해당 LivingEntity로 설정
                        targetEntity = livingEntity;

                        //for문 루프 즉시 정지
                        break;
                    }
                }
            }

            //0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }

    //추적 대상과의 거리에 따라 공격 실행
    public virtual void Attack()
    {

        //자신이 사망X, 추적 대상과의 거리이 공격 사거리 안에 있다면
        if (!dead && dist < attackRange)
        {
            //공격 반경 안에 있으면 움직임을 멈춘다.
            canMove = false;

            //추적 대상 바라보기
            this.transform.LookAt(targetEntity.transform);

            //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능
            if (lastAttackTime + attackDelay <= Time.time)
            {
                canAttack = true;
            }

            //공격 반경 안에 있지만, 딜레이가 남아있을 경우
            else
            {
                canAttack = false;
            }
        }

        //공격 반경 밖에 있을 경우 추적하기
        else
        {
            canMove = true;
            canAttack = false;
            //계속 추적
            pathFinder.isStopped = false; //계속 이동
            pathFinder.SetDestination(targetEntity.transform.position);
        }
    }

    //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기
    public void OnDamageEvent()
    {
        //공격 대상을 지정할 추적 대상의 LivingEntity 컴포넌트 가져오기
        LivingEntity attackTarget = targetEntity.GetComponent<LivingEntity>();

        //공격 처리
        attackTarget.OnDamage(damage);

        //최근 공격 시간 갱신
        lastAttackTime = Time.time;
    }


    //데미지를 입었을 때 실행할 처리
    public override void OnDamage(float damage)
    {
        /*사망하지 않을 상태에서만 피격 효과 재생
        if (!dead)
        {
            //공격 받은 지점과 방향으로 피격 효과 재생
            hitEffect.transform.position = hitPoint;
            hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal);
            hitEffect.Play();

            //피격 효과음 재생
            enemyAudioPlayer.PlayOnShot(hitSound);
        }
        */

        //피격 애니메이션 재생
        enemyAnimator.SetTrigger("Hit");


        //LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage);
    }

    //사망 처리
    public override void Die()
    {
        //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행
        base.Die();

        //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화
        Collider[] enemyColliders = GetComponents<Collider>();
        for (int i = 0; i < enemyColliders.Length; i++)
        {
            enemyColliders[i].enabled = false;
        }

        //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화
        pathFinder.isStopped = true;
        pathFinder.enabled = false;

        //사망 애니메이션 재생
        enemyAnimator.SetTrigger("Die");
        /*//사망 효과음 재생
        enemyAudioPlayer.PlayOnShot(deathSound);
        */
    }
}

 

5.3.원거리형 AI

기본적인 AI 플로우는 근접형 AI를 따르며, 근접 공격에서 원거리 공격으로 변경하여 구현했다. 이 원거리 공격 기능(마법구)은 플레이어 캐릭터의 매직 미사일 기능과 똑같아서 그대로 구현했다. 오브젝트, 컴포넌트 준비 과정은 근접형 AI 준비과정과 비슷하니 생략한다.

5.3.1.원거리 공격 기능 구현

AI의 마법구 생성 기능

1.공격 대상 방향을 바라보고 공격하기

2.공격할 때 유니티 애니메이션 이벤트로 마법구 생성 메서드 실행

    public void ShamanFire()
    {
        magicMissile = Instantiate(magicMissilePrefab, firePoint.transform.position, firePoint.transform.rotation); //Instatiate()로 매직 미사일 프리팹을 복제 생성한다.
    }

 

AI의 마법구 이동 기능

1.생성된 마법구는 전방으로 날아간다.(유도 기능X, 플레이어가 날아오는 마법구를 보고 피할 수 있다.)

2.오브젝트와 충돌했을 때 충돌한 오브젝트가 플레이어면 데미지 적용

 

5.3.2.스크립트

EnemyShaman(원거리형 AI 스크립트)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyShaman : LivingEntity
{
    public LayerMask whatIsTarget; //추적대상 레이어

    private LivingEntity targetEntity;//추적대상
    private NavMeshAgent pathFinder; //경로 계산 AI 에이전트
    private float dist; //적과 추적대상과의 거리

    /*public ParticleSystem hitEffect; //피격 이펙트
    public AudioClip deathSound;//사망 사운드
    public AudioClip hitSound; //피격 사운드
    */

    //스태프
    public GameObject firePoint; //매직미사일이 발사될 위치
    public GameObject magicMissilePrefab; //사용할 매직미사일 할당
    public GameObject magicMissile; //Instantiate()메서드로 생성하는 매직미사일을 담는 게임오브젝트


    private Animator enemyAnimator;
    //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트

    public float damage = 30f; //공격력
    public float attackDelay = 2.5f; //공격 딜레이
    private float lastAttackTime; //마지막 공격 시점

    public Transform tr;
    private float attackRange = 10f;

    //추적 대상이 존재하는지 알려주는 프로퍼티
    private bool hasTarget
    {
        get
        {
            //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
            if (targetEntity != null && !targetEntity.dead)
            {
                return true;
            }

            //그렇지 않다면 false
            return false;
        }
    }

    private bool canMove;
    private bool canAttack;

    private void Awake()
    {
        //게임 오브젝트에서 사용할 컴포넌트 가져오기
        pathFinder = GetComponent<NavMeshAgent>();
        enemyAnimator = GetComponent<Animator>();
        //enemyAudioPlayer = GetComponent<AudioSource>();
    }

    //적 AI의 초기 스펙을 결정하는 셋업 메서드
    public void Setup(float newHealth, float newDamage, float newSpeed)
    {
        //체력 설정
        startingHealth = newHealth;
        health = newHealth;
        //공격력 설정
        damage = newDamage;
        //네비메쉬 에이전트의 이동 속도 설정
        pathFinder.speed = newSpeed;
    }


    void Start()
    {
        //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작
        StartCoroutine(UpdatePath());
        tr = GetComponent<Transform>();

        //추적 대상과의 멈춤 거리 랜덤하게 설정하기(7~10사이), 적이 뭉쳐있는 것보다 산개된 모습을 주기 위해서
        pathFinder.stoppingDistance = Random.Range(7, 11);
    }

    // Update is called once per frame
    void Update()
    {
        //추적 대상의 존재 여부에 따라 다른 애니메이션 재생
        enemyAnimator.SetBool("CanMove", canMove);
        enemyAnimator.SetBool("CanAttack", canAttack);

        if (hasTarget)
        {
            //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update()
            dist = Vector3.Distance(tr.position, targetEntity.transform.position);
        }


    }

    //추적할 대상의 위치를 주기적으로 찾아 경로 갱신, 대상이 있으면 공격한다.
    private IEnumerator UpdatePath()
    {
        //살아 있는 동안 무한 루프
        while (!dead)
        {
            if (hasTarget)
            {
                Attack();
            }
            else
            {
                //추적 대상이 없을 경우, AI 이동 정지
                pathFinder.isStopped = true;
                canAttack = false;
                canMove = false;

                //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기
                Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

                //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기
                for (int i = 0; i < colliders.Length; i++)
                {
                    //콜라이더로부터 LivingEntity 컴포넌트 가져오기
                    LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                    //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면
                    if (livingEntity != null && !livingEntity.dead)
                    {
                        //추적 대상을 해당 LivingEntity로 설정
                        targetEntity = livingEntity;
                        //for문 루프 즉시 정지
                        break;
                    }
                }
            }

            //0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }

    //적과 플레이어 사이의 거리 측정, 거리에 따라 공격 메서드 실행
    public virtual void Attack()
    {
        //자신이 사망X, 최근 공격 시점에서 attackDelay 이상 시간이 지났고, 플레이어와의 거리가 공격 사거리안에 있다면 공격 가능
        if (!dead && dist <= attackRange)
        {
            //공격 반경 안에 있으면 움직임을 멈춘다.
            canMove = false;

            this.transform.LookAt(targetEntity.transform);

            //공격 딜레이가 지났다면 공격 애니 실행
            if (lastAttackTime + attackDelay <= Time.time)
            {
                canAttack = true;
                lastAttackTime = Time.time; //최근 공격시간 초기화
            }

            //공격 반경 안에 있지만, 딜레이가 남아있을 경우
            else
            {
                canAttack = false;
            }
        }

        //공격 반경 밖에 있을 경우 추적하기
        else
        {
            //추적 대상이 존재 && 추적 대상이 공격 반경 밖에 있을 경우, 경로를 갱신하고 AI 이동을 계속 진행
            canMove = true;
            canAttack = false;
            pathFinder.isStopped = false; //계속 이동
            pathFinder.SetDestination(targetEntity.transform.position);
        }
    }

    //유니티 애니메이션 이벤트로 지팡이를 앞으로 휘두를 떄 메서드 실행
    public void ShamanFire()
    {
        magicMissile = Instantiate(magicMissilePrefab, firePoint.transform.position, firePoint.transform.rotation); //Instatiate()로 매직 미사일 프리팹을 복제 생성한다.
    }

    /*미사일에서 데미지 처리하기
    
    //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기
    public void OnDamageEvent()
    {
        //상대방의 LivingEntity 타입 가져오기
        LivingEntity attackTarget = targetEntity.GetComponent<LivingEntity>();

        //공격 처리
        attackTarget.OnDamage(damage);
    }
    */


    //데미지를 입었을 때 실행할 처리
    public override void OnDamage(float damage)
    {
        /*사망하지 않을 상태에서만 피격 효과 재생
        if (!dead)
        {
            //공격 받은 지점과 방향으로 피격 효과 재생
            hitEffect.transform.position = hitPoint;
            hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal);
            hitEffect.Play();

            //피격 효과음 재생
            enemyAudioPlayer.PlayOnShot(hitSound);
        }
        */

        //피격 애니메이션 재생
        enemyAnimator.SetTrigger("Hit");


        //LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage);
    }

    //사망 처리
    public override void Die()
    {
        //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행
        base.Die();

        //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화
        Collider[] enemyColliders = GetComponents<Collider>();
        for (int i = 0; i < enemyColliders.Length; i++)
        {
            enemyColliders[i].enabled = false;
        }

        //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화
        pathFinder.isStopped = true;
        pathFinder.enabled = false;

        //사망 애니메이션 재생
        enemyAnimator.SetTrigger("Die");
        /*//사망 효과음 재생
        enemyAudioPlayer.PlayOnShot(deathSound);
        */

    }
}

 

EnemyMagicMissileMove(AI의 매직 미사일 이동 스크립트)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyMagicMissileMove : MonoBehaviour
{
    public LivingEntity targetEntity;//공격 대상

    public float speed = 15f;
    public float hitOffset = 0f;
    public bool UseFirePointRotation;
    public Vector3 rotationOffset = new Vector3(0, 0, 0);
    public GameObject hit;
    public GameObject flash;
    private Rigidbody rb;
    private SphereCollider sphCollider;
    public GameObject[] Detached;
    private float lastCollisionEnterTime;
    private float collisionDealy = 0.1f;
    
    //매직 미사일 고정 데미지, 
    public float damage;
    


    void Start()
    {
        rb = GetComponent<Rigidbody>();
        sphCollider = GetComponent<SphereCollider>();
        EnemyShaman enemyShaman = GameObject.Find("Enemy_Goblin Shaman").GetComponent<EnemyShaman>();
        damage = enemyShaman.damage;

        if (flash != null)
        {
            var flashInstance = Instantiate(flash, transform.position, Quaternion.identity); //Quaternion.identity 회전 없음
            flashInstance.transform.forward = gameObject.transform.forward;
            var flashPs = flashInstance.GetComponent<ParticleSystem>();
            if (flashPs != null)
            {
                Destroy(flashInstance, flashPs.main.duration);   //ParticleSystem의 main.duration, 기본 시간인듯, duration은 따로 값을 정할 수 있음
            }
            else
            {
                var flashPsParts = flashInstance.transform.GetChild(0).GetComponent<ParticleSystem>();
                Destroy(flashInstance, flashPsParts.main.duration);
            }
        }

        Destroy(gameObject, 5);
    }

    //매직 미사일 이동 기능
    void FixedUpdate()
    {
        if (speed != 0)
        {
            rb.velocity = transform.forward * speed; //타겟팅 대상이 없을 때 매직 미사일은 전방으로 날아간다
        }
    }

    private void Update()
    {
        OnSphereCollider();
    }

    void OnCollisionEnter(Collision collision)  //매직미사일이 충돌했을 경우
    {
        if (collision.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            rb.constraints = RigidbodyConstraints.FreezeAll;
            speed = 0;

            //상대방의 LivingEntity 타입 가져오기, 데미지를 적용하기 위한 준비
            LivingEntity attackTarget = collision.gameObject.GetComponent<LivingEntity>();

            Debug.Log("충돌한 오브젝트의 레이어" + collision.gameObject.layer + "충돌한 시간" + lastCollisionEnterTime);

            ContactPoint contact = collision.contacts[0];
            Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);
            Vector3 pos = contact.point + contact.normal * hitOffset;

            if (hit != null)
            {
                var hitInstance = Instantiate(hit, pos, rot);
                if (UseFirePointRotation) { hitInstance.transform.rotation = gameObject.transform.rotation * Quaternion.Euler(0, 180f, 0); }
                else if (rotationOffset != Vector3.zero) { hitInstance.transform.rotation = Quaternion.Euler(rotationOffset); }
                else { hitInstance.transform.LookAt(contact.point + contact.normal); }

                var hitPs = hitInstance.GetComponent<ParticleSystem>();
                if (hitPs != null)
                {
                    Destroy(hitInstance, hitPs.main.duration);
                }
                else
                {
                    var hitPsParts = hitInstance.transform.GetChild(0).GetComponent<ParticleSystem>();
                    Destroy(hitInstance, hitPsParts.main.duration);
                }
            }
            foreach (var detachedPrefab in Detached)
            {
                if (detachedPrefab != null)
                {
                    detachedPrefab.transform.parent = null;
                }
            }
            Destroy(gameObject);

            //데미지 처리
            attackTarget.OnDamage(damage);
            Debug.Log("현재 데미지" + damage);
        }

        else if(collision.gameObject.layer == LayerMask.NameToLayer("Obstacle"))
        {
            rb.constraints = RigidbodyConstraints.FreezeAll;
            speed = 0;

            Debug.Log("충돌한 오브젝트의 레이어" + collision.gameObject.layer + "충돌한 시간" + lastCollisionEnterTime);

            ContactPoint contact = collision.contacts[0];
            Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);
            Vector3 pos = contact.point + contact.normal * hitOffset;

            if (hit != null)
            {
                var hitInstance = Instantiate(hit, pos, rot);
                if (UseFirePointRotation) { hitInstance.transform.rotation = gameObject.transform.rotation * Quaternion.Euler(0, 180f, 0); }
                else if (rotationOffset != Vector3.zero) { hitInstance.transform.rotation = Quaternion.Euler(rotationOffset); }
                else { hitInstance.transform.LookAt(contact.point + contact.normal); }

                var hitPs = hitInstance.GetComponent<ParticleSystem>();
                if (hitPs != null)
                {
                    Destroy(hitInstance, hitPs.main.duration);
                }
                else
                {
                    var hitPsParts = hitInstance.transform.GetChild(0).GetComponent<ParticleSystem>();
                    Destroy(hitInstance, hitPsParts.main.duration);
                }
            }
            foreach (var detachedPrefab in Detached)
            {
                if (detachedPrefab != null)
                {
                    detachedPrefab.transform.parent = null;
                }
            }
            Destroy(gameObject);
        }

        else
        {
            sphCollider.enabled =false;
            Debug.Log("충돌한 오브젝트의 레이어" + collision.gameObject.layer + "충돌한 시간" + lastCollisionEnterTime);
        }
    }
    
    void OnSphereCollider()
    {
        if (lastCollisionEnterTime + collisionDealy < Time.time)
        {
            sphCollider.enabled = true;
            lastCollisionEnterTime = Time.time;
            Debug.Log("콜라이더 켜짐");
        }
    }
}

 

5.4.순간이동형 AI

근접형 AI에서 순간이동 기능을 추가하여 구현했다. 근접형 AI 플로우 차트에서 순간이동 기능을 추가했다.

5.4.1.순간이동형 AI 플로우 차트

 

5.4.2.기능 구현

순간이동 기능

구글에서 순간이동, teleportation 키워드로 검색하다보니 외국 사이트의 댓글에서 Random.insideUnitCircle 기능을 알게됐다. 이 기능은 자신의 위치에서 반경 1안의 원 안에서 랜덤한 위치로 이동하는데 기준을 상대방으로 바꿔서 순간이동을 구현했다.

https://docs.unity3d.com/kr/530/ScriptReference/Random-insideUnitCircle.html

 

Unity - 스크립팅 API: Random.insideUnitCircle

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. 닫기

docs.unity3d.com

    //Teleportation 메서드
    private void Teleportation()
    {
        if(bTeleportation)
        {
            //추적대상 근처 랜덤 위치 계산
            //Random.insideUnityCircle은 x, y값만 계산해서(2차원) y값을 z값에 더함(2차원에서 y값은 3차원의 z값), y값은 높이라서 그냥 y값으로 함
            tpPos = Random.insideUnitCircle * 1.5f;
            tpPos.x += targetEntity.gameObject.transform.position.x;
            tpPos.z = tpPos.y + targetEntity.gameObject.transform.position.z;
            tpPos.y = targetEntity.gameObject.transform.position.y;

            tr.position = tpPos;

            //시간 갱신
            curTpTime = Time.time;
            //순간이동 가능 여부 false로 변경
            bTeleportation = false;

            Debug.Log("순간이동 스킬을 사용했습니다" + Time.time);
        }
    }

 

순간이동 가능 여부 계산 기능

게임 시작 시 Start()에서 최초 1회 현재시간을 순간이동 사용 시간으로 기록하고 Update()에서 매 프레임마다 순간이동 사용 여부를 계산한다. 순간이동 사용 시간 + 순간이동 쿨타임 시간이 현재 시간보다 작을 경우 쿨타임이 지났으니

사용 여부를 true로 변경한다.

    private void CheckTpCooldown()
    {
        if(curTpTime + tpCooldown <= Time.time)
        {
            bTeleportation = true;
        }
    }

 

5.4.3.스크립트

EnemyGhost

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyGhost : LivingEntity
{
    public LayerMask whatIsTarget; //추적대상 레이어

    private LivingEntity targetEntity;//추적대상
    private NavMeshAgent pathFinder; //경로 계산 AI 에이전트

    private float dist; //적과 추적대상과의 거리
    public float tpRange = 10f;

    /*public ParticleSystem hitEffect; //피격 이펙트
    public AudioClip deathSound;//사망 사운드
    public AudioClip hitSound; //피격 사운드
    */

    private Animator enemyAnimator;
    //private AudioSource enemyAudioPlayer; //오디오 소스 컴포넌트

    public float damage = 50f; //공격력
    public float attackDelay = 2.5f; //공격 딜레이
    private float lastAttackTime; //마지막 공격 시점

    public Transform tr;

    private float attackRange = 1.5f;

    private float curTpTime; //teleportation 쿨다운 계산을 위한 현재 시간 저장
    public float tpCooldown = 10f; //teleportation 쿨다운
    private bool bTeleportation; //teleportation 가능 여부

    private Vector3 tpPos; //텔레포트에 사용할 좌표, 랜덤 좌표를 담음

    //추적 대상이 존재하는지 알려주는 프로퍼티
    private bool hasTarget
    {
        get
        {
            //추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
            if (targetEntity != null && !targetEntity.dead)
            {
                return true;
            }

            //그렇지 않다면 false
            return false;
        }
    }

    private bool canMove;
    private bool canAttack;

    private void Awake()
    {
        //게임 오브젝트에서 사용할 컴포넌트 가져오기
        pathFinder = GetComponent<NavMeshAgent>();
        enemyAnimator = GetComponent<Animator>();
        //enemyAudioPlayer = GetComponent<AudioSource>();
    }

    //적 AI의 초기 스펙을 결정하는 셋업 메서드
    public void Setup(float newHealth, float newDamage, float newSpeed)
    {
        //체력 설정
        startingHealth = newHealth;
        health = newHealth;
        //공격력 설정
        damage = newDamage;
        //네비메쉬 에이전트의 이동 속도 설정
        pathFinder.speed = newSpeed;
    }


    void Start()
    {
        //게임 오브젝트 활성화와 동시에 AI의 탐지 루틴 시작
        StartCoroutine(UpdatePath());
        tr = GetComponent<Transform>();
        curTpTime = Time.time; //텔레포트 쿨다운 계산을 위해서 시작 시간을 저장
    }

    // Update is called once per frame
    void Update()
    {
        //실시간으로 이동 애니메이션 변수 값을 읽고, 애니메이터에 전달
        enemyAnimator.SetBool("CanMove", canMove);
        enemyAnimator.SetBool("CanAttack", canAttack);


        if (hasTarget)
        {
            //추적 대상이 존재할 경우 거리 계산은 실시간으로 해야하니 Update()
            dist = Vector3.Distance(tr.position, targetEntity.transform.position);
        }

        CheckTpCooldown();
    }

    //추적할 대상의 위치를 주기적으로 찾아 경로 갱신
    private IEnumerator UpdatePath()
    {
        //살아 있는 동안 무한 루프
        while (!dead)
        {
            //탐지 반경 안에 있는 적이 있어서 이동과 공격이 가능
            if (hasTarget)
            {
                Attack();
            }
            else
            {
                //추적 대상이 없을 경우, AI 이동 정지
                pathFinder.isStopped = true;
                canAttack = false;
                canMove = false;

                //추적 대상 찾고, 대상으로 지정하기
                //반지름 20f의 콜라이더로 whatIsTarget 레이어를 가진 콜라이더 검출하기
                Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

                //모든 콜라이더를 순회하면서 살아 있는 LivingEntity 찾기
                for (int i = 0; i < colliders.Length; i++)
                {
                    //콜라이더로부터 LivingEntity 컴포넌트 가져오기
                    LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                    //LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아 있다면
                    if (livingEntity != null && !livingEntity.dead)
                    {
                        //추적 대상을 해당 LivingEntity로 설정
                        targetEntity = livingEntity;

                        //for문 루프 즉시 정지
                        break;
                    }
                }
            }

            //0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }

    //추적대상과의 거리에 따라 공격 실행
    //현재 이동 가능 상태
    public virtual void Attack()
    {

        //자신이 사망X, 추적 대상과의 거리이 공격 반경 안에 있다면
        if (!dead && dist < attackRange)
        {
            //공격 반경 안에 있으면 움직임을 멈춘다.
            canMove = false;

            //추적 대상 바라보기
            this.transform.LookAt(targetEntity.transform);
            
            //최근 공격 시점에서 attackDelay 이상 시간이 지나면 공격 가능
            if (lastAttackTime + attackDelay <= Time.time)
            {
                canAttack = true;
            }

            //공격 반경 안에 있지만, 딜레이가 남아있을 경우
            else
            {
                canAttack = false;
            }
        }

        //공격 반경 밖에 있을 경우 추적하기
        else
        {
            canMove = true;
            canAttack = false;
            //계속 추적
            pathFinder.isStopped = false; //계속 이동
            pathFinder.SetDestination(targetEntity.transform.position);

            //추적대상과의 거리가 tpRange 이하일 경우 && 텔레포트가 가능할 경우
            if(dist <= tpRange && bTeleportation)
            {
                Teleportation();
            }
        }
    }

    //유니티 애니메이션 이벤트로 휘두를 때 데미지 적용시키기
    public void OnDamageEvent()
    {
        //상대방의 LivingEntity 타입 가져오기
        LivingEntity attackTarget = targetEntity.GetComponent<LivingEntity>();

        //공격 처리
        attackTarget.OnDamage(damage);

        //최근 공격 시간 갱신
        lastAttackTime = Time.time;
    }


    //데미지를 입었을 때 실행할 처리
    public override void OnDamage(float damage)
    {
        /*사망하지 않을 상태에서만 피격 효과 재생
        if (!dead)
        {
            //공격 받은 지점과 방향으로 피격 효과 재생
            hitEffect.transform.position = hitPoint;
            hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal);
            hitEffect.Play();

            //피격 효과음 재생
            enemyAudioPlayer.PlayOnShot(hitSound);
        }
        */

        //피격 애니메이션 재생
        enemyAnimator.SetTrigger("Hit");


        //LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage);
    }

    //사망 처리
    public override void Die()
    {
        //LivingEntity의 DIe()를 실행하여 기본 사망 처리 실행
        base.Die();

        //다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화
        Collider[] enemyColliders = GetComponents<Collider>();
        for (int i = 0; i < enemyColliders.Length; i++)
        {
            enemyColliders[i].enabled = false;
        }

        //AI추적을 중지하고 네비메쉬 컴포넌트를 비활성화
        pathFinder.isStopped = true;
        pathFinder.enabled = false;

        //사망 애니메이션 재생
        enemyAnimator.SetTrigger("Die");
        /*//사망 효과음 재생
        enemyAudioPlayer.PlayOnShot(deathSound);
        */
    }

    //teleportation 쿨다운 계산 메서드
    private void CheckTpCooldown()
    {
        if(curTpTime + tpCooldown <= Time.time)
        {
            bTeleportation = true;
        }
    }

    //Teleportation 메서드
    private void Teleportation()
    {
        if(bTeleportation)
        {
            //추적대상 근처 랜덤 위치 계산
            //Random.insideUnityCircle은 x, y값만 계산해서 y값을 z값에 더함, y값은 그냥 y값으로 함
            tpPos = Random.insideUnitCircle * 1.5f;
            tpPos.x += targetEntity.gameObject.transform.position.x;
            tpPos.z = tpPos.y + targetEntity.gameObject.transform.position.z;
            tpPos.y = targetEntity.gameObject.transform.position.y;

            tr.position = tpPos;

            //시간 갱신
            curTpTime = Time.time;
            //순간이동 가능 여부 false로 변경
            bTeleportation = false;

            Debug.Log("순간이동 스킬을 사용했습니다" + Time.time);
        }
    }
}

 

 

5.5.네비 메쉬 굽기

적 AI(네비 메쉬 에이전트)가 게임 월드에서 이동하려면 네비 메쉬가 필요하다. 네비 메쉬는 정적 게임 오브젝트를 대상으로 생성가능하다.

 

1.적 AI(네비 메 에이전트)가 이동할 영역을 굽기위해서 발판과 장애물을 선택한다.

2.인스펙터창에서 이름 오른쪽의 Static를 체크한다.

3.유니티의 Windows > AI > Navigation를 선택해서 Navigation 창을 연다.

4.네비게이션 창에서 Bake 탭을 선택한다.

5.필요에 따라서 Agent Radius와 Agent Height를 수정한다.(나는 기본값 이용)

6.Agent Radius = 에이전트의 반경 / Agent Height = 에이전트의 크기이며 이것을 조절하여 AI가 다닐 수 있는 영역을 조절할 수 있다.

7.우하단의 Bake 버튼을 클릭한다.

8.구워진 네비 메는 파란색 영역으로 표시된다.

9.파란색 영역은 적 AI(네비 메 에이전트)가 이동할 수 있는 영역이다.

 

#확장 네비게이션 기능 활용 - 이재현 마스터의 내비게이션 활용법

현재는 고정된 영역에서 적 AI가 이동할 수 있게 네비 메를 미리 구웠지만 나중에 절차적 맵 생성으로 랜덤한 맵을 생성할 때에는 확장 네비게이션 기능을 이용해서 네비 메를 구어볼 생각이다.

 

https://www.youtube.com/watch?v=RmDRjoXUaTI

 

다음 구현 목표

1.적 HPbar UI

2.캐릭터 스킬 구현(파이어볼, 썬더, 텔레포트)

3.UI 버튼과 일반 공격 연결

 

여담...

최근 편도선염? 장염이 번갈아가며 걸려서 너무 힘들다. 장염은 대응 가능하나 편도선염의 경우 몇 일간 호전 악화를 반복한다. 열이 오르면 집중을 할 수 가 없어서 개발에 진전이 없다. 특히 블로그에 글을 작성하는 게 너무 힘들다. 나름 대응으로 목 따뜻하게 하고 따뜻한 물 많이 마시고, 생강차사서 먹어보고 있다.

생강차가 조금 효과는 있는 것 같다. 개발할 땐 몸 건강이 필수다. 빨리 나았으면 쉽지만 휴식해도 회복이 잘 안되니 디버프 상태에서 조금씩 할 일을 할 수 밖에 없다.

반응형

댓글