Skip to content
返回

godot中的2D装修系统设计与重构

发布于:  at  01:00

一、实现的效果需求

20250722152501.png 希望实现的效果有:

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

20250722160557.png

放置物品

20250722160654.png

物品之间冲突

20250722161222.png

地砖邻接编辑效果

二、重构的原因

这次重构已经是第三次重构了,第一次重构要解决的是地砖拖动的问题,以及物品放置的大小问题,第二次重构要解决的是编辑模式下的回滚 来到第三次重构,是因为遇到了没办法解决的问题:物品上的物品 单一代码文件逻辑太重,不好维护 之前的设计只有一个可编辑网格,没有考虑物品还能放在物品上的网格 物品占位框跟物品直接对齐,因为占位框需要对齐网格,导致相对位置不好计算 godot中,有一些元素是坐标按照中心对齐,有一些按照左上角对齐

三、重构前的设计

装修系统设计.svg 整体上采用MVC的架构,里面用了命令模式用来保证编辑的回滚,用多层状态机表示不同的装修状态,主要逻辑放在了BaseDecorateInterior上

四、重构的设计

装修系统架构.svg

整体上偏向了ECS架构,本质上是ECS的变式,以component作为数据节点,但是component+capability其实一定程度上尊重传统的OOP设计,主要逻辑都在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

关键特性

装修系统中的实际应用

装修系统完美体现了这种架构的优势:

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

架构优势总结

  1. 高度解耦: 能力之间无直接依赖,只通过组件通信
  2. 易于扩展: 新增能力无需修改现有代码
  3. 灵活组合: 可以任意组合不同能力实现复杂功能
  4. 便于测试: 每个能力可以独立测试
  5. 性能优化: 可以按需激活/停用能力
  6. 代码复用: 能力可以在不同组件间复用

这种架构使得装修系统具有极强的可维护性和扩展性,为复杂的交互逻辑提供了清晰的组织结构。

核心组件层次

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

关键特性:

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。



Previous Post
Godot中的交互系统设计
Next Post
单词突然连上脑袋(微信小游戏)