3D 콘텐츠 제작

좀짓막(좀비 집짓고 막기) [#6] - 공격/이동/설치기능 완성

송현호 2023. 10. 5. 12:21

오늘은 좀짓막 오브젝트에 필요한 기능을 추가해서 오브젝트들이 서로 상호작용 할 수 있도록 공격이나 이동등의 기능을 추가할 생각이다.

 

그전에 저번 포스팅에서 다 만들지 못한 설치기능을 마저 완성할 생각이다. 벽을 설치하는 기능까진 완성했었는데 벽의 경우 2칸이라 터렛이나 수리로봇을 설치하기엔 칸수가 맞지 않다 그래서 1칸을 설치하는 기능을 만들고 Drag의 DefaultPos의 위치를 활용해서 설치 오브젝트를 판정할 생각이다. 

 

그걸 위해서 Debug를 찍어서 오브젝트 아이콘이 있는 defaultPos의 좌표를 기록해주었다.

 

바리게이트 - 1620 / 300

터렛 - 1740 / 300

수리로봇 - 1860 / 300

 

그 후 게임매니저에 바리게이트 터렛 수리로봇으로 enum타입을 만들고 기존에 바리게이트만 추가되있었던 게임 오브젝트를 배열로 해서 터렛과 수리로봇도 추가해주었다. 그리고 기존에 추가되있던 Turret 멤버 변수를 삭제하고 좀비가 데미지를 입는 스크립트를 다시 작성하기로 했다.

 

그리고 드래그할 오브젝트가 3개라 3개 모두 액션함수에 이벤트를 할당해줘야 했는데 시간상의 문제로 일단은 대리자를 3번 써서 전달해주기로 하였다. 이 부분은 나중에 코드를 리팩토링할때 더 깔끔하게 정리해야 할 것 같다.

 

설치까지는 됐고 설치할때의 판정과 위치를 한칸 오브젝트에 맞게 변경해주었다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.PlayerSettings;
using static UnityEngine.GraphicsBuffer;

public class GameManager : MonoBehaviour
{
    public enum ePlaceObject
    {
        Barricade, Turret, RepairRobot
    }
    
    [SerializeField]
    private GameObject character;
    [SerializeField]
    private GameObject enemy;
    [SerializeField]
    private CameraCtrl cam;
    [SerializeField]
    private GameObject[] placeObject;
    [SerializeField]
    private Drag[] drag;
    [SerializeField]
    private AGrid grid;
    [SerializeField]
    private GameObject tile1;
    [SerializeField]
    private GameObject tile2;

    public bool[,] tileMap = new bool[128, 128];
    private Player player;
    private List<Enemy> enemies = new List<Enemy>();
    private Coroutine routine;
    private int zombieHp = 30;
    private int zombieDamage = 5;

    private void Awake()
    {
        Init();
    }
    void Start()
    {

        drag[0].onBuild = () =>
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Debug.Log(hit.point);
                
                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x) + 0.5f, hit.point.y, Mathf.Round(hit.point.z) - 0.3f);
                int tile1PosX = (int)(buildPos.x + 0.5f);
                int tile2PosX = (int)(buildPos.x - 0.5f);
                int tilePosZ = (int)Mathf.Round(hit.point.z);
                if (player != null)
                {
                    if (tileMap[tile1PosX, tilePosZ] == true || tileMap[tile2PosX, tilePosZ] == true)
                    {

                    }
                    else
                    {
                        Instantiate(placeObject[0], buildPos, placeObject[0].transform.rotation);
                        SetFulled(tile1PosX, tilePosZ);
                        SetFulled(tile2PosX, tilePosZ);
                    }

                }
            }
            tile1.SetActive(false);
            tile2.SetActive(false);
        };

        drag[0].onBuildable = () =>
        {
            tile1.SetActive(true);
            tile2.SetActive(true);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x) + 0.5f, hit.point.y, Mathf.Round(hit.point.z) - 0.3f);

                tile1.transform.position = new Vector3((buildPos.x + 0.5f), -0.49f, Mathf.Round(hit.point.z));
                tile2.transform.position = new Vector3((buildPos.x - 0.5f), -0.49f, Mathf.Round(hit.point.z));

                if (tileMap[(int)tile1.transform.position.x, (int)tile1.transform.position.z] == false)
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.green;
                }
                else
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.red;
                }

                if (tileMap[(int)tile2.transform.position.x, (int)tile2.transform.position.z] == false)
                {
                    this.tile2.GetComponent<Renderer>().materials[0].color = Color.green;
                }
                else
                {
                    this.tile2.GetComponent<Renderer>().materials[0].color = Color.red;
                }
            }
        };

        drag[1].onBuild = () =>
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Debug.Log(hit.point);

                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x), hit.point.y, Mathf.Round(hit.point.z));
                int tile1PosX = (int)(buildPos.x);
                int tilePosZ = (int)Mathf.Round(hit.point.z);
                if (player != null)
                {
                    if (tileMap[tile1PosX, tilePosZ] == true)
                    {

                    }
                    else
                    {
                        Instantiate(placeObject[1], buildPos, placeObject[1].transform.rotation);
                        SetFulled(tile1PosX, tilePosZ);
                    }

                }
            }
            tile1.SetActive(false);
        };

        drag[1].onBuildable = () =>
        {
            tile1.SetActive(true);

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x), hit.point.y, Mathf.Round(hit.point.z));

                tile1.transform.position = new Vector3((buildPos.x), -0.49f, Mathf.Round(hit.point.z));

                if (tileMap[(int)tile1.transform.position.x, (int)tile1.transform.position.z] == false)
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.green;
                }
                else
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.red;
                }
            }
        };

        drag[2].onBuild = () =>
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Debug.Log(hit.point);

                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x), hit.point.y, Mathf.Round(hit.point.z));
                int tile1PosX = (int)(buildPos.x);
                int tilePosZ = (int)Mathf.Round(hit.point.z);
                if (player != null)
                {
                    if (tileMap[tile1PosX, tilePosZ] == true)
                    {

                    }
                    else
                    {
                        Instantiate(placeObject[2], buildPos, placeObject[2].transform.rotation);
                        SetFulled(tile1PosX, tilePosZ);
                    }

                }
            }
            tile1.SetActive(false);
        };

        drag[2].onBuildable = () =>
        {
            tile1.SetActive(true);

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 50f))
            {
                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x), hit.point.y, Mathf.Round(hit.point.z));

                tile1.transform.position = new Vector3((buildPos.x), -0.49f, Mathf.Round(hit.point.z));

                if (tileMap[(int)tile1.transform.position.x, (int)tile1.transform.position.z] == false)
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.green;
                }
                else
                {
                    this.tile1.GetComponent<Renderer>().materials[0].color = Color.red;
                }
            }
        };
    }

    private void Update()
    {
        if(Input.GetMouseButtonDown(1))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if(Physics.Raycast(ray, out hit, 50f))
            {
                Debug.Log(hit.point);
                Vector3 buildPos = new Vector3(Mathf.Floor(hit.point.x) + 0.5f, hit.point.y, Mathf.Round(hit.point.z) - 0.3f);
                if (player != null)
                {
                    PathRequestManager.RequestPath(player.transform.position, hit.point, player.OnPathFound);
                }
            }
        }
    }

    private void SetEmpty(int x, int y)
    {
        if (x >= 0 && x < 128 && y >= 0 && y < 128)
        {
            tileMap[x, y] = false;
            grid.SetEmpty(x, y);
        }
    }

    private void SetFulled(int x, int y)
    {
        if (x >= 0 && x < 128 && y >= 0 && y < 128)
        {
            tileMap[x, y] = true;
            grid.SetFulled(x, y);
        }
    }

    private void Init()
    {
        tile1.SetActive(false);
        tile2.SetActive(false);
        for (int i = 0; i < 128; i++)
        {
            for(int j = 0; j < 128; j++)
            {
                if (i >= 0 && i < 128 && j >= 0 && j < 128)
                {
                    tileMap[i, j] = true;

                }
            }
        }

        for (int i = 2; i < 126; i++)
        {
            for (int j = 2; j < 126; j++)
            {
                if (i >= 0 && i < 128 && j >= 0 && j < 128)
                {
                    tileMap[i, j] = false;

                }
            }
        }

        GameObject ch = Instantiate(character);
        ch.transform.position = new Vector3(64, 0, 64);
        cam.Init(ch);
        this.player = ch.GetComponent<Player>();
        GameObject ene = Instantiate(enemy);
        ene.transform.position = new Vector3(32, 0, 32);
        Enemy zombie = ene.GetComponent<Enemy>();
        zombie.Init(zombieHp,zombieDamage);
        this.enemies.Add(zombie);
    }
}

 

이제 좀비에 A*를 적용해서 시야내에 적이 들어 왔을때 가장 가까운 적을 따라가도록 만들어 보겠다.

 

어떻게 구현할지 생각해봤는데 목표 좌표에 Random함수를 넣어서 자동으로 맵을 정찰하다가 시야 내에 목표가 들어오면 목표를 향해 이동하도록 만들면 될 것 같다. 주의할 점은 이걸 업데이트에 넣어서 했을때 좀비가 제대로 안 움직이고 갈팡질팡 할 것 같아서 코루틴으로 구현할 지 아니면 다른 방법을 사용할 지는 좀 고민해봐야 할 것 같다.

 

일단 코루틴을 사용해서 좀비를 움직여봤다. 

 

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

public class Enemy : MonoBehaviour
{
    int hp;
    int damage;

    public Transform target;
    private float speed = 20f;
    Vector3[] path;
    int targetIndex;
    public void Init(int hp, int damage)
    {
        this.hp = hp;
        this.damage = damage; 
    }

    private void Start()
    {
        StartCoroutine(CoMove());
    }

    private void Update()
    {

    }

    private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Bullet")
        {
        }
    }

    public void TakeDamage(int damage)
    {
        this.hp -= damage;

        if(hp <= 0)
        {
            Destroy(this.gameObject);
        }
    }

    public void OnPathFound(Vector3[] newPath, bool pathSuccessful)
    {
        if (pathSuccessful)
        {
            path = newPath;
            targetIndex = 0;
            StopCoroutine("FollowPath");
            StartCoroutine("FollowPath");
        }
    }

    IEnumerator FollowPath()
    {
        Vector3 currentWaypoint = path[0];
        while (true)
        {
            if (transform.position == currentWaypoint)
            {
                targetIndex++;
                if (targetIndex == path.Length)
                {
                    yield break;
                }
                currentWaypoint = path[targetIndex];
            }

            transform.position = Vector3.MoveTowards(transform.position, currentWaypoint, speed * Time.deltaTime);
            yield return null;
        }
    }

    IEnumerator CoMove()
    {
        while(true)
        {
            PathRequestManager.RequestPath(this.transform.position, new Vector3(Random.Range(0, 128), 0, Random.Range(0, 128)), OnPathFound);

            yield return new WaitForSeconds(2f);
        }
        
    }
}

그럭저럭 나쁘지 않게 움직이긴 하지만 좀비가 움직일때마다 렉이 걸린다... 프로파일러도 찍어 봤는데 코루틴으로 좀비를 움직일때가 문제인것 같다. 어떻게 해결할지 고민하다 업데이트문으로 좀비를 움직여 보기로 했다.

 

private void Update()
    {
        coolDown += Time.deltaTime;

        if(coolDown >= 3)
        {
            PathRequestManager.RequestPath(this.transform.position, new Vector3(Random.Range(0,128),0,Random.Range(0,128)), OnPathFound);
            coolDown = 0;
        }
    }

업데이트문으로 움직여봤는데도 렉이 발생했다. 그러다 문득 밑에 눈이 갔는데 A* 알고리즘에서 노드를 구할때마다 디버그를 출력하도록 되있었다. 이걸 지우고 실행해봤더니 렉이 사라졌다. 디버그도 과도하게 많이 실행하면 렉이 걸릴 수 있다는 교훈을 얻었다...

 

그래서 나중에 좀비를 많이 생성할거기 때문에 코루틴을 쓸지 업데이트를 쓸지는 chatgpt에게 한번 물어 보았다.

 

그렇다고 한다. 코루틴이 더 적게 호출되기 때문에 코루틴을 사용하기로 하였다.

 

이제 좀비를 움직이게 만들었으니 좀비 시야내에 목표물이 들어오면 따라가서 공격하게 만들도록 하겠다.

그걸 위해서 첫번째로 플레이어와 플레이어가 만들 수 있는 오브젝트들을 플레이어 레이어로 설정한 뒤 좀비가 플레이어 레이어를 가진 콜라이더를 검출하면 그 위치로 이동할 수 있도록 만들었다.

 

private void Start()
    {
        StartCoroutine(CoMove());
    }

IEnumerator CoMove()
    {
        while (true)
        {
            FindTarget();

            yield return new WaitForSeconds(3f);
        }

    }

    private void FindTarget()
    {
        Collider[] targets = Physics.OverlapSphere(this.transform.position, 20f, 1 << 7);

        foreach(var target in targets)
        {
            Debug.Log(target);
        }
        if (targets.Length >= 1)
        {
            for (int i = 0; i < targets.Length; i++)
            {
                float dis = Vector3.Distance(this.transform.position, targets[i].transform.position);
                if (targetDis > dis)
                {
                    targetDis = dis;
                    target = targets[i];
                    this.transform.LookAt(target.transform);
                    PathRequestManager.RequestPath(this.transform.position, target.transform.position, OnPathFound);
                }
            }
            targetDis = Mathf.Infinity;
        }
        else
        {
            PathRequestManager.RequestPath(this.transform.position, new Vector3(Random.Range(0, 128), 0, Random.Range(0, 128)), OnPathFound);
        }
    }

이제 좀비가 목표물과의 위치가 가까워지면 공격을 하게 만들것이다. targetDis를 이용해서 목표물과의 사이의 거리가 가까워지면 공격하는 식으로 해보려고 했는데 FindTarget이 1초마다 실행되서 동작이 부자연스러워 지기 때문에 FollowPath메서드를 수정해서 공격하도록 만들겠다.

 

먼저 좀비가 플레이어와 가까워지면 디버그를 출력하도록 해보았다.

 

잘 출력된다 그런데 너무 여러번 실행되기 때문에 공격 딜레이를 만들어서 2초에 한번씩 공격할 수 있도록 만들겠다.

 

private IEnumerator CoAttack()
    {
        while (attackDelay == false)
        {
            attackDelay = true;


            // 공격 애니메이션
           
            yield return new WaitForSeconds(2f);
            attackDelay = false;
        }
    }

이제 좀비가 공격을 실행하면 플레이어의 hp가 감소하도록 만들겠다. Enemy스크립트에 attack액션을 정의한 뒤 게임매니저를 이용해서 player가 TakeDamage스크립트를 실행할 수 있도록 할 것이다.

 

 

만들고보니 범용성이 너무 떨어지는 것 같아서 Unit스크립트를 만든 다음에 Player Turret RepairRobot에게 상속해줬다.

 

 Unit스크립트에는 필드에 hp와 TakeDamage()메서드를 정의해주었다.

그리고 좀비가 공격을 할 경우 Unit스크립트의 TakeDamage를 호출해서 유닛이 데미지를 입을 수 있도록 해주었다.

 

근데 좀비가 대각선이나 상하좌우에 있을때 공격하도록 설계했는데 거리를 한칸 남겨두고 유닛을 공격하지 않는 경우가 종종 발생했다. Debug를 찍어보니 A*의 정확도 문제 같아서 A* 알고리즘을 수정해주었다.

 

Vector3[] SimplifyPath(List<ANode> path)
    {
        List<Vector3> waypoints = new List<Vector3>();
        Vector2 directionOld = Vector2.zero;
        //마지막 노드가 안 들어갔었음
        waypoints.Add(path[0].worldPos);
        for (int i = 1; i < path.Count; i++)
        {
            Vector2 directionNew = new Vector2(path[i - 1].gridX - path[i].gridX, path[i - 1].gridY - path[i].gridY);
            if(directionNew != directionOld)
            {
                waypoints.Add(path[i].worldPos);
            }
            directionOld = directionNew;
        }
        return waypoints.ToArray();
    }