概述
在某些情况下我们可能需要在Godot中自定义2D网格,因为此时可能用TileMap
会显得太“重”,因为我们可能只需要其作为网格的功能却不需要它的其他功能,比如绘制瓦片地图。而且我们可能需要在网格功能的基础上,添加更多自定义的内容。
本篇就介绍如何通过几个简单的参数定义2D网格,以及用CanvasItem
的绘图函数来在Control
或Node2D
上显示网格。
最后,介绍如何将一个普通脚本变为为自定义节点,以便于在多个场景或多个项目中重复使用。
定义网格
定义一个2D网格,只需要2个参数,grid_size
和cell_size
:
grid_size
:定义网格的尺寸 ,也就是水平有多少列,垂直有多少行,当然你也可以拆分成类似rows
和cols
这样的两个单独参数cell_size
:定义单个单元格的尺寸
我们从一个简单的2D场景开始:
直接在空的Node2D
节点上添加如下代码:
extends Node2D
var grid_size = Vector2i(10,10) # 网格尺寸 - 有多少行、多少列
var cell_size = Vector2i(32,32) # 单元格大小
没错,一个逻辑上的二维网格就定义完了,虽然你无法看到它,但你绝对可以使用它。
显示网格
我们有两种思路绘制网格:
- 思路1:将单元格视为矩形,网格就是这些单元格水平和垂直阵列的结果
- 思路2:将整个网格视为多条水平线段和垂直线段依次绘制叠加的结果
基于单元格矩形绘制网格
为了方便绘制,这里首先编写一个简单的函数来获取对应位置单元格的矩形(Rect2
类型)。
# 返回对应单元格的矩形
func get_cell_rect(cell_pos:Vector2i) -> Rect2:
return Rect2(cell_pos * cell_size,cell_size)
然后我们只需要在_draw()
虚函数中,遍历网格中所有的(x,y)
组合的位置,用draw_rect
绘图函数绘制单元格矩形就可以了。
func _draw():
# 绘制网格
for x in grid_size.x:
for y in grid_size.y:
var rect:Rect2 = get_cell_rect(Vector2(x,y)) # 获取对应位置单元格的Rect2
draw_rect(rect,Color.YELLOW,false,1)
完整代码如下:
extends Node2D
var grid_size = Vector2i(10,10) # 网格尺寸 - 有多少行、多少列
var cell_size = Vector2i(32,32) # 单元格大小
# 返回对应单元格的矩形
func get_cell_rect(cell_pos:Vector2i) -> Rect2:
return Rect2(cell_pos * cell_size,cell_size)
func _draw():
# 绘制网格
for x in grid_size.x:
for y in grid_size.y:
var rect:Rect2 = get_cell_rect(Vector2(x,y)) # 获取对应位置单元格的Rect2
draw_rect(rect,Color.YELLOW,false,1)
运行场景后可以看到绘制出的网格:
优化代码
上面的代码优化如下:
@tool
extends Node2D
## 是否显示网格
@export var show_grid:bool = false:
set(val):
show_grid = val
queue_redraw()
## 网格尺寸
@export var grid_size = Vector2i(10,10):
set(val):
grid_size = val
queue_redraw()
## 单元格大小
@export var cell_size = Vector2i(32,32):
set(val):
cell_size = val
queue_redraw()
## 边框颜色
@export var border_color = Color.YELLOW:
set(val):
border_color = val
queue_redraw()
## 边框线宽
@export var border_width:float = 1.0:
set(val):
border_width = val
queue_redraw()
# 返回对应单元格的矩形
func get_cell_rect(cell_pos:Vector2i) -> Rect2:
return Rect2(cell_pos * cell_size,cell_size)
func _draw():
if show_grid:
# 绘制网格
for x in grid_size.x:
for y in grid_size.y:
var rect:Rect2 = get_cell_rect(Vector2(x,y)) # 获取对应位置单元格的Rect2
draw_rect(rect,border_color,false,border_width)
此时,你便拥有了一个参数化的2D网格,可以直接在检视器面板修改参数,且实时在Godot编辑器中查看效果:
初学看似神奇,实际很简单,这里我只给不知晓工具脚本的新手讲解一下,大佬可以略过。
我们拎出前面的几行代码来看:
@tool
extends Node2D
## 是否显示网格
@export var show_grid:bool = false:
set(val):
show_grid = val
queue_redraw()
- 首先,我在代码最顶部添加了
@tool
关键字,这意味着,整个脚本的逻辑可以直接在Godot编辑器中运行,而不需要运行场景 - 其次,我用
@export var
申明了一个布尔类型的show_grid
导出变量,默认值为false
,导出变量是一种可以在检视器面板显示和编辑的脚本变量。 - 在
@export var
申明导出变量的最后,我添加了一个额外的冒号,回车缩进一格后使用set(val):
,这是Godot4新版本的GDScript中,为变量设定setter
函数的新写法 - 在
show_grid
变量的setter
函数体中,我们用show_grid = val
形式将参数val
传入的新值赋值给变量自身 - 接着我们调用
queue_redraw()
,它会主动调用执行一次_draw()
然后我们再看_draw()
的部分:
func _draw():
if show_grid:
...
我们通过if
语句判断show_grid
的值是否为真,再执行其后续的代码。这样一切就联动起来了:
- 我们在检视器面板修改
show_grid
的值,也就是勾选或不勾选时,其setter
函数会在存储修改的新值后,调用queue_redraw()
, queue_redraw()
执行_draw()
,_draw()
内我们使用show_grid
的新值进行判断…
通过类似上面的联动方式,自己工具脚本在Godot编辑器中直接执行的特性,让我们可以通过修改导出变量,来获得实时生成的效果。
将普通脚本声明为自定义节点
上面的代码离自定义节点只差一步之遥。而这关键的一步就是声明自定义类名。
通过class_name
关键字,我们可以将普通脚本声明为自定义类,而如果这个脚本刚好是某个节点的脚本,那么这个自定义类就会因为extends
关键字的存在而继承这个节点的类型,从而成为其子类型,也就变成了一个自定义节点(Custom Node)。
@tool
class_name Grid2D # 申明自定义类名
extends Node2D # 继承自Node2D,所以Grid2D便是Node2D的子类型,也就成了自定义节点
class_name
和extends
可以分开两行写,也可以组合成一行写,也就是:
@tool
class_name Grid2D extends Node2D
而作为自定义节点,我们就可以通过添加节点对话框,查找和添加该类型的实例。
恭喜你已经学会了Godot自定义节点的基础技能!!
个人认为:
- 自定义节点、自定义资源、自定义类和自定义静态函数库以及面向对象思路是Godot没有明说的核心
- 包括编写Godot编辑器插件,因为这些自定义功能的存在,才让Godot拥有强大的拓展能力和无限可能。
拓展功能
上面已经成功编写出了自定义的2D网格节点,并且可以实现基础的定义、修改以及显示。
但是离实用性可能还差的很远。这时你就需要编写一些额外的方法来拓展这个自定义节点的功能,比如说:
- 获取鼠标所在位置单元格的坐标
- 获取某个单元格的中心点,放置棋子或玩家
- 基于网格实现上下左右移动等等
这里我只简单拓展几个函数,你可以根据自己的需要,自行拓展。
将屏幕坐标转为单元格坐标
这里的屏幕坐标指的是运行场景后,游戏窗体内的全局坐标。
# 将屏幕坐标转为单元格坐标
func to_cell_pos(screen_pos:Vector2):
return floor(screen_pos / Vector2(cell_size))
获取单元格中心位置
编写函数获取指定位置单元格的中心点。
# 获取单元格的中心点
func get_cell_center(cell_pos:Vector2i) -> Vector2:
return get_cell_rect(cell_pos).get_center()
我们申明一个变量target_pos
来全局的存储鼠标位置,然后在_input
中,设定当鼠标在屏幕上移动时,不断地记录新的鼠标位置,并调用queue_redraw()
请求执行_draw()
。
var target_pos:Vector2 # 记录鼠标位置
func _input(event):
if event is InputEventMouseMotion: # 鼠标移动
var mouse_pos = get_global_mouse_position() # 鼠标位置
target_pos = to_cell_pos(mouse_pos) # 获取鼠标所在位置单元格坐标
queue_redraw() # 请求重绘
同时我们修改_draw()
的代码,添加绘制鼠标所在位置单元格的中心点的代码:
func _draw():
if show_grid:
# 绘制网格
for x in grid_size.x:
for y in grid_size.y:
var rect:Rect2 = get_cell_rect(Vector2(x,y)) # 获取对应位置单元格的Rect2
draw_rect(rect,border_color,false,border_width)
# 绘制鼠标所在位置单元格的中心点
var center = get_cell_center(target_pos)
draw_circle(center,5,Color.CHARTREUSE)
运行场景,可以看到随着鼠标移动,会自动获取鼠标所在单元格的位置,并绘制一个绿色的圆点:
高亮显示鼠标所在单元格
在之前代码的基础上,我们只需要将_draw()
中绘制单元格中心点的代码改为绘制单元格矩形的就可以了:
func _draw():
if show_grid:
# 绘制网格
for x in grid_size.x:
for y in grid_size.y:
var rect:Rect2 = get_cell_rect(Vector2(x,y)) # 获取对应位置单元格的Rect2
draw_rect(rect,border_color,false,border_width)
# 绘制鼠标所在位置单元格矩形
var rect = get_cell_rect(target_pos)
draw_rect(rect,Color.GREEN_YELLOW,true)
运行后效果:
总结
本文介绍了如何用简单的参数定义一个自定义的二维网格,并通过工具脚本、导出变量以及申明自定义类,将其创建为一个可以复用的自定义参数化节点。
最后对其进行了一些方法的扩充。
补充:用多条水平线段和垂直线段形式绘制网格
通过绘制单元格矩形的方式来绘制网格,其中有很多边是重复绘制的,虽然看不出来,但是对有强迫症的人来说,还是可能无法接受。所以这里补充用多条线段绘制网格的方法。
用多条水平线段和垂直线段来绘制网格也有两种方式,一种是通过draw_line
绘图函数,一种是使用多draw_multiline
。前者每次只绘制一条线段,后者可以批量绘制线段。效果是一致的。
使用draw_line
func _draw():
if show_grid:
# 绘制网格
# 绘制所有水平线
for row in range(grid_size.y + 1):
var w = grid_size.x * cell_size.x
var offset = Vector2(0,cell_size.y) * row
var h_line = [Vector2(0,0) + offset,Vector2(w,0) + offset]
draw_line(h_line[0],h_line[1],border_color,border_width)
# 绘制所有垂直线
for col in range(grid_size.x + 1):
var h = grid_size.y * cell_size.y # 垂直线高度
var offset = Vector2(cell_size.x,0) * col
var v_line = [Vector2(0,0) + offset,Vector2(0,h) + offset]
draw_line(v_line[0],v_line[1],border_color,border_width)
使用draw_multiline
func _draw():
if show_grid:
# 绘制网格
var h_lines:PackedVector2Array = [] # 所有水平线
var v_lines:PackedVector2Array = [] # 所有垂直线
# 获取所有水平线
for row in range(grid_size.y + 1):
var w = grid_size.x * cell_size.x
var offset = Vector2(0,cell_size.y) * row
var h_line = [Vector2(0,0) + offset,Vector2(w,0) + offset]
h_lines.append_array(h_line)
# 获取所有垂直线
for col in range(grid_size.x + 1):
var h = grid_size.y * cell_size.y # 垂直线高度
var offset = Vector2(cell_size.x,0) * col
var v_line = [Vector2(0,0) + offset,Vector2(0,h) + offset]
v_lines.append_array(v_line)
draw_multiline(h_lines,border_color,border_width)
draw_multiline(v_lines,border_color,border_width)