Godot4.2实战:用AstarGrid2D给你的战棋游戏做个“行动力范围”高亮(含四种对角线模式详解)
Godot4.2实战:用AstarGrid2D实现战棋游戏行动力范围高亮
战棋游戏的核心乐趣之一在于策略性移动,而行动力范围的可视化则是提升玩家决策体验的关键。本文将深入探讨如何利用Godot4.2的AstarGrid2D系统,为战棋角色实现精确的行动力范围计算与高亮显示,并详细解析四种对角线模式在不同游戏规则中的应用差异。
1. AstarGrid2D基础配置与行动力原理
在开始构建行动力系统前,我们需要建立基础的网格导航环境。与传统的Astar2D相比,AstarGrid2D通过预定义网格结构大幅简化了路径计算的初始化工作。
var astar_grid = AStarGrid2D.new() var grid_size = Vector2i(20, 20) # 20x20的网格 var cell_size = Vector2i(64, 64) # 每格64像素 func _ready(): astar_grid.size = grid_size astar_grid.cell_size = cell_size astar_grid.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN astar_grid.update()行动力(Movement Points)系统的核心在于:
- 每个角色拥有固定的移动点数
- 每移动一格消耗1点(正交移动)或1.5点(对角线移动)
- 行动力范围是所有移动消耗不超过总行动力的可到达格子
注意:实际项目中建议将行动力数值乘以2处理,避免浮点数运算带来的精度问题。例如将3点行动力记为6,对角线移动消耗3。
2. 四种对角线模式深度解析
AstarGrid2D提供了四种对角线处理模式,直接影响行动力范围的计算结果:
| 模式 | 枚举值 | 适用场景 | 移动消耗计算 |
|---|---|---|---|
| DIAGONAL_MODE_NEVER | 1 | 国际象棋车、中国象棋车 | 仅正交移动,对角线完全禁止 |
| DIAGONAL_MODE_ALWAYS | 0 | 国际象棋王、皇后 | 对角线与正交移动消耗相同 |
| DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE | 2 | 火焰纹章系列 | 相邻两格至少有一格可通行才允许对角线 |
| DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES | 3 | 高级战争系列 | 相邻两格都必须可通行才允许对角线 |
典型应用场景对比:
- 中国象棋"士"的移动:
astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ALWAYS # 同时需要自定义is_in_bounds()检查九宫格限制- 战棋类游戏标准移动:
astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE # 配合行动力消耗系数1.5倍3. 高效计算行动力范围的三种方法
3.1 矩形范围筛选法
最直观的实现方式,适合小范围移动或性能要求不高的场景:
func get_movement_range(start_pos: Vector2i, move_points: int) -> Array: var reachable = [] var rect = Rect2i( start_pos - Vector2i(move_points, move_points), Vector2i(2 * move_points + 1, 2 * move_points + 1) ) for x in range(rect.position.x, rect.end.x): for y in range(rect.position.y, rect.end.y): var target = Vector2i(x, y) if !astar_grid.is_in_bounds(target): continue var path = astar_grid.get_point_path(start_pos, target) if path.is_empty(): continue var cost = calculate_path_cost(path) if cost <= move_points: reachable.append(target) return reachable3.2 广度优先搜索优化版
更高效的算法实现,特别适合大范围移动计算:
func bfs_movement_range(start: Vector2i, max_cost: int) -> Dictionary: var frontier = [start] var cost_so_far = {start: 0} var came_from = {} while not frontier.is_empty(): var current = frontier.pop_front() for neighbor in get_neighbors(current): var new_cost = cost_so_far[current] + get_move_cost(current, neighbor) if new_cost > max_cost: continue if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]: cost_so_far[neighbor] = new_cost frontier.append(neighbor) came_from[neighbor] = current return { "reachable": cost_so_far.keys(), "paths": came_from }3.3 预计算与缓存策略
对于固定地图的战棋游戏,可以采用预处理技术:
- 预先计算所有格子到周围格子的移动消耗
- 将结果存储在二维数组或位图中
- 运行时直接查询预处理数据
var movement_cache = [] func precompute_movement_costs(): movement_cache.resize(grid_size.x) for x in grid_size.x: movement_cache[x] = [] movement_cache[x].resize(grid_size.y) for y in grid_size.y: movement_cache[x][y] = compute_cell_movement(Vector2i(x,y)) func compute_cell_movement(pos: Vector2i) -> Dictionary: # 实现与bfs_movement_range类似,但只计算单格信息 ...4. 行动力范围的可视化实现
计算得到可移动格子后,我们需要将其直观地展示给玩家。以下是几种常见的可视化方案:
4.1 基础高亮方案
func draw_movement_range(reachable_cells: Array): var highlight = Color(0, 1, 0, 0.3) # 半透明绿色 for cell in reachable_cells: var rect = Rect2(cell * cell_size, cell_size) draw_rect(rect, highlight, true)4.2 梯度颜色方案
根据移动消耗显示不同颜色深度:
func draw_gradient_range(cost_map: Dictionary, max_cost: int): for cell in cost_map: var ratio = float(cost_map[cell]) / max_cost var color = Color(1 - ratio, ratio, 0, 0.5) # 红到绿渐变 draw_rect(Rect2(cell * cell_size, cell_size), color, true)4.3 高级Shader效果
使用材质着色器实现更炫酷的效果:
shader_type canvas_item; uniform vec4 highlight_color : source_color = vec4(0,1,0,0.5); uniform float grid_size; uniform float pulse_speed = 1.0; uniform float time; void fragment() { vec2 grid_pos = fract(UV * grid_size); float dist = distance(grid_pos, vec2(0.5)); float alpha = smoothstep(0.4, 0.5, dist); float pulse = sin(time * pulse_speed) * 0.1 + 0.9; COLOR = highlight_color * vec4(pulse) * (1.0 - alpha); }5. 性能优化技巧
当处理大型地图或多角色时,性能优化至关重要:
分层计算:
- 先计算粗略范围(如矩形区域)
- 再对边缘格子进行精确计算
增量更新:
var cached_movement = {} func get_movement_range_optimized(unit): if unit.position in cached_movement: return cached_movement[unit.position] # 否则执行完整计算并缓存 var result = calculate_full_range(unit) cached_movement[unit.position] = result return result多线程处理:
func calculate_in_thread(start_pos: Vector2i, move_points: int): var thread = Thread.new() thread.start(_thread_calculate.bind(start_pos, move_points)) func _thread_calculate(start_pos, move_points): var result = bfs_movement_range(start_pos, move_points) call_deferred("_on_calculation_done", result)可视区域优化:
func get_visible_range(camera_rect: Rect2): var visible_cells = [] var grid_rect = Rect2i( floor(camera_rect.position / cell_size), ceil(camera_rect.size / cell_size) ) # 只计算视野内的格子 ...
6. 特殊地形与行动力消耗
真实战棋游戏通常包含多种地形类型,需要扩展基础系统:
enum TERRAIN { PLAIN, # 平原 消耗1 FOREST, # 森林 消耗2 MOUNTAIN, # 山地 消耗3 WATER # 水域 不可通行 } var terrain_map = [] func get_move_cost(from: Vector2i, to: Vector2i) -> float: var base_cost = 1.0 if is_diagonal_move(from, to): base_cost = 1.5 var terrain_cost = get_terrain_cost(to) return base_cost * terrain_cost func get_terrain_cost(pos: Vector2i) -> int: return TERRAIN.values()[terrain_map[pos.x][pos.y]]对应的路径计算需要修改为:
func custom_path_cost(from: Vector2i, to: Vector2i) -> float: if astar_grid.is_point_solid(to): return -1 # 不可通行 var cost = get_move_cost(from, to) return cost7. 完整实现示例
以下是一个集成行动力计算、地形系统和高亮显示的完整场景示例:
extends Node2D class_name TacticalMovementSystem @export var grid_width: int = 20 @export var grid_height: int = 20 @export var cell_size: int = 64 var astar_grid: AStarGrid2D var terrain_map = [] var movement_range = [] var highlight_sprites = [] func _ready(): initialize_grid() generate_terrain() update_movement_range(Vector2i(5,5), 4) func initialize_grid(): astar_grid = AStarGrid2D.new() astar_grid.size = Vector2i(grid_width, grid_height) astar_grid.cell_size = Vector2i(cell_size, cell_size) astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE astar_grid.update() func generate_terrain(): terrain_map.resize(grid_width) for x in range(grid_width): terrain_map[x] = [] terrain_map[x].resize(grid_height) for y in range(grid_height): # 随机生成地形,实际项目中应从地图数据读取 var terrain = randi() % 4 terrain_map[x][y] = terrain if terrain == TERRAIN.WATER: astar_grid.set_point_solid(Vector2i(x,y), true) func update_movement_range(unit_pos: Vector2i, move_points: int): clear_highlights() var result = bfs_movement_range(unit_pos, move_points) movement_range = result.reachable for cell in movement_range: add_highlight(cell) func bfs_movement_range(start: Vector2i, max_cost: int) -> Dictionary: var frontier = [start] var cost_so_far = {start: 0} while not frontier.is_empty(): var current = frontier.pop_front() for neighbor in get_neighbors(current): var new_cost = cost_so_far[current] + get_move_cost(current, neighbor) if new_cost > max_cost: continue if neighbor not in cost_so_far: cost_so_far[neighbor] = new_cost frontier.append(neighbor) return {"reachable": cost_so_far.keys()} func get_neighbors(pos: Vector2i) -> Array: var neighbors = [] # 正交邻居 for dir in [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT]: var neighbor = pos + dir if astar_grid.is_in_bounds(neighbor): neighbors.append(neighbor) # 对角线邻居 if astar_grid.diagonal_mode != AStarGrid2D.DIAGONAL_MODE_NEVER: for dx in [-1, 1]: for dy in [-1, 1]: var neighbor = pos + Vector2i(dx, dy) if astar_grid.is_in_bounds(neighbor): # 根据对角线模式进一步筛选 if check_diagonal_condition(pos, neighbor): neighbors.append(neighbor) return neighbors func check_diagonal_condition(from: Vector2i, to: Vector2i) -> bool: match astar_grid.diagonal_mode: AStarGrid2D.DIAGONAL_MODE_ALWAYS: return true AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE: return !astar_grid.is_point_solid(from + Vector2i(to.x - from.x, 0)) || !astar_grid.is_point_solid(from + Vector2i(0, to.y - from.y)) AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES: return !astar_grid.is_point_solid(from + Vector2i(to.x - from.x, 0)) && !astar_grid.is_point_solid(from + Vector2i(0, to.y - from.y)) _: return false func add_highlight(cell: Vector2i): var sprite = Sprite2D.new() sprite.texture = preload("res://highlight.png") sprite.position = Vector2(cell) * cell_size + Vector2(cell_size/2, cell_size/2) sprite.modulate = Color(0,1,0,0.5) add_child(sprite) highlight_sprites.append(sprite) func clear_highlights(): for sprite in highlight_sprites: sprite.queue_free() highlight_sprites.clear()