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

51 阅读6分钟

背景

在游戏开发中,常常需要处理复杂的数据交互和事件响应,尤其是在组件化的环境中。为了实现这种需求,我们需要一个高效的事件调度系统来处理数据变化和节点状态。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)
}