프로젝트/프로젝트 A

#004 Skill Motion 1 (with. UI)

효따 2025. 8. 10. 19:40

안녕하세요.

 

이번 글에서는 스킬 모션에 대한 작업을 올리려고 합니다.

 

 

먼저 지난 글에서는 스킬 쿨다운을 위해 Image를 생성해 주었었는데요,

 

추가적으로 기본 UI 들도 함께 배치를 해주었습니다.

Figma Slide

 


 

다음은 스킬에 대한 작업을 하기 위해 Qude로 스킬 범위를 제작하려고 했었는데요

 

오브젝트의 위치에 따라 UI가 보이고 / 안 보이고 하던 문제가 있었습니다.

 

카메라의 Clipping Planes 값 또는 각종 인스펙터의 속성을 찾아봐도 문제가 될 만한 건 없었는데요...

 

결국 찾은 원인은

 

그라운드가 가진 Surface Type이 문제였습니다.😨

(솔직히 상상도 못했습니다)

 

이전 그라운드를 배치할 때 그라운드 위에 또 하나의 Plane을 두어 분위기 연출을 해보려고 했었는데

그 때 Transparent로 변경을 했었던것 같습니다...

 

찾아보니 Transparent는 깊이 버퍼를 사용하지 않아 순서 문제로

인해 카메라의 각도/위치에 따라 보였다 안보였다 할 수 있다는 점이었습니다.

 

그리하여 그라운드는 Transparent를 사용할 것도 아니고...

바로 "누가 앞에 있는지" 정확히 계산해 주는 Opaque로 변경해 해결해 주었습니다.

 


 

우선 어제에 이어 "SkillController" 스크립트에 추가 작업을 해줬습니다.

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

public class SkillController : 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 Transform playerTransform;

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

    [SerializeField] private List<Skill> skills;

    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.skill1Object != null)
            {
                skill.skillStartPos = skill.skill1Object.transform.position;
                skill.skill1Object.SetActive(false);
            }
        }
    }

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

    private void HandleSkillInput(Skill skill)
    {
        if (Input.GetKey(skill.skillKeyCode) && !skill.isCoolingDown)
        {
            if (!skill.skillRangeQuadParent.activeSelf)
                skill.skillRangeQuadParent.gameObject.SetActive(true);

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit, Mathf.Infinity) && skill == skills[0])
            {
                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[0])
            {
                if (skill.playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        skill.playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skill1Object != null)
                {
                    skill.skill1Object.SetActive(true);
                    skill.skillIsMoving = true;

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

                    skill.skillMoveDirection = skill.playerTransform.forward.normalized;

                    skill.skillStartPos = skill.skill1Object.transform.position;

                    skill.skill1Object.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 HandleSkill1Movement(Skill skill)
    {
        if (!skill.skillIsMoving || skill.skill1Object == null)
            return;

        skill.skill1Object.transform.position += skill.skillMoveDirection * skill.skillMoveSpeed * Time.deltaTime;

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

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

    public void ResetSkill1Object(Skill skill)
    {
        if (skill.skill1Object == null)
            return;

        skill.skill1Object.transform.position = skill.skillStartPos;
        skill.skill1Object.SetActive(false);
        skill.skillIsMoving = false;
    }
}

 

추가된 부분을 하나씩 살펴보겠습니다.

 

1.  SkillController > Skill 스크립트에 사용할 변수를 선언해 주었습니다.

        public GameObject skillRangeQuadParent;
        public Transform playerTransform;

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

skillRangeQuadParent : 스킬의 범위를 구성할 Quad Object입니다.

playerTransform : 플레이어의 위치를 가져올 Transform입니다.

skillObject : 스킬을 사용할 때 발사될 Object입니다.

skillStartPos : 발사될 Object의 초기 위치를 저장할 Vector3입니다.

skillIsMoving : 스킬이 사용 중인지 판단할 Bool입니다.

skillMoveDistance : 발사될 스킬의 거리값입니다.

skillMoveSpeed : 발사될 스킬의 속도값입니다.

skillMoveDirection : 스킬의 이동 방향벡터입니다.

 

 

2. Start 함수에서 초기화를 해 주었습니다.

    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.skill1Object != null)
            {
                skill.skillStartPos = skill.skill1Object.transform.position;
                skill.skill1Object.SetActive(false);
            }
        }
    }

 

 

3. Update 함수에서 스킬 이동처리 함수를 실행하게 해 주었습니다. (1번 스킬)

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

 

 

4. 기존 HandleSkillInput 함수에 Input.GetKey 함수를 작성해 주었습니다.

스킬 범위를 나타낼 Quad가 꺼져있으면 켜주었으며 위/아래 성분을 제거하여 수평 방향으로만 범위가 회전하게 해 주었습니다.

    private void HandleSkillInput(Skill skill)
    {
        if (Input.GetKey(skill.skillKeyCode) && !skill.isCoolingDown)
        {
            if (!skill.skillRangeQuadParent.activeSelf)
                skill.skillRangeQuadParent.gameObject.SetActive(true);

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit, Mathf.Infinity) && skill == skills[0])
            {
                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[0])
            {
                if (skill.playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        skill.playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skill1Object != null)
                {
                    skill.skill1Object.SetActive(true);
                    skill.skillIsMoving = true;

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

                    skill.skillMoveDirection = skill.playerTransform.forward.normalized;

                    skill.skillStartPos = skill.skill1Object.transform.position;

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

 

 

5. 다음은 GetKeyDown의 조건문을 GetKeyUp으로 변경하게 해 주었습니다.

스킬을 누르고 있을 때(GetKey)엔 스킬의 범위가

스킬을 눌렀다 뗐을때(GetKeyUp)엔 사용 스킬이

나가도록 구현을 하기 위함입니다.

 

또한 GetKey가 눌렸을 때 생겼던 Quad는 비활성화를 해주었고, 플레이어가 범위의 방향을 바라보게 회전을 설정해 주었습니다.

회전도 마찬가지로 y축을 0으로 제한해 주어 수평 회전만 되게 해 주었으며

LookRotation을 사용하여 회전이 되게 해 주었습니다.

마지막으로 발사될 스킬도 똑같은 위치에 발사되게 하기 위해서 방향도 저장해 주었습니다.

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

            if (skill == skills[0])
            {
                if (skill.playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        skill.playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skill1Object != null)
                {
                    skill.skill1Object.SetActive(true);
                    skill.skillIsMoving = true;

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

                    skill.skillMoveDirection = skill.playerTransform.forward.normalized;

                    skill.skillStartPos = skill.skill1Object.transform.position;

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

 

 

6 다음은 스킬 오브젝트를 활성화하며 위치와 방향을 정해주었습니다.

. 스킬도 플레이어 회전과 마찬가지로 LookRotation() 함수를 사용하여 좀 전에 저장해 뒀던 이동 방향인 skillMoveDirection을 인자로 넘겨줬습니다.

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

            if (skill == skills[0])
            {
                if (skill.playerTransform != null)
                {
                    Vector3 lookDir = skill.skillRangeQuadParent.transform.forward;
                    lookDir.y = 0;
                    if (lookDir.sqrMagnitude > 0.01f)
                    {
                        skill.playerTransform.rotation = Quaternion.LookRotation(lookDir);
                        skill.skillMoveDirection = lookDir.normalized;
                    }
                }
                if (skill.skill1Object != null)
                {
                    skill.skill1Object.SetActive(true);
                    skill.skillIsMoving = true;

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

                    skill.skillMoveDirection = skill.playerTransform.forward.normalized;

                    skill.skillStartPos = skill.skill1Object.transform.position;

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

 

 

7. 발사될 스킬 오브젝트의 이동처리 함수를 작성해 주었습니다.

단순히 transform.position += 방향 * 스피드 * 델타타임으로 이동이 되게 해 주었으며

현재는 설정한 거리 이상으로 넘어가면 ResetSkill1 Object() 함수가 실행되며 초기화를 해주었습니다.

    private void HandleSkill1Movement(Skill skill)
    {
        if (!skill.skillIsMoving || skill.skill1Object == null)
            return;

        skill.skill1Object.transform.position += skill.skillMoveDirection * skill.skillMoveSpeed * Time.deltaTime;

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

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

 

 

8. 발사된 스킬 오브젝트의 초기화 함수입니다.

스킬의 위치 초기화, 비활성화 로직을 구현해 주었습니다.

    public void ResetSkill1Object(Skill skill)
    {
        if (skill.skill1Object == null)
            return;

        skill.skill1Object.transform.position = skill.skillStartPos;
        skill.skill1Object.SetActive(false);
        skill.skillIsMoving = false;
    }

 

 


 

 

 

감사합니다.

(스킬의 속도는 이즈리얼의 Q와 같은 느낌으로 사용하려고 하는데, 현재는 테스트용으로 느리게 맞춰놨습니다!)


 

추가적으로 코드를 계속 보던 중 불필요해 보이던 로직이 있었는데요

바로 skill.skillStartPos를 초기화 해주던 부분들 이였습니다.

 

GetKey가 눌릴 때 skill.skillStartPos는 플레이어의 위치를 기준으로 초기화가 되기에

skill.skillStartPos의 좌표를 다른 곳에서 초기화를 해 줄 필요가 없었습니다.

 

과감히 제거를 해주었습니다.😀

 

 

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

#006 Click (with. Object Pooling)  (4) 2025.08.11
#005 Skill Motion 2~4  (1) 2025.08.10
#003 Skill (with. UI & Cooldown)  (2) 2025.08.10
#002 Camera (with. cinemachine)  (3) 2025.08.10
#001 Movement (with. NaviMeshAgent, Animator)  (3) 2025.08.09