一、实现的效果需求
希望实现的效果有:
- 进入装修模式的时候显示可布置的网格
- 在这个页面中预先制作哪些地方可以放置东西
- 放置的东西包括地砖,物品,墙纸,物品上的物品(例如桌子上的收银机)
- 放置的时候可以撤销,重做
- 只有在保存的时候进行持久化,没保存但编辑了,退出模式的时候询问是否保存
- 装修的时候最好可以触发暂停
- 放弃修改会回到修改前的样子
- 物品栏的东西点击放置进入装修模式,放置完成后马上退出装修
- 物品在移动或装修时,进入虚化+变色
- 物品需要描述占地多少,然后在放置的时候不可以跟已有的冲突,可放置时颜色为偏绿色,冲突时颜色偏红色
- 地砖编辑时,需要满足邻接动态变化
- 物品编辑时,有可能有不同的层级,例如说指向桌子时,桌子存在另一层的可编辑网络
- 所有编辑都在进入场景时重新加载



二、重构的原因
这次重构已经是第三次重构了,第一次重构要解决的是地砖拖动的问题,以及物品放置的大小问题,第二次重构要解决的是编辑模式下的回滚 来到第三次重构,是因为遇到了没办法解决的问题:物品上的物品 单一代码文件逻辑太重,不好维护 之前的设计只有一个可编辑网格,没有考虑物品还能放在物品上的网格 物品占位框跟物品直接对齐,因为占位框需要对齐网格,导致相对位置不好计算 godot中,有一些元素是坐标按照中心对齐,有一些按照左上角对齐
三、重构前的设计
整体上采用MVC的架构,里面用了命令模式用来保证编辑的回滚,用多层状态机表示不同的装修状态,主要逻辑放在了BaseDecorateInterior上
四、重构的设计
整体上偏向了ECS架构,本质上是ECS的变式,以component作为数据节点,但是component+capability其实一定程度上尊重传统的OOP设计,主要逻辑都在component+capability上体现。
五、核心代码实现(仅摘取部分)
核心架构
设计模式
- 组件-能力架构: 组件负责数据管理,能力负责行为逻辑
- 状态机模式: 通过模式枚举管理不同装修状态
- 命令模式: 快照管理器实现撤销重做功能
- 观察者模式: 基于信号系统的事件通信
Component+Capability架构详解
架构核心理念
Component+Capability架构是一种基于组合优于继承的设计模式,将系统功能分解为两个层次:
- Component(组件): 负责数据存储和状态管理,是系统的数据中心
- Capability(能力): 负责具体的行为逻辑,独立运行且可组合
双影奇境射箭系统案例
在双影奇境的射箭系统中,这种架构展现了其强大的解耦能力:
# BowComponent - 弓箭组件(数据中心)
class_name BowComponent
extends Component
# 弓箭状态数据
var charge_power: float = 0.0 # 蓄力值
var aim_direction: Vector2 # 瞄准方向
var is_ready_to_shoot: bool = false # 是否准备发射
var arrow_speed: float = 500.0 # 箭矢速度
三个独立的能力系统:
# 蓄力能力 - 独立管理蓄力逻辑
class_name ChargeCapability
extends Capability
func _process(delta):
if Input.is_action_pressed("charge_bow"):
bowComponent.charge_power += delta * charge_rate
bowComponent.charge_power = min(bowComponent.charge_power, max_charge)
# 瞄准能力 - 独立管理瞄准逻辑
class_name AimCapability
extends Capability
func _process(delta):
var mouse_pos = get_global_mouse_position()
var bow_pos = entity.global_position
bowComponent.aim_direction = (mouse_pos - bow_pos).normalized()
# 发射能力 - 独立管理发射逻辑
class_name ShootCapability
extends Capability
func _process(delta):
if Input.is_action_just_released("charge_bow") and bowComponent.charge_power > min_charge:
shoot_arrow()
bowComponent.charge_power = 0.0
关键特性:
- 三个能力互不引用,完全解耦
- 都通过BowComponent进行数据交互
- 可以独立开启/关闭任意能力
- 易于测试和维护
装修系统中的实际应用
装修系统完美体现了这种架构的优势:
1. 地砖装修的Component+Capability组合
# TileDecorateComponent - 地砖数据组件
class_name TileDecorateComponent
# 核心数据
var tiles: Dictionary[Vector2i, TileCustomData.TileName] # 地砖数据
var grid_pos: Vector2i # 当前网格位置
var tile: TileCustomData # 当前选中地砖
配合多个独立能力:
# 地砖跟随鼠标能力
class_name TileFollowMouseCapability
func _process(delta):
# 读取组件数据
var mouse_pos = get_global_mouse_position()
# 写入组件数据
tileDecorateComponent.grid_pos = world_to_grid(mouse_pos)
# 地砖渲染能力
class_name TileRenderCapability
func _process(delta):
# 读取组件数据
var current_tiles = tileDecorateComponent.tiles
# 执行渲染逻辑
update_tile_display(current_tiles)
# 地砖点击放置能力
class_name TileClickCapability
func _input(event):
if event.is_action_pressed("place_tile"):
# 读取和写入组件数据
var pos = tileDecorateComponent.grid_pos
var tile_type = tileDecorateComponent.tile.name
tileDecorateComponent.tiles[pos] = tile_type
2. 对象装修的Component+Capability组合
# ObjectDecorateComponent - 对象数据组件
class_name ObjectDecorateComponent
# 核心数据
var objectData: EntityComponent # 当前对象
var canPlace: bool = true # 是否可放置
var tempOccupyCells: Array[Vector2i] # 临时占用格子
var clickAction: ObjectDecorateClickAction # 点击行为状态
配合独立能力系统:
# 对象跟随鼠标能力
class_name ObjectFollowMouseCapability
func _process(delta):
if objectDecorateComponent.objectData:
# 读取数据
var mouse_pos = get_global_mouse_position()
# 写入位置数据
objectDecorateComponent.objectData.global_position = mouse_pos
# 对象碰撞检测能力
class_name ObjectCollisionCheckCapability
func _process(delta):
# 读取对象数据
var obj = objectDecorateComponent.objectData
if obj:
# 检测碰撞并写入结果
objectDecorateComponent.canPlace = check_collision(obj)
objectDecorateComponent.tempOccupyCells = get_occupy_cells(obj)
# 对象点击交互能力
class_name ObjectClickMouseCapability
func _input(event):
if event.is_action_pressed("mouse_left"):
# 读取状态数据
if objectDecorateComponent.canPlace:
# 写入行为状态
objectDecorateComponent.clickAction = ObjectDecorateClickAction.PLACE
架构优势总结
- 高度解耦: 能力之间无直接依赖,只通过组件通信
- 易于扩展: 新增能力无需修改现有代码
- 灵活组合: 可以任意组合不同能力实现复杂功能
- 便于测试: 每个能力可以独立测试
- 性能优化: 可以按需激活/停用能力
- 代码复用: 能力可以在不同组件间复用
这种架构使得装修系统具有极强的可维护性和扩展性,为复杂的交互逻辑提供了清晰的组织结构。
核心组件层次
DecorateComponent (主控制器)
├── TileDecorateComponent (地砖装修)
├── ObjectDecorateComponent (对象装修)
└── SnapshotManager (快照管理)
核心组件实现
1. DecorateComponent - 装修主控制器
职责: 装修系统的核心控制器,负责模式切换、状态管理和快照控制。
extends Component
class_name DecorateComponent
# 装修模式状态
var isDecorating : bool = false
# 互斥控制
var mode :ModeEnum = ModeEnum.EMPTY
enum ModeEnum{
EMPTY,
TILE,
OBJECT,
WALLPAPPER,
}
## 命令管理器
var snapshotManager:SnapshotManager = SnapshotManager.new()
## 进入装修模式
func enterDecorating(param :ModeEnum):
mode = param
if isDecorating==false:
isDecorating = true
## 如果已经在装修模式,不要再初始化了
snapshotManager.init(toResource())
## 退出装修模式
func exitDecorating():
# 如果快照存在未保存
# 提示保存
# todo...
realExit()
func realExit():
isDecorating = false
mode = ModeEnum.EMPTY
关键特性:
- 模式互斥控制,确保同时只有一种装修模式激活
- 集成快照管理,支持撤销重做功能
- 信号驱动的事件处理机制
2. TileDecorateComponent - 地砖装修组件
职责: 管理地砖的铺设、显示和数据存储。
extends Component
class_name TileDecorateComponent
# <网格图层> 双向绑定的,展示可铺设地砖的layer
@export var gridLayer : TileMapLayer
# <地砖图层> 双向绑定的,展示地砖的layer
@export var tileShowLayer : TileMapLayer
## 瓦片数据变化信号
signal tile_data_changed()
## 本次正在使用的砖块种类
var tile:TileCustomData
## 砖块数据,Vector2i=砖块坐标,TileType=砖块type
var tiles:Dictionary[Vector2i,TileCustomData.TileName]
## 砖块数据,倒置,快速使用
var tilesTypeMap:Dictionary[TileCustomData.TileName,Array]
## 鼠标对应网格的位置,用于判断可行性
var grid_pos:Vector2i=Vector2i.MIN
## 本次正在使用瓦片的坐标,鼠标对应地砖图层
var tileLayer_pos:Vector2i=Vector2i.MIN
关键特性:
- 双图层设计:网格图层显示可铺设区域,地砖图层显示实际地砖
- 字典数据结构优化查询性能
- 信号机制通知数据变化
3. ObjectDecorateComponent - 对象装修组件
职责: 管理装修对象的放置、移动和网格占用。
extends Component
class_name ObjectDecorateComponent
## 持久化部分========================================
## <对象图层> 所有对象
var objectSet : Dictionary[EntityComponent,int] ={}
## 非持久化部分========================================
## <网格图层> 来自场景默认的图层
@export var defaultGridLayerComponent : GridLayerComponent
## <网格图层> 正在使用的网格图层
var gridLayerComponent : GridLayerComponent
## 本次正在使用的对象
var objectData:EntityComponent
## 是否完全可以放置
var canPlace:bool = true
## 可以放置的单元
var canPlace_cells : Array[Vector2i] = []
## 当前【对象】已经占用的单元格
var tempOccupyCells : Array[Vector2i] = []
## 当前鼠标点击的行为,用于放下和拿起移动的互斥核
var clickAction:ObjectDecorateClickAction=ObjectDecorateClickAction.NONE
enum ObjectDecorateClickAction{
# 没有行动
NONE,
# 移动
MOVE,
# 放下
PLACE,
}
关键特性:
- 分离持久化和非持久化数据
- 网格占用检测和冲突处理
- 点击行为状态机管理
4. GridLayerComponent - 网格图层组件
职责: 管理网格显示和占用状态。
extends Component
class_name GridLayerComponent
## <网格图层> 网格图层
@export var gridLayer : TileMapLayer
## <网格图层> 当前图层【对象】已经占用的单元格
var occupyCellMap : Dictionary[String,Array]={}
## 显示网格图层
func show():
if gridLayer:
gridLayer.visible = true
## 隐藏网格图层
func hide():
if gridLayer:
gridLayer.visible = false
func getOccupyList()->Array:
var occupyCells = []
var mapValues = occupyCellMap.values()
if not mapValues.is_empty():
for poslist in mapValues:
occupyCells.append_array(poslist)
return occupyCells
关键特性:
- 网格可视化控制
- 占用单元格映射管理
- 序列化支持
能力系统实现
1. TileRenderCapability - 地砖渲染能力
职责: 高性能地砖渲染,支持多种优化策略。
extends Capability
class_name TileRenderCapability
## 性能优化策略枚举
enum OptimizationStrategy {
NONE, # 无优化,每次全量更新
BASIC, # 基础优化,只比较数组变化
ADVANCED, # 高级优化,排除已设置的瓦片
ULTRA # 超级优化,使用空间索引和批量操作
}
## 当前使用的优化策略
@export var optimization_strategy: OptimizationStrategy = OptimizationStrategy.ULTRA
## 性能统计数据
var performance_stats: Dictionary = {
"total_updates": 0, # 总更新次数
"cache_hits": 0, # 缓存命中次数
"last_update_time": 0, # 最后一次更新耗时
"spatial_cache_hits": 0, # 空间缓存命中次数
"batch_operations": 0 # 批量操作次数
}
## 判断是否应该激活
func shouldActive() -> bool:
return (
decorateComponent.isDecorating
)
关键特性:
- 四级性能优化策略
- 实时性能统计
- 空间索引和批量操作优化
2. ObjectFollowMouseCapability - 对象跟随鼠标能力
职责: 让装修对象跟随鼠标移动,并进行碰撞检测。
extends Capability
class_name ObjectFollowMouseCapability
# 网格
var gridCells:Array[Vector2i] = []
# 占用大小检查相关
var occupy_cells_x: int = 0
var occupy_cells_y: int = 0
var occupy_check_timer: float = 0.0
var occupy_check_interval: float = 0.5 # 每0.5秒检查一次
# 装修系统定义的网格大小
var grid_width = DecorateSetting.gridSizeWidth
var grid_height = DecorateSetting.gridSizeHeight
# 是否应该激活
func shouldActive()->bool:
# 当装修模式开启且有对象数据时激活
return (decorateComponent and decorateComponent.isDecorating and
objectDecorateComponent and objectDecorateComponent.objectData != null
and decorateComponent.mode == DecorateComponent.ModeEnum.OBJECT
)
关键特性:
- 实时鼠标跟随
- 网格对齐
- 占用检测优化
服务层实现
1. SnapshotManager - 快照管理器
职责: 实现撤销重做功能的快照管理。
extends Resource
class_name SnapshotManager
# 信号================================================================
## 数据有变化
signal data_change
# 基础属性=============================================================
## 快照历史记录最大数量
const MAX_HISTORY: int = 100
## 快照历史记录 - 存储Dictionary的深拷贝
var _snapshots: Array[Dictionary] = []
## 当前快照索引
var _currentIndex: int = -1
## 初始快照(用于取消操作)
var _initialSnapshot: Dictionary = {}
## 初始化管理器
func init(initialData: Dictionary) -> void:
clear()
if initialData:
_initialSnapshot = _deep_copy_dictionary(initialData)
_snapshots.push_back(_initialSnapshot)
_currentIndex = 0
data_change.emit()
## 保存当前状态快照
func save_snapshot(data: Dictionary) -> void:
if not data:
return
# 移除当前索引之后的所有快照
while _snapshots.size() > _currentIndex + 1:
_snapshots.pop_back()
# 保存新快照
var snapshot = _deep_copy_dictionary(data)
_snapshots.push_back(snapshot)
_currentIndex += 1
关键特性:
- 最大100个历史快照
- 深拷贝确保数据独立性
- 支持撤销、重做、取消操作
- 信号通知数据变化
2. TileCustomData - 瓦片数据服务
职责: 管理瓦片类型和数据。
extends Resource
class_name TileCustomData
enum TileType{
TILE,
SPRITE,
}
var type:TileType
var setId:int
var id:int
var name:TileName
## 砖块类型
enum TileName{
NORMAL,
TYPE1,
TYPE2,
}
static func getTileSetId(name:TileCustomData.TileName)->Vector2i:
match name:
TileName.NORMAL:
return Vector2i(0,1)
TileName.TYPE1:
return Vector2i(0,0)
TileName.TYPE2:
return Vector2i(0,1)
return Vector2i.MIN
static func newTileCustomData(name:TileCustomData.TileName)->TileCustomData:
var setId = getTileSetId(name)
match name:
TileName.NORMAL:
var tileCustomData:TileCustomData=TileCustomData.new()
tileCustomData.type=TileType.TILE
tileCustomData.setId=setId.x
tileCustomData.id=setId.y
tileCustomData.name = name
return tileCustomData
关键特性:
- 静态工厂方法创建瓦片数据
- 类型安全的枚举设计
- 瓦片集映射管理
配置与设置
DecorateSetting - 装修设置
extends Resource
class_name DecorateSetting
## 网格宽度
static var gridSizeWidth : int = 32
## 网格高度
static var gridSizeHeight : int = 32
关键特性:
- 全局静态配置
- 网格尺寸标准化
UI组件实现
OccupyShape - 占用形状组件
职责: 可视化显示对象的网格占用区域。
extends CollisionShape2D
class_name OccupyShape
# 装修系统定义的网格大小
var grid_width = DecorateSetting.gridSizeWidth
var grid_height = DecorateSetting.gridSizeHeight
var occupy_cells:Vector2i
var occupy_size:Vector2
# 网格容器节点
var cell_nodes: Array[Array] = []
const GRID_CELL_G = preload("res://core/装修系统/widget/网格/grid_cell_G.tscn")
const GRID_CELL_R = preload("res://core/装修系统/widget/网格/grid_cell_R.tscn")
func _ready() -> void:
# 计算大小
occupy_size = getSize()
# 先计算occupy是几x几,然后再检查是否能够整除,如果不能整除需要输出异常
occupy_cells.x = int(ceil(occupy_size.x / grid_width))
occupy_cells.y = int(ceil(occupy_size.y / grid_height))
# 进入装修状态
func enterDecorateMode():
addOneGridContainerAndCell()
# 退出装修状态
func exitDecorateMode():
print("OccupyShape.exitDecorateMode")
cell_nodes.clear()
_removeAllChildren()
关键特性:
- 自动计算网格占用
- 可视化网格显示
- 装修模式切换支持
六、遇到的问题和总结
在尝试双影奇境的component+capabilities的设计中,发现这是一个非常不错的设计,然后在结合到godot的过程中发现其实godot确实也能够比较好的适应这个以capability模式为核心的ECS架构变种,godot原先的node树其实就一定程度上契合了这样的设计,例如说capability在双影奇境的主创分享中是轮询的设计,其实就是每个节点的脚本文件直接使用_process,只是说如果要实现优先级或者加权之类的逻辑,则需要自主封装一个CapabilitySystem。