二维自定义绘图¶
为什么?¶
Godot有节点来绘制精灵、多边形、粒子和各种各样的东西。在大多数情况下,这是足够的;但并非总是如此。在恐惧、焦虑和愤怒中哭泣之前,因为一个节点要画出特定的 某物 不存在…很高兴知道可以轻松地制作任何二维节点(无论是 Control 或 Node2D 基于)绘制自定义命令。它是 真正地 也很容易做到。
但是…¶
在节点中手动自定义绘图是 真正地 有用。以下是一些例子,为什么:
绘制不由节点处理的形状或逻辑(例如:制作绘制圆的节点、带有轨迹的图像、特殊类型的动画多边形等)。
与节点不兼容的可视化效果:(例如:俄罗斯方块板)。俄罗斯方块示例使用自定义绘制函数绘制块。
绘制大量简单对象。自定义绘图避免了使用节点的开销,这使得它占用的内存更少,而且可能更快。
生成自定义UI控件。有很多可用的控件,但很容易遇到制作新的自定义控件的需要。
好吧,怎么办?¶
将脚本添加到任何 CanvasItem 派生节点,例如 Control 或 Node2D . 然后重写 _draw()
功能。
extends Node2D
func _draw():
# Your draw commands here
pass
public override void _Draw()
{
// Your draw commands here
}
绘图命令在 CanvasItem 类引用。它们有很多。
正在更新¶
这个 _draw()
只调用一次函数,然后缓存并记住draw命令,因此不需要进一步调用。
如果由于状态或其他更改而需要重新绘制,只需调用 CanvasItem.update() 在同一个节点上 _draw()
呼叫将发生。
下面是一个更复杂的例子,一个纹理变量,如果修改,将重新绘制:
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
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());
}
}
在某些情况下,可能需要绘制每个帧。为此,请致电 update()
从 _process()
回调,如下所示:
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中,远离相机),您可以减少其点数以节省处理成本;这被称为 详细程度(LOD) . 在我们的示例中,我们将简单地使用固定数量的点,不管半径如何。
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
S
下一步是计算组成圆弧的32个点的实际位置。这是在第一个for循环中完成的:我们迭代要计算位置的点数,再加上一个点来包含最后一个点。我们首先确定每个点的角度,在起始角和结束角之间。
每个角度减少90°的原因是我们将使用三角法(你知道,余弦和正弦的东西…)计算每个角度的二维位置。不过,简单来说, cos()
和 sin()
使用弧度,而不是度数。0度角(0弧度)从3点开始,尽管我们想从12点开始计数。所以我们把每个角度减小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)
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);
}
结果:
圆弧多边形函数¶
我们可以更进一步,不仅编写一个函数来绘制由弧定义的圆盘的平面部分,还可以绘制它的形状。该方法与以前完全相同,只是我们绘制的是多边形而不是直线:
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);
}
动态自定义绘图¶
好吧,我们现在可以在屏幕上绘制自定义内容了。但是,它是静态的;让我们让这个形状围绕中心旋转。这样做的解决方案就是简单地将角度“从”和“角度”更改为随时间变化的值。对于我们的示例,我们只需将它们增加50。该增量值必须保持不变,否则转速将相应变化。
首先,我们必须在脚本的顶部使角度从和角度到变量都是全局的。还要注意,您可以将它们存储在其他节点中,并使用 get_node()
.
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;
}
我们在_过程(delta)函数中更改这些值。
我们也从这里增加角度,从角度到值。但是,我们不能忘记 wrap()
结果值介于0和360°之间!也就是说,如果角度是361°,那么它实际上是1°。如果不包装这些值,脚本将正常工作,但角度值将随着时间的推移而越来越大,直到达到godot可以管理的最大整数值。 (2^31 - 1
)当这种情况发生时,Godot可能崩溃或产生意想不到的行为。
最后,我们不能忘记打电话给 update()
函数,自动调用 _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)
update()
private float Wrap(float value, float minVal, float maxVal)
{
float f1 = value - minVal;
float f2 = maxVal - minVal;
return (f1 % f2) + minVal;
}
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 = Wrap(_angleFrom, 0, 360);
_angleTo = Wrap(_angleTo, 0, 360);
}
Update();
}
另外,不要忘记修改 _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 )
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实际上正在尽可能快地显示帧。我们需要以这种速度“规范化”绘图;要实现这一点,我们必须利用 delta
的参数 _process()
功能。 delta
包含最后两个渲染帧之间经过的时间。它通常很小(大约0.0003秒,但这取决于您的硬件),因此使用 delta
要控制绘图,确保程序在每个人的硬件上以相同的速度运行。
在我们的例子中,我们只需要将 rotation_angle
变量 delta
在 _process()
功能。这样,我们的两个角度将增加一个更小的值,这直接取决于渲染速度。
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();
}
我们再跑一次!这次,旋转显示良好!