Unity

[TowerDefence] 타워 디펜스 게임 마무리 및 코드 정리

wny0320 2023. 4. 17. 19:45

게임 소개

게임 이름: PO!SON

 

장르: 타워 디펜스, 생존

 

게임 목표: 마을이 몬스터의 공격에 당하고 독가스에 오염되는 가운데, 주인공이 몬스터를 막고 보스 몬스터를 잡아 약초를 모아 정화 가스를 제작하여 마을을 정화해서 마을을 구하는것이 목표다.

 

게임 특징: 기본적으로 타워 디펜스 게임이지만 정해진 칸에 타워를 설치해서 몬스터가 정해진 길을 따라오는 걸 막는 타워 디펜스 게임이 아니다. 타워가 아무데나 설치, 철거, 이동이 가능하며 몹도 랜덤으로 뱀파이어 서바이벌 같은 느낌으로 맵 가장자리에 스폰되서 타워와 플레이어한테 다가오게 된다.

 

제작 기간: 2022년 10월 28일 ~ 2023년 3월 14일

플레이

플레이는 아래 링크에서 할 수 있다.

https://wny0320.itch.io/poison

 

코드

아래는 동아리 프로젝트 내에서 직접 코드를 짠 부분만 정리해보았다.

 

이해가 안되거나 고치면 좋을점을 지적하는 것은 언제나 환영

 

 

Player.cs

플레이어에 관한 모든 움직임과 특징이 들어있다.

후에는 좀 더 모듈화를 시키면 좋을 것 같다.

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class Player : MonoBehaviour
{
    //IMove 관련 필드
    [SerializeField] [Tooltip("이동속도")] float moveSpeed;
    [SerializeField] EnemySpawner enemySpawner;
    Vector3 inputDir;
    Vector3 moveDir;
    CharacterController controller;

    //Health 관련 필드
    Health health;
    int healthBuffer;
    [SerializeField] Slider hpBar;
    public bool playerRegenerating;
    [Header("체력 자연 재생 타이머")]
    [SerializeField] [Tooltip("맞지 않은 시간")] float healthTimer;
    [SerializeField] [Tooltip("자연 재생까지 맞지 않아야 하는 시간")] float healthTime;
    Coroutine healthUp;

    //Pick 관련 필드
    GameObject Hand = null;
    float standardDistance = 1.2f;

    //피격 관련 필드
    [Header("")]
    [Tooltip("플레이어가 피격 가능한지 유무")]
    public bool canDamaged = true;
    float unBeatTime = 1.0f;
    Material material;

    //Menu 관련 필드
    GameObject menuUI;
    GameObject selectUI;
    GameObject buildUI;
    GameObject manageUI;
    GameObject manageSelectUI;

    bool isEnabledMenuUI;
    /*bool isEnabledSelectUI;
    bool isEnabledBuildUI;
    bool isEnabledRepairUI;*/

    public int modeSelect;
    public int manageSelect;
    int manageSelectBuffer;
    public bool buildMode;
    public bool manageMode;
    enum towerList
    {
        BasicTower = 0,
        PunchTower = 1,
        RangeTower = 2,
        SlowTower = 3,
    }
    enum modeSelection
    {
        NonSelect = -1,
        BuildMode = 0,
        ManageMode = 1,
    }
    enum manageSelection
    {
        NonSelect = -1,
        RepairMode = 0,
        RemoveMode = 1,
        UpgradeMode = 2,
    }
    enum towerAttackType
    {
        기본 = 1,
        밀기 = 2,
        광역 = 3,
        슬로우 = 4,
    }
    [SerializeField]
    GameObject[] towerObjList;
    public int choice;
    public int[] buildCostAdder;

    //repair 관련 필드
    GameObject manageTarget;
    public bool manageOn;
    Text lv;
    Text attackType;
    Text maxHP;
    Text curHP;
    Text power;
    Text cost;
    int layerMask = (1 << 6);
    [SerializeField] Sprite[] towerSpriteList;
    [SerializeField] Image towerImage;
    [SerializeField] AudioSource source;

    void Start()
    {
        //Move 필드 초기화
        controller = GetComponent<CharacterController>();

        //Health 필드 초기화
        health = GetComponent<Health>();
        healthBuffer = health.healthCheck;

        //UnBeat에 쓰일 필드 초기화
        material = GetComponent<Renderer>().material;

        //repair 필드 초기화
        manageTarget = null;
        manageMode = false;
        manageOn = false;
        manageSelectBuffer = (int)manageSelection.NonSelect;
        lv = GameObject.Find("Lv").GetComponent<Text>();
        attackType = GameObject.Find("AttackType").GetComponent<Text>();
        maxHP = GameObject.Find("Max").GetComponent<Text>();
        curHP = GameObject.Find("Cur").GetComponent<Text>();
        power = GameObject.Find("Power").GetComponent<Text>();
        cost = GameObject.Find("Cost").GetComponent<Text>();

        //Menu에 쓰일 필드 초기화
        menuUI = GameObject.Find("MenuUI");
        selectUI = GameObject.Find("SelectUI");
        buildUI = GameObject.Find("BuildUI");
        manageUI = GameObject.Find("ManageUI");
        manageSelectUI = GameObject.Find("ManageSelectUI");

        buildUI.SetActive(false);
        manageUI.SetActive(false);
        selectUI.SetActive(false);
        manageSelectUI.SetActive(false);
        menuUI.SetActive(false);

        isEnabledMenuUI = false;
        /*isEnabledSelectUI = true;
        isEnabledBuildUI = false;
        isEnabledRepairUI = false;*/

        modeSelect = -1;
        manageSelect = -1;
        buildMode = false;
        buildCostAdder = new int[4];        
    }
    void Update()
    {
        Move();
        OnPick();
        Menu();
        BuildMode();
        ManageMode();
        HealthRegenerate();
        Clipping();
    }

    void LateUpdate()
    {
        PlayerHpCheck();
        PlayerDead();
    }

    void Pick()
    {
        //플레이어 안에 게임 오브젝트를 하나 둠
        //주울 수 있는 오브젝트 List인 PickList 안에 있는 물체에서 제일 가까운 물체의 거리와 Target 오브젝트를 찾음
        float closeDistance = float.PositiveInfinity;
        GameObject Target = null;

        //손에 들고 있지 않은 경우
        if (Hand == null)
        {
            /*foreach (GameObject i in GameManager.PickList)
            //distance에 값을 두기
            {
                //PickList에 있는 물체들로 거리 측정 반복문 
                Vector3 targetPos = i.transform.position;
                Vector3 playerPos = this.transform.position;
                float nowDistacne = Vector3.Magnitude(playerPos - targetPos);
                if (closeDistance > nowDistacne)
                {
                    closeDistance = nowDistacne;
                    Target = i;
                }
            }*/

            //PickList 사용 안한 충돌 물체 구하기
            //기준 거리 내에 있는 Collider 추출해서 목록에 넣기
            Collider[] collider = Physics.OverlapSphere(this.transform.position, standardDistance);
            foreach (Collider i in collider)
            {
                Vector3 targetPos = i.gameObject.transform.position;
                Vector3 playerPos = this.transform.position;
                float nowDistacne = Vector3.Magnitude(playerPos - targetPos);
                if (closeDistance > nowDistacne && (i.gameObject.tag == "Tower" || i.gameObject.tag == "Resource" || i.gameObject.tag == "Herb"))
                {
                    closeDistance = nowDistacne;
                    Target = i.gameObject;
                }
            }
            //collider가 IsTrigger든 아니든 줍고 내려놓을 수 있음, 단 플레이어의 위치에 내려놓는다면
            //Trigger가 아닐시 플레이어가 움직일시 밀릴 수 있음

            //기준 거리 안에 있다면(주울 수 있는 거리라면)
            if (closeDistance <= standardDistance)
            {
                //Target의 이름별로 실행
                if (Target.tag == "Tower")
                {
                    Hand = Target;
                    Hand.transform.SetParent(this.transform);
                    Hand.SetActive(false);
                    //renderer가 자식한테도 있어서 타워가 포신이 안지워짐(그거까지 넣기)
                    /*TowerAttackType HandScript = Hand.GetComponentInChildren<TowerAttackType>();
                    TowerManager HandScript2 = Hand.GetComponentInChildren<TowerManager>();
                    HandScript.enabled = false;
                    HandScript2.enabled = false;
                    Renderer Handrenderer = Hand.GetComponent<Renderer>();
                    Handrenderer.enabled = false;*/
                    //오브젝트 콜라이더 제외 비활성화
                    //상속
                }
                if (Target.tag == "Resource")
                {
                    GameManager.resource++;
                    //GameManager.PickList.Remove(Target);
                    Destroy(Target);
                    //자원 추가
                }
                if (Target.tag == "Herb")
                {
                    GameManager.herb++;
                    Destroy(Target);
                }
            }
        }
        else
        {
            //물체를 들고 있는 경우(들 수 있는 물체가 타워만 우선 구현)
            //타워 내려놓기, 설치방법에 따라 함수호출
            //임시로 플레이어 위치에 내려놓기로 설정
            Hand.transform.position = this.transform.position;
            Hand.SetActive(true);
            /*TowerAttackType HandScript = Hand.GetComponentInChildren<TowerAttackType>();
            TowerManager HandScript2 = Hand.GetComponentInChildren<TowerManager>();
            HandScript.enabled = true;
            HandScript2.enabled = true;
            Renderer Handrenderer = Hand.GetComponent<Renderer>();
            Handrenderer.enabled = true;*/
            Hand.transform.SetParent(null);
            Hand = null;
        }
    }

    void OnPick()
    {
        //space가 눌리면 Pick 함수 실행
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Pick();
        }

    }

    public void ManageHP(int amount)
    {
        health.healthCheck += amount;
        // amount는 음수, 양수로 받아 계산
    }


    //무적 코루틴
    IEnumerator UnBeat()
    {
        canDamaged = false;

        int blinkTime = (int)(unBeatTime / 0.25f);
        for (int i = 0; i < blinkTime; i++)
        {
            if (i % 2 == 0)
            {
                material.color = new Color32(255, 100, 0, 255);
            }
            else
            {
                material.color = new Color32(255, 255, 255, 255);
            }
            yield return new WaitForSeconds(0.25f);
        }
        material.color = new Color32(255, 255, 255, 255);

        canDamaged = true;

        yield return null;
    }

    void Menu()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            if (isEnabledMenuUI)
            {
                isEnabledMenuUI = false;
                buildUI.SetActive(false);
                manageUI.SetActive(false);
                selectUI.SetActive(false);
                manageSelectUI.SetActive(false);
                menuUI.SetActive(false);
                manageTarget = null;
                manageMode = false;
                modeSelect = (int)modeSelection.NonSelect;
                manageSelectBuffer = (int)manageSelection.NonSelect;
                TutorialUIOff();
            }
            else
            {
                isEnabledMenuUI = true;
                menuUI.SetActive(true);
                selectUI.SetActive(true);
            }
        }
        if (modeSelect == (int)modeSelection.BuildMode)
        {
            buildUI.SetActive(true);
            selectUI.SetActive(false);
            modeSelect = (int)modeSelection.NonSelect;
            TutorialUIOff();
        }
        if (modeSelect == (int)modeSelection.ManageMode)
        {
            selectUI.SetActive(false);
            manageSelectUI.SetActive(true);
            if (manageSelect != (int)manageSelection.NonSelect)
            {
                manageSelectUI.SetActive(false);
                manageUI.SetActive(true);
                manageMode = true;
                modeSelect = (int)modeSelection.NonSelect;
                manageSelectBuffer = manageSelect;
                manageSelect = (int)manageSelection.NonSelect;
                TutorialUIOff();
            }
            TutorialUIManageOff();
        }
    }

    void TutorialUIOff()
    {
        if (SceneManager.GetActiveScene().name == "Tutorial")
        {
            MouseEvent ME = FindObjectOfType<MouseEvent>().GetComponent<MouseEvent>();
            foreach (GameObject i in ME.eventList)
            {
                i.SetActive(false);
            }
        }
    }
    void TutorialUIManageOff()
    {
        if (SceneManager.GetActiveScene().name == "Tutorial")
        {
            MouseEvent ME = FindObjectOfType<MouseEvent>().GetComponent<MouseEvent>();
            ME.eventList[6].SetActive(false);
        }
    }
    void BuildMode()
    {
        if (buildMode)
        {
            switch (choice)
            {
                case (int)towerList.BasicTower:
                    buildMode = false;
                    if (GameManager.resource >= buildCostAdder[choice] + 6)
                    {
                        Instantiate(towerObjList[(int)towerList.BasicTower], transform.position, new Quaternion(0, 0, 0, 0));
                        GameManager.resource -= buildCostAdder[choice] + 6;
                        buildCostAdder[choice] += 3;
                    }
                    choice = -1;
                    break;

                case (int)towerList.PunchTower:
                    buildMode = false;
                    if (GameManager.resource >= buildCostAdder[choice] + 15)
                    {
                        Instantiate(towerObjList[(int)towerList.PunchTower], transform.position, new Quaternion(0, 0, 0, 0));
                        GameManager.resource -= buildCostAdder[choice] + 15;
                        buildCostAdder[choice] += 5;
                    }
                    choice = -1;
                    break;

                case (int)towerList.RangeTower:
                    buildMode = false;
                    if (GameManager.resource >= buildCostAdder[choice] + 9)
                    {
                        Instantiate(towerObjList[(int)towerList.RangeTower], transform.position, new Quaternion(0, 0, 0, 0));
                        GameManager.resource -= buildCostAdder[choice] + 9;
                        buildCostAdder[choice] += 4;
                    }
                    choice = -1;
                    break;

                case (int)towerList.SlowTower:
                    buildMode = false;
                    if (GameManager.resource >= buildCostAdder[choice] + 18)
                    {
                        Instantiate(towerObjList[(int)towerList.SlowTower], transform.position, new Quaternion(0, 0, 0, 0));
                        GameManager.resource -= buildCostAdder[choice] + 18;
                        buildCostAdder[choice] += 6;
                    }
                    choice = -1;
                    break;
            }
            source.Play();
        }
    }

    void VisualizeStatus(int select)
    {
        TowerManager targetTowerManager = manageTarget.GetComponent<TowerManager>();
        int targetCurHP = targetTowerManager.hp.health;
        int targetMaxHP = targetTowerManager.hp.healthCheck;
        int targetPower = targetTowerManager.damage;
        int targetAttackType = targetTowerManager.damageType;
        int targetLv = targetTowerManager.towerLevel;
        int targetCost = 0;
        lv.text = targetLv.ToString();
        curHP.text = targetCurHP.ToString();
        maxHP.text = targetMaxHP.ToString();
        power.text = targetPower.ToString();
        switch (targetAttackType)
        {
            case (int)towerAttackType.기본:
                attackType.text = "기본";
                towerImage.sprite = towerSpriteList[0];
                if (select == (int)manageSelection.RemoveMode)
                    targetCost = (int)((buildCostAdder[targetAttackType - 1] + 6) * 0.7);
                if (select == (int)manageSelection.RepairMode)
                    targetCost = 10;
                if (select == (int)manageSelection.UpgradeMode)
                    targetCost = targetTowerManager.upgradeCost[Mathf.Clamp(targetLv - 1, 0, 3)];
                break;

            case (int)towerAttackType.밀기:
                attackType.text = "밀기";
                towerImage.sprite = towerSpriteList[1];
                if (select == (int)manageSelection.RemoveMode)
                    targetCost = (int)((buildCostAdder[targetAttackType - 1] + 9) * 0.7);
                if (select == (int)manageSelection.RepairMode)
                    targetCost = 12;
                if (select == (int)manageSelection.UpgradeMode)
                    targetCost = targetTowerManager.upgradeCost[Mathf.Clamp(targetLv - 1, 0, 3)];
                break;

            case (int)towerAttackType.광역:
                attackType.text = "광역";
                towerImage.sprite = towerSpriteList[2];
                if (select == (int)manageSelection.RemoveMode)
                    targetCost = (int)((buildCostAdder[targetAttackType - 1] + 18) * 0.7);
                if (select == (int)manageSelection.RepairMode)
                    targetCost = 14;
                if (select == (int)manageSelection.UpgradeMode)
                    targetCost = targetTowerManager.upgradeCost[Mathf.Clamp(targetLv - 1, 0, 3)];
                break;

            case (int)towerAttackType.슬로우:
                attackType.text = "슬로우";
                towerImage.sprite = towerSpriteList[3];
                if (select == (int)manageSelection.RemoveMode)
                    targetCost = (int)((buildCostAdder[targetAttackType - 1] + 15) * 0.7);
                if (select == (int)manageSelection.RepairMode)
                    targetCost = 16;
                if (select == (int)manageSelection.UpgradeMode)
                    targetCost = targetTowerManager.upgradeCost[Mathf.Clamp(targetLv - 1, 0, 3)];
                break;
        }
        cost.text = targetCost.ToString();
    }
    void ManageMode()
    {
        if (manageMode)
        {
            if (manageTarget != null && manageTarget.tag == "Tower")
            {
                StartCoroutine("LoopViusalizeStatus");
            }
            if (Input.GetMouseButton(0))
            {
                Vector3 mousePos = Input.mousePosition;
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit, 100f, layerMask))
                {
                    manageTarget = hit.transform.gameObject;
                }
                if (manageTarget != null && manageTarget.tag == "Tower")
                {
                    VisualizeStatus(manageSelectBuffer);
                    manageOn = false;
                }
            }
            if (manageOn && manageTarget.tag == "Tower")
            {
                if (manageSelectBuffer == (int)manageSelection.RepairMode)
                {
                    TowerManager targetTowerManager = manageTarget.GetComponent<TowerManager>();
                    int targetAttackType = manageTarget.GetComponent<TowerManager>().damageType;
                    switch (targetAttackType)
                    {
                        case (int)towerAttackType.기본:
                            if (GameManager.resource >= 10)
                            {
                                targetTowerManager.hp.healthCheck = targetTowerManager.hp.health;
                                GameManager.resource -= 10;
                            }
                            break;

                        case (int)towerAttackType.밀기:
                            if (GameManager.resource >= 12)
                            {
                                targetTowerManager.hp.healthCheck = targetTowerManager.hp.health;
                                GameManager.resource -= 12;
                            }
                            break;

                        case (int)towerAttackType.광역:
                            if (GameManager.resource >= 14)
                            {
                                targetTowerManager.hp.healthCheck = targetTowerManager.hp.health;
                                GameManager.resource -= 14;
                            }
                            break;

                        case (int)towerAttackType.슬로우:
                            if (GameManager.resource >= 16)
                            {
                                targetTowerManager.hp.healthCheck = targetTowerManager.hp.health;
                                GameManager.resource -= 16;
                            }
                            break;
                    }
                    //자원 감소 코드
                    //타워 매니저 데미지 타입으로 설정하면 될듯
                }
                if (manageSelectBuffer == (int)manageSelection.RemoveMode)
                {
                    int targetAttackType = manageTarget.GetComponent<TowerManager>().damageType;
                    switch (targetAttackType)
                    {
                        case (int)towerAttackType.기본:
                            GameManager.resource += (int)((buildCostAdder[targetAttackType - 1] + 6) * 0.7);
                            break;

                        case (int)towerAttackType.밀기:
                            GameManager.resource += (int)((buildCostAdder[targetAttackType - 1] + 9) * 0.7);
                            break;

                        case (int)towerAttackType.광역:
                            GameManager.resource += (int)((buildCostAdder[targetAttackType - 1] + 18) * 0.7);
                            break;

                        case (int)towerAttackType.슬로우:
                            GameManager.resource += (int)((buildCostAdder[targetAttackType - 1] + 15) * 0.7);
                            break;
                    }
                    GameObject.Destroy(manageTarget);
                }
                if (manageSelectBuffer == (int)manageSelection.UpgradeMode)
                {
                    manageTarget.GetComponent<TowerManager>().UpgradeTower();
                    //int targetAttackType = manageTarget.GetComponent<TowerManager>().damageType;
                    //int targetLv = manageTarget.GetComponent<TowerManager>().towerLevel;
                    //int cost = 0;
                    //switch (targetAttackType)
                    //{
                    //    case (int)towerAttackType.기본:
                    //        cost = 4 + targetLv * 4;
                    //        if (GameManager.resource >= cost && targetLv < 5)
                    //        {
                    //            manageTarget.GetComponent<TowerManager>().towerLevel += 1;
                    //            GameManager.resource -= cost;
                    //        }
                    //        break;

                    //    case (int)towerAttackType.밀기:
                    //        cost = 7 + targetLv * 4;
                    //        if (GameManager.resource >= cost && targetLv < 5)
                    //        {
                    //            manageTarget.GetComponent<TowerManager>().towerLevel += 1;
                    //            GameManager.resource -= cost;
                    //        }
                    //        break;

                    //    case (int)towerAttackType.광역:
                    //        cost = 16 + targetLv * 4;
                    //        if (GameManager.resource >= cost && targetLv < 5)
                    //        {
                    //            manageTarget.GetComponent<TowerManager>().towerLevel += 1;
                    //            GameManager.resource -= cost;
                    //        }
                    //        break;

                    //    case (int)towerAttackType.슬로우:
                    //        cost = 13 + targetLv * 4;
                    //        if (GameManager.resource >= cost && targetLv < 5)
                    //        {
                    //            manageTarget.GetComponent<TowerManager>().towerLevel += 1;
                    //            GameManager.resource -= cost;
                    //        }
                    //        break;
                    //}
                }
                manageOn = false;
            }
        }
    }
    IEnumerator LoopViusalizeStatus()
    {
        VisualizeStatus(manageSelectBuffer);
        yield return 0;
    }

    void Move()
    {
        inputDir = Vector3.zero;
        if (Input.GetKey(KeyCode.A)) inputDir += Vector3.left;
        if (Input.GetKey(KeyCode.D)) inputDir += Vector3.right;
        if (Input.GetKey(KeyCode.S)) inputDir += Vector3.back;
        if (Input.GetKey(KeyCode.W)) inputDir += Vector3.forward;
        inputDir.Normalize();

        moveDir = Vector3.MoveTowards(moveDir, inputDir, Time.deltaTime * 5);
        controller.Move(moveDir * moveSpeed);
        if (inputDir != Vector3.zero)
            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(inputDir), Time.deltaTime * 5);
    }

    void PlayerDead()
    {
        if (health.healthCheck <= 0)
        {
            gameObject.SetActive(false);
        }
    }
    void PlayerHpCheck()
    {
        //체력이 변했는지 체크
        if (healthBuffer != health.healthCheck)
        {
            if (healthBuffer > health.healthCheck)
            {
                //체력이 줄음
                StartCoroutine(UnBeat());
            }
            //체력 변환 체크값을 현재 체력으로 바꿔줌
            healthBuffer = health.healthCheck;
        }
    }

    void HealthRegenerate()
    {
        healthTimer += Time.deltaTime;
        if (!canDamaged)
        {
            healthTimer = 0f;
        }
        if (healthTimer > healthTime)
        {
            if (health.healthCheck < health.health && healthUp == null)
            {
                healthUp = StartCoroutine(HealthUp());
                hpBar.gameObject.SetActive(true);
                playerRegenerating = true;
            }
            if (health.healthCheck == health.health)
            {
                hpBar.gameObject.SetActive(false);
                playerRegenerating = false;
            }
        }
    }
    IEnumerator HealthUp()
    {
        health.healthCheck += 1;
        yield return new WaitForSeconds(1f);
        healthUp = null;
    }

    void Clipping()
    {
        transform.position = new Vector3(Mathf.Clamp(transform.position.x, -enemySpawner.spawnPos[0] / 2, enemySpawner.spawnPos[0] / 2), transform.position.y,
            Mathf.Clamp(transform.position.z, -enemySpawner.spawnPos[1] / 2, enemySpawner.spawnPos[1] / 2));
    }
}

WaveManager.cs

웨이브의 시간이 될 때마다 웨이브를 소환해주는 스크립트

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class WaveManager : MonoBehaviour
{
    //웨이브 시간 관련 필드
    [Header("웨이브 대기 시간")]
    [SerializeField][Tooltip("현재 웨이브 남은 대기 시간")] float waveTime;
    [SerializeField][Tooltip("웨이브 기본 대기 시간")] float waveTimer;
    [SerializeField][Tooltip("웨이브 대기 시간 추가 가중치")] int timeAdder;
    GameObject enemyAlive;

    //현재 웨이브
    public int wave;
    public bool bossWave;

    //코드 참조
    WaveSystem waveSystem;

    //UI
    GameObject timer;

    //정화 웨이브
    [SerializeField] LabUI labUI;


    void Start()
    {
        bossWave = false;
        waveTime = 10f;
        waveTimer = 10f;
        timeAdder = 5;
        timeAdder = 0;
        wave = 0;
        waveSystem = GameObject.Find("waveSystem").GetComponent<WaveSystem>();
        timer = GameObject.Find("Timer");
    }
    void TimeAdd()
    {
        enemyAlive = GameObject.FindWithTag("Enemy");
        //몹이 전부 죽은 경우 wave 카운트 다운
        if (enemyAlive == null)
        {
            timer.SetActive(true);
            waveTime -= Time.deltaTime;
        }
        else
        {
            timer.SetActive(false);
        }

    }
    void waveStarter()
    {
        if (labUI.purifyBt)
        {
            wave = 100;
            Debug.Log("정화 웨이브 시작");
            waveTime += 30f;
        }
        if (waveTime < 0f)
        {
            if ((wave == 3 || wave == 5 || wave == 7 || wave == 9 || wave == 10) && bossWave)
            {
                bossWave = false;
                waveSystem.bossStage = true;
            }
            else if (wave < 10)
            {
                wave += 1;
                if (wave == 3 || wave == 5 || wave == 7 || wave == 9 || wave == 10) bossWave = true;
                waveSystem.bossStage = false;
            }
            waveSystem.waveStage = wave;
            Debug.Log(wave + " 웨이브 시작");
            waveTime = waveTimer + (timeAdder * waveSystem.waveStage);
            if (bossWave) waveTime -= timeAdder;
        }
    }
    void VisualizeTimer()
    {
        if (!bossWave) timer.GetComponent<Text>().text = "다음 웨이브까지 " + (int)waveTime + "초";
        else timer.GetComponent<Text>().text = "보스 등장까지 " + (int)waveTime + "초";
    }

    void Update()
    {
        TimeAdd();
        waveStarter();
        VisualizeTimer();
    }
}

EnemySpawner.cs

몬스터를 사각형 맵 범위 밖에 랜덤한 위치에 스폰시켜주는 스포너 스크립트

더보기
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class EnemySpawner : MonoBehaviour
{
    //적 프리팹 리스트
    public GameObject[] enemyPrefab;

    //적 스폰 타임
    //public float maxSpawnTime;
    //public float curSpawnTime;

    //맵 크기 좌표 x,z
    public int[] spawnPos = new int[2];
    public float spawnRange;

    public int selection;
    private void Start()
    {

    }


    // spawnPos = 1024 x 1080
    // 1024 / 2 = -512 ~ 512
    // 1080 / 2 = -540 ~ 540

    // Random 0 1
    // 0 : -pos
    // float posX = Random.Range(-700f, -512f);
    // 1 : +pos
    // float posX = Random.Range(+512f, 700f);

    public void EnemySpawn()
    {
        //정해진 적 선택 후 생성
        GameObject selectPrefab = enemyPrefab[selection];

        //소환할 좌표
        float posX;
        float posZ;

        int pos = Random.Range(0, 4);
        if (pos == 0)
        {
            posX = Random.Range((-spawnPos[0] / 2) - spawnRange, 0);
            if(posX < -spawnPos[0] / 2)
            {
                posZ = Random.Range(0, spawnPos[1] / 2 + spawnRange);
            }
            else
            {
                posZ = Random.Range(spawnPos[1] / 2, spawnPos[1] / 2 + spawnRange);
            }
        }
        else if (pos == 1)
        {
            posX = Random.Range(0, spawnPos[0] / 2 + spawnRange);
            if (posX > spawnPos[0] / 2)
            {
                posZ = Random.Range(0, spawnPos[1] / 2 + spawnRange);
            }
            else
            {
                posZ = Random.Range(spawnPos[1] / 2, spawnPos[1] / 2 + spawnRange);
            }
        }
        else if (pos == 2)
        {
            posX = Random.Range(0, spawnPos[0] / 2 + spawnRange);
            if (posX > spawnPos[0] / 2)
            {
                posZ = Random.Range(-(spawnPos[1] / 2 + spawnRange), 0);
            }
            else
            {
                posZ = Random.Range(-(spawnPos[1] / 2 + spawnRange), -(spawnPos[1] / 2));
            }
        }
        else
        {
            posX = Random.Range((-spawnPos[0] / 2) - spawnRange, 0);
            if (posX < -spawnPos[0] / 2)
            {
                posZ = Random.Range(-(spawnPos[1] / 2 + spawnRange), 0);
            }
            else
            {
                posZ = Random.Range(-(spawnPos[1] / 2 + spawnRange), -(spawnPos[1] / 2));
            }
        }
        Instantiate(selectPrefab, new Vector3(posX, 0.5f, posZ), Quaternion.identity);

    }
    public void Update()
    {

    }
}

WaveSystem.cs

웨이브의 몬스터 구성이 어떻게 되는지와 웨이브를 관리해주는 스크립트

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaveSystem : MonoBehaviour
{
    EnemySpawner spawner;
    GameObject poison;
    public int waveStage;
    public bool bossStage = false;
    int[] SpawnNum = new int[5];
    enum enemySelection
    {
        basic = 0,
        rush = 1,
        interrupt = 2,
        tanker = 3,
        healer = 4,
    }
    private void Start()
    {
        spawner = GameObject.Find("spawner").GetComponent<EnemySpawner>();
        poison = GameObject.Find("Poison");
        poison.SetActive(false);
        waveStage = 0;
    }

    void ModifyWaveInfo()
    {        
        if (waveStage == 1) SpawnNum = new int[] { 8, 3, 2, 0, 0 };
        else if(waveStage == 2) SpawnNum = new int[] { 10, 3, 2, 1, 0 };
        else if (waveStage == 3 && bossStage) SpawnNum = new int[] { 1, 0, 0, 0, 0 };
        else if (waveStage == 3) SpawnNum = new int[] { 10, 4, 4, 2, 1 };
        else if (waveStage == 4) SpawnNum = new int[] { 12, 5, 4, 2, 2 };
        else if (waveStage == 5 && bossStage) SpawnNum = new int[] { 0, 1, 0, 0, 0 };
        else if (waveStage == 5) SpawnNum = new int[] { 15, 5, 5, 3, 2 };
        else if (waveStage == 6) SpawnNum = new int[] { 15, 6, 6, 3, 3 };
        else if (waveStage == 7 && bossStage) SpawnNum = new int[] { 0, 0, 1, 0, 0 };
        else if (waveStage == 7) SpawnNum = new int[] { 17, 7, 6, 4, 3 };
        else if (waveStage == 8) SpawnNum = new int[] { 18, 7, 7, 4, 4 };
        else if (waveStage == 9 && bossStage) SpawnNum = new int[] { 0, 0, 0, 1, 0 };
        else if (waveStage == 9) SpawnNum = new int[] { 21, 8, 7, 5, 4 };
        else if (waveStage == 10 && bossStage) SpawnNum = new int[] { 0, 0, 0, 0, 1 };
        else if (waveStage == 10) SpawnNum = new int[] { 23, 8, 8, 5, 5 };
        else if (waveStage == 100) SpawnNum = new int[] { 25, 10, 8, 7, 5 };
		//개발 시간때문에 하드 코딩을 했지만 나중엔 하드코딩을 고쳐보아야한다.
        SpawnWave(SpawnNum);
    }
    
    
    void SpawnWave(int[] roopTime)
    {
        for(int i = 0; i < 5; i++)
        {
            spawner.selection = i;
            if (bossStage) spawner.selection += 5;
            for(int j = 0; j < roopTime[i]; j++)
            {
                spawner.EnemySpawn();
            }
        }
        waveStage = 0;
        bossStage = false;
        poison.SetActive(true);
    }

    private void Update()
    {
        if(waveStage != 0) ModifyWaveInfo();
    }
}

Posion.cs

독가스가 커지는것과 플레이어를 따라가는 것을 작성한 스크립트

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Poison : MonoBehaviour
{
    GameObject objPlayer;
    Player player;
    [SerializeField]
    float moveSpeed;
    [SerializeField]
    float growSpeed;
    [SerializeField]
    EnemySpawner spawner;

    int posionDamage = -5;
    // Start is called before the first frame update
    void Start()
    {
        objPlayer = GameObject.Find("Player");
        player = objPlayer.GetComponent<Player>();
        transform.position = Vector3.zero;
        PoisonSpawn();
    }

    void PoisonSpawn()
    {
        Vector3 direction = Vector3.zero;
        direction.x = Random.Range(-spawner.spawnPos[0] / 2, spawner.spawnPos[0] / 2);
        direction.z = Random.Range(-spawner.spawnPos[1] / 2, spawner.spawnPos[1] / 2);
        transform.position = direction;
    }
    void PoisonMove()
    {
        Vector3 playerPos = new Vector3(objPlayer.transform.position.x, 1, objPlayer.transform.position.z);
        transform.position = Vector3.MoveTowards(transform.position, playerPos, Time.deltaTime * moveSpeed);
    }

    void PoisonGrow()
    {
        if(transform.localScale.x < spawner.spawnPos[0] / 2)
            transform.localScale = new Vector3(transform.localScale.x + growSpeed, 1, transform.localScale.z + growSpeed);
    }

    private void OnTriggerStay(Collider other)
    {
        if(other.tag == "Player" && player.canDamaged)
        {
            player.canDamaged = false;
            player.ManageHP(posionDamage);
        }
    }

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

GameManager.cs

자원과 같은 부분을 담당하는 스크립트

GameManager라기보다 ResourceManager에 가깝다.

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    [Header("자원")]
    [Tooltip("타워에 관련된 자원")]
    [SerializeField] int reveal_resource;
    public static int resource;
    [Tooltip("클리어 정화가스")]
    [SerializeField] int reveal_purifyRs;
    [Tooltip("클리어 제작재료")]
    [SerializeField] int reveal_herb;
    public static int herb;
    public static int purifyRs;

    //public static List<GameObject> PickList = new List<GameObject>();

    void Start()
    {
        ResourceSetting();
    }
    private void Awake()
    {
        Application.targetFrameRate = 50;
    }

    
    void Update()
    {
        SynchronizeRs();
    }
    void ResourceSetting()
    {
        if (SceneManager.GetActiveScene().name == "MainScene")
        {
            resource = 30;
            purifyRs = 0;
            herb = 0;
        }
        else if(SceneManager.GetActiveScene().name == "Tutorial" || SceneManager.GetActiveScene().name == "ScenePSW")
        {
            resource = int.MaxValue;
        }
    }
    void SynchronizeRs()
    {
        reveal_purifyRs = purifyRs;
        reveal_resource = resource;
        reveal_herb = herb;
    }
}

 

PurifyLab.cs

정화가스를 제작하는 제작대에 관한 스크립트

게임 시작시 랜덤 위치 스폰에 관한 내용이 들어있다.

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PurifyLab : MonoBehaviour
{
    [SerializeField] EnemySpawner enemySpawner;
    public bool IsNearLab = false;
    Vector3 spawnloca = Vector3.zero;
    
    private void Start()
    {
        LabSpawn();
    }
    void LabSpawn()
    {
        spawnloca.x = Random.Range(-enemySpawner.spawnPos[0] / 2, enemySpawner.spawnPos[0] / 2);
        spawnloca.z = Random.Range(-enemySpawner.spawnPos[1] / 2, enemySpawner.spawnPos[1] / 2);
        spawnloca.y = 0.5f;
        transform.position = spawnloca;
    }
    private void OnTriggerStay(Collider other)
    {
        if(other.CompareTag("Player"))
        {
            IsNearLab = true;
        }
    }
    private void OnTriggerExit(Collider other)
    {
        if(other.CompareTag("Player"))
        {
            IsNearLab = false;
        }
    }
}

LabUI.cs

근처로 가면 UI를 작동시키고 아니라면 UI끄는 기능과 가스 제작에 관한 게이지바에 대한 기능이 들어있다.

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class LabUI : MonoBehaviour
{
    Camera cam;
    [SerializeField] Slider gauge;
    [SerializeField] float timer;
    [SerializeField] float purifyTime;
    public bool purifyBt = false;
    [SerializeField] PurifyLab purifyLab;
    [SerializeField] GameObject UI;
    bool manufactComple = false;
    // Start is called before the first frame update
    void Start()
    {
        cam = Camera.main;
        gauge.value = 0;
        gauge.enabled = false;
        UI.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Purify();
        VisualizeUI();
        //Debug.Log(Time.deltaTime);
    }
    IEnumerator GaugeIncrease()
    {
        gauge.value = 0;
        while(timer < purifyTime)
        {
            transform.rotation = Camera.main.transform.rotation;
            gauge.value = timer;
            yield return new WaitForSeconds(1f);
            timer += 1f;
        }
        manufactComple = true;
        gauge.value = 0;
        yield return null;
    }
    void Purify()
    {
        if(purifyBt)
        {
            if (GameManager.herb >= 5)
            {
                StartCoroutine(GaugeIncrease());
                GameManager.herb -= 5;
                purifyBt = false;
            }
            else
                purifyBt = false;
        }
    }
    public void PurifyButton()
    {
        if (GameManager.herb >= 5) purifyBt = true;
        //Debug.Log("Pressed");
    }
    void VisualizeUI()
    {
        if (purifyLab.IsNearLab)
        {
            UI.SetActive(true);
            if(manufactComple)
            {
                GameManager.purifyRs += 1;
                manufactComple = false;
                AlarmMessage.ins.Message("정화 가스를 획득했습니다.");
            }
        }
        else
        {
            UI.SetActive(false);
        }
    }
}

AlarmMessage.cs

메세지를 화면에 띄우는 UI

코루틴을 이용하여 중복해서 뜨는 것을 막고 싱글톤에 함수 처리를 활용하여 간편하게 메세지를 띄울 수 있도록 설계함

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

public class AlarmMessage : MonoBehaviour
{
    static AlarmMessage instance;
    public static Coroutine MessageCo;
    Image image;
    Text text;
    private void Awake()
    {
        image = GetComponentInChildren<Image>();
        text = GetComponentInChildren<Text>();
        image.gameObject.SetActive(false);
    }
    public static AlarmMessage ins
    {
        get
        {
            if(instance == null)
            {
                instance = FindObjectOfType<AlarmMessage>();
                if(ins == null)
                {
                    GameObject go = new GameObject();
                    instance = go.AddComponent<AlarmMessage>();
                    go.name = typeof(AlarmMessage).Name;
                }
            }
            return instance;
        }
    }
    public void Message(string contents)
    {
        if(MessageCo == null)
            MessageCo = StartCoroutine(ShowMessage(contents));
    }
    
    IEnumerator ShowMessage(string contents)
    {
        Debug.Log("show");
        text.text = contents;
        StartCoroutine(BlinkMessage());
        yield return new WaitForSeconds(1f);
    }

    IEnumerator BlinkMessage()
    {
        Debug.Log("blink");
        float timer = 0f;
        float blinkTime = 2f;
        float imageA = 0.6f;
        float textA = 1f;
        image.gameObject.SetActive(true);
        image.color = new Color(image.color.r, image.color.g, image.color.b, imageA);
        text.color = new Color(text.color.r, text.color.g, text.color.b, textA);
        while (timer < blinkTime)
        {
            timer += Time.deltaTime;
            imageA -= Time.deltaTime / 4;
            textA -= Time.deltaTime / 4;
            image.color = new Color(image.color.r, image.color.g, image.color.b, imageA);
            text.color = new Color(text.color.r, text.color.g, text.color.b, textA);
            yield return new WaitForSeconds(0.1f);
        }
        image.gameObject.SetActive(false);
        MessageCo = null;
        yield return null;
    }
}