背景
在游戏开发中,常常需要处理复杂的数据交互和事件响应,尤其是在组件化的环境中。为了实现这种需求,我们需要一个高效的事件调度系统来处理数据变化和节点状态。Cocos作为一个流行的游戏引擎,提供了丰富的组件系统和事件机制,但开发者有时需要自定义更灵活的解决方案。
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
- 适合于需要在注册监听的同时获取数据当前值的场景,比如在初始化时需要先展示当前数据,然后再处理后续的变化
- 相比
示例
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
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
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
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
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)
}