ECS渲染引擎架构文档
写在前面
之前写过一篇ECS文章,为什么还要再写一个,本质上因为之前的文档,截止到目前来说,变化巨大,底层已经改了很多很多,所以有必要把一些内容拎出来单独去说。
由于字体文件较大,加载时间会比较久😞
另外如果有性能问题,我会及时修复,引擎改造时间太仓促,只要不是内存泄漏,暂时没去处理。
还有很多东西要做。
体验地址:baiyuze.github.io/design/#/ca…
项目概览
Duck-Core 是一个基于 ECS(Entity-Component-System)架构构建的高性能 Canvas 渲染引擎,专为复杂图形编辑场景设计。引擎的核心特色在于双渲染后端架构、插件化系统设计和极致的渲染性能优化。
核心技术栈
- CanvasKit-WASM - Google Skia 图形库的 WebAssembly 移植版
- Canvas2D API - 浏览器原生渲染接口
架构核心亮点
ECS 架构模式 - 数据驱动的实体组件系统,实现逻辑与数据完全解耦
双引擎架构 - Canvas2D 与 CanvasKit 双渲染后端,运行时无缝切换
插件化设计 - 开放式扩展点,支持自定义渲染器、系统和组件
极致性能 - 颜色编码拾取、离屏渲染、渲染节流等多重优化
整体架构设计
整个引擎采用分层架构,从底层的渲染抽象到顶层的用户交互,每一层职责清晰且可独立替换。
1graph TB 2 subgraph "应用层" 3 A[React 组件] --> B[Canvas 画布组件] 4 end 5 6 subgraph "引擎核心层" 7 B --> C[Engine 引擎实例] 8 C --> D[Core 状态管理器] 9 C --> E[Camera 相机控制] 10 C --> F[Entity Manager 实体管理] 11 end 12 13 subgraph "系统层 - System" 14 C --> G[EventSystem 事件系统] 15 G --> H[InputSystem 输入系统] 16 G --> I[RenderSystem 渲染系统] 17 G --> J[PickingSystem 拾取系统] 18 G --> K[DragSystem 拖拽系统] 19 G --> L[SelectionSystem 选择系统] 20 G --> M[ZoomSystem 缩放系统] 21 G --> M[FpsSystem FPS] 22 end 23 24 subgraph "渲染层 - Renderer" 25 I --> N[RendererManager 渲染管理器] 26 N --> O{选择渲染后端} 27 O -->|Canvas2D| P[Canvas2D 渲染器组] 28 O -->|CanvasKit| Q[CanvasKit 渲染器组] 29 P --> R[RectRender] 30 P --> S[EllipseRender] 31 P --> T[TextRender] 32 Q --> U[RectRender] 33 Q --> V[EllipseRender] 34 Q --> W[TextRender] 35 end 36 37 subgraph "数据层 - Component" 38 X[StateStore 状态仓库] 39 X --> Y[Position] 40 X --> Z[Size] 41 X --> AA[Color] 42 X --> AB[Rotation] 43 X --> AC[Selected] 44 end 45 46 D <--> X 47 I --> X 48 J --> X 49 K --> X 50 L --> X 51 52 style C fill:#4A90E2,color:#fff 53 style N fill:#E94B3C,color:#fff 54 style X fill:#6ECB63,color:#fff 55 style G fill:#F39C12,color:#fff 56
ECS 架构深度解析
什么是 ECS 架构?
ECS(Entity-Component-System)是一种源自游戏引擎的设计模式,它彻底改变了传统面向对象的继承体系,转而采用组合优于继承的理念。
三大核心概念:
- Entity(实体) - 仅是一个唯一 ID,不包含任何数据和逻辑
- Component(组件) - 纯数据结构,描述实体的属性(如位置、颜色、大小)
- System(系统) - 纯逻辑处理单元,操作特定组件组合的实体
1graph TB 2 subgraph "传统 OOP 继承方式" 3 A1[GameObject] 4 A1 --> A2[Rectangle] 5 A1 --> A3[Circle] 6 A1 --> A4[Text] 7 A2 --> A5[DraggableRectangle] 8 A3 --> A6[SelectableCircle] 9 style A1 fill:#ff9999 10 end 11 12 subgraph "ECS 组合方式" 13 B1[Entity 123] -.拥有.-> B2[Position] 14 B1 -.拥有.-> B3[Size] 15 B1 -.拥有.-> B4[Color] 16 17 B5[Entity 456] -.拥有.-> B6[Position] 18 B5 -.拥有.-> B7[Font] 19 B5 -.拥有.-> B8[Selected] 20 21 B9[RenderSystem] --> B2 & B3 & B4 22 B10[DragSystem] --> B2 23 B11[SelectionSystem] --> B8 24 25 style B1 fill:#99ccff 26 style B5 fill:#99ccff 27 style B9 fill:#99ff99 28 style B10 fill:#99ff99 29 style B11 fill:#99ff99 30 end 31
ECS 架构的核心优势
1. 极致的解耦性
传统 OOP 中,功能通过继承链紧密耦合。而 ECS 中,系统只依赖组件接口,实体的行为完全由组件组合决定。
1// ❌ 传统方式:紧耦合的继承链 2class Shape { 3 render() { /* ... */ } 4} 5class DraggableShape extends Shape { 6 drag() { /* ... */ } 7} 8class SelectableDraggableShape extends DraggableShape { 9 select() { /* ... */ } 10} 11 12// ✅ ECS 方式:组件自由组合 13const rect = createEntity() 14addComponent(rect, Position, { x: 100, y: 100 }) 15addComponent(rect, Size, { width: 200, height: 150 }) 16addComponent(rect, Draggable, {}) // 可拖拽 17addComponent(rect, Selected, {}) // 可选中 18
2. 强大的可扩展性
新增功能无需修改现有代码,只需添加新的组件和系统:
3. 天然的并行处理能力
系统之间无共享状态,可以安全地并行执行:
1// 多个系统可以同时读取同一个组件 2async function updateFrame() { 3 await Promise.all([ 4 physicsSystem.update(), // 读取 Position 5 renderSystem.update(), // 读取 Position 6 collisionSystem.update(), // 读取 Position 7 ]) 8} 9
System 系统架构
系统负责处理逻辑,通过查询 StateStore 获取需要的组件数据:
1abstract class System { 2 abstract update(stateStore: StateStore): void 3} 4 5class RenderSystem extends System { 6 update(stateStore: StateStore) { 7 // 查询所有拥有 Position 组件的实体 8 for (const [entityId, position] of stateStore.position) { 9 const size = stateStore.size.get(entityId) 10 const color = stateStore.color.get(entityId) 11 const type = stateStore.type.get(entityId) 12 13 // 根据类型调用对应的渲染器 14 this.renderMap.get(type)?.draw(entityId) 15 } 16 } 17} 18
系统完整列表:
1graph TB 2 A[EventSystem<br/>事件总线] --> B[InputSystem<br/>输入捕获] 3 A --> C[HoverSystem<br/>悬停检测] 4 A --> D[ClickSystem<br/>点击处理] 5 A --> E[DragSystem<br/>拖拽逻辑] 6 A --> F[SelectionSystem<br/>选择管理] 7 A --> G[ZoomSystem<br/>缩放控制] 8 A --> H[ScrollSystem<br/>滚动平移] 9 A --> I[PickingSystem<br/>图形拾取] 10 A --> J[RenderSystem<br/>渲染绘制] 11 A --> K[FpsSystem<br/>性能监控] 12 13 style A fill:#F39C12,color:#fff 14 style J fill:#E74C3C,color:#fff 15 style I fill:#3498DB,color:#fff 16
双引擎架构设计
架构设计理念
不同的应用场景对渲染引擎有不同的需求:
- 简单场景:需要快速启动、体积小、兼容性好
- 复杂场景:需要高性能、丰富特效、大量图形
传统方案通常只支持单一渲染后端,难以兼顾两者。本引擎采用双引擎可切换架构,在运行时动态选择最优渲染后端。
1graph TB 2 A[应用启动] --> B{检测场景复杂度} 3 B -->|简单场景<br/>< 100 图形| C[Canvas2D 引擎] 4 B -->|复杂场景<br/>> 100 图形| D[CanvasKit 引擎] 5 B -->|用户手动指定| E[用户选择] 6 7 C --> F[浏览器原生 API] 8 D --> G[Skia WASM 引擎] 9 10 C --> H[渲染输出] 11 D --> H 12 13 I[运行时切换] -.->|热切换| C 14 I -.->|热切换| D 15 16 style C fill:#90EE90 17 style D fill:#87CEEB 18 style H fill:#FFD700 19
渲染后端对比
| 特性 | Canvas2D | CanvasKit (Skia) |
|---|---|---|
| 启动速度 | ⚡️ 即时(0ms) | 🐢 需加载 WASM(~2s) |
| 包体积 | ✅ 0 KB | ⚠️ ~1.5 MB |
| 浏览器兼容性 | ✅ 100% | ⚠️ 需支持 WASM |
| 渲染性能 | 🟡 中等 | 🟢 优秀 |
| 复杂路径渲染 | 🟡 一般 | 🟢 优秀 |
| 文字渲染 | 🟡 质量一般 | 🟢 亚像素级 |
| 滤镜特效 | ❌ 有限 | ✅ 丰富 |
| 离屏渲染 | ✅ 支持 | ✅ 支持 |
| 最佳场景 | 简单图形、快速原型 | 复杂设计、高性能需求 |
RendererManager 渲染管理器
RendererManager 是双引擎架构的核心枢纽,负责渲染器的注册、切换和调度:
1class RendererManager { 2 rendererName: 'Canvas2D' | 'Canvaskit' = 'Canvaskit' 3 4 // 渲染器映射表 5 renderer: { 6 rect: typeof RectRender 7 ellipse: typeof EllipseRender 8 text: typeof TextRender 9 img: typeof ImgRender 10 polygon: typeof PolygonRender 11 } 12 13 // 切换渲染后端 14 setRenderer(name: 'Canvas2D' | 'Canvaskit') { 15 this.rendererName = name 16 17 if (name === 'Canvas2D') { 18 this.renderer = Canvas2DRenderers 19 } else { 20 this.renderer = CanvaskitRenderers 21 } 22 } 23} 24
渲染器切换流程:
1sequenceDiagram 2 participant U as 用户操作 3 participant E as Engine 4 participant RM as RendererManager 5 participant RS as RenderSystem 6 participant R1 as Canvas2D Renderer 7 participant R2 as CanvasKit Renderer 8 9 U->>E: setRenderer('Canvas2D') 10 E->>RM: setRenderer('Canvas2D') 11 RM->>RM: 加载 Canvas2D 渲染器组 12 RM-->>E: 切换完成 13 14 E->>RS: 触发重新渲染 15 RS->>RM: 获取 rect 渲染器 16 RM-->>RS: 返回 Canvas2D.RectRender 17 RS->>R1: 调用 draw() 方法 18 R1->>R1: 使用 ctx.fillRect() 19 20 Note over U,R2: 用户再次切换引擎 21 22 U->>E: setRenderer('Canvaskit') 23 E->>RM: setRenderer('Canvaskit') 24 RM->>RM: 加载 CanvasKit 渲染器组 25 RM-->>E: 切换完成 26 27 E->>RS: 触发重新渲染 28 RS->>RM: 获取 rect 渲染器 29 RM-->>RS: 返回 CanvasKit.RectRender 30 RS->>R2: 调用 draw() 方法 31 R2->>R2: 使用 canvas.drawRect() 32
渲染器统一接口
所有渲染器实现相同的接口,保证可替换性:
1abstract class BaseRenderer extends System { 2 constructor(protected engine: Engine) { 3 super() 4 } 5 6 // 统一的渲染接口 7 abstract draw(entityId: string): void 8 9} 10
自定义渲染器扩展
引擎支持用户自定义渲染器,只需实现 System 接口:
1// 1. 创建自定义渲染器 2class CustomStarRender extends System { 3 draw(entityId: string) { 4 const points = this.getComponent<Polygon>(entityId, 'polygon') 5 const color = this.getComponent<Color>(entityId, 'color') 6 7 // 自定义绘制逻辑 8 const ctx = this.engine.ctx 9 ctx.beginPath() 10 points.points.forEach((p, i) => { 11 i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y) 12 }) 13 ctx.closePath() 14 ctx.fillStyle = color.fill 15 ctx.fill() 16 } 17} 18const customRenderMap = { 19 star: CustomStarRender 20} 21// 2. 注册到引擎 22new RendererRegistry().register({ 23 "custom": customRenderMap 24}) 25 26
字体渲染优化
CanvasKit 需要预加载字体文件,引擎实现了字体管理器:
1async function loadFonts(CanvasKit: any) { 2 const fontsBase = import.meta.env?.MODE === 'production' 3 ? '/design/fonts/' 4 : '/fonts/' 5 6 const [robotoFont, notoSansFont] = await Promise.all([ 7 fetch(`${fontsBase}Roboto-Regular.ttf`).then(r => r.arrayBuffer()), 8 fetch(`${fontsBase}NotoSansSC-VariableFont_wght_2.ttf`).then(r => r.arrayBuffer()), 9 ]) 10 11 const fontMgr = CanvasKit.FontMgr.FromData(robotoFont, notoSansFont) 12 return fontMgr 13} 14 15// 在 CanvasKit 初始化时调用 16export async function createCanvasKit() { 17 const CanvasKit = await initCanvasKit() 18 const FontMgr = await loadFonts(CanvasKit) 19 return { CanvasKit, FontMgr } 20} 21
引擎工厂模式
使用工厂函数创建不同配置的引擎实例:
1export function createCanvasRenderer(engine: Engine) { 2 // Canvas2D 引擎创建器 3 const createCanvas2D = (config: DefaultConfig) => { 4 const canvas = document.createElement('canvas') 5 const dpr = window.devicePixelRatio || 1 6 canvas.style.width = config.width + 'px' 7 canvas.style.height = config.height + 'px' 8 canvas.width = config.width * dpr 9 canvas.height = config.height * dpr 10 11 const ctx = canvas.getContext('2d', { 12 willReadFrequently: true, 13 }) as CanvasRenderingContext2D 14 ctx.scale(dpr, dpr) 15 16 config.container.appendChild(canvas) 17 18 return { canvasDom: canvas, canvas: ctx, ctx } 19 } 20 21 // CanvasKit 引擎创建器 22 const createCanvasKitSkia = async (config: DefaultConfig) => { 23 const { CanvasKit, FontMgr } = await createCanvasKit() 24 const canvasDom = document.createElement('canvas') 25 const dpr = window.devicePixelRatio || 1 26 27 canvasDom.style.width = config.width + 'px' 28 canvasDom.style.height = config.height + 'px' 29 canvasDom.width = config.width * dpr 30 canvasDom.height = config.height * dpr 31 canvasDom.id = 'canvasKitCanvas' 32 33 config.container.appendChild(canvasDom) 34 35 const surface = CanvasKit.MakeWebGLCanvasSurface('canvasKitCanvas') 36 const canvas = surface!.getCanvas() 37 38 return { 39 canvasDom, 40 surface, 41 canvas: canvas, 42 FontMgr: FontMgr, 43 ck: CanvasKit, 44 } 45 } 46 47 return { 48 createCanvas2D, 49 createCanvasKitSkia, 50 } 51} 52
Engine 引擎核心
Engine 类是整个渲染系统的中枢,协调所有子系统的运行:
1class Engine implements EngineContext { 2 camera: Camera = new Camera() 3 entityManager: Entity = new Entity() 4 SystemMap: Map<string, System> = new Map() 5 rendererManager: RendererManager = new RendererManager() 6 7 canvas!: Canvas // 渲染画布(类型取决于渲染后端) 8 ctx!: CanvasRenderingContext2D 9 ck!: CanvasKit 10 11 constructor(public core: Core, rendererName?: string) { 12 // 初始化渲染器 13 this.rendererManager.rendererName = rendererName || 'Canvaskit' 14 this.rendererManager.setRenderer(this.rendererManager.rendererName) 15 } 16 17 // 添加系统 18 addSystem(system: System) { 19 this.system.push(system) 20 this.SystemMap.set(system.constructor.name, system) 21 } 22 23 // 获取系统 24 getSystemByName<T extends System>(name: string): T | undefined { 25 return this.SystemMap.get(name) as T 26 } 27 28 // 清空画布(适配双引擎) 29 clear() { 30 const canvas = this.canvas as any 31 if (canvas?.clearRect) { 32 // Canvas2D 清空方式 33 canvas.clearRect(0, 0, this.defaultSize.width, this.defaultSize.height) 34 } else { 35 // CanvasKit 清空方式 36 this.canvas.clear(this.ck.WHITE) 37 } 38 } 39} 40
插件化系统设计
系统即插件
引擎的所有功能都以 System 形式实现,每个 System 都是独立的插件。这种设计带来极高的灵活性:
1graph TB 2 A[Engine 核心] --> B{System Manager} 3 4 B --> C[核心系统] 5 B --> D[可选系统] 6 B --> E[自定义系统] 7 8 C --> C1[EventSystem<br/>必需] 9 C --> C2[RenderSystem<br/>必需] 10 11 D --> D1[DragSystem<br/>拖拽功能] 12 D --> D2[ZoomSystem<br/>缩放功能] 13 D --> D3[FpsSystem<br/>性能监控] 14 15 E --> E1[UndoRedoSystem<br/>撤销重做] 16 E --> E2[SnappingSystem<br/>吸附对齐] 17 E --> E3[AnimationSystem<br/>动画播放] 18 19 style C1 fill:#e74c3c,color:#fff 20 style C2 fill:#e74c3c,color:#fff 21 style D1 fill:#3498db,color:#fff 22 style D2 fill:#3498db,color:#fff 23 style D3 fill:#3498db,color:#fff 24 style E1 fill:#2ecc71,color:#fff 25 style E2 fill:#2ecc71,color:#fff 26 style E3 fill:#2ecc71,color:#fff 27
核心系统详解
1. EventSystem - 事件总线
EventSystem 是整个引擎的调度中枢,协调所有其他系统的执行:
1class EventSystem extends System { 2 private eventQueue: Event[] = [] 3 4 update(stateStore: StateStore) { 5 // 执行系统更新顺序 6 this.executeSystem('InputSystem') // 1. 捕获输入 7 this.executeSystem('HoverSystem') // 2. 检测悬停 8 this.executeSystem('ClickSystem') // 3. 处理点击 9 this.executeSystem('DragSystem') // 4. 处理拖拽 10 this.executeSystem('ZoomSystem') // 5. 处理缩放 11 this.executeSystem('SelectionSystem') // 6. 更新选择 12 this.executeSystem('PickingSystem') // 7. 更新拾取缓存 13 this.executeSystem('RenderSystem') // 8. 最后渲染 14 } 15 16} 17
2. RenderSystem - 渲染系统
RenderSystem 负责将实体绘制到画布:
1class RenderSystem extends System { 2 private renderMap = new Map<string, BaseRenderer>() 3 4 constructor(engine: Engine) { 5 super() 6 this.engine = engine 7 this.initRenderMap() 8 } 9 10 // 初始化渲染器映射 11 initRenderMap() { 12 Object.entries(this.engine.rendererManager.renderer).forEach( 13 ([type, RendererClass]) => { 14 this.renderMap.set(type, new RendererClass(this.engine)) 15 } 16 ) 17 } 18 19 async update(stateStore: StateStore) { 20 // 清空画布 21 this.engine.clear() 22 23 // 应用相机变换 24 this.engine.canvas.save() 25 this.engine.canvas.translate( 26 this.engine.camera.translateX, 27 this.engine.camera.translateY 28 ) 29 this.engine.canvas.scale( 30 this.engine.camera.zoom, 31 this.engine.camera.zoom 32 ) 33 34 // 遍历所有实体进行渲染 35 for (const [entityId, pos] of stateStore.position) { 36 this.engine.canvas.save() 37 this.engine.canvas.translate(pos.x, pos.y) 38 39 const type = stateStore.type.get(entityId) 40 await this.renderMap.get(type)?.draw(entityId) 41 42 this.engine.canvas.restore() 43 } 44 45 this.engine.canvas.restore() 46 } 47} 48
DSL 配置系统
DSL 配置系统
设计目标
DSL(Domain Specific Language)模块的目标是将图形场景序列化为 JSON 格式,实现:
- 场景持久化 - 保存到数据库或本地存储
- 场景传输 - 前后端数据交换
- 场景快照 - 撤销/重做功能的基础
- 模板复用 - 创建可复用的图形模板
配置结构
1interface DSLParams { 2 type: 'rect' | 'ellipse' | 'text' | 'img' | 'polygon' 3 id?: string 4 position: { x: number; y: number } 5 size?: { width: number; height: number } 6 color?: { fill: string; stroke: string } 7 rotation?: { value: number } 8 scale?: { value: number } 9 zIndex?: { value: number } 10 selected?: { isSelected: boolean } 11 // 形状特定属性 12 font?: { family: string; size: number; weight: string } 13 radius?: { value: number } 14 polygon?: { points: Point[] } 15} 16
DSL 解析器
1class DSL { 2 constructor(params: DSLParams) { 3 this.type = params.type 4 this.id = params.id || this.generateId() 5 this.position = new Position(params.position) 6 this.size = params.size ? new Size(params.size) : new Size() 7 this.color = params.color ? new Color(params.color) : new Color() 8 // ... 初始化其他组件 9 } 10 11 // 转换为纯数据对象 12 toJSON(): DSLParams { 13 return { 14 type: this.type, 15 id: this.id, 16 position: { x: this.position.x, y: this.position.y }, 17 size: { width: this.size.width, height: this.size.height }, 18 color: { fill: this.color.fill, stroke: this.color.stroke }, 19 // ... 20 } 21 } 22} 23
低耦合架构实践
依赖方向
整个引擎严格遵循依赖倒置原则:
1graph TB 2 A[应用层<br/>React 组件] --> B[引擎接口<br/>Engine API] 3 B --> C[系统层<br/>System] 4 C --> D[组件层<br/>Component] 5 C --> E[实体层<br/>Entity] 6 7 F[渲染层<br/>Renderer] --> G[渲染接口<br/>BaseRenderer] 8 C --> G 9 10 style B fill:#f39c12,color:#fff 11 style G fill:#f39c12,color:#fff 12
关键设计:
- 上层依赖接口,不依赖具体实现
- System 不直接依赖 Renderer,通过 RendererManager 解耦
- Component 纯数据,零依赖
总结
Duck-Core 前端渲染引擎通过以下设计实现了高性能、高扩展性:
核心优势
- ECS 架构 - 数据与逻辑完全分离,组件自由组合
- 双引擎架构 - Canvas2D 与 CanvasKit 可热切换,兼顾兼容性与性能
- 插件化系统 - 所有功能以 System 形式实现,按需加载
- 低耦合设计 - 接口隔离、依赖倒置、事件驱动
- 极致性能 - 渲染节流、离屏缓存、视口裁剪、内存优化
《前端图形引擎架构设计:双引擎架构设计》 是转载文章,点击查看原文。
