diff --git a/.obsidian/app.json b/.obsidian/app.json index 131a11e..a00803f 100644 --- a/.obsidian/app.json +++ b/.obsidian/app.json @@ -1,6 +1,5 @@ { "promptDelete": false, "attachmentFolderPath": "渲染/软渲染/图片", - "alwaysUpdateLinks": true, - "showLineNumber": false + "alwaysUpdateLinks": true } \ No newline at end of file diff --git a/1Project/学习/开发文章/将开发文章阅读并分类.md b/1Project/学习/开发文章/将开发文章阅读并分类.md index 5fa1f55..9a3d38f 100644 --- a/1Project/学习/开发文章/将开发文章阅读并分类.md +++ b/1Project/学习/开发文章/将开发文章阅读并分类.md @@ -1,8 +1,5 @@ -### GAS优先!! - [ ] [焦虑、内耗、爱破防,真的是你的锅吗?硬核心理学让你从此掌控自我!-我要等到什么时候-稍后再看-哔哩哔哩视频](https://www.bilibili.com/list/watchlater?oid=114079968532107&bvid=BV1YM9gYdECb&spm_id_from=333.1007.top_right_bar_window_view_later.content.click) - [ ] [动作游戏框架05.扩展Timeline:使用组合,而非继承,来扩展Timeline的基础功能-我要等到什么时候-稍后再看-哔哩哔哩视频](https://www.bilibili.com/list/watchlater?oid=114522652147889&bvid=BV1HKJAzyEZ5&spm_id_from=333.1007.top_right_bar_window_view_later.content.click) -- [ ] https://zhuanlan.zhihu.com/p/28352150798# Unity-SM节点式动画技能编辑器 -- [ ] [[Log插件]] -- [ ] [[UE-CORE-002 TickTaskManager]] \ No newline at end of file +- [ ] https://zhuanlan.zhihu.com/p/28352150798# Unity-SM节点式动画技能编辑器 \ No newline at end of file diff --git a/1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md b/1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md deleted file mode 100644 index e69de29..0000000 diff --git a/1Project/宠物宇宙/地图编辑器/需要支持的小功能.md b/1Project/宠物宇宙/地图编辑器/需要支持的小功能.md new file mode 100644 index 0000000..fd10f64 --- /dev/null +++ b/1Project/宠物宇宙/地图编辑器/需要支持的小功能.md @@ -0,0 +1,40 @@ + + +> [!important] 优先级 **1** + + + - [x] 添加RoomComponents + - [ ] IExtension + - [ ] 生成怪物测试 + - [ ] id不再自增长 + - [x] 碰撞盒和标志需要添加自定义功能 + + - [ ] 地图地板图片导入 自动修改图片大小 + - [ ] 添加白膜地图 + - 创建100*100的地图,宽高跟随Rect大小变化 + + - [ ] 将RoomConfig改到==关卡表==里 + RoomConfig的内容为SpecialRoom + - [ ] 碰撞盒需要添加旋转 + + + +> [!note] 优先级 **2** + + - [ ] 枚举改为英文名 + - [ ] 瓦片地图 + - [ ] 优化重构 + [[需要做的优化]] + + +> [!note] 优先级 **3** + + - [ ] 提取单个地块编辑 + +> [!note] 优先级 **4** + + - [ ] 过关门动画优化 + - [ ] 地图大背景优化移动方案 + + + \ No newline at end of file diff --git a/1Project/宠物宇宙/版本需求.md b/1Project/宠物宇宙/版本需求.md deleted file mode 100644 index 18f5af0..0000000 --- a/1Project/宠物宇宙/版本需求.md +++ /dev/null @@ -1,31 +0,0 @@ - -> # 此版本 - - -- [x] 皮肤导入 - - [x] 上海限定 C:\Users\1\Desktop\cache\57015 - - 游戏中 - - [x] C:\Users\1\Desktop\cache\红/蓝/绿皮肤7.0 - - [x] 胳膊导入 - - 结算 - - [x] C:\Users\1\Desktop\cache\角色胜利结算 - - 卡面 - - [x] C:\Users\1\Desktop\cache\第三弹裁切输出 - - UI名字 - - [x] C:\Users\1\Desktop\cache\皮肤 -- [x] 枪械宠物UI导入 -- [ ] 测试网络点数 开机检测 -- [x] 地图编辑器 [[需要支持的地图功能]] - - [x] Boss立绘 - - [x] C:\Users\1\Desktop\cache\雷鸟血条UI -- [ ] 积分系统 - - [ ] - ---- - - - -> ## 下个版本 - - - [ ] 网络状态3\4 - [[1Project/宠物宇宙/硬件交互/业务插件(RTwoGameBusiness)TCP接口设计说明]] diff --git a/1Project/宠物宇宙/皮肤导入.md b/1Project/宠物宇宙/皮肤导入.md new file mode 100644 index 0000000..6b7056b --- /dev/null +++ b/1Project/宠物宇宙/皮肤导入.md @@ -0,0 +1,13 @@ + +- [ ] 皮肤导入 + - [x] 上海限定 C:\Users\1\Desktop\cache\57015 + - 游戏中 + - [x] C:\Users\1\Desktop\cache\红/蓝/绿皮肤7.0 + - [ ] 胳膊导入 + - 结算 + - [x] C:\Users\1\Desktop\cache\角色胜利结算 + - 卡面 + - [x] C:\Users\1\Desktop\cache\第三弹裁切输出 + - UI名字 + - [ ] C:\Users\1\Desktop\cache\皮肤 + diff --git a/1Project/宠物宇宙/硬件交互/业务插件(RTwoGameBusiness)TCP接口设计说明.md b/1Project/宠物宇宙/硬件交互/业务插件(RTwoGameBusiness)TCP接口设计说明.md deleted file mode 100644 index 2db50d1..0000000 --- a/1Project/宠物宇宙/硬件交互/业务插件(RTwoGameBusiness)TCP接口设计说明.md +++ /dev/null @@ -1,1541 +0,0 @@ -# 业务插件(RTwoGameBusiness)TCP接口说明 - -## 1. 文档概述 - -### 1.1 版本 - -| 版本号 | 日期 | 作者 | 变更内容 | 状态 | -|-------|------|------|---------|------| -| 1.0 | 2025-05-29 | JJX | 初始创建 | 正式发布 | -| 1.1 | 2025-06-15 | JJX | 添加get-system-info接口说明和更新check-unis-net-state接口说明 | 正式发布 | - -### 1.2 目的 - -本文档详细描述RTwoGameBusiness业务插件提供的TCP接口规范,用于游戏客户端与业务插件之间的通信。该文档面向开发人员、测试人员和集成人员,提供接口的详细说明、参数格式、错误码以及使用示例。 - -### 1.3 适用范围 - -- 开发人员:用于实现游戏客户端与业务插件的通信 -- 测试人员:用于验证接口功能和性能 -- 集成人员:用于系统集成和故障排查 -- 维护人员:用于系统维护和问题定位 - -### 1.4 接口概述 - -RTwoGameBusiness插件通过TCP协议提供服务,主要包括以下功能类别: - -1. **用户卡功能**:用户信息查询、刷卡登录等 -2. **点数卡功能**:点数查询、充值、扣点等 -3. **游戏记录功能**:游戏积分、金币、宝石、卡牌、投币记录等 -4. **4G网络控制**:4G模块状态查询、启动和停止等 - -## 2. 基础信息 - -### 2.1 服务端口 - -RTwoGameBusiness插件默认监听TCP端口:**12103** - -该端口可通过配置文件修改,修改后需重启插件生效。 - -### 2.2 通信协议 - -- 通信协议:TCP -- 字符编码:UTF-8 -- 数据格式:JSON - -### 2.3 请求格式 - -所有请求都应遵循以下JSON格式: - -```json -{ - "command": "命令名称", - "data": { - // 根据不同命令类型包含不同的参数 - }, - "ts": 1621234567890 // 可选,请求时间戳(毫秒) -} -``` - -### 2.4 响应格式 - -所有响应都遵循以下JSON格式: - -```json -{ - "result": true, // 响应结果,true成功,false失败 - "errMsg": "", // 错误信息,成功时为空 - "statusCode": 0, // 状态码,0表示成功,非0表示各种错误 - "command": "命令名称", // 关联的指令标识 - "data": { // 返回数据,根据不同接口有不同结构 - // 根据不同命令返回不同的数据 - }, - "ts": 1621234567890 // 可选,响应时间戳(毫秒) -} -``` - -## 3. 接口详细说明 - -根据功能分类,本章节将详细介绍RTwoGameBusiness插件提供的所有TCP接口。 - -### 3.1 用户卡功能接口 - -#### 3.1.1 用户信息查询 (select-user-info) - -该接口用于查询用户卡所绑定的用户信息,并可选择性地返回排行榜数据。 - -**请求命令**:`select-user-info` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是(与cardSn二选一) | 卡ID | -| cardSn | string | 是(与cardId二选一) | 卡序列号 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 排行榜条数限制,默认为10 | -| topType | int | 否 | 排行榜类型:0=积分排行榜(默认),1=时长排行榜 | - -**请求示例**: - -```json -{ - "command": "select-user-info", - "data": { - "cardId": "1234567890", - "gameLevel": "1", - "topLimit": 10, - "topType": 0 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| cardId | string | 卡ID | -| cardSn | string | 卡序列号 | -| userName | string | 用户名称 | -| userAvatar | string | 用户头像URL | -| userSex | int | 用户性别:0-未知 1-男 2-女 | -| topType | int | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分(当topType=0时有效) | -| duration | int | 用户在此排行榜中的游戏时长(当topType=1时有效) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "select-user-info", - "data": { - "cardId": "1234567890", - "cardSn": "A1B2C3D4", - "userName": "玩家001", - "userAvatar": "http://example.com/avatar/001.jpg", - "userSex": 1, - "topType": 0, - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡ID和卡序列号都为空) | -| 40 | 用户不存在 | - -**备注**: -1. 卡ID和卡序列号必须至少提供一个,优先使用卡ID查询。 -2. 如果未指定排行榜类型,默认返回积分排行榜。 -3. 如果未指定游戏关卡,将返回所有关卡的排行榜数据。 -4. 排行榜数据按分数或时长降序排列。 - -#### 3.1.2 刷卡登录 (cwyz-swipe-card) - -该接口用于处理游戏中的刷卡操作,关联用户与游戏。 - -**请求命令**:`cwyz-swipe-card` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardSn | string | 是 | 卡序列号 | -| playerIdx | int | 是 | 玩家索引,用于多人游戏时区分不同玩家位置 | - -**请求示例**: - -```json -{ - "command": "cwyz-swipe-card", - "data": { - "cardSn": "A1B2C3D4", - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| cardId | string | 卡ID | -| cardSn | string | 卡序列号 | -| userName | string | 用户名称 | -| userAvatar | string | 用户头像URL | -| userSex | int | 用户性别:0-未知 1-男 2-女 | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-swipe-card", - "data": { - "cardId": "1234567890", - "cardSn": "A1B2C3D4", - "userName": "玩家001", - "userAvatar": "http://example.com/avatar/001.jpg", - "userSex": 1, - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡序列号为空) | -| 40 | 用户卡不存在 | -| 30 | 网络错误(与服务器通信失败) | - -**备注**: -1. 刷卡操作会尝试从服务器获取最新的用户信息,如果网络不可用,会使用本地存储的用户信息。 -2. 玩家索引用于区分多人游戏时的不同位置,通常从1开始,对应游戏机的不同位置。 -3. 如果卡片未绑定用户,会返回默认的用户信息。 - -### 3.2 点数卡功能接口 - -#### 3.2.1 查询剩余点数 (get-remaining-points) - -该接口用于查询当前游戏机剩余的点数,用于显示给用户或判断是否有足够点数执行出卡操作。 - -**请求命令**:`get-remaining-points` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "get-remaining-points", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 剩余点数 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "get-remaining-points", - "data": { - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 20 | 数据库错误(读取点数卡信息失败) | - -**备注**: -1. 此接口会返回所有已绑定点数卡的剩余点数总和。 -2. 如果没有任何点数卡,返回的剩余点数为0。 -3. 点数信息优先从本地数据库获取,确保即使在网络不可用时也能正常工作。 - -#### 3.2.2 绑定点卡 (bind-point-card) - -该接口用于绑定新的点数卡到游戏机上。 - -**请求命令**:`bind-point-card` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardCode | string | 是 | 点卡卡号 | - -**请求示例**: - -```json -{ - "command": "bind-point-card", - "data": { - "cardCode": "PC12345678" - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 绑定后的剩余点数 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "bind-point-card", - "data": { - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡号为空) | -| 20 | 数据库错误(绑定点数卡失败) | -| 60 | 卡片不存在(点卡号无效) | -| 61 | 点卡已使用 | - -**备注**: -1. 绑定点卡后,点数将自动添加到游戏机的总点数中。 -2. 一个点卡只能被绑定一次,绑定后不能重复使用。 -3. 点卡绑定后立即生效,无需重启游戏机。 - -#### 3.2.3 出卡扣点 (deduct-points) - -该接口用于游戏出卡时扣除相应点数。 - -**请求命令**:`deduct-points` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| pointsToDeduct | int | 否 | 需要扣除的点数,默认为1 | -| playerIdx | int | 否 | 机台位置索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "deduct-points", - "data": { - "pointsToDeduct": 1, - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 扣除后的剩余点数 | -| isUploaded | bool | 是否已上报到服务器 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "deduct-points", - "data": { - "remainingPoints": 99, - "isUploaded": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(扣点数量无效) | -| 20 | 数据库错误(扣点操作失败) | -| 50 | 点数不足 | - -**备注**: -1. 扣点操作会根据当前配置的扣点模式执行(本地模式、实时模式或混合模式)。 -2. 如果使用本地模式,操作会先在本地执行,然后在适当时机上报到服务器。 -3. 如果使用实时模式,操作会直接上报到服务器,并立即返回结果。 -4. 如果点数不足,将返回错误码50。 -5. 每次扣点操作都会记录在扣点记录表中,用于后续统计和审计。 - -### 3.3 游戏记录功能接口 - -#### 3.3.1 保存游戏积分 (cwyz-score-save) - -该接口用于保存玩家的游戏积分和相关游戏数据,同时可以获取游戏排行榜信息。 - -**请求命令**:`cwyz-score-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| score | int | 是 | 游戏得分 | -| gameLevel | string | 是 | 游戏关卡 | -| gameType | string | 是 | 游戏类型 | -| topLimit | int | 否 | 排行榜总数,默认为10 | -| gameDuration | int | 否 | 游戏时长(秒) | -| golds | int | 否 | 获得金币数量 | -| gemstone | int | 否 | 获得的未鉴定宝石数量 | -| playerIdx | int | 否 | 玩家索引,默认为1 | -| skinId | string | 否 | 皮肤ID | -| playerCount | int | 否 | 共同游戏的玩家数量,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-score-save", - "data": { - "cardId": "1234567890", - "score": 1500, - "gameLevel": "1", - "gameType": "standard", - "topLimit": 10, - "gameDuration": 300, - "golds": 50, - "gemstone": 2, - "playerIdx": 1, - "skinId": "skin_001", - "playerCount": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分 | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-score-save", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(保存积分失败) | -| 40 | 用户不存在 | - -**备注**: -1. 如果提供了金币(golds)参数且大于0,系统会自动调用保存金币记录的功能。 -2. 如果提供了宝石(gemstone)参数且大于0,系统会自动调用保存宝石记录的功能。 -3. 积分数据会根据游戏类型和游戏关卡进行分类,便于后续查询。 -4. 响应中的排行榜数据包含当前用户的排名和指定数量的最高分记录。 -5. 如果用户卡ID不存在,系统会尝试创建一个游客记录。 - -#### 3.3.2 获取游戏排行榜 (cwyz-query-score-top) - -该接口用于获取游戏排行榜信息。 - -**请求命令**:`cwyz-query-score-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-score-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分 | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-score-top", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(获取排行榜失败) | -| 40 | 用户不存在 | - -**备注**: -1. 排行榜数据包含当前用户的排名和指定数量的最高分记录。 -2. 如果用户卡ID不存在,系统会尝试创建一个游客记录。 - -#### 3.3.3 获取游戏时长排行榜 (cwyz-query-duration-top) - -该接口用于获取游戏时长排行榜信息。 - -**请求命令**:`cwyz-query-duration-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-duration-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| duration | int | 用户在此排行榜中的游戏时长(秒) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-duration-top", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 2, - "duration": 280, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 1800, - "gameDuration": 320, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1200, - "gameDuration": 240, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(游戏类型为空) | -| 20 | 数据库错误(获取排行榜失败) | - -**备注**: -1. 该接口专门用于获取按游戏时长排序的排行榜。 -2. 排行榜数据按游戏时长降序排列。 -3. 如果设置isGlobal为true,将获取全网的排行榜数据,可能需要更长的响应时间。 - -#### 3.3.4 统一获取游戏排行榜 (cwyz-query-top) - -该接口统一了获取积分排行榜和时长排行榜的功能,通过topType参数区分不同类型的排行榜。 - -**请求命令**:`cwyz-query-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| topType | int | 是 | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "topType": 0, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| topType | int | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分(当topType=0时有效) | -| duration | int | 用户在此排行榜中的游戏时长(当topType=1时有效) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例(积分排行榜)**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-top", - "data": { - "gameType": "standard", - "topType": 0, - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(游戏类型为空或排行榜类型无效) | -| 20 | 数据库错误(获取排行榜失败) | - -**备注**: -1. 该接口可以根据topType参数灵活获取不同类型的排行榜。 -2. 当topType=0时,排行榜按积分降序排列;当topType=1时,排行榜按游戏时长降序排列。 -3. 如果设置isGlobal为true,将获取全网的排行榜数据,可能需要更长的响应时间。 -4. 推荐使用该接口代替单独的积分排行榜和时长排行榜接口,提高代码复用性。 - -#### 3.3.5 保存出卡记录 (cwyz-outcard-save) - -该接口用于记录游戏中玩家获得卡片的信息。 - -**请求命令**:`cwyz-outcard-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| cardStar | int | 是 | 卡片星级 | -| cardType | string | 是 | 卡片类型 | -| cardName | string | 是 | 卡片名称 | -| gameType | string | 是 | 游戏类型 | -| playerIdx | int | 否 | 玩家索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-outcard-save", - "data": { - "cardId": "1234567890", - "cardStar": 4, - "cardType": "monster", - "cardName": "火焰龙", - "gameType": "standard", - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| cardStar | int | 卡片星级 | -| cardType | string | 卡片类型 | -| cardName | string | 卡片名称 | -| drawTime | string | 出卡时间(格式化字符串) | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-outcard-save", - "data": { - "success": true, - "cardId": "1234567890", - "userName": "玩家001", - "cardStar": 4, - "cardType": "monster", - "cardName": "火焰龙", - "drawTime": "2025-05-28 16:30:45", - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(保存出卡记录失败) | -| 40 | 用户不存在 | - -**备注**: -1. 出卡记录会保存在本地数据库中,并在网络可用时上传到服务器。 -2. 出卡时间使用服务器时间,确保统一性。 -3. 卡片星级通常为1-5,表示卡片的稀有程度。 -4. 卡片类型可以是游戏定义的任何类型,如"monster"、"magic"、"trap"等。 - -#### 3.3.6 保存硬币记账 (cwyz-coin-save) - -该接口用于记录玩家投币和消耗硬币的信息。 - -**请求命令**:`cwyz-coin-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| coinCount | int | 是 | 硬币数量 | -| type | int | 是 | 类型:1=投币,2=消耗 | -| playerIdx | int | 否 | 玩家索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-coin-save", - "data": { - "cardId": "1234567890", - "coinCount": 5, - "type": 1, - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| coinCount | int | 硬币数量 | -| type | int | 操作类型:1=投币,2=消耗 | -| balance | int | 当前余额 | -| processTime | string | 处理时间(格式化字符串) | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-coin-save", - "data": { - "success": true, - "cardId": "1234567890", - "userName": "玩家001", - "coinCount": 5, - "type": 1, - "balance": 15, - "processTime": "2025-05-28 17:15:30", - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失或硬币数量为0) | -| 20 | 数据库错误(保存硬币记录失败) | -| 40 | 用户不存在 | -| 50 | 余额不足(当type=2,消耗硬币时) | - -**备注**: -1. 硬币记账会记录玩家的投币和消耗情况,便于统计和分析。 -2. 类型为1表示投币(增加余额),类型为2表示消耗(减少余额)。 -3. 响应中的balance字段表示操作后的硬币余额。 -4. 如果是消耗操作且余额不足,将返回错误码50。 -5. 所有操作都会记录在硬币记账表中,用于后续统计和审计。 - -### 3.4 4G网络控制接口 - -#### 3.4.1 查询4G模块信息 (query-4g-info) - -该接口用于查询4G网络模块的详细信息,包括网络状态、信号强度、SIM卡信息等。 - -**请求命令**:`query-4g-info` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "query-4g-info", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| mnc | string | 网络MNC码 | -| cid | string | 基站CID | -| lac | string | 位置区域码 | -| serialPort | string | 串口名称 | -| dbm | int | 信号强度 | -| revision | string | 模块固件版本 | -| sim | string | SIM卡ICCID | -| imsi | string | SIM卡IMSI | -| imei | string | 设备IMEI | -| ndisEnabled | bool | NDIS网卡状态 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "query-4g-info", - "data": { - "mnc": "01", - "cid": "123456", - "lac": "7890", - "serialPort": "COM3", - "dbm": -65, - "revision": "EG25GGBR07A08M2G", - "sim": "898600123456789012345", - "imsi": "460012345678901", - "imei": "862512345678901", - "ndisEnabled": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | - -**备注**: -1. 信号强度(dbm)通常在-50到-120之间,数值越大(越接近0)表示信号越强。 -2. ndisEnabled表示4G网卡是否启用,true表示已启用,false表示已禁用。 -3. 如果设备没有4G模块,将返回错误码80。 -4. 该接口仅查询信息,不会改变任何网络设置。 - -#### 3.4.2 禁用4G网络 (stop-4g-net) - -该接口用于禁用4G网络,可用于节省流量或解决网络冲突问题。 - -**请求命令**:`stop-4g-net` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "stop-4g-net", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "stop-4g-net", - "data": { - "success": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | -| 82 | 网络已经处于禁用状态 | - -**备注**: -1. 该接口会关闭4G网卡,停止所有网络通信。 -2. 操作成功后,设备将无法通过4G网络连接互联网,但可以通过其他网络接口(如WiFi、有线网络)连接。 -3. 4G网络禁用后,可通过start-4g-net接口重新启用。 -4. 如果设备没有4G模块,将返回错误码80。 - -#### 3.4.3 启用4G网络 (start-4g-net) - -该接口用于启用4G网络,恢复设备的移动网络连接。 - -**请求命令**:`start-4g-net` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "start-4g-net", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "start-4g-net", - "data": { - "success": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | -| 83 | 网络已经处于启用状态 | -| 84 | SIM卡异常 | - -**备注**: -1. 该接口会启用4G网卡,恢复移动网络连接。 -2. 操作成功后,设备将能够通过4G网络连接互联网。 -3. 网络连接过程可能需要几秒到几十秒不等,取决于网络信号强度和运营商网络状况。 -4. 如果设备没有4G模块或SIM卡异常,将返回相应的错误码。 - -### 3.5 世宇接口状态 - -#### 3.5.1 检查世宇接口状态 (check-unis-net-state) - -该接口用于检查与世宇服务器的连接状态。 - -**请求命令**:`check-unis-net-state` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "check-unis-net-state", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| state | int | 世宇网络连接状态: 0-断开连接 1-有线网络 2-4G网络 3-服务异常 4-系统异常 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "check-unis-net-state", - "data": { - "state": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | - -**网络状态代码说明**: - -| 网络状态码 | 描述 | 说明 | -|-----------|------|------| -| 0 | 网络断开 | 无网络连接 | -| 1 | 有线网络 | 有线网络连接正常且能访问世宇服务器 | -| 2 | 4G网络 | 4G网络连接正常且能访问世宇服务器 | -| 3 | 服务异常 | 网络连接正常但无法登录世宇服务器 | -| 4 | 系统异常 | 网络连接正常但无法获取登录凭证 | - -**备注**: -1. 该接口用于检查业务插件与世宇服务器的连接状态。 -2. 检查过程包括网络连接检测和世宇服务器登录尝试。 -3. 有线网络和4G网络状态表示网络连接正常且可成功登录世宇服务器。 -4. 服务异常表示网络连接正常但无法登录世宇服务器,可能是服务器问题。 -5. 系统异常表示网络连接正常但无法获取登录凭证,可能是配置问题。 - -#### 3.5.2 获取系统信息 (get-system-info) - -该接口用于获取系统综合信息,包括网络状态、版本信息、磁盘信息和剩余点数。 - -**请求命令**:`get-system-info` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "get-system-info", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| networkState | int | 网络状态代码: 0-断开连接 1-有线网络 2-4G网络 3-服务异常 4-系统异常 | -| networkStateDesc | string | 网络状态描述文本 | -| versionInfo | object | 版本信息对象 | -| diskInfos | array | 磁盘信息数组 | -| remainingPoints | int | 剩余点数 | - -**versionInfo对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameVersion | string | 游戏版本 | -| updaterVersion | string | 更新器版本 | -| businessVersion | string | 业务模块版本 | -| consoleVersion | string | 控制台版本 | -| deviceId | string | 设备ID | - -**diskInfos数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| driveLetter | string | 盘符 | -| freeSpace | string | 可用空间(格式化后的字符串) | -| totalSpace | string | 总空间(格式化后的字符串) | -| usedSpace | string | 已用空间(格式化后的字符串) | -| freePercent | float | 可用空间百分比 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "get-system-info", - "data": { - "networkState": 1, - "networkStateDesc": "有线网络", - "versionInfo": { - "gameVersion": "1.0.5", - "updaterVersion": "1.2.0", - "businessVersion": "1.1.0", - "consoleVersion": "1.0.8", - "deviceId": "ABCDEF123456" - }, - "diskInfos": [ - { - "driveLetter": "C:", - "freeSpace": "50.5 GB", - "totalSpace": "100 GB", - "usedSpace": "49.5 GB", - "freePercent": 50.5 - }, - { - "driveLetter": "D:", - "freeSpace": "120 GB", - "totalSpace": "500 GB", - "usedSpace": "380 GB", - "freePercent": 24.0 - } - ], - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | - -**备注**: -1. 该接口汇总了系统的多项信息,便于快速了解系统状态。 -2. 网络状态检测逻辑与check-unis-net-state接口保持一致。 -3. 版本信息通过向更新器发送get-version-info指令获取。 -4. 磁盘信息按盘符字母顺序排列。 -5. 剩余点数与get-remaining-points接口返回的值一致。 diff --git a/1Project/战斗编辑器/参考文章/如何实现一个强大的MMO技能系统——BUFF.md b/1Project/战斗编辑器/参考文章/如何实现一个强大的MMO技能系统——BUFF.md deleted file mode 100644 index 4a07b96..0000000 --- a/1Project/战斗编辑器/参考文章/如何实现一个强大的MMO技能系统——BUFF.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -tags: - - Buff ---- - -source:[(2 封私信 / 4 条消息) 如何实现一个强大的MMO技能系统——BUFF - 知乎](https://zhuanlan.zhihu.com/p/150812545?utm_psn=1894773352150832417) - ---- - -## 前言 - -Buff模块可以说是技能中最核心又最复杂的系统了。一个优秀的[Buff系统](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Buff%E7%B3%BB%E7%BB%9F&zhida_source=entity)能够让策划的创意得到最大限度的发挥,大幅增强游戏的战斗深度和可玩性,并且同时也能让开发者轻易的扩展维护,支持更多的效果和功能。本章将为你详细讲述一个强大的Buff系统是如何实现的。(长文预警) - -## 正文 - -### 第一节:Buff定义 - -首先我们将Buff系统分为三个层次,具体继承关系如下: - -![](https://picx.zhimg.com/v2-fbac31f1aed84bb08ba0df09f73b6117_1440w.jpg) - -**Buff:**所有Buff的基类,包含各类成员函数和基本接口。 - -**[Modifier](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Modifier&zhida_source=entity):**继承于Buff,代表这个Buff是一个修改器,它可以用来修改当前目标的各种属性,状态等等。抽象Modifier这个类的目的是出于性能优化的考虑。因为当Buff修改角色的属性或者状态时,会导致重新计算角色的动态属性, 而在游戏中我们很多的Buff并不需要修改角色的属性状态,仅仅用来提供一段逻辑。那么如果它是一个Buff不是Modifier,就不需要重新计算角色的动态属性。 - -**[MotionModifier](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=MotionModifier&zhida_source=entity):**继承于Modifier,代表此类Buff提供修改玩家运动效果的功能。因为牵涉到与运动组件的交互,所以抽象出一个新的类。 - -Buff类层次结构划分了之后,那么Buff需要包含那些成员数据呢? - -我们提供BuffTypeId(Buff类型Id), **Caster**(Buff施加者),Parent(Buff当前挂载的目标), **Ability**(Buff由哪个技能创建),BuffLayer(层数), BuffLevel(等级)BuffDuration(时长),**BuffTag,BuffImmuneTag(免疫BuffTag)**以及**Context**(Buff创建时的一些相关上下文数据)等等。 - -在这里,我将说明一下Caster,Ability以及Context这三个成员,这也可能是我们Buff系统中一些独特的点。 - -**Caster代表Buff的施加者**,它有可能为空,也有可能不为空,视具体构造时是否传Caster参数而定。但是Buff有一个配置项bNoCaster(是否强制设置Caster为空)。**如果bNoCaster = true。则Buff的Caster一定为空。** - -为什么要有一个bNoCaster设置呢?那是因为**我们的Caster不仅仅是一个成员项,它还关系到Buff合并问题。**如果存在两个TypeId类型相同的Buff时候,当他们的Caster相同才可以走合并流程(Buff层数增加),如果Caster不同,则不能合并。**当策划有一些玩法需求可以多人给BOSS叠Buff时就可以配置Buff的bNoCaster=true,**这样就不需要开发者在写代码添加Buff的时候小心翼翼的设置Caster参数为空了。另外还有几种情况也需要设置bNoCaster=true,比如存在一个熔岩地图,或者冰雪地图,玩家每秒掉多少血量,这个时候也可以配置bNoCaster=true。再比如说一些活动buff,如双倍经验buff,红名惩罚buff,都可以由策划配置bNoCaster=true。类似于双倍经验,还有红名Buff这种**所有需要存盘的Buff,我们都需要设置bNoCaster=true。**也许会有人有疑问,这样能满足需求吗?完全可以,我会在最后的示例部分举出一个例子来解答这个疑问。 - -**Ability代表Buff是由哪个技能创建**,它有可能为空,也有可能不为空,视具体构造时是否传Ability参数而定。通过Ability这个成员类型,我们就将Buff与技能联系起来了,我们能在Buff中取得技能的各种数据,通过获取技能的数据,然后由Buff来实现各种各样的技能效果。 - -**BuffTag,BuffImmuneTag由策划配置(基于标记位),标注这个Buff属于那些种类以及免疫哪些种类。**策划可以定义一些Tag如下: - -1. Metal = 1 << 1 (金系) -2. Wood = 1 << 2(木系) -3. Water = 1 << 3(水系) -4. Fire = 1 << 4(火系) -5. Earth = 1 << 5(土系) - -当策划配置BuffTag为Meta | Wood时,则代表这个Buff归属为金系和木系Buff。如果策划配置BuffImmuneTag为Wood | Fire时,则代表这个Buff可以免疫所有木系和火系Buff。由于Tag的实际定义由策划控制,策划可以根据他们的需求组合出各种各样的免疫效果。我将在后面的示例里面描述一些基于Tag和ImmuneTag用法的例子来让读者体会Tag和ImmuneTag者两个概念抽象的简洁之美。 - -**Context代表Buff创建时候的一些上下文数据**,它是一个不确定的项,通过外部传入各种自定义的数据,然后在Buff逻辑中使用这些自定义数据。 - -### 第二节:Buff执行流程 - -在Buff从创建到销毁的过程中,我们划分为如下几个阶段: - -1. Buff创建前检查当前Buff是否可创建。一般主要是检测目标身上是否存在免疫该Buff的相关Buff,如果被免疫则不会创建该Buff。 -2. Buff在实例化之后,生效之前(还未加入到Buff容器中)时会抛出一个**[OnBuffAwake](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffAwake&zhida_source=entity)**事件。如果存在某种Buff的效果是:受到负面效果时,驱散当前所有负面效果,并给自己加一个护盾。那么这个时候就需要监听BuffAwake事件了,此时会给自己加护盾,并且把所有负面Buff驱散。**这意味着一个Buff可能还未生效之前即销毁了(小心Buff的生命周期)。** -3. 当Buff生效时(加入到Buff容器后),我们提供给策划一个抽象接口**[OnBuffStart](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffStart&zhida_source=entity)**,由策划配置具体效果。 -4. 当Buff添加时存在相同类型且Caster相等的时候,Buff执行刷新流程(更新Buff层数,等级,持续时间等数据)。我们提供给策划一个抽象接口**[OnBuffRefresh](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRefresh&zhida_source=entity)**,由策划配置具体效果。 -5. 当Buff销毁前(还未从Buff容器中移除),我们提供给策划一个抽象接口**[OnBuffRemove](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRemove&zhida_source=entity)**,由策划配置具体效果。 -6. 当Buff销毁后(已从Buff容器中移除),我们提供给策划一个抽象接口**[OnBuffDestroy](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffDestroy&zhida_source=entity)**,由策划配置具体效果。 -7. Buff还可以创建定时器,以触发间隔持续效果。通过策划配置时调用**StartIntervalThink**操作,提供**OnIntervalThink**抽象接口供策划配置具体效果。 -8. Buff还可以通过请求改变运动来触发相关效果。通过策划配置时调用**ApplyMotion**操作,提供**[OnMotionUpdate](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionUpdate&zhida_source=entity)**和**[OnMotionInterrupt](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionInterrupt&zhida_source=entity)**接口供策划配置具体效果。 - -Buff由于其有着生命周期可控,低耦合(通过监听事件修改逻辑),高内聚、易于扩展的特性,因此通过使用Buff来管理逻辑的话,不仅方便处理各种复杂的行为,同时还能有效的减少开发者的维护难度。 - -例如延迟触发伤害是游戏中非常常见的需求,在一些开发者的设计中就是直接给角色挂个定时器触发伤害。简单的游戏里这样做没什么大问题,但是如果技能逻辑稍微复杂点,这样就会带来很多问题。例如某天策划提出需求,如果受到控制效果时需要取消该延迟伤害。此时你怎么办,直接干掉timer?结果策划过了两天又提出了个新的需求,还是受到控制效果时,需要这个延迟伤害立即触发,你又怎么办?再又比如说,当角色受到伤害超过1000点时,这个延迟伤害立即触发,你又该怎么做? - -这里就体现出Buff的方便之处了,我们可以直接添加一个持续时间为N秒的Buff。Buff销毁时触发伤害。如果需求变更为受到控制时取消伤害,那么我们就在Buff中检查当前是否包含有Tag为Control的Buff。如果有,则设置Buff.bTriggerDamage=false,同时自我销毁。然后在BuffDestroy触发的时候检查是否触发伤害,如果bTriggerDamage为false则不触发伤害。同理,当需求为Buff监听伤害超过1000点伤害立即触发时,我们只需要通过Buff监听OnTakeDamage事件,检查当前受到的伤害值是否大于1000点,如果是则销毁Buff,此时立即触发BuffDestroy并执行伤害效果。 - -从上面的例子我们可以看出整个控制逻辑都是在Buff内部完成的,不需要各种手动开启/取消定时器。只需要Buff扩展下逻辑检查即可,具有非常好的扩展性和高内聚性。 - -### 第三节:Buff修改状态(ModifyState) - -Buff可以通过修改状态去影响角色行为逻辑。以下列举一些最常见的状态: - -1. Stun(眩晕状态——目标不再响应任何操控) -2. Root(缠绕,又称定身——目标不响应移动请求,但是可以执行某些操作,如施放某些技能) -3. Silence (沉默——目标禁止施放技能) -4. Invincible (无敌——几乎不受到所有的伤害和效果影响) -5. Invisible (隐身——不可被其他人看见) - -这些状态是高度凝练的精华,抽象到极致的代表。**非常多的游戏效果实际上都是这几种状态+运动+动画的组合。**这里很多开发者都会有一个**设计误区**就是**把Buff的状态跟运动和动画耦合在一块**,比如:眩晕状态一定就是播个眩晕动画,然后击退状态就是击退位移+击退动画。这样最后导致的问题就是状态膨胀,而且各种逻辑耦合,Bug频出,最后维护成本大大提高。 - -以Stun为例,很多人第一眼看过去就觉得它是个Debuff,是个敌人给我方加的控制Buff。实际上并非如此,Stun可以用到的地方非常多。例如有个技能是野蛮冲撞,释放后2秒内向前移动10米并将敌人推开。那这个Buff的实现就是技能Spell的时候给角色加个Buff,这个Buff会有个Stun状态同时带位移突进效果。挂上这个Buff后,技能施放后角色2秒内就不会响应角色按键移动和释放其他技能的请求了,同时往前突进的效果由Buff控制,将来处理各种位移打断效果也很方便。 再比如说有个技能叫寒冰屏障:你被一道寒冰屏障所笼罩,在十秒内不会受到任何物理和法术伤害,但这期间无法移动、攻击或施法。那这个技能的实现也很简单,就是一个十秒的Buff同时添加了眩晕和无敌这两个状态,如果还需要每秒回血,则StartIntervalThink(interval),然后OnIntervalThink的时候Heal当前角色即可。 - -除了各类战斗效果之外,我们的Buff甚至可以扩展到一些其他场景。比如说打BOSS前有个播过场动画的需求,此时策划希望隐藏Boss和玩家的血条和姓名。那么此时我们完全可以做个Buff,这个Buff扩展个状态HideHpBar,当有这个状态时即隐藏血条和名字就行了。而且我们还可以让这个Buff加上无敌状态,毕竟播过场动画的时候我们不希望玩家或者BOSS真的受到什么伤害。 - -总而言之,Buff状态除了上面提到几种高度凝练抽象的状态外,我们还可以根据具体游戏的需求去扩展各种特殊状态,以满足策划的需求,同时方便开发者管理逻辑。 - -### 第四节:Buff修改属性(ModifyAttribute) - -在游戏中Buff的添加与移除是一个频繁的过程。而玩家的属性来源有很多,如等级,装备,成就,任务,时装等等各种各样的来源。相比于Buff,这些模块修改属性的频率要远低于Buff,所以我们一般将玩家的属性划分为两层,第一层时Core(核心层),第二层是External(外部层)。Core层是玩家各个其他模块的属性总和,而External层则是Buff修改属性的总和。两者相加既为玩家的实时属性。 - -### 第五节:Buff修改运动(ModifyMotion) - -现在的MMO中为了增加动作表现力,经常会有很多位移效果,如突进,翻滚,千斤坠,击退,击飞,拖拽,吸引等等。那么这些效果该如何实现呢?而且有时候会遇到各种复杂的运动打断效果,比如击飞时不能被击退,击飞过程又能被冰冻效果定住,然后又有破冰技能击退冰冻物体并解除冰冻效果。面对这些复杂的情况,我们该如何设计呢? - -在我们的系统中,运动都是统一通过MovementComponent来管理。因此通过使用MotionModifier来与MovementComponent交互。MovementComponent中有一个CustomMotion,用来具体实现各种运动位移。具体运动实现相关细节我们将在后面的运动章节讲述。 - -在MotionModifier中,**我们会提供一个接口ApplyMotion(motionTypeId,priority, forceInterrupt)来向运动组件请求运动效果。**同时通过设置回调UpdateBeforeMovement和UpdateAfterMovement来触发运动前和运动后的Buff效果。下面我们初步介绍下ApplyMotion函数的三个参数: - -- motionTypeId:运动类型id,配置项。包含运动位移参数及相关数据。 -- priority:运动优先级,每个运动都有优先级,低优先级不能打断高优先级。 -- forceInterrrupt:是否忽略优先级,强制打断当前的Motion。 - -通过这三个参数,我们就能实现各类打断需求了。 - -比如说击退的运动优先级是100,击飞的运动优先级是200。那么在击飞过程中,施加击退Buff调用ApplyMotion的时候会返回false,这时可以销毁掉这个击退Buff,即击飞时无法击退。如果击飞时被冰冻,且冻在半空中停止不动,那么我们就需要设计一个静止Buff:运动优先级是300,作用效果是速度设置为0,不受重力影响,同时修改Stun状态并挂载冰冻特效。当破冰技消除冰冻效果时,则设置破冰Buff的位移效果为击退,设置运动优先级为100,forceInterrupt为true。此时ApplyMotion强制打断运动,冰冻Buff会触发OnMotionInterrupt回调,在此接口中冰冻Buff自我销毁即可。 - -**Buff修改运动仅代表修改运动轨迹。**比如说击退仅仅只是以直线移动一段距离。而击飞是以曲线移动一段距离。同理**轻功的翻滚,突刺其实都与击退是相同的运动轨迹。**他们都是在一定的时间内以直线到达目标地点,且都设置Stun状态。它们不一样的地方其实仅仅只是动画层的表现的不同。(可能策划还会设置不同的Tag和ImmuneTag标记下) - -**我们要牢牢记住,玩家看起来各种花哨的轻功击退击飞等位移效果实际上是State+Motion+Animation的组合。**掌握住了这一点,我们就可以通过简单的组合实现各种丰富的效果了,而不会被各种花哨的效果所迷惑,以为他们都是不一样的效果,导致最后设计出无比庞杂且难以维护的系统了。 - -### 第六节:Buff监听事件 - -Buff可以通过监听各类事件,执行特定逻辑或者修改事件数据来实现各种效果。 - -最常见的事件监听一般有: - -- **OnAbilityExecuted,监听某个主动技能执行成功。**常用于被动技能Buff,比如说角色施法时有10%概率获得30%的攻速提升。那么我们通常是Buff-A监听OnAbilityExcuted事件,然后10%概率添加Buff-B。Buff-B的作用是修改玩家属性,增加30%攻速。 -- **OnBeforeGiveDamage,OnAfterGiveDamage监听我方给目标造成伤害时触发。**比如说对目标造成的伤害有10%概率无法被闪避,那么这个效果我们就可以通过监听OnBeforeGiveDamage的流程来实现。当执行伤害流程时,在计算伤害前我们抛出一个事件event。event里面有当前伤害数据。Buff在调用OnBeforeGiveDamage(event)时,修改event.Damage.DamageFlag |= DamageFlag_NotMiss,标注该伤害无法被闪避就行了。又或者如果有一个需求是给目标造成伤害后有10%几率触发DOT伤害效果,那么我们在OnAfterGiveDamage的时候取出event.Target并给这个目标加个DOT类Buff即可。 -- **OnBeforeTakeDamage,OnAfterTakeDamage监听我方受到伤害时触发。**如护盾类Buff通常在OnBeforeTakeDamage的时候修改伤害数据。又或者有某些Buff在受到伤害后可以触发各类效果就可以通过监听OnAfterTakeDamage事件来触发指定逻辑。 -- **OnBeforeDead,OnAfterDead监听我方死亡时触发。**如免疫致死效果可以通过监听OnBeforeDead事件修改角色当前的Hp>0,从而让角色提前退出死亡流程以避免死亡。死亡后触发额外效果,如爆炸或者召唤其他生物都可以通过监听OnAfterDead事件来执行。 -- **OnKill事件,监听我方击杀目标时触发。**如当击杀目标后获得治疗效果回复即可通过监听到Kill事件时给自己加一个HOT的Buff来实现。 - -开发者可以通过扩展各类事件列表,让Buff通过监听对应事件就能执行任意逻辑。不需要与任何模块耦合,只需要抛出事件,监听事件,执行逻辑即可获得Buff功能上的扩展。 - -## 总结 - -以上我们通过六个小节讲述了Buff系统主要模块的实现方法。通过这样的设计,我们让Buff的深度和扩展性都能够得到了极大的提升,几乎能实现各种各样的效果。足以让策划的创意得到最大限度的发挥。 - -## 示例 - -为了让读者便于直观理解,我会提出一些具体实现的例子以供参考: - -- 问:Buff互斥效果也很常见,怎么做? -- 答:BuffTag和BuffImmuneTag可轻松实现。比如说火系Buff和水系Buff互斥。无论策划的需求是存在水系Buff的时候无法添加火系Buff,还是存在水系Buff的时候添加火系Buff会驱散水系Buff都可以实现。第一种情况最简单,水系Buff配置Tag 为Water的时候配置ImmuneTag为Fire。此时存在水系Buff的时候即可免疫火系Buff了。第二中情况也好办。配置BuffTag为Water。当OnBuffStart的时候调用驱散接口DispelByTag(Water),驱散掉所有火系Tag相关Buff即可。 - - - -- 问:霸体效果怎么实现,而且假如说存在破霸体效果又怎么实现,而且Boss的霸体效果完全不受影响又怎么实现?万一还存在特殊效果可以让Boss受到控制怎么办? -- 答:我们可以定义两个BuffTag:WeakControl(弱控制)和StrongControl(强控制),普通霸体效果通过Buff配置ImmuneTag:WeakControl即可免疫控制效果。如果是破霸体效果,我们给这个Buff的Tag标记StrongControl就行,同时Boss的Buff配置ImmuneTag为WeakControl | StrongControl(免疫弱控制和强控制)就满足需求了。如果存在某个特殊的效果能让Boss受到控制效果的话,那这个Buff的Tag不要标记WeakControl和StrongControl就行了,这样它就无法被免疫掉了。看起来复杂的霸体破霸体效果实际实现就这么简单,就这么清晰,不需要引入任何新的系统。 - - - -- 问:Buff存盘那块如何处理跟施法者相关的属性数据?如施法者可以给目标添加一个强力的毒Buff,具体伤害数值有施法者属性决定,离线后依旧生效,直到Buff时间结束才移除。 -- 答:这块我们的处理依旧很简单,Buff依然设置bNoCaster=true。但是在Buff创建的Context里面我们设置Context.DamageValue为根据施法者属性计算出来的伤害数值。然后Buff持续造成伤害的时候直接取Context.DamageValue即可。至于说想要玩家离线再上线,Caster离线再上线后,毒的伤害数值还能实时修改的话,这样的需求是不存在的,如果一定要做,当然也能做,只是麻烦一点而且也没有必要。这样的需求一般仅仅存在测试的大脑中,策划是不会有这样的玩法需求了。 - - - -- 问:常见的基于指定地点延迟触发的AOE效果怎么实现?当技能施法成功后就延迟触发,不会被打断AOE效果。(如果能被打断,我们可以用引导类技能轻松实现) -- 答:我们将技能标记为可指定目标地点释放,当技能Spell的时候我们先给自己加一个Buff,这个Buff仅仅用于延迟效果(当然可以有更多的可能性,如监听到某种事件立即结束并触发AOE效果),当Buff持续时间到了的时候在OnBuffDestroy的时候创建AOE效果Buff。这个AOE Buff会调用StartIntervalThink函数,在OnIntervalThink的时候通过Buff:GetAbility():GetCastPosition()为基准位置检查周围的敌方单位是否在AOE半径内,如果是,则施加作用效果。 \ No newline at end of file diff --git a/2Areas/提升效率/未命名.md b/2Areas/提升效率/未命名.md deleted file mode 100644 index a653333..0000000 --- a/2Areas/提升效率/未命名.md +++ /dev/null @@ -1,5 +0,0 @@ - -[【全网首发】专注力与创造力的秘诀|大脑炎症与创伤修复|脑科学与大脑默认模式网络|本世纪最详尽脑网络_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1DdXLYNEGz/?spm_id_from=333.788.recommend_more_video.17&vd_source=c0e8ba1ae97ae182776824f6b7c45879) -评论摘要 - -大道至简: 第一步先把身体搞好,优先级排序睡眠、饮食、运动。第二步减少干扰,黄片、游戏、小说、电影能免则免。第三步专项训练,冥想和正念。第四步才是术,番茄工作法、todolist、心流理论之类的。 \ No newline at end of file diff --git a/3Projects/UE/UE-CORE-002 TickTaskManager.md b/3Projects/UE/UE-CORE-002 TickTaskManager.md deleted file mode 100644 index 80780dd..0000000 --- a/3Projects/UE/UE-CORE-002 TickTaskManager.md +++ /dev/null @@ -1,1949 +0,0 @@ - -source:[UE-CORE-002 TickTaskManager](https://descriptive-cemetery-9e2.notion.site/UE-CORE-002-TickTaskManager-20da28348a39808cb364f311819b06dc#20fa28348a39807596ddc5508ea7b477) - ---- - - -# 一、概述 - -## 1.1 Tick 任务调度的重要性 - -在游戏引擎中,**Tick**(如 UE 的 Tick 以及 Unity 的 Update)是驱动动态内容更新的核心机制。每一帧的时间内,引擎需要完成大量的计算和逻辑处理,例如物理模拟、动画更新、AI 决策、用户输入响应等。这些任务的执行顺序和时机直接决定了游戏世界的稳定性和一致性。因此,**Tick 任务调度**的设计和实现是游戏引擎架构中的关键部分。 - -Tick 是一个每帧调用一次的函数,用于更新对象的状态或执行特定的逻辑。在 UE 中,几乎所有动态行为都依赖于 Tick 系统。以下是一些常见的 Tick 使用场景: - -- **输入处理** :如根据玩家输入,更新角色的位置和方向。 -- **物理模拟** :计算刚体动力学、碰撞检测和约束求解。 -- **动画更新** :驱动骨骼动画系统,确保角色动作流畅且与物理状态一致。 -- **网络同步** :将本地状态同步到服务器或其他客户端。 -- **UI 更新** :刷新界面元素的状态(如血条、计时器等)。 - -由于这些任务之间存在复杂的依赖关系(例如,物理结算必须先于动画更新),如果 Tick 任务的调度不当,可能会导致数据竞争、状态不一致甚至崩溃。 - -另外,由于每帧的时间是有限的,尤其是在高帧率(如 60 FPS 或更高)的情况下,每一帧只有约 16 毫秒的时间来完成所有任务。如果 Tick 系统没有经过良好的设计和优化,可能会导致以下问题: - -- **性能瓶颈** :高频 Tick 任务会占用大量 CPU 时间,影响其他系统的运行效率。 -- **资源浪费** :某些对象可能并不需要每帧都更新(例如远处的静态物体),但仍然被频繁调用。 - -同时,现代游戏引擎通常需要处理大量的异步任务(如数据加载与解码),为了支持这些异步任务,Tick 系统需要提供专门的机制,确保它们能够与其他任务正确协作。 - -综上所述,Tick 调度系统不仅是一个简单的“每帧调用”机制,更是游戏引擎运行时的核心支柱。它承担着以下几个重要职责: - -1. **确保任务按正确的顺序执行** :通过分组和依赖管理,避免逻辑冲突和状态不一致。 -2. **优化性能** :通过按需更新、多线程支持和细粒度控制,最大限度地提高运行效率。 -3. **支持复杂场景** :通过异步任务和动态注册机制,适应现代游戏开发中的多样化需求。 - -正因为 Tick 调度如此重要,UE 提供了一个专门的组件——**`FTickTaskManager`**,来管理和调度所有的 Tick 任务。它并非一个简单的功能优化,而是维系整个游戏世界稳定、高效、可预测运行的**核心基石**。 - -![Tick 任务调度核心与挑战](attachment:7bd27db8-d583-45aa-a558-11c3669e4e86:Editor___Mermaid_Chart-2025-06-11-030235.png) - -Tick 任务调度核心与挑战 - -## 1.2 FTickTaskManager 的角色与定位 - -`FTickTaskManager` 并非一个孤立的系统,而是紧密集成在 UWorld 的 Tick 流程中,扮演着一个至关重要的角色。 - -大致可以从以下三个层面来精确定位 `FTickTaskManager` 的角色: - -1. **UWorld 的中央调度核心**:`UWorld::Tick` 函数本身并不直接执行任何 Actor 或 Component 的 Tick 逻辑。它将这一复杂任务完全委托给了 `FTickTaskManager`。在每一帧,UWorld 会首先命令 `FTickTaskManager` 进行准备 (StartFrame),然后根据预设的 Tick Group 顺序,多次调用 `FTickTaskManager` 的 `RunTickGroup` 来执行相应阶段的任务,最后再通知其进行清理 (EndFrame)。可以说,FTickTaskManager 是 `UWorld::Tick` 的“大脑”和“执行引擎”,负责将 UWorld 的更新意图转化为具体的、有序的任务执行。 -2. **依赖关系解析器**:`FTickTaskManager` 的核心能力之一,是理解并处理 `FTickFunction` 之间通过 `AddPrerequisite` 建立的依赖关系。在 StartFrame 阶段,它会收集所有需要更新的 Tick 任务,并分析它们之间的前后置依赖,最终构建出一个有向无环图。这个依赖图精确地描述了本帧所有任务的执行约束,是保证逻辑正确性的基础。 -3. **并发任务分发器**:`FTickTaskManager` 自身不直接执行并行计算,但它扮演着连接上层游戏逻辑与底层 Task Graph 系统的关键桥梁。在 `RunTickGroup` 阶段,它会将依赖图中那些没有前置约束、可以并行执行的任务,打包成 `FGraphEvent` 并提交给 Task Graph 系统。由 Task Graph 系统负责将这些任务高效地分配到不同的 CPU 工作线程上。因此,`FTickTaskManager` 的角色更像一个“计划者”和“分包商”,它制定了详细的施工计划(依赖图),然后将具体的施工任务(可并行的 Tick)分发给专业的施工队(Task Graph)。 - -总结而言,**FTickTaskManager 的定位是:一个位于 UWorld 内部,负责将海量、无序、且相互依赖的 Tick 任务,转化为一个结构化、有序、且部分可并行的执行计划,并将其提交给底层系统来完成的高级调度器。** 它是 UE 解决 Tick 任务调度三大难题(顺序、依赖、性能)的核心技术实现。 - -![FTickTaskManager 分层架构](attachment:1b0f4837-c508-43f3-b708-132be492a28f:Editor___Mermaid_Chart-2025-06-11-031205.png) - -FTickTaskManager 分层架构 - -# 二、源码剖析 - -## 2.1 核心数据结构 - -### 2.1.1 FTickFunction - -> **源码文件**:`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h` - -`FTickFunction` 是一个 **USTRUCT**,这意味着它可以被 UE 的反射系统识别。它本身**不是一个可执行的函数**,而是一个**描述 Tick 任务所有属性和状态的“数据包”或“配置文件”**。当一个 Actor 或 Component 需要逐帧更新时,它内部就会包含一个 `FTickFunction` 的派生实例,并将其注册到 `FTickTaskManager` 中。 - ---- - -1. **公开配置属性** - -这部分成员变量主要用于配置一个 Tick 任务的基本行为。 - -```cpp -// 定义了这个Tick任务所属的Tick组,决定了其大致的执行阶段 -UPROPERTY(...) -TEnumAsByte TickGroup; - -// 定义了这个Tick任务必须在此Tick组之前完成 -UPROPERTY(...) -TEnumAsByte EndTickGroup; - -// 该任务是否在游戏暂停时也执行 -UPROPERTY(...) -uint8 bTickEvenWhenPaused:1; - -// 【关键】这个Tick功能是否在代码层面被永久启用。如果为false,它永远不会被注册。 -UPROPERTY() -uint8 bCanEverTick:1; - -// 如果为true,该任务在创建时默认是启用的,但后续可以动态关闭。 -UPROPERTY(...) -uint8 bStartWithTickEnabled:1; - -// 是否允许在专用服务器上运行 -UPROPERTY(...) -uint8 bAllowTickOnDedicatedServer:1; - -// Tick的执行频率(秒)。如果大于0,引擎会尝试按这个间隔来调用,而不是每帧都调。 -UPROPERTY(...) -float TickInterval; -``` - -**分析**: - -这部分定义了 Tick 任务的**静态行为蓝图**。`bCanEverTick` 是最重要的一个开关,它在编译时就决定了这个 Tick 功能是否存在。`TickGroup` 和 `EndTickGroup` 则是向 `FTickTaskManager` 声明其执行顺序的“意图”。开发者通过调整这些参数,可以精细地控制一个 Tick 任务的基础属性。 - ---- - -1. **内部状态与控制标志** - -这部分主要是一些非 UPROPERTY 的 bool 标志,用于更精细地控制 Tick 的行为。 - -```cpp -// 是否允许将多个类似的Tick任务打包在一起执行以提升性能 -uint8 bAllowTickBatching:1; - -// 是否在本Tick组内优先执行,用于需要尽早开始的异步任务 -uint8 bHighPriority:1; - -// 【关键】如果为true,该任务可以在任何工作线程上并行执行,否则只能在游戏主线程。 -uint8 bRunOnAnyThread:1; - -// ... 其他一些实验性或特殊用途的标志 ... - -// Tick任务的当前状态(启用、禁用、冷却中) -ETickState TickState : 2; -``` - -**分析:** - -`bRunOnAnyThread` 是实现并行优化的关键。如果一个 Tick 任务的逻辑不依赖于任何游戏线程特有的数据(比如 UI、某些 UObject 操作),就可以将此标志设为 true,`FTickTaskManager` 就会放心地将它交给任意一个空闲的 CPU 核心去执行,从而与游戏主线程并行工作,缩短帧时间。 - ---- - -1. **核心功能性成员** - -这部分是 `FTickFunction` 实现其调度能力的核心数据。 - -```cpp -// 【关键】存储该Tick任务的所有前置依赖项。 -// FTickTaskManager在构建任务图时会读取这个数组,来确定任务间的先后关系。 -TArray Prerequisites; - -// 【关键】一个懒加载的、包含所有运行时状态的内部数据结构。 -// 只有当Tick函数被注册后,这个指针才会被分配内存。 -// 这样做可以节省未注册的Tick函数的内存开销。 -TUniquePtr InternalData; -``` - -**分析:** - -`Prerequisites` 是依赖系统的**物理实现**。任何通过 `AddPrerequisite()` 添加的依赖,最终都会被存储在这个数组中。`InternalData` 的设计则体现了**性能优化的思想**。对于成千上万个可能永远不会Tick的组件,引擎不会为它们分配额外的运行时数据,只有当 `RegisterTickFunction` 被调用时,才会按需创建 `FInternalData`,这是一种非常高效的内存管理策略。 - -`FInternalData` 内部包含了诸如 `TaskPointer`(指向底层T ask Graph 任务的指针)、`ActualStartTickGroup`(因依赖延迟后,实际开始执行的 Tick 组)等**运行时动态变化的状态**。将这些动态状态与静态配置分离,使得整个结构更加清晰。 - ---- - -1. **核心功能性函数** - -这部分是 `FTickFunction` 对外暴露的主要API,以及其虚函数。 - -```cpp -// 注册/注销Tick函数到FTickTaskManager -ENGINE_API void RegisterTickFunction(class ULevel* Level); -ENGINE_API void UnRegisterTickFunction(); - -// 动态启用/禁用Tick -ENGINE_API void SetTickFunctionEnable(bool bInEnabled); - -// 【关键】添加/移除依赖项 -ENGINE_API void AddPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction); -ENGINE_API void RemovePrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction); - -// 【关键】纯虚函数,必须由派生类实现。这里是真正执行Tick逻辑的地方。 -ENGINE_API virtual void ExecuteTick(...) PURE_VIRTUAL(,); - -// 【关键】纯虚函数,用于在产生循环依赖等错误时,提供可读的诊断信息。 -ENGINE_API virtual FString DiagnosticMessage() PURE_VIRTUAL(, ...); -``` - -**分析:** - -`RegisterTickFunction` 和 `UnRegisterTickFunction` 是 Tick 任务生命周期的起点和终点。`AddPrerequisite` 是构建复杂依赖关系网的API入口。 - -最重要的两个**纯虚函数(PURE_VIRTUAL)** `ExecuteTick` 和 `DiagnosticMessage`,揭示了 `FTickFunction` 的设计模式:它是一个**抽象基类**,定义了一个**契约**。任何想要成为可Tick任务的类(如 `FActorTickFunction`),都必须实现这两个函数。`ExecuteTick` 负责“做什么”,`DiagnosticMessage` 负责“我是谁”。`FTickTaskManager` 在调度时,并不关心具体是谁在Tick,它只知道调用 `ExecuteTick` 这个标准接口即可,这是一种典型的**多态**应用。 - ---- - -**总结**: - -`FTickFunction` 是一个设计精巧、高度工程化的数据结构。它完美地体现了以下设计思想: - -- **配置与状态分离:** 将静态配置(UPROPERTY)与运行时动态数据(InternalData)分离。 -- **懒加载:** 通过 `TUniquePtr` 按需分配 `InternalData`,优化内存使用。 -- **抽象与多态:** 通过纯虚函数定义了一个统一的执行契约,让上层调度代码可以与具体实现解耦。 -- **数据驱动:** 整个结构就是一个数据容器,其行为完全由内部的成员变量配置来驱动。 - -![FTickFunction 结构类图](attachment:3dbe052f-4415-4baf-8e3f-c0145e11d5d3:Editor___Mermaid_Chart-2025-06-11-084044.png) - -FTickFunction 结构类图 - -### 2.1.2 FActorTickFunction & FActorComponentTickFunction - -> **源码文件**:`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h` - -```cpp -/** -* Tick function that calls AActor::TickActor -**/ -USTRUCT() -struct FActorTickFunction : public FTickFunction -{ - GENERATED_USTRUCT_BODY() - - /** AActor that is the target of this tick **/ -#if UE_WITH_REMOTE_OBJECT_HANDLE - TObjectPtr Target; -#else - class AActor* Target; -#endif - - /** - * Abstract function actually execute the tick. - * @param DeltaTime - frame time to advance, in seconds - * @param TickType - kind of tick for this frame - * @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created - * @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete. - **/ - ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; - /** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/ - ENGINE_API virtual FString DiagnosticMessage() override; - ENGINE_API virtual FName DiagnosticContext(bool bDetailed) override; - -#if UE_WITH_REMOTE_OBJECT_HANDLE - /** Exposes references to GC system */ - ENGINE_API void AddStructReferencedObjects(FReferenceCollector& Collector); -#endif -}; -``` - -```cpp -/** -* Tick function that calls UActorComponent::ConditionalTick -**/ -USTRUCT() -struct FActorComponentTickFunction : public FTickFunction -{ - GENERATED_USTRUCT_BODY() - -#if UE_WITH_REMOTE_OBJECT_HANDLE - TObjectPtr Target; -#else - /** AActor component that is the target of this tick **/ - class UActorComponent* Target; -#endif - - /** - * Abstract function actually execute the tick. - * @param DeltaTime - frame time to advance, in seconds - * @param TickType - kind of tick for this frame - * @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created - * @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete. - **/ - ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override; - /** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/ - ENGINE_API virtual FString DiagnosticMessage() override; - ENGINE_API virtual FName DiagnosticContext(bool bDetailed) override; - - /** - * Conditionally calls ExecuteTickFunc if registered and a bunch of other criteria are met - * @param Target - the actor component we are ticking - * @param bTickInEditor - whether the target wants to tick in the editor - * @param DeltaTime - The time since the last tick. - * @param TickType - Type of tick that we are running - * @param ExecuteTickFunc - the lambda that ultimately calls tick on the actor component - */ - - //NOTE: This already creates a UObject stat so don't double count in your own functions - - template - static void ExecuteTickHelper(UActorComponent* Target, bool bTickInEditor, float DeltaTime, ELevelTick TickType, const ExecuteTickLambda& ExecuteTickFunc); - -#if UE_WITH_REMOTE_OBJECT_HANDLE - /** Exposes references to GC system */ - ENGINE_API void AddStructReferencedObjects(FReferenceCollector& Collector); -#endif -}; -``` - -由上面的源码可知,`FActorTickFunction` & `FActorComponentTickFunction` 这两个结构体都直接继承自 `FTickFunction`。它们的核心作用是**将抽象的 Tick 任务具体化**,使其与一个特定的 `AActor` 或 `UActorComponent` 实例绑定,并负责在 `ExecuteTick` 被调用时,真正地去执行那个实例的 `Tick` 或 `TickComponent` 方法。 - -在分别解析之前,我们先看它们完全相同的设计模式: - -- **共同的核心设计**: - 1. **继承 `FTickFunction`:** 它们都继承了基类所有的配置属性(`TickGroup`等)、状态管理(`InternalData`)和API(`RegisterTickFunction`等)。 - 2. **实现纯虚函数:** 它们都用 `override` 关键字实现了基类中定义的两个纯虚函数 `ExecuteTick` 和 `DiagnosticMessage`,从而满足了基类定义的“契约”。 - 3. **持有目标指针:** 它们都包含一个名为 `Target` 的成员变量,用于**指向**它们所服务的具体 `UObject` 实例。 - ---- - -1. **`struct FActorTickFunction` 解析** - -- **`Target` 成员** - - **职责:** 这个指针就是 `FActorTickFunction` 的“服务对象”。它明确地指出了这个 Tick 任务是属于哪一个 `AActor` 实例的。 - - **类型选择:** - - `TObjectPtr`: 在较新的 UE 版本中,使用 `TObjectPtr` 是一种更现代、更安全的指针类型,它能被 GC 系统更好地追踪。 - - `AActor*`: 在旧版本或特定编译配置下,使用裸指针。 -- **`ExecuteTick()` 实现** - - **职责:** 这是**连接调度与执行的最后一环**。当 `FTickTaskManager` 调度到这个任务时,就会调用这个函数。 - - **内部实现(在 `.cpp` 文件中):** 这个函数的实现非常直接,其核心逻辑为:检查 `Target` 指针是否有效,然后调用 `AActor` 类中真正的 `TickActor` 方法(`TickActor` 内部会再调用我们熟悉的 `virtual void Tick(float DeltaTime)`)。 - -```cpp -void FActorTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) -{ - if (IsValid(Target)) - { - if (TickType != LEVELTICK_ViewportsOnly || Target->ShouldTickIfViewportsOnly()) - { - FScopeCycleCounterUObject ActorScope(Target); - Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this); - } - } -} -``` - -- **`DiagnosticMessage()` 实现** - - **职责:** 当 `FTickTaskManager` 检测到循环依赖等问题时,会调用这个函数来获取一段可读的错误信息,用于日志输出。 - - **内部实现:** 它通常会返回类似 `FString::Printf(TEXT("FActorTickFunction for %s"), *Target->GetFullName())` 的字符串,清晰地指明是哪个 Actor 的 Tick 出了问题。 - ---- - -1. **`struct FActorComponentTickFunction` 解析** - -它与 `FActorTickFunction` 的结构和原理几乎完全相同,只是服务对象不同。 - -- **`Target` 成员** - - **职责:** 指向其所服务的具体 `UActorComponent` 实例。 -- **`ExecuteTick()` 实现** - - **内部实现:** 其核心逻辑与 `FActorTickFunction` 类似,但它最终调用的是 `Target` 组件的 `TickComponent` 方法: - -```cpp -void FActorComponentTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FActorComponentTickFunction::ExecuteTick); - - ExecuteTickHelper(Target, Target->bTickInEditor, DeltaTime, TickType, [this, TickType](float DilatedTime) - { - Target->TickComponent(DilatedTime, TickType, this); - }); -} -``` - ---- - -**总结**: - -`FActorTickFunction` 和 `FActorComponentTickFunction` 是 `FTickFunction` 抽象概念的**具体化实现**。它们扮演着“适配器”(Adapter)的角色,将 `FTickTaskManager` 的泛型调度指令,精准地“翻译”为对特定 `AActor` 或 `UActorComponent` 实例的成员函数调用。 - -它们的设计体现了面向对象中**多态**和**继承**的核心思想,使得上层调度系统无需关心 Tick 任务的具体类型,从而实现了高度的解耦和可扩展性。 - -### 2.1.3 FInternalData - -> **源码文件**:`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h` - -`FInternalData` 是一个专门用于存储 **已注册** 的 `FTickFunction` 在**运行时动态变化的状态**的内部数据结构。 - -它的存在本身就是一个**性能优化设计**。`FTickFunction` 通过 `TUniquePtr InternalData;` 来持有它。这意味着,只有当一个 Tick 任务通过 `RegisterTickFunction()` 被激活时,引擎才会为其分配 `FInternalData` 的内存。对于成千上万个可能永远不会被注册的 Tick 任务(例如,一个从未被使用的组件),引擎不会浪费任何内存来存储它们的运行时状态。这是一种典型的 **懒加载** 策略。 - ---- - -1. **核心状态与注册信息** - -```cpp -// 标志位,表示该Tick函数是否已向FTickTaskManager注册。 -bool bRegistered : 1; - -// 内部状态,决定了 TaskPointer 指针的类型和含义。 -// ETickTaskState 枚举的可能值:NotQueued, Pending, HasTask, HasCompletionEvent -ETickTaskState TaskState; - -// 指向其所属的 FTickTaskLevel 的反向指针。 -// 这使得FTickTaskManager可以快速地从一个FTickFunction找到它所在的Level。 -class FTickTaskLevel* TickTaskLevel; -``` - -**分析:** - -`bRegistered` 是生命周期的起点。`TaskState` 是整个 Tick 调度过程中最重要的状态机,`FTickTaskManager` 通过改变这个状态来管理一个 Tick 任务从“待处理”到“已排队”再到“已完成”的整个流程。`TickTaskLevel` 则提供了上下文信息。 - ---- - -1. **运行时 Tick Group 信息** - -```cpp -// 由于依赖关系,一个Tick任务实际开始执行的Tick组可能晚于其配置的TickGroup。 -// 这个变量记录了它在本帧实际开始的Tick组。 -TEnumAsByte ActualStartTickGroup; - -// 类似地,记录了它在本帧保证会结束的Tick组。 -TEnumAsByte ActualEndTickGroup; -``` - -**分析:** 这组变量体现了依赖系统的动态性。一个配置为 `TG_PrePhysics` 的任务,如果它依赖的另一个任务直到 `TG_EndPhysics` 才完成,那么它自己的 `ActualStartTickGroup` 就会被动态调整为 `TG_EndPhysics` 之后的某个组。这是 `FTickTaskManager` 在 `StartFrame` 阶段进行依赖图解析后得出的**动态调度计划**。 - ---- - -1. **并发与任务图相关数据** - -```cpp -// 【关键】一个void*指针,其具体指向的类型由 TaskState 决定。 -// 它可能指向一个 FGraphEventRef(代表一个独立的Task Graph任务)或 -// 其他内部任务结构。这是FTickFunction与底层Task Graph系统连接的纽带。 -void* TaskPointer; - -// 用于多线程同步的原子计数器,记录该Tick任务在哪一帧被访问过,以防止重复处理。 -std::atomic TickVisitedGFrameCounter; - -// 记录该Tick任务在哪一帧被排入队列。 -std::atomic TickQueuedGFrameCounter; -``` - -**分析:** - -`TaskPointer` 是与底层并行系统交互的核心。`FTickTaskManager` 将一个 `FTickFunction` 包装成一个 `Task Graph` 任务后,会将任务的句柄(Handle)存储在这里。`std::atomic` 的使用则表明这些变量会在多线程环境下被访问,用于保证在并行调度时,每个任务在一帧内只被处理一次,避免了竞态条件。 - ---- - -1. **Tick 间隔与冷却管理** - -这部分成员专门用于实现 `TickInterval` 功能。 - -```cpp -// 标志位,缓存该函数是否因设置了TickInterval而被重新调度。 -bool bWasInterval:1; - -// 指向冷却列表中的下一个FTickFunction。 -// FTickTaskManager内部维护了一个按冷却时间排序的链表来管理所有设置了TickInterval的任务。 -FTickFunction* Next; - -// 剩余的冷却时间(秒)。 -// 它存储的是相对于链表中前一个元素的相对时间,这种设计可以高效地更新整个链表。 -float RelativeTickCooldown; - -// 上一次该函数被Tick时的游戏时间。 -// 用于计算下一次应该在何时Tick。 -float LastTickGameTimeSeconds; -``` - -**分析:** 这组数据揭示了 `TickInterval` 的实现原理。引擎并非为每个有间隔的 Tick 都创建一个独立的计时器,而是将它们组织成一个**高效的排序链表(冷却列表)**。每一帧,引擎只需要检查链表头部的任务,看看它的 `RelativeTickCooldown` 是否已经到期。这种设计避免了每帧遍历所有任务来检查时间,极大地提升了效率。 - ---- - -**总结**: - -![FInternalData Mindmap](attachment:7a4ed083-c473-4b10-b01d-db2a3b05c91a:Editor___Mermaid_Chart-2025-06-11-080937.png) - -FInternalData Mindmap - -FInternalData 是一个纯粹的**运行时数据容器**,它的设计充满了对性能和效率的考量: - -- **懒加载:** 只有在需要时才分配内存。 -- **状态驱动:** 通过 ETickTaskState 状态机来管理复杂的任务生命周期。 -- **动态调度:** 通过 ActualStartTickGroup 等变量记录依赖解析后的动态结果。 -- **并发安全:** 使用原子变量来确保多线程环境下的数据一致性。 -- **高效算法:** 通过精巧的链表和相对时间来管理 Tick 间隔,避免了不必要的计算。 - -FTickFunction 主体定义了“我是谁以及我想做什么”,而 FInternalData 则记录了“我现在正在做什么以及我接下来要去哪”。两者结合,构成了一个完整的、功能强大的 Tick 任务单元。 - -### 2.1.4 FTickPrerequisite - -> **源码文件**:`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h` - -`FTickPrerequisite` 是一个专门用于**安全地存储和引用一个前置依赖 Tick 任务**的 `USTRUCT`。当我们在 `FTickFunction` 中调用 `AddPrerequisite` 时,实际上就是在其内部的 `Prerequisites` 数组中添加了一个 `FTickPrerequisite` 的实例。`FTickPrerequisite` 这个结构体虽然小,但却是实现 `FTickTaskManager` 依赖系统的关键“连接件”。它的核心设计目标是解决一个问题:**如何在保证内存安全的前提下,持有一个指向另一个 `FTickFunction` 的指针?** - ---- - -```cpp -/** - * This is small structure to hold prerequisite tick functions - */ -USTRUCT() -struct FTickPrerequisite -{ - GENERATED_USTRUCT_BODY() - - /** Tick functions live inside of UObjects, so we need a separate weak pointer to the UObject solely for the purpose of determining if PrerequisiteTickFunction is still valid. */ - TWeakObjectPtr PrerequisiteObject; - - /** Pointer to the actual tick function and must be completed prior to our tick running. */ - struct FTickFunction* PrerequisiteTickFunction; - - /** Noop constructor. */ - FTickPrerequisite() - : PrerequisiteTickFunction(nullptr) - { - } - /** - * Constructor - * @param TargetObject - UObject containing this tick function. Only used to verify that the other pointer is still usable - * @param TargetTickFunction - Actual tick function to use as a prerequisite - **/ - FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction) - : PrerequisiteObject(TargetObject) - , PrerequisiteTickFunction(&TargetTickFunction) - { - check(PrerequisiteTickFunction); - } - /** Equality operator, used to prevent duplicates and allow removal by value. */ - bool operator==(const FTickPrerequisite& Other) const - { - return PrerequisiteObject == Other.PrerequisiteObject && - PrerequisiteTickFunction == Other.PrerequisiteTickFunction; - } - /** Return the tick function, if it is still valid. Can be null if the tick function was null or the containing UObject has been garbage collected. */ - struct FTickFunction* Get() - { - if (PrerequisiteObject.IsValid(true)) - { - return PrerequisiteTickFunction; - } - return nullptr; - } - - const struct FTickFunction* Get() const - { - if (PrerequisiteObject.IsValid(true)) - { - return PrerequisiteTickFunction; - } - return nullptr; - } -}; -``` - ---- - -1. **核心成员变量解析** - -由上面的源码可知,这个结构体只有两个成员变量,但它们的设计配合得很巧妙。 - -```cpp -// 用于验证 PrerequisiteTickFunction 指针是否仍然有效的弱引用。 -TWeakObjectPtr PrerequisiteObject; - -// 指向实际的前置依赖Tick函数的裸指针。 -struct FTickFunction* PrerequisiteTickFunction; -``` - -**分析:为什么定义两个指针?** - -- `FTickFunction` 本身并不是一个 `UObject`,所以它不受 UE 的垃圾回收(GC)系统直接管理。它通常是作为 `UObject`(如 `AActor` 或 `UActorComponent`)的成员变量存在的。 -- 如果直接持有一个 `FTickFunction*` 裸指针,我们无法知道它所依附的 `UObject` 是否已经被GC销毁。如果其宿主`UObject`被销毁,这个裸指针就会变成一个危险的**悬空指针**。 -- **`TWeakObjectPtr` 的关键作用:**`TWeakObjectPtr`(弱对象指针)是 UE 提供的一种智能指针,它**不会**阻止其指向的 `UObject` 被 GC 回收。它的核心能力是提供了一个 `IsValid()` 方法。在访问裸指针之前,我们可以通过检查 `PrerequisiteObject.IsValid()` 来安全地判断那个 `UObject` 是否还存活。如果 `IsValid()` 返回 `false`,就意味着宿主对象已经被销毁,那么 `PrerequisiteTickFunction` 这个裸指针也必然失效了。 -- **协同工作机制:** - - `PrerequisiteObject` 扮演着“哨兵”的角色。 - - `PrerequisiteTickFunction` 存储着实际的目标地址。 - - 只有在“哨兵”确认安全(`IsValid()`)之后,我们才会去使用那个地址。 - -![FTickPrerequisite 安全访问机制](attachment:7c645043-e873-45c0-b3e2-f341eefbb72f:mermaid-diagram-2025-06-11-151136.png) - -FTickPrerequisite 安全访问机制 - ---- - -1. **核心功能性函数解析** - -- **构造函数**: - -```cpp -FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction) -: PrerequisiteObject(TargetObject) -, PrerequisiteTickFunction(&TargetTickFunction) -{ - check(PrerequisiteTickFunction); -} -``` - -**分析:** - -构造函数非常直白,它同时接收宿主 UObject 的指针和 FTickFunction 的引用,并将它们分别存入两个成员变量中。这确保了每次创建一个依赖关系时,安全验证所需的信息都被完整地记录下来。 - ---- - -- **Get() 方法** - -```cpp -struct FTickFunction* Get() -{ - if (PrerequisiteObject.IsValid(true)) - { - return PrerequisiteTickFunction; - } - return nullptr; -} -``` - -**分析:** - -这是该结构体最核心的对外接口。它实践了我们上述提到的“先检查,后访问”的安全模式。 - -- 首先调用 PrerequisiteObject.IsValid(true)。参数 true 表示即使对象正在等待被清理(Pending Kill),也将其视为无效。 -- 只有在弱指针验证通过,确认宿主 UObject 仍然存活的情况下,它才会返回那个可能有效的 PrerequisiteTickFunction 裸指针。 -- 如果宿主对象已失效,它会安全地返回 nullptr,避免了任何访问悬空指针的风险。 - -FTickTaskManager 在构建依赖图时,就会通过调用这个 Get() 方法来安全地获取每一个依赖项。 - ---- - -**总结**: - -FTickPrerequisite 虽然定义简单,但却是一个展示**如何在非 UObject 上下文中安全引用 UObject 成员**的优秀范例。它通过将一个**裸指针**与一个**用于生命周期验证的弱指针**巧妙地捆绑在一起,构建了一个既高效(直接访问裸指针)又安全(通过 TWeakObjectPtr 验证)的依赖引用机制。 - ---- - -### 2.1.5 ETickingGroup - -> **源码文件**:`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h` - -`ETickingGroup` 是一个枚举类型,它定义了一系列离散的、有序的**“时间阶段”。`FTickTaskManager` 在一帧内,会严格按照这个枚举定义的顺序,一个接一个地执行属于每个阶段(Group)的 Tick 任务。**这套机制是 UE 用来解决 Tick 任务执行顺序和依赖关系问题的基石。 - -接下来,我将按照它们的实际执行顺序来逐一分析每个 Tick Group 的职责和设计目的。 - -(下面分析中提到的 ”注释描述“ 部分指的是源码中对于当前枚举值的注释。) - ---- - -1. **`TG_PrePhysics` - 物理模拟前** - -- **注释描述:** `Any item that needs to be executed before physics simulation starts.` (任何需要在物理模拟开始前执行的项) -- **职责:** 这是绝大多数游戏逻辑的默认执行阶段。所有决定物体“本帧想要如何运动”的逻辑都应该放在这里。 -- **典型用例:** - - **玩家输入处理:** 根据玩家的键盘/手柄输入,计算角色的移动向量。 - - **AI 决策:** AI Controller 在此决定本帧的移动目标或攻击行为。 - - **动画蓝图更新(非物理部分):** 更新状态机,决定要播放哪个动画序列。 -- **设计原因:** 物理引擎需要知道物体的“意图”(如目标速度、施加的力)才能开始计算。此阶段就是为了准备好所有这些输入数据。 - ---- - -1. **`TG_StartPhysics` - 物理模拟开始** - -- **注释描述:** `Special tick group that starts physics simulation.` (启动物理模拟的特殊Tick组) -- **元数据:** `UMETA(Hidden)` - 表示这个选项通常不在编辑器中对用户暴露。 -- **职责:** 一个内部阶段,用于触发物理引擎(如 Chaos)开始本帧的模拟计算。它更像是一个命令信号,而不是一个让用户注册任务的阶段。 - ---- - -1. **`TG_DuringPhysics` - 物理模拟中** - -- **注释描述:** `Any item that can be run in parallel with our physics simulation work.` (任何可以与物理模拟并行运行的项) -- **职责:** 提供一个时间窗口,用于执行那些**不依赖**于本帧物理计算最终结果的、且可以并行化的任务。 -- **典型用例:** - - **异步场景查询:** 发起一些耗时的射线检测或形状检测,而不阻塞主线程。 - - 一些独立的视觉效果或系统更新。 -- **设计原因:** 物理模拟是CPU密集型任务,通常在独立的线程上异步进行。`TG_DuringPhysics` 允许游戏主线程在等待物理计算的同时,去“插空”执行其他不相关的任务,从而提高CPU的利用率和整体性能。 - ---- - -1. **`TG_EndPhysics` - 物理模拟结束** - -- **注释描述:** `Special tick group that ends physics simulation.` (结束物理模拟的特殊Tick组) -- **元数据:** `UMETA(Hidden)` -- **职责:** 这是一个关键的**同步点**。游戏主线程会在此等待,直到异步的物理模拟线程完成其所有计算。完成后,物理引擎会将计算出的最终结果(如新的位置、旋转、碰撞事件)写回到游戏世界中的对象上。 - ---- - -1. **`TG_PostPhysics` - 物理模拟后** - -- **注释描述:** `Any item that needs rigid body and cloth simulation to be complete before being executed.` (任何需要刚体和布料模拟完成才能执行的项) -- **职责:** 执行所有**依赖于**本帧物理结算结果的逻辑。 -- **典型用例:** - - **骨骼动画更新:** 这是骨骼网格体组件的默认 Tick Group。它需要根据角色经过物理计算后的最终位置和速度来调整动画,例如使用 IK 将脚踩在正确的地面上,或根据速度混合不同的跑动动画。 - - **相机更新:** 相机需要跟随已经移动到最终位置的角色。 - - **碰撞事件响应:** 在这里处理 `OnComponentHit` 等事件是安全的,因为所有的碰撞信息此时都已生成。 -- **设计原因:** 这是为了保证**“表现”基于“事实”**。物理结果就是本帧的“事实”,而动画、相机等视觉表现必须基于这个事实来进行调整,否则就会出现“脚滑”、穿模等视觉错误。 - ---- - -1. **`TG_PostUpdateWork` - 更新工作后** - -- **注释描述:** `Any item that needs the update work to be done before being ticked.` (任何需要在更新工作完成后被Tick的项) -- **职责:** 在所有主要的游戏逻辑和视觉表现更新都完成后,执行一些收尾的更新任务。 -- **典型用例:** - - **布料模拟:** 布料通常需要附着在已经更新完动画的角色骨骼上,所以放在这里执行。 - - 一些最终的清理或同步逻辑。 - ---- - -1. **`TG_LastDemotable` - 可降级的最后任务** - -- **注释描述:** `Catchall for anything demoted to the end.` (一个用于接收所有被降级到末尾任务的“捕集器”) -- **元数据:** `UMETA(Hidden)` -- **职责:** 优先级最低的阶段。用于执行那些即使在一帧内被跳过也不会造成严重问题的任务。 - ---- - -1. **`TG_NewlySpawned` - 特殊:新生成对象** - -- **注释描述:** `...After every tick group this is repeatedly re-run until there are no more newly spawned items to run.` (在每个Tick组之后,这个组会重复运行,直到没有新生成的对象需要运行) -- **职责:** 这是一个非常特殊的“伪”组。它的目的是处理那些**在本帧的Tick过程中被动态生成(Spawn)出来的新Actor**。 -- **工作机制:** 假设在 `TG_PrePhysics` 阶段,一个Actor A 的 Tick 函数 `Spawn` 了一个新的 Actor B。Actor B 错过了本帧的 `TG_PrePhysics` 调度。为了让 B 也能在**当前帧**就被更新(而不是等到下一帧),`FTickTaskManager` 在 `TG_PrePhysics` 执行完后,会立即检查并执行所有新生成的、属于`TG_PrePhysics` 的对象的Tick。这个过程会在每个主Tick组后都重复一次。 -- **设计原因:** 解决了“帧内生成,帧内更新”的问题,使得动态生成的对象能够更及时地融入游戏世界,避免了一帧的延迟。 - ---- - -**总结**: - -ETickingGroup 是 UE 对一帧游戏时间的**“时间切片”**。通过将 Tick 任务强制归入这些有序的“切片”中,FTickTaskManager 实现了对整个世界模拟流程的精确控制,从根本上保证了复杂系统中数据流的正确性和逻辑的稳定性。这是 UE 高性能和高保真模拟能力的架构基石。 - -![ETickingGroup 流程图](attachment:c79803ca-b0d3-4f5f-bbae-c6c9b68a267c:Editor___Mermaid_Chart-2025-06-11-074346.png) - -**ETickingGroup 流程图** - -## 2.2 核心类 - -### 2.2.1 FTickTaskManagerInterface - -> **源码文件**:`Source\\Runtime\\Engine\\Public\\TickTaskManagerInterface.h` - -```cpp -/** - * Interface for the tick task manager - **/ -class FTickTaskManagerInterface -{ -public: - virtual ~FTickTaskManagerInterface() - { - } - - /** Allocate a new ticking structure for a ULevel **/ - virtual FTickTaskLevel* AllocateTickTaskLevel() = 0; - - /** Free a ticking structure for a ULevel **/ - virtual void FreeTickTaskLevel(FTickTaskLevel* TickTaskLevel) = 0; - - /** - * Queue all of the ticks for a frame - * - * @param World - World currently ticking - * @param DeltaSeconds - time in seconds since last tick - * @param TickType - type of tick (viewports only, time only, etc) - */ - virtual void StartFrame(UWorld* InWorld, float DeltaSeconds, ELevelTick TickType, const TArray& LevelsToTick) = 0; - - /** - * Run all of the ticks for a pause frame synchronously on the game thread. - * The capability of pause ticks are very limited. There are no dependencies or ordering or tick groups. - * @param World - World currently ticking - * @param DeltaSeconds - time in seconds since last tick - * @param TickType - type of tick (viewports only, time only, etc) - */ - virtual void RunPauseFrame(UWorld* InWorld, float DeltaSeconds, ELevelTick TickType, const TArray& LevelsToTick) = 0; - - /** - * Run a tick group, ticking all actors and components - * @param Group - Ticking group to run - * @param bBlockTillComplete - if true, do not return until all ticks are complete - */ - virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete ) = 0; - - /** Finish a frame of ticks **/ - virtual void EndFrame() = 0; - - /** Dumps all registered tick functions to output device. */ - virtual void DumpAllTickFunctions(FOutputDevice& Ar, UWorld* InWorld, bool bEnabled, bool bDisabled, bool bGrouped) = 0; - - /** Returns a map of enabled ticks, grouped by 'diagnostic context' string, along with count of enabled ticks */ - virtual void GetEnabledTickFunctionCounts(UWorld* InWorld, TSortedMap& TickContextToCountMap, int32& EnabledCount, bool bDetailed, bool bFilterCoolingDown=false) = 0; - - /** - * Singleton to retrieve the GLOBAL tick task manager - * - * @return Reference to the global cache tick task manager - */ - static ENGINE_API FTickTaskManagerInterface& Get(); - -}; -``` - -由以上源码可知,`FTickTaskManagerInterface` 是一个**纯抽象基类**。它定义了全局唯一的 Tick 任务管理器(`FTickTaskManager`)所必须提供的一系列公共服务和功能。 - -它的所有核心方法都被声明为**纯虚函数 (`= 0`)**,这意味着这个接口本身不能被实例化,任何想要成为一个有效的 Tick Task Manager 的类,都**必须**继承自这个接口并实现其所有纯虚函数。在UE中,这个唯一的实现者就是我们后面会分析的 `class FTickTaskManager`。 - -这种设计是**接口与实现分离**设计模式的典型应用,它允许引擎的其他部分(主要是`UWorld`)依赖于一个稳定的、抽象的接口,而无需关心其底层的具体实现可能会如何变化。 - -我们可以将 `FTickTaskManagerInterface` 中的接口函数按照其在 Tick 生命周期中的作用进行分组,下面我们分组进行分析。 - ---- - -1. **生命周期管理函数** - -这组函数构成了 `FTickTaskManager` 在一帧内的主要工作流程,由 `UWorld::Tick` 严格按顺序调用。 - -```cpp -// 在一帧的Tick开始时被调用,用于收集和规划所有任务。 -virtual void StartFrame(...) = 0; - -// 在一帧的Tick结束时被调用,用于清理和重置状态。 -virtual void EndFrame() = 0; - -// 【关键】执行一个指定Tick Group中的所有任务。 -// UWorld::Tick 会为每个ETickingGroup(如TG_PrePhysics)调用一次这个函数。 -virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete) = 0; -``` - -**分析:** - -`StartFrame`, `RunTickGroup`, `EndFrame` 共同定义了 Tick 管理器的**三段式工作模型**。这是 `UWorld` 与 `FTickTaskManager` 交互的主干道。`RunTickGroup` 中的 `bBlockTillComplete` 参数更是暴露了其支持同步/异步执行模式的能力,这是实现高性能并发调度的关键接口。 - ---- - -1. **特殊帧处理函数** - -```cpp -// 在游戏暂停时,执行一个简化的、同步的Tick流程。 -// 暂停时的Tick功能受限,没有依赖、排序或分组。 -virtual void RunPauseFrame(...) = 0; -``` - -**分析:** 这个函数提供了一种处理“游戏暂停”这一特殊状态的专用路径。它将暂停时的 Tick 逻辑与正常的游戏 Tick 逻辑分离开来,使得两种情况的处理都更加清晰。 - ---- - -1. **Tick 任务结构管理函数** - -这组函数负责管理与 `ULevel` 绑定的底层数据结构。 - -```cpp -// 为一个ULevel分配一个新的、用于存储其Tick任务的数据结构。 -virtual FTickTaskLevel* AllocateTickTaskLevel() = 0; - -// 释放一个ULevel不再使用的Tick任务数据结构。 -virtual void FreeTickTaskLevel(FTickTaskLevel* TickTaskLevel) = 0; -``` - -**分析:** 这两个函数揭示了 `FTickTaskManager` 内部是**以 Level 为单位**来组织 Tick 任务的。当一个`ULevel`被加载到世界中时,`UWorld`会调用`AllocateTickTaskLevel`来为它创建一个“账本”(`FTickTaskLevel`),用于记录该关卡内所有 Actor 和 Component 的 Tick 任务。当 Level 被卸载时,则调用`FreeTickTaskLevel`来销毁这个“账本”。这与 UE 的关卡流送(Level Streaming)机制紧密相关。 - ---- - -1. **调试与分析函数** - -这组函数主要用于开发和调试,提供了查询和输出 Tick 系统内部状态的能力。 - -```cpp -// 将所有已注册的Tick函数的信息(启用/禁用、分组等)转储到输出设备(如日志文件)。 -virtual void DumpAllTickFunctions(...) = 0; - -// 获取当前已启用的Tick函数的统计信息,按上下文(通常是类名)分组计数。 -// 这是Unreal Insights等性能分析工具获取数据的重要来源。 -virtual void GetEnabledTickFunctionCounts(...) = 0; -``` - -**分析:** 这些接口的存在表明,`FTickTaskManager` 在设计之初就充分考虑到了**可观测性**和**可调试性**。开发者可以通过这些工具,清晰地看到当前有哪些 Tick 在运行,它们的分组是什么,以及各类 Tick 的数量,这对于性能优化和问题排查至关重要。 - ---- - -1. **单例访问器** - -```cpp -// 全局静态函数,用于获取唯一的、全局的FTickTaskManagerInterface实例。 -static ENGINE_API FTickTaskManagerInterface& Get(); -``` - -**分析:** 这是典型的**单例模式**。整个引擎中只有一个 `FTickTaskManager` 实例,任何需要与它交互的代码(如`UWorld`, `AActor`等)都通过这个静态 `Get()` 方法来获取对它的引用。 - ---- - -**总结**: - -`FTickTaskManagerInterface` 如同一份清晰的“产品说明书”,它通过一系列纯虚函数,向整个引擎精确地定义了 Tick 调度系统所能提供的服务: - -- **核心服务:** 提供了一套完整的 `StartFrame` -> `RunTickGroup` -> `EndFrame` 的生命周期管理。 -- **组织方式:** 明确了其内部是以 `Level` 为单位来管理 Tick 任务的。 -- **特殊处理:** 定义了处理暂停帧的专门逻辑。 -- **可观测性:** 提供了强大的调试和性能分析接口。 -- **访问模式:** 规定了其作为一个全局单例的存在形式。 - -通过分析这个接口,我们无需深入其复杂的实现细节,就能从一个很高的层面理解`FTickTaskManager`的**设计目标、功能边界和核心职责**。 - -![FTickTaskManagerInterface 类图](attachment:4183eddf-e8df-42fa-b083-8b06068e7bcf:image.png) - -FTickTaskManagerInterface 类图 - -### 2.2.2 FTickTaskManager - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp` - -`FTickTaskManager` 是 `FTickTaskManagerInterface` 的**唯一实现**。它是一个全局单例,负责接收 `UWorld` 的指令,并将海量的、无序的 `FTickFunction` 组织成一个有序的、部分可并行的执行计划,然后提交给底层的 `Task Graph` 系统去执行。 - ---- - -1. **核心成员变量** - -```cpp -// 对一个更底层的、专门负责任务排序和依赖解析的辅助类的引用。 -FTickTaskSequencer& TickTaskSequencer; - -// 本帧需要进行Tick的所有Level的FTickTaskLevel的列表。 -TArray LevelList; - -// 存储本帧Tick的上下文信息,如World, DeltaSeconds, TickType等。 -FTickContext Context; - -// 一个关键的状态标志。当为true时,表示正处于一帧的Tick流程中, -// 此时任何新注册的Tick函数都需要被特殊处理(加入到新生成队列)。 -bool bTickNewlySpawned; -``` - -**分析:** - -`TickTaskSequencer` 是一个非常重要的内部辅助类,`FTickTaskManager` 将大量的复杂排序和依赖解析工作都委托给了它。`LevelList` 表明了其以`Level`为单位的管理模式。`Context` 作为一个上下文结构体,方便地将本帧的所有共享信息传递给各个子系统。`bTickNewlySpawned` 则是控制“帧内生成,帧内更新”机制的总开关。 - ---- - -1. **生命周期函数** - -这是 `UWorld::Tick` 驱动 `FTickTaskManager` 工作的主流程。 - - - ---- - - - ---- - - - ---- - -1. **Tick 函数注册/注销接口** - -```cpp -void AddTickFunction(ULevel* InLevel, FTickFunction* TickFunction) -{ - // ... - FTickTaskLevel* Level = TickTaskLevelForLevel(InLevel); // 找到或创建Level对应的TickTaskLevel - Level->AddTickFunction(TickFunction); // 在Level的“账本”中添加这个Tick函数 - // ... -} - -void RemoveTickFunction(FTickFunction* TickFunction) -{ - // ... - FTickTaskLevel* Level = TickFunction->InternalData->TickTaskLevel; // 从TickFunction自身找到它的“账本” - Level->RemoveTickFunction(TickFunction); // 从“账本”中移除 -} -``` - -**分析:** - -`AActor` 和 `UActorComponent` 在创建和销毁时调用的就是这两个函数。它们清晰地展示了 `FTickTaskManager` 是如何通过 `FTickTaskLevel` 来**按 Level 组织**所有 Tick 函数的。每个 `FTickFunction` 也通过其 `InternalData` 反向持有一个指向其所属 `FTickTaskLevel` 的指针,形成了一个双向链接,方便快速地添加和移除。 - ---- - -**总结**: - -![FTickTaskManager 工作流程](attachment:6f9c6260-4c36-4e42-b495-449f11e15aef:Editor___Mermaid_Chart-2025-06-11-100647.png) - -FTickTaskManager 工作流程 - -`FTickTaskManager` 的实现是一个典型的**分层委托**架构: - -- 它作为**高层管理者**,定义了 `StartFrame` -> `RunTickGroup` -> `EndFrame` 的宏观流程。 -- 它将**复杂的任务收集和排序逻辑**,委托给了内部的 `FTickTaskLevel` 和 `FTickTaskSequencer`。 -- 它将**最终的并行执行**,委托给了 `TickTaskSequencer` 背后的 `Task Graph` 系统。 - -通过这种层层委托,`FTickTaskManager` 以一种相对清晰、高内聚的方式,精心安排了整个极其复杂的 Tick 调度过程。 - -### 2.2.3 FTickTaskSequencer - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp` - -`FTickTaskSequencer` 是一个全局单例,是`FTickTaskManager`的**核心工作委托对象**。它的名字“Sequencer”(序列器)精准地描述了其核心职责:将一堆无序的、带有依赖关系的 Tick 任务,组织成一个可以被`Task Graph`系统高效执行的**任务序列**。 - ---- - -1. **核心成员变量** - -```cpp -// 存储用于批处理(Batching)的Tick任务信息 -TArray< TPair > > TickBatches; - -// 【关键】为每个Tick组(TG_MAX个)存储一个FGraphEventRef数组。 -// 当一个Tick任务被创建时,它的完成事件(Completion Event)会被添加到这个数组中。 -TArrayWithThreadsafeAdd > TickCompletionEvents[TG_MAX]; - -// 【关键】一个二维数组,用于存储所有Tick任务。 -// 第一个维度是任务的开始Tick组,第二个维度是结束Tick组。 -// 分为高优先级和普通优先级。 -TArrayWithThreadsafeAdd HiPriTickTasks[TG_MAX][TG_MAX]; -TArrayWithThreadsafeAdd TickTasks[TG_MAX][TG_MAX]; - -// 用于存储需要在帧末尾完成的清理任务。 -FGraphEventArray CleanupTasks; - -// 记录上一个被阻塞等待的Tick组,用于确定下一次需要等待哪些组。 -ETickingGroup WaitForTickGroup; - -// 一系列控制行为的布尔开关,由CVar控制。 -bool bAllowConcurrentTicks; // 是否允许并行Tick -bool bAllowBatchedTicksForFrame; // 是否允许Tick批处理 -// ... -``` - -**分析:** - -- **`TickCompletionEvents`** 和 **`TickTasks` / `HiPriTickTasks`** 是这个类的**核心数据容器**。 -- `TickTasks[StartGroup][EndGroup]` 这种二维数组的设计非常精巧。它使得`Sequencer`可以快速地根据**开始组**来分发任务,并根据**结束组**来收集它们的完成事件。 -- `TArrayWithThreadsafeAdd` 的使用,暗示了这些数组可以在**多个线程中被安全地添加元素**,这对于并行化的`StartFrame`流程至关重要。 -- `TickBatches` 则是一种性能优化,它尝试将属性相同(比如都在同一个 Tick 组、都没有依赖)的多个小 Tick 任务,打包成一个大的`Task Graph`任务来执行,以减少任务调度的开销。 - ---- - -1. **核心函数** - - - - - - - - - ---- - -**总结**: - -![FTickTaskSequencer 工作流程](attachment:5a98047a-d98f-4e09-b89f-a4d15db8ba90:Editor___Mermaid_Chart-2025-06-11-125137.png) - -FTickTaskSequencer 工作流程 - -`FTickTaskSequencer` 是一个高度优化的、与底层 `Task Graph` 系统紧密耦合的精密调度器。 - -- 它使用**二维数组**来高效地组织和索引海量的 Tick 任务,实现了按“开始组”和“结束组”的快速访问。 -- 它的 `Queue` 系列函数负责将上层的 `FTickFunction` **翻译**成底层的 `Task Graph` 任务。 -- 它的 `ReleaseTickGroup` 函数通过“分发”(Dispatch)和“等待”(Wait)两个步骤,精确地控制了**任务的并发执行和必要的同步**。 - -可以说,`FTickTaskManager` 是“战略层”,而 `FTickTaskSequencer` 则是“战术层”。它处理了所有最棘手的并发、同步和性能优化细节,是 UE Tick 机制能够高性能运行的真正幕后功臣。 - -### 2.2.4 FTickTaskLevel - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp` - -`FTickTaskLevel` 可以被理解为 **一个 `ULevel` 中所有 `FTickFunction` 的“账本”或“花名册”**。它的核心职责是**管理隶属于单个 `ULevel` 的所有 Tick 任务**,并对它们进行分类、预处理,为上层的 `FTickTaskManager` 提供干净、有序的数据。 - -每一个`ULevel`对象内部都有一个指向`FTickTaskLevel`实例的指针(`Level->TickTaskLevel`),这构成了`FTickTaskManager`按 Level 组织 Tick 任务的基础。 - ---- - -1. **核心成员变量** - -这是 `FTickTaskLevel` 用来分类和管理 `FTickFunction` 的内部容器。 - -```cpp -// 对全局Sequencer的引用,方便直接调用 -FTickTaskSequencer& TickTaskSequencer; - -// 【关键】存储所有明确启用的、需要每帧Tick的函数。TSet保证了不重复。 -TSet AllEnabledTickFunctions; - -// 【关键】一个自定义的链表,用于存储所有设置了TickInterval且当前处于“冷却”状态的函数。 -FCoolingDownTickFunctionList AllCoolingDownTickFunctions; - -// 存储所有明确禁用的Tick函数。 -TSet AllDisabledTickFunctions; - -// 临时数组,用于存放本帧结束后需要重新计算冷却时间的Tick函数。 -TArrayWithThreadsafeAdd TickFunctionsToReschedule; - -// 存储在本帧Tick期间新生成的Tick函数。 -TSet NewlySpawnedTickFunctions; - -// 当前帧的Tick上下文 -FTickContext Context; - -// 是否处于Tick流程中的标志 -bool bTickNewlySpawned; -``` - -**分析:** - -- `FTickTaskLevel` 的核心就是这**四大容器**:`AllEnabledTickFunctions`, `AllCoolingDownTickFunctions`, `AllDisabledTickFunctions`, 和 `NewlySpawnedTickFunctions`。它将一个 Level 内的所有 Tick 函数分门别类地进行管理。 -- **`AllCoolingDownTickFunctions`** 的链表结构设计非常巧妙。因为它需要处理基于`DeltaTime`的动态激活,链表结构比数组或集合更适合进行高效的遍历和中间节点的移除。 -- `TickFunctionsToReschedule` 和 `NewlySpawnedTickFunctions` 是**临时性**的容器,用于处理帧内的状态变化,体现了其逻辑的严谨性。 - ---- - -1. **核心函数**: - - - - - - - - - ---- - -**总结**: - -![FTickTaskLevel 数据组织与工作流](attachment:7280ffcf-e8ba-4fb7-a182-3a9f9fd1b764:Editor___Mermaid_Chart-2025-06-11-132305.png) - -FTickTaskLevel 数据组织与工作流 - -`FTickTaskLevel` 是一个**面向单个`ULevel`的、高度特化的 Tick 任务管理器**。它的存在体现了 UE 设计的**分而治之**思想。 - -- **数据组织者:** 它将一个`ULevel`中成百上千的 Tick 函数,通过`TSet`和自定义链表,分门别类地管理起来,极大地降低了上层`FTickTaskManager`的管理复杂度。 -- **预处理器:** 它在`StartFrame`阶段就独立处理完了复杂的`TickInterval`逻辑,为主调度器提供了干净、明确的“待办事项列表”。 -- **执行委托者:** 它通过调用`FTickFunction::QueueTickFunction`,将最终的排队任务委托出去,自身不直接与`FTickTaskSequencer`深度耦合。 - -通过引入`FTickTaskLevel`这一中间层,UE 的 Tick 系统架构变得更加清晰、模块化,并且能够与关卡流送系统完美协同工作。 - -## 2.3 调用上下文 - -### 2.3.1 UWorld::Tick - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\LevelTick.cpp` - -`UWorld::Tick` 的源码虽然冗长,但我们只需要聚焦于它与 `FTickTaskManager` 直接交互的**三个关键阶段**,就能清晰地看到 `FTickTaskManager` 是如何被驱动的。 - ---- - -1. **第一阶段:`StartFrame` - 规划与准备** - -在 `UWorld::Tick` 的核心循环中,当确定了本帧需要进行 Actor Tick (`bDoingActorTicks` 为 `true`) 之后,第一件重要的事情就是调用 `FTickTaskManager` 的 `StartFrame` 方法。 - -```cpp -// in UWorld::Tick, inside the main for-loop over LevelCollections - -if (bDoingActorTicks) -{ - // ... - TickGroup = TG_PrePhysics; // 重置世界的当前Tick组 - - // 【关键调用 1】通知TickTaskManager开始新的一帧 - FTickTaskManagerInterface::Get().StartFrame(this, DeltaSeconds, TickType, LevelsToTick); - - // ... 后续的RunTickGroup调用 ... -} -``` - -**上下文剖析:** - -1. **时机:** 这个调用发生在所有 `RunTickGroup` **之前**。 -2. **传递的参数:** - - `this` (`UWorld*`): 告诉 `FTickTaskManager` 当前工作的世界是哪一个。 - - `DeltaSeconds`, `TickType`: 传递本帧的时间和Tick类型上下文。 - - `LevelsToTick`: 一个 `TArray`,明确告知 `FTickTaskManager` **本帧只需要考虑这些 `ULevel` 中的 Tick 任务**。这对于关卡流送至关重要,它避免了对已卸载或隐藏的 Level 进行不必要的工作。 -3. **作用:** `UWorld` 在这里扮演了**“信息提供者”**和**“启动者”**的角色。它将所有必要的上下文信息打包好,然后按下 `FTickTaskManager` 的“启动按钮”。`StartFrame` 接收到指令后,会执行我们之前分析过的内部逻辑:收集所有相关Level的Tick函数,并构建本帧的任务依赖图。 - ---- - -1. **第二阶段:`RunTickGroup` - 按部就班地执行** - -在 `StartFrame` 完成规划后,`UWorld::Tick` 进入了一个高度结构化的执行阶段,它像一个严谨的工序流程单,依次调用 `RunTickGroup`。 - -```cpp -// in UWorld::Tick, right after StartFrame - -// 【关键调用 2】按严格顺序执行每个Tick组 -{ SCOPE_CYCLE_COUNTER(STAT_TG_PrePhysics); RunTickGroup(TG_PrePhysics); } -EnsureCollisionTreeIsBuilt(); -{ SCOPE_CYCLE_COUNTER(STAT_TG_StartPhysics); RunTickGroup(TG_StartPhysics); } -{ SCOPE_CYCLE_COUNTER(STAT_TG_DuringPhysics); RunTickGroup(TG_DuringPhysics, false); } -// ... -{ SCOPE_CYCLE_COUNTER(STAT_TG_EndPhysics); RunTickGroup(TG_EndPhysics); } -{ SCOPE_CYCLE_COUNTER(STAT_TG_PostPhysics); RunTickGroup(TG_PostPhysics); } -// ... -{ SCOPE_CYCLE_COUNTER(STAT_TG_PostUpdateWork); RunTickGroup(TG_PostUpdateWork); } -{ SCOPE_CYCLE_COUNTER(STAT_TG_LastDemotable); RunTickGroup(TG_LastDemotable); } -``` - -**上下文剖析:** - -1. **严格的顺序:** `UWorld` **硬编码**了 `RunTickGroup` 的调用顺序,从 `TG_PrePhysics` 一直到 `TG_LastDemotable`。这从根本上保证了不同阶段任务的执行顺序,是整个 Tick 依赖系统能够正常工作的基础。 -2. **阻塞与非阻塞:** - - 大部分 `RunTickGroup` 调用(如 `TG_PrePhysics`, `TG_EndPhysics`)都使用了默认的 `bBlockTillComplete = true` 参数。这意味着 `UWorld` 的执行流会在这里**暂停**,直到该 Tick 组的所有任务完成。这是实现**同步点**的关键。 - - `RunTickGroup(TG_DuringPhysics, false)` 是一个特例。`false` 参数告诉 `UWorld` **不要等待**物理模拟完成。这使得游戏主线程可以在物理计算在后台线程进行的同时,继续执行其他工作,从而实现了**并发**,提升了性能。 -3. **作用:** `UWorld` 在这里扮演了**“工序调度员”**的角色。它不关心每个`TickGroup`内部具体有哪些任务,只负责按照预设的蓝图,一步步地命令 `FTickTaskManager`去执行每一个工序。 - ---- - -1. **第三阶段:`EndFrame` - 清理与收尾** - -在所有 `RunTickGroup` 都执行完毕后,`UWorld::Tick` 会调用 `FTickTaskManager` 的 `EndFrame` 来结束本帧的 Tick 调度。 - -```cpp -// in UWorld::Tick, after all RunTickGroup calls - -if (bDoingActorTicks) -{ - // ... - // 【关键调用 3】通知TickTaskManager本帧的Tick调度工作全部结束 - FTickTaskManagerInterface::Get().EndFrame(); -} -``` - -**上下文剖析:** - -1. **时机:** 这个调用发生在所有 `RunTickGroup` **之后**。 -2. **作用:** `UWorld` 在这里发出一个**“收工”**的信号。`FTickTaskManager` 接收到后,会执行其内部的清理逻辑,如重置状态、清空临时容器等,为下一帧做好准备。 - ---- - -**总结**: - -![UWorld::Tick 与 FTickTaskManager 交互时序图](attachment:de5ca54d-82ca-4c26-bf55-01f5e0c0f00c:Editor___Mermaid_Chart-2025-06-11-133444.png) - -UWorld::Tick 与 FTickTaskManager 交互时序图 - -从 `UWorld::Tick` 这个调用上下文的视角来看,`FTickTaskManager` 并非一个自我驱动的系统,而是一个**被动响应的、服务于 `UWorld` 的高级工具**。 - -- `UWorld` 负责**定义“做什么”和“按什么顺序做”**(提供 LevelsToTick,按序调用 RunTickGroup)。 -- `FTickTaskManager` 负责**解决“如何高效、正确地做”**(收集任务、解析依赖、并行执行)。 - -这种清晰的**职责分离**是 UE 架构设计的精髓。`UWorld` 掌握着宏观的、游戏逻辑驱动的流程控制,而 `FTickTaskManager` 则封装了所有复杂的、与具体游戏逻辑无关的底层调度技术。 - -### 2.3.2 AActor - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\Actor.cpp` - -`AActor` 是整个 Tick 调度系统最主要的服务对象。引擎为`AActor`提供了一套封装良好、易于使用的 API,让游戏开发者可以在不了解`FTickTaskManager`复杂实现的情况下,方便地控制 Actor 的 Tick 行为。 - ---- - -1. **注册与注销:`RegisterAllActorTickFunctions`** - - **函数:** `void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)` - - - **调用时机:** - - - `bRegister = true`: 当Actor被创建并添加到世界时(如通过 `SpawnActor` 或关卡加载)。 - - `bRegister = false`: 当Actor被销毁时。 - - **源码剖析:** - - ```cpp - void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents) - { - // ... - // 防止重复注册/注销 - if (bTickFunctionsRegistered != bRegister) - { - // 【关键】调用RegisterActorTickFunctions来处理自身的PrimaryActorTick - RegisterActorTickFunctions(bRegister); - bTickFunctionsRegistered = bRegister; - // ... - } - - if (bDoComponents) - { - // 递归地为所有子组件也执行注册/注销 - for (UActorComponent* Component : GetComponents()) - { - if (Component) - { - Component->RegisterAllComponentTickFunctions(bRegister); - } - } - } - // ... - } - ``` - - ```cpp - void AActor::RegisterActorTickFunctions(bool bRegister) - { - if(bRegister) - { - if(PrimaryActorTick.bCanEverTick) - { - // 1. 设置Target,让FActorTickFunction知道要Tick哪个Actor - PrimaryActorTick.Target = this; - // 2. 设置初始启用状态 - PrimaryActorTick.SetTickFunctionEnable(...); - // 3. 【核心调用】将自身的PrimaryActorTick注册到其所在Level的FTickTaskLevel中 - PrimaryActorTick.RegisterTickFunction(GetLevel()); - } - } - else - { - if(PrimaryActorTick.IsTickFunctionRegistered()) - { - // 【核心调用】从管理器中注销 - PrimaryActorTick.UnRegisterTickFunction(); - } - } - } - ``` - - - **上下文剖析:** - - - 这是`AActor`与`FTickTaskManager`的**第一次握手**。当一个 Actor 诞生时,`RegisterAllActorTickFunctions`会被调用。 - - 它首先通过调用`RegisterActorTickFunctions`,将自己的`PrimaryActorTick`(一个`FActorTickFunction`实例)注册到调度系统中。 - - `PrimaryActorTick.RegisterTickFunction(GetLevel())` 这个调用最终会触发 `FTickTaskManager::AddTickFunction`,将这个Tick任务添加到正确的`FTickTaskLevel`中。 - - 同时,它还会递归地为所有子组件执行相同的操作,确保整个Actor及其所有部分的Tick都能被正确管理。 - ---- - -1. **执行 Tick 逻辑:从`ExecuteTick`到`AActor::Tick`** - - **函数:** `void FActorTickFunction::ExecuteTick(...)` - - - **调用时机:** 当`FTickTaskSequencer`通过`Task Graph`执行这个`FActorTickFunction`对应的任务时。 - - - **源码剖析:**`AActor::TickActor`是一个内部函数,它会进一步调用我们熟悉的`AActor::Tick`。 - - ```cpp - void FActorTickFunction::ExecuteTick(float DeltaTime, ...) - { - if (IsValid(Target)) // Target就是之前注册时设置的AActor* - { - // ... - // 【关键】调用Actor自身的TickActor方法 - Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this); - } - } - ``` - - ```cpp - void AActor::Tick( float DeltaSeconds ) - { - // 如果是蓝图Actor,调用其蓝图事件图中的Tick事件 - if (/* 是蓝图 */) - { - ReceiveTick(DeltaSeconds); - } - // ... - // 处理该Actor的Latent Actions - LatentActionManager.ProcessLatentActions(this, ...); - } - ``` - - - **上下文剖析:** - - - 这里展示了**从底层调度到上层逻辑的完整调用链**。 - - `FTickTaskSequencer` 并不知道`AActor`的存在,它只知道执行一个`FTickFunction`的`ExecuteTick`虚函数。 - - `FActorTickFunction` 作为派生类,在`ExecuteTick`的实现中,调用了其`Target`(即`AActor`实例)的`Tick`方法。 - - 最终,控制权传递到了我们游戏开发者所编写的C++ `Tick`函数或蓝图的`Event Tick`节点。这是一个典型的**策略模式**和**多态**的应用。 - ---- - -1. **动态控制 API:`SetActorTickEnabled`及其伙伴** - - **函数:** `SetActorTickEnabled`, `SetActorTickInterval`, `SetTickGroup`, `AddTickPrerequisiteActor`等。 - - - **调用时机:** 在游戏运行时的任何时候,由开发者根据逻辑需要调用。 - - - **源码剖析:** - - ```cpp - void AActor::SetActorTickEnabled(bool bEnabled) - { - // 直接调用其PrimaryActorTick的API - PrimaryActorTick.SetTickFunctionEnable(bEnabled); - } - - void AActor::AddTickPrerequisiteActor(AActor* PrerequisiteActor) - { - // 将对Actor的依赖,翻译成对该Actor的PrimaryActorTick的依赖 - PrimaryActorTick.AddPrerequisite(PrerequisiteActor, PrerequisiteActor->PrimaryActorTick); - } - ``` - - - **上下文剖析:** - - - `AActor` 提供的这一整套API,本质上都是对其内部成员`PrimaryActorTick`的**简单封装**。 - - 这种设计非常优雅。它为上层开发者提供了简洁、易于理解的接口(`SetActorTickEnabled`),同时将所有与底层调度系统交互的复杂性都**隐藏**在了`FTickFunction`的实现内部。 - - 开发者只需要与`AActor`打交道,而无需关心`FTickFunction`、`FTickTaskManager`这些底层细节,这大大降低了使用的门槛。 - ---- - -**总结**: - -![AActor 与 Tick 系统的交互](attachment:77dae94b-ae07-434b-8118-2e4366fb884a:Editor___Mermaid_Chart-2025-06-11-140839.png) - -AActor 与 Tick 系统的交互 - -`AActor` 作为 Tick 系统的主要**“客户端”**,通过以下方式与调度系统交互: - -1. **生命周期绑定:** 在创建和销毁时,通过`RegisterAllActorTickFunctions`来**订阅/退订** Tick 服务。 -2. **执行回调:** 通过`FActorTickFunction`这个**适配器(Adapter)**,让底层的`ExecuteTick`调用能够最终触发到上层的`AActor::Tick`逻辑。 -3. **接口封装:** 提供了一系列简洁的 API,将对`FTickFunction`属性的修改**封装**起来,为开发者提供了便利。 - -### 2.3.3 UActorComponent - -> **源码文件**:`Source\\Runtime\\Engine\\Private\\Components\\ActorComponent.cpp` - -`UActorComponent` 作为 Actor 功能的载体,同样需要与底层的 Tick 调度系统进行交互。它的实现方式与`AActor`如出一辙,都是通过**封装**其内部的`FTickFunction`派生实例(`FActorComponentTickFunction`)来提供 API。 - ---- - -1. **注册与注销:`RegisterAllComponentTickFunctions`** - - **函数:** `void UActorComponent::RegisterAllComponentTickFunctions(bool bRegister)` - - - **调用时机:** 这个函数通常由其所属的`AActor`在`RegisterAllActorTickFunctions`中递归调用。 - - - **源码剖析:** - - ```cpp - void UActorComponent::RegisterAllComponentTickFunctions(bool bRegister) - { - // 只有当组件已经注册到世界后,才能注册其Tick函数 - if (bRegistered) - { - // ... 防止重复注册/注销 ... - if (bTickFunctionsRegistered != bRegister) - { - // 【关键】调用RegisterComponentTickFunctions来处理自身的PrimaryComponentTick - RegisterComponentTickFunctions(bRegister); - bTickFunctionsRegistered = bRegister; - // ... - } - } - } - ``` - - ```cpp - void UActorComponent::RegisterComponentTickFunctions(bool bRegister) - { - if(bRegister) - { - // 【核心调用】SetupActorComponentTickFunction是注册逻辑的核心 - if (SetupActorComponentTickFunction(&PrimaryComponentTick)) - { - // 设置Target,让FPrimaryComponentTickFunction知道要Tick哪个Component - PrimaryComponentTick.Target = this; - } - } - else - { - // ... 注销逻辑 ... - } - } - ``` - - ```cpp - bool UActorComponent::SetupActorComponentTickFunction(struct FTickFunction* TickFunction) - { - if(TickFunction->bCanEverTick && !IsTemplate()) - { - // ... - // 【核心调用】最终还是调用FTickFunction的RegisterTickFunction - TickFunction->RegisterTickFunction(ComponentLevel); - return true; - } - return false; - } - ``` - - - **上下文剖析:** - - - 流程与`AActor`高度相似:`RegisterAll...` -> `Register...` -> `Setup...` -> `FTickFunction::RegisterTickFunction`。 - - 一个重要的区别是,Component 的 Tick 注册有一个前提条件:`if (bRegistered)`,即**组件必须先附加到 Actor 并注册到世界中**,然后才能注册它的 Tick。这体现了 Component 对 Actor 的从属关系。 - - 最终,它同样是通过调用`FTickFunction`的公共 API `RegisterTickFunction`,将自己注册到`FTickTaskManager`中。 - ---- - -1. **执行 Tick 逻辑:从`ExecuteTick`到`UActorComponent::TickComponent`** - - **函数:** `void FActorComponentTickFunction::ExecuteTick(...)` - - - **调用时机:** 当`FTickTaskSequencer`执行这个`FPrimaryComponentTickFunction`对应的任务时。 - - - **源码剖析:** - - ```cpp - void FActorComponentTickFunction::ExecuteTick(float DeltaTime, ...) - { - // ... - // 【关键】ExecuteTickHelper是一个辅助函数,最终会调用下面的Lambda - ExecuteTickHelper(Target, ..., [this, TickType](float DilatedTime) - { - // 在Lambda中,调用Component自身的TickComponent方法 - Target->TickComponent(DilatedTime, TickType, this); - }); - } - ``` - - ```cpp - void UActorComponent::TickComponent(float DeltaTime, ...) - { - // ... - // 如果是蓝图组件,调用其蓝图事件图中的Tick事件 - if (/* 是蓝图 */) - { - ReceiveTick(DeltaTime); - } - // ... 处理Latent Actions ... - } - ``` - - - **上下文剖析:** - - - 这再次展示了从底层调度到上层逻辑的**回调机制**。 - - `FTickTaskSequencer` 调用 `FActorComponentTickFunction::ExecuteTick`。 - - `ExecuteTick` 内部通过其 `Target` 指针,最终调用到我们为组件编写的 `TickComponent` 函数或蓝图的 `Event Tick` 节点。 - - 这种设计模式的复用,使得 Actor 和 Component 在 Tick 执行层面遵循着完全一致的逻辑。 - ---- - -1. **动态控制 API:一系列`Set...`和`Add...`函数** - - **函数:** `SetComponentTickEnabled`, `SetTickGroup`, `AddTickPrerequisiteComponent`等。 - - - **调用时机:** 游戏运行时,由开发者调用。 - - - **源码剖析:** - - ```cpp - void UActorComponent::SetComponentTickEnabled(bool bEnabled) - { - // 直接调用其PrimaryComponentTick的API - PrimaryComponentTick.SetTickFunctionEnable(bEnabled); - } - - void UActorComponent::AddTickPrerequisiteComponent(UActorComponent* PrerequisiteComponent) - { - // 将对Component的依赖,翻译成对该Component的PrimaryComponentTick的依赖 - PrimaryComponentTick.AddPrerequisite(PrerequisiteComponent, PrerequisiteComponent->PrimaryComponentTick); - } - ``` - - - **上下文剖析:** - - - 与`AActor`的设计**完全一致**。所有这些上层 API 都是对内部成员`PrimaryComponentTick`(一个`FPrimaryComponentTickFunction`实例)的**简单、直接的封装**。 - - 这种一致性极大地降低了开发者的学习成本。一旦你学会了如何控制 Actor 的 Tick,你就自然而然地学会了如何控制 Component 的 Tick。 - ---- - -**结论**: - -![UActorComponent 与 Tick 系统的交互](attachment:f40fd509-be3a-4923-a41d-6cdaa7510b75:Editor___Mermaid_Chart-2025-06-11-142214.png) - -UActorComponent 与 Tick 系统的交互 - -通过剖析`UActorComponent`的上下文,我们可以得出结论: - -`UActorComponent`在与 Tick 系统交互方面,是**`AActor`设计模式的完美复刻**。它同样通过**生命周期绑定**、**执行回调**和**接口封装**这三大手段,实现了与底层`FTickTaskManager`的解耦和交互。 - -这种高度一致的设计,体现了 UE 在 API 设计上的**泛化和复用**思想。它将`Tick`的能力从一个宏观的`AActor`,下放到了更细粒度的、可组合的`UActorComponent`上,同时保持了接口的统一和简洁。这使得开发者可以像组装乐高积木一样,为 Actor 添加各种带有独立更新逻辑的功能模块,而无需关心它们底层的调度细节。 - -# 三、总结 - -经过对`FTickTaskManager`从宏观定位、核心数据结构,到内部实现,再到外部调用上下文的逐层剖析,我们已经相对完整地构建了 UE 核心 Tick 调度系统的全貌。 - -## 3.1 设计思想:FTickTaskManager 如何解决复杂性 - -`FTickTaskManager` 的核心设计,可以归结为对**“分层、委托与解耦”**这一软件工程黄金法则的极致应用。它通过以下三个层面的设计,成功地将一个极其复杂的调度问题分解为多个可控的子问题: - -1. **宏观分层(`UWorld` vs. `FTickTaskManager`):** `UWorld` 作为“战略层”,只负责定义“做什么”(提供Tick上下文)和“按什么顺序做”(按序调用`RunTickGroup`)。而`FTickTaskManager`作为“战术层”,则完全封装了“如何高效正确地做”的所有技术细节。这种职责分离,使得游戏逻辑与底层调度技术彻底解耦。 -2. **中观委托(`FTickTaskManager` vs. `FTickTaskSequencer`):** `FTickTaskManager` 自身也并非单体,它将更底层的、与`Task Graph`系统直接交互的任务排序、依赖解析和并发提交工作,进一步委托给了内部的`FTickTaskSequencer`。这使得`FTickTaskManager`可以更专注于高层的管理和接口封装,而`FTickTaskSequencer`则可以专注于性能极致的调度算法。 -3. **微观组织(`FTickTaskManager` vs. `FTickTaskLevel`):** `FTickTaskManager` 并没有采用一个巨大的全局列表来管理所有Tick任务,而是创造性地通过`FTickTaskLevel`,将任务**按`ULevel`进行分组**。这种“分而治之”的策略,不仅极大地提升了管理效率,更与 UE 的关卡流送(Level Streaming)机制完美契合,使得动态加载和卸载关卡时的 Tick 管理变得轻而易举。 - -## 3.2 性能考量:为何它比简单的循环更高效 - -`FTickTaskManager` 的高性能源于其对两大性能瓶颈的精准打击: - -1. **依赖解析与拓扑排序:** 通过`FTickPrerequisite`和对依赖图的构建,`FTickTaskManager`解决了任务间的执行顺序问题。它能够生成一个保证逻辑正确的**拓扑排序**,这是高性能调度的前提。 -2. **并发执行:** 它的真正威力在于,在保证了依赖关系的前提下,能够识别出依赖图中所有**可以并行执行**的任务(那些没有前置依赖的节点),并将它们打包成`FGraphEvent`,提交给底层的`Task Graph`系统。由`Task Graph`将这些任务分发到多个 CPU 核心上同时运行,从而将原本漫长的串行`for`循环,变成了一个高效的并行计算过程,极大地缩短了帧时间。 - -## 3.3 启示:如何更好地利用 Tick 系统 - -通过本次剖析,我们作为上层开发者可以得到以下重要启示: - -1. **善用`TickGroup`:** 理解并善用`TickGroup`是编写健壮、无抖动逻辑的第一步。 -2. **巧用`AddPrerequisite`:** 当 Actor 或 Component 之间存在明确的逻辑先后关系时,使用`AddTickPrerequisite`来明确声明这种依赖,而不是依赖于不确定的执行顺序或使用延迟等“魔法”手段。 -3. **为性能优化着想:** 对于那些不依赖游戏线程数据、纯计算密集型的 Tick 逻辑,可以考虑将其封装并尝试开启`bRunOnAnyThread`(需谨慎评估线程安全),以充分利用引擎的并行调度能力。 - -![FTickTaskManager 机制总结](attachment:3ef28401-8182-47c8-8d55-700873bf47b9:Mermaid_Chart_-_Create_complex_visual_diagrams_with_text._A_smarter_way_of_creating_diagrams.-2025-06-11-144606.png) - -FTickTaskManager 机制总结 - -总而言之,`FTickTaskManager` 是 UE 工程智慧的集中体现。它不仅仅是一个功能模块,更是一套解决大规模实时系统中任务调度问题的完整方案。 \ No newline at end of file diff --git a/3Projects/Unity/Log插件/Log插件.md b/3Projects/Unity/Log插件/Log插件.md deleted file mode 100644 index 6b537a6..0000000 --- a/3Projects/Unity/Log插件/Log插件.md +++ /dev/null @@ -1,153 +0,0 @@ - -source:[加Log就卡?不加Log就瞎?”——这个插件治好了我的精神内耗](https://mp.weixin.qq.com/s/Nii4fC15eKoNN8MOAzrmDQ) - ---- - - -# - -- 1 现有日志打印情况 - - -- 1 日志阻塞 - -- 2 从调试到生产,日志策略的抉择 - - -- 2 问题出现原因 - - -- 2.1 日志打印原理分析 - -- 2.2 log4j2 Disruptor 的初始化 - -- 2.3 队列满导致日志阻塞 - -- 2.4 产生的根本原因 - - -- 3 应对方案 - - -- 3.1 方案选择 - -- 3.2 技术选择 - -- 3.3 落地实现 - - -- 4 总结 - - - - -## 1 现有日志打印情况 - -  日志作为软件工程实践中的重要基础设施,在系统监控、异常诊断及行为追溯等关键环节发挥着不可替代的作用。Apache Log4j2作为当前主流的日志框架,凭借其模块化架构和高度可扩展的特性,为开发者提供了灵活的多维度日志管理方案。然而若未能深入理解其异步日志机制、缓冲区策略等核心原理,或存在配置参数与业务场景匹配度不足等问题,则可能导致日志I/O阻塞、内存资源过度消耗等负面效应,甚至引发严重的服务性能瓶颈。因此,在实际工程实践中需遵循科学合理的使用准则,通过日志分级管理、输出格式优化、滚动策略定制等手段,方能充分发挥其技术优势,有效规避潜在风险。 - -### 1 日志阻塞 - -  日志导致线程Block的问题,相信你或许已经遇到过,对此应该深有体会;或许你还没遇到过,但不代表没有问题,只是可能还没有触发而已。常见的现象是出现大量的block线程,查看jstack常常是下图现象:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHIR2NAfdDEgKBvNfWKcAd1jXHF2qRU1ibbNA8jIMaTs5hrlGibj02kiblg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -### 2 从调试到生产,日志策略的抉择 - -  在软件项目的全生命周期中,从开发阶段到生产环境的演进过程中,日志管理往往面临着微妙的平衡。开发阶段我们倾向于采用详尽的日志策略:业务接口的入参出参被完整记录,跨系统的调用链路被清晰标注,甚至非核心逻辑的辅助性信息也得以留存——这些详实的日志如同开发者的双目,为联调排障与功能验证提供了不可或缺的洞察。 - -  然而当服务迈向生产环境时,过度日志带来的问题便逐渐显现。冗余的调试信息不仅会影响系统性能,更可能淹没真正关键的业务轨迹。尽管我们尝试在上线前进行日志裁剪,但总存在令人踌躇的灰色地带:某些开发期辅助日志是否暗含未来的诊断价值?那些看似非核心的流程记录会否在某个异常场景下成为关键线索?这种取舍的困境,本质上反映了我们对系统可观测性与运行效能之间永续的权衡。 - -## 2 问题出现原因 - -### 2.1 日志打印原理分析 - -  在我们使用的log4j的应用中采用的是异步日志配置,简单的说明一下一条日志打印在log4j中的处理流程如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHwWGYn7OQfa7tNica1TerbgEjIP0VNJnNbpGlKdAvCic5Lbk0sTCNElTw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  简单点来说,就是多线程通过 log4j2 的门面类进行日志的打印,日志经过一系列的处理(过滤,包装)后放入到Disruptor的环形 buffer 中,在服务器的消费端会单启一个线程进行这些日志的消费,最终放入到我们指定的文件中。 - -### 2.2 log4j2 Disruptor 的初始化 - -  当LoggerContext启动时,所有AsyncLoggerConfig会通过start()方法初始化其Disruptor:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHvaPOfupg1o0Wnb2HpqzUewncx7ibHiczTLJdCq6Na2yopKYxZ3thu14w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  其中Disruptor 是一个环形 buffer,官方做了很多的性能优化,这里有兴趣的可以了解其实现原理,这里不进行深入的讨论,其中在我们的应用log4j.xml配置中,没对RingBuffer进行自定义的配置,使用的是默认的大小256K。 - -### 2.3 队列满导致日志阻塞 - -  Disruptor 的 RingBuffer 是一个固定大小的环形队列,其发布逻辑:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHiav0RIpxia6HI61Y86ohwoyxYkic8Zsiaj6CygPU5PEibrVuEMC7kialgKWg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -  队列满时的默认行为:AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=true,此时会等待着消费出下一个可以生产的环形 buffer 槽;此时所有打印日志的线程会尝试获取全局锁。此时会阻塞线程,也就是我们上述堆栈中看到的异常。 - -### 2.4 产生的根本原因 - -  生产者速度 > 消费者速度: - -  AsyncAppender 的后台线程从队列中取出事件并交给实际 Appender(如 FileAppender)处理,如果实际 Appender 的写入速度慢(如磁盘 I/O 高),消费者线程无法及时清空队列,导致队列积压。其实log4j消费时会调用多次 flush,这些flush的调用根本在文件写入的 native 调用,当这种native调用太多时,系统写入不过来。 - -## 3 应对方案 - -### 3.1 方案选择 - -  上述问题情况解决,大致分成两个方向:生产者方向&消费者方向,具体行为如下图简述:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHWnueNI4rlREl0V3JSEtRDC1ibkpR4jhZ2XqMFbrsqiarajepynlCIvfA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  在应对日志管理的挑战时,除了调整日志队列容量等基础优化(需警惕OOM风险),更核心的问题在于如何平衡日志的详实性与系统稳定性。开发者往往陷入两难:若详尽记录日志,可能引发阻塞风险;若过度精简,则排查问题时如盲人摸象,难溯根源。 - -为此,可考虑将日志划分为两类: - -功能日志(必须):如埋点数据、核心流程记录,确保业务可观测性; - -业务排查日志(非必须):如RPC入参/出参、调试断点等,按需动态启停; - -  通过这种分层策略,既能在高并发场景下保障核心日志的稳定输出,又能灵活控制辅助日志的打印量,使系统整体具备更强的适应性与可控性。如此,我们既能从容应对生产环境的严苛要求,又能在需要时快速激活详尽的诊断信息,实现运维效率与系统性能的兼得。 - -### 3.2 技术选择 - -  在日志打印的精细化控制中,核心在于灵活性与精准度的平衡。传统的全局级别过滤(如INFO/WARN/ERROR)虽能粗放管理,却难以适配复杂多变的业务场景。理想的方案应突破层级限制,实现行级细粒度控制——无论是核心链路的关键节点,还是特定业务场景的临时调试,均可针对单行日志动态启停。 - -  这种设计赋予开发者更高的自主权:业务视角:按需捕获特定模块的完整上下文;链路视角:精准聚焦某次调用的全生命周期轨迹;应急场景:即时激活深层诊断日志,无需重启或改码。 - -  通过将控制粒度细化至代码行,我们既能维持生产环境的日志精简,又能随时按业务诉求“点亮”关键路径,使系统可观测性兼具严谨与弹性。 - -#### 3.2.1 区分必要日志和非必要日志打印: - - 自定义封装日志打印的方式如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHvb9FLG3BmhibttKEy83BOWs8t7gXOdibkPicJaV18MVzicLUKlsO7a1nVQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -#### 3.2.2 如何对非必要日志进行行级别控制 - - 1、自定义Appender中的filter: - -  在实现行级日志控制时,若需精确控制特定代码行(如第133行)的日志输出,采用自定义Appender过滤机制是一种可行方案。其核心思路在于:通过解析日志调用的堆栈信息,动态判断当前行号是否符合预设的打印条件,若不符合则直接过滤。 - -  然而,该方案存在若干固有局限: 堆栈解析的可靠性问题:Lambda表达式中的日志调用往往难以准确获取行号信息,即使通过堆栈缓存优化,仍存在定位失准的风险;性能损耗隐患:频繁的堆栈遍历操作会引入不可忽视的性能开销,在高并发场景下可能成为新的瓶颈;分类管理缺失:该机制难以与既有的日志分级体系(必需/非必需日志)形成有机协同,增加了运维复杂度。 - - 2、在打印日志前获取日志的行信息: - -  最简单的方式是人工的形式,在写日志的时候同时将日志的行信息写入进去,比如:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHeZpkYq1kU9e6CAgUu57X9iblfAWCDURp2yZhDkJN2HEcicND4nIiciaTLA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)这种方式在可扩展性和可观测性维度存在设计缺陷。 - - 3、在编译的时候获取日志的行信息: - -  我们想使用LogUtils.debug(()->log.info("业务日志"));这种方式,但是我们不会在代码中明显的写入,可以在代码编译期间将行信息获取到后使用字节码修改这行代码,利用Java的重写。将它转变成 LogUtils.debug("类+行",()->log.info("业务日志")); 然后再这个方法执行中进行条件判断。 - -字节码技术选择: -  ![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHgVuiaHTK1MtAjYCibgrCVDZJ6oOyx4ac2R1O3CydMwX9fngib5diaWpnoQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  在本次实现中需要更灵活的方式操作字节码,还要考虑性能的问题,以及对应用框架的支持, 我们选择ASM的形式。 - -4、如何随心控制开启和关闭: - -  这里我们采用的是ider插件的方式,有idea插件上报我们的对这行日志的控制行为,如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHNHrI1ibxujSAmoPy2AXYUx8PbdXEyDyl6cJWIJrOEvSaEWYNMcRerIw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -### 3.3 落地实现 - -#### 3.3.1 Maven编译插件 - -  目的:获取日志所在的类和行信息。 - -  运行时获取的方式:在 Logger 配置中启用 includeLocation,代码从 LogEvent通过堆栈分析获取行号。这种方式存在很大的弊端,堆栈跟踪生成开销很大,每次调用 getStackTrace() 时,JVM 需要遍历当前线程的调用栈,生成完整的堆栈信息,这是一个 同步且耗时 的操作(尤其在深调用链中),如果每秒有数万次日志调用,频繁生成堆栈跟踪可能导致 CPU 使用率飙升,直接影响吞吐量。 - -   Maven的process-classes阶段获取:通过Maven编译之后获取到字节码文件时,对字节码文件进行修改,存放日志的类和行信息。对运行期间无额外消耗。处理逻辑如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHDvnxCOu4GPuag84E5dITibibdYwfVPKdRwMSIiby36Mb08YNX88CmAWLA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -#### 3.3.2 Idea插件 - -  目的:精准的控制某一行日志是否进行打印。 - -  利用Idea的插件能力,将我们对某一行日志的开启和关闭状态进行上报,整个过程不阻塞主线程,保持Idea操作流畅性。提供定时能力,保障线上我们可以更灵活的控制日志是否打印的状态。处理逻辑如下图所示;![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHSqkWttkMvjV3ELwy9OKhHwrsz1msFA18D5vaOFneT1AJ7QkjtRWIPQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -#### 3.3.3 整体流程 - -  使用方式:通过在项目中使用上述Maven插件对项目进行编译部署,使用Idea插件对目标日志的是否开启打印状态进行上报,存储状态采用的Apollo的能力,通过自定义打印工具类中对Apollo配置内容的分析,进一步做判断逻辑,最终将日志进行打印或者不打印。处理逻辑如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHU1ibLCk1ZVPspwevm1y1AAX5QKibQ1nXKic2YKA3RWoDlxzlD5ibym8vMw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1) - -## 4 总结 - -  在分布式系统日益复杂的今天,日志管理已从简单的信息记录演进为系统可观测性的核心支柱。本文揭示的日志阻塞与策略困境,折射出现代化服务在稳定性与可维护性之间的深层博弈。通过剖析Log4j2异步日志机制的内在原理,我们识别出队列积压导致线程阻塞的关键症结,并由此展开对日志治理体系的深度重构。 - -  本次优化方案突破传统日志分级思维的桎梏,创新性地提出双轨制日志管理体系:将日志划分为功能型与诊断型两类,前者确保核心业务脉络的持续可见,后者实现按需动态管控。通过编译期字节码增强技术,我们实现代码行级别的精准控制,配合IDE插件的可视化操作,使开发人员能够像调试断点般自由启停日志输出。这种"外科手术式"的日志管理,既避免了传统方案"一刀切"的弊端,又赋予系统在高负载场景下的弹性适应能力。 \ No newline at end of file diff --git a/3Projects/Unity/loxodon/loxodon-framework Public.md b/3Projects/Unity/loxodon/loxodon-framework Public.md deleted file mode 100644 index 7e4d377..0000000 --- a/3Projects/Unity/loxodon/loxodon-framework Public.md +++ /dev/null @@ -1,22 +0,0 @@ - - -Source:[loxodon-framework/Loxodon.Framework.TextUGUI at master · vovgou/loxodon-framework · GitHub](https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework.TextUGUI) - ---- - - -单向绑定 -``` csharp -[OnValueChanged(viewModel=>viewModel.hp, value=>string.Format("{0:D4}"))] -CustomText text; - -public class CustomText -{ - public Text text; - public void SetText(string str) - { - text.text = str; - } -} - -``` \ No newline at end of file diff --git a/1Project/宠物宇宙/硬件交互/用户卡及游戏信息处理过程设计文档.md b/4Archives/宠物宇宙/用户卡及游戏信息处理过程设计文档.md similarity index 100% rename from 1Project/宠物宇宙/硬件交互/用户卡及游戏信息处理过程设计文档.md rename to 4Archives/宠物宇宙/用户卡及游戏信息处理过程设计文档.md diff --git a/InBox/loxodon-framework.md b/InBox/loxodon-framework.md deleted file mode 100644 index e69de29..0000000 diff --git a/InBox/【Unity插件 - 图标轮廓渲染插件 SDF Image - Quality UI Outlines and Shadow-哔哩哔哩】.md b/InBox/【Unity插件 - 图标轮廓渲染插件 SDF Image - Quality UI Outlines and Shadow-哔哩哔哩】.md deleted file mode 100644 index bb22d63..0000000 --- a/InBox/【Unity插件 - 图标轮廓渲染插件 SDF Image - Quality UI Outlines and Shadow-哔哩哔哩】.md +++ /dev/null @@ -1,3 +0,0 @@ - - - https://b23.tv/tZzXJrB \ No newline at end of file diff --git a/InBox/网络/封装可扩展网络请求框架.md b/InBox/网络/封装可扩展网络请求框架.md deleted file mode 100644 index 902b0e5..0000000 --- a/InBox/网络/封装可扩展网络请求框架.md +++ /dev/null @@ -1,5 +0,0 @@ - -Source:[Unity网络请求封装实战:手把手教你打造一套可扩展的网络请求框架](https://mp.weixin.qq.com/s/p9qDdJqHj-7x58XAPNNabA) - ---- - diff --git a/渲染/软渲染/图片/img_v3_02n6_ff40a1a4-d813-45f7-b132-6f14a74cfd7g.jpg b/渲染/软渲染/图片/img_v3_02n6_ff40a1a4-d813-45f7-b132-6f14a74cfd7g.jpg deleted file mode 100644 index 7f4f666..0000000 Binary files a/渲染/软渲染/图片/img_v3_02n6_ff40a1a4-d813-45f7-b132-6f14a74cfd7g.jpg and /dev/null differ diff --git a/渲染/软渲染/图片/业务插件(RTwoGameBusiness)TCP接口设计说明.md b/渲染/软渲染/图片/业务插件(RTwoGameBusiness)TCP接口设计说明.md deleted file mode 100644 index 2db50d1..0000000 --- a/渲染/软渲染/图片/业务插件(RTwoGameBusiness)TCP接口设计说明.md +++ /dev/null @@ -1,1541 +0,0 @@ -# 业务插件(RTwoGameBusiness)TCP接口说明 - -## 1. 文档概述 - -### 1.1 版本 - -| 版本号 | 日期 | 作者 | 变更内容 | 状态 | -|-------|------|------|---------|------| -| 1.0 | 2025-05-29 | JJX | 初始创建 | 正式发布 | -| 1.1 | 2025-06-15 | JJX | 添加get-system-info接口说明和更新check-unis-net-state接口说明 | 正式发布 | - -### 1.2 目的 - -本文档详细描述RTwoGameBusiness业务插件提供的TCP接口规范,用于游戏客户端与业务插件之间的通信。该文档面向开发人员、测试人员和集成人员,提供接口的详细说明、参数格式、错误码以及使用示例。 - -### 1.3 适用范围 - -- 开发人员:用于实现游戏客户端与业务插件的通信 -- 测试人员:用于验证接口功能和性能 -- 集成人员:用于系统集成和故障排查 -- 维护人员:用于系统维护和问题定位 - -### 1.4 接口概述 - -RTwoGameBusiness插件通过TCP协议提供服务,主要包括以下功能类别: - -1. **用户卡功能**:用户信息查询、刷卡登录等 -2. **点数卡功能**:点数查询、充值、扣点等 -3. **游戏记录功能**:游戏积分、金币、宝石、卡牌、投币记录等 -4. **4G网络控制**:4G模块状态查询、启动和停止等 - -## 2. 基础信息 - -### 2.1 服务端口 - -RTwoGameBusiness插件默认监听TCP端口:**12103** - -该端口可通过配置文件修改,修改后需重启插件生效。 - -### 2.2 通信协议 - -- 通信协议:TCP -- 字符编码:UTF-8 -- 数据格式:JSON - -### 2.3 请求格式 - -所有请求都应遵循以下JSON格式: - -```json -{ - "command": "命令名称", - "data": { - // 根据不同命令类型包含不同的参数 - }, - "ts": 1621234567890 // 可选,请求时间戳(毫秒) -} -``` - -### 2.4 响应格式 - -所有响应都遵循以下JSON格式: - -```json -{ - "result": true, // 响应结果,true成功,false失败 - "errMsg": "", // 错误信息,成功时为空 - "statusCode": 0, // 状态码,0表示成功,非0表示各种错误 - "command": "命令名称", // 关联的指令标识 - "data": { // 返回数据,根据不同接口有不同结构 - // 根据不同命令返回不同的数据 - }, - "ts": 1621234567890 // 可选,响应时间戳(毫秒) -} -``` - -## 3. 接口详细说明 - -根据功能分类,本章节将详细介绍RTwoGameBusiness插件提供的所有TCP接口。 - -### 3.1 用户卡功能接口 - -#### 3.1.1 用户信息查询 (select-user-info) - -该接口用于查询用户卡所绑定的用户信息,并可选择性地返回排行榜数据。 - -**请求命令**:`select-user-info` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是(与cardSn二选一) | 卡ID | -| cardSn | string | 是(与cardId二选一) | 卡序列号 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 排行榜条数限制,默认为10 | -| topType | int | 否 | 排行榜类型:0=积分排行榜(默认),1=时长排行榜 | - -**请求示例**: - -```json -{ - "command": "select-user-info", - "data": { - "cardId": "1234567890", - "gameLevel": "1", - "topLimit": 10, - "topType": 0 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| cardId | string | 卡ID | -| cardSn | string | 卡序列号 | -| userName | string | 用户名称 | -| userAvatar | string | 用户头像URL | -| userSex | int | 用户性别:0-未知 1-男 2-女 | -| topType | int | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分(当topType=0时有效) | -| duration | int | 用户在此排行榜中的游戏时长(当topType=1时有效) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "select-user-info", - "data": { - "cardId": "1234567890", - "cardSn": "A1B2C3D4", - "userName": "玩家001", - "userAvatar": "http://example.com/avatar/001.jpg", - "userSex": 1, - "topType": 0, - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡ID和卡序列号都为空) | -| 40 | 用户不存在 | - -**备注**: -1. 卡ID和卡序列号必须至少提供一个,优先使用卡ID查询。 -2. 如果未指定排行榜类型,默认返回积分排行榜。 -3. 如果未指定游戏关卡,将返回所有关卡的排行榜数据。 -4. 排行榜数据按分数或时长降序排列。 - -#### 3.1.2 刷卡登录 (cwyz-swipe-card) - -该接口用于处理游戏中的刷卡操作,关联用户与游戏。 - -**请求命令**:`cwyz-swipe-card` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardSn | string | 是 | 卡序列号 | -| playerIdx | int | 是 | 玩家索引,用于多人游戏时区分不同玩家位置 | - -**请求示例**: - -```json -{ - "command": "cwyz-swipe-card", - "data": { - "cardSn": "A1B2C3D4", - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| cardId | string | 卡ID | -| cardSn | string | 卡序列号 | -| userName | string | 用户名称 | -| userAvatar | string | 用户头像URL | -| userSex | int | 用户性别:0-未知 1-男 2-女 | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-swipe-card", - "data": { - "cardId": "1234567890", - "cardSn": "A1B2C3D4", - "userName": "玩家001", - "userAvatar": "http://example.com/avatar/001.jpg", - "userSex": 1, - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡序列号为空) | -| 40 | 用户卡不存在 | -| 30 | 网络错误(与服务器通信失败) | - -**备注**: -1. 刷卡操作会尝试从服务器获取最新的用户信息,如果网络不可用,会使用本地存储的用户信息。 -2. 玩家索引用于区分多人游戏时的不同位置,通常从1开始,对应游戏机的不同位置。 -3. 如果卡片未绑定用户,会返回默认的用户信息。 - -### 3.2 点数卡功能接口 - -#### 3.2.1 查询剩余点数 (get-remaining-points) - -该接口用于查询当前游戏机剩余的点数,用于显示给用户或判断是否有足够点数执行出卡操作。 - -**请求命令**:`get-remaining-points` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "get-remaining-points", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 剩余点数 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "get-remaining-points", - "data": { - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 20 | 数据库错误(读取点数卡信息失败) | - -**备注**: -1. 此接口会返回所有已绑定点数卡的剩余点数总和。 -2. 如果没有任何点数卡,返回的剩余点数为0。 -3. 点数信息优先从本地数据库获取,确保即使在网络不可用时也能正常工作。 - -#### 3.2.2 绑定点卡 (bind-point-card) - -该接口用于绑定新的点数卡到游戏机上。 - -**请求命令**:`bind-point-card` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardCode | string | 是 | 点卡卡号 | - -**请求示例**: - -```json -{ - "command": "bind-point-card", - "data": { - "cardCode": "PC12345678" - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 绑定后的剩余点数 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "bind-point-card", - "data": { - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(卡号为空) | -| 20 | 数据库错误(绑定点数卡失败) | -| 60 | 卡片不存在(点卡号无效) | -| 61 | 点卡已使用 | - -**备注**: -1. 绑定点卡后,点数将自动添加到游戏机的总点数中。 -2. 一个点卡只能被绑定一次,绑定后不能重复使用。 -3. 点卡绑定后立即生效,无需重启游戏机。 - -#### 3.2.3 出卡扣点 (deduct-points) - -该接口用于游戏出卡时扣除相应点数。 - -**请求命令**:`deduct-points` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| pointsToDeduct | int | 否 | 需要扣除的点数,默认为1 | -| playerIdx | int | 否 | 机台位置索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "deduct-points", - "data": { - "pointsToDeduct": 1, - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| remainingPoints | int | 扣除后的剩余点数 | -| isUploaded | bool | 是否已上报到服务器 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "deduct-points", - "data": { - "remainingPoints": 99, - "isUploaded": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(扣点数量无效) | -| 20 | 数据库错误(扣点操作失败) | -| 50 | 点数不足 | - -**备注**: -1. 扣点操作会根据当前配置的扣点模式执行(本地模式、实时模式或混合模式)。 -2. 如果使用本地模式,操作会先在本地执行,然后在适当时机上报到服务器。 -3. 如果使用实时模式,操作会直接上报到服务器,并立即返回结果。 -4. 如果点数不足,将返回错误码50。 -5. 每次扣点操作都会记录在扣点记录表中,用于后续统计和审计。 - -### 3.3 游戏记录功能接口 - -#### 3.3.1 保存游戏积分 (cwyz-score-save) - -该接口用于保存玩家的游戏积分和相关游戏数据,同时可以获取游戏排行榜信息。 - -**请求命令**:`cwyz-score-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| score | int | 是 | 游戏得分 | -| gameLevel | string | 是 | 游戏关卡 | -| gameType | string | 是 | 游戏类型 | -| topLimit | int | 否 | 排行榜总数,默认为10 | -| gameDuration | int | 否 | 游戏时长(秒) | -| golds | int | 否 | 获得金币数量 | -| gemstone | int | 否 | 获得的未鉴定宝石数量 | -| playerIdx | int | 否 | 玩家索引,默认为1 | -| skinId | string | 否 | 皮肤ID | -| playerCount | int | 否 | 共同游戏的玩家数量,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-score-save", - "data": { - "cardId": "1234567890", - "score": 1500, - "gameLevel": "1", - "gameType": "standard", - "topLimit": 10, - "gameDuration": 300, - "golds": 50, - "gemstone": 2, - "playerIdx": 1, - "skinId": "skin_001", - "playerCount": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分 | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-score-save", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(保存积分失败) | -| 40 | 用户不存在 | - -**备注**: -1. 如果提供了金币(golds)参数且大于0,系统会自动调用保存金币记录的功能。 -2. 如果提供了宝石(gemstone)参数且大于0,系统会自动调用保存宝石记录的功能。 -3. 积分数据会根据游戏类型和游戏关卡进行分类,便于后续查询。 -4. 响应中的排行榜数据包含当前用户的排名和指定数量的最高分记录。 -5. 如果用户卡ID不存在,系统会尝试创建一个游客记录。 - -#### 3.3.2 获取游戏排行榜 (cwyz-query-score-top) - -该接口用于获取游戏排行榜信息。 - -**请求命令**:`cwyz-query-score-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-score-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分 | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-score-top", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(获取排行榜失败) | -| 40 | 用户不存在 | - -**备注**: -1. 排行榜数据包含当前用户的排名和指定数量的最高分记录。 -2. 如果用户卡ID不存在,系统会尝试创建一个游客记录。 - -#### 3.3.3 获取游戏时长排行榜 (cwyz-query-duration-top) - -该接口用于获取游戏时长排行榜信息。 - -**请求命令**:`cwyz-query-duration-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-duration-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| duration | int | 用户在此排行榜中的游戏时长(秒) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-duration-top", - "data": { - "gameType": "standard", - "leaderboards": { - "1": { - "rank": 2, - "duration": 280, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 1800, - "gameDuration": 320, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1200, - "gameDuration": 240, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(游戏类型为空) | -| 20 | 数据库错误(获取排行榜失败) | - -**备注**: -1. 该接口专门用于获取按游戏时长排序的排行榜。 -2. 排行榜数据按游戏时长降序排列。 -3. 如果设置isGlobal为true,将获取全网的排行榜数据,可能需要更长的响应时间。 - -#### 3.3.4 统一获取游戏排行榜 (cwyz-query-top) - -该接口统一了获取积分排行榜和时长排行榜的功能,通过topType参数区分不同类型的排行榜。 - -**请求命令**:`cwyz-query-top` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| gameType | string | 是 | 游戏类型 | -| gameLevel | string | 否 | 游戏关卡,不传则返回所有关卡的排行榜 | -| topLimit | int | 否 | 需要查询的排行榜条数,默认为10 | -| topType | int | 是 | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| isGlobal | bool | 否 | 是否获取全网排名,默认为false(即默认获取本地排名) | - -**请求示例**: - -```json -{ - "command": "cwyz-query-top", - "data": { - "gameType": "standard", - "gameLevel": "1", - "topLimit": 10, - "topType": 0, - "isGlobal": false - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameType | string | 游戏类型 | -| topType | int | 排行榜类型:0=积分排行榜,1=时长排行榜 | -| leaderboards | object | 按游戏关卡分组的排行榜,key为游戏关卡 | - -**leaderboards对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| rank | int | 用户在此排行榜中的排名 | -| score | int | 用户在此排行榜中的积分(当topType=0时有效) | -| duration | int | 用户在此排行榜中的游戏时长(当topType=1时有效) | -| gameLevel | string | 游戏关卡 | -| records | array | 排行榜记录列表 | - -**records数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| id | int | 记录ID | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| score | int | 游戏得分 | -| gameDuration | int | 游戏时长(秒) | -| playTime | string | 游戏时间(格式化字符串) | -| gameLevel | string | 游戏关卡 | -| gameType | string | 游戏类型 | -| skinId | string | 皮肤ID | -| playerCount | int | 共同游戏的玩家数量 | - -**响应示例(积分排行榜)**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-query-top", - "data": { - "gameType": "standard", - "topType": 0, - "leaderboards": { - "1": { - "rank": 3, - "score": 1500, - "gameLevel": "1", - "records": [ - { - "id": 123, - "cardId": "9876543210", - "userName": "玩家088", - "score": 2000, - "gameDuration": 300, - "playTime": "2025-05-28 14:30:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_001", - "playerCount": 1 - }, - { - "id": 124, - "cardId": "8765432109", - "userName": "玩家072", - "score": 1800, - "gameDuration": 280, - "playTime": "2025-05-28 15:20:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_002", - "playerCount": 1 - }, - { - "id": 125, - "cardId": "1234567890", - "userName": "玩家001", - "score": 1500, - "gameDuration": 260, - "playTime": "2025-05-28 16:10:00", - "gameLevel": "1", - "gameType": "standard", - "skinId": "skin_003", - "playerCount": 1 - } - ] - } - } - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(游戏类型为空或排行榜类型无效) | -| 20 | 数据库错误(获取排行榜失败) | - -**备注**: -1. 该接口可以根据topType参数灵活获取不同类型的排行榜。 -2. 当topType=0时,排行榜按积分降序排列;当topType=1时,排行榜按游戏时长降序排列。 -3. 如果设置isGlobal为true,将获取全网的排行榜数据,可能需要更长的响应时间。 -4. 推荐使用该接口代替单独的积分排行榜和时长排行榜接口,提高代码复用性。 - -#### 3.3.5 保存出卡记录 (cwyz-outcard-save) - -该接口用于记录游戏中玩家获得卡片的信息。 - -**请求命令**:`cwyz-outcard-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| cardStar | int | 是 | 卡片星级 | -| cardType | string | 是 | 卡片类型 | -| cardName | string | 是 | 卡片名称 | -| gameType | string | 是 | 游戏类型 | -| playerIdx | int | 否 | 玩家索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-outcard-save", - "data": { - "cardId": "1234567890", - "cardStar": 4, - "cardType": "monster", - "cardName": "火焰龙", - "gameType": "standard", - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| cardStar | int | 卡片星级 | -| cardType | string | 卡片类型 | -| cardName | string | 卡片名称 | -| drawTime | string | 出卡时间(格式化字符串) | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-outcard-save", - "data": { - "success": true, - "cardId": "1234567890", - "userName": "玩家001", - "cardStar": 4, - "cardType": "monster", - "cardName": "火焰龙", - "drawTime": "2025-05-28 16:30:45", - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失) | -| 20 | 数据库错误(保存出卡记录失败) | -| 40 | 用户不存在 | - -**备注**: -1. 出卡记录会保存在本地数据库中,并在网络可用时上传到服务器。 -2. 出卡时间使用服务器时间,确保统一性。 -3. 卡片星级通常为1-5,表示卡片的稀有程度。 -4. 卡片类型可以是游戏定义的任何类型,如"monster"、"magic"、"trap"等。 - -#### 3.3.6 保存硬币记账 (cwyz-coin-save) - -该接口用于记录玩家投币和消耗硬币的信息。 - -**请求命令**:`cwyz-coin-save` - -**请求参数**: - -| 参数名 | 类型 | 必填 | 说明 | -|-------|------|------|------| -| cardId | string | 是 | 卡ID,用于关联用户 | -| coinCount | int | 是 | 硬币数量 | -| type | int | 是 | 类型:1=投币,2=消耗 | -| playerIdx | int | 否 | 玩家索引,默认为1 | - -**请求示例**: - -```json -{ - "command": "cwyz-coin-save", - "data": { - "cardId": "1234567890", - "coinCount": 5, - "type": 1, - "playerIdx": 1 - } -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | -| cardId | string | 卡ID | -| userName | string | 用户名称 | -| coinCount | int | 硬币数量 | -| type | int | 操作类型:1=投币,2=消耗 | -| balance | int | 当前余额 | -| processTime | string | 处理时间(格式化字符串) | -| playerIdx | int | 玩家索引 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "cwyz-coin-save", - "data": { - "success": true, - "cardId": "1234567890", - "userName": "玩家001", - "coinCount": 5, - "type": 1, - "balance": 15, - "processTime": "2025-05-28 17:15:30", - "playerIdx": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 10 | 参数错误(必填参数缺失或硬币数量为0) | -| 20 | 数据库错误(保存硬币记录失败) | -| 40 | 用户不存在 | -| 50 | 余额不足(当type=2,消耗硬币时) | - -**备注**: -1. 硬币记账会记录玩家的投币和消耗情况,便于统计和分析。 -2. 类型为1表示投币(增加余额),类型为2表示消耗(减少余额)。 -3. 响应中的balance字段表示操作后的硬币余额。 -4. 如果是消耗操作且余额不足,将返回错误码50。 -5. 所有操作都会记录在硬币记账表中,用于后续统计和审计。 - -### 3.4 4G网络控制接口 - -#### 3.4.1 查询4G模块信息 (query-4g-info) - -该接口用于查询4G网络模块的详细信息,包括网络状态、信号强度、SIM卡信息等。 - -**请求命令**:`query-4g-info` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "query-4g-info", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| mnc | string | 网络MNC码 | -| cid | string | 基站CID | -| lac | string | 位置区域码 | -| serialPort | string | 串口名称 | -| dbm | int | 信号强度 | -| revision | string | 模块固件版本 | -| sim | string | SIM卡ICCID | -| imsi | string | SIM卡IMSI | -| imei | string | 设备IMEI | -| ndisEnabled | bool | NDIS网卡状态 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "query-4g-info", - "data": { - "mnc": "01", - "cid": "123456", - "lac": "7890", - "serialPort": "COM3", - "dbm": -65, - "revision": "EG25GGBR07A08M2G", - "sim": "898600123456789012345", - "imsi": "460012345678901", - "imei": "862512345678901", - "ndisEnabled": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | - -**备注**: -1. 信号强度(dbm)通常在-50到-120之间,数值越大(越接近0)表示信号越强。 -2. ndisEnabled表示4G网卡是否启用,true表示已启用,false表示已禁用。 -3. 如果设备没有4G模块,将返回错误码80。 -4. 该接口仅查询信息,不会改变任何网络设置。 - -#### 3.4.2 禁用4G网络 (stop-4g-net) - -该接口用于禁用4G网络,可用于节省流量或解决网络冲突问题。 - -**请求命令**:`stop-4g-net` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "stop-4g-net", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "stop-4g-net", - "data": { - "success": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | -| 82 | 网络已经处于禁用状态 | - -**备注**: -1. 该接口会关闭4G网卡,停止所有网络通信。 -2. 操作成功后,设备将无法通过4G网络连接互联网,但可以通过其他网络接口(如WiFi、有线网络)连接。 -3. 4G网络禁用后,可通过start-4g-net接口重新启用。 -4. 如果设备没有4G模块,将返回错误码80。 - -#### 3.4.3 启用4G网络 (start-4g-net) - -该接口用于启用4G网络,恢复设备的移动网络连接。 - -**请求命令**:`start-4g-net` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "start-4g-net", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| success | bool | 操作是否成功 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "start-4g-net", - "data": { - "success": true - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | -| 80 | 4G模块不存在 | -| 81 | 4G模块通信错误 | -| 83 | 网络已经处于启用状态 | -| 84 | SIM卡异常 | - -**备注**: -1. 该接口会启用4G网卡,恢复移动网络连接。 -2. 操作成功后,设备将能够通过4G网络连接互联网。 -3. 网络连接过程可能需要几秒到几十秒不等,取决于网络信号强度和运营商网络状况。 -4. 如果设备没有4G模块或SIM卡异常,将返回相应的错误码。 - -### 3.5 世宇接口状态 - -#### 3.5.1 检查世宇接口状态 (check-unis-net-state) - -该接口用于检查与世宇服务器的连接状态。 - -**请求命令**:`check-unis-net-state` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "check-unis-net-state", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| state | int | 世宇网络连接状态: 0-断开连接 1-有线网络 2-4G网络 3-服务异常 4-系统异常 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "check-unis-net-state", - "data": { - "state": 1 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | - -**网络状态代码说明**: - -| 网络状态码 | 描述 | 说明 | -|-----------|------|------| -| 0 | 网络断开 | 无网络连接 | -| 1 | 有线网络 | 有线网络连接正常且能访问世宇服务器 | -| 2 | 4G网络 | 4G网络连接正常且能访问世宇服务器 | -| 3 | 服务异常 | 网络连接正常但无法登录世宇服务器 | -| 4 | 系统异常 | 网络连接正常但无法获取登录凭证 | - -**备注**: -1. 该接口用于检查业务插件与世宇服务器的连接状态。 -2. 检查过程包括网络连接检测和世宇服务器登录尝试。 -3. 有线网络和4G网络状态表示网络连接正常且可成功登录世宇服务器。 -4. 服务异常表示网络连接正常但无法登录世宇服务器,可能是服务器问题。 -5. 系统异常表示网络连接正常但无法获取登录凭证,可能是配置问题。 - -#### 3.5.2 获取系统信息 (get-system-info) - -该接口用于获取系统综合信息,包括网络状态、版本信息、磁盘信息和剩余点数。 - -**请求命令**:`get-system-info` - -**请求参数**: -无需参数 - -**请求示例**: - -```json -{ - "command": "get-system-info", - "data": {} -} -``` - -**响应参数**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| networkState | int | 网络状态代码: 0-断开连接 1-有线网络 2-4G网络 3-服务异常 4-系统异常 | -| networkStateDesc | string | 网络状态描述文本 | -| versionInfo | object | 版本信息对象 | -| diskInfos | array | 磁盘信息数组 | -| remainingPoints | int | 剩余点数 | - -**versionInfo对象内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| gameVersion | string | 游戏版本 | -| updaterVersion | string | 更新器版本 | -| businessVersion | string | 业务模块版本 | -| consoleVersion | string | 控制台版本 | -| deviceId | string | 设备ID | - -**diskInfos数组内部结构**: - -| 参数名 | 类型 | 说明 | -|-------|------|------| -| driveLetter | string | 盘符 | -| freeSpace | string | 可用空间(格式化后的字符串) | -| totalSpace | string | 总空间(格式化后的字符串) | -| usedSpace | string | 已用空间(格式化后的字符串) | -| freePercent | float | 可用空间百分比 | - -**响应示例**: - -```json -{ - "result": true, - "errMsg": "", - "statusCode": 0, - "command": "get-system-info", - "data": { - "networkState": 1, - "networkStateDesc": "有线网络", - "versionInfo": { - "gameVersion": "1.0.5", - "updaterVersion": "1.2.0", - "businessVersion": "1.1.0", - "consoleVersion": "1.0.8", - "deviceId": "ABCDEF123456" - }, - "diskInfos": [ - { - "driveLetter": "C:", - "freeSpace": "50.5 GB", - "totalSpace": "100 GB", - "usedSpace": "49.5 GB", - "freePercent": 50.5 - }, - { - "driveLetter": "D:", - "freeSpace": "120 GB", - "totalSpace": "500 GB", - "usedSpace": "380 GB", - "freePercent": 24.0 - } - ], - "remainingPoints": 100 - } -} -``` - -**错误码说明**: - -| 状态码 | 说明 | -|-------|------| -| 0 | 成功 | -| 1 | 通用错误 | - -**备注**: -1. 该接口汇总了系统的多项信息,便于快速了解系统状态。 -2. 网络状态检测逻辑与check-unis-net-state接口保持一致。 -3. 版本信息通过向更新器发送get-version-info指令获取。 -4. 磁盘信息按盘符字母顺序排列。 -5. 剩余点数与get-remaining-points接口返回的值一致。