프로젝트/프로젝트 A

# 024 GameManager (with.production)

효따 2025. 8. 30. 00:35

안녕하세요.

 

이번 작업은 크게 5가지를 해주었습니다.

 

1. 텔레포트 작업

 

2. 기본 공격 및 스킬의 연출효과

 

3. 매니저를 이용한 전체 시스템 메시지 및 흐름 관리

 

4. 플레이어 to 포탑 공격 

 

5. 아이콘 해제

 


 

먼저 텔레포트 작업입니다.

 

PlayerTeleport 스크립트입니다.

using UnityEngine;
using UnityEngine.AI;

public class PlayerTeleport : MonoBehaviour
{
    private Vector3 initialPosition;        
    private Vector3 returnStartPosition;    

    [SerializeField] private Animator animator;
    [SerializeField] private NavMeshAgent agent;
    [SerializeField] private GameObject returnParticle;
    private bool isReturning = false;

    void Start()
    {
        initialPosition = transform.position;
    }
    void Update()
    {
        if (isReturning)
        {
            float moved = Vector3.Distance(returnStartPosition, transform.position);
            if (moved > 0.1f)
            {
                CancelReturn();
            }
        }
        if (Input.GetKeyDown(KeyCode.B) && !isReturning)
        {
            returnStartPosition = transform.position;
            returnParticle.SetActive(true);
            isReturning = true;
            animator.SetTrigger("IsReturn");
        }
    }
    public void OnReturnAnimationEnd()
    {
        if (!isReturning) return;

        returnParticle.SetActive(false);
        agent.Warp(initialPosition);
        isReturning = false;
    }
    private void CancelReturn()
    {
        returnParticle.SetActive(false);
        isReturning = false;
        animator.Play("IdleToRun", 0, 0f);  
    }
}

 

핵심 부분을 빠르게 살펴보자면.

    void Update()
    {
        if (isReturning)
        {
            float moved = Vector3.Distance(returnStartPosition, transform.position);
            if (moved > 0.1f)
            {
                CancelReturn();
            }
        }
        if (Input.GetKeyDown(KeyCode.B) && !isReturning)
        {
            returnStartPosition = transform.position;
            returnParticle.SetActive(true);
            isReturning = true;
            animator.SetTrigger("IsReturn");
        }
    }

 

Update에서 Input.GetKeyDown() 을 이용해 KeyCode.B가 눌리면

파티클 활성화와 애니메이션 등 관련 작업을 실행시켜주었습니다.

 

또한 Vector3의 Distance를 이용해 B 키를 누른 위치와

플레이어의 현재 위치가 0.1f보다 크다면 이동한 것으로 판단하여

CancleReturn 함수를 실행하게 해 주었습니다.

 

    public void OnReturnAnimationEnd()
    {
        if (!isReturning) return;

        returnParticle.SetActive(false);
        agent.Warp(initialPosition);
        isReturning = false;
    }
    private void CancelReturn()
    {
        returnParticle.SetActive(false);
        isReturning = false;
        animator.Play("IdleToRun", 0, 0f);  
    }

 

귀환이 완료되면 실행될 OnReturnAnimationEnd() 함수는 단순합니다.

NavMeshAgent를 이용하여 플레이어의 이동을 하고 있기에

NavMeshAgent의 Warp 함수를 사용하여 귀환 위치로 이동시켜 주었고

 

CancleReturn() 함수는 귀환 도중 끊겼을 때 즉시 애니메이션 전환과 파티클을 비활성화해주었습니다.

 


 

다음은 기본공격 및 스킬의 연출효과 강화입니다.

 

기존의 기본 도형으로 사용되던 오브젝트의 Mesh Renderer를 꺼준 뒤

자식으로 파티클을 지정해 주었습니다.

 


 

다음은 GameManager 스크립트입니다.

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public enum GameState
    {
        Ready, Play, Pause, Stop
    }
    public GameState gameState = GameState.Ready;

    public static GameManager Instance = null;

    void Awake()
    {
        Instance = this;
    }

}

 

enum을 활용하여 Ready, Play, Pause, Stop을 지정해 주었으며

 

Enemy가 Spawn 되는 EnemySpawn 스크립트에서

    private IEnumerator SpawnEnemies()
    {
        while (true)
        {
            if (GameManager.Instance.gameState == GameManager.GameState.Ready)
            {
                yield return null;
                continue;
            }

 

현재 게임의 상태가 Ready 상태이면 생성이 안되게 Continue로 제어를 해주었습니다.

 


 

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

public class SystemMessage : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI textMeshProUGUI;
    [SerializeField] private GameObject systemMessageObj;

    // 0초
    private string messageStep1 = "소환사의 협곡에 오신것을 환영합니다";
    // 30초
    private string messageStep2 = "미니언 생성까지 30초 남았습니다";
    // 60초
    private string messageStep3 = "미니언이 생성되었습니다";

    private byte currentStep = 0;
    [SerializeField] private float messageOffTime = 5.0f;

    public void SystemMessagePopup()
    {
        if (currentStep == 0)
        {
            textMeshProUGUI.text = messageStep1;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
        else if (currentStep == 1)
        {
            textMeshProUGUI.text = messageStep2;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
        else if (currentStep == 2)
        {
            GameManager.Instance.gameState = GameManager.GameState.Play;
            textMeshProUGUI.text = messageStep3;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
    }

    IEnumerator MessageOff()
    {
        yield return new WaitForSeconds(messageOffTime);
        systemMessageObj.SetActive(false);
    }
}

 

다음은 SystemMessage를 만들어 주었습니다.

 

string 타입으로 text를 미리 지정해 주었으며,

    public void SystemMessagePopup()
    {
        if (currentStep == 0)
        {
            textMeshProUGUI.text = messageStep1;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
        else if (currentStep == 1)
        {
            textMeshProUGUI.text = messageStep2;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
        else if (currentStep == 2)
        {
            GameManager.Instance.gameState = GameManager.GameState.Play;
            textMeshProUGUI.text = messageStep3;
            systemMessageObj.SetActive(true);
            currentStep++;
            StartCoroutine(MessageOff());
        }
    }

 

특이사항으로 GameManager의 gameState를 Play로 변경해 주어 미니언이 Spawn 되게 해주었습니다.

 

또한

 

currentStep 변수를 증가시켜 주어

 

각 다른 조건문이 실행되게 해 주었습니다.

 

    IEnumerator MessageOff()
    {
        yield return new WaitForSeconds(messageOffTime);
        systemMessageObj.SetActive(false);
    }

 

다음 실행되는 코루틴은 지정 시간 지연 후 시스템 메시지를 비활성화해주는 코드입니다.

 


 

이 함수가 실행되는 곳은

 

기존에 게임의 흐른 시간을 나타내는 ElapsedTimeDisplay 스크립트에서 작업을 해주었습니다.

 

using TMPro;
using UnityEngine;

public class ElapsedTimeDisplay : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI elapsedText;
    private float elapsedTime = 0f;
    private float nextPopupTime = 0f;
    [SerializeField] private SystemMessage systemMessage;
    public void ElapsedResult()
    {
        elapsedTime += Time.deltaTime;
        int minutes = Mathf.FloorToInt(elapsedTime / 60f);
        int seconds = Mathf.FloorToInt(elapsedTime % 60f);

        elapsedText.text = string.Format("{0:00}:{1:00}", minutes, seconds);
 
        if (elapsedTime >= nextPopupTime)
        {
            systemMessage.SystemMessagePopup();
            nextPopupTime += 30f
        }
    }
}

 

현재 흐른 시간인 elapsedTime을 판단하여 주었으며

 

각 0초, 30초, 60초에 함수가 실행됩니다.

 


 

다음으로는 플레이어가 포탑을 공격하는 작업을 해주었습니다.

 

PlayerSkillQ를 예로 들면

 

public class PlayerSkillQ : MonoBehaviour
{
    [SerializeField] private PlayerSkillController playerSkillController;
    [SerializeField] private PlayerStats playerStats;
    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Enumy")
        {
            EnemyStats enumyStats = other.gameObject.GetComponent<EnemyStats>();
            enumyStats?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);

            playerSkillController.ResetSkillObject();
        }
        if (other.tag == "Golem")
        {
            GolemAI golemAI = other.gameObject.GetComponent<GolemAI>();
            golemAI?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);

            playerSkillController.ResetSkillObject();
        }
        if (other.tag == "TurretEnemy")
        {
            TurretStats turretStat = other.gameObject.GetComponent<TurretStats>();
            turretStat?.TakeDamage(playerStats.playerDamage * playerStats.playerSkillQDamage);
        }
    }
}

 

이 스킬 오브젝트의 TriggerEnter가 된 other의 태그가 "TurretEnemy" 일 때를 추가해 주었습니다.

 

 

또한 기본 플레이어가 기본 공격을 할 때에도

    private void TryAttack()
    {
        if (Time.time >= lastAttackTime + attackCooldown)
        {
            lastAttackTime = Time.time;

            if (currentTarget.tag == minionTag)
            {
                currentTarget.GetComponent<EnemyStats>().TakeDamageEnemy(enemyStats.enemyDamage);
            }
            if (currentTarget.tag == turretTag)
            {
                currentTarget.GetComponent<TurretStats>().TakeDamage(enemyStats.enemyDamage);
            }
            enumyAnim.SetTrigger("IsAttack");
        }
    }

 

위와 같이 turret의 태그를 검사해 TakeDamage 함수를 실행시켜

 

대미지를 주도록 작업했습니다.

 


 

마지막으로 포탑이 사라질 때, 아이콘 제거 부분도 추가해 주었습니다.

 

TurretDestory 스크립트를 만들어 주었으며, 다음과 같이 작업을 했습니다.


using UnityEngine;
using UnityEngine.UI;

public class TurretDestory : MonoBehaviour
{
    [SerializeField] private Image enemyTurret01;
    [SerializeField] private Image enemyTurret02;
    [SerializeField] private Image enemyTurret03;
    [SerializeField] private Image Turret01;
    [SerializeField] private Image Turret02;
    [SerializeField] private Image Turret03;

    [SerializeField] private Sprite destoryTurretIcon;

    public void EnemyTurretDestoryIcon(int number)
    {
        if (number == 1)
        {
            enemyTurret01.sprite = destoryTurretIcon;
        }
        else if (number == 2)
        {
            enemyTurret02.sprite = destoryTurretIcon;
        }
        else if (number == 3)
        {
            enemyTurret03.sprite = destoryTurretIcon;
        }
    }
    public void TurretDestoryIcon(int number)
    {
        if (number == 1)
        {
            Turret01.sprite = destoryTurretIcon;
        }
        else if (number == 2)
        {
            Turret02.sprite = destoryTurretIcon;
        }
        else if (number == 3)
        {
            Turret03.sprite = destoryTurretIcon;
        }
    }
}

 

이 함수가 실행되는 곳은 Turret의 Hp가 0이 되어 Destory로 인해 사라질 때입니다.

using UnityEngine;

public class TurretStats : MonoBehaviour
{
    public float damage = 5.0f;

    public float turretMaxHp = 200.0f;
    public float turretHp = 0f;
    [SerializeField] private TurretHpBar turretHpBar;

    [SerializeField] private TurretDestory turretDestory;
    [SerializeField] private string classification = "";
    [SerializeField] private int number;
    void Start()
    {
        turretMaxHp = turretHp;
    }

    public void TakeDamage(float damage)
    {
        turretHp -= damage;
        turretHpBar.HpVarUpdate(turretHp, turretMaxHp);
        if (turretHp <= 0)
        {
            if (classification == "EnemyTurret")
            {
                turretDestory.EnemyTurretDestoryIcon(number);
            }
            else if (classification == "Turret")
            {
                turretDestory.TurretDestoryIcon(number);
            }

            Destroy(this.gameObject);
        }
    }
}

 

구분은 string이 EnemyTurret / Turret 두 가지로 나눴습니다.

 

 


 

마지막으로 시스템 메시지의 간격을 30초 -> 10초로 변경하고,

 

아군의 미니니언만 대미지를 올려서 

 

 

촬영된 영상을 보는 것으로 마무리를 하겠습니다.

 

 

맨날 집에 오면 9시~10시 여서 작업을 할 시간이 많이 부족했는데,

 

드디어 주말입니다!

 

 

내일은 이어서 디버깅 및 개선사항들과 함께

 

플레이어 죽음 관련, 그리고 승리 조건까지를 목표로 잡아보도록 하겠습니다.

 

감사합니다.😁


 

모두들 한 주 고생하셨고

 

즐거운 주말 보내세요!

 

 

'프로젝트 > 프로젝트 A' 카테고리의 다른 글

# 026 Git (with. ScriptableObject)  (3) 2025.09.01
# 025 Victory (with. DOTween)  (4) 2025.08.30
# 023 Object Icon (with. Minimap Drag & Click)  (2) 2025.08.29
# 022 Map Design  (4) 2025.08.28
# 021 Golem (2) (with. PlayerHpBar)  (7) 2025.08.26