기타/Unity

[Unity] 베지어 곡선으로 카이사 q 따라하기

푸쿠이 2021. 9. 24. 17:24
참고

https://tonikat.tistory.com/10

https://lee-seokhyun.gitbook.io/workspace/client/easy-mathematics/gdc2012/3

 

만들어 본 이유

평소에도 리그오브레전드 게임의 챔피언, 카이사의 Q 스킬 '이케시아 폭우'를 쓸 때마다 토도도독 박히는 느낌이 좋았다.

그런 느낌은 못 냈지만, 비슷하게 구현한 게시글을 봐서 나도 내 방식대로 구현해보았다.

 

베지어 곡선에 대해 많이 공부하게 되었다.

 

핵심 코드

가장 핵심은 역시 베지어 곡선이다.

3차 베지어 곡선을 썼는데, 3차 베지어 곡선은 점 4개로 위치를 구하는 것이다.

방정식으로 정석으로 코딩하면 이렇게 되는데, 수포자인 나는 이해가 한번에 가지 않는다.

/// <summary>
/// 3차 베지어 곡선.
/// </summary>
/// <param name="a">시작 위치</param>
/// <param name="b">시작 위치에서 얼마나 꺾일 지 정하는 위치</param>
/// <param name="c">도착 위치에서 얼마나 꺾일 지 정하는 위치</param>
/// <param name="d">도착 위치</param>
/// <returns></returns>
private float CubicBezierCurve(float a, float b, float c, float d)
{
    // (0~1)의 값에 따라 베지어 곡선 값을 구하기 때문에, 비율에 따른 시간을 구했다.
    float t = m_timerCurrent / m_timerMax; // (현재 경과 시간 / 도착 위치에 도착할 시간)

    // 방정식.
    return Mathf.Pow((1 - t), 3) * a
        + Mathf.Pow((1 - t), 2) * 3 * t * b
        + Mathf.Pow(t, 2) * 3 * (1 - t) * c
        + Mathf.Pow(t, 3) * d;
}

 

베지어 곡선은 결국에는 원리가 간단하므로, 그대로 코드로 짜면 이렇게 쉽게 짤 수 있다.

구글링 하다보니, 그래프로 슥슥 설명하시던데 

private float CubicBezierCurve(float a, float b, float c, float d)
{
    float t = m_timerCurrent / m_timerMax;

    // 이해한대로 편하게 쓰면.
    float ab = Mathf.Lerp(a, b, t);
    float bc = Mathf.Lerp(b, c, t);
    float cd = Mathf.Lerp(c, d, t);

    float abbc = Mathf.Lerp(ab, bc, t);
    float bccd = Mathf.Lerp(bc, cd, t);

    return Mathf.Lerp(abbc, bccd, t);
}

 

그 외에 생각했던 것들

a와 d는 시작 위치와 도착 위치이므로, 쉽게 값을 대입할 수 있는데, b와 c는 따로 위치를 잡아야한다.

랜덤하게 위치를 잡아주었는데 랜덤으로 하니, 오브젝트를 기준으로 앞으로 나가거나 뒤로 나가거나 별로 안 이뻤다.

// 시작 지점.
m_points[0] = _startTr.position; 

// 시작 지점을 기준으로 랜덤 포인트 지정.
m_points[1] = _startTr.position +
    (_newPointDistanceFromStartTr * Random.Range(-1.0f, 1.0f) * _startTr.right) + // X (좌, 우 전체)
    (_newPointDistanceFromStartTr * Random.Range(-1.0f, 1.0f) * _startTr.up) + // Y (위, 아래 전체)
    (_newPointDistanceFromStartTr * Random.Range(-1.0f, 1.0f) * _startTr.forward); // Z (앞, 뒤 전체)

// 도착 지점을 기준으로 랜덤 포인트 지정.
m_points[2] = _endTr.position +      
    (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.right) + // X (좌, 우 전체)
    (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.up) + // Y (위, 아래 전체)
    (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.forward); // Z (앞, 뒤 전체)

// 도착 지점.
m_points[3] = _endTr.position;

 

이 부분은 어느 정도의 고정한 랜덤 값을 줘서 해결했다.

플레이어의 뒤쪽 윗부분에서 시작. -> 도착지점의 앞 부분으로 도착. 느낌으로 랜덤 값을 주었다.

// 시작 지점.
m_points[0] = _startTr.position; 

// 시작 지점을 기준으로 랜덤 포인트 지정.
m_points[1] = _startTr.position +
    (_newPointDistanceFromStartTr * Random.Range(-1.0f, 1.0f) * _startTr.right) + // X (좌, 우 전체)
    (_newPointDistanceFromStartTr * Random.Range(-0.15f, 1.0f) * _startTr.up) + // Y (아래쪽 조금, 위쪽 전체)
    (_newPointDistanceFromStartTr * Random.Range(-1.0f, -0.8f) * _startTr.forward); // Z (뒤 쪽만)

// 도착 지점을 기준으로 랜덤 포인트 지정.
m_points[2] = _endTr.position +
    (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.right) + // X (좌, 우 전체)
    (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.up) + // Y (위, 아래 전체)
    (_newPointDistanceFromEndTr * Random.Range(0.8f, 1.0f) * _endTr.forward); // Z (앞 쪽만)

// 도착 지점.
m_points[3] = _endTr.position;

참고로 위에 사용한 2개의 움짤은 같은 속성 값으로 사용했다.

 

코드

참고에 올린 사이트의 코드를 참고해서, 클래스는 동일한 상태에서 안의 코드만 내 방식대로 바꾸었다.

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

public class Shooter : MonoBehaviour
{
    public GameObject m_missilePrefab; // 미사일 프리팹.
    public GameObject m_target; // 도착 지점.

    [Header("미사일 기능 관련")]
    public float m_speed = 2; // 미사일 속도.
    [Space(10f)]
    public float m_distanceFromStart = 6.0f; // 시작 지점을 기준으로 얼마나 꺾일지.
    public float m_distanceFromEnd = 3.0f; // 도착 지점을 기준으로 얼마나 꺾일지.
    [Space(10f)]
    public int m_shotCount = 12; // 총 몇 개 발사할건지.
    [Range(0, 1)] public float m_interval = 0.15f;
    public int m_shotCountEveryInterval = 2; // 한번에 몇 개씩 발사할건지.

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.A))
        {
            // Shot.
            StartCoroutine(CreateMissile());
        }
    }

    IEnumerator CreateMissile()
    {
        int _shotCount = m_shotCount;
        while (_shotCount > 0)
        {
            for(int i = 0; i < m_shotCountEveryInterval; i++)
            {
                if(_shotCount > 0)
                {
                    GameObject missile = Instantiate(m_missilePrefab);
                    missile.GetComponent<BezierMissile>().Init(this.gameObject.transform, m_target.transform, m_speed, m_distanceFromStart, m_distanceFromEnd);

                    _shotCount--;
                }
            }
            yield return new WaitForSeconds(m_interval);
        }
        yield return null;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BezierMissile : MonoBehaviour
{
    Vector3[] m_points = new Vector3[4];

    private float m_timerMax = 0;
    private float m_timerCurrent = 0;
    private float m_speed;

    public void Init(Transform _startTr, Transform _endTr, float _speed, float _newPointDistanceFromStartTr, float _newPointDistanceFromEndTr)
    {
        m_speed = _speed;

        // 끝에 도착할 시간을 랜덤으로 줌.
        m_timerMax = Random.Range(0.8f, 1.0f);

        // 시작 지점.
        m_points[0] = _startTr.position; 

        // 시작 지점을 기준으로 랜덤 포인트 지정.
        m_points[1] = _startTr.position +
            (_newPointDistanceFromStartTr * Random.Range(-1.0f, 1.0f) * _startTr.right) + // X (좌, 우 전체)
            (_newPointDistanceFromStartTr * Random.Range(-0.15f, 1.0f) * _startTr.up) + // Y (아래쪽 조금, 위쪽 전체)
            (_newPointDistanceFromStartTr * Random.Range(-1.0f, -0.8f) * _startTr.forward); // Z (뒤 쪽만)

        // 도착 지점을 기준으로 랜덤 포인트 지정.
        m_points[2] = _endTr.position +
            (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.right) + // X (좌, 우 전체)
            (_newPointDistanceFromEndTr * Random.Range(-1.0f, 1.0f) * _endTr.up) + // Y (위, 아래 전체)
            (_newPointDistanceFromEndTr * Random.Range(0.8f, 1.0f) * _endTr.forward); // Z (앞 쪽만)

        // 도착 지점.
        m_points[3] = _endTr.position;

        transform.position = _startTr.position;
    }

    void Update()
    {
        if (m_timerCurrent > m_timerMax)
        {
            return;
        }

        // 경과 시간 계산.
        m_timerCurrent += Time.deltaTime * m_speed;

        // 베지어 곡선으로 X,Y,Z 좌표 얻기.
        transform.position = new Vector3(
            CubicBezierCurve(m_points[0].x, m_points[1].x, m_points[2].x, m_points[3].x),
            CubicBezierCurve(m_points[0].y, m_points[1].y, m_points[2].y, m_points[3].y),
            CubicBezierCurve(m_points[0].z, m_points[1].z, m_points[2].z, m_points[3].z)
        );
    }

    /// <summary>
    /// 3차 베지어 곡선.
    /// </summary>
    /// <param name="a">시작 위치</param>
    /// <param name="b">시작 위치에서 얼마나 꺾일 지 정하는 위치</param>
    /// <param name="c">도착 위치에서 얼마나 꺾일 지 정하는 위치</param>
    /// <param name="d">도착 위치</param>
    /// <returns></returns>
    private float CubicBezierCurve(float a, float b, float c, float d)
    {
        // (0~1)의 값에 따라 베지어 곡선 값을 구하기 때문에, 비율에 따른 시간을 구했다.
        float t = m_timerCurrent / m_timerMax; // (현재 경과 시간 / 최대 시간)

        // 방정식.
        /*
        return Mathf.Pow((1 - t), 3) * a
            + Mathf.Pow((1 - t), 2) * 3 * t * b
            + Mathf.Pow(t, 2) * 3 * (1 - t) * c
            + Mathf.Pow(t, 3) * d;
        */

        // 이해한대로 편하게 쓰면.
        float ab = Mathf.Lerp(a, b, t);
        float bc = Mathf.Lerp(b, c, t);
        float cd = Mathf.Lerp(c, d, t);

        float abbc = Mathf.Lerp(ab, bc, t);
        float bccd = Mathf.Lerp(bc, cd, t);

        return Mathf.Lerp(abbc, bccd, t);
    }

    void OnTriggerEnter(Collider collision)
    {
        Destroy(this.gameObject, 0.35f); // 한쪽에 Trigger 체크하는 것과 Rigidbody 컴포넌트 추가 잊지 말기.
    }
}

 

구현해봤으니까 제일 멋있어보이게 움짤 하나 남겨야겠다!!

생각보단 멋있지않다...  Trail 머터리얼이 이뻐야 할 듯.