告别TileMap!用Godot4.2手搓一个轻量级2D网格节点(附鼠标交互与高亮源码)
Godot4.2轻量级2D网格解决方案:从基础实现到交互优化
在游戏开发中,2D网格系统是构建棋盘游戏、策略游戏和关卡编辑器的基础设施。虽然Godot内置的TileMap功能强大,但对于只需要基础网格功能的项目来说,它显得过于笨重。本文将带你从零开始构建一个轻量级的自定义2D网格节点,实现比TileMap更高效的解决方案。
1. 为什么需要自定义网格节点
TileMap作为Godot的官方解决方案,确实提供了完善的瓦片地图功能。但在实际开发中,我们经常遇到以下场景:
- 只需要基础的网格布局功能
- 需要完全自定义的网格渲染样式
- 项目对性能极其敏感,需要最小化开销
- 要集成特殊的交互逻辑
性能对比测试数据:
| 功能 | TileMap | 自定义网格 |
|---|---|---|
| 内存占用 | 较高 | 极低 |
| 绘制调用 | 多 | 少 |
| 初始化速度 | 慢 | 快 |
| 灵活性 | 受限 | 完全可控 |
提示:在只需要基础网格功能的项目中,自定义方案通常能减少30%-50%的性能开销
2. 核心网格实现
2.1 基础参数定义
我们从一个简单的Node2D节点开始,只需要两个核心参数:
extends Node2D var grid_size = Vector2i(10, 10) # 网格行列数 var cell_size = Vector2i(32, 32) # 单元格像素尺寸这种定义方式相比TileMap的优势在于:
- 参数简单直观
- 没有多余的瓦片数据开销
- 完全可控的绘制逻辑
2.2 网格绘制方法
Godot提供了多种绘制2D图形的方式,对于网格系统,我们主要考虑两种绘制策略:
单元格矩形绘制法:
func _draw(): for x in grid_size.x: for y in grid_size.y: var rect = Rect2(Vector2(x, y) * cell_size, cell_size) draw_rect(rect, Color.YELLOW, false, 1.0)线段批量绘制法:
func _draw(): # 水平线 for row in grid_size.y + 1: var start = Vector2(0, row * cell_size.y) var end = Vector2(grid_size.x * cell_size.x, row * cell_size.y) draw_line(start, end, Color.YELLOW, 1.0) # 垂直线 for col in grid_size.x + 1: var start = Vector2(col * cell_size.x, 0) var end = Vector2(col * cell_size.x, grid_size.y * cell_size.y) draw_line(start, end, Color.YELLOW, 1.0)
性能考量:
- 小网格(<50x50):两种方法差异不大
- 大网格:线段法通常更高效
- 动态更新:矩形法局部更新更方便
3. 编辑器集成与参数化
为了让我们的网格节点更加实用,我们需要实现编辑器集成:
@tool class_name Grid2D extends Node2D @export var show_grid := true: set(value): show_grid = value queue_redraw() @export var grid_size := Vector2i(10, 10): set(value): grid_size = value queue_redraw() @export var cell_size := Vector2i(32, 32): set(value): cell_size = value queue_redraw() @export var line_color := Color.YELLOW: set(value): line_color = value queue_redraw() @export var line_width := 1.0: set(value): line_width = value queue_redraw()关键实现技巧:
@tool使脚本在编辑器中运行@export暴露参数到检查器- setter函数确保参数修改后自动重绘
queue_redraw()触发_draw()调用
4. 交互功能实现
4.1 鼠标坐标转换
实现鼠标与网格交互的基础是将屏幕坐标转换为网格坐标:
func get_cell_at_position(screen_pos: Vector2) -> Vector2i: return Vector2i(floor(screen_pos / cell_size))4.2 悬停高亮效果
通过_input事件和重绘实现动态高亮:
var highlighted_cell := Vector2i(-1, -1) func _input(event): if event is InputEventMouseMotion: highlighted_cell = get_cell_at_position(get_global_mouse_position()) queue_redraw() func _draw(): # 基础网格绘制... # 高亮绘制 if highlighted_cell.x >= 0 && highlighted_cell.y >= 0: var rect = Rect2(highlighted_cell * cell_size, cell_size) draw_rect(rect, Color(1, 1, 0, 0.3), true)4.3 点击事件处理
添加网格点击检测:
signal cell_clicked(cell_position: Vector2i) func _unhandled_input(event): if event is InputEventMouseButton and event.pressed: var cell = get_cell_at_position(get_global_mouse_position()) if cell.x >= 0 && cell.y >= 0 && cell.x < grid_size.x && cell.y < grid_size.y: emit_signal("cell_clicked", cell)5. 高级功能扩展
5.1 自定义绘制样式
通过扩展绘制函数实现多样化的网格样式:
@export enum DrawStyle {LINES, DOTS, CHECKERED} @export var draw_style: DrawStyle = DrawStyle.LINES func _draw(): match draw_style: DrawStyle.LINES: draw_line_grid() DrawStyle.DOTS: draw_dot_grid() DrawStyle.CHECKERED: draw_checkered_grid()5.2 动态网格调整
实现运行时动态调整网格大小:
func resize_grid(new_size: Vector2i): grid_size = new_size # 可能需要调整父节点大小或其他相关逻辑 queue_redraw()5.3 性能优化技巧
对于大型网格,考虑以下优化:
视口裁剪:
func _draw(): var visible_rect = get_viewport_rect() # 只绘制可见区域的网格批处理绘制:
func draw_line_grid(): var lines := PackedVector2Array() # 收集所有线段 draw_multiline(lines, line_color, line_width)LOD(细节层级):
func _draw(): var zoom = get_viewport().get_camera_2d().zoom.x if zoom < 0.5: draw_simplified_grid() else: draw_detailed_grid()
6. 实际应用案例
6.1 棋盘游戏实现
以五子棋为例展示网格节点的应用:
func _ready(): cell_clicked.connect(_on_cell_clicked) func _on_cell_clicked(cell: Vector2i): var center = cell * cell_size + cell_size / 2 var piece = CircleShape2D.new() piece.radius = cell_size.x * 0.4 draw_circle(center, piece.radius, current_player_color)6.2 策略游戏地图
构建基于网格的策略游戏地图:
var terrain_map := {} func set_terrain(cell: Vector2i, type: TerrainType): terrain_map[cell] = type queue_redraw() func _draw(): for cell in terrain_map: var rect = Rect2(cell * cell_size, cell_size) draw_texture(terrain_textures[terrain_map[cell]], rect.position)6.3 UI布局系统
将网格用作UI布局工具:
func arrange_controls_in_grid(): for i in controls.size(): var col = i % grid_size.x var row = i / grid_size.x controls[i].position = Vector2(col, row) * cell_size controls[i].size = cell_size7. 完整实现与最佳实践
一个完整的网格节点实现应包含:
- 网格基础参数(尺寸、单元格大小)
- 多种绘制模式
- 坐标转换功能
- 交互事件处理
- 性能优化选项
推荐的项目结构:
Grid2D/ ├── grid_2d.gd # 主脚本 ├── demo/ │ ├── board_game # 棋盘游戏示例 │ ├── strategy_map # 策略地图示例 │ └── ui_layout # UI布局示例 └── README.md # 使用文档注意:在实际项目中,建议将网格节点打包为插件,方便在不同项目间复用
在实现过程中,我发现最实用的几个技巧是:
- 使用
@tool实现编辑器实时预览 - 通过信号机制解耦交互逻辑
- 为不同使用场景提供多种绘制模式
- 在大型网格中实现视口裁剪优化
将网格系统模块化后,可以轻松应用到各种类型的2D项目中,相比直接使用TileMap,这种自定义方案提供了更好的性能和更高的灵活性。特别是在需要特殊交互逻辑或自定义视觉表现的场景下,优势更加明显。
