프로젝트/프로젝트 A

# 009 Enumy Hp (With. Skill Attack)

효따 2025. 8. 15. 23:04

안녕하세요.😊

 

이번 작업은 Enumy를 공격해서 Enumy의 Hp Bar가 줄어드는 작업을 진행했습니다.

 

 

가장 먼저 이전에 작업했던 Skill에 애니메이션 전이 조건을 실행되게 작업하려던 도중

 

스킬을 발사할 때 원하던 방향으로 발사체가 날아가지만,

 

원하던 위치에 생기지 않는 문제를 발견했습니다.

 

 

수정 전 코드입니다.

skill.skill1 Object.transform.position = playerTransform.position + new Vector3(0f, 1f, 0.4f);

 

스킬 오브젝트의 포지션이 생기는 위치를

 

플레이어의 기준 + 살짝 위 방향에서 z 축 0.4f의 위치에 생성되도록 했었는데요

 

 

바로 원인을 살펴보자면

 

단순히 봤을 땐 이상이 없어 보이지만!

 

새로 만든 new Vector3()가 캐릭터의 전방 기준이 아닌

 

월드 전방 기준으로 적용되던 문제였습니다.

 

 

수정 후 코드

skill.skill1 Object.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));

 

즉 플레이어가 어느 위치에 있던 플레이어의 월드 축 (z = 0.4f)가 되던 문제로

 

방향벡터를 오브젝트의 현재 회전에 맞춰서 월드 좌표계 방향으로 변환해 주는 함수

 

playerTransform.TransformDirection()를 사용하여 수정해 주었습니다.

 

 


 

 

가장 먼저 PlayerSkillController 스크립트의 수정 및 추가된 부분부터 살펴보겠습니다.

using TMPro;
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using UnityEngine.AI;
using System.Collections;
public class PlayerSkillController : MonoBehaviour
{
    [Serializable]
    public class Skill
    {
        public Image skillImage;
        public TextMeshProUGUI skillText;
        public KeyCode skillKeyCode;
        public float cooldown;

        [HideInInspector] public bool isCoolingDown = false;
        [HideInInspector] public float currentCooldown = 0f;

        public GameObject skillRangeQuadParent;

        public GameObject skillObject;
        [HideInInspector] public Vector3 skillStartPos;
        [HideInInspector] public bool skillIsMoving = false;
        public float skillMoveDistance = 5f;
        public float skillMoveSpeed = 2f;
        [HideInInspector] public Vector3 skillMoveDirection = Vector3.zero;
    }

    [SerializeField] private Transform playerTransform;
    [SerializeField] private Animator playerAnim;
    [SerializeField] private NavMeshAgent playerNav;
    [SerializeField] private List<Skill> skills;
    private Vector3? skillETargetPos = null;

    [SerializeField] private Skill fireObjectInpomation;

    public bool isSkill01 = false;
    public bool isSkill02 = false;
    public bool isSkill03 = false;
    public bool isSkill04 = false;
    void Start()
    {
        foreach (var skill in skills)
        {
            if (skill.skillImage != null)
                skill.skillImage.fillAmount = 0f;
            if (skill.skillText != null)
                skill.skillText.text = "";
            if (skill.skillRangeQuadParent != null)
                skill.skillRangeQuadParent.gameObject.SetActive(false);

            if (skill.skillObject != null)
            {
                // skill.skillStartPos = skill.skillObject.transform.position;
                skill.skillObject.SetActive(false);
            }
        }
    }

    void Update()
    {
        foreach (var skill in skills)
        {
            HandleSkillInput(skill);
            HandleSkillCooldown(skill);
            HandleSkillMovement(skill);
        }
    }

    private void HandleSkillInput(Skill skill)
    {
        if (Input.GetKey(skill.skillKeyCode) && !skill.isCoolingDown)
        {

            if (!skill.skillRangeQuadParent.activeSelf)
                skill.skillRangeQuadParent.gameObject.SetActive(true);

            if (skill == skills[2])
            {
                Plane groundPlane = new Plane(Vector3.up, playerTransform.position);
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                float enter;

                if (groundPlane.Raycast(ray, out enter))
                {
                    Vector3 hitPoint = ray.GetPoint(enter);

                    Vector3 playerPos = playerTransform.position;
                    playerPos.y = 0;

                    Vector3 targetPos = hitPoint;
                    targetPos.y = 0;

                    Vector3 dir = targetPos - playerPos;

                    // 최대 거리 제한
                    if (dir.magnitude > skill.skillMoveDistance)
                    {
                        dir = dir.normalized * skill.skillMoveDistance;
                        targetPos = playerPos + dir;
                    }

                    if (!skill.skillRangeQuadParent.activeSelf)
                        skill.skillRangeQuadParent.SetActive(true);

                    if (skill.skillObject != null)
                    {
                        skill.skillObject.SetActive(true);
                        skill.skillObject.transform.position = new Vector3(targetPos.x, playerTransform.position.y + 0.01f, targetPos.z);

                        skillETargetPos = skill.skillObject.transform.position;
                    }
                }
            }
            else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
            {
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hit;

                if (Physics.Raycast(ray, out hit, Mathf.Infinity))
                {
                    Vector3 dir = hit.point - skill.skillRangeQuadParent.transform.position;
                    dir.y = 0;
                    skill.skillRangeQuadParent.transform.rotation = Quaternion.LookRotation(dir);
                }
            }
        }

        if (Input.GetKeyUp(skill.skillKeyCode) && !skill.isCoolingDown)
        {
            skill.isCoolingDown = true;
            skill.currentCooldown = skill.cooldown;
            skill.skillRangeQuadParent.gameObject.SetActive(false);

            if (skill == skills[2])
            {
                if (skillETargetPos.HasValue)
                {
                    Vector3 teleportPos = skillETargetPos.Value;
                    teleportPos.y = playerTransform.position.y;

                    Vector3 lookDir = (teleportPos - playerTransform.position);
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
                    }

                    if (playerNav != null)
                    {
                        StartCoroutine(SkillEDelay(teleportPos));
                    }
                    else
                    {
                        playerTransform.position = teleportPos;
                    }
                    skillETargetPos = null;
                }

                if (skill.skillObject != null)
                    skill.skillObject.SetActive(false);
            }
            else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
            {
                if (playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skillObject != null)
                {

                }
                if (skill != null)
                {
                    if (skill == skills[0] || skill == skills[1])
                    {
                        if (skill == skills[0])
                        {
                            playerAnim.SetBool("IsSkillQ", true);
                        }
                        else if (skill == skills[1])
                        {
                            playerAnim.SetBool("IsSkillW", true);
                        }
                        ObjectGen(skill);
                    }
                    else if (skill == skills[3])
                    {                        
                        if (skill == skills[3])
                        {
                            StartCoroutine(SkillRDelay(skill));
                        }
                    }
                }
            }
        }
    }
    private IEnumerator SkillEDelay(Vector3 pos)
    {
        isSkill03 = true;
        playerAnim.SetTrigger("IsSkillE");
        yield return new WaitForSeconds(0.5f);
        playerNav.Warp(pos);
        yield return new WaitForSeconds(0.5f);
        isSkill03 = false;
    }
    private IEnumerator SkillRDelay(Skill skill)
    {
        isSkill04 = true;
        playerAnim.SetTrigger("IsSkillR");
        yield return new WaitForSeconds(1f);
        isSkill04 = false;
        ObjectGen(skill);
    }
    private void ObjectGen(Skill skill)
    {
        print("33");
        skill.skillObject.SetActive(true);
        skill.skillIsMoving = true;

        skill.skillObject.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));

        skill.skillMoveDirection = playerTransform.forward.normalized;

        skill.skillStartPos = skill.skillObject.transform.position;

        skill.skillObject.transform.rotation = Quaternion.LookRotation(skill.skillMoveDirection);
    }

    private void HandleSkillCooldown(Skill skill)
    {
        if (skill.isCoolingDown)
        {
            skill.currentCooldown -= Time.deltaTime;

            if (skill.currentCooldown <= 0f)
            {
                skill.isCoolingDown = false;
                skill.currentCooldown = 0f;

                if (skill.skillImage != null)
                    skill.skillImage.fillAmount = 0f;
                if (skill.skillText != null)
                    skill.skillText.text = "";
            }
            else
            {
                if (skill.skillImage != null)
                    skill.skillImage.fillAmount = skill.currentCooldown / skill.cooldown;
                if (skill.skillText != null)
                    skill.skillText.text = Mathf.Ceil(skill.currentCooldown).ToString();
            }
        }
    }

    private void HandleSkillMovement(Skill skill)
    {
        if (!skill.skillIsMoving || skill.skillObject == null)
            return;
        fireObjectInpomation = skill;
        skill.skillObject.transform.position += skill.skillMoveDirection * skill.skillMoveSpeed * Time.deltaTime;

        float traveledDist = Vector3.Distance(skill.skillObject.transform.position, skill.skillStartPos);

        if (traveledDist >= skill.skillMoveDistance)
        {            
            ResetSkillObject();
        }
    }

    public void ResetSkillObject()
    {
        if (fireObjectInpomation.skillObject == null)
            return;

        // skill.skillObject.transform.position = skill.skillStartPos;
        if (fireObjectInpomation.skillObject != null)
        {
            fireObjectInpomation.skillObject.SetActive(false);
        }
        fireObjectInpomation.skillIsMoving = false;
        playerAnim.SetBool("IsSkillQ", false);
        playerAnim.SetBool("IsSkillW", false);
        fireObjectInpomation = null;
    }
}

 


 

 

1. 먼저 스킬에 각 애니메이션을 적용시켜 주기 위해 스킬 키를 떼었을 때 작업을 해주었습니다.

        if (Input.GetKeyUp(skill.skillKeyCode) && !skill.isCoolingDown)
        {
            skill.isCoolingDown = true;
            skill.currentCooldown = skill.cooldown;
            skill.skillRangeQuadParent.gameObject.SetActive(false);

            if (skill == skills[2])
            {
                if (skillETargetPos.HasValue)
                {
                    Vector3 teleportPos = skillETargetPos.Value;
                    teleportPos.y = playerTransform.position.y;

                    Vector3 lookDir = (teleportPos - playerTransform.position);
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
                    }

                    if (playerNav != null)
                    {
                        StartCoroutine(SkillEDelay(teleportPos));
                    }
                    else
                    {
                        playerTransform.position = teleportPos;
                    }
                    skillETargetPos = null;
                }

                if (skill.skillObject != null)
                    skill.skillObject.SetActive(false);
            }
            else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
            {
                if (playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skillObject != null)
                {

                }
                if (skill != null)
                {
                    if (skill == skills[0] || skill == skills[1])
                    {
                        if (skill == skills[0])
                        {
                            playerAnim.SetBool("IsSkillQ", true);
                        }
                        else if (skill == skills[1])
                        {
                            playerAnim.SetBool("IsSkillW", true);
                        }
                        ObjectGen(skill);
                    }
                    else if (skill == skills[3])
                    {                        
                        if (skill == skills[3])
                        {
                            StartCoroutine(SkillRDelay(skill));
                        }
                    }
                }
            }
        }
    }

 

 

1.1 스킬 "E" 처리 부분입니다.

스킬이 즉시 이동되는 것이 원하던 연출이 아니었기에 코루틴으로 시간을 지연시켜 주게 작업을 해주었습니다.

            if (skill == skills[2])
            {
                if (skillETargetPos.HasValue)
                {
                    Vector3 teleportPos = skillETargetPos.Value;
                    teleportPos.y = playerTransform.position.y;

                    Vector3 lookDir = (teleportPos - playerTransform.position);
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir.normalized);
                    }

                    if (playerNav != null)
                    {
                        StartCoroutine(SkillEDelay(teleportPos));
                    }
                    else
                    {
                        playerTransform.position = teleportPos;
                    }
                    skillETargetPos = null;
                }

                if (skill.skillObject != null)
                    skill.skillObject.SetActive(false);
            }

 

 

1.2 스킬 "Q, W, R" 처리 부분입니다.

기본 로직에서 크게 달라진 건 없지만 특이사항으로

스킬 Q와 W는 딜레이를 주지 않고

스킬 R 같은 경우는 딜레이를 주었습니다.

            else if (skill == skills[0] || skill == skills[1] || skill == skills[3])
            {
                if (playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skillObject != null)
                {

                }
                if (skill != null)
                {
                    if (skill == skills[0] || skill == skills[1])
                    {
                        if (skill == skills[0])
                        {
                            playerAnim.SetBool("IsSkillQ", true);
                        }
                        else if (skill == skills[1])
                        {
                            playerAnim.SetBool("IsSkillW", true);
                        }
                        ObjectGen(skill);
                    }
                    else if (skill == skills[3])
                    {                        
                        if (skill == skills[3])
                        {
                            StartCoroutine(SkillRDelay(skill));
                        }
                    }
                }
            }

 

 


 

 

2. 스킬 "E" 일 때 사용되는 코루틴입니다.

애니메이션의 전이 조건을 즉시 실행시켜 주었으며

0.5초 후 대기시간을 준 뒤 NavMeshAgent의 Warp를 활용해 플레이어를 텔레포트시켜 주었습니다.

 

다음 0.5초의 딜레이를 주는 이유는 단순히 연출 효과인데

플레이어가 스킬 E를 쓰고 자연스럽게 애니메이션이 "IdelToRun"으로 돌아오게 한 뒤 이동이 되게 하기 위함입니다.

    private IEnumerator SkillEDelay(Vector3 pos)
    {
        isSkill03 = true;
        playerAnim.SetTrigger("IsSkillE");
        yield return new WaitForSeconds(0.5f);
        playerNav.Warp(pos);
        yield return new WaitForSeconds(0.5f);
        isSkill03 = false;
    }

 

 


 

 

3. 스킬 "R" 일 때 사용되는 코루틴입니다.

일명 롤에서 "궁극기"라고도 불리는 스킬 "R"은 그에 걸맞게(?) 딜레이를 좀 더 주었으며 (1f)

ObjectGen() 함수를 실행시켜 주었습니다.

    private IEnumerator SkillRDelay(Skill skill)
    {
        isSkill04 = true;
        playerAnim.SetTrigger("IsSkillR");
        yield return new WaitForSeconds(1f);
        isSkill04 = false;
        ObjectGen(skill);
    }

 

 


 

 

4. 다음으로 스킬 "Q, W, R"이 실행되면 호출될 ObjectGen() 함수입니다.

기존에 Input.GetKeyUp()이 실행될 때 작성되어 있던 코드를

함수로 나누어 재사용을 해주었습니다.

    private void ObjectGen(Skill skill)
    {
        skill.skillObject.SetActive(true);
        skill.skillIsMoving = true;

        skill.skillObject.transform.position = playerTransform.position + playerTransform.TransformDirection(new Vector3(0f, 0.5f, 0.4f));

        skill.skillMoveDirection = playerTransform.forward.normalized;

        skill.skillStartPos = skill.skillObject.transform.position;

        skill.skillObject.transform.rotation = Quaternion.LookRotation(skill.skillMoveDirection);
    }

 

 


 

 

5. 다음은 스킬 오브젝트의 리셋 함수입니다.

이 리셋 함수는 오브젝트가 일정 거리 이상 이동했을 때와, Enumy에게 맞았을 때 호출 됩니다.

특별히 추가가 된 곳은 애니메이션의 전이 조건을 False로 해주어

반복 실행이 안되게 해 주었습니다.

    public void ResetSkillObject()
    {
        if (fireObjectInpomation.skillObject == null)
            return;

        // skill.skillObject.transform.position = skill.skillStartPos;
        if (fireObjectInpomation.skillObject != null)
        {
            fireObjectInpomation.skillObject.SetActive(false);
        }
        fireObjectInpomation.skillIsMoving = false;
        playerAnim.SetBool("IsSkillQ", false);
        playerAnim.SetBool("IsSkillW", false);
        fireObjectInpomation = null;
    }

 

 


 

 

6. 다음은 스킬 Q에 부착될 스크립트 "PlayerQ"를 만들어 주었습니다.

OnTriggerEnter()로 콜라이더가 충돌될 때 Enumy의 기존에 만들어둔 "EnumyStats" 스크립트에 접근하여

TakeDamage() 함수를 실행시켜 주었습니다.

(Q와 동일하게 W, R 스크립트도 같습니다 (E는 공격 스킬이 아니기에 애니메이션만 실행되게 해 주었습니다.))

 

using UnityEngine;

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

            playerSkillController.ResetSkillObject();
        }
    }
}

 

 


 

 

7. 플레이어의 기본 스텟입니다.

임시로 만들어둔 Hp, Damage, Attack Speed에 더해

Q, W, R의 대미지를 선언해 주었습니다.

(스킬 Q 대미지 = PlayerDamage * PlayerSkillQDamage)

 

다음 TakeDamage() 함수는 아직 미구현입니다.

using UnityEngine;

public class PlayerStats : MonoBehaviour
{
    private float playerHp = 100f;
    public float playerDamage = 10f;
    public float playerSkillQDamage = 2f;
    public float playerSkillWDamage = 0f;
    public float playerSkillRDamage = 4f;
    public float playerAttackSpeed = 3f;

    public void TakeDamage(GameObject player, float damage)
    {
        playerHp -= damage;
        if (playerHp <= 0)
        {
            Destroy(this.gameObject);
        }
    }
}

 

 


 

 

8. 다음은 "EnumyStats" 스크립트입니다.

선언된 변수는 다음과 같습니다.

최대 체력 / 현재 체력 / 공격력 / 공격 스피드 

 

EnumyStats의 TakeDamage 함수는 인자로 damage를 받아서

(현재 체력 = 현재 체력 - 대미지)를 적용시켜 주었습니다.

 

또한 EnumyHpBar의 스크립트를 새로 생성하여 UI - Hp Bar(Slider)를 따로 관리해 주었습니다.

using UnityEngine;

public class EnumyStats : MonoBehaviour
{
    [SerializeField] private EnumyHpBar enumyHpBar;
    public float enumyMaxHp = 100f;
    public float enumyHp = 100f;
    public float enumyDamage = 10f;
    public float enumyAttackSpeed = 3f;

    public void TakeDamage(float damage)
    {
        print("11");
        enumyHp -= damage;
        enumyHpBar.HpVarUpdate(enumyHp, enumyMaxHp);
        if (enumyHp <= 0)
        {
            Destroy(this.gameObject);
        }
    }
}

 

 


 

 

9. EnumyHpBar 스크립트입니다.

Enumy에 부착되어 있는 Slider를 가져와

Slider.value를 제어해 주었으며

Slider.value의 최솟값: 0 / 최댓값:1입니다.

using UnityEngine;
using UnityEngine.UI;

public class EnumyHpBar : MonoBehaviour
{
    [SerializeField] private Slider sliderHpBar;

    public void HpVarUpdate(float hp, float maxHp)
    {
        // print("22");
        sliderHpBar.value = hp / maxHp;
    }
}

 

 


 

 

10. 마지막으로 플레이어의 이동을 제어하는 스크립트인 "PlayerMovement"에서 다음과 같이 조건문을 걸어주었습니다.

특이사항으로, 내비게이션의 목적지 초기화 및 속도 제어와 멈춤을 해준 이유는

플레이어가 이동 중 일 때 스킬을 사용할 수 있기 때문입니다.

 

이러한 조건문이 없는 상태로 이동도중 스킬을 발사하게 되면 

스킬 애니메이션이 진행되면서 플레이어가 이동을 하는 상황이 나오게 됩니다.

    private void MoveController()
    {
        if (playerSkillController.isSkill03 || playerSkillController.isSkill04)
        {
            playerNav.isStopped = true;
            playerNav.ResetPath();
            playerNav.velocity = Vector3.zero;
            return;
        }
        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);
                }
            }
        }
        if (enumyGameobject != null)
        {
            if (Vector3.Distance(transform.position, enumyGameobject.position) > stopDistance)
            {
                playerNav.SetDestination(enumyGameobject.position);
            }
        }
    }

 

 


Player Animator

-참고-

 

작업된 내용을 영상으로 살펴보겠습니다.

 

 

 

 

아직 전체적으로 뭔가 자연스럽지 않으면서.. 어색한 부분들이 많은 것 같습니다.

 

그래도 "첫 술에 배부르랴"라는 속담이 있지 않습니까? 하하

Rome wasn’t built in a day

 

앞으로도 프로젝트의 모든 과정은 최대한 기록을 할 예정이며

 

과정과 결과로 보여드리겠습니다.

 


 

 

감사합니다.😌

 

 

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

#011 Minimap  (3) 2025.08.16
# 010 Player Mp (with. Runtime Performance Stats)  (9) 2025.08.16
# 008 Attack (with. Animation)  (10) 2025.08.15
#007 Follow (with. Enumy)  (5) 2025.08.13
#006 Click (with. Object Pooling)  (4) 2025.08.11