Rect Transform은 게임오브젝트의 Transform과 같이 위치정보로 UI항목에 기본적으로 추가되어 있다.

GameObject>UI>Panel을  추가한다. 주된용도는 UI항목을 그룹화하는거다

씬뷰 오른쪽 위  2D를 눌러준다 

판넬이 보이게 조정한다.

Panel은 기본적으로 Image컴포넌트가 추가되어 있다

앵커프리셋

앵커프리셋은 Shift, ALT, ALT+SHIFT키의 조합에 따라 여러 모드가 있다.

 

기본앵커프리셋

Panel의 앵커프리셋 옵션은 가로세로가 모두 stretch모드로 되어있다. 판넬의 크기에 자동으로 맞춰진다.

정중앙 정렬, 왼쪽 상단 정렬  왼쪽 상하 리사이즈 정렬 

ALT키 조합의 앵커 프리셋 - 선택된 UI항목을 프리셋으로 이동시켜준다.

SHIFT키 조합의 앵커 프리셋  -  선택된 UI항목의 Pivot위치 변경

ALT+SHIFT - 2가지를 동시에 변경

앵커포지션 속성 - Rect Transform 속성 맨위에 있는 Pos X Y Z는 해당 UI 항목의 앵커 포인트를 기준으로 피벗 좌표가 얼마만큼 떨어져 있는지 나타내는 anchoredPosition이다. 

 

Image Component - Sprite만 사용가능하다.

04.Images/menu폴더에 있는 SF window파일을 Image컴포넌트의 Source Image속성으로 연결한다.

 

다음과 같이 앵커프리셋을 적용한다.

UI-Button을 하나 만든다. Button-Start라고 한다.

크기를 적당히 조정한다.

인스펙터의 Transition을 Animation으로 하고 Auto Generation을 누르면 자동으로 컨트롤러가 만들어진다.

Animator Button컨트롤러를 눌러보면 다음과 같이 애니메이터가 자동으로 생성되어 있다.

이제 마우스가 버튼위로 Roll-Over되었을때 약간 커지는 효과를 만들기 위해 Button을 고른후 Windows-Animation을 누르면 애니메이션클립을 수정할수 있다. 애니메이션뷰가 열리면 Normal에서 Highlighted로 변경한후 녹화버튼을 누른다.

Add Property>Rect Transform>Scale을 누러 추가한다.

스케일을 아래로 펼치고 약간 크게하기 위해 X Y Z를 1.1로 변경후 첫번째 프레임외는 우클릭 Delete를 하고 녹화버튼을 눌러 저장을 한다.

버튼의 이미지도 동그라미를 눌러 Button-Trimmed Image를 선택해 변경한다. 색도 바꿔준다.

버튼을 판넬에 2개더 추가하고 이름을 바꾸고 다음과 같이 배치한다. 버튼의 자식인 텍스트도 적당해 바꾼다.

게임 실행시 첫번째 버튼이 선택되도록 EventSystems의 First Selected 속성에 Button-Start를 끌어다 놓는다.

Button Event

버튼이 눌렸을대 어떤 작업을 할것인지 지정할 수 있다. Button컴포넌트 밑에 +버튼을 이용해 추가할 수 있다.

우선 빈게임오브젝트를 만들고 UIMgr로 이름짓고, UIManager 스크립트를 만든다.

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

public class UIManager : MonoBehaviour
{
    public void OnButtonClick(string msg) {
        Debug.Log($"Click Button : {msg}");
    }
}

Button 컴포넌트에서 +를 클릭후 UIMgr을 끌어다 연결해주고 OnButtonClick()을 선택한다.

위와 같이 Button-Start를 클릭했을때 호출할 함수를 인스펙터뷰에서 연결하는 방법은 귀찮고 제약이 있다.

따라서 스크립트에서 연결해 보겠다. 일단 호출할 함수를 No Function으로 한다.

UIManager 에 버튼의 동작을 정의한다.

특정 이벤트가 발생하면 호출할 함수를 연결하기 위해 AddListner(UnityAction call)함수를 사용한다. 함수를 연결하는 3가지 방법을 각 버튼마다 다르게 적용하였다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;

public class UIManager : MonoBehaviour
{
    public Button startBtn;
    public Button optionBtn;
    public Button stopBtn;
    private UnityAction action;
    // Start is called before the first frame update
    void Start() {
        action = () => OnButtonClick(startBtn.name);
        startBtn.onClick.AddListener(action);
        optionBtn.onClick.AddListener(delegate{ OnButtonClick(optionBtn.name); });
        stopBtn.onClick.AddListener(() => OnButtonClick(stopBtn.name));
    }
    public void OnButtonClick(string msg) {
        Debug.Log($"Click Button : {msg}");
    }
}

 

 

준비

01.Scene 폴더를 선택후 메뉴에서 File>New Scene 선택후 새로운 씬을 생성한다. Basic을 선택하고 Create 버튼을 누른다. 씬의 이름은 Main으로 변경한다. 다운로드한 Resources/Texture폴더의 Menu 폴더를 04.Images폴더로 끌어다 놓는다.

 

Canvas 객체는 Canva 컴포넌트를 포함하고 있는 게임오브젝트의 일종이다. UI항목은 반듯이 이 Canvas 객체의 하위에 있어야 한다. 

GameObject>UI>Canvas를 선택 Canvas를 하나 만든다. 입력정보들을 Canvas에 전달해주는 EventSystem 객체도 자동으로 생성된다. 

EventSystem객체

Canvas 객체의 컴포넌트

 

Canvas 컴포넌트

UI항목을 화면에 배치하고 렌더링하는 역할 Render Mode옵션에 따라 UI항목의 화면 배치방식을 결정할 수 있다

Screen Space - Overlay

기본 설정값으로 최 상위 계층에 표현되고 일반적은 UI구성이며 어떤 3D 객체에 의해서도 가려지지 않는다. 화면 해상도에 맞춰 자동 조정된다.

 

Screen Space - Camera

UI 항목을 렌더링하는 별도의 카메라 설정 메뉴가 나타난다. 기본 Main Camera와의 충돌이 없도록 Clear Flag, Culling Mask, Depth속성을 적절히 설정해야 한다.

 

World Space

특정 게임 오브젝트에 Canvas객체를 추가하면 더는 Rect Transform의 영향을 받지 않으며 해당 게임 오브젝트의 위치에 영향을 받는다. 

유니티에서 제공하는 UI는 3가지가 있다.

  • IMGUI (Immediate Made GUI) : 코드를 이용해서 UI를 표시하는 방법
  • UI Toolkit : 현재 개방중, 속성을 수치로 관리
  • UNIT UI (UGUI) : 게임 오브젝트 기반의 UI, UI요소를 컴포넌트로 구현

 

주인공이 죽으면  PlayerDie()::PlayerCtrl 함수에서 씬뷰의 모든 몬스터들을 찾아 SendMessage를 이용 하나하나 메세지를 보냈다. 만일 적캐릭터가 엄청 많다면 그닥 효율적인 방법이 아니다.

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

순차적 호출 방식을 이벤트 구동방식으로 변경하자. 주인공이 죽었다는 이벤트를 시스템이 통보해주는 방식이다.

 

델리게이트

델리게이트는 함수(메서드)를 참조하는 변수를 의미한다. C++의 함수 포인터와 같은 의미이다. 사용하기전 아래와 같이 델리게이션 타일을 선언후 델리게이트타입을 이용 변수를 만들고 함수와 연결후 사용해야 한다.

using UnityEngine;

public class DelegateDemo : MonoBehavior
{
	//델리게이트 타입선언
	delegate float SumHandler(float a, float b);
	//델리게이트타입 델리게이트변수
	SumHandler sumHandler;
	//덧셈연산을 하는 함수
	float sum(float a, float b) {
		return a + b;
	}
	void Start() {
		sumHandler = sum;  //델리게이트변수에 함수를 연결
		float sum = sumHandler(10.0f, 5.0f);
		Dubug.Log($"Sum = {sum}"};
	}
}

 

주인공의 사망 이벤트 처리

PlayerCtrl() 함수내 델리게이트와 이벤트를 선언한다.

public delegate void PlayerDieHandler();  //델리게이트타입 선언
public static event PlayerDieHandler OnPlayerDie; //델리게이트 변수선언

PlayerCtrl() 함수내  PlayerDie()함수 내용을 지우고 OnPlayerDie()함수로 이벤트를 콜한다

void PlayerDie() {
    //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
    /*
    GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
    foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
        monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
    }
    */
    OnPlayerDie();  //주인공 사망 이벤트 호출(발생)
}

이제 MonsterCtrl를 변경한다. 우선 애니메이터의 "Die" 파라미터 해시값을 추출해 놓는다.

몬스트의 생명치를 저장할 hp변수를 만들어 놓는다.

 private readonly int hashDie = Animator.StringToHash("Die"); //파라미터해시값 추출
    
 private int hp = 100; //몬스터 생명점수

이제 MonsterCtrl에서 발생하는 이벤트에 반응할 OnPlayerDie()함수를 Player.OnPlayerDie와 연결한다.

PlayerCtrl는 스크립트이름인데 선언없이 사용가능하다. 교재는 PlayerCtrl이다. 변경하려고 했는데 자잘한 에러때문에 그냥 쓴다.

이벤트 스크립트는 반드시 스크립트의 활성화 시점에 연결하고 비활성화될때 해제해야 한다. 

MonsterCtrl Start()함수 앞에 OnEnable() OnDisable()함수를 추가한다.

    private int hp = 20; //몬스터 생명점수
    private void OnEnable() {  //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie; //교재는 PlayerCtrl.
    }
    private void OnDisable() { //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie -= this.OnPlayerDie; //교재는 PlayerCtrl.
    }

 

몬스터의 사망처리

CheckMonsterState()에 state.DIE일 경우 코루틴을 빠져나가는 조건문을 추가한다.

IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            if(state == State.DIE) yield break;  //몬스터 상태가 DIE면 코루틴 종료

OnCollisionEnter()함수에 충돌이 났을때 hp를 -10을 감소시키고 state를 DIE로 바꿔준다.

    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
			//중략
            hp -= 10;
            if(hp <= 0) state = State.DIE;
        }
    }

state가 DIE로 변경되면 MonsterAction()코루틴에서 "DIE" 트리거가 일어나고 Collider가 비활성화돼 죽은 몬스터에 총을 발사했을때 혈흔이 일어나지 않는다.

IEnumerator MonsterAction() {
    while (!isDie) {
            //중략
            case State.DIE: //사망상태
                isDie= true;  
                agent.isStopped = true;  //추적 정지
                anim.SetTrigger(hashDie); // 애니메이트 hasDie 트리거발생
                GetComponent<CapsuleCollider>().enabled = false;  //충돌컴포넌트 비활성화
                break;
        }
        yield return new WaitForSeconds(0.3f);
    }
}

Monster의 die 애니메이션 클립을 MonsterAnim 애니메이터뷰에 추가하고 Any State->Die State로 Transition을 만든다. 조건으로 사용할 파라미터는 Trigger타입으로 die를 추가하고 설정한다. Die State는 마지막 스테이트인 Exit스테이트로 연결하고 각 Transition의 연결조건은 다음과 같다.

 

 

 

실행해보면 죽은 몬스터가 떠 있다. 0.5 모델/Monster폴더의 Monster의 Die 애니메이션의 프리뷰를 봐도 떠 있다.

05.Models>Monster>Monster모델 원본을 눌러 인스펙터에서

Animation 탭을 눌러 die애니메이션을 골라 프리뷰를 봐도 공중에 떠 있다. 몬스터 아래 원이 피봇위치이다. 피봇위치는 Transform.position이다

아래쪽 Root Transform Position(Y)의 Bake Into Pose를 체크하던지 Based Upon값을 Feed로 설정하면  Pivot좌표값이 발위치로 조정된다.

실행하려면 방금 몬스터 모델 원형을 변경했기 때문에 Apply를 물어보면 적용시킨다. 실행해보면 잘된다.

PlayerCtrl.cs
0.00MB
MonsterCtrl.cs
0.01MB

전체파일을 원하시는 분은 출판사의 깃허브에서 다운받으시길.

https://github.com/IndieGameMaker/SpaceShooter2021/releases/tag/6%EC%9E%A5

 

Release 절대강좌! 유니티 - 6장까지 완료된 프로젝트 · IndieGameMaker/SpaceShooter2021

06장 - 적캐릭터 제작 이번 장에서는 유한상태머신을 이용히 적 캐릭터의 인공지능을 구현하고 유연한 애니메이션을 위한 메카님 애니메이션 시스템에 대해 실 습합니다. 또한, 내비게이션 시스

github.com

 

주인공 캐릭터의 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);
    }
}

 

보통의 인체형 모델은 애미메이션을 수행하기 위해 Rig(본 구조, 뼈대)가 설정돼 있다.적 캐릭터 모델도 이러한 관절이 설정돼 있으며 하리라키 뷰에서 확인할 수 있다. 하이라키 뷰상에 표시된 각 관절은 게임오브젝트이며 모드 Transform 컴포넌트를 가지고 있다. 이렇게 하나의 3D모델안의 많은 Transform연산은 로드가 높으므로 최적화가 필요하다. 필요한것만 두고 다른것들은 노출되지 않게 하면 속도 향상에 도움을 줄 수 있다.

몬스터 모델에서 필요한 Rig는 Sphere Collider와 Rigidbody를 추가한 양손이므로 이부분만을 하이라키 뷰에 노출되게 설정해보자. 05.Models/Monster폴더에 있는 Monster원본 모델을 선택후 인스펙터뷰 Rig  탭의 Optimize Game Objects 옵션을 선택하면 하단에 Extra Transforms to Exppose 항목이 나타난다.

 L_wrist와 R_wrist만 선택하고 Apply를 누르면 하이라키뷰의 Monster가 정리된걸 볼 수 있다.

)

본 구조 최적화는 프리팹으로 만들기 전에 해야한다. 하이라키뷰의 Monster는 아직 프리팹으로 만들기 전이므로 원본 Monster모델을 선택하고 본 구조를 최적화 할 수 있었다. 

몬스터 본체의 Capsule Collider와 양손의 Sphere Collider는 걸을때 마다 충돌을 일으킨다. 이를 막기위해 레이어로 분리한다.

인스펙터뷰의 레이어를 누르고 +를 눌러 MONSTER_BODY, MONSTER_PUNCH라는 2개의 레이어를 추가한다.

우선 하이라키뷰의 Monster를 눌러  MONSTER_BODY로 레이어를 선택하면 

하위 하이라키도 바꿀꺼냐고 물어보면 No를 대답한다. L_wrist와 R_Wrist를 눌러 MONSTER_PUNCH로 레이어를 바꾼다.

 

메뉴>Edit>ProjectSetting>Phsics 맨 아래 Layer Collision Matrix 설정에서  MONSTER_BODY와 MONSTER_PUNCH가 XY로 교차하는 부분의 체크박스를 언체크하면 두 레이어간의 물리적인 충돌은 물리엔진에서 처리하지 않는다.

몬스터가 플레이어와 일정 거리 이내로 좁혀지면 공격 애니메이션을 수행한다. 이때 플레이어의 생명력이 줄어드는 코드를 짜보자.

방법은 collider를 통해 처리하는 방법과 주기적으로 데미지를 주는 방법이 있다. 몬스터는 공격 상태가 되면 양손으로 타격하는 애니메이션을 실행하므로 전자의 방식으로 구현해보자.

하이라키뷰에서 L_wrist, R_wrist를 선택한후 다음 속성을 변경하면 동시에 적용된다.

물리적 충격을 주가위해 몬스터의 양속에 Collider와  Rigidbody컴포넌트를 추가한다. 

몬스터본체 Capsule Collider와의 간섭을 없애기 위해 Collider의 Is Trigger check, 움직임은 애니메이션에서 컨트롤 하므로 물리엔진인 Rigidbody의 Is Kinematic check한다.

Tag에서 PUNCH를 만들고 다시 L_wrist, R_wrist를 선택한후 TAG를 PUNCH로 바꿔준다.

주인공 Player에도 Capsule Collider를 추가해 다음과 같이 Center (0,1,0), Height:2을 설정한다. 

Rigidbody를 추가하고 FreezeRotation x,z를 check한다. 이러면 넘어지지 않는다

보통 FPS나 TPS게임을 개발할때 주인공은 character Controller컴포넌트를 사용한다. collider+rigidbody에 물리엔진을 disable해서 떨림현상이나 미림현상을 방지할 수 있다

 

OnTriggerEnter 콜백 함수

몬스터 양손에 Sphere Collider에서 IsTrigger를 체크했기때문에 OnTrigger계열의 콜백함수가 호출된다.

 

PlayerCtrl에 OnTriggerEnter()를 추가한다.

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    

    Transform tr;
    private Animation anim;

    private float moveSpeed = 10f; //이동속도
    private float turnSpeed = 0f;  //회전속도

    private readonly float initHP = 100.0f;  //초기 생명값
    public float curHP; //현재생명값

    IEnumerator  Start() {  //start()함수는 코루틴으로 실행할 수 있다.
        curHP = initHP;
        tr = GetComponent<Transform>();
        anim = GetComponent<Animation>();  //추가된 코드
        anim.Play("Idle");
        turnSpeed = 0.0f;  //프로그램 기동시 가비지 마우스값을 막기위해
        yield return new WaitForSeconds(0.3f);  //0.3초 기다린후
        turnSpeed = 100f;  //변수값을 설정
    }

    // Update is called once per frame
    void Update() {
        float h = Input.GetAxis("Horizontal");  //AD 입력 좌우
        float v = Input.GetAxis("Vertical");  //WS 입력 전후
        if (Mathf.Abs(h) > float.Epsilon || Mathf.Abs(v) > float.Epsilon) {  //움직임이 없다면 불필요한 동작을 안한다.
            Vector3 dir = Vector3.right * h + Vector3.forward * v;
            tr.Translate(dir.normalized * moveSpeed * Time.deltaTime);
            PlayerAnim(h, v);
        }
        float r = Input.GetAxis("Mouse X");  //마우스 x축 입력 
        tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
    }
    void PlayerAnim(float h, float v) {
        GetComponent<NavCtrl>().SetNavStop();
        if (v >= 0.1f) {  //앞으로 달림
            anim.CrossFade("RunF",0.25f);
        } else if(v <= -0.1f) { //뒤로 움직임
            anim.CrossFade("RunB", 0.25f);
        } else if(h >= 0.1f) { //오른쪽으로 움직임
            anim.CrossFade("RunR", 0.25f);
        } else if (h <= -0.1f) { //왼쪽으로 움직임
            anim.CrossFade("RunL", 0.25f);
        } else { //제자리대기
            anim.CrossFade("Idle", 0.25f);
        }
    }
    void OnTriggerEnter(Collider coll) {
        if(curHP >= 0.0f && coll.CompareTag("PUNCH")) {
            curHP -= 10.0f;
            Debug.Log($"Player hp = { curHP / initHP }");
            if (curHP <= 0.0f) {
                PlayerDie();
            }
        }
    }
    void PlayerDie() {
        Debug.Log("Player Die !");
    }
}

실행하다보니 플레이어를 마우스로 좌우로 회전 컨트롤 하는게 잘 안된다. 

처음에는 마우스로 우클릭을 하면 그 지점을 바라보는 걸 할려고 했다.

물론 스크린좌표를 월드좌표로 변환해서 레이를 쏴서 그 오브젝트를 바라보게해도 되는데 간단하게 LookAt()을 이용해보겠다.

스크립트를 하나 만들고  RightClick으로 이름 짓는다. 스크립트를 Player에 적용한다.

Monster 인스펙터  TAG가 MONSTER로 되어 있어야 한다.

FindwidhtTag()로 간단히 찾아 monsterTR에 저장하고 LookAt()을 이용해 방향을 바꾼다. 

다만. 순간적으로 바꿔지기 때문에 부드럽게 바꾸려면 다른 함수를 써야 한다. 

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

public class RightClick : MonoBehaviour
{
    // Start is called before the first frame update
    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    void Start()
    {
        monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            transform.LookAt(monsterTr);
        }
    }
}

다른 방법은  어차피 NavMesh가 적에 적용되어 있으니 그걸 응용해보기로 했다. 목적은 자동추적보다는 플레이어의 자동 회전에 가깝다. 일단 하이리키 Player에 NavMesh Agent를 추가한다.

교재의 프로젝트에 영향을 주지 않게 하기 위해 따로 스크립트도 추가하고 NavCtrl로 이름짓는다.

코드는 간단하다. 마우스 우클릭을 하면 Player가 자동으로 적을 따라가고 다시 우클릭을 하면 멈춘다.

다른 스크립트에도 Navigation을 멈추게 하기 위해  SetNavStop()을 추가한다.

    public void SetNavStop() {
        agent.isStopped = true;
    }

추가된 NavCtrl 전체코드, 대부분 MonsterCtrl에서 NavMesh에 관련된 코드를 복붙했다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;  //for Navigation
public class NavCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    private NavMeshAgent agent;
    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.isStopped = true; //추적을 중지
        monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            if (agent.isStopped == true) {
                agent.SetDestination(monsterTr.position);  //목표설정
                agent.isStopped = false; //추적을 개시
            } else {
                agent.isStopped = true; //추적을 중지
            }
        }
    }
    public void SetNavStop() {
        agent.isStopped = true;
    }
}

WASD키를 눌렀을때 Navigation을 멈추기 위해 다음 코드를 PlayerCtrl스크립트 PlayerAnim()  맨윗에 추가한다.

 GetComponent<NavCtrl>().SetNavStop();로 NavCtrl의 SetNavStop에 접근할 수 있다. 스크립트 이름이 class이름이고 이걸 GetComponet<T>의 인수로 바로 사용할 수 있다.

void PlayerAnim(float h, float v) {
        GetComponent<NavCtrl>().SetNavStop();

좌클릭을 하면 총이 나갈때도 네비게이션을 멈추게 해야하는데 다행이 멈춘다 . 이유는 각자 찾아보세요. ㅎㅎ 아마 애니메이터의 State가 변하면서 뭔가 바뀌는듯.

주인공 캐릭터(Player)가 쏜 총알이 몬스터에 명중하면 몬스터가 피를 흘리는 혈흔 효과(Blood Effect)를 추가해보자. 혈은 효과는 파티클로 구현하며 프로젝트뷰의 03.Prefabs/EffecExamples/Blood/Prefabs폴더에 있는 BloodSprayeffect를 사용한다.  버전의 차이인지 발견하지 못해서 아래 EffectExamples > Goop Effects > Prefabs> GoopSpray를 사용하였다.

Resources 폴더

지금까지는 프리팹을 사용하기 위해 public GameObject xxx를 선언하고 인스펙터에서 연결해놓고 사용하였다. 이번에는 스크립트에서 런모드(동적)으로 로드하는 방법을 사용한다. 이방법을 쓰기 위해서는 프리팹이나 에셋이 Resouces 폴더안에 있어야 한다. 

 Assets root에 Resources 폴더를 하나 만들자. BloodSprayeffect나 GoopSpray를 끌어다 놓는다. 이 Resources폴더는 빌드할때 포함되므로 필요한 것들만 놓기 바랍니다.

MonsterCtrl 스크립트에 다음과 같이 추가한다.  Goopspray효과는 피튀는게 좀 작아서 localScale을 변경해 크게했다

  • blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.

효과의 인스턴스를 만드는데 인수가 4개이다 마지막 monsterTr의 Child로 만들겠다는 뜻이다.

  • GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);

이렇게 child로 만들면 Monster가 총알을 맞은뒤 이동해도 혈흔이  따라다닌다.

private GameObject bloodEffect;
void Start(){
	bloodEffect = Resources.Load<GameObject>("GoopSpray");
}
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);
}

MonsterCtrl 전체코드

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 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) {
            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);
        }
    }
}

+ Recent posts