EasyCastleUNITY

[과제]Hero Shooter Stage1 까지 본문

유니티 심화

[과제]Hero Shooter Stage1 까지

EasyCastleT 2023. 8. 27. 00:35

Tutorial

플레이어가 목표위치에 도달하면, 문이 열리는 애니메이션과 함께, 포탈이 생성된다.

포탈에 다가가면, 화면 fadeout/in과 함께 다음 씬인 StageOne으로 이동한다. 

튜토리얼 씬의 계층구조

튜토리얼 씬을 제어하는 TutorialMain이 있고, 이를 통해 제어를 한다. 

TutorialMain

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using static UnityEngine.GraphicsBuffer;

public class TutorialMain : MonoBehaviour
{
    [SerializeField]
    private PortalGenerator portalGenerator;
    [SerializeField]
    private PlayerController playerController;
    [SerializeField] private GameObject door;
    [SerializeField] private Transform target;
    [SerializeField] private GameObject fadeOut;

    private Transform doorTransform;

    private GameObject portal;

    public GameObject Portal
    {
        get { return this.portal; }
    }

    // Start is called before the first frame update
    void Start()
    {
        this.doorTransform = this.door.transform;
        this.playerController.onPortal = () => {
            Debug.Log("<color=yellow>포탈위치에 도착</color>");
            //Debug.LogError("!");//error pause용
            this.fadeOut.SetActive(true);
            //씬전환
        };
        this.playerController.onTarget = () => {
            Debug.Log("<color=yellow>목표위치에 도착</color>");
            Destroy(target.gameObject);

            if (this.door != null)
            {
                this.doorTransform.position = new Vector3(this.door.transform.position.x, -0.24f, this.door.transform.position.z);
                this.portal = this.portalGenerator.PortalGenerate(this.doorTransform);
                this.playerController.Portal = this.portal; //포탈 설정
                this.door.GetComponent<DoorController>().OpenDoor();
            }
        };
    }
}

플레이어가 타겟 위치에 도착하면, onTarget 대리자를 통해, 타겟에 도착했다는 것을 Main에 알려준다. 

플레이어가 포탈 근처에 도착하면, onPortal 대리자를 통해, 포탈에 도착했다는 것을 Main에 알려준다. 

포탈에 도착하면, fadeOut 게임오브젝트가 활성화되며, 화면이 fadeOut이 된다. 

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

public class FadeOutMain : MonoBehaviour
{
    [SerializeField] private Image dim;

    private System.Action onFadeOutComplete;
    // Start is called before the first frame update
    void Start()
    {
        Image image =this.dim.GetComponent<Image>();
        image.enabled = true;
        this.onFadeOutComplete = () => {
            SceneManager.LoadScene("StageOne");
        };
        this.StartCoroutine(this.FadeOut());
    }

    private IEnumerator FadeOut()
    {
        Color color = this.dim.color;
        //어두어짐
        while(true)
        {
            color.a += 0.01f;
            this.dim.color = color;

            if(this.dim.color.a >= 1)
            {
                break;
            }
            yield return null; 
        }
        Debug.Log("FadeOut Complete!");
        this.onFadeOutComplete();
    }

}

fadeout이 종료되면 다음 씬인 StageOne으로 씬전환이 된다. 

용량문제로 인해, 플레이어 속도 빠르게 하여 촬영

StageOne

공격을 하는 조건은 다음과 같다. 

1. 몬스터가 플레이어의 인식 범위 안에 들어와, 몬스터를 인식한다.

2. 이 상태에서 이동을 하지 않으면, 일정시간마다 총알이 발사되어 공격한다. 

StageOneMain

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.SceneManagement;

public class StageOneMain : MonoBehaviour
{
    [SerializeField] private PlayerController player; //플레이어
    [SerializeField] private BulletGenerator bulletGenerator; //총알 생성기
    [SerializeField] private MonsterController monsterController1; //몬스터
    [SerializeField] private MonsterController monsterController2; //몬스터 

    [SerializeField]
    private PortalGenerator portalGenerator;

    [SerializeField] private GameObject door;

    private int monsterCount = 2;
    // Start is called before the first frame update
    void Start()
    {
        if (monsterController1 != null)
        {
            this.monsterController1.onDie = () => { //몬스터가 죽으면 
                Debug.LogFormat("{0} 사망", monsterController1.gameObject.name);
                monsterController1.Die();
                this.monsterCount--;
                if (monsterCount == 0)
                {
                    GameObject portal = this.portalGenerator.PortalGenerate(this.door.transform);
                    this.player.Portal = portal; //포탈 설정
                    this.door.GetComponent<DoorController>().OpenDoor();
                }
            };
        }
        if(monsterController2 != null)
        {
            this.monsterController2.onDie = () => { //몬스터가 죽으면 
                Debug.LogFormat("{0} 사망", monsterController2.gameObject.name);
                monsterController2.Die();
                this.monsterCount--;
                if (monsterCount == 0)
                {
                    GameObject portal = this.portalGenerator.PortalGenerate(this.door.transform);
                    this.player.Portal = portal; //포탈 설정
                    this.door.GetComponent<DoorController>().OpenDoor();
                }
            };
        }
        this.player.onPortal = () => {
            Debug.Log("<color=yellow>포탈위치에 도착</color>");
            //씬전환
            SceneManager.LoadScene("StageTwo"); //아직 fadeout/in 안넣음
        };
    }

    // Update is called once per frame
    void Update()
    {
        if(player.H ==0 && player.V == 0) //이동하지 않으면 
        {
            if(player.TargetMonster != null) //선택된 몬스터가 있을 시 
            {
                bulletGenerator.BulletGenerate();
            }
        }
    }
}

Main의 업데이트에서, 이동여부와, 타겟몬스터가 있는지 없는지 확인

 

MonsterController

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

public class MonsterController : MonoBehaviour
{
    [SerializeField] private int hp = 5;
    public System.Action onDie;
    [SerializeField] private GameObject indicator;

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("Bullet"))
        {
            this.hp--;
            if(this.hp <= 0)
            {
                this.onDie();
            }
        }
    }

    public void Die()
    {
        Debug.Log("몬스터 사망!");
        Destroy(this.gameObject);
    }

    public void HideIndicator()
    {
        Debug.Log("선택 마크 숨기기!");
        this.indicator.SetActive(false);
    }

    public void ShowIndicator()
    {
        Debug.Log("선택 마크 보여주기!");
        this.indicator.SetActive(true);
    }
}

PlayerController

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

public class PlayerController : MonoBehaviour
{
    //몬스터 선택하는 부분까지는 완료, 공격하는 조건을 자세히 다시 설정할것 
    public enum eControlType
    {
        KeyBoard,Joystick
    }
    [SerializeField] private FloatingJoystick joystick; //조이스틱
    [SerializeField] private eControlType controlType; //플레이어 이동 타입
    [SerializeField] private float moveSpeed = 1.0f; //플레이어 이동 속도

    [SerializeField] private Transform target; //튜토리얼에서의 특정위치
    [SerializeField] private float radius = 1.0f; //플레이어가 자기 주변을 인식하는 반경
    [SerializeField] private float rayOffsetY = 0.5f; //레이 오프셋
    private GameObject portal;
    private MonsterController targetMonster;

    public System.Action onPortal; //튜토리얼 포탈
    public System.Action onTarget; //튜토리얼 타겟

    private float h = 0;
    private float v = 0;

    public float H
    {
        get { return h; }
    }
    public float V
    {
        get { return v; }
    }
    public GameObject Portal
    {
        set { this.portal = value; Debug.Log("설정완료"); }
    }

    public MonsterController TargetMonster
    {
        get { return this.targetMonster; }
    }
    //게임오브젝트와 거리를 저장, 범위에 들어오는 경우에 사전에 저장
    Dictionary<GameObject, float> dic = new Dictionary<GameObject, float>();
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        this.Move();
        this.OnTarget();
        this.OnPortal();
        this.SearchMonster();
        
    }
    private void Move()
    {
        if (this.controlType == eControlType.Joystick)
        {
            h = this.joystick.Direction.x;
            v = this.joystick.Direction.y;
        }
        else if (this.controlType == eControlType.KeyBoard)
        {
            h = Input.GetAxisRaw("Horizontal");
            v = Input.GetAxisRaw("Vertical");
        }
        Vector3 direction = new Vector3(h, 0, v);
        float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
        //이동
        this.transform.Translate(direction * this.moveSpeed * Time.deltaTime, Space.World);
        //회전
        this.transform.localRotation = Quaternion.AngleAxis(angle, Vector3.up);
    }
    //튜토리얼에서 특정 위치 가면 실행되는 메서드
    private void OnTarget()
    {
        if(this.target != null)
        {
            float distance = Vector3.Distance(this.transform.position, target.position);
            //Debug.LogFormat("distance:{0}", distance);
            if (distance < 1.6f) //목표위치에 도착
            {
                this.onTarget(); //대리자 호출
            }
        }
    }
    //튜토리얼에서 포탈의 위치에 가면 실행되는 메서드 
    private void OnPortal()
    {
        if(this.portal != null)
        {
            float distance = Vector3.Distance(this.transform.position,
                this.portal.transform.position);
            if(distance < 4.8f)
            {
                //다음 씬으로 이동
                this.onPortal();
            }
        }
    }
    //주변에 있는 몬스터를 검색하여 선택하는 메서드
    private void SearchMonster()
    {
        int monsterLayerMask = 1 << LayerMask.NameToLayer("Monster");
        Collider[] targetColls = Physics.OverlapSphere(this.transform.position, radius, monsterLayerMask);
        this.dic.Clear(); //사전에 있는 정보 초기화

        //for문을 돌며, 해당 정보를 사전에 저장, 키값은 GameObject
        for(int i=0; i<targetColls.Length; i++)
        {
            Collider col = targetColls[i];
            float distance = Vector3.Distance(col.transform.position, this.transform.position);
            this.dic.Add(col.gameObject, distance);
        }
        //공격 사거리 내에 몬스터가 존재한다면
        if(this.dic.Count > 0)
        {
            float nearestDistance = this.dic.Values.Min(); //값 중에 가장 작은 것을 저장
            //시퀸스의 첫번째 요소를 반환하거나, 시퀸스에 요소가 없으면 기본값을 반환
            //여기서는 값, 즉 거리가, nearestDistance와 같은 요소를 가져와서, 그 요소의 키 값
            //즉, 게임오브젝트를 받아온다. 
            GameObject target = this.dic.FirstOrDefault(x => x.Value == nearestDistance).Key;
            Vector3 dir = target.transform.position - this.transform.position; // 플레이어에서의 타겟의 방향
            Ray ray = new Ray(this.transform.position + (Vector3.up * this.rayOffsetY), dir); //레이 생성
            Debug.DrawRay(ray.origin, ray.direction * 1000f, Color.red); //레이를 씬에서 그려봄
            int layerMask = 1 << LayerMask.NameToLayer("Monster") |
                            1 << LayerMask.NameToLayer("Wall") |
                            1 << LayerMask.NameToLayer("Tree");
            RaycastHit hit; 
            if(Physics.Raycast(ray, out hit, 1000f, layerMask)) //몬스터, 벽 , 나무에 관한 정보를 검출
            {
                Debug.Log(hit.collider.tag);

                if (hit.collider.CompareTag("Monster"))
                {
                    if(this.targetMonster != null)
                    {
                        this.targetMonster.HideIndicator();
                        this.transform.LookAt(this.targetMonster.transform.position);
                    }
                    MonsterController monster = hit.collider.gameObject.GetComponent<MonsterController>();
                    monster.ShowIndicator();
                    this.targetMonster = monster;
                }
                else
                {
                    MonsterController[] monsters = GameObject.FindObjectsOfType<MonsterController>();
                    Debug.LogFormat("몬스터의 수:{0}",monsters.Count());

                    foreach(MonsterController monster in monsters)
                    {
                        monster.HideIndicator();
                    }
                    this.target = null;
                }
            }
        }

    }
    //주변에 몬스터가 있다면 공격하는 메서드
    private void Attack()
    {

    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.transform.position, radius);
    }
}

SearchMonster 메서드를 통해, 몬스터 찾음 

몬스터가 선택되면, 밑에 선택되었다는 표식이 나타남 

 

시연영상 

https://youtu.be/h_SoC9fARBk

 

전체적인 기능 구현은 완료해서, 이제 플레이어와 몬스터의 애니메이션을 수정할 생각이다. 

플레이어 애니메이션
몬스터 애니메이션

간단하게 애니메이션을 만들어보았다. 

아직 몬스터의 hit 애니메이션이 미흡하지만, 추후 고치도록 하겠다.

PlayerController - 애니메이션 추가 

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

public class PlayerController : MonoBehaviour
{
    //몬스터 선택하는 부분까지는 완료, 공격하는 조건을 자세히 다시 설정할것 
    public enum eControlType
    {
        KeyBoard,Joystick
    }

    private enum eAnimationType
    {
        Idle,Run,Shoot
    }
    [SerializeField] private FloatingJoystick joystick; //조이스틱
    [SerializeField] private eControlType controlType; //플레이어 이동 타입
    [SerializeField] private float moveSpeed = 1.0f; //플레이어 이동 속도

    [SerializeField] private Transform target; //튜토리얼에서의 특정위치
    [SerializeField] private float radius = 1.0f; //플레이어가 자기 주변을 인식하는 반경
    [SerializeField] private float rayOffsetY = 0.5f; //레이 오프셋
    private GameObject portal;
    private MonsterController targetMonster;

    public System.Action onPortal; //튜토리얼 포탈
    public System.Action onTarget; //튜토리얼 타겟

    private float h = 0;
    private float v = 0;

    private Animator anim;
    //애니메이션 파라미터 해쉬값 추출
    private readonly int hashState = Animator.StringToHash("State");

    public float H
    {
        get { return h; }
    }
    public float V
    {
        get { return v; }
    }
    public GameObject Portal
    {
        set { this.portal = value; Debug.Log("설정완료"); }
    }

    public MonsterController TargetMonster
    {
        get { return this.targetMonster; }
    }
    //게임오브젝트와 거리를 저장, 범위에 들어오는 경우에 사전에 저장
    Dictionary<GameObject, float> dic = new Dictionary<GameObject, float>();
    // Start is called before the first frame update
    void Start()
    {
        this.anim = this.GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        this.Move();
        this.OnTarget();
        this.OnPortal();
        this.SearchMonster();
        
    }
    private void Move()
    {
        if (this.controlType == eControlType.Joystick)
        {
            h = this.joystick.Direction.x;
            v = this.joystick.Direction.y;
        }
        else if (this.controlType == eControlType.KeyBoard)
        {
            h = Input.GetAxisRaw("Horizontal");
            v = Input.GetAxisRaw("Vertical");
        }
        Vector3 direction = new Vector3(h, 0, v);
        float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
        //이동
        this.transform.Translate(direction * this.moveSpeed * Time.deltaTime, Space.World);
        //회전
        this.transform.localRotation = Quaternion.AngleAxis(angle, Vector3.up);
        if(this.h == 0 && this.v == 0)
        {
            if(this.targetMonster != null)
            {
                this.transform.LookAt(this.targetMonster.transform.position); //공격 할 때만 바라보도록 수정
                this.anim.SetInteger(this.hashState, (int)eAnimationType.Shoot);
            }
            else
            {
                this.anim.SetInteger(this.hashState, (int)eAnimationType.Idle);
            }
        }
        else
        {
            //이동 애니메이션
            this.anim.SetInteger(this.hashState, (int)eAnimationType.Run);
        }
    }
    //튜토리얼에서 특정 위치 가면 실행되는 메서드
    private void OnTarget()
    {
        if(this.target != null)
        {
            float distance = Vector3.Distance(this.transform.position, target.position);
            //Debug.LogFormat("distance:{0}", distance);
            if (distance < 1.6f) //목표위치에 도착
            {
                this.onTarget(); //대리자 호출
            }
        }
    }
    //튜토리얼에서 포탈의 위치에 가면 실행되는 메서드 
    private void OnPortal()
    {
        if(this.portal != null)
        {
            float distance = Vector3.Distance(this.transform.position,
                this.portal.transform.position);
            if(distance < 4.8f)
            {
                //다음 씬으로 이동
                this.onPortal();
            }
        }
    }
    //주변에 있는 몬스터를 검색하여 선택하는 메서드
    private void SearchMonster()
    {
        int monsterLayerMask = 1 << LayerMask.NameToLayer("Monster");
        Collider[] targetColls = Physics.OverlapSphere(this.transform.position, radius, monsterLayerMask);
        this.dic.Clear(); //사전에 있는 정보 초기화

        //for문을 돌며, 해당 정보를 사전에 저장, 키값은 GameObject
        for(int i=0; i<targetColls.Length; i++)
        {
            Collider col = targetColls[i];
            float distance = Vector3.Distance(col.transform.position, this.transform.position);
            this.dic.Add(col.gameObject, distance);
        }
        //공격 사거리 내에 몬스터가 존재한다면
        if(this.dic.Count > 0)
        {
            float nearestDistance = this.dic.Values.Min(); //값 중에 가장 작은 것을 저장
            //시퀸스의 첫번째 요소를 반환하거나, 시퀸스에 요소가 없으면 기본값을 반환
            //여기서는 값, 즉 거리가, nearestDistance와 같은 요소를 가져와서, 그 요소의 키 값
            //즉, 게임오브젝트를 받아온다. 
            GameObject target = this.dic.FirstOrDefault(x => x.Value == nearestDistance).Key;
            Vector3 dir = target.transform.position - this.transform.position; // 플레이어에서의 타겟의 방향
            Ray ray = new Ray(this.transform.position + (Vector3.up * this.rayOffsetY), dir); //레이 생성
            Debug.DrawRay(ray.origin, ray.direction * 1000f, Color.red); //레이를 씬에서 그려봄
            int layerMask = 1 << LayerMask.NameToLayer("Monster") |
                            1 << LayerMask.NameToLayer("Wall") |
                            1 << LayerMask.NameToLayer("Tree");
            RaycastHit hit; 
            if(Physics.Raycast(ray, out hit, 1000f, layerMask)) //몬스터, 벽 , 나무에 관한 정보를 검출
            {
                Debug.Log(hit.collider.tag);

                if (hit.collider.CompareTag("Monster"))
                {
                    if(this.targetMonster != null)
                    {
                        this.targetMonster.HideIndicator();
                    }
                    MonsterController monster = hit.collider.gameObject.GetComponent<MonsterController>();
                    monster.ShowIndicator();
                    this.targetMonster = monster;
                }
                else
                {
                    MonsterController[] monsters = GameObject.FindObjectsOfType<MonsterController>();
                    Debug.LogFormat("몬스터의 수:{0}",monsters.Count());

                    foreach(MonsterController monster in monsters)
                    {
                        monster.HideIndicator();
                    }
                    this.targetMonster = null;
                }
            }
        }

    }


    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.transform.position, radius);
    }
}

MonsterController -> 애니메이션 추가

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

public class MonsterController : MonoBehaviour
{
    private enum eMonsterState
    {
        Idle,Hit,Die
    }
    [SerializeField] private int hp = 5;
    public System.Action onDie;
    [SerializeField] private GameObject indicator;
    
    private Animator anim;

    private void Awake()
    {
        this.anim = this.GetComponent<Animator>();
    }
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("Bullet"))
        {
            this.hp--;
            this.anim.SetInteger("MonsterState", 1); //hit //아직 미흡
            if(this.hp <= 0)
            {
                this.Die();
            }
        }
    }

    public void Die()
    {
        StartCoroutine(this.CoDie());
        //Destroy(this.gameObject);
    }

    private IEnumerator CoDie()
    {
        this.anim.SetInteger("MonsterState", 2); //die
        yield return new WaitForSeconds(2.0f);
        this.onDie();
    }

    public void HideIndicator()
    {
        Debug.Log("선택 마크 숨기기!");
        this.indicator.SetActive(false);
    }

    public void ShowIndicator()
    {
        Debug.Log("선택 마크 보여주기!");
        this.indicator.SetActive(true);
    }
}

https://youtu.be/VBUINIaJhBU

몬스터 hit 애니메이션이 아직 미흡하다.