Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

2D 中的自訂繪圖

前言

Godot 有用於繪製精靈、多邊形、粒子以及各種東西的節點。在大多數情況下,這就已經足夠了,但並不總是這樣。可別因為不存在繪製*某種特定*東西的節點,而在恐懼、焦慮、憤怒……中哭泣。要知道,任何 2D 節點(不管它是繼承自 Control 還是 Node2D)都可以很輕鬆地繪製自訂命令。而且這*真的*非常容易操作。

2D 節點中的自訂繪製*非常*有用。下面是一些用例:

  • 繪製現有節點型別無法完成的形狀或邏輯,例如帶有軌跡或特殊動態多邊形的圖像。

  • 與節點不太相容的呈現方式,比如俄羅斯方塊的棋盤。(俄羅斯方塊的例子使用的是自訂繪製函式來繪製方塊。)

  • 繪製大量簡單的物件。自訂繪製避免了使用大量節點的開銷,能降低記憶體佔用,並提高性能。

  • 製作自訂的 UI 控制項,以滿足很多可用的控制項之外的特別需求。

繪製

新增一個腳本到任何 CanvasItem 的衍生節點,如 ControlNode2D。然後重載 _draw() 函式。

extends Node2D

func _draw():
    # Your draw commands here
    pass

繪製命令在 CanvasItem 的類參考中有所描述,數量很多。

更新

_draw() 函式只呼叫一次, 然後繪製命令被快取並記住, 因此不需要進一步呼叫.

如果因為狀態或其他方面的變化而需要重新繪製,在目前節點中呼叫 CanvasItem.update() ,觸發新的 _draw() 呼叫。

這是一個更複雜的範例,一個被修改就會重新繪製的紋理變數:

extends Node2D

@export var texture: Texture:
        set = _set_texture

func _set_texture(value):
    # If the texture variable is modified externally,
    # this callback is called.
    texture = value  # Texture was changed.
    queue_redraw()  # Trigger a redraw of the node.

func _draw():
    draw_texture(texture, Vector2())

在某些情況下, 可能需要繪製每一影格. 為此, 只需從 _process() 回呼函式呼叫 update() , 如下所示:

extends Node2D

func _draw():
    # Your draw commands here
    pass

func _process(delta):
    queue_redraw()

座標

繪圖 API 使用 CanvasItem 的坐標系統,不一定是像素座標。這意味著它使用在應用 CanvasItem 的變換後建立的座標空間。此外,你可以使用 draw_set_transformdraw_set_transform_matrix 在它上面應用自訂變換。

使用 draw_line 時,應考慮線條的寬度。如果使用的寬度是奇數,則應將位置移動 0.5 以保持線條居中,如下圖所示。

../../_images/draw_line.png
func _draw():
    draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
    draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
    draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)

這同樣適用於使用 filled = falsedraw_rect 方法。

../../_images/draw_rect.png
func _draw():
    draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
    draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
    draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
    draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)

範例:繪製圓弧

我們現在將使用 Godot 引擎的自訂繪圖功能來繪製 Godot 未提供函式的內容。比如,Godot 提供了 draw_circle() 函式,它可以繪製一個完整的圓。但是,畫一個圓的一部分怎麼說?你必須編寫一個函式來執行此操作,自己繪製它。

弧函式

弧由其所在的圓的參數定義. 即: 中心位置和半徑. 弧本身由開始的角度和停止的角度來定義. 這些是我們必須為繪圖提供的4個參數. 我們還將提供顏色值, 因此我們可以根據需要繪製不同顏色的圓弧.

基本上, 在螢幕上繪製形狀需要將其分解為一定量首位相接的點. 你可以預見到, 點越多, 它就越平滑, 但處理開銷就越大. 一般來說, 如果你的形狀很大(或者在3D場景中靠近相機), 則需要繪製更多的點才不會看起來像是有棱角的. 相反, 如果你的形狀很小(或在3D場景裡遠離相機), 你可以減少其點數以節省處理成本. 這稱為 多層次細節(Level of Detail, LoD) . 在我們的範例中, 無論半徑如何, 我們都只使用固定數量的點.

func draw_circle_arc(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PackedVector2Array()

    for i in range(nb_points + 1):
        var angle_point = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
        points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)

    for index_point in range(nb_points):
        draw_line(points_arc[index_point], points_arc[index_point + 1], color)

還記得我們的形狀必須分解成多少點嗎?我們將「nb_points」變數中的這個數字固定為「32」。然後,我們初始化一個空的“PackedVector2Array”,它只是一個“Vector2”陣列。

下一步包括計算構成弧的這32個點的實際位置. 這是在第一個for迴圈中完成的: 我們反覆運算我們想要計算位置的點的數量, 後面+1來包括最後一個點. 我們首先確定起點和終點之間每個點的角度.

每個角度減小90°的原因是我們將使用三角函式計算每個角度的2D位置(你知道, 餘弦和正弦之類的東西......). 但是, 為了簡單, cos()sin() 使用弧度, 而不是度數作為參數. 雖然我們想在12點鐘位置開始計數, 但0°(0弧度)的角度從3點鐘位置開始. 因此我們將每個角度減小90°, 以便從12點位置開始計數.

以角度 angle (單位是弧度)位於圓上的點的實際位置由 Vector2(cos(angle), sin(angle)) 給出. 由於 cos()sin() 返回介於-1和1之間的值, 因此位置位於半徑為1的圓上. 要將此位置放在我們的半徑為 radius 的輔助圓上, 我們只需要將那個位置乘以 radius . 最後, 我們需要將我們的輔助圓定位在 center 位置, 這是通過將其與我們的 Vector2 相加來實作的. 最後, 我們在之前定義的 PoolVector2Array 中插入這個點.

現在, 我們需要實際繪製我們的點. 你可以想像, 我們不會簡單地畫出我們的32個點: 我們需要繪製每一點之間的所有內容. 我們可以使用前面的方法自己計算每個點, 然後逐個繪製. 但這太複雜和低效了(除非確實需要). 因此, 我們只需在每對點之間繪製線條. 除非我們的輔助圓的半徑很大, 否則一對點之間每條線的長度永遠不會長到足以看到它們. 如果發生這種情況, 我們只需要增加點的個數就可以了.

在螢幕上繪製弧形

我們現在有一個在螢幕上繪製內容的函式; 是時候在 _draw() 函式中呼叫它了:

func _draw():
    var center = Vector2(200, 200)
    var radius = 80
    var angle_from = 75
    var angle_to = 195
    var color = Color(1.0, 0.0, 0.0)
    draw_circle_arc(center, radius, angle_from, angle_to, color)

結果:

../../_images/result_drawarc.png

弧多邊形函式

我們可以更進一步, 不僅僅繪製一個由弧定義的扇形的邊緣, 還可以繪製其形體. 該方法與以前完全相同, 只是我們繪製的是多邊形而不是線條:

func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
    var nb_points = 32
    var points_arc = PackedVector2Array()
    points_arc.push_back(center)
    var colors = PackedColorArray([color])

    for i in range(nb_points + 1):
        var angle_point = deg_to_rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
        points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
    draw_polygon(points_arc, colors)
../../_images/result_drawarc_poly.png

動態自訂繪圖

好吧, 我們現在能夠在螢幕上繪製自訂內容. 然而, 它是靜態的; 我們讓這個形狀圍繞中心轉動吧. 這樣做的方法就是隨著時間的推移改變angle_from和angle_to值. 對於我們的範例, 我們將簡單地將它們遞增50. 此差異量值必須保持不變, 否則旋轉速度將相應地改變.

首先, 我們必須在我們的angle_from和angle_to變數變成全域變數, 放在腳本頂部. 另請注意, 您可以將它們儲存在其他節點中並使用 get_node() 存取它們.

extends Node2D

var rotation_angle = 50
var angle_from = 75
var angle_to = 195

我們在_process(delta)函式中更改這些值.

我們也在這裡增加angle_from和angle_to值. 但是, 我們不能忘記將結果 wrap() 在0到360°之間! 也就是說, 如果角度是361°, 那麼它實際上是1°. 如果您不包裝這些值, 腳本將正常工作, 但角度值將隨著時間的推移變得越來越大, 直到它們達到Godot可以管理的最大整數值(2^31 - 1). 當發生這種情況時,Godot可能會當機或產生意外行為.

最後, 我們一定不要忘記呼叫 queue_redraw() 函式, 它會自動呼叫 _draw() 。這樣, 你就可以控制何時去更新這一影格。

func _process(delta):
    angle_from += rotation_angle
    angle_to += rotation_angle

    # We only wrap angles when both of them are bigger than 360.
    if angle_from > 360 and angle_to > 360:
        angle_from = wrapf(angle_from, 0, 360)
        angle_to = wrapf(angle_to, 0, 360)
    queue_redraw()

另外, 不要忘記修改 _draw() 函式來使用這些變數:

func _draw():
   var center = Vector2(200, 200)
   var radius = 80
   var color = Color(1.0, 0.0, 0.0)

   draw_circle_arc( center, radius, angle_from, angle_to, color )

我們運作吧! 它工作正常, 但弧線旋轉快得瘋掉了! 怎麼了?

原因是你的GPU實際上正在盡可能快地顯示影格. 我們需要以這個速度為基準 "標準化" 繪圖的速度. 為了實作這個效果, 我們必須使用 _process() 函式的 delta 參數. delta 包含最後兩個渲染影格之間經過的時間. 它通常很小(約0.0003秒, 但這取決於你的硬體). 因此, 使用 delta 來控制繪圖可確保程式在每個人的硬體上以相同的速度運作.

在我們的範例中, 我們只需要在 _process() 函式中將 rotation_angle 變數乘以 delta . 這樣, 我們的2個角度會以一個小得多的值遞增, 值的大小直接取決於渲染速度.

func _process(delta):
    angle_from += rotation_angle * delta
    angle_to += rotation_angle * delta

    # We only wrap angles when both of them are bigger than 360.
    if angle_from > 360 and angle_to > 360:
        angle_from = wrapf(angle_from, 0, 360)
        angle_to = wrapf(angle_to, 0, 360)
    queue_redraw()

讓我們再運作一次! 這次, 旋轉顯示正常!

抗鋸齒:

Godot在 draw_line<class_CanvasItem_method_draw_line>`中提供方法參數來啟用抗鋸齒功能,但並非所有自訂繪圖方法都提供這個 ``抗鋸齒(antialiased)` 參數。

對於不提供 抗鋸齒(antialiased) 參數的自訂繪圖方法,你可以啟用 2D MSAA,這會影響整個視口的渲染。這個功能(2D MSAA)提供了高品質的抗鋸齒,但性能成本更高,而且只適用於特定元素。參見 抗鋸齒: 以瞭解更多資訊。

工具

在編輯器中運作節點時,可能也會用到繪圖。可以用於某些功能或行為的預覽或視覺化。詳情請參閱 在編輯器中運作程式碼