Cocos Creator 构建高效的数据驱动事件系统

背景

在游戏开发中,常常需要处理复杂的数据交互和事件响应,尤其是在组件化的环境中。为了实现这种需求,我们需要一个高效的事件调度系统来处理数据变化和节点状态。Cocos Creator 作为一个流行的游戏引擎,提供了丰富的组件系统和事件机制,但开发者有时需要自定义更灵活的解决方案。

StData 模块

StData 模块负责数据的存取、监听以及清理操作。它使用了一种基于路径的存储方式来管理数据,并通过事件调度器来处理数据变化的通知。

  • 获取数据get(path: string, dv?: any)
    • 根据路径从缓存中获取数据,如果路径不存在则返回默认值。
  • 设置数据set<T>(path: string, v: T, ignoreEvent?: boolean)
    • 根据路径设置数据,并可选择是否触发事件。
  • 监听数据变化: listen(path, node, func)
    • 监听数据的变化,并在数据改变时调用指定的回调函数。
  • 取消监听unlisten(path, node)
    • 取消对某个数据路径的监听。
  • 清除数据clear()
    • 清空所有缓存的数据

Datar 模块

Datar 命名空间封装了对 StData 模块的操作,提供了一些便捷的接口来简化数据管理工作。它的功能包括:

  • 获取数据get<T extends keyof DatarList>(key: T): DatarList[T]
  • 设置数据set<T extends keyof DatarList>(key: T, value: DatarList[T]): void
  • 监听数据listen<T extends keyof DatarList>(key: T, node: Node, call: (value: DatarList[T]) => void): void
  • 取消监听unlisten<T extends keyof DatarList>(key: T, node: Node): void
  • 监听数据listenget<T extends keyof DatarList>(key: T, dv: any, node: Node, call: (value: DatarList[T]) => void): void
    • 相比listen有以下特点
      • 除了设置监听器,还会立即获取当前数据的值,并将其作为参数传递给回调函数
      • 如果路径不存在,使用提供的默认值 dv
      • 适合于需要在注册监听的同时获取数据当前值的场景,比如在初始化时需要先展示当前数据,然后再处理后续的变化

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Datar } from "./Datar";
import { Node } from "cc";

// 设置数据
Datar.set("game/selectroom", "room2");

// 获取数据
const room = Datar.get("game/selectroom");

// 监听数据变化
Datar.listen("game/selectroom", someNode, (value) => {
console.log("Room changed to:", value);
});

// 监听数据变化
Datar.listenget("game/selectroom", 'default_value', this.node, (gameInfo) => {
console.log('gameInfo=>', gameInfo);
});

stEventDispatcher 模块

stEventDispatcher 类是事件调度系统的核心,负责管理事件的绑定、派发以及清理。它支持节点之间的事件传递,并在节点销毁时自动清理相关事件。

  • 添加事件监听listen(eventName: string, func: EventFunc)

    • 注册一个事件监听函数。
  • 移除事件监听removeEventFuncs(eventName: string)

    • 移除指定事件的所有监听函数。
  • 分发事件dispatch(eventName: string, ...datas)

    • 分发事件给所有注册的监听函数。
  • 管理子事件调度器
    addChild(disp: stEventDispatcher)removeChild(disp:stEventDispatcher)

    • 添加和移除子事件调度器,以支持复杂的事件结构。

DestroyOb 模块

DestroyOb 组件用于管理 Cocos Creator 节点的生命周期,确保在节点销毁时清理相关资源和事件监听器。

  • 获取组件实例getCom(node: Node): DestroyOb

    • 获取指定节点上的 DestroyOb 组件实例,如果不存在则添加一个新的。
  • 监听销毁事件listen(func: Function, node?: Node)

    • 注册一个回调函数,以在节点销毁时触发。

获取源码

Datar.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { StData } from "./StData";
import { Node } from "cc";


export namespace Datar {

export function get<T extends keyof DatarList>(key: T): DatarList[T] {
let value: any = StData.get(key);
return value;
}
export function set<T extends keyof DatarList>(key: T, value: DatarList[T]): void {
StData.set(key, value);
}
export function listen<T extends keyof DatarList>(key: T, node: Node, call: (value: DatarList[T]) => void): void {
StData.listen(key, node, call);
}
export function listenget<T extends keyof DatarList>(key: T, dv: any, node: Node, call: (value: DatarList[T]) => void): void {
StData.listenget(key, dv, node, call);
}
export function unlisten<T extends keyof DatarList>(key: T, node: Node): void {
StData.unlisten(key, node);
}
export function unlistenall(node: Node): void {
StData.unlistenall(node);
}

};

export type DatarList = {
"game/selectroom": string,
//...
}

StData.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { stEventDispatcher } from "./StEventDispatcher"

let cache: any = {}
let datadisp = new stEventDispatcher

function tostring(v) {
return v.toString()
}

function tonumber(v) {
return Number.parseInt(v)
}

function isnumber(v) {
return !Number.isNaN(tonumber(v))
}

export namespace StData {
export function get(path: string, dv?: any) {
if (path == null) { return null }
let paths = path.split("/")
let cur = cache

let prev
let len = paths.length
let name = null
for (let i = 0; i < len; i++) {
name = paths[i]
if (typeof (cur) != "object") {
return null
}
let isArr = false
if (isnumber(name)) {
name = tonumber(name)

isArr = true
}

prev = cur
cur = cur[name]
if (cur == null && i < len - 1 && len > 1) {
if (isArr) {
cur = []
} else {
cur = {}
}
prev[name] = cur
}
}
if (cur == null) {
prev[name] = dv
cur = dv
}
if (cur != null && typeof (cur) == "object") {
cur["__path__"] = path
}
return cur
}

export function set<T>(path: string, v: T, ignoreEvent?: boolean): T {
if (path == null) { return null }
let paths = path.split("/")
let cur = cache
let prev
for (let i = 0; i < paths.length - 1; i++) {
let name: any = paths[i]
if (typeof (cur) != "object") {
return null
}
if (isnumber(name)) {
name = tonumber(name)
}
prev = cur
cur = cur[name]
if (cur == null) {
cur = {}
prev[name] = cur
}
}
let last = paths[paths.length - 1]
if (typeof (cur) != "object") {
return null
}
cur[last] = v
if (!ignoreEvent) {
datadisp.dispatch(path, v)
}
}

export function change(input: string | any, dv?: any) {
let path = ""
if (typeof (input) == "string") {
path = input
} else {
if (typeof (input) != "object") {
return false
}
path = input["__path__"]
}
let value = get(path, dv)
datadisp.dispatch(path, value)
return true
}

export function clear() {
cache = {}
}

export function listen(path, node, func) {
datadisp.addNode(node, "__datadisp").listen(path, func)
}

export function unlisten(path, node) {
let disp = datadisp.getDisp(node, "__datadisp")
if (disp) {
disp.removeEventFuncs(path)
}
}
export function unlistenall(node) {
let disp = datadisp.getDisp(node, "__datadisp")
if (disp) {
disp.removeAll()
}
}
export function listenget(path, dv, node, func) {
if (func == null) {
func = node
node = dv
dv = null
}
datadisp.addNode(node, "__datadisp").listen(path, func)
let value = get(path, dv)
func(value)
}
export function listencallget(path, node, func, getfunc) {
datadisp.addNode(node, "__datadisp").listen(path, func)
let value = get(path, null)
if (value == null) {
getfunc()
} else {
func(value)
}
}

export function getDisp() {
return datadisp
}
}

StEventDispatcher.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { StDestroy } from "./DestroyOb"
import { _decorator, Component, Node } from "cc";
export type EventFunc = (...datas) => any

type funcInfo = {
func: EventFunc,
enabled: boolean,
}

type childInfo = {
child: stEventDispatcher,
enabled: boolean,
}

class EventComponent extends Component {
private disps_: stEventDispatcher[] = []
addDisp(disp: stEventDispatcher) {
if (this.disps_.find((v) => v == disp) == null) {
this.disps_.push(disp)
}
}
onDestroy() {
for (let disp of this.disps_) {
disp.removeFromParent()
}
}
}
let sID = 0
export class stEventDispatcher {
static bind(node: Node, name: string) {
let disp: stEventDispatcher = node[name]
if (disp == null) {
disp = new stEventDispatcher
disp.link(node)
node[name] = disp
}
return disp
}

constructor() {
// director.getScheduler().scheduleUpdate(this,-1,false)
}

private funcs_ = new Map<string, funcInfo[]>()
private childs_: childInfo[] = []
private parent_: stEventDispatcher
get parent() {
return this.parent_
}
private sid_ = sID++

private node_: Node
link(node: Node) {
let self = this
StDestroy(node, function () {
self.clear()
})
this.node_ = node
}

listen(eventName: string, func: EventFunc) {
let infos = this.funcs_.get(eventName)
if (infos == null) {
infos = []
this.funcs_.set(eventName, infos)
}
infos.push({
func: func,
enabled: true,
})
return this
}

removeEventFuncs(eventName: string) {
let infos = this.funcs_.get(eventName)
if (infos) {
for (let info of infos) {
info.enabled = false
}
this.refresh()
}
return this
}

removeFunc(func: EventFunc) {
let self = this
this.funcs_.forEach(function (infos, eventName) {
for (let info of infos) {
if (info.func == func) {
info.enabled = false
}
}
})
this.refresh()
return this
}

addNode(node: Node, dispName?: string) {
dispName = dispName || "_default_child_disp_"
let disp: stEventDispatcher = node[dispName]
if (disp) {
return disp
}

disp = new stEventDispatcher()
this.addChild(disp)
node[dispName] = disp

disp.link(node)
return disp
}

getDisp(node: Node, dispName?: string) {
dispName = dispName || "_default_child_disp_"
let disp: stEventDispatcher = node[dispName]
return disp
}

addChild(disp: stEventDispatcher) {
if (disp.parent_ != null) {
return null
}
let childInfo = this.childs_.find((v) => v.child == disp)
if (childInfo == null) {
childInfo = {
child: disp,
enabled: true
}
this.childs_.push(childInfo)
}
childInfo.enabled = true

disp.parent_ = this
return this
}

removeChild(disp: stEventDispatcher) {
let info = this.childs_.find((v) => v.child == disp)
if (info) {
info.enabled = false
info.child.parent_ = null
this.refresh()
}
return this
}

removeAllChildren() {
for (let child of this.childs_) {
child.enabled = false
}
this.refresh()
return this
}

removeFromParent() {
if (this.parent_) {
this.parent_.removeChild(this)
}
return this
}

removeAll() {
for (let child of this.childs_) {
child.enabled = false
}
this.funcs_.forEach(function (infos, eventName) {
for (let info of infos) {
info.enabled = false
}
})
this.refresh()
return this
}

clear() {
this.removeFromParent()
this.removeAll()
}

private isDispatching_ = false
dispatch(eventName: string, ...datas) {
this.isDispatching_ = true
let tempChilds = this.childs_.slice()
for (let info of tempChilds) {
if (info.enabled) {
//EventDispatcher.prototype.dispatch.apply(child.child,datas)
info.child.dispatch(eventName, ...datas)
}
}
let self = this
let infos = this.funcs_.get(eventName)
if (infos) {
let tempInfos = infos.slice()
for (let info of tempInfos) {
if (info.enabled) {
try {
//info.func.apply(null,datas)
info.func(...datas)
} catch (error) {
}
}
}
}
this.isDispatching_ = false
this.refresh()
return this
}

private isDirty_ = false
private refresh() {
if (this.isDispatching_) {
this.isDirty_ = true
return
}
if (this.isDirty_ == false) {
return
}
this.isDirty_ = false

let len = 0
len = this.childs_.length
let count = 0
for (let i = len - 1; i >= 0; i--) {
let child = this.childs_[i]
if (child.enabled == false) {
this.childs_.splice(i, 1)
count++
}
}

let self = this
this.funcs_.forEach(function (infos, eventName) {
let len = infos.length
for (let i = len - 1; i >= 0; i--) {
let info = infos[i]
if (info.enabled == false) {
infos.splice(i, 1)
}
}
})
}
}

DestroyOb.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { stEventDispatcher } from "./StEventDispatcher";
import { _decorator, Component, Node } from "cc";
const { ccclass, property } = _decorator;

let eventName = "_onDestroy_"
@ccclass('DestroyOb')
export class DestroyOb extends Component {
static getCom(node: Node): DestroyOb {
let com = node.getComponent(DestroyOb)
if (com == null) {
com = node.addComponent(DestroyOb)
}
return com
}

private disp_ = new stEventDispatcher
listen(func: Function, node?: Node) {
let disp: stEventDispatcher
if (node) {
disp = this.disp_.addNode(node, "__destroy_ob__")
} else {
disp = this.disp_
}
disp.listen(eventName, function (...params) {
func(...params)
})
}

protected onDestroy(): void {
this.disp_.dispatch(eventName)
this.disp_.clear()
}
}

export function StDestroy(node: Node, func: Function, targetNode?: Node) {
node.active = true
DestroyOb.getCom(node).listen(func, targetNode)
}