게임 개발 공부 기록

59일차 - Abyss_Slayer 최종 팀 프로젝트 9 (메이지 스킬 구현)

00lwt 2025. 4. 16. 22:23

▶▷ 메이지 a 스킬

어제에 이어 여러개의 투사체를 발사하는 메이지의 a 스킬을 마저 구현했다.

스킬 사용 시 플레이어의 주변에 투사체가 생성되고 타겟을 따라가는 유도탄 발사 스킬이다.

MageSkill_a.cs

using UnityEngine;
using System.Collections;

[CreateAssetMenu(menuName = "Skill/Mage/Mage_a")]
public class MageSkill_a : SkillExecuter
{
    public float damage;

    [Header("발사정보")]
    public int count;                       // 투사체 개수
    public float startCircleR;              // 투사체 시작 위치 반지름
    public float fireDegree;                // 중심 발사 각도
    public float spreadDegree;              // 투사체 퍼짐 각도

    [Header("투사체 정보")]
    public float homingPower;               // 유도 강도
    public float speed;                     // 투사체 속도
    public float homingTime;                // 유도 지속 시간
    public AnimationCurve homingCurve;      // 유도 세기 커브

    private MageSkillRangeVisualizer rangeVisualizer;

    public override void Execute(Player user, Boss target, SkillData skillData)
    {
        // 범위 시각화 컴포넌트를 플레이어에 추가하고 설정
        if (rangeVisualizer == null)
        {
            rangeVisualizer = user.gameObject.AddComponent<MageSkillRangeVisualizer>();
        }
        rangeVisualizer.SetRange(skillData.targetingData.range);

        // 타겟이 없을 경우 범위 내에서 탐색
        if (target == null)
        {
            Collider2D[] colliders = Physics2D.OverlapCircleAll(user.transform.position, skillData.targetingData.range, LayerMask.GetMask("Enemy"));
            
            foreach (var collider in colliders)
            {
                if (collider.TryGetComponent<Boss>(out Boss boss))
                {
                    target = boss;
                    break;
                }
            }
        }

        // 탐색 후에도 타겟이 없다면 스킬 발동 취소
        if (target == null)
        {
            Debug.LogAssertion("타겟이 없습니다.");
            return;
        }

        // 지정된 개수만큼 투사체 생성
        for (int i = 0; i < count; i++)
        {
            // 각 투사체의 발사 각도를 계산
            float degree = fireDegree - ((spreadDegree / 2) - (i * spreadDegree / (count - 1)));
            degree = user.SpriteRenderer.flipX ? 180 - degree : degree;
            Quaternion rotate = Quaternion.Euler(0, 0, degree);

            // 투사체 시작 위치 계산 (원을 기준으로 배치)
            degree *= Mathf.Deg2Rad;
            Vector3 position = new Vector3(Mathf.Cos(degree), Mathf.Sin(degree)) * startCircleR;
            position = user.transform.TransformPoint(position);

            // 유도탄 초기화 데이터 투사체에 전달
            PoolManager.Instance.Get<MageProjectile>().Init(damage, position, rotate, target.transform, speed, homingPower, homingTime, homingCurve);
        }
    }
}

스킬을 사용하게 되면 먼저 OverlapCircleAll을 통해 타게팅 데이터에 설정해둔 range안의 Enemy 레이어를 가진 오브젝트를 탐색한다.

범위 내에 타겟이 없다면 스킬이 작동하지 않고, 있다면 count의 수만큼 반복문을 돌려서 투사체를 생성한다.
투사체의 발사 각도는 플레이어가 바라보는 방향을 기준으로 왼쪽을 보면 180도를 빼서 반대쪽으로 생성되게 하였다.

투사체를 생성한 다음엔 해당 투사체들의 정보를 Init을 통해 메이지 투사체 스크립트에 전달한다.

MageProjectile.cs

public class MageProjectile : BasePoolable
{
    float homingTime;
    [SerializeField] AnimationCurve speedCurve;
    [SerializeField] AnimationCurve homingCurve;
    [SerializeField] TrailRenderer trailRenderer;

    // Init으로 전달받은 매개변수를 저장하는 변수
    Transform target;
    float damage, inputSpeed, speed, homingPower, fireTime;
    bool fired;

    void Awake()
    {
        trailRenderer.enabled = false;
    }

    private void Update()
    {
        if(fired)
        {
            Move();
            Rotate();
            UpdateTrailPosition();
        }
    }

    // 투사체 궤적 위치 업데이트
    private void UpdateTrailPosition()
    {
        trailRenderer.transform.position = transform.position;
        trailRenderer.transform.rotation = transform.rotation;
    }

    public override void Init()
    {
        // BasePoolable의 추상 메서드 구현
    }
    
    /// <summary>
    /// 법사 유도탄 스킬 초기화
    /// </summary>
    /// <param name="damage">데미지</param>
    /// <param name="position">생성위치</param>
    /// <param name="rotation">생성 시 회전값</param>
    /// <param name="target">따라갈 목표</param>
    /// <param name="speed">투사체 속도(비례하여 유동적으로 변화)</param>
    /// <param name="homingPower">투사체 유도력(비례하여 유동적으로 변경)</param>
    /// <param name="homingTime">투사체 유도시간</param>
    /// <param name="homingCurve">투사체 유도곡선</param>
    public void Init(float damage, Vector3 position, Quaternion rotation, Transform target, float speed, float homingPower, float homingTime, AnimationCurve homingCurve)
    {
        transform.position = position;
        transform.rotation = rotation;
        this.damage = damage;
        this.target = target;
        inputSpeed = speed;
        this.homingPower = homingPower;
        if(homingCurve != null)
            this.homingCurve = homingCurve;
        this.homingTime = homingTime;
        trailRenderer.Clear();
        trailRenderer.enabled = true;
        fired = true;
        fireTime = Time.time;
    }

    //정해진 속도에 따라, 자신(투사체)의 right(+x)방향으로 고정적으로 진행
    void Move()
    {
        speed = inputSpeed * speedCurve.Evaluate((Time.time - fireTime)/homingTime); //animationCurve와 시간 에따라 속도 유동적으로 변경
        transform.Translate(Vector3.right * 10 * speed * Time.deltaTime);
    }

    // 정해진 유도력에 따라 회전
    void Rotate() 
    {        
        Vector3 targetDirection = target.position - transform.position;                                                     // 타겟 방향 계산          
        float targetAngle = Mathf.Atan2(targetDirection.y, targetDirection.x) * Mathf.Rad2Deg;                              // 목표물과의 각도 계산

        float homingSpeed = homingPower * homingCurve.Evaluate((Time.time - fireTime) / homingTime);                        // animationCurve와 시간 에따라 유도력 유동적으로 변경

        float newAngle = Mathf.MoveTowardsAngle(transform.eulerAngles.z, targetAngle, 10 * homingSpeed * Time.deltaTime);   // 각도 계산    
        transform.rotation = Quaternion.Euler(0, 0, newAngle);                                                              // 유동적인 유도력에 따라 자신을 회전
    }

    // 타겟과 충돌 시 행동
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.TryGetComponent<Boss>(out Boss boss))
        {
            trailRenderer.enabled = false;      // 투사체 궤적 비활성화
            fired = false;                      // 발사 여부 초기화
            boss.Damage((int)damage);           // 데미지 전달
            ReturnToPool();                     // 투사체 반환
        }
    }
}

Init을 통해 받은 정보들을 필드에 저장한 다음 이동, 회전, 충돌 등의 로직을 작성하였다.

속도는 인스펙터 상에서 animationCurve로 설정하였고 플레이어의 오른쪽 방향으로 이동하게 만들었다.
(각도를 조절하여 좌우를 관리하기 때문)

회전은 타겟 위치에서 플레이어의 위치를 빼서 방향을 구한 다음 타겟과의 각도를 계산하고
유도 속도는 유도력, 유도 커브, 유도 시간을 통해 계산하였다.
그런 다음 타겟과의 각도와 유도 속도를 이용해 각도를 계산하고 해당 각도만큼 회전하도록 구현했다.

충돌 시에는 궤적과 발사 여부를 false로 바꾼다음 아처와 동일하게 보스에게 데미지 값을 전달하여 체력을 변화시키고 풀로 반환하는 로직이다.

▶▷ 트러블 슈팅

막힌 점
1. 투사체가 깜빡이듯이 잠깐 생성되었다가 바로 사라지는 현상
2. 투사체와 궤적을 그리는 TrailRenderer의 위치가 맞지 않는 현상
3. 영상에서 보듯 투사체 초반 생성 시 Trail의 잔상이 생기는 현상

시도한 점
1-1. 오브젝트 풀 생성 및 할당 정상적인지 확인
1-1. 확인 못한 충돌체 있는지 확인
2-1. 임의로 궤적 및 투사체의 생성 위치 변경
3-1. 풀에 들어갔다 나온 이후로는 해당 현상이 해결되는 것을 보고 Trail 초기화 순서에 문제가 있다고 판단하고 Awake 및 Start에서 비활성화 시도

해결
1. 원인을 찾지 못한채 어느 순간 해결되어버림..
2. Trail의 위치를 투사체의 위치로 초기화하는 로직을 Update문에 작성하여 강제로 맞춤
3. 해결 진행중