이제 적 캐릭터를 만듭니다. 움직이는 데미지 블록과 비슷합니다.

적 캐릭터는 다음과 같이 동작합니다.

플레이어가 접촉하면 게임오버가 됩니다. (Dead Tag)

일정 범위를 왔다갔다한다. (MovingBlock)

벽에 접촉하면 180도 방향을 바꾼다.

이번 예제에서 만들 적캐릭터 데이터는 Empty폴더를 만들어 저장합니다.

 

적 캐릭터 만들기

적 캐릭터는 4장에서 애니메이션이 되도록 이미지를 준비했습니다.

images폴더에서 enemy1~4를 한꺼번에 선택해 씬뷰에 끌어다 애니메이션을 만듭니다. 게임오브젝트와 애니메이션 이름을 Enemy로 변경합니다.

태그를 Dead로 설정합니다. Sprite Renderer 컴포넌트의 Order in Layer는 2로 합니다. 어태치할 컴포넌트는 RigidBody 2D, Circle Collider 2D, Box Collider 2D입니다.  어태치한 후 Rigidbody 2D의 Freeze Rotation에서 Z를 체크해 회전하지 않도록 합니다.

Circle Collider 2D와 Box Collider 2D의 위치는 다음 그림과 같이 설정합니다. Collider영역은 Edit Collider를 누르면 나타나는 영역과 점을 이용해 조정합니다.

Box Collider가 Circle Collider보다 약간 크고 isTrigger를 체크해서 충돌을 담당합니다.

Player라 접촉하기면 죽일수 있게 Enemy game object는 Dead Tag를 지정하였습니다. Player가 충돌하면 죽는 DeadZone이 함정, 니들, 적으로 늘어났습니다. Circle Collider2D는 지면과의 물리적 충돌을 처리하기위해 약간 아래로 내립니다. 원이 박스보다 접촉이 적어 저항이 적습니다. 

 

적 캐릭터의 스크립트 만들기

스크립트를 하나 만들고 EnemyController라고 이름을 바꿉니다.  스크립트를 만들고 어태치합니다. 플레이어 캐릭터를 움직이는 PlayerController의 간소한 버전입니다.

 

변수

public float speed = 3.0f;          // 이동 속도
public string direction = "left";   // 방향 right or left 
public float range = 0.0f;          // 움직이는 범위
Vector3 defPos;                     // 시작 위치

Start()

변수 direction이 "right"일 경우 스프라이트 localScale을  x축으로 반전시켜 줍니다. 현재위치를 디폴트위치로 합니다.

void Start()  {
    if (direction == "right") {
        transform.localScale = new Vector2(-1, 1);// 방향 변경
    }
    defPos = transform.position;  // 시작 위
}

Update()

적캐릭터가 이동범위를 벗어나면 direction="right"<->"left"로 방향을 바꿔준다. range가 0일 경우 범위 체크는 안하고 벽을 부딪쳐야만 반전한다.

void Update() {
    if (range > 0.0f)  {
        if (transform.position.x < defPos.x - (range / 2)) {
            direction = "right";
            transform.localScale = new Vector2(-1, 1);// 방향 변경
            if (transform.position.x > defPos.x + (range / 2)){
                direction = "left";
                transform.localScale = new Vector2(1, 1);// 방향 변경
            }
        }
    }
}

FixedUpdate()

direction방향에 따라 velocity를 가속해준다

void FixedUpdate()   {
    // 속도 갱신
    // Rigidbody2D 가져오기
    Rigidbody2D rbody = GetComponent<Rigidbody2D>();
    if (direction == "right")  {
        rbody.velocity = new Vector2(speed, rbody.velocity.y);
    } else  {
        rbody.velocity = new Vector2(-speed, rbody.velocity.y);
    }
}

OnTriggerEnter2d()

벽과 충돌하면 direction="right"<-> "left"를 반전시켜 방향을 바꿔준다. 충돌체크를 위해서는  BoxCollider 2D isTrigger가 체크되어 있어야 합니다.

private void OnTriggerEnter2D(Collider2D collision) {
    if (direction == "right")  {
        direction = "left";
        transform.localScale = new Vector2(1, 1); // 방향 변경
    }  else {
        direction = "right";
        transform.localScale = new Vector2(-1, 1); // 방향 변경
    }
}

EnemyController.cs
0.00MB

스크립트를 적캐릭터에 어태치하고 플레이한다.

Range가 0일 경우 무대 전체를 왔다갔다하면서 벽에 부딪치면 반대방향으로 달립니다. Range를 4로 하면 4만큼의 범위내에서만 움직입니다.

 

주인공 캐릭터의 hp가 0 이하일때는 죽는 애니메이션을 실행하고 나서 Game Over화면으로 넘어가게 된다.  계속 공격하는 몬스터에게 주인공의 죽음을 알릴 필요가 있다. 몬스터는 공격을 멈추고 추적도 정지하게 된다.

씬뷰의 오브젝트에 접근하는 방법중 Tag를 이용하는 방법이 있다.

Tag로 설정된 오브젝트를 하나만 리턴하는 함수와 여러개를 배열로 리턴하는 2가지 함수가 마련되어 있다.

GameObject.FindGameObjectWithTag(string tag);
GameObject.FindGameObjectsWithTag(string tag);

MOSTER라는 태그를 새로 추가하고 Monster의 태그로 지정한다. 없다면 +를 눌러 만든후 지정해 준다.

Player가 사망했을때 호출되는 PlayerCtrl스크립트의 PlayerDie함수에서 모든 몬스터를 찾아 공격 중지 함수를 호출하는 코드를 다음과 같이 추가한다.

PlayerCtrl - PlayerDie()

SendMessage() 함수는 "OnPlayDie()"가 해당 게임오브젝트의 스크립트에 있다면 실행하라는 거다. 두번째 인자는 함수가 없더라도 없다는 메시지를 반환하지 않겠다는 옵션이다. 

void PlayerDie() {
    //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
    GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTERS");
    foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
        monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
    }
}

이제 MonsterCtrl에 - OnPlayerDie()함수를 추가하겠다.

private readonly int hashPlayerDie = Animator.StringToHash("PlayerDie");

void OnPlayerDie() {
    StopAllCoroutines();
    agent.isStopped= true;
    anim.SetTrigger(hashPlayerDie);
}

MonsterAnim을 더블클릭해 애니메이터를 열고 07.animations폴더에서 Monster@Gangnam Style애니메이션 파일을 드래그에 추가한다. 파라미터 +를 누르고 PlayerDie라는 이름의 Trigger를 만든다. Gangnam Style애니메이션은 주인공이 사망하는 경우에 실행할 것이므로 Any State에서 Gangnam Style 스테이트로 Transition을 연결하고 Gangnam Style에서 Exit로 가는 Transition도 연결한다.

 

 

Gangnam Style state를 한번만 클릭해서 인스펙터에서 Multiplier Parameter를 체크하면 자동으로 Speed가 생긴다

MonsterCtrl 스크립터에 아래 코드를 추가한다.

애니메이터의 Speed 변수를 랜덤으로 설정하고 Any->GangnamStyle transition을 트리거 시킨다.

private readonly int hashSpeed = Animator.StringToHash("Speed");

void OnPlayerDie() {
    StopAllCoroutines();
    agent.isStopped= true;
    anim.SetFloat(hashSpeed, Random.Range(0.5f, 1.2f));
    anim.SetTrigger(hashPlayerDie);
}

하이라키의 몬스터를 프로젝트 프리팹 폴더로 끌어와 프리팹을 만든다. 몬스터 프리팹을 끌어다 씬뷰에 여러개 배치한다.

다가가면 몬스터가 공격을 하고 HP가 0이되면 몬스터들이 승리의 강남스타일을 춤춘다.

처음에 잘안되서 디버깅 해보니 몬스트의 TAG는 MONSTER지만 wrist는 PUNCH로 되어 있어야 HP가 떨어진다.

만일 잘못되어 있다면 하이라키의 몬스터가 아니라 이제는 프로젝트 프리팹의 몬스터 프리팹을 수정해야한다.

 

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;  //for Navigation


public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
    }

    public State state = State.IDLE;  //몬스트 현재상태
    public float traceDist = 10.0f;  //추적 사정거리
    public float attackDist = 2.0f; //공격 사정거리
    public bool isDie = false;  //몬스터사망여부

    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    private Transform playerTr;  
    private NavMeshAgent agent;
    private Animator anim;

    public readonly int hashTrace = Animator.StringToHash("IsTrace");
    public readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");
    private readonly int hashPlayerDie = Animator.StringToHash("PlayerDie");
    private readonly int hashSpeed = Animator.StringToHash("Speed");
    private GameObject bloodEffect;

    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        bloodEffect = Resources.Load<GameObject>("GoopSpray");
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
        
    }

    //일정한 간격으로 몬스터의 행동 상태를 체크
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    //몬스터의 상태에 따라 몬스터의 동작을 수행
    IEnumerator MonsterAction() {
        while (!isDie) {
            transform.LookAt(playerTr);
            switch (state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool(hashTrace, true);
                    anim.SetBool(hashAttack, false); //공격파라메터 변경
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool(hashAttack, true);  //공격파라메터 변경
                    break;
                case State.DIE: //사망상태
                    break;

            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            Destroy(coll.gameObject);             //충돌한 총알을 삭제
            anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
            Vector3 pos = coll.GetContact(0).point;
            Quaternion rot = Quaternion.LookRotation(-coll.GetContact(0).normal);
            ShowBloodEffect(pos, rot);  
        }
    }
    void ShowBloodEffect(Vector3 pos, Quaternion rot) {
        GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);
        blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.
        Destroy(blood, 1.0f);
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {

            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
    void OnPlayerDie() {
        Debug.Log("PlayerDie");
        StopAllCoroutines();
        agent.isStopped= true;
        anim.SetFloat(hashSpeed, Random.Range(0.5f, 1.2f));
        anim.SetTrigger(hashPlayerDie);
    }
}

 

+ Recent posts