프로젝트/프로젝트 A

# 025 Victory (with. DOTween)

효따 2025. 8. 30. 23:42

안녕하세요.🙂

 

오늘은 크게 4가지를 작업했습니다.

 

1. 플레이어가 포탑에 공격될 때

2. 플레이어 사망 관련

3. 넥서스 공격

4. 승리 / 패배

 


 

 

먼저 플레이어가 포탑에 공격될 때입니다.

 

포탑이 플레이어를 공격하는 작업은 기존 TurretAttackObject 스크립트에서 구현했습니다.

 

    private void HitTarget(GameObject target)
    {
        EnemyStats enemy = target.GetComponent<EnemyStats>();
        if (enemy != null)
        {
            enemy.TakeDamageTurret(turrentStats.damage);
        }
        else
        {
            PlayerStats player = target.GetComponent<PlayerStats>();
            if (player != null && player.tag == "Player")
            {
                player.TakeDamage(turrentStats.damage);
            }
        }

        gameObject.SetActive(false);
    }

 

조건으로 EnemyStats을 가져온다면 Enemy가 있는 것으로 판단였고

 

EnemyStats를 못 가져온다면 범위 내에 Enemy가 없는 것으로 판단하여

 

플레이를 공격하게 해 주었습니다.


 

플레이어를 판단하는 조건은 다음과 같습니다.

 

    private void AcquireTarget()
    {
        GameObject nearestTarget = null;
        float closestDistance = float.MaxValue;

        // 1. 플레이어 먼저 체크
        if (player != null && player.tag == "Player")
        {
            float playerDistance = Vector3.Distance(transform.position, player.position);
            if (playerDistance <= attackRange)
            {
                nearestTarget = player.gameObject;
                closestDistance = playerDistance;
            }
        }

        // 2. 미니언 체크
        GameObject[] enemies = GameObject.FindGameObjectsWithTag(minionTag);
        foreach (var enemy in enemies)
        {
            float distance = Vector3.Distance(transform.position, enemy.transform.position);
            if (distance <= attackRange && distance < closestDistance)
            {
                closestDistance = distance;
                nearestTarget = enemy;
            }
        }

        currentTarget = nearestTarget;
    }

 

가장 먼저 범위 내에 플레이어가 있는지 판단합니다.

 

그다음 미니언을 체크해 주었습니다.

 

거리 체크는 Vector3의 Distance로 동일하게 해 주었고

 

동일하게 currentTarget을 지정해 주었습니다.

 


 

다음은 플레이어 사망 관련입니다.

 

게임의 전체 상태를 관리하는 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;
    }

}

 

플레이어가 사망하게 되면 

 

게임의 전체 흐름을 관리하는 GameManager의 GameStat를 Pause로 변경해 주었습니다.

    void Update()
    {
        if (GameManager.Instance.gameState == GameManager.GameState.Pause || GameManager.Instance.gameState == GameManager.GameState.Stop)
        {
            return;
        }
        foreach (var skill in skills)
        {
            HandleSkillInput(skill);
            HandleSkillCooldown(skill);
            HandleSkillMovement(skill);
        }
    }

 

이는 플레이어의 스킬이나, 움직임 등 

 

플레이어가 죽어있을 때 동작되지 않게 하기 위함입니다.


 

또한 PlayerDieController 스크립트를 만들어 주었습니다.

using TMPro;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.UI;
using System.Collections;

public class PlayerDieController : MonoBehaviour
{
    [Header("Player Components")]
    [SerializeField] private NavMeshAgent playerAgent;
    [SerializeField] private GameObject playerMesh;
    [SerializeField] private GameObject playerCanvas;
    [SerializeField] private PlayerStats playerStats;

    [Header("Respawn Settings")]
    [SerializeField] private Vector3 originPos;
    [SerializeField] private TextMeshProUGUI cooldownText;
    [SerializeField] private GameObject cooldownObj;
    [SerializeField] private Image profileImage;
    [SerializeField] private GameObject dieBackground;

    private void Start()
    {
        originPos = transform.position;
    }

    public void PlayerDieOn()
    {
        GameManager.Instance.gameState = GameManager.GameState.Pause;
        gameObject.tag = "Untagged";

        playerMesh.SetActive(false);
        playerCanvas.SetActive(false);

        cooldownObj.SetActive(true);
        profileImage.fillAmount = 0;

        playerAgent.Warp(originPos);

        dieBackground.SetActive(true);

        int level = playerStats.playerLevel;
        float waitTime = level * 5f;

        StartCoroutine(ResurrectionCooldown(waitTime));
    }

    private IEnumerator ResurrectionCooldown(float waitTime)
    {
        float elapsed = 0f;

        while (elapsed < waitTime)
        {
            elapsed += Time.deltaTime;

            profileImage.fillAmount = Mathf.Clamp01(elapsed / waitTime);

            cooldownText.text = Mathf.Ceil(waitTime - elapsed).ToString("F0");

            yield return null;
        }


        PlayerResurrection();
    }

    private void PlayerResurrection()
    {
        GameManager.Instance.gameState = GameManager.GameState.Play;
        gameObject.tag = "Player";

        playerMesh.SetActive(true);
        playerCanvas.SetActive(true);
        cooldownObj.SetActive(false);

        dieBackground.SetActive(false);

        playerStats.PlayerResurrection();
    }
}

 


 

먼저 변수 선언부부터 살펴보겠습니다.

    [Header("Player Components")]
    [SerializeField] private NavMeshAgent playerAgent;
    [SerializeField] private GameObject playerMesh;
    [SerializeField] private GameObject playerCanvas;
    [SerializeField] private PlayerStats playerStats;

    [Header("Respawn Settings")]
    [SerializeField] private Vector3 originPos;
    [SerializeField] private TextMeshProUGUI cooldownText;
    [SerializeField] private GameObject cooldownObj;
    [SerializeField] private Image profileImage;
    [SerializeField] private GameObject dieBackground;

 

 

Player Components

playerAgent: NavMeshAgent, 플레이어 이동 제어

playerMesh: 실제 플레이어 모델 렌더링

playerCanvas: 플레이어 UI(체력바, 이름 등)

playerStats: 플레이어 능력치/레벨 정보

 

Respawn Settings

originPos: 부활 시 위치

cooldownText: 부활 대기 시간 표시 UI

cooldownObj: 부활 UI 오브젝트

profileImage: 부활 게이지 UI

dieBackground: 죽음 배경/화면 효과

 

플레이어가 포탑에 공격될 때

TurretAttackObject

TurretAttack

 


 

Start() 함수입니다.

    private void Start()
    {
        originPos = transform.position;
    }

 

 

플레이어가 처음 위치한 좌표를 부활 지점(originPos)으로 저장합니다.

이후 사망 시 이 위치로 플레이어를 되돌리기 위함입니다.

 


 

 

플레이어 사망 처리 함수 PlayerDieOn() 함수입니다.

    public void PlayerDieOn()
    {
        GameManager.Instance.gameState = GameManager.GameState.Pause;
        gameObject.tag = "Untagged";

        playerMesh.SetActive(false);
        playerCanvas.SetActive(false);

        cooldownObj.SetActive(true);
        profileImage.fillAmount = 0;

        playerAgent.Warp(originPos);

        dieBackground.SetActive(true);

        int level = playerStats.playerLevel;
        float waitTime = level * 5f;

        StartCoroutine(ResurrectionCooldown(waitTime));
    }

 

주요 기능

 

1. 게임 상태를 Pause로 변경 → 플레이어가 죽어 있는 동안 움직임이나 입력 차단

2. 태그를 "Untagged"로 변경 → 다른 스크립트가 플레이어를 대상으로 하는 행동 방지

3. 플레이어 모델과 UI 비활성화

4. 부활 UI 활성화 및 초기화

5. NavMeshAgent를 이용해 플레이어를 초기 위치(originPos)로 이동

6. 레벨 기반 부활 대기 시간 계산 (waitTime = level * 5)

7. 부활 대기 코루틴 시작

8. 플레이어 사망 관련


 

부활까지 남은 시간을 카운트하는 코루틴 ResurrectionColldown입니다.

    private IEnumerator ResurrectionCooldown(float waitTime)
    {
        float elapsed = 0f;

        while (elapsed < waitTime)
        {
            elapsed += Time.deltaTime;

            profileImage.fillAmount = Mathf.Clamp01(elapsed / waitTime);

            cooldownText.text = Mathf.Ceil(waitTime - elapsed).ToString("F0");

            yield return null;
        }
        PlayerResurrection();
    }

 

주요 기능

 

1. 경과 시간을 elapsed에 누적

2. 프로필 이미지 게이지(profileImage.fillAmount)와 남은 시간 텍스트(cooldownText.text) 업데이트

3. waitTime이 끝나면 PlayerResurrection() 호출 → 플레이어 부활

 


 

플레이어 부활 처리 함수 PlayerResurrection() 함수입니다.

    private void PlayerResurrection()
    {
        GameManager.Instance.gameState = GameManager.GameState.Play;
        gameObject.tag = "Player";

        playerMesh.SetActive(true);
        playerCanvas.SetActive(true);
        cooldownObj.SetActive(false);

        dieBackground.SetActive(false);

        playerStats.PlayerResurrection();
    }

 

주요 기능

 

1. 게임 상태를 Play로 변경 → 플레이어가 다시 행동 가능

2. 태그 "Player"로 복원 → 다른 스크립트가 플레이어를 인식하도록

3. 플레이어 모델, UI, 죽음 배경, 부활 UI 복원

4. playerStats.PlayerResurrection() 호출 → 플레이어 능력치/상태 초기화

 


 

다음은 넥서스 공격입니다.

기본 Enemy는 상대 Minion과 Turret만 타깃을 잡고 있었기에

 

Nexus를 추가해 주었습니다.

 

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

            if (currentTarget.CompareTag(minionTag))
            {
                currentTarget.GetComponent<EnemyStats>().TakeDamageEnemy(enemyStats.enemyDamage);
            }
            else if (currentTarget.CompareTag(turretTag))
            {
                currentTarget.GetComponent<TurretStats>().TakeDamage(enemyStats.enemyDamage);
            }
            else if (currentTarget.gameObject == nexusObj)
            {
                currentTarget.GetComponent<NexusStats>().TakeDamage(enemyStats.enemyDamage);
            }

            enumyAnim.SetTrigger("IsAttack");
        }
    }

 

+

Player도 마찬가지로 공격 타깃을 넥서스도 가능하게 해 주었습니다.

        if (Input.GetMouseButtonDown(1))
        {
            RaycastHit hit;

            if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, Mathf.Infinity))
            {
                if (hit.collider.CompareTag("Ground"))
                {
                    PlayerNavToMove(hit.point);
                    PlayerRotation(hit.point);
                }
                else if (hit.collider.CompareTag("Enumy"))
                {
                    if (outline != null)
                        outline.enabled = false;
                    outline = hit.transform.GetComponent<Outline>();
                    PlayerNavToEnumy(hit.collider.transform);
                }
                else if (hit.collider.CompareTag("Golem"))
                {
                    if (outline != null)
                        outline.enabled = false;
                    outline = hit.transform.GetComponent<Outline>();
                    PlayerNavToEnumy(hit.collider.transform);
                }
                else if (hit.collider.CompareTag("TurretEnemy"))
                {
                    if (outline != null)
                        outline.enabled = false;
                    outline = hit.transform.GetComponent<Outline>();
                    PlayerNavToEnumy(hit.collider.transform);
                }
                else if (hit.collider.CompareTag("EnemyNexus"))
                {
                    if (outline != null)
                        outline.enabled = false;
                    outline = hit.transform.GetComponent<Outline>();
                    PlayerNavToEnumy(hit.collider.transform);
                }
            }
        }

 

또한 터렛의 파괴될 때 새로 만든 NexusController 스크립트에

 

변수를 증가시켜 주어

 

변수가 3 이상일 때 Nexus의 Hpbar를 활성화되게 해 주었습니다.

using UnityEngine;

public class NexusController : MonoBehaviour
{
    public int turretCount = 0;
    public int enemyTurretCount = 0;

    [SerializeField] GameObject turretHpBarObj;
    [SerializeField] GameObject enemyTurrethpBarObj;

    public void TurretCountUp()
    {
        turretCount++;
        if (enemyTurretCount >= 3)
        {
            NexusHpController();
        }
    }
    public void EnemyTurretCountUp()
    {
        enemyTurretCount++;
        if (enemyTurretCount >= 3)
        {
            EnemyNexusHpController();
        }
    }
    private void NexusHpController()
    {
        turretHpBarObj.SetActive(true);
    }

    private void EnemyNexusHpController()
    {
        enemyTurrethpBarObj.SetActive(true);
    }

}

 


 

다음은 승리 / 패배 조건입니다.

 

승리했을 때와 패배했을 때의 각 이미지를 가져와 

 

DOTween을 사용해 연출 효과를 줘봤습니다.

 

using UnityEngine;
using DG.Tweening;

public class ObjectTweenEffect : MonoBehaviour
{
    [SerializeField] private RectTransform vicObject;   // Victory UI 오브젝트
    [SerializeField] private RectTransform defObject;   // Defeat UI 오브젝트

    private Vector3 startPos = Vector3.zero;          // 원래 위치
    private Vector3 hiddenPos = new Vector3(0, 1200, 0); // 화면 위로 올린 위치

    void Start()
    {
        // 시작할 때는 위로 올려놓고 비활성화
        vicObject.anchoredPosition3D = hiddenPos;
        defObject.anchoredPosition3D = hiddenPos;

        vicObject.gameObject.SetActive(false);
        defObject.gameObject.SetActive(false);
    }

    public void GameVictory()
    {
        vicObject.gameObject.SetActive(true);
        vicObject.anchoredPosition3D = hiddenPos;

        vicObject.DOAnchorPos3D(startPos, 3f)
            .SetEase(Ease.OutBounce);
    }

    public void GameDefeat()
    {
        defObject.gameObject.SetActive(true);
        defObject.anchoredPosition3D = hiddenPos;

        defObject.DOAnchorPos3D(startPos, 3f)
            .SetEase(Ease.OutBounce);
    }
}

 


 

먼저 변수선언부입니다.

    [SerializeField] private RectTransform vicObject;  
    [SerializeField] private RectTransform defObject;  

    private Vector3 startPos = Vector3.zero;        
    private Vector3 hiddenPos = new Vector3(0, 1200, 0);

 

 

vicObject / defObject: 승리, 패배 UI를 각각 가리키는 RectTransform

startPos: UI가 보일 원래 위치 (0,0,0 기준)

hiddenPos: 화면 밖(위쪽, y=1200) 위치로, 처음엔 이곳에 숨겨둠


 

 

Start() 초기 설정입니다.

    void Start()
    {
        // 시작할 때는 위로 올려놓고 비활성화
        vicObject.anchoredPosition3D = hiddenPos;
        defObject.anchoredPosition3D = hiddenPos;

        vicObject.gameObject.SetActive(false);
        defObject.gameObject.SetActive(false);
    }

 


 

승리 / 패배했을 때입니다.

    public void GameVictory()
    {
        vicObject.gameObject.SetActive(true);
        vicObject.anchoredPosition3D = hiddenPos;

        vicObject.DOAnchorPos3D(startPos, 3f)
            .SetEase(Ease.OutBounce);
    }

    public void GameDefeat()
    {
        defObject.gameObject.SetActive(true);
        defObject.anchoredPosition3D = hiddenPos;

        defObject.DOAnchorPos3D(startPos, 3f)
            .SetEase(Ease.OutBounce);
    }

 

동작 순서

 

SetActive(true)

UI 오브젝트를 화면에 나타나게 함

 

anchoredPosition3 D = hiddenPos

UI 위치를 화면 위쪽으로 올려 숨김 상태로 초기화

hiddenPos = (0, 1200, 0)

 

DOAnchorPos3 D(startPos, 3f)

DOTween을 사용해서 3초 동안 startPos로 이동

startPos = (0,0,0) → 화면 중앙

3초 동안 UI가 천천히 내려오는 애니메이션

 

. SetEase(Ease.OutBounce)

이동하는 동안 바운스 효과 적용

UI가 내려오다 약간 튀면서 멈추는 자연스러운 느낌

 

재미있는 효과들이 있어서 조금 더 찾아봤습니다.


  1. Linear
  • 일정한 속도로 움직임
  • 가속이나 감속 없이 일정하게 이동
  • 사용 예: Ease.Linear
  1. Quadratic, Cubic, Quart, Quint
  • 가속과 감속을 조절하는 곡선 기반 이동
  • 종류
    • In: 시작은 느리고 끝으로 갈수록 빨라짐
    • Out: 시작 빠르고 끝으로 갈수록 느려짐
    • InOut: 시작과 끝은 느리고 중간은 빠름
  • 사용 예: Ease.InQuad, Ease.OutCubic, Ease.InOutQuart
  1. Back
  • 이동 시작이나 끝에서 살짝 뒤로 당기거나 튀는 느낌
  • 주로 UI 팝업이나 등장 애니메이션에 적합
  • 사용 예: Ease.OutBack, Ease.InBack
  1. Elastic
  • 목표 위치에 도달 후 탄성처럼 몇 번 튕기며 안정
  • 캐릭터 점프나 UI 팝업에 재미있는 연출 가능
  • 사용 예: Ease.OutElastic, Ease.InElastic
  1. Bounce
  • 공처럼 튕기는 효과
  • UI나 오브젝트가 바닥에 닿는 듯한 느낌
  • 사용 예: Ease.OutBounce, Ease.InBounce, Ease.InOutBounce
  1. Flash, Shake, InFlash
  • 순간적으로 흔들리거나 깜빡이는 느낌
  • 주로 효과 강조용
  • 사용 예: Ease.Flash, Ease.InFlash, Ease.Shake

사용 팁

  • UI 등장 애니메이션에는 OutBounce, OutBack가 자연스럽다
  • 캐릭터 이동에는 InOutQuad, InOutCubic이 부드럽다
  • 강조 효과에는 Elastic, Flash, Shake가 적합하다

DOTween 공식문서 사이트입니다.

https://dotween.demigiant.com/

 

DOTween (HOTween v2)

DOTween is a fast, efficient, fully type-safe object-oriented animation engine for Unity, optimized for C# users, free and open-source, with tons of advanced features It is also the evolution of HOTween, my previous Unity tween engine. Compared to it, DOTw

dotween.demigiant.com



 

마지막으로 아군 Minion의 대미지를 1000으로 높게 잡고,

 

즉시 스폰되게 하여 테스트 영상을 살펴보겠습니다.

 

 

 

작업을 하면서 생각해 보니

 

Sound 신경을 전혀 안 쓰고 있었더라고요..

 

그리하여..

 

내일은 사운드 작업과 함께 Git으로 관리를 할 예정입니다.


 

또한 포탑이 파괴될 때 무료 Asset인 Particle을 사용해 봤는데

 

은근히 재미있는 포인트가 더해졌습니다.

 

 

이를 통해 조금 더 게임에 연출효과를 강화하고

 

재미를 더 해줄 부분을 고민해 봐야겠다고 생각했습니다.

 


 

 

감사합니다.

 

 

 

 

 

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

# 027 Portfolio video (with. perf)  (0) 2025.09.22
# 026 Git (with. ScriptableObject)  (3) 2025.09.01
# 024 GameManager (with.production)  (2) 2025.08.30
# 023 Object Icon (with. Minimap Drag & Click)  (2) 2025.08.29
# 022 Map Design  (4) 2025.08.28