【遊戲開發實戰】Unity實現類似GitHub地球射線的效果(LineRenderer | 貝塞爾曲線)

2022-01-03 12:00:02

一、前言

嗨,大傢伙,我是新發。
好久不見,這是2022年第一篇部落格,今天有同學私信我,問我在Unity中如何實現這種地球輻射線的效果,
在這裡插入圖片描述
這一看,我就想到了GitHub主頁的地球射線,
請新增圖片描述
那麼,今天就來講講如何實現這個效果吧~
本文最終效果如下:
請新增圖片描述
本文工程原始碼見文章末尾~

二、實現思路

我們先把問題進行拆解,
在這裡插入圖片描述

現在挨個問題進行思考與解答。

1、曲線的本質是什麼?

一條曲線,它本質上是由N條直線段組成的,當N足夠大的時候,曲線就會看起來很平滑,我用Blender給大家演示一下,請新增圖片描述

2、如何繪製曲線?

我在之前的一些篇文章中有講過使用LineRenderer來繪製曲線,這裡我們是用LineRenderer來實現就好啦。
往期相關文章:
《【遊戲開發解答】教你在Unity中使用LineRenderer製作行軍螞蟻線(行軍 | 虛線 | 路徑 | 線段)》

《【遊戲開發實戰】Unity實現水果忍者切水果的刀痕效果教學(兩種實現方式:TrailRenderer、LineRenderer)》

《【遊戲開發實戰】TapTap物理畫線遊戲,教你使用Unity實現2D物理畫線功能》

3、如何構造麴線的點?

上面我們說使用LineRenderer來繪製曲線,而LineRenderer需要我們告訴它點的座標,那麼我們如何來構造麴線的點座標呢?常用的曲線有B樣條、貝塞爾曲線等,關於貝塞爾曲線,我之前也有專門寫過文章,
《【遊戲開發進階】玩轉貝塞爾曲線,教你在Unity中畫Bezier貝塞爾曲線(二階、三階),手把手教你推導公式》

這裡我們就用貝塞爾曲線即可,這裡我打算使用三階貝塞爾曲線,三階貝塞爾曲線需要知道四個點座標,現在問題變成了我們如何確定這四個點的座標,其中起始點和終止點是在球的表面選取,中間兩個點我們通過一些幾何運算來獲得,現在問題變成了如何在球的表面獲取兩個點。

4、如何在球的表面選取兩個點?

這是一個幾何問題,我們已知球的球心座標,想要在球的表面隨機選取兩個點,我們只需要在球心處隨機兩個方向向量,然後從球心出發,分別沿著這兩個方向走一個半徑長度的距離即可到達球的表面,畫個圖方便大家理解,
在這裡插入圖片描述
寫成程式碼大概是這樣子:

// 球心座標
Vector3 centerPos = Vector3.zero;
// 球半徑
float radius = 1;
// 隨機一個單位向量作為方向向量
Vector3 randomDir = new Vector3(Random.Range(-1f, 1f), 
						Random.Range(-1f, 1f), 
						Random.Range(-1f, 1f)).normalized;
// 球表面的點				
Vector pos = centerPos + randomDir * radius;

我們使用上面的方法分別取到球表面的兩個點即可。

上面我們說使用三階貝塞爾曲線,現在還差中間兩個控制點的座標,我們可以用下面這樣的方法來計算控制點的座標:先求出球表面兩個點的連線的中點,然後從球心指向這個中點,得到一個方向向量,再分別從點1點2朝這這個方向向量走一段距離,得到控制點1控制點2的座標,如下
在這裡插入圖片描述

5、如何讓曲線有動畫效果?

我們可以看到曲線是有一個從起始點飛向目標點的效果的,
請新增圖片描述
這個我們可以在指令碼中動態設定LineRenderer的點來達到這個效果。

三、具體實操

講完了理論,下面我們就來具體實操吧~

1、建立Unity工程

工程名就叫UnityEarthRay好了,工程模板使用3D,點選建立,
在這裡插入圖片描述

2、製作宇宙天空盒

2.1、天空盒貼圖

我們先去找6張宇宙天空盒的圖片,匯入到工程目錄中,如下
在這裡插入圖片描述

2.2、天空盒材質球

建立一個材質球,重新命名為Skybox,如下
在這裡插入圖片描述

設定材質球的ShaderSkybox/6 Sided,並設定六個面的貼圖,如下,
在這裡插入圖片描述

2.3、設定場景天空盒

Skybox材質球拖入Scene視窗中,請新增圖片描述
我們也可以通過點選選單Window / Rendering / Lighting
在這裡插入圖片描述
然後點選Environment分頁,設定Skybox Material來設定天空盒,
在這裡插入圖片描述
如果想改回原來的天空盒,只需把Skybox Material設定為預設的天空盒材質即可,如下
請新增圖片描述

3、製作地球

3.1、建立球體

Hierarchy視窗空白處右鍵滑鼠,點選選單3D Object / Sphere,建立一個球體,
在這裡插入圖片描述
重新命名為Earth,作為地球的外形,
在這裡插入圖片描述
如下
在這裡插入圖片描述

3.2、地球貼圖

去找一下地球的貼圖,匯入工程目錄中,我找了雲層貼圖、地球貼圖、燈光貼圖,如下
在這裡插入圖片描述

3.3、製作地球材質球

建立一個材質球,重新命名為Earth
在這裡插入圖片描述
設定材質球的ShaderStandard,設定Albedo貼圖,調節Matallic(金屬度)和Smoothness(光滑度),開啟Emission(自發光),並設定發光貼圖,設定發光顏色為橘黃色,如下
在這裡插入圖片描述

然後把材質球賦值給模型,如下
在這裡插入圖片描述
此時效果,
在這裡插入圖片描述

3.4、製作雲層

同理,我們在Earth子節點下再建立一個球體,重新命名為Clouds,如下

在這裡插入圖片描述
調節Clouds的縮放,讓它比Earth大一點點,
在這裡插入圖片描述

建立一個材質球,重新命名為Clouds
在這裡插入圖片描述
設定材質球的ShaderStandard,設定Rendering ModeTransparent(透明),設定Albedo貼圖,設定顏色值的A通道為0,調節Matallic(金屬度)和Smoothness(光滑度),開啟Emission(自發光),並設定發光貼圖,設定發光顏色為灰色,如下
在這裡插入圖片描述
Clouds材質球賦值給Clouds節點,
在這裡插入圖片描述
此時效果
請新增圖片描述

4、製作LineRenderer

4.1、建立LineRenderer

Earth節點右鍵滑鼠,點選選單Effects / Line,建立一個LineRenderer
在這裡插入圖片描述
如下
在這裡插入圖片描述
此時可以看到場景中多了一條粗粗的白色短線,它就是LineRenderer本君了,
在這裡插入圖片描述

4.2、調節寬度

我們可以調節一下它的寬度,讓它細一點,
在這裡插入圖片描述
如下
在這裡插入圖片描述

4.3、設定材質球

我們可以給他建立一個材質球,
在這裡插入圖片描述
如下
在這裡插入圖片描述

5、Line指令碼:曲線邏輯

建立一個C#指令碼,重新命名為Line,編寫曲線的邏輯程式碼,
在這裡插入圖片描述
首先我們封裝一個三階貝塞爾曲線的函數,如下

注:這裡三階貝塞爾曲線程式碼看不懂的建議先看我之前的這篇文章,《【遊戲開發進階】玩轉貝塞爾曲線,教你在Unity中畫Bezier貝塞爾曲線(二階、三階),手把手教你推導公式》

// Line.cs

// 三階貝塞爾曲線
private Vector3 cubicBezier(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
{
    Vector3 aa = a + (b - a) * t;
    Vector3 bb = b + (c - b) * t;
    Vector3 cc = c + (d - c) * t;

    Vector3 aaa = aa + (bb - aa) * t;
    Vector3 bbb = bb + (cc - bb) * t;
    return aaa + (bbb - aaa) * t;
}

接著我們定義曲線的最大點數,

/// <summary>
/// 曲線最大點數
/// </summary>
public int MAX_FRAG_CNT = 100;

宣告一個List用於儲存點座標

private List<Vector3> posList = new List<Vector3>();

我們宣告一個lineRenderer成員,並在Awake中獲取它,

[RequireComponent(typeof(LineRenderer))]
public class RadiationLine : MonoBehaviour
{
    private LineRenderer lineRenderer;
	...
	
	void Awake()
    {
        lineRenderer = GetComponent<LineRenderer>();
    }
	
	...
}

封裝一個繪製曲線的方法,如下

// Line.cs

/// <summary>
/// 繪製曲線
/// </summary>
/// <param name="fromPos">起始座標</param>
/// <param name="ctrlPoint1">控制點1</param>
/// <param name="ctrlPoint2">控制點2</param>
/// <param name="toPos">目標座標</param>
public void DrawRay(Vector3 fromPos, Vector3 ctrlPoint1, Vector3 ctrlPoint2, Vector3 toPos)
{
    posList.Clear();
    for (int i = 0; i <= MAX_FRAG_CNT; ++i)
    {
        posList.Add(cubicBezier(fromPos, ctrlPoint1, ctrlPoint2, toPos, (float)i / MAX_FRAG_CNT));
        lineRenderer.positionCount = posList.Count;
    }
    lineRenderer.SetPositions(posList.ToArray());
}

Line指令碼掛到Line節點上,如下
在這裡插入圖片描述

6、Earth指令碼:地球邏輯

建立一個C#指令碼,重新命名為Earth,編寫地球邏輯程式碼,
在這裡插入圖片描述
程式碼很簡單,這裡就不拆開講解了,我都有寫註釋,

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

/// <summary>
/// 地球邏輯
/// </summary>
public class Earth : MonoBehaviour
{
    private Transform selfTrans;
    // 曲線
    public Line line;


    void Start()
    {
        selfTrans = transform;
        StartCoroutine(FireLine());
    }

    void Update()
    {
        // 地球自轉
        selfTrans.Rotate(Vector3.up * Time.deltaTime, Space.Self);
    }

    /// <summary>
    /// 發射輻射射線
    /// </summary>
    /// <returns></returns>
    IEnumerator FireLine()
    {
        this.line.gameObject.SetActive(false);
        while (true)
        {
            // 迴圈生成曲線,這裡只是演示效果,我就不是用物件池了
            var line = Instantiate(this.line);
            line.gameObject.SetActive(true);
            line.transform.SetParent(selfTrans);

            // 半徑
            var radius = selfTrans.localScale.x / 2f;

            // 在地球表面隨機一個起始點
            var from = selfTrans.position + new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized * radius;

            // 在地球表面隨機一個終點
            var to = selfTrans.position + new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized * radius;
            var center = (from + to) / 2f;
            // 控制點1
            var ctrlPoint1 = from + (center - selfTrans.position).normalized * (from - to).magnitude * 0.6f;
            // 控制點2
            var ctrlPoint2 = to + (center - selfTrans.position).normalized * (from - to).magnitude * 0.6f;

            line.DrawRay(from, ctrlPoint1, ctrlPoint2, to);
            // 隨機一個時間後銷燬曲線
            Destroy(line.gameObject, Random.Range(4, 7));
            // 隨機等待一個事件
            yield return new WaitForSeconds(Random.Range(0.3f, 2f));
        }
    }
}

Earth指令碼掛到Earth節點上,並賦值Line成員,如下
在這裡插入圖片描述

7、執行效果

執行Unity,基本的效果已經出來了,
請新增圖片描述

四、動態效果

1、Line指令碼:動態效果

我們要讓曲線有動態的效果,把原來的DrawRay改成協程,動態設定點的座標,如下,

// Line.cs

/// <summary>
/// 繪製曲線
/// </summary>
/// <param name="fromPos">起始座標</param>
/// <param name="ctrlPoint1">控制點1</param>
/// <param name="ctrlPoint2">控制點2</param>
/// <param name="toPos">目標座標</param>
public IEnumerator DrawRay(Vector3 fromPos, Vector3 handPos1, Vector3 handPos2, Vector3 toPos)
{
    for (int i = 0; i <= MAX_FRAG_CNT; ++i)
    {
        posList.Clear();
        for (int j = 0; j <= i; ++j)
        {
            posList.Add(cubicBezier(fromPos, handPos1, handPos2, toPos, (float)j / MAX_FRAG_CNT));
        }
        lineRenderer.positionCount = posList.Count;
        lineRenderer.SetPositions(posList.ToArray());
        yield return new WaitForSeconds(0.02f);
    }

    yield return new WaitForSeconds(2);

    for (int i = 0; i <= MAX_FRAG_CNT; ++i)
    {
        posList.Clear();
        for (int j = i; j <= MAX_FRAG_CNT; ++j)
        {
            posList.Add(cubicBezier(fromPos, handPos1, handPos2, toPos, (float)j / MAX_FRAG_CNT));
        }
        lineRenderer.positionCount = posList.Count;
        lineRenderer.SetPositions(posList.ToArray());
        yield return new WaitForSeconds(0.001f);
    }
    Destroy(gameObject);
}

改下Earth指令碼中的呼叫,如下

// Earth.cs

/// <summary>
/// 發射輻射射線
/// </summary>
/// <returns></returns>
IEnumerator FireLine()
{
	this.line.gameObject.SetActive(false);
	while (true)
	{
  		...
  		// 啟動協程
        StartCoroutine(line.DrawRay(from, ctrlPoint1, ctrlPoint2, to));

        // 隨機等待一個事件
        yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
   }
}

2、執行效果

執行效果如下,可以,
請新增圖片描述

五、加點粒子特效

製作個粒子效果,
請新增圖片描述
包裝成預設,掛到Line節點下,作為起始點和目標點的特效,如下
在這裡插入圖片描述
Line指令碼中加上粒子控制的邏輯,

// Line.cs

public Transform startPoint;
public Transform endPoint;

/// <summary>
/// 繪製曲線
/// </summary>
/// <param name="fromPos">起始座標</param>
/// <param name="ctrlPoint1">控制點1</param>
/// <param name="ctrlPoint2">控制點2</param>
/// <param name="toPos">目標座標</param>
public IEnumerator DrawRay(Vector3 earthPos, Vector3 fromPos, Vector3 handPos1, Vector3 handPos2, Vector3 toPos)
{
    // 起始位置粒子
    startPoint.gameObject.SetActive(true);
    startPoint.forward = fromPos - earthPos;
    startPoint.localPosition = fromPos + startPoint.forward * 0.01f;
    for (int i = 0; i <= MAX_FRAG_CNT; ++i)
    {
        posList.Clear();
        for (int j = 0; j <= i; ++j)
        {
            posList.Add(cubicBezier(fromPos, handPos1, handPos2, toPos, (float)j / MAX_FRAG_CNT));
        }
        lineRenderer.positionCount = posList.Count;
        lineRenderer.SetPositions(posList.ToArray());
        yield return new WaitForSeconds(0.02f);
    }

    // 目標位置粒子
    endPoint.gameObject.SetActive(true);
    endPoint.forward = toPos - earthPos;
    endPoint.localPosition = toPos + endPoint.forward * 0.01f;
    yield return new WaitForSeconds(2);
    startPoint.gameObject.SetActive(false);
    for (int i = 0; i <= MAX_FRAG_CNT; ++i)
    {
        posList.Clear();
        for (int j = i; j <= MAX_FRAG_CNT; ++j)
        {
            posList.Add(cubicBezier(fromPos, handPos1, handPos2, toPos, (float)j / MAX_FRAG_CNT));
        }
        lineRenderer.positionCount = posList.Count;
        lineRenderer.SetPositions(posList.ToArray());
        yield return new WaitForSeconds(0.001f);
    }
    Destroy(gameObject);
}

最終執行效果如下
請新增圖片描述

六、工程原始碼

本文工程我已上傳到GitCode,感興趣的同學可自行下載學習,
地址:https://gitcode.net/linxinfa/UnityEarthRay
注:我使用的Unity版本是2021.1.7.f1c1
在這裡插入圖片描述

七、完畢

好了,就寫到這裡吧。
我是新發,https://blog.csdn.net/linxinfa
一個在小公司默默奮鬥的Unity開發者,希望可以幫助更多想學Unity的人,共勉~