본문 바로가기

유니티 개발 정보/인공지능

Space Shooter AI(우주 발사체 인공지능) - 1

원문이곳에서 보실수 있습니다.
내용이 길어 총 3편으로 나눠서 올리겠습니다.
본문에서 프로젝트 파일을 다운받아서 같이 따라하시면 더 이해가 쉽습니다.:)


인공지능의 소개,
혼란스러울 만큼 마음을 사로잡는 주제

우주 발사체 AI

이 튜토리얼에서는, 우리는 간단한 우주 발사체를 개발할 것이다. AI를 배우기 위해 2D개발 환경으로 시작할 것이다. 게임은 기본적인 횡 스크롤 게임 플레이를 사용할 것이며, 이것은 적들이 오른쪽에서 왼쪽으로 이동하는 동안 우리의 우주선은 오른쪽으로 이동한다는 것을 의미한다.  핵심은 우리의 적들에게 생명을 주는 것이다.

우리는 약간의 벡터 연산뿐만 아니라, 삼각법, 객체지향(상속, 다형성)을 사용할 것이다. 만약 이런 것들에 친숙하다면 문제가 없겠지만, 만약 그렇지 않더라도 이 글에서는 간단하게 설명하고 있다. 우리는 이글에서 당신의 모든 것을 만족시켜줄 수 없기 때문에, 좀 더 많은 정보를 찾는 것은 당신에게 달려있다.

이 프로젝트는 이곳에서 받을 수 있다. 당신은 이 프로젝트를 사용하거나, 글을 읽고 당신만의 컴포넌트를 작성할 수 있을 것이다. 만약 컴포넌트가 충돌을 발생시킨다면 기존의 것을 삭제하던지 이름을 바꿔야 한다. 또한 새로운 것을 추가할 때는 기존의 스크립트를 삭제해야 된다는 것을 명심해야한다. 그러나 우리는 당신이 우리가 제공하는 컴포넌트를 사용한다고 가정할 것이다.

여기서는 다음과 같은 사항을 알아 볼 것이다:
(튜토리얼 소개 동영상추가, 12분, 영어)

우주선 생성하기


우선, 우리는 우주선을 생성할 것이다. 유저가 사용하는 부분에 대한 코드를 만들 것이기 때문에, 이 부분은 인공지능(이하 AI)이 아니다. AI부분은 바로 다음에 나올 것이다. 프로젝트를 다운로드 했다면 우주선을 가지고 있을 것이다. 프로젝트에서는 이미 필요로 하든 모든 것을 가지고 있으며, 당신이 알아야 하는 대부분의 정보가 주석을 포함으로 설명이 되어 있다.


이제 우리는 비-플레이어-캐릭터(이하 NPC)를 만들어야 한다(여기서는 비-플레이어-우주선). 첫번째 랜덤한 장소에 랜덤한 양의 우주선을 생성하는 것이다. SpawnObject폴더에, 밑에 스크립트가 존재한다. 그것은 이미 Hierarchy에 있는 Spawn객체에 포함이 되어 있다. 당신은 간단하게 그것을 확인할 수 있다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;
 
public class CreateRandom : MonoBehaviour {
    public GameObject ship;
    Transform _transform;
    void Start () {
         _transform= GetComponent<Transform>();
         InvokeRepeating("CreateRandomShip",0.01f,2.0f);
    }
 
    void CreateRandomShip(){
        Vector3 vec= new Vector3(_transform.position.x,0,Random.Range (-1f,-9f));
        Instantiate(ship,vec,transform.rotation);
    }
}

모든 마법은 Vector3 vec =new Vector3(22,0,Random.Range(-1f,-9f)); 문장 안에 존재한다. 들이 하는 것이 무엇인가? 이들은 화면 밖에 존재하는 x 좌표 22에서 적들을 생성하기 때문에 적들은 화면 중앙에서 나타나지 않는다. y 좌표는 0을 나타내는데 이는 현재 2D환경에 있기 때문에 여기서는 y 좌표를 사용하지 않기 때문이다. 마지막으로 우리는 적이 생성되는 곳을 정의하는 범위내에서 랜덤 숫자를 사용할 것이다. 이는 화면에서 지저분하게 적들을 나타나게 한다.

튜토리얼안에서 주어진 대부분의 값들은 당신의 게임에서 plane 원본 위치에 따라 다시 재조정 될 것이다.

우리의 프로젝트는 간단한 빈 게임 오브젝트를 생성하는 Spawn 객체를 가지는 것이다. 위에 스크립트를  Spawn객체에 첨부해라. CreateRandomShip은 InvokeRepeating 함수를 통해 2초마다 호출될 것이다.
(역자 주: CreateRandom스크립트는 Spawn객체에 이미 포함이 되어있다. 따로 추가할 필요는 없다)

우주선 움직이기


이제 두번째로, 우주선을 생성하는 것은 잘되지만, 또한 움직여야 한다. 여기서 객체 지향 프로그래밍(이하 OOP)의 효과가 나타나기 시작한다. 우리는 우주선의 기본 행동을 나타내는 클래스를 선언할 것이다. 만약 우주선이 좀 더 구체적인 행동을 하기를 원한다면, 기본클래스(부모클래스)를 상속해서 새로운 클래스를 선언하면 된다.

밑의 코드는 기본 Enemy 클래스이다. 나중에 좀 더 많은 기본적인 액션을 추가하기 위해 다시 이 코드로 돌아올 것이다. Enemy스크립트를 프로젝트 창에 있는 Enemy프리팹에 추가하라.


반드시  Hierarchy 창에 있는 객체인지 아니면 프로젝트 창에 있는 객체인지 확인을 하면서 따라하기를 바란다.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;
 
public class Enemy : MonoBehaviour {
 
    protected Vector3 velocity;
    public Transform _transform;
    public virtual void Start () {
                _transform = GetComponent<Transform>();
                velocity = new Vector3(Random.Range(0.5f,1.5f),0,0);
    }
        public virtual void Update(){
              _transform.Translate ( -velocity.x*Time.deltaTime,0,velocity.z*Time.deltaTime,Space.World);
        }
 
       void OnTriggerEnter(Collider other){
        if(other.gameObject.tag == "EnemyDeath")Destroy(gameObject);
    }
}

이 스크립트는 우리의 기본 클래스를 나타낸다. 모든 적 클래스는 이 클래스를 상속한다. 그래서 모든 적들에게 일반적으로 원하는 맴버변수는 모두 이곳에 첨부되어야 한다. 이러한 이유로 지금까지 대부분의 변수가 protected 혹은 public으로 선언된 것이다.

대부분의 함수는 virtual(이하 가상 메소드)로 선언되어 있는데, 우리는 후에 자식클래스에서 그들을 오버라이드할 것이다. 또한 Trigger 함수는 가상 메소드가 아닌 것을 주목해야 한다. 우리는 모든 우주선들이 화면 밖으로 나갔을 때 파괴되기를 원한다(프로젝트에서 화면의 양 끝에 trigger 박스가 있다)
이는 모든 우주선들에 대한 공통된 사항이다. 우리는 나중에 다시 이것을 작성하는 것을 원하지 않는다.

velocity =new Vector3(Random.Range(0.5f,3.0f),0,0); 우리는 다시 velocity를 위해 랜덤 값을 할당 할 것이다. 그래서 모든 우주선은 같은 속도로 움직이지 않을 것이다.


이제 우리는 단순한 첫번째 레벨을 가졌다. 바위에 대해서도 같은 규칙을 사용할 것이고, 우리는 우주선과 소행성을 얻었다.


우주선은 움직이지만, 더 많은 것들을 하지는 않는다. 우리는 그들이 미사일을 발사하는 것을 원한다.

또한 ,우주선의 z 속도를 수정하기 위해서 InvokeRepeating을 사용할 수 있다. 그리고 그들은 항상 왼쪽으로 가지 않고, 위나 아래로 조금씩 움직일 것이다.

01
02
03
04
05
06
07
08
09
10
11
public virtual void Start(){
    InvokeRepeating("ChangeZ",2.0f,1.0f);
}
public virtual void Update(){
     _transform.Translate( -velocity.x*Time.deltaTime,0,
         velocity.z*Time.deltaTime,Space.World);
}
 
void ChangeZ(){
    velocity.z = Random.Range(-1.0f,1.0f);
}

이것은 꽤 지저분해보이지만, 이것 또한 당신이 찾고 있는 것일 수도 있다.

미사일 발사

우주선은 2개의 발사대를 가지기 때문에, 그들에게 재발사를 하기 전에 "재장전"의 기능을 줄 것이다. 사실 우리는 단지 왼쪽과 오른쪽 발사대를 왔다 갔다 할 것이다.  우주선은 2개의 발사대 앞에 발사를 위해 사용될 2개의 빈 게임오브젝트를 가진다. 밑의 코드는 기본클래스에 추가되었다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Transform _primaryShoot;
Transform _secondaryShoot;
bool side = true;
public GameObject proj;
 
protected void Shoot(){
    if(_primaryShoot){
        if(_secondaryShoot){
            if(side)
                Instantiate (proj,_primaryShoot.position,Quaternion.identity);
            else
                Instantiate (proj,_secondaryShoot.position,Quaternion.identity);
             side = !side;
        }else
             Instantiate (proj,_primaryShoot.position,Quaternion.identity);
   }
}
void OnCollisionEnter(Collision other){
      if(other.gameObject.tag == "Ship"){
           Destroy (gameObject);
           other.gameObject.SendMessage("DecreaseHealth",15);
      }
}

Shoot 함수를 잠깐 봐라. 기본적으로 적은 아마 1개 혹은 2개의 무기를 가지거나, 하나도 가지지 않을 수도 있다. 만약 발사를 미사일을 발사해야할 무기가 없다면, 우리는 실수가 발생할 것이라고 생각할 것이다. 사실 밑에 else문을 추가하여 "당신은 미사일 발사를 시도하지만, 객체는 무기를 가지고 있지 않다"라는 경고문을 출력할 수 있을 것이다. 만약 첫번째 if문이 없다면 당신의 게임은 충돌이 발생할 것이다.

이제 우리는 첫번째 BasicEnemy 클래스를 추가해야 한다. EnmeyScript 폴더에서, BasicEnemy스크립트를 찾은 다음 그것을 Enemy프리팹에 추가해라. 당신은 실제로 Emeny Component를 제거하거나 비활성화 시킬 수 있다. BasicEnemy는 Enemy의 자식이기때문에, 유니티는 자동적으로 부모 클래스의 모든 public과 protected 맴버변수를 자동으로 추가할 것이다.
(역자 주: BasicEnemy를 추가했다면, 기존의 Enemy 스크립트는 제거하면 된다.)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;
 
public class BasicEnemy : Enemy {
 
    public override void Start () {
        base.Start();
        _primaryShoot = transform.Find ("SpawnLeft").transform;
        _secondaryShoot = transform.Find ("SpawnRight").transform; 
               InvokeRepeating("Shoot",Random.Range(2.0f,4.0f),Random.Range(2.0f,4.0f));
    }
 
    public override void Update () {
        base.Update ();
    }
}

우리는 Start 함수를 오버라이드한다. 하지만 약간의 변수들은 부모의 Start함수에서 할당되기 때문에 base.Start()함수도 같이 호출할 것이다. 다음, 2개의 무기를 찾을 것이고 Shoot함수를 실행시킬 것이다. 혹시 왜 base.Shoot()를 호출하지 않는지 의문이 든다면, Shoot 함수는 기본클래스에서 protected되어 있고 그래서 상속된다. 상속된 클래스에 의해서 오버라이드 된 부모클래스의 맴버에 접근하기를 원한다면 base 키워드를 사용하면 된다.

이제 발사할 준비가 되었다. 이제는 ProjectileEnemy 프리팹을 BasicEnemy에 끌어다 놓기만 하면 된다. 
이는 Projectile 폴더 안에서 찾을 수 있을 것이다. 나는 또한  Player가 우주선에 맞았을 경우를 대비해 collision 함수를 추가할 것이다. 죽는 것또한 AI의 한 종류임을 명심해라.


만약 프로젝트에서 projectile을 사용하고 있다면, 이미 밑의 스크립트는 첨부가 되어 있을 것이다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using System.Collections;
 
public class ProjEnemy : MonoBehaviour {
 
    Transform _transform;
    void Start () {
        _transform = GetComponent<Transform>();
    }
 
    void Update () {
        _transform.Translate ( speed*Time.deltaTime,0,0,Space.World);
    }
    void OnCollisionEnter(Collision other){
        if(other.gameObject.tag == "Ship"){
            other.gameObject.GetComponent<ShipControl>().DecreaseHealth(10);
            Destroy(gameObject);
        }
    }
    void OnTriggerEnter(Collider other){
        if(other.gameObject.tag == "EnemyDeath")Destroy(gameObject);
    }
}

collision 함수는 ship에 체력을 감소시키는 메시지를 보낸다. 만약 나중에 좀 더 진보한 발사체를 만들려고 한다면, 상속을 사용할 수 있다는 것을 명심하라.

삼각법을 이용해 fancy pattern 주기

기본 AI 움직임의 두번째 타입을 고려해보자. 우리는 이제 진동과 파동의 영역으로 들어갈려고 한다.
우리가 지금 여기서 다룰려고 하는 것은 간단하게 우주의 원리에 대한 기본 방정식이다.
나는 이 말이 약간 지나치게 밀어붙이는 것 같다는 것을 알고는 있지만 나는 거기에 대해서 아무 것도 하지 않았다 :)


이제 우리는 sine(사인) 혹은 cosine(코사인)과 같은 간단한 함수를 사용하면서, 우리는 쉽게 훌륭한 모양을 얻을 수 있을 것이다.

당신이 sin을 사용하든 cos을 사용하든 그것은 중요한 게 아니다. 우리는 약간의 모양을 얻을 것이다. 똑같이 0에서, 하나는 sin(0) = 0으로 시작하고 그리고 다른 하나는 cos(0) = 1로 시작을 한다.
다시 말하지만 이것은 내가 결정한 것이 아니라, 우주가 그렇게 만들었다.
sin과 cos의 멋진 특징 1가지는 결과가 선형 보간이 되고, -1과 1로 값이 고정이 된다는 것이다. 우리는 계속해서 1 에서 -1 그리고 다시 1로 돌아가는 변수를 얻을 수 있을 것이다.
그래서 우리는 기본 클래스 그리고 기본 클래스를 상속하는 새로운 클래스를 선언할 것이다. 우선, 스크립트를 EnemyShip 객체로 부터 Enemy 스크립트를 제거하고 새롭게 만들 것이다.


Enemy 클래스안에 있는 private이 아닌 모든 맴버는 EnemyShip에서 접근이 가능하다. 만약 private이 아니라면, public 또는 protected일것이다. 이전에 private는 언급되었다. 남은 건 public과 protected이다.(또한 internal이 있지만 그것은 당장은 언급하지 않겠다) public은 다른 모든 객체에서 접근이 가능하다. private은 오직 그것이 선언된 파일/클래스안에서만 보이며 자식클래스는 기본클래스로부터 private 맴버를 상속할 수 없다. protected는 private과 유사하지만 자식클래스는 protected 맴버를 상속할 수 있다.
새로운 클래스 선언할 것이다. 그리고 우주선의 움직임 패턴을 만들 것이다. 밑에 코드는 간단하게 적 우주선이 오른쪽에서 왼쪽으로 진동체 패턴으로 화면을 가로지르게 할 것이다.

using UnityEngine;
using System.Collections;

public class ShipOsc : Enemy {

    float amplitude = 2;
    float omega = 0.5f;
    float index;
    float zPos;

    public override void Start(){
        base.Start ();
        _transform.position = new Vector3(22f,0f,-5f);    
        _primaryShoot = transform.Find ("SpawnLeft").transform;
        _secondaryShoot = transform.Find ("SpawnRight").transform;
        zPos=_transform.position.z;
        InvokeRepeating("Shoot",Random.Range(2.0f,4.0f),Random.Range(2.0f,4.0f));
    }

    public override void Update(){
        index += Time.deltaTime;
        float zPos=amplitude*(Mathf.Cos(omega*index))*Time.deltaTime;
        _transform.Translate ( -velocity.x*Time.deltaTime,0,zPos,Space.World);
    }
}
여기에 기본적인 cos/sin 함수가 있다:  Amplitude*Mathf.Cos(omega*Time.time+phase);
방정식에 대해서 간단하게 설명하자면, Amplitude(진폭)는 파동의 크기에 영향을 끼치며, omegas는 진파동의 진동수 - 하나의 정점에서 다른 정점으로 얼마나 빠르게 이동하는지- 에 영향을 미친다. time은 값을 수정하기 위해서 필요하다.  phase는 위상변위를 나타낸다. radian으로 표시된 값은 동등한 값의 진동의 시작 위치를 이동시킬 것이다. 후에 우리는 예제를 보게 될 것이다.
 
또, enemy 클래스는 미사일을 발사하는 능력을 가진다.

밑에 그림은 시간에 따라 달라지는 위치를 보여주는데, 만약 모든 우주선이 동시에 주어진다면, 모두 같은 위치를 가질 것이다. 우주선이 증가될 때 index는 증가하기 시작하고, 그래서 모든 우주선은 각자 다른 값을 가지고 있을 것이다.


이 간단한 코드로, 당신의 적에 약간의 생명을 줄 수 있다.


우주선은 서로를 따라가는 멋진 패턴처럼 보인다.


















첫번째 그림을 보면, omega의 값은 두번째  그림에서보다 더 크다. 그 결과 우주선의 파동은 줄어들었다. 만약 amplitude(진폭)에 더 작은 값을 준다면, 파동은 더 작은 위아래 간격을 다룰 것이고, 반대로 움직일 것이다. 진동체와 파동에 대해서 우선 이해하기 위해서 amplitude와 omega를 가지고 놀아봐라. 이는 게임 패턴에서 유용할 뿐만 아니라 많은 물리적 법칙을 발생시키는 역할을 한다.

마찬가지로 발사체에 대해서도 이 규칙을 사용하는 것을 생각해볼 수 있다. 동일하게 적용을 하면 동일하게 이 진동체 패턴을 사용할 수 있을 것이다. 당신은 이것을 할 수 있는 도구를 획득했다. 이제 그것을 사용하는 것은 당신에게 달렸다.

(다음 글에서 계속)