본문 바로가기

Development/[Game] Basic Game

[Game] #3. AI 구현

728x90
반응형

지난 포스팅 복습

저번 포스팅에서는 타일 팔레트를 설정하여 타일을 사용하기 위해 모아둔 프리 펩 / 타일 맵을 사용하기 위한 준비단계를 진행했고, 타일 팔레트를 이용하여 지형을 빠르게 생성했다.

https://udangtangtang-cording-oldcast1e.tistory.com/117

 

[Game] #4. 새로운 프로젝트

타일 팔레트 설정하기 타일 팔레트(Tile Palette) : 타일을 사용하기 위해 모아둔 프리 펩 / 타일 맵을 사용하기 위한 준비단계 사용법: Window -> 2D -> Tile Palette 먼저 Asset에 TileMap 폴더를 생성한 다음..

udangtangtang-cording-oldcast1e.tistory.com

 

새로운 2D 게임

이번에는 게임에서 사용할 몬스터의 AI를 설정하여 보자. 이는 앞으로 만들 마리오 게임에도 똑같이 적용될 것이다.

몬스터 AI 구현하기

먼저 AI를 구현하기 전 몬스터 몬스터를 생성하고 애니메이션을 넣어보자.

  1. 앞서 만든 Enemy 스프라이트를 하이어 라키에 드래그&드롭한다.
  2. 걷기 애니메이션을 생성하여 할당한다.
  3. 플레이어가 이동할 Tile으로 옮긴다.

몬스터의 기능 구현

몬스터의 기능을 하기 위해서는 플레이어를 위협하는 존재이며 낭떠러지를 알아서 판단할 수 있는 기능들을 갖춰야 한다.

몬스터는 다음과 같은 기능을 기본 적으로 만족해야 한다.

  1. 특정 시간마다 특정 위치만큼으로 자동으로 이동한다.
  2. 플레이어와 접촉 시 플레이어의 상태를 Die로 변경한다.
  3. 낭떠러지를 인지하여 떨어지지 않도록 한다. 이는 마리오 게임에서 제거한다.
  4. 몬스터의 행동에 따른 애니메이션을 실행한다.
애니메이터 창 하이어 라키 및 인스펙터

몬스터의 기본 이동 구현

몬스터는 플레이어에 위협이 되는 존재로 위치를 변경하며 움직인다. 이를 위해 몬스터의 이동을 구현해보자.

using UnityEngine;

public class EnemyMove : MonoBehaviour{
    private Rigidbody2D EnemyRigd; //물리이동을 위한 변수 선언 
    private SpriteRenderer spriteRenderer; //방향전환을 위한 변수 
    private Animator Enemyanimator; //애니메이터 조작을 위한 변수 

    void Start(){EnemyRigd = GetComponent<Rigidbody2D>();}

    void Update(){EnemyRigd.velocity = new Vector2( 1,EnemyRigd.velocity.y);}
}

몬스터의 행동 설정

이번에는 몬스터가 스스로 방향을 바꾸어 이동하여 플레이어에게 조금 더 위협이 되는 행동을 하도록 설정한다.

몬스터의 방향을 랜덤으로 지정하여 Think() 함수 내에서 다음 이동 방향을 임의로 결정하도록 만든다.

value = Random.Range(x, y);
Random.Range(x, y);
x, y 사이의 값을 임의로 반환한다.

이때 주의할 점은 랜덤 반환의 최솟값 x는 범위에 포함되지만 최댓값 y는 범위에 포함되지 않는다!

using System;
using System.Numerics;
using UnityEngine;

public class EnemyMove : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D EnemyRigd; //물리이동을 위한 변수 선언 
    private SpriteRenderer spriteRenderer; //방향전환을 위한 변수 
    private Animator Enemyanimator; //애니메이터 조작을 위한 변수 
	public int NextMove;
    
    void Start(){EnemyRigd = GetComponent<Rigidbody2D>();}
    void Update(){EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);}
    void Think(){NextMove = Random.Range(-1,2);}
}

이때 몬스터의 행동 변경을 특정 시간마다 수행하게 되면, 몬스터의 행동을 랜덤으로 수행하게 만들 수 있다.

Invoke("Fuction", Sec);
Invoke: 주어진 시간이 지난 뒤, 지정한 함수를 실행하는 함수

Invoke 함수를 이용해 특정 시간이 흐른 뒤 수행할 함수를 지정할 수 있다.

using UnityEngine;

public class EnemyMove : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D EnemyRigd; //물리이동을 위한 변수 선언 
    private SpriteRenderer spriteRenderer; //방향전환을 위한 변수 
    private Animator Enemyanimator; //애니메이터 조작을 위한 변수 
    private Transform EnemyTransform; //애니메이터 조작을 위한 변수 

    public int NextMove;

    void Start()
    {
        EnemyRigd = GetComponent<Rigidbody2D>();
        EnemyTransform = GetComponent<Transform>();
        Invoke("Think",5);
    }
    // Update is called once per frame
    void Update()
    {
        EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);
    }
    void Think(){
        // EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);
        NextMove = Random.Range(-1,2);
        if(NextMove==1) transform.localScale = new Vector3(1, 1, 1); // 왼쪽 바라보기
        else if(NextMove==-1) transform.localScale = new Vector3(-1, 1, 1);
        Invoke("Think",5);
    }
}

몬스터의 낭떠러지 파악

몬스터가 스스로 낭떠러지로 떨어지지 않도록 몬스터가 스스로 낭떠러지를 판단할 수 있는 코드를 작성한다. 이를 위해서는 이동하는 경로의 상태를 미리 예측해야 하므로, 앞쪽을 주기적으로 체크해야 한다.

 

이때 게임 창을 보면 가로/세로 방향으로 의도치 않은 선이 생긴 모습을 볼 수 있는데, 이 문제를 먼저 해결해보자.

https://www.youtube.com/watch?v=xgqx9f-X6-c 

문제 해결 유튜브

위 영상에 따르면 이는 스프라이트의 겹침으로 발생하는 문제로, 스프라이트 아틀라스를 이용하면 게임 실행 시 스프라이트의 가로/세로줄이 사라지는 것을 확인할 수 있다.

이때 스프라이트 아틀라스는 다음과 같이 생성할 수 있다.

스프라이트 아틀라스 생성 objects for Packing

스프라이트 아틀라스를 생성 후, objects for Packing에 스프라이트 부모 스프라이트를 선택해준다. 이후 게임을 실행하면 다음과 같이 가로/세로줄이 사라진 것을 확인할 수 있다.

스프라이트 아틀라스를 생성 전 스프라이트 아틀라스를 생성 후

몬스터의 AI 구현: 낭떨어지 파악

몬스터가 스스로 낭떨어지를 파악하기 위해선 앞서 다룬 내용처럼 자신의 이동 방향보다 조금 앞선 방향에서의 아래 방향이 낭떠러지 즉, 레이 케스트가 null인 경우를 찾으면 된다. 여기서 중요한 점플레이어가 아닌 플레이어의 이동방향에 조금 앞선 위치를 파악하는 것이다.

몬스터 스크립트 변경

이를 위해서 앞선 코드를 다음과 같이 변경한다.

using UnityEngine;

public class EnemyMove : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D EnemyRigd; //물리이동을 위한 변수 선언 
    private SpriteRenderer spriteRenderer; //방향전환을 위한 변수 
    private Animator Enemyanimator; //애니메이터 조작을 위한 변수 
    private Transform EnemyTransform; //애니메이터 조작을 위한 변수 

    public int NextMove;

    void Start()
    {
        EnemyRigd = GetComponent<Rigidbody2D>();
        EnemyTransform = GetComponent<Transform>();
        Invoke("Think",5); 
    }
    // Update is called once per frame
    void Update()
    {
        //Move
        EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);

        //Platform Check
        Vector2 frontVec = new Vector2(EnemyRigd.position.x+NextMove,EnemyRigd.position.y);
        Debug.DrawRay(frontVec, Vector3.down, new Color(0,1,0)); //빔을 쏨(디버그는 게임상에서보이지 않음 ) 시작위치, 어디로 쏠지, 빔의 색 
        RaycastHit2D rayHit = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
        //빔의 시작위치, 빔의 방향 , 1:distance , ( 빔에 맞은 오브젝트를 특정 레이어로 한정 지어야할 때 사용 ) // RaycastHit2D : Ray에 닿은 오브젝트 클래스 
        if(rayHit.collider == null){ //빔을 맞은 오브젝트가 있을때  -> 맞지않으면 collider도 생성되지않음 
            Debug.Log("Danger!");
            // if(rayHit.distance < 0.5f) 
            //     animator.SetBool("isJumping",false); //거리가 0.5보다 작아지면 변경
 
        }
    }
    void Think(){
        // EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);
        NextMove = Random.Range(-1,2);
        if(NextMove==1) transform.localScale = new Vector3(1, 1, 1); // 왼쪽 바라보기
        else if(NextMove==-1) transform.localScale = new Vector3(-1, 1, 1);
        Invoke("Think",5);
    }
}

코드를 실행하면 다음과 같이 몬스터 앞에 작은 레이케이스가 생성된 것을 볼 수 있다. 이 레이 케스트는 기즈모를 켜야 확인할 수 있다.

하지만 여전히 문제점이 존재한다. 레이케스트가 몬스터를 기준으로 너무 멀다는 문제가 있다.

레이케이스

하지만 아직 몬스터가 낭떨어지를 발견해도 그대로 떨어지는 문제점이 발생한다. 이를 해결하기 위해 낭떠러지 발견 시, 즉 rayHit.collider == null인 경우 이동 방향을 바꿔 낭떨어지에 떨어지지 않도록 한다.

if(rayHit.collider == null){ //빔을 맞은 오브젝트가 있을때  -> 맞지않으면 collider도 생성되지않음 
    Debug.Log("Danger!");
    NextMove *= -1;
    CancelInvoke();
    Invoke("Think",5);
}

 

CancelInvoke();
CancelInvoke(): 현재 작동 중인 모든 invoke 함수를 멈추는 함수
using UnityEngine;

public class EnemyMove : MonoBehaviour
{
    // Start is called before the first frame update
    private Rigidbody2D EnemyRigd; //물리이동을 위한 변수 선언 
    private SpriteRenderer spriteRenderer; //방향전환을 위한 변수 
    private Animator Enemyanimator; //애니메이터 조작을 위한 변수 
    private Transform EnemyTransform; //애니메이터 조작을 위한 변수 

    public int NextMove;

    void Start()
    {
        EnemyRigd = GetComponent<Rigidbody2D>();
        EnemyTransform = GetComponent<Transform>();

        NextMove = Random.Range(-1,2);
        Invoke("Think",5); 
    }
    // Update is called once per frame
    void Update()
    {
        

        //Move
        EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);

        //Platform Check
        Vector2 frontVec = new Vector2(EnemyRigd.position.x+NextMove,EnemyRigd.position.y);
        Debug.DrawRay(frontVec, Vector3.down, new Color(0,1,0)); //빔을 쏨(디버그는 게임상에서보이지 않음 ) 시작위치, 어디로 쏠지, 빔의 색 
        // float EnemyRay = (frontVec>0)?(frontVec-0.5f):frontVec+(0.5f);
        RaycastHit2D rayHit = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
        //빔의 시작위치, 빔의 방향 , 1:distance , ( 빔에 맞은 오브젝트를 특정 레이어로 한정 지어야할 때 사용 ) // RaycastHit2D : Ray에 닿은 오브젝트 클래스 
        if(rayHit.collider == null){ //빔을 맞은 오브젝트가 있을때  -> 맞지않으면 collider도 생성되지않음 
            // Debug.Log("Danger!");
            NextMove *= -1;
            if(NextMove==1) transform.localScale = new Vector3(1, 1, 1); // 왼쪽 바라보기
            else if(NextMove== -1) EnemyTransform.localScale = new Vector3(-1, 1, 1);

            CancelInvoke();
            Invoke("Think",5);
        }
    }
    void Think(){
        NextMove = Random.Range(-1,2);

        // EnemyRigd.velocity = new Vector2(NextMove,EnemyRigd.velocity.y);
        
        float nextThinkTime = Random.Range(2f,5f);
        Invoke("Think",nextThinkTime);
    }
}

게임 실행

게임 플레이 영상

 

728x90
반응형
댓글