2D 中的自定义绘图
前言
Godot 有用于绘制精灵、多边形、粒子以及各种东西的节点。在大多数情况下,这就已经足够了,但并不总是这样。可别因为不存在绘制某种特定东西的节点,而在恐惧、焦虑、愤怒……中哭泣。要知道,任何 2D 节点(不管它是继承自 Control 还是 Node2D)都可以很轻松地绘制自定义命令。而且这真的非常容易操作。
2D 节点中的自定义绘制非常有用。下面是一些用例:
绘制现有节点类型无法完成的形状或逻辑,例如带有轨迹或特殊动态多边形的图像。
与节点不太兼容的呈现方式,比如俄罗斯方块的棋盘。(俄罗斯方块的例子使用的是自定义绘制函数来绘制方块。)
绘制大量简单的对象。自定义绘制避免了使用大量节点的开销,能降低内存占用,并提高性能。
制作自定义的 UI 控件,以满足很多可用的控件之外的特别需求。
绘制
添加一个脚本到任何 CanvasItem 的派生节点,如 Control 或 Node2D。然后重载 _draw()
函数。
GDScriptC#
extends Node2D
func _draw():
# Your draw commands here
pass
public override void _Draw()
{
// Your draw commands here
}
绘制命令在 CanvasItem 的类参考中有所描述,数量很多。
更新
_draw()
函数只调用一次, 然后绘制命令被缓存并记住, 因此不需要进一步调用.
如果因为状态或其他方面的变化而需要重新绘制,在当前节点中调用 CanvasItem.update() ,触发新的 _draw()
调用。
这是一个更复杂的示例,一个被修改就会重新绘制的纹理变量:
GDScriptC#
extends Node2D
export (Texture) var texture setget _set_texture
func _set_texture(value):
# If the texture variable is modified externally,
# this callback is called.
texture = value # Texture was changed.
update() # Update the node's visual representation.
func _draw():
draw_texture(texture, Vector2())
public class CustomNode2D : Node2D
{
private Texture _texture;
public Texture Texture
{
get
{
return _texture;
}
set
{
_texture = value;
Update();
}
}
public override void _Draw()
{
DrawTexture(_texture, new Vector2());
}
}
在某些情况下, 可能需要绘制每一帧. 为此, 只需从 _process()
回调调用 update()
, 如下所示:
GDScriptC#
extends Node2D
func _draw():
# Your draw commands here
pass
func _process(delta):
update()
public class CustomNode2D : Node2D
{
public override void _Draw()
{
// Your draw commands here
}
public override void _Process(float delta)
{
Update();
}
}
示例:绘制圆弧
我们现在将使用 Godot 引擎的自定义绘图功能来绘制 Godot 未提供函数的内容。比如,Godot 提供了 draw_circle()
函数,它可以绘制一个完整的圆。但是,画一个圆的一部分怎么说?你必须编写一个函数来执行此操作,自己绘制它。
弧函数
弧由其所在的圆的参数定义. 即: 中心位置和半径. 弧本身由开始的角度和停止的角度来定义. 这些是我们必须为绘图提供的4个参数. 我们还将提供颜色值, 因此我们可以根据需要绘制不同颜色的圆弧.
基本上, 在屏幕上绘制形状需要将其分解为一定量首位相接的点. 你可以预见到, 点越多, 它就越平滑, 但处理开销就越大. 一般来说, 如果你的形状很大(或者在3D场景中靠近相机), 则需要绘制更多的点才不会看起来像是有棱角的. 相反, 如果你的形状很小(或在3D场景里远离相机), 你可以减少其点数以节省处理成本. 这称为 多层次细节(Level of Detail, LoD) . 在我们的示例中, 无论半径如何, 我们都只使用固定数量的点.
GDScriptC#
func draw_circle_arc(center, radius, angle_from, angle_to, color):
var nb_points = 32
var points_arc = PoolVector2Array()
for i in range(nb_points + 1):
var angle_point = deg2rad(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)
public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints];
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
for (int i = 0; i < nbPoints - 1; ++i)
DrawLine(pointsArc[i], pointsArc[i + 1], color);
}
还记得我们的形状必须分解成成的点的数量吗? 我们将 nb_points
变量中的这个数字修改为32. 然后, 我们初始化一个空的 PoolVector2Array
, 就是一个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()
函数中调用它了:
GDScriptC#
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)
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
float angleFrom = 75;
float angleTo = 195;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, angleFrom, angleTo, color);
}
结果:
弧多边形函数
我们可以更进一步, 不仅仅绘制一个由弧定义的扇形的边缘, 还可以绘制其形体. 该方法与以前完全相同, 只是我们绘制的是多边形而不是线条:
GDScriptC#
func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
var nb_points = 32
var points_arc = PoolVector2Array()
points_arc.push_back(center)
var colors = PoolColorArray([color])
for i in range(nb_points + 1):
var angle_point = deg2rad(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)
public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
int nbPoints = 32;
var pointsArc = new Vector2[nbPoints + 1];
pointsArc[0] = center;
var colors = new Color[] { color };
for (int i = 0; i < nbPoints; ++i)
{
float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
}
DrawPolygon(pointsArc, colors);
}
动态自定义绘图
好吧, 我们现在能够在屏幕上绘制自定义内容. 然而, 它是静态的; 我们让这个形状围绕中心转动吧. 这样做的方法就是随着时间的推移改变angle_from和angle_to值. 对于我们的示例, 我们将简单地将它们递增50. 此增量值必须保持不变, 否则旋转速度将相应地改变.
首先, 我们必须在我们的angle_from和angle_to变量变成全局变量, 放在脚本顶部. 另请注意, 您可以将它们存储在其他节点中并使用 get_node()
访问它们.
GDScriptC#
extends Node2D
var rotation_angle = 50
var angle_from = 75
var angle_to = 195
public class CustomNode2D : Node2D
{
private float _rotationAngle = 50;
private float _angleFrom = 75;
private float _angleTo = 195;
}
我们在_process(delta)函数中更改这些值.
我们也在这里增加angle_from和angle_to值. 但是, 我们不能忘记将结果 wrap()
在0到360°之间! 也就是说, 如果角度是361°, 那么它实际上是1°. 如果您不包装这些值, 脚本将正常工作, 但角度值将随着时间的推移变得越来越大, 直到它们达到Godot可以管理的最大整数值(2^31 - 1
). 当发生这种情况时,Godot可能会崩溃或产生意外行为.
最后, 我们不要忘记调用 update()
函数, 它自动调用 _draw()
. 这样, 您可以控制何时要刷新这一帧.
GDScriptC#
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)
update()
public override void _Process(float delta)
{
_angleFrom += _rotationAngle;
_angleTo += _rotationAngle;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Mathf.Wrap(_angleFrom, 0, 360);
_angleTo = Mathf.Wrap(_angleTo, 0, 360);
}
Update();
}
另外, 不要忘记修改 _draw()
函数来使用这些变量:
GDScriptC#
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 )
public override void _Draw()
{
var center = new Vector2(200, 200);
float radius = 80;
var color = new Color(1, 0, 0);
DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
}
我们运行吧! 它工作正常, 但弧线旋转快得疯掉了! 怎么了?
原因是你的GPU实际上正在尽可能快地显示帧. 我们需要以这个速度为基准 “标准化” 绘图的速度. 为了实现这个效果, 我们必须使用 _process()
函数的 delta
参数. delta
包含最后两个渲染帧之间经过的时间. 它通常很小(约0.0003秒, 但这取决于你的硬件). 因此, 使用 delta
来控制绘图可确保程序在每个人的硬件上以相同的速度运行.
在我们的示例中, 我们只需要在 _process()
函数中将 rotation_angle
变量乘以 delta
. 这样, 我们的2个角度会以一个小得多的值递增, 值的大小直接取决于渲染速度.
GDScriptC#
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)
update()
public override void _Process(float delta)
{
_angleFrom += _rotationAngle * delta;
_angleTo += _rotationAngle * delta;
// We only wrap angles when both of them are bigger than 360.
if (_angleFrom > 360 && _angleTo > 360)
{
_angleFrom = Wrap(_angleFrom, 0, 360);
_angleTo = Wrap(_angleTo, 0, 360);
}
Update();
}
让我们再运行一次! 这次, 旋转显示正常!
抗锯齿绘图
Godot 在 draw_line 方法中提供了启用抗锯齿的参数,但并不是所有情况下都能够正常工作(例如在移动/Web 平台上,或者启用 HDR 时)。另外,draw_polygon 中没有提供 antialiased
参数。
替代方案是安装并使用抗锯齿 Line2D 插件(也支持绘制抗锯齿的 Polygon2D)。请注意,这个插件依赖高级节点,而非底层 _draw()
函数。
工具
在编辑器中运行节点时,可能也会用到绘图。可以用于某些特性或行为的预览或可视化。详情请参阅 在编辑器中运行代码。