EasyCastleUNITY

간단 RPG 보스 씬 Test 본문

유니티 기초

간단 RPG 보스 씬 Test

EasyCastleT 2023. 8. 14. 16:28

1. 시야 범위 안에 있으면, 영웅을 향해 움직임

2. 움직이다가 영웅이 사거리 안으로 들어오면 공격, 밖으로 나갈때 까지 계속 공격

3. 영웅이 시야 범위 밖으로 나가면, 보스는 빠른 속도로 원래 위치로 돌아감 

Test Main 컴포넌트
보스 Bull 컴포넌트
영웅 컴포넌트

Bull, 보스를 컨트롤

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

namespace Test_Boss
{
    public class Bull : MonoBehaviour
    {
        private Transform targetTrans;
        private Animator anim;
        private Coroutine moveForwardRoutine;
        private Coroutine moveBackRoutine;
        private Coroutine attackRoutine;
        public System.Action onAttackCancel;
        public System.Action onMoveComplete;
        public System.Action onAttack;
        public System.Action onSightOut; //시야 범위 밖이다. 

        private bool isSightOut = false;
        private float impactTime=0.7f;
        private float distance = 100f; //null 피하기  초기값을 일부러 크게 줘서 시야 범위에 들어가지 않도록 

        [SerializeField]
        private float sight = 5.0f;

        public float Sight
        {
            get { return this.sight; }
        }
        [SerializeField]
        private float range = 1.0f;

        public float Range
        {
            get { return this.range; }
        }

        private void Awake()
        {
            this.anim = GetComponent<Animator>();
        }
        public void MoveForward(Transform targetTrans)
        {
            this.targetTrans = targetTrans;

            
            //코루틴 실행 
            //this.StartCoroutine(this.CoMoveForward());
            if (this.moveForwardRoutine != null)
            {
                this.StopCoroutine(this.moveForwardRoutine);
            }
            this.moveForwardRoutine = this.StartCoroutine(this.CoMoveForward());
        }

        private IEnumerator CoMoveForward()
        {
            //매프레임마다 앞으로 이동 
            while (true)
            {

                if (targetTrans == null)
                {
                    break;
                }

                this.distance = Vector3.Distance(transform.position, targetTrans.position);
                //시야안에 있는지 확인
                if (distance > this.sight) //시야 밖에 있음
                {
                    this.anim.SetInteger("State", 0);
                    this.onSightOut();
                    yield return null;
                    continue;
                }
                else //시야 안에 있음
                {
                    this.anim.SetInteger("State", 1);
                }

                //방향을 바라보고
                this.transform.LookAt(this.targetTrans);
                //이동
                this.transform.Translate(Vector3.forward * 1f * Time.deltaTime);
                Debug.Log(targetTrans);
                //목표지점까지 오면 멈춤

                if (distance <= this.range)
                {
                    break;
                }
                yield return null;
            }
            if (this.targetTrans == null)
            {
                Debug.Log("타겟을 잃었습니다");
                this.anim.SetInteger("State", 0);
            }
            else
            {
                Debug.Log("이동을 완료함");
                this.anim.SetInteger("State", 0);
                this.onMoveComplete();
            }
        }

       public void MoveBack()
        {
            if(this.moveBackRoutine != null)
            {
                this.StopCoroutine(this.moveBackRoutine);
            }
            this.moveBackRoutine =StartCoroutine(CoMoveBack());
        }

        private IEnumerator CoMoveBack()
        {
            //while (true)
            //{
                float dis = Vector3.Distance(this.transform.position, Vector3.zero);
                if (dis > 0.1f)
                {
                    this.anim.SetInteger("State", 1);
                    //방향을 바라보고
                    this.transform.LookAt(Vector3.zero);
                    //이동
                    this.transform.Translate(Vector3.forward * 1.5f * Time.deltaTime);
                }
                else
                {
                    this.anim.SetInteger("State", 0);
                    this.transform.rotation = Quaternion.identity;
                    Debug.Log("<color=yellow>보스, 원래자리로 돌아옴 </color>");
                    //break;
                }
                
                yield return null;
            //}
        }

        public void Attack(Transform targetTrans)
        {
            this.targetTrans = targetTrans;
            if(this.attackRoutine != null)
            {
                this.StopCoroutine(this.attackRoutine);
            }
            this.attackRoutine =StartCoroutine(this.CoAattack());
        }

        private IEnumerator CoAattack()
        {
            yield return null;

            float distance = Vector3.Distance(transform.position, targetTrans.position);

            //시야 안에 있는지 확인
            if (distance <= this.sight)
            {
                if(distance <= this.range * 2)
                {
                    //사거리안에 있다면 공격 애니메이션
                    this.anim.SetInteger("State", 2);
                }

                else
                {
                    this.onAttackCancel(); //공격 취소됨
                }

            }
            
            yield return new WaitForSeconds(this.impactTime);
            Debug.Log("<color=red>Boss Attack Impact!</color>");
            yield return new WaitForSeconds(1.0f-this.impactTime);

            this.onAttack(); //대리자에서 다시 Attack을 부름, //여러번 공격 
            

        }

        private void OnDrawGizmos()
        {
            //시야
            if (this.distance <= this.sight) //시야 안에 있음
            {
                Gizmos.color = Color.red;
                GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.sight, 40);
            }
            else
            {
                Gizmos.color = Color.yellow;
                GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.sight, 40);
            }

            //범위
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.range, 40);
        }
    }

}

HeroController, 영웅 움직임, 재사용이라 조금 지저분함, 여기서는 영웅 움직임만 사용 

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

namespace Test_Boss
{
    public class HeroController : MonoBehaviour
    {
        //이동 구현을 코루틴으로 함
        private Vector3 targetPosition; //타겟이 되는 위치 
        private Coroutine moveRoutine;  //전에 실행하던 코루틴을 저장하는 변수
        private Animator anim; //영웅의 애니메이터 
        [SerializeField]
        private float radius = 1.0f; //영웅의 사거리 

        private MonsterController monsterTarget; //target이 된 몬스터의 MonsterController 컴포넌트
        private ItemController itemTarget; //target이 된 아이템의 ItemController 컴포넌트

        public System.Action<MonsterController> onMoveComplete; //이동이 끝나면 호출하는 대리자 
        private List<ItemController> getItems; //영웅이 획득한 아이템들
        private float impactTime = 0.399f; //실제로 영웅의 애니메이션이 공격하는 시간 

        private int hp = 150; //영웅의 Hp
        private int damage = 1;
        private int armor = 1;
        private int index = 0;

        public int Hp
        {
            get { return hp; }
        }

        public int Damage
        {
            get { return this.damage; }
        }

        public int Armor
        {
            get { return this.armor; }
        }

        public float Radius //사거리 속성
        {
            get
            {
                return this.radius;
            }
        }
        // Start is called before the first frame update
        void Start()
        {
            this.getItems = new List<ItemController>();
            this.anim = this.GetComponent<Animator>();
            Debug.LogFormat("<color=lime>영웅의 체력: {0}</color>", this.hp);
        }
        //목적지가 몬스터가 아닐 때 하는 움직임
        public void Move(Vector3 targetPosition)
        {
            //타겟을 지움
            this.monsterTarget = null;
            this.itemTarget = null;

            //이동할 목표 지점을 저장
            this.targetPosition = targetPosition;
            Debug.Log("Move");

            //이동 애니메이션 실행
            this.anim.SetInteger("State", 1);

            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.anim.SetInteger("State", 1);
            if (this.moveRoutine != null)
            {
                //이미 코루틴이 실행중이다 -> 중지 
                this.StopCoroutine(this.moveRoutine);
            }
            this.moveRoutine = this.StartCoroutine(this.CoMove());
        }
        //목적지가 아이템이면 하는 움직임 
        public void Move(ItemController target)
        {
            this.itemTarget = target;
            this.targetPosition = this.itemTarget.gameObject.transform.position;
            this.anim.SetInteger("State", 1);
            if (this.moveRoutine != null)
            {
                //이미 코루틴이 실행중이다 -> 중지 
                this.StopCoroutine(this.moveRoutine);
            }
            this.moveRoutine = this.StartCoroutine(this.CoMove());
        }
        private IEnumerator CoMove()
        {
            while (true)
            //무한 반복이 되어 유니티가 멈출 수도 있음
            //그러므로 yield return 필수
            {
                Vector3 pos = new Vector3(targetPosition.x, 0, targetPosition.z);
                //방향을 바라봄
                this.transform.LookAt(pos);
                //이미 바라봤으니깐 정면으로 이동 (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 (this.itemTarget != null)
                {
                    if (distance <= 0.5f)
                    {
                        Debug.Log("<color=lime>item log</color>");
                        break;
                    }
                }
                else
                {
                    if (distance <= 0.1f)
                    {
                        //도착
                        break;
                    }
                }
                yield return null; //다음 프레임 시작
            }
            Debug.Log("<color=yellow>도착!</color>");
            this.anim.SetInteger("State", 0);
            //대리자 호출
            //this.onMoveComplete(this.monsterTarget);
        }

        //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(this.damage);

            yield return new WaitForSeconds(animStateInfo.length - this.impactTime);
            //Idle 애니메이션을 함
            //this.anim.SetInteger("State", 0);
            this.anim.SetInteger("State", 0);
        }

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

Test_BossMain, 메인을 총괄

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

namespace Test_Boss
{
    public class Test_BossMain : MonoBehaviour
    {
        [SerializeField]
        private Button btnMove;

        [SerializeField]
        private Button btnRemove;

        [SerializeField]
        private Bull bull;

        [SerializeField]
        private Transform targetTrans;

        [SerializeField]
        private HeroController heroController;
        // Start is called before the first frame update
        void Start()
        {
            this.bull.onAttack = () =>
            {
                this.BullReAttack();
            };
            this.bull.onAttackCancel = () =>
            {
                this.BullMoveAttack();
            };
            this.bull.onMoveComplete = () =>
            {
                this.BullMoveAttack();
            };
            this.bull.onSightOut = () =>{
                this.BullMoveBack();
            };
            //this.btnMove.onClick.AddListener(() => {
            //    Debug.Log("move");
            this.bull.MoveForward(targetTrans);
            //});

            this.btnRemove.onClick.AddListener(() =>
            {
                Debug.Log("remove");
                Destroy(this.targetTrans.gameObject);
            });
        }

        private void BullMoveAttack()
        {
            //공격할지 사거리 계산
            Debug.Log("사거리 계산");
            float dis = Vector3.Distance(this.bull.transform.position, this.targetTrans.position);
            if (dis <= this.bull.Range * 2)
            {
                //공격
                Debug.Log("공격");
                this.bull.Attack(targetTrans);
            }
            else
            {
                //이동
                Debug.Log("이동");
                this.bull.MoveForward(targetTrans);
            }
        }

        private void BullMoveBack()
        {
            float dis = Vector3.Distance(this.bull.transform.position, this.targetTrans.position);
            if (dis > this.bull.Sight)
            {
                this.bull.MoveBack();
            }
        }

        private void BullReAttack()
        {
            this.bull.Attack(targetTrans);
        }


        // Update is called once per frame
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                //ray 생성
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                float maxDistance = 1000f;
                Debug.DrawRay(ray.origin, ray.direction * maxDistance, Color.yellow, 2f);
                RaycastHit hit;
                if(Physics.Raycast(ray,out hit, maxDistance))
                {
                    if (hit.collider.tag == "Ground")
                    {
                        this.heroController.Move(hit.point);
                    }
                }
            }
            
        }
    }
}

시야 범위 안으로 들어오면 이동, 사거리 안으로 들어오면 공격, 시야 밖으로 나가면, 원래위치로 이동

시야에 들어온지 확인하기 위해, 시야 범위안으로 들어오면 시야 기즈모가 빨간색으로 변경됨

 

고칠점

1.공격이, 영웅이 그자리에서 벗어나도 조금 늦게 끝나는 경우가 있음

2. 시야 밖으로 나갔을 때, 한번만 불려도 되는 원래자리로 돌아가는 메서드가, 시야 밖에 있는 경우 계속 불리고 있음 

 

해결법

2-1. sight 밖으로 나간 경우에도, 계속 추적을 하고 있기에 생기 문제였다. 

2-2. 범위 밖으로 나가면 추적을 멈추도록 바꾸자. 

2-3. 범위 밖으로 나간 것을 대리자를 통해 main으로 보내지 말고, Bull 클래스 안에서 자체적으로 판단하도록 만들자.