【Godot4.3】CanvasShape资源化改造
概述
通过把之前自定义的CanvasShape
类变成资源类型,将可以同时用于CanvasItem
绘图和创建扩展的Node2D
和Polygon2D
节点等。
本篇就完成CanvasShape
类的资源化改造,并记录改造过程和思路。
CanvasShape资源类型体系
CanvasShape
仍然为图形基类,提供共有的属性和方法,只不过改为继承自Resource
,成为自定义资源类型CanvasXXX
代表具体的图形类,扩展自CanvasShape
,因为继承的好处,可以只关注自己的参数设计
CanvasShape(图形基类)
# =============================================
# 名称:CanvasShape
# 类型:Resource
# 描述:资源化后的CanvasItem绘图函数图形基类
# 作者:巽星石
# 创建时间:2024年7月25日15:25:24
# 最后修改时间:2024年9月5日22:26:00
# =============================================
@tool
@icon("res://lib/绘图相关/CanvasShape/icons/CanvasShape.png")
class_name CanvasShape extends Resource
# ============================== 属性 ==============================
@export_category("CanvasShape")
# -------------------- 路径点
# 点集合
@export var points:PackedVector2Array = []:
set(val):
points = val
emit_changed() # 触发资源的changed信号
# 是否闭合轮廓线
@export var close_path:bool = false:
set(val):
close_path = val
emit_changed() # 触发资源的changed信号
# 是否绘制局部坐标系原点
@export var draw_position:bool = false:
set(val):
draw_position = val
emit_changed() # 触发资源的changed信号
# -------------------- 边线样式
@export_group("border")
## 是否绘制轮廓
@export var draw_border:bool = true:
set(val):
draw_border = val
emit_changed() # 触发资源的changed信号
## 虚线间隔
@export_range(0.0,100.0,1.0,"suffix:px") var dash = 0.0:
set(val):
dash = val
emit_changed() # 触发资源的changed信号
## 轮廓线宽度
@export_range(0,100,1,"suffix:px") var border_width:int = 1:
set(val):
border_width = val
emit_changed() # 触发资源的changed信号
## 轮廓线颜色
@export var border_color:Color = Color.BLACK:
set(val):
border_color = val
emit_changed() # 触发资源的changed信号
# -------------------- 边线样式
@export_group("fill")
## 是否绘制填充
@export var draw_fill:bool = true:
set(val):
draw_fill = val
emit_changed() # 触发资源的changed信号
## 填充颜色
@export var fill_color:Color = Color.WHITE:
set(val):
fill_color = val
emit_changed() # 触发资源的changed信号
# -------------------- 偏移
## 相对于坐标原点的偏移位置
@export var offset:Vector2 = Vector2.ZERO:
set(val):
offset = val
emit_changed() # 触发资源的changed信号
# -------------------- 阴影设置
@export_group("shadow")
## 是否绘制阴影
@export var draw_shadow:bool = false:
set(val):
draw_shadow = val
emit_changed() # 触发资源的changed信号
## 阴影颜色
@export var shadow_color:Color = Color(Color.BLACK.lightened(0.1),0.6):
set(val):
shadow_color = val
emit_changed() # 触发资源的changed信号
## 阴影偏移
@export var shadow_offset:Vector2 = Vector2(10,10):
set(val):
shadow_offset = val
emit_changed() # 触发资源的changed信号
# 顶点绘制参数
@export_group("point")
## 是否绘制顶点
@export var draw_points:bool = false:
set(val):
draw_points = val
emit_changed() # 触发资源的changed信号
## 顶点圆半径
@export var point_r = 3:
set(val):
point_r = val
emit_changed() # 触发资源的changed信号
## 顶点轮廓线宽度
@export var point_border_width:int = 1:
set(val):
point_border_width = val
emit_changed() # 触发资源的changed信号
## 顶点轮廓线颜色
@export var point_border_color:Color = Color.BLACK:
set(val):
point_border_color = val
emit_changed() # 触发资源的changed信号
## 顶点填充颜色
@export var point_fill_color:Color = Color.WHITE:
set(val):
point_fill_color = val
emit_changed() # 触发资源的changed信号
# 矩形范围绘制参数
@export_group("rect2")
## 是否绘制矩形范围
@export var draw_rect2:bool = false:
set(val):
draw_rect2 = val
emit_changed() # 触发资源的changed信号
## 矩形轮廓线宽度
@export var rect2_border_width:int = 1:
set(val):
rect2_border_width = val
emit_changed() # 触发资源的changed信号
## 矩形轮廓线颜色
@export var rect2_border_color:Color = Color.AQUAMARINE:
set(val):
rect2_border_color = val
emit_changed() # 触发资源的changed信号
# ============================== 方法 ==============================
func draw(canvas:CanvasItem) -> void:
# 对所有点进行位移变换
var offset_points = Transform2D().translated(offset) * points
# 1.绘制阴影
if draw_shadow and offset_points.size()>2: # 至少有3个点
var shadow_points = Transform2D().translated(shadow_offset) * offset_points
canvas.draw_colored_polygon(shadow_points,shadow_color)
# 2.绘制矩形范围
if draw_rect2:
canvas.draw_rect(get_rect(),Color(rect2_border_color,0.5),false,rect2_border_width)
# 3.绘制填充
if draw_fill and fill_color and offset_points.size()>2: # 至少有3个点
canvas.draw_colored_polygon(offset_points,fill_color)
# 4.绘制轮廓
if draw_border and border_width!=0 and border_color!=null:
var close_points = offset_points.duplicate()
# 闭合
if close_path:
close_points.append(close_points[0])
if dash>0.0: # 虚线间隔大于0
for seg in segments(close_points):
canvas.draw_dashed_line(seg[0],seg[1],border_color,border_width,dash)
pass
else:
canvas.draw_polyline(close_points,border_color,border_width) # 绘制实现闭合轮廓
# 5.绘制顶点
if draw_points:
for p in points:
# 绘制圆点
canvas.draw_circle(p,point_r,point_fill_color)
# 绘制边线
canvas.draw_arc(p,point_r,0,TAU,TAU * point_r,point_border_color,point_border_width/2.0,true)
# 6.绘制位置点
if draw_position:
var line_count = 4
var ang = 360.0/float(line_count) # 每次旋转角度
for i in range(line_count):
var p1 = offset
var p2 = p1 + pVector2(ang * i,5)
canvas.draw_line(p1,p2,border_color,border_width)
# 获取图形的矩形
func get_rect() -> Rect2:
# 对所有点进行位移变换
var points = Transform2D().translated(offset) * points
# 拆分出X坐标和Y坐标数组
var x_arr = []
var y_arr = []
for p in points:
x_arr.append(p.x)
y_arr.append(p.y)
# 最小值构成Rect2的offset
var pos = Vector2(x_arr.min(),y_arr.min())
# 最大值 - pos = Rect2 的 size
var siz = Vector2(x_arr.max(),y_arr.max()) - pos
print(pos," ",siz)
return Rect2(pos,siz)
# ============================== 子类通用函数 ==============================
# 极坐标点函数 - 通过角度和长度定义一个点
func pVector2(angle:float = 0.0,length:float =0.0) -> Vector2:
var dir = Vector2.RIGHT.rotated(deg_to_rad(angle))
return dir * length
# points的点按顺序两两相连的所有线段
func segments(points:PackedVector2Array) -> Array[PackedVector2Array]:
var arr:Array[PackedVector2Array]
if points.size() >1: # 至少有两个点
for i in range(points.size() -1):
var seg:PackedVector2Array = [points[i],points[i+1]]
arr.append(seg)
return arr
代码改造要点:
- 属性:单纯的类设计时,属性不能也不需要被设计为导出变量形式,而自定义资源类型,其参数大多数都需要设置为导出变量,用于方便在编辑器检视器面板修改
# 点集合
@export var points:PackedVector2Array = []:
set(val):
points = val
emit_changed() # 触发资源的changed信号
emit_changed()
是触发资源实例的changed
信号,在自定义节点或场景中我们可以连接此信号,用于在资源的属性修改后,进行一定的处理
CanvasRect(矩形)
# =============================================
# 名称:CanvasRect
# 类型:类
# 描述:CanvasItem绘图函数图形类 - 矩形
# 作者:巽星石
# 创建时间:2024年7月27日16:07:23
# 最后修改时间:2024年9月5日23:08:37
# =============================================
@tool
class_name CanvasRect extends CanvasShape
# ============================== 属性 ==============================
# 宽度
@export var width:float = 50:
set(val):
width = val
update_points()
emit_changed()
# 高度
@export var height:float = 50:
set(val):
height = val
update_points()
emit_changed()
func _init() -> void:
close_path = true
update_points()
# ============================== 方法 ==============================
# 更新点集
func update_points() -> void:
points.clear()
var center = Vector2(width,height)/2.0
var half_width = Vector2(width,0)/2.0
var half_height = Vector2(0,height)/2.0
# 求点
points.append(offset - center)
points.append(offset - half_height + half_width)
points.append(offset + center)
points.append(offset - half_width + half_height)
func draw(canvas:CanvasItem) -> void:
# 绘制
super.draw(canvas)
CanvasRegularPolygon(正多边形)
# =============================================
# 名称:CanvasRegularPolygon
# 类型:类
# 描述:CanvasItem绘图函数图形类 - 正多边形
# 作者:巽星石
# 创建时间:2024年7月25日16:03:41
# 最后修改时间:2024年9月5日23:09:18
# =============================================
@tool
class_name CanvasRegularPolygon extends CanvasShape
# ============================== 属性 ==============================
## 正多边形的外接圆半径
@export var r:float = 30:
set(val):
r = val
update_points()
emit_changed()
## 正多边形的边数
@export_range(3,1000,1,"suffix:边") var edges:int = 3:
set(val):
edges = val
update_points()
emit_changed()
## 起始角度(与X轴正方向夹角)
@export_range(-360,360,1,"degrees") var start_angle:= 0.0:
set(val):
start_angle = val
update_points()
emit_changed()
func _init() -> void:
close_path = true
update_points()
# ============================== 方法 ==============================
# 更新点集
func update_points() -> void:
points.clear()
# 求点
var ang = 360.0/float(edges) # 每次旋转角度
for i in range(edges):
points.append(pVector2(i * ang + start_angle,r))
func draw(canvas:CanvasItem) -> void:
# 绘制
super.draw(canvas)
CanvasLine(线段)
# =============================================
# 名称:CanvasLine
# 类型:类
# 描述:CanvasItem绘图函数图形类 - 线段
# 作者:巽星石
# 创建时间:2024年7月27日16:33:28
# 最后修改时间:2024年9月5日23:10:27
# =============================================
@tool
class_name CanvasLine extends CanvasShape
# ============================== 属性 ==============================
# 起点
@export var p1:=Vector2():
set(val):
p1 = val
update_points()
emit_changed()
# 起点
@export var p2:=Vector2(100,0):
set(val):
p2 = val
update_points()
emit_changed()
func _init() -> void:
border_color = Color.WHITE
update_points()
# ============================== 方法 ==============================
# 更新点集
func update_points() -> void:
points.clear()
# 求点
points.append(p1)
points.append(p2)
func draw(canvas:CanvasItem) -> void:
# 绘制
super.draw(canvas)
测试场景
场景根节点代码如下:
@tool
extends Node2D
# 修改shape属性时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
@export var shape:CanvasShape:
set(val):
shape = val
queue_redraw()
_enter_tree()
# 加载场景时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
func _enter_tree() -> void:
if shape:
shape.changed.connect(func():
queue_redraw()
)
# 调用CanvasShape的draw()方法,绘制
func _draw() -> void:
if shape:
shape.draw(self)
此时便可以在检视器面板选择相应的CanvasShape
资源及其子类型了。赋值shape属性后,会在场景根节点按默认参数绘制图形,修改资源的属性时,根节点会动态进行修改。
自定义图标
在InkScape中简单绘制一个大致正方形区域的图标,导出图标到Godot项目中。可以是SVG或PNG格式。
在自定义资源或节点顶部用@icon("图标路径")
语法可以设定图标。例如:
@icon("res://lib/绘图相关/CanvasShape/icons/CanvasShape.png")
此时,就可以在检视器面板看到自定义类的图标:
ShapeNode2D
通过为上面测试场景的根节点设定class_name
,就可以创建自定义的Node2D
节点。这里我创建了一个专门显示CanvasShape
资源的ShapeNode2D
节点。
# =============================================
# 名称:ShapeNode2D
# 类型:Node2D
# 描述:专用于显示CanvasShape资源图形的自定义2D节点
# 作者:巽星石
# 创建时间:2024年9月5日20:04:05
# 最后修改时间:2024年9月5日23:03:01
# =============================================
@tool
@icon("res://lib/绘图相关/CanvasShape/icons/CanvasShape.png")
class_name ShapeNode2D extends Node2D
# 修改shape属性时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
@export var shape:CanvasShape:
set(val):
shape = val
queue_redraw()
_enter_tree()
# 加载场景时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
func _enter_tree() -> void:
if shape:
shape.changed.connect(func():
queue_redraw()
)
# 调用CanvasShape的draw()方法,绘制
func _draw() -> void:
if shape:
shape.draw(self)
ShapeNode2D实例化测试
ShapeNode2D
只有一个shape
属性,通过设定CanvasShape
资源及其子类型,然后设定其属性就可以动态的绘制出一些参数化的2D几何图形。
- 基于继承的优势:因为
ShapeNode2D
继承自Node2D
,所以它天然的可以使用Node2D
类型及其继承链上的所有类型的属性、方法和信号。当然也就包括了Node2D的位移、旋转、缩放等。
ShapePolygon2D
以同样的思路,我们可以设计一个Polygon2D
的扩展类型节点。
# =============================================
# 名称:ShapePolygon2D
# 类型:Polygon2D拓展类型
# 描述:专用于显示CanvasShape资源图形的自定义Polygon2D节点
# 作者:巽星石
# 创建时间:2024年9月5日23:59:17
# 最后修改时间:2024年9月6日00:02:23
# =============================================
@tool
class_name ShapePolygon2D extends Polygon2D
# 修改shape属性时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
@export var shape:CanvasShape:
set(val):
shape = val
update_polygon()
_enter_tree()
# =================================== 虚函数 ===================================
func _init():
update_polygon()
# 加载场景时,连接CanvasShape资源的changed信号处理函数
# 仅在已经存在赋值时
func _enter_tree() -> void:
if shape:
shape.changed.connect(func():
update_polygon()
)
# =================================== 方法 ===================================
# 更新形状
func update_polygon():
polygon = shape.points
ShapePolygon2D测试
总结
- 通过将
CanvasItem
绘制的图形编写为自定义类CanvasShape
,完成了第一次简单的进化 - 而将
CanvasShape
变成自定义资源类型,则完成了二次进化,它将可以用于Node2D、Control、Polygon2D以及其他自定义节点上,也可以单独用于代码形式的CanvasItem
绘图 - 通过在类、场景节点或自定义节点中设定
Array[CanvasShape]
的导出变量,我们将可以在检视器面板设定和维护多个CanvasShape
的列表。