EasyCastleUNITY

간단 RPG Game Main (몬스터가 전부 죽으면, 포탈 생성) 본문

유니티 기초

간단 RPG Game Main (몬스터가 전부 죽으면, 포탈 생성)

EasyCastleT 2023. 8. 9. 22:31

앞에 포스팅에서 몬스터가 죽으면 포탈이 생성되는 것을 추가하였다.

몬스터에 한 번 도착하면, 죽을 때 까지 공격하는 것은 아직 이해하고 연습이 부족한지 구현하지 못했다. 

-> 이를 통해 내가 아직 코루틴을 사용하는 방법에 대해 미숙하다는 것을 알게 되었다. 

GameMain

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

public class GameMain : MonoBehaviour
{
    [SerializeField]
    private HeroController heroController; //영웅 컨트롤러 
    [SerializeField]
    private MonsterController monsterController1; //몬스터 컨트롤러
    [SerializeField]
    private MonsterController monsterController2; //몬스터 컨트롤러
    [SerializeField]
    private GameObject hitFxPrefab; //피격 이펙트 
    [SerializeField]
    private GameObject gravePrefab;
    [SerializeField]
    private GameObject TurtleHp;
    [SerializeField]
    private GameObject ChestHp;
    [SerializeField]
    private GameObject PortalPrefab;
    public int monsterCount;
    private bool isCreate = false;
    // Start is called before the first frame update
    void Start()
    {
        Debug.LogFormat("monsterCount: {0}", monsterCount);
        //영웅 이동에 대한 대리자 익명 메서드 
        this.heroController.onMoveComplete = (target) =>
        {
            Debug.Log("<color=cyan>이동을 완료 했습니다.</color>");
            //타겟이 있다면 공격 애니메이션 실행
            if (target != null )
            {
                this.heroController.Attack(target);
            }
        };
        //몬스터가 공격을 받으면 실행하는 익명 메서드
        this.monsterController1.onHit = () => {
            Debug.Log("이펙트 생성");
            Vector3 offset = new Vector3(0, 0.5f, 0);
            Vector3 tpos = this.monsterController1.transform.position + offset;

            Debug.LogFormat("이펙트 생성위치: {0}", tpos);
            //프리팹 인스턴스 생성
            GameObject fxGo = Instantiate(this.hitFxPrefab);
            //위치 설정
            fxGo.transform.position = tpos;
            //파티클 실행
            fxGo.GetComponent<ParticleSystem>().Play();
        };
        this.monsterController2.onHit = () => {
            Debug.Log("이펙트 생성");
            Vector3 offset = new Vector3(0, 0.5f, 0);
            Vector3 tpos = this.monsterController2.transform.position + offset;

            Debug.LogFormat("이펙트 생성위치: {0}", tpos);
            //프리팹 인스턴스 생성
            GameObject fxGo = Instantiate(this.hitFxPrefab);
            //위치 설정
            fxGo.transform.position = tpos;
            //파티클 실행
            fxGo.GetComponent<ParticleSystem>().Play();
        };
        //몬스터가 공격을 받으면 해당 위치에 무덤 생성 
        this.monsterController1.onDie = () =>{
            Debug.LogFormat("{0} 사망",monsterController1.gameObject.name);
            Debug.Log("무덤 생성");
            Vector3 tpos = this.monsterController1.transform.position;
            Debug.LogFormat("무덤 생성위치: {0}", tpos);
            //프리팹 인스턴스 생성
            GameObject graveGo = Instantiate(this.gravePrefab);
            //위치 설정
            graveGo.transform.position = tpos;
            Destroy(monsterController1.gameObject);
            this.monsterCount--;
            Debug.LogFormat("monsterCount: {0}", monsterCount);
        };
        this.monsterController2.onDie = () => {
            Debug.LogFormat("{0} 사망", monsterController2.gameObject.name);
            Debug.Log("무덤 생성");
            Vector3 tpos = this.monsterController2.transform.position;
            Debug.LogFormat("무덤 생성위치: {0}", tpos);
            //프리팹 인스턴스 생성
            GameObject graveGo = Instantiate(this.gravePrefab);
            //위치 설정
            graveGo.transform.position = tpos;
            Destroy(monsterController2.gameObject);
            this.monsterCount--;
            Debug.LogFormat("monsterCount: {0}", monsterCount);
        };
    }

    //사거리 계산
    private bool isWithinRange(float distance, float radius)
    {
        return radius >= distance; //true
    }

    // Update is called once per frame
    void Update()
    {
        //화면을 클릭하면 클릭한 위치로 영웅이 이동
        if(Input.GetMouseButtonDown(0))
        {
            //Ray 생성
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float maxDistance = 1000f; //Ray의 최고 길이
            //화면에 Ray를 출력
            Debug.DrawRay(ray.origin, ray.direction * maxDistance , Color.yellow ,2f);
            //충돌검사 
            RaycastHit hit; //out 매개변수를 쓰려면 미리 정의를 해야된다. 
            if(Physics.Raycast(ray,out hit, maxDistance))
            {
                //클릭한 오브젝트가 몬스터라면
                if (hit.collider.tag == "Monster")
                {
                    //거리를 구한다
                    float distance = Vector3.Distance(this.heroController.gameObject.transform.position,
                        hit.collider.gameObject.transform.position);

                    MonsterController monsterController =
                        hit.collider.gameObject.GetComponent<MonsterController>();
                    //각 반지름 더한거와 비교
                    float sumRadius = this.heroController.Radius + monsterController.Radius;

                    Debug.LogFormat("<color=green>distance:{0}, sumRadius:{1}</color>",
                        distance, sumRadius);
                    //사거리 안에 들어옴
                    if (distance <= sumRadius)
                    {
                        //공격

                    }
                    else
                    {
                        //이동
                        this.heroController.Move(monsterController);
                    }
                }
                else if (hit.collider.tag == "Ground")
                {
                    //충돌정보가 hit 변수에 담김
                    Debug.Log(hit.point); //월드 상의 충돌 지점 위치 
                                          //Hero Gameobject 이동
                    this.heroController.Move(hit.point);
                }
            }
        }
        Text text1 = TurtleHp.GetComponent<Text>();
        Text text2 = ChestHp.GetComponent<Text>();
        text1.text = string.Format("TurtleHp: {0}",monsterController1.Hp);
        text2.text = string.Format("ChestHp: {0}",monsterController2.Hp);

        if(monsterCount <= 0 && isCreate==false)
        {
            GameObject portalGo = Instantiate(this.PortalPrefab);
            portalGo.transform.position = new Vector3(-2.0f, 0, -3.5f);
            this.isCreate = true;

        }
    }
}

HeroController

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

//Game
public class HeroController : MonoBehaviour
{
    public enum eState
    {
        Idle,Run,Attack
    }
    //이동 구현을 코루틴으로 함
    private Vector3 targetPosition; //타겟이 되는 위치 
    private Coroutine moveRoutine;  //전에 실행하던 코루틴을 저장하는 변수
    private Animator anim; //영웅의 애니메이터 
    [SerializeField]
    private float radius = 1.0f; //영웅의 사거리 
    private MonsterController monsterTarget; //target이 된 몬스터의 MonsterController 컴포넌트 
    public System.Action<MonsterController> onMoveComplete; //대리자 
    private eState state;
    private float impactTime = 0.399f; //실제로 영웅의 애니메이션이 공격하는 시간 

    public float Radius //사거리 속성
    {
        get
        {
            return this.radius;
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        this.anim = this.GetComponent<Animator>();
    }
    //목적지가 몬스터가 아닐 때 하는 움직임
    public void Move(Vector3 targetPosition)
    {
        //타겟을 지움
        this.monsterTarget = null;
        //이동할 목표 지점을 저장
        this.targetPosition = targetPosition;
        Debug.Log("Move");

        //이동 애니메이션 실행
        this.PlayAnimation(eState.Run);

        if (this.moveRoutine != null)
        {
            //이미 코루틴이 실행중이다 -> 중지
            this.StopCoroutine(this.moveRoutine);
        }
        this.moveRoutine = StartCoroutine(this.CoMove());

    }
    //목적지가 몬스터이면 하는 움직임 
    public void Move(MonsterController target)
    {
        this.monsterTarget = target;
        this.targetPosition = this.monsterTarget.gameObject.transform.position;
        this.PlayAnimation(eState.Run);
        if (this.moveRoutine != null)
        {
            //이미 코루틴이 실행중이다 -> 중지 
            this.StopCoroutine(this.moveRoutine);
        }
        this.moveRoutine = this.StartCoroutine(this.CoMove());
    }
    private IEnumerator CoMove()
    {
        while (true)
        //무한 반복이 되어 유니티가 멈출 수도 있음
        //그러므로 yield return 필수
        {
            //방향을 바라봄
            this.transform.LookAt(targetPosition);
            //이미 바라봤으니깐 정면으로 이동 (relateTo: Self/지역좌표)
            //방향 * 속도 * 시간 
            this.transform.Translate(Vector3.forward * 2f * Time.deltaTime);
            //목표지점과 나 사이의 거리를 계산, 즉 1프레임 마다 거리를 계산 
            float distance = Vector3.Distance(this.transform.position, this.targetPosition);
            //타겟이 있을 경우
            if (this.monsterTarget != null)
            {
                if (distance <= (1f + 1f))
                {
                    break;
                }
            }
            else
            {
                if (distance <= 0.1f)
                {
                    //도착
                    break;
                }
            }
            yield return null; //다음 프레임 시작
        }
        Debug.Log("<color=yellow>도착!</color>");
        this.PlayAnimation(eState.Idle);
        //대리자 호출
        this.onMoveComplete(this.monsterTarget);
    }
    public void Attack(MonsterController target)
    {
        this.transform.LookAt(target.transform.position);
        this.monsterTarget = target;
        //공격애니메이션을 1회 실행
        this.PlayAnimation(eState.Attack);
        //Debug.LogError("Stop");
    }

    private void PlayAnimation(eState state)
    {
        //this.state : prev 현재, state: current: 미래
        //중복막기
        if (this.state != state)
        {
            Debug.LogFormat("State: {0} -> {1}", this.state, state);
            this.state = state;
            this.anim.SetInteger("State", (int)state);

            switch (state)
            {
                case eState.Attack:

                    this.StartCoroutine(this.WaitForCompleteAttackAnimation());
                    break;
            }
        }
        else
        {
            Debug.LogFormat("{0}은 현재와 동일한 상태입니다.", state);
        }


    }
    //코루틴 함수
    //Update와 동일하게 동작 가능 
    private IEnumerator WaitForCompleteAttackAnimation()
    {
        yield return null; //1프레임 건너뜀

        Debug.Log("공격 애니메이션이 끝날때까지 기다림");
        AnimatorStateInfo animStateInfo = this.anim.GetCurrentAnimatorStateInfo(0);
        //실제 애니메이션 이름으로 찾아야 한다
        bool isAttackState = animStateInfo.IsName("Attack01");
        Debug.LogFormat("isAttackState: {0}", isAttackState);
        if (isAttackState)
        {
            Debug.LogFormat("Hero's animStateInfo.length: {0}", animStateInfo.length);
        }
        else
        {
            Debug.LogFormat("Attack State가 아닙니다");
        }

        yield return new WaitForSeconds(this.impactTime);
        Debug.Log("<color=red>Impact!!!!</color>");
        //대상에게 피해를 입힘
        this.monsterTarget.HitDamage();

        yield return new WaitForSeconds(animStateInfo.length - this.impactTime);
        //Idle 애니메이션을 함
        //this.anim.SetInteger("State", 0);
        this.PlayAnimation(eState.Idle);
    }
    private void OnDrawGizmos()
    {
        GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, 1, 40);
    }
}

MonsterController

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

public class MonsterController : MonoBehaviour
{
    public enum eMonsterState
    {
        Idle,GetHit,Die
    }
    [SerializeField]
    private float radius = 1.0f; //몬스터의 사거리 
    [SerializeField]
    private int hp = 10;

    private Animator anim; //몬스터 애니메이터
    private eMonsterState monsterState; //enum 
    private float hitTime = 0.01f; //애니메이션 실행 시간 

    public System.Action onHit; //몬스터가 hit되면 메서드를 넘기는 대리자 
    public System.Action onDie; //몬스터가 죽으면 메서드를 넘기는 대리자

    public float Radius //사거리 속성
    {
        get { return this.radius; }    
    }

    public int Hp //hp 속성 
    {
        get { return this.hp; }
    }
    // Start is called before the first frame update
    void Start()
    {
       this.anim=GetComponent<Animator>();
        GameObject gameMainGo = GameObject.Find("GameMain");
        gameMainGo.GetComponent<GameMain>().monsterCount++;
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void HitDamage()
    {
        //피격애니메이션을 1회 실행
        //this.PlayAnimation(eMonsterState.GetHit);
        if(this.hp>0)
        this.PlayAnimation(eMonsterState.GetHit);
        this.hp--;
        this.onHit();
        if (this.hp <= 0)
        {
            this.hp = 0;
            this.anim.SetInteger("MonsterState",(int)eMonsterState.Die);
            this.onDie(); 
        }
    }

    private void PlayAnimation(eMonsterState monsterState)
    {
        //중복막기
        if (this.monsterState != monsterState)
        {
            Debug.LogFormat("Monster: {0} -> {1}", this.monsterState, monsterState);
            this.monsterState = monsterState;
            this.anim.SetInteger("MonsterState", (int)monsterState);

            switch (monsterState)
            {
                case eMonsterState.GetHit:

                    this.StartCoroutine(this.WaitForCompleteGetHitAnimation());
                    break;
            }
        }
        else
        {
            Debug.LogFormat("{0}은 현재와 동일한 상태입니다.", monsterState);
        }
    }

    //코루틴
    private IEnumerator WaitForCompleteGetHitAnimation()
    {
        yield return null;

        Debug.Log("피격 애니메이션이 끝날때까지 기다림");
        AnimatorStateInfo animStateInfo = this.anim.GetCurrentAnimatorStateInfo(0);
        //실제 애니메이션 이름으로 찾아야 한다
        bool isGetHit = animStateInfo.IsName("GetHit");
        Debug.LogFormat("isGetHit: {0}", isGetHit);
        if (isGetHit)
        {
            Debug.LogFormat("Monster's animStateInfo.length: {0}", animStateInfo.length);
        }
        else
        {
            Debug.LogFormat("GetHit State가 아닙니다");
        }
        yield return new WaitForSeconds(this.hitTime);
        Debug.Log("<color=red>Hit!!!!!</color>");

        yield return new WaitForSeconds(animStateInfo.length - this.hitTime);
        
        this.PlayAnimation(eMonsterState.Idle);

    }

    //이벤트 함수 
    private void OnDrawGizmos()
    {
        GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, 1, 40);
    }
}

https://youtu.be/i8amy_VdMW8

 

'유니티 기초' 카테고리의 다른 글

간단 RPG Test3 (불완전한 장착)  (0) 2023.08.10
간단 RPG Test 3-1  (0) 2023.08.10
간단 RPG 게임 메인(수정중)  (0) 2023.08.09
간단 RPG Test2-2  (0) 2023.08.09
간단 RPG Test2-1  (0) 2023.08.09