Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e019197f43 |
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: "source-command-split-commit"
|
||||
description: "将当前未提交改动拆分为多个符合规范的原子提交"
|
||||
---
|
||||
|
||||
# source-command-split-commit
|
||||
|
||||
Use this skill when the user asks to run the migrated source command `split-commit`.
|
||||
|
||||
## Command Template
|
||||
|
||||
将当前未提交改动拆分为多个原子提交。执行前会分析变更、生成分组方案,经用户确认后逐组提交。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 前置检查
|
||||
|
||||
1. **清理残留文件**:若存在 `msg.txt`,先 `rm -f msg.txt`
|
||||
2. **读取规范**:确认 `COMMIT_CONVENTION.md` 存在且可读
|
||||
|
||||
### Step 2: 保护分支检查
|
||||
|
||||
保护分支列表(按优先级):`AGENTS.md` > `.cursor/rules/` > `.git/config branch.protected` > 默认(`master|main|develop`)
|
||||
|
||||
若当前在保护分支,询问用户:
|
||||
- **A** - 创建 feature 分支后继续拆分(推荐)
|
||||
- **B** - 转移到新分支后压成单提交
|
||||
- **C** - 取消
|
||||
|
||||
### Step 3: 盘点变更
|
||||
|
||||
执行以下命令分析:
|
||||
- `git status` - 整体状态
|
||||
- `git diff --stat` - 文件统计
|
||||
- `git diff` - 详细改动
|
||||
- `git diff --cached` - 已暂存改动
|
||||
|
||||
### Step 4: 规则化分组
|
||||
|
||||
按"意图一致"原则分组,**优先按 type 分,再按 scope 分**。
|
||||
|
||||
**必须拆分的情况**:
|
||||
- 同一组可归属多个 type
|
||||
- 同文件包含多个独立目的(如功能+重构)
|
||||
- 资源替换与代码逻辑混合
|
||||
|
||||
**可以合并的情况**(最小拆分原则):
|
||||
- 同一功能的多文件改动(脚本+Prefab)
|
||||
- 同一模块的多个相关 Bug 修复
|
||||
|
||||
### Step 5: 生成方案表格
|
||||
|
||||
以表格形式展示拆分方案:
|
||||
|
||||
| 顺序 | 文件 | 变更目的 | 提交头 | 依据 | 方式 |
|
||||
|------|------|----------|--------|------|------|
|
||||
| 1 | `路径` | 描述 | `type(scope): subject` | 原因 | `git add`/`git add -p` |
|
||||
|
||||
**Type 选择优先级**:`fix` > `feat` > `art` > `audio` > `scene` > `yarn` > `perf|refactor|...`
|
||||
|
||||
**Scope 规则**:单模块用模块名(如 `blockpuzzle`),跨模块省略。
|
||||
|
||||
### Step 6: 用户确认
|
||||
|
||||
询问:`[Y 执行 / n 取消 / 提出调整]`
|
||||
|
||||
- 若调整:根据反馈修改方案,再次确认
|
||||
- 若执行:进入 Step 7
|
||||
- 若取消:清理并退出
|
||||
|
||||
### Step 7: 逐组提交
|
||||
|
||||
对每组执行:
|
||||
1. `git add <file>` 或 `git add -p <file>`
|
||||
2. 复查:`git diff --cached --stat` 和 `git diff --cached`
|
||||
3. 写入 `msg.txt` 并提交:`git commit -F msg.txt`
|
||||
4. 检查状态:`git status`
|
||||
|
||||
**失败处理**:
|
||||
- `git add -p` 跳过关键 hunk → 暂停,提示处理方案
|
||||
- pre-commit hook 失败 → 询问 [R]重试 [S]跳过 [A]中止
|
||||
|
||||
### Step 8: 结果校验
|
||||
|
||||
输出:
|
||||
1. `git log --oneline -<N>`
|
||||
2. 回放清单(验证 type/scope/subject 合规性)
|
||||
3. 若不合规,询问是否修正
|
||||
|
||||
### Step 9: 清理
|
||||
|
||||
`rm -f msg.txt`
|
||||
|
||||
---
|
||||
|
||||
## Unity 改动速查表
|
||||
|
||||
| 改动类型 | type | scope 示例 |
|
||||
|----------|------|-----------|
|
||||
| 脚本逻辑修复 | `fix` | `blockpuzzle`, `dialog` |
|
||||
| 脚本新功能 | `feat` | `huoshan`, `clue` |
|
||||
| 美术资源(贴图/模型/动画) | `art` | 对应模块或省略 |
|
||||
| 音频资源 | `audio` | `audio-kit` 或省略 |
|
||||
| 场景配置(灯光/镜头) | `scene` | 对应模块或省略 |
|
||||
| Yarn 对话 | `yarn` | `dialog` |
|
||||
| 测试代码 | `test` | 对应模块 |
|
||||
|
||||
---
|
||||
|
||||
## 规范校验清单(执行前检查)
|
||||
|
||||
- [ ] 格式:`<type>(<scope>): <subject>` 或 `<type>: <subject>`
|
||||
- [ ] type ∈ {`feat`,`fix`,`art`,`audio`,`scene`,`yarn`,`perf`,`refactor`,`style`,`docs`,`build`,`chore`,`test`}
|
||||
- [ ] subject:中文、动词开头、≤50字、无句号
|
||||
- [ ] 一次提交只做一件事
|
||||
|
||||
---
|
||||
|
||||
## 中文提交方法
|
||||
|
||||
由于 PowerShell 中文乱码问题,统一使用文件方式:
|
||||
|
||||
```bash
|
||||
echo "type(scope): 描述" > msg.txt
|
||||
git commit -F msg.txt
|
||||
rm -f msg.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关于 Pre-commit Hooks
|
||||
|
||||
若配置了 husky/lint-staged:
|
||||
- 每次提交都会触发检查
|
||||
- 如需跳过(**不推荐**):`git commit -F msg.txt --no-verify`
|
||||
- 跳过可能导致 CI 失败
|
||||
|
||||
---
|
||||
|
||||
开始执行请回复 **"开始"**,或告知需要的调整。
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
name: yarn-text-audit
|
||||
description: 全量审校 Yarn 文本,生成可追溯问题台账并可导出飞书多维表格
|
||||
---
|
||||
|
||||
对用户指定的 Yarn 目录执行全量文本审校。确定性覆盖与安全门禁由 `Tools/yarn_text_audit.py` 提供;语言和叙事判断必须保持可复核。
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 明确目标目录、参考目录、是否包含注释、是否只出报告。
|
||||
2. 对所有目标目录运行:
|
||||
|
||||
```powershell
|
||||
python Tools/yarn_text_audit.py scan `
|
||||
--root <Yarn目录,可重复> `
|
||||
--output Temp/YarnTextAudit `
|
||||
--review-memory <可选:上次飞书审阅导出的 reviewed-base.json>
|
||||
```
|
||||
|
||||
3. 检查 `Temp/YarnTextAudit/summary.json`,必须满足:
|
||||
- `unknown_active_lines == 0`
|
||||
- `unreviewed_records == 0`
|
||||
- `active_records == ledger.jsonl` 实际行数
|
||||
4. 分文件、分节点复核 `ledger.jsonl`。活动文本包括命名对白、无角色旁白、系统文本、选项和 option prompt;排除首个非空白字符为 `//` 的注释、节点元数据和 Yarn 命令。
|
||||
5. 将确定性错误标为“建议修改”,语义、节奏、专名和重要信息等判断标为“需确认”;不以自动规则冒充绝对正确。
|
||||
6. 交付 `issues.csv` 和 `coverage.csv`。用户要求飞书时,先运行 `python Tools/yarn_text_audit.py lark-payloads --output Temp/YarnTextAudit`,再创建 `问题清单`、`覆盖报告`、`术语与样式` 三张表;单批不超过 200 条并核对返回数量。
|
||||
7. 用户完成多维表格审阅后,导出完整记录并执行:
|
||||
|
||||
```powershell
|
||||
python Tools/yarn_text_audit.py apply-review `
|
||||
--reviews Temp/YarnTextAudit/reviewed-base.json `
|
||||
--manifest Temp/YarnTextAudit/manifest.json `
|
||||
--report Temp/YarnTextAudit/apply-report.json
|
||||
|
||||
# 预览无误后才实际写入
|
||||
python Tools/yarn_text_audit.py apply-review `
|
||||
--reviews Temp/YarnTextAudit/reviewed-base.json `
|
||||
--manifest Temp/YarnTextAudit/manifest.json `
|
||||
--report Temp/YarnTextAudit/apply-report.json `
|
||||
--apply
|
||||
```
|
||||
|
||||
8. 修改后重新扫描并运行 `verify`。不得新增、重生成或修改 `#line:` 标签;不得增加文本显示行数。
|
||||
|
||||
## 人工反馈优先级
|
||||
|
||||
- 后续扫描使用 `--review-memory`;脚本会按原文或已应用后的预期文本重建稳定 ID,不重复报告已经人工决策的记录。
|
||||
- “按照备注修改”高于分类和脚本建议;只执行备注明确要求的断行、颜色、文字或效果。
|
||||
- 不因 `Task` 说话者、`role="Aside"` 或专有名词出现本身自动判定颜色。必须结合是否为玩家行动目标、真实内心表达及当前叙事重点逐处判断。
|
||||
- 专有名词不要求每次出现都标蓝;检查/拆除/短接/装回等玩家下一步操作可以按上下文标黄。
|
||||
- 笑话效果优先只包裹双关或谐音落点,不默认包裹整句。
|
||||
- `#autonext` 不应一律修正为 `#auto_next`;若审阅备注要求移除,则直接移除。
|
||||
- 长行断点以约 18 个全角字符的实际 UI 宽度估算自动折行数,优先在逗号处显式控制;已有断行未经批准不重排。
|
||||
|
||||
## AIBIS 审校规则
|
||||
|
||||
- 检查错别字、错误重复、缺字候选、的地得、全半角和标点、标签配对及换行舒适度。
|
||||
- 换行优先考虑逗号附近;只有在不增加显示行数时才建议调整。逗号停顿只用于调整后的断点或明确节奏位置。
|
||||
- `[[aside]]...[[/aside]]`:实际自言自语、念文本或小声说话;斜体本身不是充分条件。
|
||||
- `[[term]]...[[/term]]`:游戏专有名词,默认蓝色 `#52B6FF`。
|
||||
- `[[key]]...[[/key]]`:玩家必须关注的信息,优先级高于专名。
|
||||
- `[[comma-pause]]`:可配置的 0.2 秒逗号停顿。
|
||||
- `[[ulm]]`:FP_Peipei2 中每条活动 `ULM自训练进程:` 文本。
|
||||
- `[[joke]]...[[/joke]]`:火山实际讲出的笑话,不包括讨论笑话的说明对白。
|
||||
- 本地输出放入已忽略的 `Temp/YarnTextAudit`;修改前保存文件 SHA-256,应用时必须精确匹配原文与哈希。
|
||||
|
||||
## 自检
|
||||
|
||||
```powershell
|
||||
python Tools/yarn_text_audit.py self-test
|
||||
python Tools/yarn_text_audit.py verify --output Temp/YarnTextAudit
|
||||
```
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
name: narrative-timeline-flow
|
||||
description: >-
|
||||
叙事 Timeline:aseprite/静图改 Timeline、Addressable 登记、黑屏防穿帮、替换 show_full_screen。
|
||||
Use when 加叙事 timeline、aseprite 转 timeline、英里初登场、CG 演出、替换 show_full_screen、
|
||||
hide_full_screen、黑屏穿帮、fade_out 时机、wait_timeline、Day1 begin / Day2 night 天桥 CG、
|
||||
或用户提到 TimelineKit 叙事接入。
|
||||
---
|
||||
|
||||
# 叙事 Timeline 接入(Aseprite / 静图 → Yarn)
|
||||
|
||||
API 细节见 [`Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md`](../../Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md)。本 skill 只记**本仓库约定与踩坑**。
|
||||
|
||||
## 1. 何时用哪种 Yarn 命令
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 播完再往下走 | `<<play_timeline 名>>`(阻塞) |
|
||||
| 黑屏下先起播 / 与对白并行 / Loop CG | `<<start_timeline 名>>` |
|
||||
| 已 `start_timeline`,要等播完 | `<<wait_timeline 名>>`(不重头播) |
|
||||
| 关掉整棵 Director GO | `<<hide_timeline 名>>` |
|
||||
|
||||
名称与场景里 `DirectorHandler.timelineName` **完全一致**(叙事多用中文单名,内嵌 Playable)。
|
||||
|
||||
## 2. 资产与 Addressable
|
||||
|
||||
1. Aseprite → `Assets/GameContent/Scene_{场景}/animationRaw/`(或现有 `AnimationRaw/`)
|
||||
2. Timeline → `GameContent/Scene_{场景}/Timeline/{名}.playable`
|
||||
3. Addressable Group = 对应场景线(如 `Scene_ClinicOut`、`Scene_Bridge`)
|
||||
4. Address = `Timeline/{名}`(必须带 `Timeline/` 前缀)
|
||||
5. 叙事 Timeline **一般不**加 `TimelineAssetRef`(维修玩法才用)
|
||||
|
||||
参考范本:`医生出地铁`、`day2夕阳医生看佩佩侧脸`、`英里初登场`。
|
||||
|
||||
## 3. 场景 GO 约定
|
||||
|
||||
- 根节点挂 `PlayableDirector`(`Play On Awake = false`)+ `DirectorHandler`
|
||||
- **根节点默认 Active**:`Start` 里要注册到 `TimelineCenter`;关掉根节点会注册失败
|
||||
- **带 Sprite 的子物体(如 `Visual`)默认 Inactive**;用 ActivationTrack 或播放时再显
|
||||
- `hide_timeline` = 根 `SetActive(false)`;之后再播时 `PlayTimeline` 会重新打开
|
||||
- 全屏 CG 常需调位置(例:天桥侧脸根节点 `Y = -5`);PPU / scale 按相机再调
|
||||
|
||||
## 4. 黑屏防穿帮(必做)
|
||||
|
||||
**错误**:`fade_out` 揭开场景 → 再 `play_timeline`(先露底再播 CG)。
|
||||
|
||||
**正确**(切场景后的 intro CG):
|
||||
|
||||
```yarn
|
||||
<<fade_in>>
|
||||
<<load_scene Scene/ClinicOut>>
|
||||
<<init_actor ...>>
|
||||
<<start_timeline 英里初登场>>
|
||||
<<wait 0.2>>
|
||||
<<fade_out>>
|
||||
<<wait_timeline 英里初登场>>
|
||||
<<hide_timeline 英里初登场>>
|
||||
<<show_actor ...>>
|
||||
<<play_timeline 医生出地铁>>
|
||||
```
|
||||
|
||||
要点:黑屏内起播并稳住首帧 → 再结束黑屏 → 再等播完。CG 盖住初登场时,角色可延后到 `hide_timeline` 后再 `show_actor`。
|
||||
|
||||
## 5. 替换 `show_full_screen` 静图
|
||||
|
||||
常驻 / Loop 演出(对白期间一直播):
|
||||
|
||||
```yarn
|
||||
<<start_timeline day2夕阳医生看佩佩侧脸>>
|
||||
<<fade_out 1>>
|
||||
# ... 对白 ...
|
||||
<<fade_in 1>>
|
||||
<<hide_timeline day2夕阳医生看佩佩侧脸>>
|
||||
<<fade_out 1>>
|
||||
```
|
||||
|
||||
不要再用 `show_full_screen` / `hide_full_screen` 播同一段。
|
||||
|
||||
## 6. 其它坑
|
||||
|
||||
- `<<load_scene>>` **只需场景名**。旧脚本第二参数(如 `1`)已兼容忽略,**新代码不要写**。
|
||||
- 改 `.yarn`:**不要动**已有 `#line:`;新行不加 `#line:`。
|
||||
- 只改运行时实际绑定的 Yarn(如 Day1_begin → FP,不是 Fiction 草稿)。
|
||||
- Aseprite 多 tag:选主演出 clip;Loop CG 用循环 clip + Director `Loop` 或长轨;一次性 intro 用有限时长 + `wait_timeline`。
|
||||
|
||||
## 7. 落地检查清单
|
||||
|
||||
- [ ] Timeline 在正确 `GameContent/Scene_* /Timeline/`
|
||||
- [ ] Addressable:`Timeline/{名}` + 正确 Group
|
||||
- [ ] `timelineName` 与 Yarn 一致;`Play On Awake = false`
|
||||
- [ ] Visual 默认关;进 Play 不会提前露 CG
|
||||
- [ ] 切场景 intro:黑屏内 `start` → 短 wait → `fade_out` → `wait_timeline`
|
||||
- [ ] 静图替换:`start` / `hide` 成对
|
||||
- [ ] 跑一次确认无先露场景、无 `load_scene` 参数错误
|
||||
+1
-2
@@ -75,8 +75,7 @@
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.psb filter=lfs diff=lfs merge=lfs -text
|
||||
*.bank filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# DOTween editor images - exclude from LFS (stored as regular binaries, Unity re-import causes false changes)
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.png -filter -diff -merge
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
|
||||
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
|
||||
+1
-3
@@ -110,6 +110,4 @@ fmod_editor.log
|
||||
.history/*
|
||||
**/.history/*
|
||||
|
||||
.fake
|
||||
Keystore/aibis-release-key.keystore
|
||||
Keystore/keystore.bat
|
||||
.fake
|
||||
Vendored
+1
-65
@@ -56,70 +56,6 @@
|
||||
"temp/": true,
|
||||
"Temp/": true
|
||||
},
|
||||
"files.associations": {
|
||||
"**/Plugins/Android/*Template.gradle": "plaintext"
|
||||
},
|
||||
"dotnet.defaultSolution": "aibis-dream.sln",
|
||||
"dotnet.preferCSharpExtension": true,
|
||||
"cSpell.enableFiletypes": [
|
||||
"yarnspinner"
|
||||
],
|
||||
"cSpell.languageSettings": [
|
||||
{
|
||||
"languageId": "yarnspinner",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "yarn-commands",
|
||||
"pattern": "/<<[^>]*>>/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-expressions",
|
||||
"pattern": "/\\{[^}]*\\}/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-variables",
|
||||
"pattern": "/\\$[a-zA-Z_][a-zA-Z0-9_]*/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-markup",
|
||||
"pattern": "/\\[[^\\]]*\\]/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-hashtags",
|
||||
"pattern": "/#[a-zA-Z_][a-zA-Z0-9_:-]*/g"
|
||||
},
|
||||
{
|
||||
"name": "yarn-node-markers",
|
||||
"pattern": "/^(---|===)$/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-headers",
|
||||
"pattern": "/^(title|tags|position|color|colorID|tracking|custom|style):.*/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-character-names",
|
||||
"pattern": "/^\\s*[A-Za-z_][A-Za-z0-9_]*(?=:)/gm"
|
||||
},
|
||||
{
|
||||
"name": "yarn-comments",
|
||||
"pattern": "/\\/\\/.*$/gm"
|
||||
}
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"yarn-commands",
|
||||
"yarn-expressions",
|
||||
"yarn-variables",
|
||||
"yarn-markup",
|
||||
"yarn-hashtags",
|
||||
"yarn-node-markers",
|
||||
"yarn-headers",
|
||||
"yarn-character-names",
|
||||
"yarn-comments"
|
||||
]
|
||||
}
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Aibis",
|
||||
"Newtonsoft"
|
||||
]
|
||||
"dotnet.preferCSharpExtension": true
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
AIBIS Dream (also known as "AllOurBrokenParts") is a Unity-based narrative adventure game with investigative elements. The project uses Yarn Spinner for dialogue, features a custom framework with modular systems, and includes multiple mini-games and a modular repair/fix system.
|
||||
|
||||
**Unity Version:** 2022.3.7f1c1 (Universal Render Pipeline)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Framework (Assets/Scripts/Framework/)
|
||||
|
||||
Modular framework with Kit-pattern utilities:
|
||||
|
||||
- **ActionKit**: Action sequencing and command pattern
|
||||
- **AudioKit**: Audio management (includes FMOD integration)
|
||||
- **Config / Core**: Configuration and core utilities
|
||||
- **EventSystemKit**: Decoupled event system
|
||||
- **LogKit**: Structured logging with categories
|
||||
- **PoolKit**: Object pooling
|
||||
- **ResourceKit**: Asset loading via `ResourceSystem` (Addressables-based)
|
||||
- **Note**: New code must use `ResourceSystem`, never `Resources.Load`. See [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md). Asset placement rules in [Resource Management Spec](Docs/ResourceManagementSpec.md).
|
||||
- **SelectorKit**: Weighted/batch/non-repeat random selection
|
||||
- **SingletonKit**: Singleton base classes
|
||||
- **StateMachineKit**: State management
|
||||
- **OtherKit**: Miscellaneous utilities (`CameraKit`, `AnimatorKit`, `FadeKit`, `LocalizationKit`, etc.)
|
||||
- **Attribute**: Custom editor attributes
|
||||
|
||||
### Key Game Systems
|
||||
|
||||
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
|
||||
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
|
||||
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
|
||||
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
|
||||
- **MiniGame** (Scripts/MiniGame/): `HuoShan/` (language particles, EmotionWave), `BlockPuzzle/`, `Peipei/` (Eye, CutLine, Memory)
|
||||
- **FixSystem** (Scripts/FixSystem/): Legacy — HeatMap, PunchTape, Memory, screen effects
|
||||
- **FixSystemNew** (Scripts/FixSystemNew/): Active repair system — `FixSystemCenter`, `BodyModule/`, `Cable/`, `WhackMole/`, `Screen System/`, `Open/`, `Clinic/`
|
||||
- **AssetRefs** (Scripts/AssetRefs/): Addressable key constants (`ConstRef`, `FixAssetRef`, `TimelineAssetRef`)
|
||||
|
||||
### Content & Localization
|
||||
|
||||
- **Assets/GameContent/**: Runtime assets by scope (`Common/`, `UI/`, `Scene_{Name}/`, `Feature_{Name}/`)
|
||||
- **Assets/Language/**: Unity Localization tables (runtime active: `UIText/`, `Locales/`, `Sprite/`; `Dialog/`、`ActorName/`、`Params/` currently kept as migration backups)
|
||||
- **Assets/Resources/Yarn/**: Current Yarn dialogue files (target migration to `Assets/Narrative/` per spec, not yet complete)
|
||||
- **Assets/StreamingAssets/Config/**: CSV-driven localized gameplay data (`character.csv` for actor names, `params.csv` for `l10n.*` substitution terms)
|
||||
- **Assets/Prototype/**: HTML/JS prototypes for mechanic testing
|
||||
|
||||
## Critical Dependencies
|
||||
|
||||
### Unity Packages
|
||||
- **Yarn Spinner** 3.2.2 (OpenUPM), **Addressables** 1.21.21, **Cinemachine** 2.9.7
|
||||
- **URP** 14.0.8, **TextMesh Pro** 3.0.6, **Timeline** 1.7.5, **Localization** 1.4.5
|
||||
- **Visual Effect Graph** 14.0.8, **Visual Scripting** 1.8.0
|
||||
|
||||
### Third-Party
|
||||
- **FMOD** (audio engine), **DOTween** (tweening), **Shapes** (vector graphics)
|
||||
- **More Mountains Feedbacks** (game feel), **Destructible 2D**
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Organization
|
||||
1. Each major system self-contained in its own folder under `Assets/Scripts/`
|
||||
2. Kit-pattern utilities in `Framework/` — reusable across systems
|
||||
3. Singleton pattern for global managers (derived from `SingletonKit`)
|
||||
4. Favor composition over inheritance
|
||||
5. All runtime asset loading via `ResourceSystem` — never `Resources.Load`
|
||||
|
||||
### Scene Structure
|
||||
- Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader
|
||||
- Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`)
|
||||
- Test scenes: `languageTest`, `HuoShanExpressionTest`, `wavetest`, etc.
|
||||
|
||||
### Addressables
|
||||
- Config in `Assets/AddressableAssetsData/`; Groups: `Core`, `Scene_*`, `Feature_*`, `Shared_*`, `Localization-*`
|
||||
- Key format: `Category/Path` (e.g. `Animation/Peipei`, `Timeline/BlockPuzzle/PanelOpen`)
|
||||
- Scene keys use bare PascalCase names (e.g. `ClinicOut`)
|
||||
- Details in [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md)
|
||||
|
||||
### Yarn Dialogue Files
|
||||
- Current path: `Assets/Resources/Yarn/` (migration to `Assets/Narrative/Yarn/` planned)
|
||||
- Pattern: `{Character}_{Stage}.yarn`; UTF-8 encoding required for Chinese
|
||||
|
||||
#### Yarn Line Tag Maintenance
|
||||
- **Line tags** (`#line:xxxxxxx`) must be unique across the entire file. Duplicate tags break Yarn Spinner localization.
|
||||
- Duplicates typically occur when copying an existing node to create a variant (e.g. `地铁ver1` → `地铁ver3`).
|
||||
- **Use the dedup tool** (`Tools/dedup_yarn_tags.py`) for bulk cleanup:
|
||||
```bash
|
||||
# Preview duplicates from a specific node onward
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-node 地铁ver4
|
||||
# Fix duplicates from a specific line onward
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-line 535 --fix
|
||||
# Scan an entire directory (preview only)
|
||||
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/ --scan-all
|
||||
```
|
||||
|
||||
#### Yarn Script Generation
|
||||
- **Do NOT add `#line:` tags when generating or editing Yarn dialogue.** Leave all line-tag generation to the user's localization workflow (Unity Localization + Yarn Spinner import tools).
|
||||
- When creating new `.yarn` files, write plain dialogue lines without any `#line:` suffixes.
|
||||
|
||||
### Asset Naming
|
||||
- **Scripts**: PascalCase (e.g. `DialogController.cs`)
|
||||
- **Addressable Keys**: `Category/Path` — no spaces, no extensions, ≤3 levels
|
||||
- **Addressable Groups**: `{Type}_{PascalCaseName}`
|
||||
|
||||
### Git & Commits
|
||||
- LFS for large binaries; ignored: Library/, Temp/, Build/, *.csproj, *.sln
|
||||
- **Commit format**: `<type>(<scope>): <subject>` — subject in Chinese, type/scope in English
|
||||
- Extended types: `feat`, `fix`, `art`, `audio`, `scene`, `yarn`, `perf`, `refactor`, `style`, `docs`, `build`, `chore`, `test`
|
||||
- See [COMMIT_CONVENTION.md](COMMIT_CONVENTION.md) for full details
|
||||
|
||||
## Key Build Commands
|
||||
|
||||
```bash
|
||||
# Windows build
|
||||
"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" -batchmode -quit -projectPath . -buildWindows64Player "Build/AIBIS_Dream.exe" -logFile Build/log.txt
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Mini-Game
|
||||
1. Create `Assets/Scripts/MiniGame/{GameName}/`
|
||||
2. Add assets to `Assets/GameContent/Feature_{GameName}/`
|
||||
3. Create Addressable Group `Feature_{GameName}`
|
||||
4. Create test scene in `Assets/Scenes/`
|
||||
|
||||
### Adding Yarn Dialogue
|
||||
1. Create `.yarn` in `Assets/Resources/Yarn/{Module}/`
|
||||
2. Ensure YarnProject localization assets/CSV are updated (Unity `Assets/Language/Dialog/` is backup-only during migration)
|
||||
3. Reference in scene's `DialogueRunner`; test with `DialogController`
|
||||
|
||||
## Debugging & Performance
|
||||
- `LogKit` for structured logging; Addressables Event Viewer for resource debugging
|
||||
- Monitor `SceneResourceLoader.TotalHandleCount` for leak detection
|
||||
- Always use PoolKit for frequent instantiation; `LoadUncachedAsync` + `EarlyRelease` for large Timeline assets
|
||||
- Use `LoadAsync<GameObject>` + `Instantiate` for prefabs, never `InstantiateAsync`
|
||||
|
||||
## Important Notes
|
||||
- Narrative-focused game — dialogue and story take precedence
|
||||
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
|
||||
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
|
||||
|
||||
## Reference Documents
|
||||
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
|
||||
- [Resource Management Spec](Docs/ResourceManagementSpec.md) — Asset placement: `GameContent/`, `Narrative/`, `_Archive/`
|
||||
- [Commit Convention](COMMIT_CONVENTION.md) — Commit message format and branch naming
|
||||
- [HuoShan Docs](Docs/HuoShan/) — Narrative flow, resources, test flowcharts
|
||||
|
||||
---
|
||||
|
||||
*This document provides guidance for Codex when working with the AIBIS Dream codebase.*
|
||||
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 09546aca93a801441859af2a811fe53b
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: e92b9b9776d0601449874b8c1b0ed1ae
|
||||
Hash: 8e8b410f1714c542808a3bfbe0724666
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_BundleLocalCatalog: 0
|
||||
@@ -59,15 +59,13 @@ MonoBehaviour:
|
||||
- {fileID: 11400000, guid: 6d259d339c6615248b893837398c1438, type: 2}
|
||||
- {fileID: 11400000, guid: 8718a71c99923e247a960cbf4e4795f5, type: 2}
|
||||
- {fileID: 11400000, guid: 1e521cfc13b314642b1e682e3be7ea17, type: 2}
|
||||
- {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
|
||||
- {fileID: 11400000, guid: 724eed66b2d8eef4faf77a4bee4d2845, type: 2}
|
||||
- {fileID: 11400000, guid: 62a92c74b9bd5bd439690727c1831500, type: 2}
|
||||
- {fileID: 11400000, guid: b593dc98ea11a484995123de77d9bf1a, type: 2}
|
||||
- {fileID: 11400000, guid: d582f9080605ab34cbcde3249c980030, type: 2}
|
||||
- {fileID: 11400000, guid: 54799118a585dda439cab043d56ec68c, type: 2}
|
||||
- {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
- {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
- {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
|
||||
m_BuildSettings:
|
||||
m_CompileScriptsInVirtualMode: 0
|
||||
m_CleanupStreamingAssetsAfterBuilds: 1
|
||||
@@ -114,7 +112,6 @@ MonoBehaviour:
|
||||
- Locale-en
|
||||
- Locale-zh-Hans
|
||||
- SceneSO
|
||||
- Locale-ja-JP
|
||||
m_SchemaTemplates: []
|
||||
m_GroupTemplateObjects:
|
||||
- {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2}
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: bac626c44cee44544a45a750681e92e0
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 8b2d41616f1a75840a5800ee28d1c4ed
|
||||
m_Address: sprite_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 842c6a62aa3abdf4d9b230f249b9fd76, type: 2}
|
||||
- {fileID: 11400000, guid: 1706e28ae8e595149b2ac1fbea7f8071, type: 2}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af94af36950b08d4f9a5037546bbe040
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -15,11 +15,17 @@ MonoBehaviour:
|
||||
m_GroupName: Localization-Assets-English (en)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 59bf392f3aef3b24da46dd8d3fa9259f
|
||||
m_SerializeEntries: []
|
||||
m_GUID: 50aef5699023b21459e68677a9449d80
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
|
||||
m_Address: discord-square-color-icon
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: fa74e99b946159549971be154369b2cb, type: 2}
|
||||
- {fileID: 11400000, guid: 8e1cbbf76f0150145a39ce648fbf3985, type: 2}
|
||||
- {fileID: 11400000, guid: 895c7c97ef5802446a395377ef704940, type: 2}
|
||||
- {fileID: 11400000, guid: 8004f70cd10ce4a439cb9e822bb078c2, type: 2}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60fd2fd8c7094b242b01c63de65de9ea
|
||||
guid: aafc8258a2111e94d8339a3152cf50e0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
@@ -22,6 +22,145 @@ MonoBehaviour:
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 37c291d6dce732d45afb2e2b62218683
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_begin/FP_Day1_begin Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c1de143c98cd0804ab3b7b9a36fcf06a
|
||||
m_Address: Assets/Language/ActorName/ActorName Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 76902d55aa2d3474aa96c8e574b8212f
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei1/FP_Peipei1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0cb567219be64c04b9552ecc61f8396a
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_night/Fiction_Day1_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 685539ac5030ab5409faa8feccba91e2
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_mid/FP_Day1_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4f3df7a54674e8343b4699544a25db67
|
||||
m_Address: Assets/Language/Dialog/FP_Shitou2/FP_Shitou2 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2c7f3692831aa5f4daa13d00e99ad77c
|
||||
m_Address: Assets/Language/Dialog/FP_Shitou1/FP_Shitou1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 949f8d1a26133a94c83ac9cc7e6d94c7
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei2/FP_Peipei2 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 59e2a0d8fc60a784993a6f6cdfeb9c7d
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_night/FP_Day1_night Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 831d81945a7dc8d498ccebcdd4d05044
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_mid/FP_Day2_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 45899a60a64d58a4f81fad32d2171702
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_night/FP_Day2_night Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: ea388b59e37ddc14da4d8e3e9ead4133
|
||||
m_Address: Assets/Language/Dialog/Test_Huoshan/Test_Huoshan Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8f3dfd038e75c3a41a865ab8dd730809
|
||||
m_Address: Assets/Language/Dialog/FP_Prologue/FP_Prologue Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 011b21a8c75449e4a982549cebc1a6ca
|
||||
m_Address: Assets/Language/Params/Params Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f6d220fec7b888b4f89463b5da038e62
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_mid/Fiction_Day1_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c5f887d31e1d7bc46b8a7ab784f19b9a
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_begin/Fiction_Day1_begin Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 578cb0ae76a180d45ac70e058c676ac7
|
||||
m_Address: Assets/Language/Dialog/FP_Peipei3/FP_Peipei3 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f5108536d82f2c14a922f0a1d3e83638
|
||||
m_Address: Assets/Language/Dialog/FP_Day3_mid/FP_Day3_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0ee8d3542a2395745b4ad6bf55e3b097
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_mid/Fiction_Day2_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 771ed3ef48efbb645827ce5c29d63754
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_night/Fiction_Day2_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: aaa27d8b7dfc3fc4294bda026ae135f2
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day3_mid/Fiction_Day3_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3c243fd229013b84e9c88091eaa74103
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day3_night/Fiction_Day3_night Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bb8e2763fb266ff4d8b9c0b0f37882e2
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day4_mid/Fiction_Day4_mid Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bb6fa95706f665c469e270567be5cfe9
|
||||
m_Address: Assets/Language/Dialog/Fiction_Huoshan1/New Table Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 042ab1d25056cbd4984621037ac848f0
|
||||
m_Address: Assets/Language/Dialog/Fiction_Yingli1/Fiction_Yingli1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 70f8ff033cddcd543a618974781d0bb0
|
||||
m_Address: Assets/Language/Dialog/FP_Day3_night/FP_Day3_night Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 08cb937dd06038b4486d6a3662539542
|
||||
m_Address: Assets/Language/Dialog/Test_GeneralEnd/New Table Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9abdd56d318b1ec459c7e866748d6878
|
||||
m_Address: Assets/Language/Sprite/sprite Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
@@ -33,22 +172,47 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
- Locale-zh-Hans
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4a2d391471bd9cd439150c52659e9846
|
||||
m_Address: Assets/Language/Params/Params Shared Data.asset
|
||||
- m_GUID: dd4a0fe1d258fbc4384ab1db825d5ae3
|
||||
m_Address: Gts1/Fiction_Gts1 Shared Data.asset/Fiction_Gts1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
|
||||
m_Address: discord-square-color-icon
|
||||
- m_GUID: 57cfa4d941133b840b4345cf815dda66
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_begin/Fiction_Day2_begin Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
- Locale-ja-JP
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 7ea8c68fdc624de4c86434963c06ec55
|
||||
m_Address: Assets/Language/ChapterInfo/ChapterInfo Shared Data.asset
|
||||
- m_GUID: c3d23bd971e02d34cb356b2029523427
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day1_sleep/Fiction_Day1_sleep Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e3c75f951d9792449906a3a8010d2e74
|
||||
m_Address: Assets/Language/Dialog/Fiction_Day2_sleep/Fiction_Day2_sleep Shared
|
||||
Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 28396c5e50d205645b94b89018d01a2a
|
||||
m_Address: Assets/Language/Dialog/Ficion_Shitou1/Fiction_Shitou1 Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 73c880a15817b7f408ad0596f07a3626
|
||||
m_Address: Assets/Language/Dialog/FP_Day1_sleep/FP_Day1_sleep Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: afa9b26bd11b9d847bbb9714b8049a4b
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_begin/FP_Day2_begin Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 90d6cbb308df1484f9146a0ad156202e
|
||||
m_Address: Assets/Language/Dialog/FP_Day2_sleep/FP_Day2_sleep Shared Data.asset
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -29,12 +29,6 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d3da569ceb2f3740bd6f4fd01e51c62
|
||||
m_Address: Japanese (Japan) (ja-JP)
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
+195
-3
@@ -23,14 +23,206 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: aeb318c895d57014b89d5540c6baff95
|
||||
- m_GUID: c21a8fbe9bf147649b5b01320ada3f11
|
||||
m_Address: FP_Day1_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0b504f7f6402d8747896b0cf19674e69
|
||||
m_Address: ActorName_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 247e0ac5e48cbe24ea7e228e1a039590
|
||||
m_Address: FP_Peipei1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d6149a0fedb6cc24ba2a318aedab1de0
|
||||
m_Address: FP_Day1_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8a56c6bff47205b439b07425f6f5ad94
|
||||
m_Address: FP_Shitou2_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 32954c9c2dce35e42a31befcec1e861f
|
||||
m_Address: FP_Shitou1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 860bd75190298994b9d5dee30aebd910
|
||||
m_Address: FP_Peipei2_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bfc0211d0386bc9468ecace61129ecd5
|
||||
m_Address: FP_Day1_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 204ffaeec9d0f7e4cb708476b4d459a2
|
||||
m_Address: FP_Day2_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2516ada36f949234caf993f1deaf79e6
|
||||
m_Address: FP_Day2_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 523b3962bff363849a36683b375adc06
|
||||
m_Address: Test_Huoshan_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 83f5756a759af6f459aa1871eb248f5b
|
||||
m_Address: FP_Prologue_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 5d4915d0761772c41902e704e2ecfd0b
|
||||
m_Address: Params_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: bdf8ad79a9f707b45805e23c25ccdadb
|
||||
m_Address: ChapterInfo_zh-Hans
|
||||
- m_GUID: fb51befc2226dfa44bbde7203364c019
|
||||
m_Address: Fiction_Day2_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 6603078d8eb41524e9ad2edd5d182ccc
|
||||
m_Address: Fiction_Day1_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 72755a9b5a5ca5c4e92028ca44c66a6d
|
||||
m_Address: FP_Peipei3_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c260e1e2604154648971c06353edad0e
|
||||
m_Address: FP_Day3_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a75bb4d4a30c22f40ad5c7d70c8276d4
|
||||
m_Address: Fiction_Day1_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b2f0a2aa5e0b95346bf499980c2994d4
|
||||
m_Address: Fiction_Day2_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 6c498406bf979ca45b345e5bdbd3cc4b
|
||||
m_Address: Fiction_Day3_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8e5812f1d9f10a4488702f35405d197d
|
||||
m_Address: Fiction_Day3_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 35f017da97194574da316391464204b2
|
||||
m_Address: Fiction_Day4_mid_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 75ed4030af85d794f832919dbe427f51
|
||||
m_Address: Fiction_Huoshan1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4b181f70ec2e65f4cab1545662415306
|
||||
m_Address: Fiction_Yingli1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a525b6d2bcfeb2446b2f4d2b36b043c7
|
||||
m_Address: FP_Day3_night_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1f031d78df49e5645a4ceaafa7283f91
|
||||
m_Address: Test_GeneralEnd_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9cecc6d4b999d5c489b69406176bab99
|
||||
m_Address: Fiction_Gts1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fbfb0ced2ba1bf44e86964158ae53d72
|
||||
m_Address: Fiction_Day1_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 999ab1a8772cb1a4cba0325144ff5488
|
||||
m_Address: Fiction_Day1_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4d88b47f4a7e3f249b44203f411e4adf
|
||||
m_Address: Fiction_Day2_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 03f5785bc02343d448436956c6991b38
|
||||
m_Address: Fiction_Shitou1_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 14a941304f21e2042afd56019acfca6c
|
||||
m_Address: FP_Day1_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: d9f05be3049f65b4c82800b1bde3b7c2
|
||||
m_Address: FP_Day2_begin_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 09898f26fbea1454cb10f2c27e2ca907
|
||||
m_Address: FP_Day2_sleep_zh-Hans
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-zh-Hans
|
||||
|
||||
+195
-3
@@ -23,14 +23,206 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9248cb179e7aafa4c903f1fc54b41ef7
|
||||
- m_GUID: 1e7115110ec21ec4da592999daa0c146
|
||||
m_Address: FP_Day1_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e80667e8ff7367e4ab9bc4f9421271e8
|
||||
m_Address: ActorName_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 08ef2a7dddda7ee49b869bcd73858404
|
||||
m_Address: FP_Peipei1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 30ab319b5e0bafe46aa622bb93381c7e
|
||||
m_Address: FP_Day1_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 927a25bda151fc7469749a3724bef6b0
|
||||
m_Address: FP_Shitou2_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fda6eb219437ffc40ab860aa89d9446d
|
||||
m_Address: FP_Shitou1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 13f447125bbae5e45a4a446c2c9ac397
|
||||
m_Address: FP_Peipei2_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c65e073d94ffbbb4b9b6107dee05e670
|
||||
m_Address: FP_Day1_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 399374eeba6905048a7342d0bb494f9c
|
||||
m_Address: FP_Day2_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: cbc3925d28fc66c4cb3293b7bd66db0b
|
||||
m_Address: FP_Day2_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fb0b8f197b5a607468eeb9326737d547
|
||||
m_Address: Test_Huoshan_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 49a6e4d039261bf42a18782ca317fca5
|
||||
m_Address: FP_Prologue_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0a9b8bbf38d109d409792ae7cc48cec7
|
||||
m_Address: Params_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 8906df9235e52fb41b11fe8776a77032
|
||||
m_Address: ChapterInfo_en
|
||||
- m_GUID: eb9ebf027dd4b564cb035d7167ff8099
|
||||
m_Address: Fiction_Day2_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 705a631ebd09db04fa1bda39e20e505e
|
||||
m_Address: Fiction_Day1_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: fde90ade684482142af49533e4f4ed62
|
||||
m_Address: FP_Peipei3_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 036c8c6ed1ecc314a8e0ae02fe15ab5e
|
||||
m_Address: FP_Day3_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0d2928a7d58c26a4ca78abf2f5da1373
|
||||
m_Address: Fiction_Day1_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 59fc388134f5d884c8d7ca28f2d7017e
|
||||
m_Address: Fiction_Day2_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 379ead226bea4f844b1c136c4f748633
|
||||
m_Address: Fiction_Day3_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: aaf54fb66c2fa9d4ab68626ced632a7a
|
||||
m_Address: Fiction_Day3_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: c0c8b8aea11da444ea41eece5a698e5f
|
||||
m_Address: Fiction_Day4_mid_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2a14b1d82f4aef94aac2b230392605fc
|
||||
m_Address: Fiction_Huoshan1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 79ccbc35144b91a45bb9fdf9732d238a
|
||||
m_Address: Fiction_Yingli1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0e9bb0974017aa346a379b78126f6f7b
|
||||
m_Address: FP_Day3_night_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 27ad0341432ae0b46a8fe117eb5c73e2
|
||||
m_Address: Test_GeneralEnd_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1adbe96b15171134584c9a2d7d69c045
|
||||
m_Address: Fiction_Gts1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d72f86c018628e418747a7d0a66cb21
|
||||
m_Address: Fiction_Day1_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 30a0c1ae8c45da34f8e3f90692e96e11
|
||||
m_Address: Fiction_Day1_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 36d94b46b4a12b54b8e084d8d275e675
|
||||
m_Address: Fiction_Day2_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 265c923a085f98042b44f8b18dd8389b
|
||||
m_Address: Fiction_Shitou1_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: ef76bcd9daf9b6a4580eaa28d2073ebf
|
||||
m_Address: FP_Day1_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a6b685d3b3e17c64da7ca72add7b3540
|
||||
m_Address: FP_Day2_begin_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a0e634b20b52dd34abb7999e889ec772
|
||||
m_Address: FP_Day2_sleep_en
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-en
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Localization-String-Tables-Japanese (Japan) (ja-JP)
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 62cf9265bfa710b4b9152c336336a38e
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 7c757424270c91e43bf45198f69c983a
|
||||
m_Address: UIText_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9ed23d1d910a1a147ba5c316a87cbad4
|
||||
m_Address: Params_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 9d8accfdeabc8034c986233920a17184
|
||||
m_Address: ChapterInfo_ja-JP
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels:
|
||||
- Locale-ja-JP
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 7df396a6b5136fe4e9d2d659731161d2, type: 2}
|
||||
- {fileID: 11400000, guid: 545ed8780a6508047bd42b964feb82fd, type: 2}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc8d31ed80ea35945812ff6552efbbe5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -47,13 +47,8 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f19cff809e7020b41ba8ad7da199db86
|
||||
m_Address: "Timeline/day2\u5915\u9633\u533B\u751F\u770B\u4F69\u4F69\u4FA7\u8138"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f18fc85f0eeecb44b9cfae24e8e19fa1
|
||||
m_Address: "FrameAnimation/\u5929\u6865\u4F69\u4F69"
|
||||
- m_GUID: ea008b0c912c0d748997bf458879cb74
|
||||
m_Address: "Animation/\u5929\u6865\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -17,8 +17,8 @@ MonoBehaviour:
|
||||
m_SerializedData: []
|
||||
m_GUID: 9ae1da1b7a680244cbe735b3b4437f26
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 01b1640d62cd5ff4686088bb3d4c8fba
|
||||
m_Address: "FrameAnimation/\u8BCA\u5BA4\u5916\u4F69\u4F69"
|
||||
- m_GUID: b6a14d8a19b9a3041baf76df2e8e4558
|
||||
m_Address: "Animation/\u8BCA\u5BA4\u5916\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
@@ -72,21 +72,16 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 7760fc67933fb684d81ea5275fac0a50
|
||||
m_Address: "Animation/\u8BCA\u5BA4\u5916\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 253f47db959b2814998695fb5d1f4d97
|
||||
m_Address: "Timeline/\u65CB\u8F6C\u53F6\u67C4"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 1799dbcdb4a76624bb89622707581f92
|
||||
m_Address: "Timeline/\u82F1\u91CC\u521D\u767B\u573A"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 3d0198d0d6d221948a3d5ddf9cd2c34f
|
||||
m_Address: "FrameAnimation/\u8BCA\u5BA4\u5916\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
@@ -32,6 +32,11 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 417a7984630786f46a5ade5d2b457852
|
||||
m_Address: "Animation/\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
|
||||
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
|
||||
m_ReadOnly: 0
|
||||
@@ -82,21 +87,11 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 58d09572ecd54714a787a1fc5aec6c5f
|
||||
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB3"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: e69af1ed498c57f4799f936aa28e4ed4
|
||||
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB1"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: efa335dcb6e706347bd4acf3222331b9
|
||||
m_Address: "FrameAnimation/\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
@@ -32,8 +32,8 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 26105571c9644aa4194f2b981ee33a58
|
||||
m_Address: "FrameAnimation/\u4F69\u4F69"
|
||||
- m_GUID: 809f95af8dd580448ab804b94fbd9cb2
|
||||
m_Address: "Animation/\u4F69\u4F69"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
|
||||
@@ -22,8 +22,23 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b0ee8d48dab31814da87651bba9b5041
|
||||
m_Address: "FrameAnimation/\u5730\u94C1\u533B\u751F"
|
||||
- m_GUID: 2d4ccf1c582289344976ab5c3af05515
|
||||
m_Address: "Animation/\u5730\u94C1\u533B\u751F"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 0057bf4b5fe16aa4b8c7db27d0ddfc0a
|
||||
m_Address: "Animation/\u5730\u94C1\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 40f07326ace570241bd93fcfc3decb87
|
||||
m_Address: "Animation/\u5730\u94C1\u706B\u5C71\u6697"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: a63b52b3e8d9e0441aaff086b9c1ae6b
|
||||
m_Address: "Animation/\u5730\u94C1\u533B\u751F\u6697"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
@@ -42,16 +57,6 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b02d1e0ae4bc971408a0b6a667597624
|
||||
m_Address: "FrameAnimation/\u5730\u94C1\u706B\u5C71"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 66be33a1bdb16ee409c6ac05b61e08a7
|
||||
m_Address: "Subway/\u76F8\u9519\u5217\u8F66"
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
|
||||
m_LoadPath:
|
||||
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||
m_BundleNaming: 1
|
||||
m_AssetLoadMode: 0
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 842c6a62aa3abdf4d9b230f249b9fd76
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
|
||||
m_StaticContent: 0
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1706e28ae8e595149b2ac1fbea7f8071
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1
-1
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-Assets-English (en)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa74e99b946159549971be154369b2cb
|
||||
guid: 895c7c97ef5802446a395377ef704940
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
+1
-1
@@ -12,5 +12,5 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-Assets-English (en)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
|
||||
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
|
||||
m_StaticContent: 0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1cbbf76f0150145a39ce648fbf3985
|
||||
guid: 8004f70cd10ce4a439cb9e822bb078c2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
|
||||
m_LoadPath:
|
||||
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||
m_BundleNaming: 1
|
||||
m_AssetLoadMode: 0
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7df396a6b5136fe4e9d2d659731161d2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
|
||||
m_StaticContent: 0
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 545ed8780a6508047bd42b964feb82fd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -32,11 +32,6 @@ MonoBehaviour:
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: b9e730c7e5824c5ca2858a253fe93c98
|
||||
m_Address: Prefab/FrameAnimationActor
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: cd996317bdc6ae8478acb2de956f25d9
|
||||
m_Address: Prefab/AnimaMobileObject
|
||||
m_ReadOnly: 0
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7273616238501396146
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
|
||||
--- !u!114 &-3378792728507138695
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (1)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: 7967868333749088964}
|
||||
m_Duration: 0.75
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3378792728507138695}
|
||||
m_EaseInDuration: 0.05
|
||||
m_EaseOutDuration: 0.28
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-3117513502812618692
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -7273616238501396146}
|
||||
m_Duration: 0.75
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -3117513502812618692}
|
||||
m_EaseInDuration: 0.05
|
||||
m_EaseOutDuration: 0.28
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-2955631063214815081
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
|
||||
m_Name: Volume Track (2)
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips:
|
||||
- m_Version: 1
|
||||
m_Start: 0
|
||||
m_ClipIn: 0
|
||||
m_Asset: {fileID: -1361877534822022742}
|
||||
m_Duration: 0.75
|
||||
m_TimeScale: 1
|
||||
m_ParentTrack: {fileID: -2955631063214815081}
|
||||
m_EaseInDuration: 0.05
|
||||
m_EaseOutDuration: 0.28
|
||||
m_BlendInDuration: -1
|
||||
m_BlendOutDuration: -1
|
||||
m_MixInCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_MixOutCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
m_BlendInCurveMode: 0
|
||||
m_BlendOutCurveMode: 0
|
||||
m_ExposedParameterNames: []
|
||||
m_AnimationCurves: {fileID: 0}
|
||||
m_Recordable: 0
|
||||
m_PostExtrapolationMode: 0
|
||||
m_PreExtrapolationMode: 0
|
||||
m_PostExtrapolationTime: 0
|
||||
m_PreExtrapolationTime: 0
|
||||
m_DisplayName: VolumeAsset
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_Layer: 0
|
||||
m_Priority: 0
|
||||
--- !u!114 &-1361877534822022742
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
|
||||
m_Name: "\u706B\u5C71\u5934\u75DB3"
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 0
|
||||
m_Tracks:
|
||||
- {fileID: -3117513502812618692}
|
||||
- {fileID: -3378792728507138695}
|
||||
- {fileID: -2955631063214815081}
|
||||
- {fileID: 2112685405309563144}
|
||||
m_FixedDuration: 0
|
||||
m_EditorSettings:
|
||||
m_Framerate: 60
|
||||
m_ScenePreview: 1
|
||||
m_DurationMode: 0
|
||||
m_MarkerTrack: {fileID: 0}
|
||||
--- !u!74 &1077202520131138910
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Recorded
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
m_UseHighQualityCurve: 1
|
||||
m_RotationCurves: []
|
||||
m_CompressedRotationCurves: []
|
||||
m_EulerCurves: []
|
||||
m_PositionCurves: []
|
||||
m_ScaleCurves: []
|
||||
m_FloatCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.07
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.42
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.68
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_PPtrCurves: []
|
||||
m_SampleRate: 60
|
||||
m_WrapMode: 0
|
||||
m_Bounds:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 0, y: 0, z: 0}
|
||||
m_ClipBindingConstant:
|
||||
genericBindings:
|
||||
- serializedVersion: 2
|
||||
path: 0
|
||||
attribute: 304273561
|
||||
script: {fileID: 0}
|
||||
typeID: 212
|
||||
customType: 0
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 0.68
|
||||
m_OrientationOffsetY: 0
|
||||
m_Level: 0
|
||||
m_CycleOffset: 0
|
||||
m_HasAdditiveReferencePose: 0
|
||||
m_LoopTime: 0
|
||||
m_LoopBlend: 0
|
||||
m_LoopBlendOrientation: 0
|
||||
m_LoopBlendPositionY: 0
|
||||
m_LoopBlendPositionXZ: 0
|
||||
m_KeepOriginalOrientation: 0
|
||||
m_KeepOriginalPositionY: 1
|
||||
m_KeepOriginalPositionXZ: 0
|
||||
m_HeightFromFeet: 0
|
||||
m_Mirror: 0
|
||||
m_EditorCurves:
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.07
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.42
|
||||
value: 0.8
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.68
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: m_Color.a
|
||||
path:
|
||||
classID: 212
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
--- !u!114 &2112685405309563144
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
|
||||
m_Name: Animation Track
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_AnimClip: {fileID: 0}
|
||||
m_Locked: 0
|
||||
m_Muted: 0
|
||||
m_CustomPlayableFullTypename:
|
||||
m_Curves: {fileID: 0}
|
||||
m_Parent: {fileID: 11400000}
|
||||
m_Children: []
|
||||
m_Clips: []
|
||||
m_Markers:
|
||||
m_Objects: []
|
||||
m_InfiniteClipPreExtrapolation: 1
|
||||
m_InfiniteClipPostExtrapolation: 1
|
||||
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_InfiniteClipTimeOffset: 0
|
||||
m_InfiniteClipRemoveOffset: 0
|
||||
m_InfiniteClipApplyFootIK: 1
|
||||
mInfiniteClipLoop: 0
|
||||
m_MatchTargetFields: 63
|
||||
m_Position: {x: 0, y: 0, z: 0}
|
||||
m_EulerAngles: {x: 0, y: 0, z: 0}
|
||||
m_AvatarMask: {fileID: 0}
|
||||
m_ApplyAvatarMask: 1
|
||||
m_TrackOffset: 0
|
||||
m_InfiniteClip: {fileID: 1077202520131138910}
|
||||
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_ApplyOffsets: 0
|
||||
--- !u!114 &7967868333749088964
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
|
||||
m_Name: VolumeAsset
|
||||
m_EditorClassIdentifier:
|
||||
m_Template:
|
||||
Weight: 1
|
||||
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58d09572ecd54714a787a1fc5aec6c5f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b51c643e2acc12549a81d17c232253a5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e274bde4016eca842be36e589f6285f6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -244,14 +244,3 @@ MonoBehaviour:
|
||||
m_PreInfinity: 0
|
||||
m_PostInfinity: 0
|
||||
m_RotationOrder: 0
|
||||
- m_From: '**ANY CAMERA**'
|
||||
m_To: Dream Camera
|
||||
m_Blend:
|
||||
m_Style: 0
|
||||
m_Time: 2
|
||||
m_CustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve: []
|
||||
m_PreInfinity: 0
|
||||
m_PostInfinity: 0
|
||||
m_RotationOrder: 0
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 638cd3fa1c2470d42aceb69214c01786
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,260 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream.Kit;
|
||||
using AibisDream.SaveSystem;
|
||||
using FMOD;
|
||||
using FMOD.Studio;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace AibisDream.SystemEditor.Tests
|
||||
{
|
||||
public sealed class AudioGlobalParameterSnapshotTests
|
||||
{
|
||||
[Test]
|
||||
public void SetFromYarn_TracksOnlySuccessfulUniqueNames()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
|
||||
Assert.That(registry.SetFromYarn("reverb", 0.4f), Is.EqualTo(RESULT.OK));
|
||||
Assert.That(registry.SetFromYarn("REVERB", 0.7f), Is.EqualTo(RESULT.OK));
|
||||
Assert.That(
|
||||
registry.SetFromYarn("missing", 1f),
|
||||
Is.EqualTo(RESULT.ERR_EVENT_NOTFOUND));
|
||||
|
||||
Assert.That(registry.TrackedNames, Has.Count.EqualTo(1));
|
||||
Assert.That(registry.TrackedNames.Single(), Is.EqualTo("reverb"));
|
||||
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.False);
|
||||
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CaptureFinalValues_ReadsFmodFinalValueAndRetainsFailuresForRetry()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.Reads["stage"] = new ParameterRead(9f, 2.37f, RESULT.OK);
|
||||
api.Reads["missing"] = new ParameterRead(0f, 0f, RESULT.ERR_EVENT_NOTFOUND);
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
registry.SetFromYarn("stage", 9f);
|
||||
registry.SetFromYarn("missing", 1f);
|
||||
var warnings = new List<string>();
|
||||
|
||||
var snapshot = registry.CaptureFinalValues(warnings.Add);
|
||||
|
||||
Assert.That(snapshot, Has.Count.EqualTo(1));
|
||||
Assert.That(snapshot["stage"], Is.EqualTo(2.37f));
|
||||
Assert.That(snapshot["stage"], Is.Not.EqualTo(9f));
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage", "missing" }));
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("missing", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResetToDefaults_RemovesOnlyParametersThatFmodResetSuccessfully()
|
||||
{
|
||||
var goodId = new PARAMETER_ID { data1 = 1, data2 = 2 };
|
||||
var failedId = new PARAMETER_ID { data1 = 3, data2 = 4 };
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.Descriptions["good"] = new ParameterDescriptionRead(
|
||||
new PARAMETER_DESCRIPTION { id = goodId, defaultvalue = 0.25f },
|
||||
RESULT.OK);
|
||||
api.Descriptions["failed"] = new ParameterDescriptionRead(
|
||||
new PARAMETER_DESCRIPTION { id = failedId, defaultvalue = 0.75f },
|
||||
RESULT.OK);
|
||||
api.SetByIdResults[failedId] = RESULT.ERR_INVALID_PARAM;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
registry.SetFromYarn("good", 1f);
|
||||
registry.SetFromYarn("failed", 1f);
|
||||
var warnings = new List<string>();
|
||||
|
||||
registry.ResetToDefaults(warnings.Add);
|
||||
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "failed" }));
|
||||
Assert.That(api.SetByIdCalls, Has.Count.EqualTo(2));
|
||||
var goodReset = api.SetByIdCalls.Single(call => call.Id.Equals(goodId));
|
||||
Assert.That(goodReset.Value, Is.EqualTo(0.25f));
|
||||
Assert.That(goodReset.IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("failed", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RestoreFinalValues_JumpsImmediatelyThenNextYarnTargetUsesSeekSpeed()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
var warnings = new List<string>();
|
||||
|
||||
registry.RestoreFinalValues(
|
||||
new Dictionary<string, float>
|
||||
{
|
||||
["stage"] = 2.37f,
|
||||
["missing"] = 1f
|
||||
},
|
||||
warnings.Add);
|
||||
registry.SetFromYarn("stage", 3f);
|
||||
|
||||
Assert.That(api.SetByNameCalls, Has.Count.EqualTo(3));
|
||||
Assert.That(api.SetByNameCalls[0].Value, Is.EqualTo(2.37f));
|
||||
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(api.SetByNameCalls[1].Name, Is.EqualTo("missing"));
|
||||
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(api.SetByNameCalls[2].Value, Is.EqualTo(3f));
|
||||
Assert.That(api.SetByNameCalls[2].IgnoreSeekSpeed, Is.False);
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage" }));
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("missing", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AudioSnapshotDto_RoundTripsFinalValuesWithoutChangingSchema()
|
||||
{
|
||||
var source = new SaveSnapshot();
|
||||
source.sections[SnapshotProviderIds.Audio] = new AudioSnapshotDto
|
||||
{
|
||||
ambState = "Dream",
|
||||
yarnGlobalParameters = new Dictionary<string, float>
|
||||
{
|
||||
["reverb"] = 0.42f,
|
||||
["hs1LogStage"] = 2.37f
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(source);
|
||||
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
|
||||
var audio = ((JObject)restored.sections[SnapshotProviderIds.Audio])
|
||||
.ToObject<AudioSnapshotDto>();
|
||||
|
||||
Assert.That(restored.schemaVersion, Is.EqualTo(2));
|
||||
Assert.That(SaveSnapshotSchema.CurrentVersion, Is.EqualTo(2));
|
||||
Assert.That(audio.yarnGlobalParameters["reverb"], Is.EqualTo(0.42f));
|
||||
Assert.That(audio.yarnGlobalParameters["hs1LogStage"], Is.EqualTo(2.37f));
|
||||
}
|
||||
|
||||
private readonly struct SetByNameCall
|
||||
{
|
||||
internal SetByNameCall(string name, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
IgnoreSeekSpeed = ignoreSeekSpeed;
|
||||
}
|
||||
|
||||
internal string Name { get; }
|
||||
internal float Value { get; }
|
||||
internal bool IgnoreSeekSpeed { get; }
|
||||
}
|
||||
|
||||
private readonly struct SetByIdCall
|
||||
{
|
||||
internal SetByIdCall(PARAMETER_ID id, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
Id = id;
|
||||
Value = value;
|
||||
IgnoreSeekSpeed = ignoreSeekSpeed;
|
||||
}
|
||||
|
||||
internal PARAMETER_ID Id { get; }
|
||||
internal float Value { get; }
|
||||
internal bool IgnoreSeekSpeed { get; }
|
||||
}
|
||||
|
||||
private readonly struct ParameterRead
|
||||
{
|
||||
internal ParameterRead(float value, float finalValue, RESULT result)
|
||||
{
|
||||
Value = value;
|
||||
FinalValue = finalValue;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
internal float Value { get; }
|
||||
internal float FinalValue { get; }
|
||||
internal RESULT Result { get; }
|
||||
}
|
||||
|
||||
private readonly struct ParameterDescriptionRead
|
||||
{
|
||||
internal ParameterDescriptionRead(
|
||||
PARAMETER_DESCRIPTION description,
|
||||
RESULT result)
|
||||
{
|
||||
Description = description;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
internal PARAMETER_DESCRIPTION Description { get; }
|
||||
internal RESULT Result { get; }
|
||||
}
|
||||
|
||||
private sealed class FakeFmodGlobalParameterApi : IFmodGlobalParameterApi
|
||||
{
|
||||
internal readonly Dictionary<string, RESULT> SetByNameResults =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<string, ParameterRead> Reads =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<string, ParameterDescriptionRead> Descriptions =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<PARAMETER_ID, RESULT> SetByIdResults = new();
|
||||
internal readonly List<SetByNameCall> SetByNameCalls = new();
|
||||
internal readonly List<SetByIdCall> SetByIdCalls = new();
|
||||
|
||||
public RESULT SetParameterByName(
|
||||
string name,
|
||||
float value,
|
||||
bool ignoreSeekSpeed)
|
||||
{
|
||||
SetByNameCalls.Add(new SetByNameCall(name, value, ignoreSeekSpeed));
|
||||
return SetByNameResults.TryGetValue(name, out var result)
|
||||
? result
|
||||
: RESULT.OK;
|
||||
}
|
||||
|
||||
public RESULT GetParameterByName(
|
||||
string name,
|
||||
out float value,
|
||||
out float finalValue)
|
||||
{
|
||||
if (Reads.TryGetValue(name, out var read))
|
||||
{
|
||||
value = read.Value;
|
||||
finalValue = read.FinalValue;
|
||||
return read.Result;
|
||||
}
|
||||
|
||||
value = 0f;
|
||||
finalValue = 0f;
|
||||
return RESULT.OK;
|
||||
}
|
||||
|
||||
public RESULT GetParameterDescriptionByName(
|
||||
string name,
|
||||
out PARAMETER_DESCRIPTION description)
|
||||
{
|
||||
if (Descriptions.TryGetValue(name, out var read))
|
||||
{
|
||||
description = read.Description;
|
||||
return read.Result;
|
||||
}
|
||||
|
||||
description = default;
|
||||
return RESULT.ERR_EVENT_NOTFOUND;
|
||||
}
|
||||
|
||||
public RESULT SetParameterById(
|
||||
PARAMETER_ID id,
|
||||
float value,
|
||||
bool ignoreSeekSpeed)
|
||||
{
|
||||
SetByIdCalls.Add(new SetByIdCall(id, value, ignoreSeekSpeed));
|
||||
return SetByIdResults.TryGetValue(id, out var result)
|
||||
? result
|
||||
: RESULT.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e120d9cfce5a4723a6e640b3cc3ce29b
|
||||
@@ -1,123 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using AibisDream.Utility;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Yarn.Markup;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream.SystemEditor.Tests
|
||||
{
|
||||
public sealed class AutoNextMetadataTests
|
||||
{
|
||||
[Test]
|
||||
public void AutoNextWithoutParameter_UsesExistingFixedDelay()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextWithParameter_UsesSpecifiedSeconds()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextWithZeroDelay_IsValid()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:0"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextParameter_UsesInvariantCulture()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("auto_next:")]
|
||||
[TestCase("auto_next:abc")]
|
||||
[TestCase("auto_next:-1")]
|
||||
[TestCase("auto_next:NaN")]
|
||||
[TestCase("auto_next:Infinity")]
|
||||
[TestCase("auto_next:2147484")]
|
||||
public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata)
|
||||
{
|
||||
LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效"));
|
||||
|
||||
var lineInfo = LineInfo.Generate(CreateLine(metadata));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
|
||||
}
|
||||
|
||||
[TestCase("auto_next_extra")]
|
||||
[TestCase("auto_next_extra:4.5")]
|
||||
[TestCase("AUTO_NEXT")]
|
||||
[TestCase("AUTO_NEXT:4.5")]
|
||||
public void SimilarMetadata_IsNotRecognized(string metadata)
|
||||
{
|
||||
var line = CreateLine(metadata);
|
||||
|
||||
Assert.That(line.IsAutoSkipLine(), Is.False);
|
||||
Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParameterizedTag_TakesPrecedenceOverPlainTag()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MultipleParameterizedTags_UseFirstAndWarn()
|
||||
{
|
||||
LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签"));
|
||||
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
|
||||
}
|
||||
|
||||
private static LocalizedLine CreateLine(params string[] metadata)
|
||||
{
|
||||
return new LocalizedLine
|
||||
{
|
||||
TextID = "line:auto-next-test",
|
||||
Metadata = metadata,
|
||||
Text = new MarkupParseResult("Test line", new List<MarkupAttribute>())
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d476e493adf4855468ce46409dae6661
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09d2581fcb827614bba6bf5c3279358e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,277 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphEditorWindow : EditorWindow
|
||||
{
|
||||
private ChapterGraphView _graphView;
|
||||
private ChapterGraphInspector _inspector;
|
||||
private IMGUIContainer _inspectorContainer;
|
||||
|
||||
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
|
||||
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
|
||||
private string _rootFolder;
|
||||
private string _currentFolder;
|
||||
private DropdownField _folderDropdown;
|
||||
private readonly Dictionary<string, string> _displayToPath = new();
|
||||
|
||||
[MenuItem(AibisEditorMenus.ChapterGraphEditor)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
|
||||
window.minSize = new Vector2(800, 600);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_graphView == null) return;
|
||||
_graphView.LoadGraph(_currentFolder);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_graphView != null)
|
||||
{
|
||||
_graphView.OnNodeDeleted -= OnNodeDeleted;
|
||||
}
|
||||
if (_inspector != null)
|
||||
{
|
||||
_inspector.OnColorChanged -= OnNodeColorChanged;
|
||||
_inspector.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
// 根容器:纵向排列
|
||||
var root = rootVisualElement;
|
||||
root.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
// 顶部工具栏
|
||||
var toolbar = new Toolbar();
|
||||
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
|
||||
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
|
||||
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
|
||||
|
||||
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
|
||||
|
||||
_folderDropdown = new DropdownField("路径");
|
||||
_folderDropdown.style.width = 160;
|
||||
_folderDropdown.style.marginLeft = 8;
|
||||
_folderDropdown.style.marginRight = 4;
|
||||
RefreshFolderChoices();
|
||||
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
|
||||
toolbar.Add(_folderDropdown);
|
||||
|
||||
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
|
||||
setRootBtn.style.width = 28;
|
||||
toolbar.Add(setRootBtn);
|
||||
|
||||
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
|
||||
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
|
||||
root.Add(toolbar);
|
||||
|
||||
// 主内容区:横向分栏
|
||||
var mainContainer = new VisualElement();
|
||||
mainContainer.style.flexGrow = 1;
|
||||
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||
mainContainer.style.overflow = Overflow.Hidden;
|
||||
|
||||
// 左侧 Inspector 面板
|
||||
var leftPanel = new VisualElement();
|
||||
leftPanel.style.width = 320;
|
||||
leftPanel.style.minWidth = 200;
|
||||
leftPanel.style.maxWidth = 500;
|
||||
leftPanel.style.flexShrink = 0;
|
||||
leftPanel.style.flexDirection = FlexDirection.Column;
|
||||
leftPanel.style.borderRightWidth = 1;
|
||||
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
|
||||
|
||||
// Inspector 标题
|
||||
var header = new Label("章节属性")
|
||||
{
|
||||
style =
|
||||
{
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
fontSize = 14,
|
||||
paddingTop = 8,
|
||||
paddingBottom = 8,
|
||||
paddingLeft = 12,
|
||||
paddingRight = 12,
|
||||
borderBottomWidth = 1,
|
||||
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
|
||||
}
|
||||
};
|
||||
leftPanel.Add(header);
|
||||
|
||||
// IMGUI Inspector
|
||||
_inspector = new ChapterGraphInspector();
|
||||
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
|
||||
_inspectorContainer.style.flexGrow = 1;
|
||||
leftPanel.Add(_inspectorContainer);
|
||||
|
||||
mainContainer.Add(leftPanel);
|
||||
|
||||
// 拖拽条
|
||||
var resizer = new VisualElement();
|
||||
resizer.style.width = 5;
|
||||
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
|
||||
mainContainer.Add(resizer);
|
||||
|
||||
float startWidth = 0;
|
||||
float startMouseX = 0;
|
||||
resizer.RegisterCallback<MouseDownEvent>(evt =>
|
||||
{
|
||||
startWidth = leftPanel.resolvedStyle.width;
|
||||
startMouseX = evt.mousePosition.x;
|
||||
resizer.CaptureMouse();
|
||||
evt.StopPropagation();
|
||||
});
|
||||
resizer.RegisterCallback<MouseMoveEvent>(evt =>
|
||||
{
|
||||
if (!resizer.HasMouseCapture()) return;
|
||||
var delta = evt.mousePosition.x - startMouseX;
|
||||
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
|
||||
leftPanel.style.width = newWidth;
|
||||
});
|
||||
resizer.RegisterCallback<MouseUpEvent>(evt =>
|
||||
{
|
||||
if (resizer.HasMouseCapture())
|
||||
resizer.ReleaseMouse();
|
||||
});
|
||||
|
||||
// 右侧 GraphView
|
||||
// 套一层容器:GraphView 与左侧 Inspector 并列放在 Flex Row 中时,
|
||||
// 内置 RectangleSelector 绘制的框选矩形会出现坐标偏移(已知 Unity 问题)。
|
||||
// 将 GraphView 放在独立的 Flex 子容器中可使其局部原点与父容器对齐,规避该偏移。
|
||||
var graphViewWrapper = new VisualElement();
|
||||
graphViewWrapper.style.flexGrow = 1;
|
||||
graphViewWrapper.style.flexDirection = FlexDirection.Column;
|
||||
graphViewWrapper.style.overflow = Overflow.Hidden;
|
||||
|
||||
_graphView = new ChapterGraphView();
|
||||
_graphView.style.flexGrow = 1;
|
||||
graphViewWrapper.Add(_graphView);
|
||||
|
||||
mainContainer.Add(graphViewWrapper);
|
||||
|
||||
root.Add(mainContainer);
|
||||
|
||||
// 加载图数据
|
||||
_graphView.LoadGraph(_currentFolder);
|
||||
|
||||
// 选中节点同步到 Inspector
|
||||
_graphView.OnNodeSelected += OnNodeSelected;
|
||||
_graphView.OnNodeDeleted += OnNodeDeleted;
|
||||
_inspector.OnColorChanged += OnNodeColorChanged;
|
||||
}
|
||||
|
||||
private void OnNodeDeleted(TalkSceneSO so)
|
||||
{
|
||||
_inspector?.SetTarget(null);
|
||||
_inspectorContainer?.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
private void OnNodeColorChanged(TalkSceneSO so)
|
||||
{
|
||||
_graphView?.UpdateNodeColor(so);
|
||||
}
|
||||
|
||||
private Button CreateToolbarButton(string text, System.Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = text };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void OnSaveClicked()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
|
||||
}
|
||||
|
||||
private void OnAutoLayoutClicked()
|
||||
{
|
||||
_graphView?.AutoLayout();
|
||||
}
|
||||
|
||||
private void OnValidateClicked()
|
||||
{
|
||||
_graphView?.ValidateGraph();
|
||||
}
|
||||
|
||||
private void OnCreateChapterClicked()
|
||||
{
|
||||
var center = new Vector2(position.width / 2f, position.height / 2f);
|
||||
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
|
||||
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
|
||||
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
|
||||
}
|
||||
|
||||
private void OnNodeSelected(TalkSceneSO so)
|
||||
{
|
||||
_inspector?.SetTarget(so);
|
||||
_inspectorContainer?.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
private void RefreshFolderChoices()
|
||||
{
|
||||
_displayToPath.Clear();
|
||||
_displayToPath["All"] = null;
|
||||
|
||||
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
|
||||
foreach (var folder in subFolders)
|
||||
{
|
||||
var name = Path.GetFileName(folder);
|
||||
_displayToPath[name] = folder;
|
||||
}
|
||||
|
||||
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
|
||||
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
||||
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
|
||||
|
||||
if (rootHasSO)
|
||||
{
|
||||
_displayToPath["(Root)"] = _rootFolder;
|
||||
}
|
||||
|
||||
_folderDropdown.choices = _displayToPath.Keys.ToList();
|
||||
|
||||
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
|
||||
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
|
||||
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
|
||||
}
|
||||
|
||||
private void OnFolderChanged(ChangeEvent<string> evt)
|
||||
{
|
||||
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
|
||||
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
|
||||
_graphView?.LoadGraph(_currentFolder);
|
||||
}
|
||||
|
||||
private void OnSetRootFolderClicked()
|
||||
{
|
||||
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var absoluteRoot = Application.dataPath.Replace('\\', '/');
|
||||
var selected = path.Replace('\\', '/');
|
||||
if (!selected.StartsWith(absoluteRoot))
|
||||
{
|
||||
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
|
||||
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
|
||||
RefreshFolderChoices();
|
||||
_graphView?.LoadGraph(_currentFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c30bb2152169f544cb672c29ac8cb5d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,70 +0,0 @@
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphInspector
|
||||
{
|
||||
private Editor _editor;
|
||||
private TalkSceneSO _target;
|
||||
private Vector2 _scrollPosition;
|
||||
|
||||
public event System.Action<TalkSceneSO> OnColorChanged;
|
||||
|
||||
public void SetTarget(TalkSceneSO so)
|
||||
{
|
||||
// 不能用 ==,Unity 重载了 ==,已销毁的对象 == null 为 true
|
||||
if (ReferenceEquals(_target, so)) return;
|
||||
|
||||
_target = so;
|
||||
if (_editor != null)
|
||||
{
|
||||
Object.DestroyImmediate(_editor);
|
||||
_editor = null;
|
||||
}
|
||||
|
||||
if (so != null)
|
||||
{
|
||||
_editor = Editor.CreateEditor(so);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
if (_editor == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("请在图中选中一个章节节点以编辑属性", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newColor = EditorGUILayout.ColorField("节点颜色", _target.nodeColor);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.RecordObject(_target, "Change Node Color");
|
||||
_target.nodeColor = newColor;
|
||||
EditorUtility.SetDirty(_target);
|
||||
OnColorChanged?.Invoke(_target);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
_editor.OnInspectorGUI();
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_editor != null)
|
||||
{
|
||||
Object.DestroyImmediate(_editor);
|
||||
_editor = null;
|
||||
}
|
||||
|
||||
_target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b122b6f926732f46b067d9b26eb191b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,503 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterGraphView : GraphView
|
||||
{
|
||||
public event Action<TalkSceneSO> OnNodeSelected;
|
||||
public event Action<TalkSceneSO> OnNodeDeleted;
|
||||
|
||||
private const float HorizontalSpacing = 420f;
|
||||
private const float VerticalSpacing = 220f;
|
||||
public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder";
|
||||
public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO";
|
||||
|
||||
public ChapterGraphView()
|
||||
{
|
||||
style.flexGrow = 1;
|
||||
|
||||
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
|
||||
this.AddManipulator(new ContentDragger());
|
||||
this.AddManipulator(new SelectionDragger());
|
||||
this.AddManipulator(new RectangleSelector());
|
||||
|
||||
var grid = new GridBackground();
|
||||
Insert(0, grid);
|
||||
grid.StretchToParentSize();
|
||||
|
||||
graphViewChanged += OnGraphViewChanged;
|
||||
}
|
||||
|
||||
public void UpdateNodeColor(TalkSceneSO so)
|
||||
{
|
||||
foreach (var node in graphElements.OfType<ChapterNode>())
|
||||
{
|
||||
if (node.SceneSo == so)
|
||||
{
|
||||
node.UpdateColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGraph(string folderPath = null)
|
||||
{
|
||||
ClearGraph();
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null);
|
||||
var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList();
|
||||
var nodeMap = new Dictionary<TalkSceneSO, ChapterNode>();
|
||||
|
||||
// 创建节点
|
||||
foreach (var so in allSos)
|
||||
{
|
||||
var node = new ChapterNode(so);
|
||||
AddElement(node);
|
||||
nodeMap[so] = node;
|
||||
}
|
||||
|
||||
// 创建连线
|
||||
foreach (var so in allSos)
|
||||
{
|
||||
if (so.exits == null) continue;
|
||||
if (!nodeMap.TryGetValue(so, out var sourceNode)) continue;
|
||||
|
||||
for (int i = 0; i < so.exits.Count; i++)
|
||||
{
|
||||
var exit = so.exits[i];
|
||||
if (exit == null || exit.targetScene == null) continue;
|
||||
if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue;
|
||||
|
||||
var outputPort = sourceNode.GetOutputPort(i);
|
||||
var inputPort = targetNode.InputPort;
|
||||
if (outputPort == null || inputPort == null) continue;
|
||||
|
||||
var edge = new Edge
|
||||
{
|
||||
output = outputPort,
|
||||
input = inputPort
|
||||
};
|
||||
edge.output.Connect(edge);
|
||||
edge.input.Connect(edge);
|
||||
AddElement(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearGraph()
|
||||
{
|
||||
// 清除所有元素
|
||||
var elementsToRemove = graphElements.ToList();
|
||||
foreach (var element in elementsToRemove)
|
||||
{
|
||||
RemoveElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
base.BuildContextualMenu(evt);
|
||||
|
||||
if (evt.target is GraphView)
|
||||
{
|
||||
var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition);
|
||||
evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition));
|
||||
}
|
||||
}
|
||||
|
||||
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
|
||||
{
|
||||
var compatiblePorts = new List<Port>();
|
||||
var startNode = startPort.node as ChapterNode;
|
||||
if (startNode == null) return compatiblePorts;
|
||||
|
||||
ports.ForEach(port =>
|
||||
{
|
||||
var targetNode = port.node as ChapterNode;
|
||||
if (targetNode == null) return;
|
||||
if (targetNode == startNode) return;
|
||||
if (startPort.direction == port.direction) return;
|
||||
if (startPort.node == port.node) return;
|
||||
|
||||
compatiblePorts.Add(port);
|
||||
});
|
||||
|
||||
return compatiblePorts;
|
||||
}
|
||||
|
||||
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
|
||||
{
|
||||
// 先处理节点删除确认:用户取消则从移除列表中剔除
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
var nodesToRemove = change.elementsToRemove.OfType<ChapterNode>().ToList();
|
||||
foreach (var node in nodesToRemove)
|
||||
{
|
||||
if (!ConfirmRemoveNode(node))
|
||||
{
|
||||
change.elementsToRemove.Remove(node);
|
||||
// 同时保留与该节点相连的边,避免边被单独删掉
|
||||
var edgesToKeep = change.elementsToRemove.OfType<Edge>()
|
||||
.Where(e => e.output?.node == node || e.input?.node == node)
|
||||
.ToList();
|
||||
foreach (var edge in edgesToKeep)
|
||||
{
|
||||
change.elementsToRemove.Remove(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 Edge 删除
|
||||
if (change.elementsToRemove != null)
|
||||
{
|
||||
foreach (var edge in change.elementsToRemove.OfType<Edge>())
|
||||
{
|
||||
HandleEdgeRemoved(edge);
|
||||
}
|
||||
|
||||
foreach (var node in change.elementsToRemove.OfType<ChapterNode>())
|
||||
{
|
||||
HandleNodeRemoved(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 Edge 创建
|
||||
if (change.edgesToCreate != null)
|
||||
{
|
||||
foreach (var edge in change.edgesToCreate)
|
||||
{
|
||||
HandleEdgeCreated(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点移动
|
||||
if (change.movedElements != null)
|
||||
{
|
||||
foreach (var element in change.movedElements)
|
||||
{
|
||||
if (element is ChapterNode node)
|
||||
{
|
||||
node.SyncPositionToSo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
private void HandleEdgeCreated(Edge edge)
|
||||
{
|
||||
if (!(edge.output?.node is ChapterNode sourceNode)) return;
|
||||
if (!(edge.input?.node is ChapterNode targetNode)) return;
|
||||
|
||||
var sourceSo = sourceNode.SceneSo;
|
||||
var targetSo = targetNode.SceneSo;
|
||||
if (sourceSo == null || targetSo == null) return;
|
||||
|
||||
int exitIndex = edge.output.userData is int idx ? idx : -1;
|
||||
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
|
||||
|
||||
Undo.RecordObject(sourceSo, "Connect Chapter");
|
||||
sourceSo.exits[exitIndex].targetScene = targetSo;
|
||||
EditorUtility.SetDirty(sourceSo);
|
||||
}
|
||||
|
||||
private void HandleEdgeRemoved(Edge edge)
|
||||
{
|
||||
if (!(edge.output?.node is ChapterNode sourceNode)) return;
|
||||
|
||||
var sourceSo = sourceNode.SceneSo;
|
||||
if (sourceSo == null) return;
|
||||
|
||||
int exitIndex = edge.output.userData is int idx ? idx : -1;
|
||||
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
|
||||
|
||||
Undo.RecordObject(sourceSo, "Disconnect Chapter");
|
||||
sourceSo.exits[exitIndex].targetScene = null;
|
||||
EditorUtility.SetDirty(sourceSo);
|
||||
}
|
||||
|
||||
private bool ConfirmRemoveNode(ChapterNode node)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so == null) return true;
|
||||
|
||||
var referrers = new List<string>();
|
||||
foreach (var other in graphElements.OfType<ChapterNode>())
|
||||
{
|
||||
if (other == node) continue;
|
||||
if (other.SceneSo == null || other.SceneSo.exits == null) continue;
|
||||
foreach (var exit in other.SceneSo.exits)
|
||||
{
|
||||
if (exit.targetScene == so)
|
||||
{
|
||||
referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name = string.IsNullOrEmpty(so.title) ? so.name : so.title;
|
||||
var msg = $"确定要删除章节节点 \"{name}\" 吗?";
|
||||
if (referrers.Count > 0)
|
||||
{
|
||||
msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n• ", referrers)}";
|
||||
}
|
||||
|
||||
return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消");
|
||||
}
|
||||
|
||||
private void HandleNodeRemoved(ChapterNode node)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so == null) return;
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(so);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
}
|
||||
|
||||
OnNodeDeleted?.Invoke(so);
|
||||
}
|
||||
|
||||
public override void AddToSelection(ISelectable selectable)
|
||||
{
|
||||
base.AddToSelection(selectable);
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
public override void RemoveFromSelection(ISelectable selectable)
|
||||
{
|
||||
base.RemoveFromSelection(selectable);
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
public override void ClearSelection()
|
||||
{
|
||||
base.ClearSelection();
|
||||
NotifySelectionChanged();
|
||||
}
|
||||
|
||||
private void NotifySelectionChanged()
|
||||
{
|
||||
var selectedNode = selection.OfType<ChapterNode>().FirstOrDefault();
|
||||
OnNodeSelected?.Invoke(selectedNode?.SceneSo);
|
||||
}
|
||||
|
||||
public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null)
|
||||
{
|
||||
var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder);
|
||||
var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder);
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var so = ScriptableObject.CreateInstance<TalkSceneSO>();
|
||||
so.name = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
so.title = "新章节";
|
||||
so.nodeColor = new Color(0.85f, 0.45f, 0.45f);
|
||||
so.graphPosition = position;
|
||||
so.exits = new System.Collections.Generic.List<SceneExit>();
|
||||
|
||||
AssetDatabase.CreateAsset(so, path);
|
||||
EditorUtility.SetDirty(so);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
// 记住文件夹
|
||||
var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(savedFolder))
|
||||
{
|
||||
EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder);
|
||||
}
|
||||
|
||||
var node = new ChapterNode(so);
|
||||
AddElement(node);
|
||||
}
|
||||
|
||||
public void AutoLayout()
|
||||
{
|
||||
var nodes = graphElements.OfType<ChapterNode>().ToList();
|
||||
if (nodes.Count == 0) return;
|
||||
|
||||
// 计算入度
|
||||
var inDegree = nodes.ToDictionary(n => n, _ => 0);
|
||||
var edges = graphElements.OfType<Edge>().ToList();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target))
|
||||
{
|
||||
inDegree[target]++;
|
||||
}
|
||||
}
|
||||
|
||||
// 分层 BFS
|
||||
var layer = new Dictionary<ChapterNode, int>();
|
||||
var queue = new Queue<ChapterNode>();
|
||||
|
||||
// 入度为 0 的节点作为起点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (inDegree[node] == 0)
|
||||
{
|
||||
layer[node] = 0;
|
||||
queue.Enqueue(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有入度为 0 的节点(纯循环),任选一个作为起点
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
layer[nodes[0]] = 0;
|
||||
queue.Enqueue(nodes[0]);
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
var currentLayer = layer[current];
|
||||
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
if (edge.output?.node == current && edge.input?.node is ChapterNode next)
|
||||
{
|
||||
if (!layer.ContainsKey(next))
|
||||
{
|
||||
layer[next] = currentLayer + 1;
|
||||
queue.Enqueue(next);
|
||||
}
|
||||
else
|
||||
{
|
||||
layer[next] = Math.Max(layer[next], currentLayer + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为未分配层级的节点(孤立或不连通部分)分配层级
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (!layer.ContainsKey(node))
|
||||
{
|
||||
layer[node] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 按层级分组并计算位置
|
||||
var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList();
|
||||
Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout");
|
||||
|
||||
for (int i = 0; i < groups.Count; i++)
|
||||
{
|
||||
var group = groups[i];
|
||||
var groupNodes = group.ToList();
|
||||
float x = group.Key * HorizontalSpacing;
|
||||
float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f;
|
||||
|
||||
for (int j = 0; j < groupNodes.Count; j++)
|
||||
{
|
||||
var node = groupNodes[j].Key;
|
||||
var y = startY + j * VerticalSpacing;
|
||||
var newPos = new Vector2(x, y);
|
||||
node.SetPosition(new Rect(newPos, Vector2.zero));
|
||||
node.SceneSo.graphPosition = newPos;
|
||||
EditorUtility.SetDirty(node.SceneSo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateGraph()
|
||||
{
|
||||
var nodes = graphElements.OfType<ChapterNode>().ToList();
|
||||
var issues = new List<string>();
|
||||
var soSet = new HashSet<TalkSceneSO>(nodes.Select(n => n.SceneSo));
|
||||
|
||||
// 孤立节点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
bool hasIncoming = graphElements.OfType<Edge>().Any(e => e.input?.node == node);
|
||||
bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null);
|
||||
|
||||
if (!hasIncoming && !hasOutgoing)
|
||||
{
|
||||
issues.Add($"孤立节点: {so.name} (没有入边也没有出边)");
|
||||
}
|
||||
}
|
||||
|
||||
// 循环引用 (DFS)
|
||||
var visited = new HashSet<TalkSceneSO>();
|
||||
var visiting = new HashSet<TalkSceneSO>();
|
||||
|
||||
foreach (var so in soSet)
|
||||
{
|
||||
if (!visited.Contains(so))
|
||||
{
|
||||
CheckCycle(so, visited, visiting, soSet, issues);
|
||||
}
|
||||
}
|
||||
|
||||
// 空 exitName
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var so = node.SceneSo;
|
||||
if (so.exits == null) continue;
|
||||
for (int i = 0; i < so.exits.Count; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(so.exits[i].exitName))
|
||||
{
|
||||
issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.Count == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定");
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Join("\n", issues.Take(20));
|
||||
if (issues.Count > 20)
|
||||
{
|
||||
msg += $"\n... 还有 {issues.Count - 20} 个问题";
|
||||
}
|
||||
EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCycle(TalkSceneSO current, HashSet<TalkSceneSO> visited, HashSet<TalkSceneSO> visiting, HashSet<TalkSceneSO> soSet, List<string> issues)
|
||||
{
|
||||
visited.Add(current);
|
||||
visiting.Add(current);
|
||||
|
||||
if (current.exits != null)
|
||||
{
|
||||
foreach (var exit in current.exits)
|
||||
{
|
||||
var next = exit.targetScene;
|
||||
if (next == null || !soSet.Contains(next)) continue;
|
||||
|
||||
if (visiting.Contains(next))
|
||||
{
|
||||
issues.Add($"循环引用: {current.name} -> {next.name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!visited.Contains(next))
|
||||
{
|
||||
CheckCycle(next, visited, visiting, soSet, issues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 913b090485a3d844fab3e436e7bf4f57
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,282 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterNode : Node
|
||||
{
|
||||
public TalkSceneSO SceneSo { get; }
|
||||
public Port InputPort { get; private set; }
|
||||
|
||||
private const float PortLabelMaxWidth = 120f;
|
||||
|
||||
public ChapterNode(TalkSceneSO so)
|
||||
{
|
||||
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
|
||||
|
||||
// 节点整体放大
|
||||
style.minWidth = 240;
|
||||
style.paddingTop = 8;
|
||||
style.paddingBottom = 8;
|
||||
style.paddingLeft = 10;
|
||||
style.paddingRight = 10;
|
||||
|
||||
UpdateTitle();
|
||||
UpdateColor();
|
||||
BuildInputPort();
|
||||
BuildOutputPorts();
|
||||
BuildSubtitle();
|
||||
BuildAddExitButton();
|
||||
SetPosition(new Rect(so.graphPosition, Vector2.zero));
|
||||
|
||||
// 放大标题
|
||||
var titleLabel = this.Q<Label>("title-label");
|
||||
if (titleLabel != null)
|
||||
{
|
||||
titleLabel.style.fontSize = 16;
|
||||
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTitle()
|
||||
{
|
||||
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
base.BuildContextualMenu(evt);
|
||||
|
||||
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
var idx = i;
|
||||
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateColor()
|
||||
{
|
||||
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
|
||||
}
|
||||
|
||||
private void BuildInputPort()
|
||||
{
|
||||
InputPort = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Input,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
InputPort.portName = "▶";
|
||||
InputPort.style.fontSize = 14;
|
||||
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
inputContainer.Add(InputPort);
|
||||
}
|
||||
|
||||
private void BuildOutputPorts()
|
||||
{
|
||||
outputContainer.Clear();
|
||||
if (SceneSo.exits == null) return;
|
||||
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var port = CreateOutputPort(exit, i);
|
||||
outputContainer.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
private Port CreateOutputPort(SceneExit exit, int index)
|
||||
{
|
||||
var port = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Output,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
|
||||
port.userData = index;
|
||||
|
||||
// 限制端口标签宽度并放大字体
|
||||
var label = port.Q<Label>("type");
|
||||
if (label != null)
|
||||
{
|
||||
label.style.maxWidth = PortLabelMaxWidth;
|
||||
label.style.fontSize = 12;
|
||||
}
|
||||
|
||||
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
|
||||
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
|
||||
{
|
||||
var currentIndex = port.userData is int idx ? idx : -1;
|
||||
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
|
||||
}, TrickleDown.TrickleDown);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
private void BuildSubtitle()
|
||||
{
|
||||
var subtitleLabel = new Label($"{SceneSo.name}")
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 12,
|
||||
unityTextAlign = TextAnchor.MiddleCenter,
|
||||
color = new Color(0.8f, 0.8f, 0.8f),
|
||||
paddingTop = 4,
|
||||
paddingBottom = 2
|
||||
}
|
||||
};
|
||||
extensionContainer.Add(subtitleLabel);
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
private void BuildAddExitButton()
|
||||
{
|
||||
var removeButton = new Button(() => RemoveLastExit())
|
||||
{
|
||||
text = "-",
|
||||
style =
|
||||
{
|
||||
width = 26,
|
||||
height = 22,
|
||||
fontSize = 16,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginRight = 4
|
||||
}
|
||||
};
|
||||
titleButtonContainer.Add(removeButton);
|
||||
|
||||
var addButton = new Button(() => AddExit())
|
||||
{
|
||||
text = "+",
|
||||
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
};
|
||||
titleButtonContainer.Add(addButton);
|
||||
}
|
||||
|
||||
private void RemoveLastExit()
|
||||
{
|
||||
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
|
||||
RemoveExitAt(SceneSo.exits.Count - 1);
|
||||
}
|
||||
|
||||
public void AddExit()
|
||||
{
|
||||
Undo.RecordObject(SceneSo, "Add Exit");
|
||||
if (SceneSo.exits == null)
|
||||
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
|
||||
|
||||
var newExit = new SceneExit
|
||||
{
|
||||
exitName = $"exit_{SceneSo.exits.Count}"
|
||||
};
|
||||
SceneSo.exits.Add(newExit);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
|
||||
outputContainer.Add(newPort);
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RemoveExitAt(int index)
|
||||
{
|
||||
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
|
||||
|
||||
// 断开并移除该端口的所有边
|
||||
var portToRemove = GetOutputPort(index);
|
||||
if (portToRemove != null)
|
||||
{
|
||||
var edges = portToRemove.connections.ToList();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
portToRemove.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
Undo.RecordObject(SceneSo, "Remove Exit");
|
||||
SceneSo.exits.RemoveAt(index);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
// 更新后续端口的 userData 和 portName
|
||||
for (int i = index; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var port = GetOutputPort(i);
|
||||
if (port != null)
|
||||
{
|
||||
port.userData = i;
|
||||
var exit = SceneSo.exits[i];
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RefreshOutputPorts()
|
||||
{
|
||||
// 先断开所有输出端口的边
|
||||
var edgesToRemove = outputContainer.Children()
|
||||
.OfType<Port>()
|
||||
.SelectMany(p => p.connections.ToList())
|
||||
.ToList();
|
||||
|
||||
foreach (var edge in edgesToRemove)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
BuildOutputPorts();
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public Port GetOutputPort(int index)
|
||||
{
|
||||
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
|
||||
}
|
||||
|
||||
public override void SetPosition(Rect newPos)
|
||||
{
|
||||
base.SetPosition(newPos);
|
||||
if (SceneSo != null)
|
||||
{
|
||||
SceneSo.graphPosition = newPos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncPositionToSo()
|
||||
{
|
||||
if (SceneSo == null) return;
|
||||
var pos = GetPosition();
|
||||
if (SceneSo.graphPosition != pos.position)
|
||||
{
|
||||
SceneSo.graphPosition = pos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyEdgeListener : IEdgeConnectorListener
|
||||
{
|
||||
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
|
||||
public void OnDrop(GraphView graphView, Edge edge) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3a4f1beaaa3a5740b1904f8b9b926b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,32 +0,0 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
[CustomEditor(typeof(TalkSceneSO))]
|
||||
public class TalkSceneSOEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("exits"), true);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("yarnProject"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("firstSceneName"));
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("显示信息", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("title"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("titleKey"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("description"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("coverPic"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("chapter"));
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("编辑器设置", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("nodeColor"), new GUIContent("节点颜色"));
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4988e2ee30c565e40989210408a234e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 665561f3805a96446b45ea1d50bfe520
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,117 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using AibisDream.Kit;
|
||||
using AibisDream.UI;
|
||||
using AibisDream.Utility;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.DeveloperMode.Editor
|
||||
{
|
||||
public sealed class DeveloperModeCoreTests
|
||||
{
|
||||
[Test]
|
||||
public void RuntimeLogBufferEvictsOldestAndPreservesOrder()
|
||||
{
|
||||
var buffer = new RuntimeLogBuffer(3);
|
||||
buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, "one"));
|
||||
buffer.Add(LogEntry.System(LogLevel.Warning, LogCategory.Save, "two"));
|
||||
buffer.Add(LogEntry.System(LogLevel.Error, LogCategory.Yarn, "three"));
|
||||
buffer.Add(LogEntry.System(LogLevel.Fatal, LogCategory.Scene, "four"));
|
||||
|
||||
var snapshot = buffer.Snapshot();
|
||||
Assert.That(snapshot.Select(item => item.Message), Is.EqualTo(new[] { "two", "three", "four" }));
|
||||
Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending);
|
||||
Assert.That(snapshot.Length, Is.EqualTo(buffer.Capacity));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RuntimeLogBufferAcceptsConcurrentWritersAndClears()
|
||||
{
|
||||
var buffer = new RuntimeLogBuffer(128);
|
||||
System.Threading.Tasks.Parallel.For(0, 1000, index =>
|
||||
buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, index.ToString())));
|
||||
|
||||
var snapshot = buffer.Snapshot();
|
||||
Assert.That(snapshot.Length, Is.EqualTo(128));
|
||||
Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending);
|
||||
buffer.Clear();
|
||||
Assert.That(buffer.Snapshot(), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void YarnStorageTypedSettersAndDebugSnapshotAreReadOnlyAndConsistent()
|
||||
{
|
||||
var go = new GameObject("YarnVariableStorage Test");
|
||||
try
|
||||
{
|
||||
var storage = go.AddComponent<YarnVariableStorage>();
|
||||
storage.SetValue("$score", 12.5f);
|
||||
storage.SetValue("$global_flag", true);
|
||||
storage.SetValue("$name", "AIBIS");
|
||||
|
||||
var snapshot = DeveloperVariableSnapshot.Capture(storage, null);
|
||||
Assert.That(snapshot.Select(item => item.Name),
|
||||
Is.EquivalentTo(new[] { "$score", "$global_flag", "$name" }));
|
||||
Assert.That(snapshot.Single(item => item.Name == "$score").Value, Is.EqualTo("12.5"));
|
||||
Assert.That(snapshot.Single(item => item.Name == "$global_flag").IsGlobal, Is.True);
|
||||
Assert.That(snapshot.All(item => item.HasRuntimeOverride), Is.True);
|
||||
|
||||
storage.ClearLocal();
|
||||
var remaining = DeveloperVariableSnapshot.Capture(storage, null);
|
||||
Assert.That(remaining.Select(item => item.Name), Is.EqualTo(new[] { "$global_flag" }));
|
||||
storage.Clear();
|
||||
Assert.That(DeveloperVariableSnapshot.Capture(storage, null), Is.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EditorBuildEnablesDeveloperMode()
|
||||
{
|
||||
Assert.That(DeveloperModeGate.IsEnabled, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PrefabIsConfiguredAndPersistenceReferencesIt()
|
||||
{
|
||||
const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
|
||||
const string scenePath = "Assets/Scenes/Persistence.unity";
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Assert.That(prefab, Is.Not.Null);
|
||||
Assert.That(prefab.activeSelf, Is.False);
|
||||
var panel = prefab.GetComponent<DeveloperModePanel>();
|
||||
Assert.That(panel, Is.Not.Null);
|
||||
Assert.That(panel.IsConfigured, Is.True);
|
||||
Assert.That(prefab.GetComponent<RectTransform>().sizeDelta, Is.EqualTo(new Vector2(840f, 760f)));
|
||||
Assert.That(AssetDatabase.GetDependencies(scenePath), Does.Contain(prefabPath));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PanelToggleIsNonModalAndEscapeClosesIt()
|
||||
{
|
||||
const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
var instance = Object.Instantiate(prefab);
|
||||
try
|
||||
{
|
||||
var panel = instance.GetComponent<DeveloperModePanel>();
|
||||
var originalScale = Time.timeScale;
|
||||
panel.TogglePanel();
|
||||
Assert.That(panel.IsOpen, Is.True);
|
||||
Assert.That(Time.timeScale, Is.EqualTo(originalScale));
|
||||
Assert.That(panel.HandleEscape(), Is.True);
|
||||
Assert.That(panel.IsOpen, Is.False);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b95ba9d1e80e3dc4e83bc408569c4955
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,476 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.Framework;
|
||||
using AibisDream.SaveSystem;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.DeveloperMode.Editor.Tests
|
||||
{
|
||||
public sealed class TestSaveSystemTests
|
||||
{
|
||||
private string _root;
|
||||
private TestSaveRepository _repository;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"AibisDream-TestSaveTests",
|
||||
Guid.NewGuid().ToString("N"));
|
||||
_repository = new TestSaveRepository(_root);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (Directory.Exists(_root))
|
||||
{
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Record_SameAnchor_ReplacesSnapshotAndKeepsFirstSeenOrder()
|
||||
{
|
||||
var first = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneOne"));
|
||||
var second = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneTwo"));
|
||||
var scan = _repository.Scan("1.0");
|
||||
|
||||
Assert.That(scan.Entries.Count, Is.EqualTo(1));
|
||||
Assert.That(second.Meta.firstSeenOrder, Is.EqualTo(first.Meta.firstSeenOrder));
|
||||
Assert.That(scan.Entries[0].Meta.sceneName, Is.EqualTo("SceneTwo"));
|
||||
Assert.That(_repository.TryLoad(scan.Entries[0], out var snapshot, out var error), Is.True, error);
|
||||
Assert.That(snapshot.scene.sceneName, Is.EqualTo("SceneTwo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Record_ReplacingEarlierEntry_DoesNotMoveItsOrder()
|
||||
{
|
||||
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
|
||||
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB"));
|
||||
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA2"));
|
||||
|
||||
var entries = _repository.Scan().Entries.ToArray();
|
||||
Assert.That(entries.Select(item => item.NodeName), Is.EqualTo(new[] { "NodeA", "NodeB" }));
|
||||
Assert.That(entries[0].Meta.firstSeenOrder, Is.LessThan(entries[1].Meta.firstSeenOrder));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Scan_RetainsMissingSnapshotAsInvalid()
|
||||
{
|
||||
var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
|
||||
File.Delete(entry.SnapshotPath);
|
||||
|
||||
var scan = _repository.Scan();
|
||||
|
||||
Assert.That(scan.ValidCount, Is.Zero);
|
||||
Assert.That(scan.InvalidCount, Is.EqualTo(1));
|
||||
Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.MissingSnapshot));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Scan_RetainsOrphanSnapshotWithBrokenMeta()
|
||||
{
|
||||
var directory = Path.Combine(_root, "chapter", "broken");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(
|
||||
Path.Combine(directory, TestSaveRepository.SnapshotFileName),
|
||||
"{}",
|
||||
Encoding.UTF8);
|
||||
|
||||
var scan = _repository.Scan();
|
||||
|
||||
Assert.That(scan.InvalidCount, Is.EqualTo(1));
|
||||
Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptMeta));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validate_CorruptSnapshot_IsRetainedAndDisabledOnNextScan()
|
||||
{
|
||||
var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
|
||||
File.WriteAllText(entry.SnapshotPath, "{broken", Encoding.UTF8);
|
||||
|
||||
Assert.That(_repository.Validate(entry, out var error), Is.False);
|
||||
var rescanned = _repository.Scan();
|
||||
|
||||
Assert.That(error, Does.Contain("无法解析"));
|
||||
Assert.That(rescanned.InvalidCount, Is.EqualTo(1));
|
||||
Assert.That(rescanned.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptSnapshot));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Scan_RecoversCompleteStagingDirectory()
|
||||
{
|
||||
var final = Path.Combine(_root, "chapter", "node");
|
||||
var staging = final + ".__staging";
|
||||
Directory.CreateDirectory(staging);
|
||||
var request = CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA");
|
||||
var meta = CreateMeta(request, 1);
|
||||
File.WriteAllText(
|
||||
Path.Combine(staging, TestSaveRepository.MetaFileName),
|
||||
JsonConvert.SerializeObject(meta),
|
||||
Encoding.UTF8);
|
||||
File.WriteAllText(
|
||||
Path.Combine(staging, TestSaveRepository.SnapshotFileName),
|
||||
SnapshotPersistence.Serialize(request.Snapshot),
|
||||
Encoding.UTF8);
|
||||
|
||||
var scan = _repository.Scan();
|
||||
|
||||
Assert.That(Directory.Exists(final), Is.True);
|
||||
Assert.That(Directory.Exists(staging), Is.False);
|
||||
Assert.That(scan.ValidCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Record_ConcurrentRequests_ProducesCompleteUniqueEntries()
|
||||
{
|
||||
var tasks = Enumerable.Range(0, 12)
|
||||
.Select(index => Task.Run(() =>
|
||||
_repository.Record(CreateRequest(
|
||||
"ChapterA",
|
||||
"ProjectA",
|
||||
$"Node{index:00}",
|
||||
$"Scene{index:00}"))))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
var scan = _repository.Scan();
|
||||
|
||||
Assert.That(scan.ValidCount, Is.EqualTo(12));
|
||||
Assert.That(scan.InvalidCount, Is.Zero);
|
||||
Assert.That(scan.Entries.Select(item => item.Meta.firstSeenOrder).Distinct().Count(), Is.EqualTo(12));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeleteInvalid_RemovesOnlyInvalidEntries()
|
||||
{
|
||||
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
|
||||
var invalid = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB"));
|
||||
File.Delete(invalid.SnapshotPath);
|
||||
|
||||
var removed = _repository.DeleteInvalid(out var error);
|
||||
var scan = _repository.Scan();
|
||||
|
||||
Assert.That(error, Is.Null);
|
||||
Assert.That(removed, Is.EqualTo(1));
|
||||
Assert.That(scan.ValidCount, Is.EqualTo(1));
|
||||
Assert.That(scan.InvalidCount, Is.Zero);
|
||||
}
|
||||
|
||||
[TestCase(new[] { "hub" }, true)]
|
||||
[TestCase(new[] { "linear" }, true)]
|
||||
[TestCase(new[] { "content" }, true)]
|
||||
[TestCase(new[] { "event" }, false)]
|
||||
[TestCase(new[] { "hub", "no_save" }, false)]
|
||||
[TestCase(new string[0], true)]
|
||||
public void SilentCoverageEvaluator_MatchesSavePointTagSemantics(string[] tags, bool expected)
|
||||
{
|
||||
var actual = SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently(
|
||||
"Node",
|
||||
tags,
|
||||
out _);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(new[] { "content" }, NodeSaveTiming.NodeEnter, SaveResumeMode.RestartNode)]
|
||||
[TestCase(new[] { "interaction" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
|
||||
[TestCase(new[] { "content", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
|
||||
[TestCase(new[] { "event", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
|
||||
[TestCase(new[] { "interaction", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
|
||||
[TestCase(new[] { "interaction", "no_save" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)]
|
||||
[TestCase(new[] { "content", "event" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)]
|
||||
public void ResolveNodePolicy_MapsNodeTypeAndModifiers(
|
||||
string[] tags,
|
||||
NodeSaveTiming expectedTiming,
|
||||
SaveResumeMode expectedResumeMode)
|
||||
{
|
||||
var policy = SavePointEvaluator.ResolveNodePolicy(
|
||||
"Node",
|
||||
tags,
|
||||
out _,
|
||||
logWarnings: false);
|
||||
|
||||
Assert.That(policy.Timing, Is.EqualTo(expectedTiming));
|
||||
Assert.That(policy.ResumeMode, Is.EqualTo(expectedResumeMode));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Repository_StateOnlySnapshot_PreservesAnchorNode()
|
||||
{
|
||||
var request = CreateRequest("ChapterA", "ProjectA", "InteractionNode", "SceneA");
|
||||
request.Snapshot.anchor.startDialogueOnRestore = false;
|
||||
request.SaveTrigger = "DialogueExit";
|
||||
request.ResumeMode = nameof(SaveResumeMode.StateOnly);
|
||||
request.StartDialogueOnRestore = false;
|
||||
request.DedupeKey = TestSaveRecorder.BuildDedupeKey(
|
||||
request.SceneSoName,
|
||||
request.YarnProjectId,
|
||||
request.NodeName,
|
||||
request.ResumeMode);
|
||||
request.EntryId = TestSaveRepository.StableHash(request.DedupeKey);
|
||||
|
||||
var entry = _repository.Record(request);
|
||||
var loaded = _repository.TryLoad(entry, out var snapshot, out var error);
|
||||
|
||||
Assert.That(loaded, Is.True, error);
|
||||
Assert.That(entry.NodeName, Is.EqualTo("InteractionNode"));
|
||||
Assert.That(entry.ResumeMode, Is.EqualTo(nameof(SaveResumeMode.StateOnly)));
|
||||
Assert.That(snapshot.anchor.nodeName, Is.EqualTo("InteractionNode"));
|
||||
Assert.That(snapshot.anchor.startDialogueOnRestore, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SnapshotPersistence_MissingRestoreFlag_IsRejected()
|
||||
{
|
||||
const string json =
|
||||
"{\"schemaVersion\":2,\"anchor\":{\"sceneSoName\":\"ChapterA\"," +
|
||||
"\"yarnProjectId\":\"ProjectA\",\"nodeName\":\"NodeA\"}}";
|
||||
|
||||
Assert.Throws<Newtonsoft.Json.JsonSerializationException>(
|
||||
() => SnapshotPersistence.Deserialize(json));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SerialTaskQueue_DoesNotDropOrReorderRequests()
|
||||
{
|
||||
var queue = new SerialTaskQueue();
|
||||
var releaseFirst = new TaskCompletionSource<bool>();
|
||||
var firstStarted = new TaskCompletionSource<bool>();
|
||||
var order = new List<int>();
|
||||
|
||||
var first = queue.Enqueue(async () =>
|
||||
{
|
||||
order.Add(1);
|
||||
firstStarted.SetResult(true);
|
||||
await releaseFirst.Task;
|
||||
order.Add(2);
|
||||
});
|
||||
var second = queue.Enqueue(() =>
|
||||
{
|
||||
order.Add(3);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
Assert.That(firstStarted.Task.Wait(TimeSpan.FromSeconds(2)), Is.True);
|
||||
Assert.That(order, Is.EqualTo(new[] { 1 }));
|
||||
Assert.That(queue.PendingCount, Is.EqualTo(2));
|
||||
|
||||
releaseFirst.SetResult(true);
|
||||
Assert.That(
|
||||
Task.WaitAll(
|
||||
new[] { first.Completion, second.Completion },
|
||||
TimeSpan.FromSeconds(2)),
|
||||
Is.True);
|
||||
|
||||
Assert.That(first.Sequence, Is.LessThan(second.Sequence));
|
||||
Assert.That(order, Is.EqualTo(new[] { 1, 2, 3 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveRequest_MapsResumeModeToExplicitAnchorFlag()
|
||||
{
|
||||
var enter = SaveRequest.NodeEnter("NodeA", "ProjectA", "ChapterA", 3, 7);
|
||||
var exit = SaveRequest.DialogueExit("NodeB", "ProjectA", "ChapterA", 3, 9);
|
||||
|
||||
Assert.That(enter.AnchorSpec.NodeName, Is.EqualTo("NodeA"));
|
||||
Assert.That(enter.AnchorSpec.StartDialogueOnRestore, Is.True);
|
||||
Assert.That(exit.AnchorSpec.NodeName, Is.EqualTo("NodeB"));
|
||||
Assert.That(exit.AnchorSpec.StartDialogueOnRestore, Is.False);
|
||||
}
|
||||
|
||||
[TestCase(false, "DetourNode", true)]
|
||||
[TestCase(false, "NoSaveNode", false)]
|
||||
[TestCase(true, "AnotherNode", true)]
|
||||
public void ValidateDialogueTiming_NonExitRequestIgnoresLaterDialogueState(
|
||||
bool explicitCommand,
|
||||
string currentNodeName,
|
||||
bool isDialogueRunning)
|
||||
{
|
||||
var request = explicitCommand
|
||||
? SaveRequest.Explicit("SourceNode", "ProjectA", "ChapterA", 3, 7)
|
||||
: SaveRequest.NodeEnter("SourceNode", "ProjectA", "ChapterA", 3, 7);
|
||||
|
||||
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
|
||||
request,
|
||||
currentDialogueRunId: 99,
|
||||
currentFlowRevision: 101,
|
||||
currentNodeName: currentNodeName,
|
||||
isDialogueRunning: isDialogueRunning,
|
||||
out var reason);
|
||||
|
||||
Assert.That(valid, Is.True, reason);
|
||||
Assert.That(request.AnchorSpec.NodeName, Is.EqualTo("SourceNode"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateDialogueTiming_ConsecutiveNodeEnterRequestsRemainEligible()
|
||||
{
|
||||
var first = SaveRequest.NodeEnter("FirstNode", "ProjectA", "ChapterA", 3, 7);
|
||||
var second = SaveRequest.NodeEnter("SecondNode", "ProjectA", "ChapterA", 3, 8);
|
||||
|
||||
var firstValid = SaveRestoreOrchestrator.ValidateDialogueTiming(
|
||||
first,
|
||||
currentDialogueRunId: 3,
|
||||
currentFlowRevision: 8,
|
||||
currentNodeName: "SecondNode",
|
||||
isDialogueRunning: true,
|
||||
out var firstReason);
|
||||
var secondValid = SaveRestoreOrchestrator.ValidateDialogueTiming(
|
||||
second,
|
||||
currentDialogueRunId: 3,
|
||||
currentFlowRevision: 8,
|
||||
currentNodeName: "SecondNode",
|
||||
isDialogueRunning: true,
|
||||
out var secondReason);
|
||||
|
||||
Assert.That(firstValid, Is.True, firstReason);
|
||||
Assert.That(secondValid, Is.True, secondReason);
|
||||
Assert.That(first.AnchorSpec.NodeName, Is.EqualTo("FirstNode"));
|
||||
Assert.That(second.AnchorSpec.NodeName, Is.EqualTo("SecondNode"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateDialogueTiming_DialogueExitAcceptsStableCompletedFlow()
|
||||
{
|
||||
var request = SaveRequest.DialogueExit(
|
||||
"InteractionNode",
|
||||
"ProjectA",
|
||||
"ChapterA",
|
||||
3,
|
||||
9);
|
||||
|
||||
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
|
||||
request,
|
||||
currentDialogueRunId: 3,
|
||||
currentFlowRevision: 9,
|
||||
currentNodeName: null,
|
||||
isDialogueRunning: false,
|
||||
out var reason);
|
||||
|
||||
Assert.That(valid, Is.True, reason);
|
||||
}
|
||||
|
||||
[TestCase(4, 9, null, false, "dialogue run changed")]
|
||||
[TestCase(3, 10, null, false, "flow revision changed")]
|
||||
[TestCase(3, 9, "DetourNode", false, "current Yarn node")]
|
||||
[TestCase(3, 9, "NoSaveNode", false, "current Yarn node")]
|
||||
[TestCase(3, 9, "JumpTarget", true, "still running")]
|
||||
public void ValidateDialogueTiming_DialogueExitRejectsChangedOrRunningFlow(
|
||||
long currentDialogueRunId,
|
||||
long currentFlowRevision,
|
||||
string currentNodeName,
|
||||
bool isDialogueRunning,
|
||||
string expectedReason)
|
||||
{
|
||||
var request = SaveRequest.DialogueExit(
|
||||
"InteractionNode",
|
||||
"ProjectA",
|
||||
"ChapterA",
|
||||
3,
|
||||
9);
|
||||
|
||||
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
|
||||
request,
|
||||
currentDialogueRunId,
|
||||
currentFlowRevision,
|
||||
currentNodeName,
|
||||
isDialogueRunning,
|
||||
out var reason);
|
||||
|
||||
Assert.That(valid, Is.False);
|
||||
Assert.That(reason, Does.Contain(expectedReason));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InteractionLock_ComposesWithDialogueLock()
|
||||
{
|
||||
var gameObject = new GameObject("EventSystemEx-Test");
|
||||
try
|
||||
{
|
||||
var eventSystem = gameObject.AddComponent<EventSystemEx>();
|
||||
EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart);
|
||||
var checkpointLock = eventSystem.AcquireInteractionLock("test");
|
||||
|
||||
EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd);
|
||||
Assert.That(eventSystem.isLocked, Is.True);
|
||||
|
||||
checkpointLock.Dispose();
|
||||
Assert.That(eventSystem.isLocked, Is.False);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static TestSaveRecordRequest CreateRequest(
|
||||
string sceneSoName,
|
||||
string yarnProject,
|
||||
string nodeName,
|
||||
string sceneName)
|
||||
{
|
||||
var key = TestSaveRecorder.BuildDedupeKey(sceneSoName, yarnProject, nodeName);
|
||||
return new TestSaveRecordRequest
|
||||
{
|
||||
Snapshot = new SaveSnapshot
|
||||
{
|
||||
gameVersion = "1.0",
|
||||
savedAt = "2026-01-01 00:00:00",
|
||||
scene = new SceneSnapshotDto { sceneName = sceneName },
|
||||
anchor = new AnchorSnapshot
|
||||
{
|
||||
sceneSoName = sceneSoName,
|
||||
yarnProjectId = yarnProject,
|
||||
nodeName = nodeName,
|
||||
startDialogueOnRestore = true
|
||||
}
|
||||
},
|
||||
DedupeKey = key,
|
||||
EntryId = TestSaveRepository.StableHash(key),
|
||||
ChapterId = sceneSoName,
|
||||
ChapterTitle = sceneSoName,
|
||||
SceneSoName = sceneSoName,
|
||||
YarnProjectId = yarnProject,
|
||||
NodeName = nodeName,
|
||||
SaveTrigger = "NodeEnter",
|
||||
ResumeMode = nameof(SaveResumeMode.RestartNode),
|
||||
StartDialogueOnRestore = true,
|
||||
SceneName = sceneName,
|
||||
GameVersion = "1.0"
|
||||
};
|
||||
}
|
||||
|
||||
private static TestSaveMeta CreateMeta(TestSaveRecordRequest request, long order)
|
||||
{
|
||||
return new TestSaveMeta
|
||||
{
|
||||
entryId = request.EntryId,
|
||||
dedupeKey = request.DedupeKey,
|
||||
chapterId = request.ChapterId,
|
||||
chapterTitle = request.ChapterTitle,
|
||||
sceneSoName = request.SceneSoName,
|
||||
yarnProjectId = request.YarnProjectId,
|
||||
nodeName = request.NodeName,
|
||||
saveTrigger = request.SaveTrigger,
|
||||
resumeMode = request.ResumeMode,
|
||||
startDialogueOnRestore = request.StartDialogueOnRestore,
|
||||
sceneName = request.SceneName,
|
||||
firstSeenOrder = order,
|
||||
firstRecordedAt = "2026-01-01 00:00:00",
|
||||
lastRecordedAt = "2026-01-01 00:00:00",
|
||||
snapshotSchemaVersion = SaveSnapshotSchema.CurrentVersion,
|
||||
gameVersion = request.GameVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbbba0c3621cf204483f453bad2d75a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e244d3d62dec3f4f881f7e681da8c03
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "AibisDream.FrameAnimation.Editor",
|
||||
"rootNamespace": "AibisDream.FrameAnimation.Editor",
|
||||
"references": [
|
||||
"AibisDream.FrameAnimation.Runtime",
|
||||
"AibisDream.Menus",
|
||||
"Unity.2D.Sprite.Editor"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1ffe139ca2417941a09835467d1e442
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,198 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal static class AsepriteJsonParser
|
||||
{
|
||||
public static bool TryParse(
|
||||
string json,
|
||||
string sourceId,
|
||||
out AsepriteSourceDocument document,
|
||||
out FrameAnimationImportIssue issue)
|
||||
{
|
||||
document = null;
|
||||
issue = null;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
issue = Error(FrameAnimationImportIssueCode.JsonInvalid, sourceId, "Aseprite JSON 为空。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var root = JObject.Parse(json);
|
||||
var framesToken = root["frames"];
|
||||
if (framesToken == null ||
|
||||
(framesToken.Type != JTokenType.Object && framesToken.Type != JTokenType.Array))
|
||||
{
|
||||
issue = Error(FrameAnimationImportIssueCode.FramesMissing, sourceId, "JSON 缺少 Object 或 Array 格式的 frames。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var frames = ParseFrames(framesToken);
|
||||
var meta = root["meta"] as JObject;
|
||||
var size = ParseSize(meta?["size"]);
|
||||
var imageName = meta?.Value<string>("image") ?? string.Empty;
|
||||
var tags = ParseTags(meta?["frameTags"]);
|
||||
document = new AsepriteSourceDocument(frames, tags, size, imageName);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.JsonInvalid,
|
||||
sourceId,
|
||||
$"Aseprite JSON 解析失败:{exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryExpandTag(
|
||||
AsepriteSourceTag tag,
|
||||
int frameCount,
|
||||
string sourceId,
|
||||
out IReadOnlyList<int> indices,
|
||||
out FrameAnimationImportIssue issue)
|
||||
{
|
||||
indices = Array.Empty<int>();
|
||||
issue = null;
|
||||
if (tag == null || tag.From < 0 || tag.To < tag.From || tag.To >= frameCount)
|
||||
{
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.TagRangeInvalid,
|
||||
sourceId,
|
||||
$"Tag '{tag?.Name}' 的范围 {tag?.From}..{tag?.To} 无效。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = new List<int>();
|
||||
switch ((tag.Direction ?? string.Empty).ToLowerInvariant())
|
||||
{
|
||||
case "forward":
|
||||
AddAscending(result, tag.From, tag.To);
|
||||
break;
|
||||
case "reverse":
|
||||
AddDescending(result, tag.To, tag.From);
|
||||
break;
|
||||
case "pingpong":
|
||||
AddAscending(result, tag.From, tag.To);
|
||||
AddDescending(result, tag.To - 1, tag.From + 1);
|
||||
break;
|
||||
case "pingpong_reverse":
|
||||
AddDescending(result, tag.To, tag.From);
|
||||
AddAscending(result, tag.From + 1, tag.To - 1);
|
||||
break;
|
||||
default:
|
||||
issue = Error(
|
||||
FrameAnimationImportIssueCode.TagDirectionInvalid,
|
||||
sourceId,
|
||||
$"Tag '{tag.Name}' 使用了不支持的 direction '{tag.Direction}'。");
|
||||
return false;
|
||||
}
|
||||
|
||||
indices = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<AsepriteSourceFrame> ParseFrames(JToken token)
|
||||
{
|
||||
var frames = new List<AsepriteSourceFrame>();
|
||||
if (token is JObject objectFrames)
|
||||
{
|
||||
foreach (var property in objectFrames.Properties())
|
||||
{
|
||||
frames.Add(ParseFrame(property.Name, property.Value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (JArray)token)
|
||||
{
|
||||
frames.Add(ParseFrame(item.Value<string>("filename") ?? string.Empty, item));
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
private static AsepriteSourceFrame ParseFrame(string frameName, JToken token)
|
||||
{
|
||||
return new AsepriteSourceFrame(
|
||||
frameName,
|
||||
ParseRect(token["frame"]),
|
||||
token.Value<int?>("duration") ?? 0,
|
||||
token.Value<bool?>("rotated") ?? false,
|
||||
token.Value<bool?>("trimmed") ?? false,
|
||||
ParseRect(token["spriteSourceSize"]),
|
||||
ParseSize(token["sourceSize"]));
|
||||
}
|
||||
|
||||
private static List<AsepriteSourceTag> ParseTags(JToken token)
|
||||
{
|
||||
var tags = new List<AsepriteSourceTag>();
|
||||
if (!(token is JArray array))
|
||||
{
|
||||
return tags;
|
||||
}
|
||||
|
||||
foreach (var item in array)
|
||||
{
|
||||
tags.Add(new AsepriteSourceTag(
|
||||
(item.Value<string>("name") ?? string.Empty).Trim(),
|
||||
item.Value<int?>("from") ?? -1,
|
||||
item.Value<int?>("to") ?? -1,
|
||||
item.Value<string>("direction") ?? string.Empty));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
private static RectInt ParseRect(JToken token)
|
||||
{
|
||||
return token == null
|
||||
? default
|
||||
: new RectInt(
|
||||
token.Value<int?>("x") ?? 0,
|
||||
token.Value<int?>("y") ?? 0,
|
||||
token.Value<int?>("w") ?? 0,
|
||||
token.Value<int?>("h") ?? 0);
|
||||
}
|
||||
|
||||
private static Vector2Int ParseSize(JToken token)
|
||||
{
|
||||
return token == null
|
||||
? default
|
||||
: new Vector2Int(token.Value<int?>("w") ?? 0, token.Value<int?>("h") ?? 0);
|
||||
}
|
||||
|
||||
private static void AddAscending(List<int> target, int from, int to)
|
||||
{
|
||||
for (var index = from; index <= to; index++)
|
||||
{
|
||||
target.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddDescending(List<int> target, int from, int to)
|
||||
{
|
||||
for (var index = from; index >= to; index--)
|
||||
{
|
||||
target.Add(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Error(
|
||||
FrameAnimationImportIssueCode code,
|
||||
string sourceId,
|
||||
string message)
|
||||
{
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
code,
|
||||
sourceId,
|
||||
sourceId,
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a21095473b1495440acab244f9e470c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8edeae42e1831084cbf05db9d9e5d1e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,307 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameAnimationTextInputWindow : EditorWindow
|
||||
{
|
||||
private string label;
|
||||
private string value;
|
||||
private string warning;
|
||||
private Func<string, string> validate;
|
||||
private Action<string> confirm;
|
||||
private Vector2 scroll;
|
||||
|
||||
public static void Show(
|
||||
string title,
|
||||
string label,
|
||||
string initialValue,
|
||||
string warning,
|
||||
Func<string, string> validate,
|
||||
Action<string> confirm)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationTextInputWindow>();
|
||||
window.titleContent = new GUIContent(title);
|
||||
window.label = label;
|
||||
window.value = initialValue ?? string.Empty;
|
||||
window.warning = warning ?? string.Empty;
|
||||
window.validate = validate;
|
||||
window.confirm = confirm;
|
||||
window.minSize = new Vector2(460f, 220f);
|
||||
window.maxSize = new Vector2(700f, 420f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
|
||||
GUI.SetNextControlName("FrameAnimationTextInput");
|
||||
value = EditorGUILayout.TextField(value);
|
||||
if (!string.IsNullOrEmpty(warning))
|
||||
{
|
||||
EditorGUILayout.HelpBox(warning, MessageType.Warning);
|
||||
}
|
||||
var error = validate?.Invoke(value) ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
|
||||
{
|
||||
if (GUILayout.Button("确认", GUILayout.Width(90f)))
|
||||
{
|
||||
var callback = confirm;
|
||||
var result = value;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.delayCall += FocusInput;
|
||||
}
|
||||
|
||||
private void FocusInput()
|
||||
{
|
||||
if (this != null)
|
||||
{
|
||||
Focus();
|
||||
EditorGUI.FocusTextInControl("FrameAnimationTextInput");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationManualClipWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private FrameClip source;
|
||||
private string id = "NewClip";
|
||||
private string displayName = "New Clip";
|
||||
private bool external;
|
||||
private Action<FrameClip> completed;
|
||||
|
||||
public static void Show(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip source,
|
||||
Action<FrameClip> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationManualClipWindow>();
|
||||
window.titleContent = new GUIContent(source == null ? "Create Manual Clip" : "Copy As Manual Clip");
|
||||
window.graph = graph;
|
||||
window.source = source;
|
||||
window.completed = completed;
|
||||
var baseId = source != null ? source.Id + "_Manual" : "NewClip";
|
||||
window.id = MakeUniqueId(graph, baseId);
|
||||
window.displayName = source != null ? source.DisplayName + " Manual" : window.id;
|
||||
window.minSize = new Vector2(460f, 260f);
|
||||
window.maxSize = new Vector2(700f, 420f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField(source == null ? "创建 Manual Clip" : "复制为 Manual Clip", EditorStyles.boldLabel);
|
||||
id = EditorGUILayout.TextField("ID", id);
|
||||
displayName = EditorGUILayout.TextField("Display Name", displayName);
|
||||
external = EditorGUILayout.ToggleLeft("保存为外部独立 .asset(默认保存为 Graph sub-asset)", external);
|
||||
var error = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
|
||||
? string.Empty
|
||||
: "ID 为空或与当前 Graph 的 Clip / Flow 冲突。";
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
if (source != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"帧表将深拷贝,但 Sprite 仍复用原资源;ImportSource 关联会被清除。",
|
||||
MessageType.Info);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
|
||||
{
|
||||
if (GUILayout.Button("创建", GUILayout.Width(90f)))
|
||||
{
|
||||
Create();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void Create()
|
||||
{
|
||||
var path = string.Empty;
|
||||
if (external)
|
||||
{
|
||||
path = EditorUtility.SaveFilePanelInProject(
|
||||
"保存外部 Manual Clip",
|
||||
id,
|
||||
"asset",
|
||||
"请选择 Assets 下的保存位置。");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var succeeded = source == null
|
||||
? FrameAnimationAssetOperations.CreateManualClip(graph, id, displayName, path, out var result, out var error)
|
||||
: FrameAnimationAssetOperations.CopyToManual(graph, source, id, displayName, path, out result, out error);
|
||||
if (!succeeded)
|
||||
{
|
||||
EditorUtility.DisplayDialog("操作失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
|
||||
private static string MakeUniqueId(FrameAnimationGraph graph, string baseId)
|
||||
{
|
||||
var candidate = baseId;
|
||||
var suffix = 2;
|
||||
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
||||
{
|
||||
candidate = baseId + suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationExistingClipWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private FrameClip clip;
|
||||
private Action<FrameClip> completed;
|
||||
|
||||
public static void Show(FrameAnimationGraph graph, Action<FrameClip> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationExistingClipWindow>();
|
||||
window.titleContent = new GUIContent("Add Existing Manual Clip");
|
||||
window.graph = graph;
|
||||
window.completed = completed;
|
||||
window.minSize = new Vector2(460f, 180f);
|
||||
window.maxSize = new Vector2(640f, 260f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("添加外部 Manual Clip", EditorStyles.boldLabel);
|
||||
clip = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(clip == null))
|
||||
{
|
||||
if (GUILayout.Button("添加", GUILayout.Width(90f)))
|
||||
{
|
||||
if (!FrameAnimationAssetOperations.AddExistingManualClip(graph, clip, out var error))
|
||||
{
|
||||
EditorUtility.DisplayDialog("添加失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
var result = clip;
|
||||
Close();
|
||||
callback?.Invoke(result);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationFlowWindow : EditorWindow
|
||||
{
|
||||
private FrameAnimationGraph graph;
|
||||
private AnimationNode entry;
|
||||
private string id;
|
||||
private string displayName;
|
||||
private Action<AnimationFlow> completed;
|
||||
|
||||
internal static void Show(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode entry,
|
||||
Action<AnimationFlow> completed)
|
||||
{
|
||||
var window = CreateInstance<FrameAnimationFlowWindow>();
|
||||
window.titleContent = new GUIContent("Create Animation Flow");
|
||||
window.graph = graph;
|
||||
window.entry = entry;
|
||||
window.id = FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry);
|
||||
window.displayName = window.id;
|
||||
window.completed = completed;
|
||||
window.minSize = new Vector2(460f, 230f);
|
||||
window.maxSize = new Vector2(700f, 360f);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("从选中节点创建 Flow", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.TextField("Entry Node", entry?.DisplayName ?? string.Empty);
|
||||
}
|
||||
id = EditorGUILayout.TextField("ID", id);
|
||||
displayName = EditorGUILayout.TextField("Display Name", displayName);
|
||||
var idError = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
|
||||
? string.Empty
|
||||
: "ID 为空或与当前 Graph 中的 Clip / Flow 冲突。";
|
||||
var owner = graph?.Flows.FirstOrDefault(flow => flow != null && flow.EntryNodeId == entry?.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
idError = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
}
|
||||
if (!string.IsNullOrEmpty(idError))
|
||||
{
|
||||
EditorGUILayout.HelpBox(idError, MessageType.Error);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("取消", GUILayout.Width(90f))) Close();
|
||||
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(idError)))
|
||||
{
|
||||
if (GUILayout.Button("创建", GUILayout.Width(90f)))
|
||||
{
|
||||
if (!FrameAnimationGraphMutationService.CreateFlow(
|
||||
graph, entry, id, displayName, out var flow, out var error))
|
||||
{
|
||||
EditorUtility.DisplayDialog("创建 Flow 失败", error, "确定");
|
||||
return;
|
||||
}
|
||||
var callback = completed;
|
||||
Close();
|
||||
callback?.Invoke(flow);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8f58e7b80263c24087a98874796e91d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,254 +0,0 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal enum FrameAnimationBrowserEntryKind
|
||||
{
|
||||
Header,
|
||||
Clip,
|
||||
Flow,
|
||||
Source
|
||||
}
|
||||
|
||||
internal enum FrameAnimationBrowserIssueState
|
||||
{
|
||||
None,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationBrowserEntry
|
||||
{
|
||||
public FrameAnimationBrowserEntryKind Kind;
|
||||
public string Title = string.Empty;
|
||||
public string Id = string.Empty;
|
||||
public string TypeLabel = string.Empty;
|
||||
public string Meta = string.Empty;
|
||||
public string StatusLabel = string.Empty;
|
||||
public Color? AccentColor;
|
||||
public FrameAnimationBrowserIssueState IssueState;
|
||||
public FrameAnimationEditorSelection Selection;
|
||||
|
||||
public bool IsHeader => Kind == FrameAnimationBrowserEntryKind.Header;
|
||||
}
|
||||
|
||||
internal readonly struct FrameAnimationFlowColorPalette
|
||||
{
|
||||
public readonly Color Raw;
|
||||
public readonly Color Stroke;
|
||||
public readonly Color Header;
|
||||
public readonly Color Muted;
|
||||
public readonly Color Text;
|
||||
|
||||
public FrameAnimationFlowColorPalette(Color raw, Color stroke, Color header, Color muted, Color text)
|
||||
{
|
||||
Raw = raw;
|
||||
Stroke = stroke;
|
||||
Header = header;
|
||||
Muted = muted;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationFlowColorUtility
|
||||
{
|
||||
private static readonly Color Panel = new Color32(43, 47, 52, 255);
|
||||
private static readonly Color LightText = new Color32(244, 246, 248, 255);
|
||||
private static readonly Color DarkText = new Color32(24, 27, 30, 255);
|
||||
private static readonly Color Neutral = new Color32(100, 168, 216, 255);
|
||||
|
||||
internal static Color ResolveRaw(FrameAnimationGraph graph, AnimationFlow flow)
|
||||
{
|
||||
return Opaque(ResolveStored(graph, flow));
|
||||
}
|
||||
|
||||
internal static Color ResolveStored(FrameAnimationGraph graph, AnimationFlow flow)
|
||||
{
|
||||
if (graph == null || flow == null)
|
||||
{
|
||||
return Neutral;
|
||||
}
|
||||
var data = graph.EditorData?.FlowEditorData == null
|
||||
? null
|
||||
: System.Linq.Enumerable.FirstOrDefault(graph.EditorData.FlowEditorData,
|
||||
item => item != null && item.FlowId == flow.Id);
|
||||
return data?.Color ?? Neutral;
|
||||
}
|
||||
|
||||
internal static FrameAnimationFlowColorPalette CreatePalette(Color value)
|
||||
{
|
||||
var raw = Opaque(value);
|
||||
var luminance = Luminance(raw);
|
||||
var stroke = luminance < 0.30f
|
||||
? Color.Lerp(raw, Color.white, Mathf.InverseLerp(0.30f, 0.02f, luminance) * 0.42f)
|
||||
: luminance > 0.82f
|
||||
? Color.Lerp(raw, Color.black, Mathf.InverseLerp(0.82f, 1f, luminance) * 0.22f)
|
||||
: raw;
|
||||
stroke.a = 1f;
|
||||
var header = Color.Lerp(Panel, stroke, 0.68f);
|
||||
header.a = 1f;
|
||||
var muted = Color.Lerp(Panel, stroke, 0.42f);
|
||||
muted.a = 1f;
|
||||
var text = Luminance(header) > 0.22f ? DarkText : LightText;
|
||||
return new FrameAnimationFlowColorPalette(raw, stroke, header, muted, text);
|
||||
}
|
||||
|
||||
internal static Color Opaque(Color value) => new Color(
|
||||
Mathf.Clamp01(value.r), Mathf.Clamp01(value.g), Mathf.Clamp01(value.b), 1f);
|
||||
|
||||
internal static float Luminance(Color value)
|
||||
{
|
||||
var linear = value.linear;
|
||||
return linear.r * 0.2126f + linear.g * 0.7152f + linear.b * 0.0722f;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationResourceRowElement : VisualElement
|
||||
{
|
||||
private readonly VisualElement flowDot;
|
||||
private readonly Label typeBadge;
|
||||
private readonly Label titleLabel;
|
||||
private readonly Label idLabel;
|
||||
private readonly Label metaLabel;
|
||||
private readonly Label statusBadge;
|
||||
|
||||
internal FrameAnimationResourceRowElement()
|
||||
{
|
||||
AddToClassList("fa-resource-row");
|
||||
|
||||
var main = new VisualElement();
|
||||
main.AddToClassList("fa-resource-row__main");
|
||||
flowDot = new VisualElement { pickingMode = PickingMode.Ignore };
|
||||
flowDot.AddToClassList("fa-flow-dot");
|
||||
typeBadge = new Label { pickingMode = PickingMode.Ignore };
|
||||
typeBadge.AddToClassList("fa-badge");
|
||||
titleLabel = new Label { pickingMode = PickingMode.Ignore };
|
||||
titleLabel.AddToClassList("fa-resource-row__name");
|
||||
idLabel = new Label { pickingMode = PickingMode.Ignore };
|
||||
idLabel.AddToClassList("fa-resource-row__id");
|
||||
statusBadge = new Label { pickingMode = PickingMode.Ignore };
|
||||
statusBadge.AddToClassList("fa-badge");
|
||||
main.Add(flowDot);
|
||||
main.Add(typeBadge);
|
||||
main.Add(titleLabel);
|
||||
main.Add(idLabel);
|
||||
main.Add(statusBadge);
|
||||
|
||||
metaLabel = new Label { pickingMode = PickingMode.Ignore };
|
||||
metaLabel.AddToClassList("fa-resource-row__meta");
|
||||
Add(main);
|
||||
Add(metaLabel);
|
||||
}
|
||||
|
||||
internal void Bind(FrameAnimationBrowserEntry entry, bool selected)
|
||||
{
|
||||
userData = entry;
|
||||
var isHeader = entry?.IsHeader == true;
|
||||
EnableInClassList("fa-resource-group", isHeader);
|
||||
EnableInClassList("fa-resource-row--selected", selected);
|
||||
EnableInClassList("fa-resource-row--error", entry?.IssueState == FrameAnimationBrowserIssueState.Error);
|
||||
EnableInClassList("fa-resource-row--warning", entry?.IssueState == FrameAnimationBrowserIssueState.Warning);
|
||||
if (entry == null)
|
||||
{
|
||||
style.display = DisplayStyle.None;
|
||||
return;
|
||||
}
|
||||
style.display = DisplayStyle.Flex;
|
||||
titleLabel.text = entry.Title;
|
||||
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
idLabel.text = entry.Id;
|
||||
metaLabel.text = entry.Meta;
|
||||
typeBadge.text = entry.TypeLabel;
|
||||
typeBadge.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
idLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
metaLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
statusBadge.text = entry.StatusLabel;
|
||||
statusBadge.style.display = isHeader || string.IsNullOrEmpty(entry.StatusLabel) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
flowDot.style.display = !isHeader && entry.AccentColor.HasValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (entry.AccentColor.HasValue)
|
||||
{
|
||||
flowDot.style.backgroundColor = FrameAnimationFlowColorUtility.CreatePalette(entry.AccentColor.Value).Stroke;
|
||||
}
|
||||
typeBadge.EnableInClassList("fa-badge--blue", entry.Kind == FrameAnimationBrowserEntryKind.Clip);
|
||||
typeBadge.EnableInClassList("fa-badge--purple", entry.Kind == FrameAnimationBrowserEntryKind.Flow);
|
||||
typeBadge.EnableInClassList("fa-badge--green", entry.Kind == FrameAnimationBrowserEntryKind.Source);
|
||||
statusBadge.EnableInClassList("fa-badge--red", entry.IssueState == FrameAnimationBrowserIssueState.Error);
|
||||
statusBadge.EnableInClassList("fa-badge--yellow", entry.IssueState == FrameAnimationBrowserIssueState.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationFrameRowElement : VisualElement
|
||||
{
|
||||
private readonly ObjectField spriteField;
|
||||
private readonly IntegerField durationField;
|
||||
private readonly Label frameNameLabel;
|
||||
private readonly Label sourceIndexLabel;
|
||||
private FrameClip clip;
|
||||
private int index;
|
||||
private bool readOnly;
|
||||
private Action changed;
|
||||
|
||||
internal FrameAnimationFrameRowElement()
|
||||
{
|
||||
AddToClassList("fa-frame-row");
|
||||
spriteField = new ObjectField("Sprite") { objectType = typeof(Sprite), allowSceneObjects = false };
|
||||
durationField = new IntegerField("Duration Ms");
|
||||
var readOnlyRow = new VisualElement();
|
||||
readOnlyRow.AddToClassList("fa-frame-row__readonly");
|
||||
frameNameLabel = new Label();
|
||||
sourceIndexLabel = new Label();
|
||||
readOnlyRow.Add(frameNameLabel);
|
||||
readOnlyRow.Add(sourceIndexLabel);
|
||||
Add(spriteField);
|
||||
Add(durationField);
|
||||
Add(readOnlyRow);
|
||||
spriteField.RegisterValueChangedCallback(OnSpriteChanged);
|
||||
durationField.RegisterValueChangedCallback(OnDurationChanged);
|
||||
}
|
||||
|
||||
internal void Bind(FrameClip value, int itemIndex, bool isReadOnly, Action onChanged)
|
||||
{
|
||||
clip = value;
|
||||
index = itemIndex;
|
||||
readOnly = isReadOnly;
|
||||
changed = onChanged;
|
||||
userData = itemIndex;
|
||||
var frame = clip != null && index >= 0 && index < clip.Frames.Count ? clip.Frames[index] : null;
|
||||
spriteField.SetValueWithoutNotify(frame?.Sprite);
|
||||
durationField.SetValueWithoutNotify(frame?.DurationMs ?? 100);
|
||||
frameNameLabel.text = "Frame " + (frame?.FrameName ?? string.Empty);
|
||||
sourceIndexLabel.text = "Source " + (frame?.SourceIndex.ToString() ?? "-1");
|
||||
spriteField.SetEnabled(!readOnly);
|
||||
durationField.SetEnabled(!readOnly);
|
||||
}
|
||||
|
||||
private void OnSpriteChanged(ChangeEvent<UnityEngine.Object> evt)
|
||||
{
|
||||
if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return;
|
||||
var serialized = new SerializedObject(clip);
|
||||
serialized.Update();
|
||||
var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index);
|
||||
element.FindPropertyRelative("sprite").objectReferenceValue = evt.newValue;
|
||||
serialized.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(clip);
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
private void OnDurationChanged(ChangeEvent<int> evt)
|
||||
{
|
||||
if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return;
|
||||
var serialized = new SerializedObject(clip);
|
||||
serialized.Update();
|
||||
var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index);
|
||||
element.FindPropertyRelative("durationMs").intValue = Mathf.Max(1, evt.newValue);
|
||||
serialized.ApplyModifiedProperties();
|
||||
durationField.SetValueWithoutNotify(Mathf.Max(1, evt.newValue));
|
||||
EditorUtility.SetDirty(clip);
|
||||
changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 876191b33b10eef44b133cc291ebc912
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,744 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal enum FrameAnimationEditorSelectionKind
|
||||
{
|
||||
None,
|
||||
Graph,
|
||||
Clip,
|
||||
Flow,
|
||||
Source,
|
||||
Node,
|
||||
Edge
|
||||
}
|
||||
|
||||
internal readonly struct FrameAnimationEditorSelection
|
||||
{
|
||||
public FrameAnimationEditorSelectionKind Kind { get; }
|
||||
public object Value { get; }
|
||||
|
||||
public FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind kind, object value)
|
||||
{
|
||||
Kind = kind;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum FrameAnimationClipFilter
|
||||
{
|
||||
All,
|
||||
Manual,
|
||||
Imported,
|
||||
Normal,
|
||||
Missing,
|
||||
Referenced,
|
||||
Unused
|
||||
}
|
||||
|
||||
internal enum FrameAnimationClipSort
|
||||
{
|
||||
Name,
|
||||
Source,
|
||||
FrameCount,
|
||||
Duration,
|
||||
Missing,
|
||||
ReferenceCount
|
||||
}
|
||||
|
||||
internal static class FrameAnimationAssetReferenceIndex
|
||||
{
|
||||
public static IReadOnlyList<FrameAnimationGraph> FindGraphsReferencing(FrameClip clip)
|
||||
{
|
||||
if (clip == null)
|
||||
{
|
||||
return Array.Empty<FrameAnimationGraph>();
|
||||
}
|
||||
|
||||
return AssetDatabase.FindAssets("t:FrameAnimationGraph")
|
||||
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid)))
|
||||
.Where(graph => graph != null && graph.Clips.Any(item => item == clip))
|
||||
.Distinct()
|
||||
.OrderBy(graph => AssetDatabase.GetAssetPath(graph), StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationResourceQuery
|
||||
{
|
||||
public static IReadOnlyList<FrameClip> QueryClips(
|
||||
FrameAnimationGraph graph,
|
||||
string search,
|
||||
FrameAnimationClipFilter filter,
|
||||
FrameAnimationClipSort sort)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return Array.Empty<FrameClip>();
|
||||
}
|
||||
|
||||
var sourceNames = graph.ImportSources.Where(source => source != null)
|
||||
.GroupBy(source => source.InternalId ?? string.Empty)
|
||||
.ToDictionary(group => group.Key, group => group.First().DisplayName ?? string.Empty);
|
||||
var query = graph.Clips.Where(clip => clip != null && MatchesSearch(clip, sourceNames, search));
|
||||
query = filter switch
|
||||
{
|
||||
FrameAnimationClipFilter.Manual => query.Where(clip => !clip.IsImported),
|
||||
FrameAnimationClipFilter.Imported => query.Where(clip => clip.IsImported),
|
||||
FrameAnimationClipFilter.Normal => query.Where(clip => !clip.IsImported || !clip.ImportInfo.IsMissingFromSource),
|
||||
FrameAnimationClipFilter.Missing => query.Where(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource),
|
||||
FrameAnimationClipFilter.Referenced => query.Where(clip => ReferenceCount(graph, clip) > 0),
|
||||
FrameAnimationClipFilter.Unused => query.Where(clip => ReferenceCount(graph, clip) == 0),
|
||||
_ => query
|
||||
};
|
||||
|
||||
IOrderedEnumerable<FrameClip> ordered = sort switch
|
||||
{
|
||||
FrameAnimationClipSort.Source => query.OrderBy(clip => SourceName(clip, sourceNames), StringComparer.Ordinal),
|
||||
FrameAnimationClipSort.FrameCount => query.OrderBy(clip => clip.FrameCount),
|
||||
FrameAnimationClipSort.Duration => query.OrderBy(clip => clip.TotalDurationMs),
|
||||
FrameAnimationClipSort.Missing => query.OrderBy(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource ? 1 : 0),
|
||||
FrameAnimationClipSort.ReferenceCount => query.OrderBy(clip => ReferenceCount(graph, clip)),
|
||||
_ => query.OrderBy(clip => clip.DisplayName ?? clip.Id, StringComparer.Ordinal)
|
||||
};
|
||||
return ordered.ThenBy(clip => clip.Id, StringComparer.Ordinal)
|
||||
.ThenBy(clip => AssetDatabase.GetAssetPath(clip), StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static int ReferenceCount(FrameAnimationGraph graph, FrameClip clip)
|
||||
{
|
||||
return graph?.Nodes.Count(node => node != null && node.ClipId == clip?.Id) ?? 0;
|
||||
}
|
||||
|
||||
public static string SourceName(
|
||||
FrameClip clip,
|
||||
IReadOnlyDictionary<string, string> sourceNames = null)
|
||||
{
|
||||
if (clip == null || !clip.IsImported)
|
||||
{
|
||||
return clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "Manual";
|
||||
}
|
||||
var sourceId = clip.ImportInfo?.ImportSourceId;
|
||||
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
|
||||
sourceNames.TryGetValue(sourceId, out var name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
return sourceId ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool MatchesSearch(
|
||||
FrameClip clip,
|
||||
IReadOnlyDictionary<string, string> sourceNames,
|
||||
string search)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return Contains(clip.Id, search) || Contains(clip.DisplayName, search) ||
|
||||
Contains(clip.ImportInfo?.SourceTagName, search) ||
|
||||
Contains(SourceName(clip, sourceNames), search);
|
||||
}
|
||||
|
||||
private static bool Contains(string value, string search)
|
||||
{
|
||||
return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationAssetOperations
|
||||
{
|
||||
public static bool IsPlayableIdAvailable(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
FrameClip exceptClip = null,
|
||||
AnimationFlow exceptFlow = null)
|
||||
{
|
||||
return graph != null && !string.IsNullOrWhiteSpace(id) &&
|
||||
graph.Clips.All(clip => clip == null || clip == exceptClip || clip.Id != id) &&
|
||||
graph.Flows.All(flow => flow == null || flow == exceptFlow || flow.Id != id);
|
||||
}
|
||||
|
||||
public static bool CreateManualClip(
|
||||
FrameAnimationGraph graph,
|
||||
string id,
|
||||
string displayName,
|
||||
string externalPath,
|
||||
out FrameClip clip,
|
||||
out string error)
|
||||
{
|
||||
clip = null;
|
||||
error = string.Empty;
|
||||
if (!IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "Clip id 为空或与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Create Manual Frame Clip");
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(graph, "Add Manual Frame Clip");
|
||||
clip = ScriptableObject.CreateInstance<FrameClip>();
|
||||
clip.name = id;
|
||||
clip.Configure(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName,
|
||||
Array.Empty<FrameAnimationFrame>(), 1f,
|
||||
graph.Settings.NewManualClipDefaultEndBehavior);
|
||||
if (string.IsNullOrWhiteSpace(externalPath))
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(clip, graph);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.CreateAsset(clip, externalPath);
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(clip, "Create Manual Frame Clip");
|
||||
graph.AddClip(clip);
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Undo.RevertAllDownToGroup(group);
|
||||
clip = null;
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool AddExistingManualClip(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null)
|
||||
{
|
||||
error = "Graph 或 Clip 为空。";
|
||||
return false;
|
||||
}
|
||||
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
|
||||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
|
||||
{
|
||||
error = "只能添加独立 .asset 形式、且不由 Graph ImportSource 管理的 Clip。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "当前 Graph 已引用该 Clip。";
|
||||
return false;
|
||||
}
|
||||
if (!IsPlayableIdAvailable(graph, clip.Id))
|
||||
{
|
||||
error = $"playable id '{clip.Id}' 与当前 Graph 中的 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.RecordObject(graph, "Add Existing Manual Frame Clip");
|
||||
graph.AddClip(clip);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CopyToManual(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip source,
|
||||
string id,
|
||||
string displayName,
|
||||
string externalPath,
|
||||
out FrameClip copy,
|
||||
out string error)
|
||||
{
|
||||
copy = null;
|
||||
error = string.Empty;
|
||||
if (source == null || !IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "来源为空,或新 id 与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Copy Frame Clip As Manual");
|
||||
try
|
||||
{
|
||||
Undo.RecordObject(graph, "Add Manual Frame Clip Copy");
|
||||
copy = ScriptableObject.CreateInstance<FrameClip>();
|
||||
copy.name = id;
|
||||
copy.Configure(id,
|
||||
string.IsNullOrWhiteSpace(displayName) ? source.DisplayName : displayName,
|
||||
source.Frames.Select(frame => frame == null
|
||||
? null
|
||||
: new FrameAnimationFrame(frame.Sprite, frame.DurationMs, frame.FrameName, frame.SourceIndex)),
|
||||
source.Speed,
|
||||
source.DefaultEndBehavior);
|
||||
if (string.IsNullOrWhiteSpace(externalPath))
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(copy, graph);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.CreateAsset(copy, externalPath);
|
||||
}
|
||||
Undo.RegisterCreatedObjectUndo(copy, "Create Manual Frame Clip Copy");
|
||||
graph.AddClip(copy);
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(copy);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Undo.RevertAllDownToGroup(group);
|
||||
copy = null;
|
||||
error = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool RenameClip(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
string newId,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "Clip 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||||
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
|
||||
{
|
||||
error = "外部 Manual Clip 被多个 Graph 共享,禁止重命名:" +
|
||||
string.Join(", ", owners.Select(owner => owner.name));
|
||||
return false;
|
||||
}
|
||||
if (!IsPlayableIdAvailable(graph, newId, clip))
|
||||
{
|
||||
error = "新 id 为空或与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldId = clip.Id;
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Rename Frame Clip");
|
||||
Undo.RecordObject(graph, "Update Frame Clip References");
|
||||
Undo.RecordObject(clip, "Rename Frame Clip");
|
||||
clip.SetId(newId);
|
||||
foreach (var node in graph.Nodes.Where(node => node != null && node.ClipId == oldId))
|
||||
{
|
||||
node.SetClipId(newId);
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == oldId)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(newId);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
EditorUtility.SetDirty(clip);
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RenameFlow(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationFlow flow,
|
||||
string newId,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || !graph.Flows.Contains(flow) ||
|
||||
!IsPlayableIdAvailable(graph, newId, exceptFlow: flow))
|
||||
{
|
||||
error = "Flow 不属于当前 Graph,或新 id 与现有 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldId = flow.Id;
|
||||
Undo.RecordObject(graph, "Rename Animation Flow");
|
||||
flow.SetId(newId);
|
||||
if (graph.Settings.DefaultPlayableId == oldId)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(newId);
|
||||
}
|
||||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null && data.FlowId == oldId))
|
||||
{
|
||||
data.SetFlowId(newId);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RenameGraph(FrameAnimationGraph graph, string newId, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || string.IsNullOrWhiteSpace(newId))
|
||||
{
|
||||
error = "Graph id 不能为空。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Rename Frame Animation Graph");
|
||||
graph.SetId(newId);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||||
{
|
||||
if (!CanRemoveClip(graph, clip, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Remove Frame Clip");
|
||||
Undo.RecordObject(graph, "Remove Frame Clip Reference");
|
||||
graph.RemoveClip(clip);
|
||||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) == AssetDatabase.GetAssetPath(graph))
|
||||
{
|
||||
Undo.DestroyObjectImmediate(clip);
|
||||
}
|
||||
EditorUtility.SetDirty(graph);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanRemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "Clip 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var nodeCount = graph.Nodes.Count(node => node != null && node.ClipId == clip.Id);
|
||||
if (nodeCount > 0)
|
||||
{
|
||||
error = $"Clip 仍被 {nodeCount} 个 AnimationNode 引用。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == clip.Id)
|
||||
{
|
||||
error = "Clip 仍被 defaultPlayableId 引用。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||||
{
|
||||
if (!CanRemoveFlow(graph, flow, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Remove Animation Flow");
|
||||
graph.RemoveFlow(flow);
|
||||
graph.EditorData.RemoveFlowData(flow.Id);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanRemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || !graph.Flows.Contains(flow))
|
||||
{
|
||||
error = "Flow 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Settings.DefaultPlayableId == flow.Id)
|
||||
{
|
||||
error = "Flow 仍被 defaultPlayableId 引用。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveSource(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationImportSource source,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || source == null || !graph.ImportSources.Contains(source))
|
||||
{
|
||||
error = "ImportSource 不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var clips = graph.Clips.Where(clip => clip != null &&
|
||||
clip.ImportInfo?.ImportSourceId == source.InternalId).ToArray();
|
||||
if (clips.Length > 0)
|
||||
{
|
||||
error = "ImportSource 仍关联 Imported Clip:" + string.Join(", ", clips.Select(clip => clip.Id));
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Remove Frame Animation ImportSource");
|
||||
graph.RemoveImportSource(source);
|
||||
EditorUtility.SetDirty(graph);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationEditorIssue
|
||||
{
|
||||
public FrameAnimationValidationSeverity Severity { get; }
|
||||
public string Code { get; }
|
||||
public FrameAnimationEditorSelection Selection { get; }
|
||||
public string Message { get; }
|
||||
public string Suggestion { get; }
|
||||
|
||||
public FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity severity,
|
||||
string code,
|
||||
FrameAnimationEditorSelection selection,
|
||||
string message,
|
||||
string suggestion)
|
||||
{
|
||||
Severity = severity;
|
||||
Code = code ?? string.Empty;
|
||||
Selection = selection;
|
||||
Message = message ?? string.Empty;
|
||||
Suggestion = suggestion ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationEditorValidationService
|
||||
{
|
||||
public static IReadOnlyList<FrameAnimationEditorIssue> Validate(
|
||||
FrameAnimationGraph graph,
|
||||
bool includeImports,
|
||||
out FrameAnimationImportPreview importPreview)
|
||||
{
|
||||
importPreview = null;
|
||||
if (graph == null)
|
||||
{
|
||||
return Array.Empty<FrameAnimationEditorIssue>();
|
||||
}
|
||||
|
||||
var issues = FrameAnimationGraphValidator.Validate(graph).Issues
|
||||
.Select(issue => new FrameAnimationEditorIssue(
|
||||
issue.Severity,
|
||||
issue.Code.ToString(),
|
||||
Locate(graph, issue.TargetType, issue.TargetId),
|
||||
issue.Message,
|
||||
Suggestion(issue.Code)))
|
||||
.ToList();
|
||||
|
||||
foreach (var duplicate in graph.Clips.Where(clip => clip != null)
|
||||
.GroupBy(clip => clip).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"DuplicateClipReference",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, duplicate.Key),
|
||||
$"Graph 重复引用 Clip '{duplicate.Key.Id}'。",
|
||||
"移除重复的 Clip 引用。"));
|
||||
}
|
||||
|
||||
var graphPath = AssetDatabase.GetAssetPath(graph);
|
||||
foreach (var clip in graph.Clips.Where(clip => clip != null))
|
||||
{
|
||||
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) != graphPath)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"ForeignSubAssetClip",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||||
$"Clip '{clip.Id}' 是其他资产的 sub-asset。",
|
||||
"移除该引用,并复制为当前 Graph 的 Manual Clip。"));
|
||||
}
|
||||
if (clip.IsImported && (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"ImportedClipOwnership",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
||||
$"Imported Clip '{clip.Id}' 不是当前 Graph 的 sub-asset。",
|
||||
"删除非法引用并从对应 ImportSource 重新刷新。"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||||
{
|
||||
var clip = graph.Clips.SingleOrDefault(item => item != null && item.Id == node.ClipId);
|
||||
if (clip?.ImportInfo?.IsMissingFromSource == true)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"NodeReferencesMissingClip",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||||
$"Node '{node.InternalId}' 引用了 Missing Clip '{clip.Id}'。",
|
||||
"恢复源 Tag、修改节点引用或删除该节点。"));
|
||||
}
|
||||
}
|
||||
|
||||
var nodeIds = new HashSet<string>(graph.Nodes.Where(node => node != null)
|
||||
.Select(node => node.InternalId));
|
||||
foreach (var node in graph.Nodes.Where(node => node != null &&
|
||||
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"EditorDataMissing",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
|
||||
$"Node '{node.InternalId}' 缺少画布位置数据。",
|
||||
"打开 Graph Editor 初始化位置,或执行自动布局。"));
|
||||
}
|
||||
foreach (var data in graph.EditorData.NodeEditorData.Where(data => data != null &&
|
||||
!nodeIds.Contains(data.NodeId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"EditorDataOrphan",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"存在孤立的 NodeEditorData '{data.NodeId}'。",
|
||||
"该数据不会影响运行时;确认无引用后可由后续维护工具清理。"));
|
||||
}
|
||||
foreach (var duplicate in graph.EditorData.NodeEditorData.Where(data => data != null)
|
||||
.GroupBy(data => data.NodeId).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"EditorDataDuplicate",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"NodeEditorData '{duplicate.Key}' 重复。",
|
||||
"保留一条对应位置数据并移除重复项。"));
|
||||
}
|
||||
|
||||
var flowIds = new HashSet<string>(graph.Flows.Where(flow => flow != null).Select(flow => flow.Id));
|
||||
foreach (var flow in graph.Flows.Where(flow => flow != null &&
|
||||
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"FlowEditorDataMissing",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
|
||||
$"Flow '{flow.Id}' 缺少画布颜色数据。",
|
||||
"打开 Graph Editor 初始化 Flow 颜色。"));
|
||||
}
|
||||
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null &&
|
||||
!flowIds.Contains(data.FlowId)))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
"FlowEditorDataOrphan",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"存在孤立的 FlowEditorData '{data.FlowId}'。",
|
||||
"确认 Flow 已删除后可由后续维护工具清理。"));
|
||||
}
|
||||
foreach (var duplicate in graph.EditorData.FlowEditorData.Where(data => data != null)
|
||||
.GroupBy(data => data.FlowId).Where(group => group.Count() > 1))
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
"FlowEditorDataDuplicate",
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
|
||||
$"FlowEditorData '{duplicate.Key}' 重复。",
|
||||
"保留一条对应颜色数据并移除重复项。"));
|
||||
}
|
||||
|
||||
if (!includeImports)
|
||||
{
|
||||
return issues;
|
||||
}
|
||||
|
||||
importPreview = FrameAnimationImportService.PreviewAll(graph);
|
||||
foreach (var sourcePreview in importPreview.Sources)
|
||||
{
|
||||
foreach (var importIssue in sourcePreview.Issues)
|
||||
{
|
||||
issues.Add(new FrameAnimationEditorIssue(
|
||||
importIssue.Severity,
|
||||
importIssue.Code.ToString(),
|
||||
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, sourcePreview.Source),
|
||||
importIssue.Message,
|
||||
"打开对应 ImportSource,修复来源设置后重新 Preview。"));
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static FrameAnimationEditorSelection Locate(
|
||||
FrameAnimationGraph graph,
|
||||
FrameAnimationValidationTargetType type,
|
||||
string id)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
FrameAnimationValidationTargetType.Clip => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Clip, graph.Clips.FirstOrDefault(clip => clip != null && clip.Id == id)),
|
||||
FrameAnimationValidationTargetType.Flow => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Flow, graph.Flows.FirstOrDefault(flow => flow != null && flow.Id == id)),
|
||||
FrameAnimationValidationTargetType.Node => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Node, graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == id)),
|
||||
FrameAnimationValidationTargetType.Edge => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Edge, graph.Edges.FirstOrDefault(edge => edge != null && edge.InternalId == id)),
|
||||
FrameAnimationValidationTargetType.ImportSource => new FrameAnimationEditorSelection(
|
||||
FrameAnimationEditorSelectionKind.Source, graph.ImportSources.FirstOrDefault(source => source != null && source.InternalId == id)),
|
||||
_ => new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
|
||||
};
|
||||
}
|
||||
|
||||
private static string Suggestion(FrameAnimationValidationCode code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
FrameAnimationValidationCode.PlayableIdEmpty => "使用正式 Rename 命令设置唯一 playable id。",
|
||||
FrameAnimationValidationCode.PlayableIdDuplicate => "重命名冲突的 Clip 或 Flow。",
|
||||
FrameAnimationValidationCode.ClipFramesEmpty => "为 Manual Clip 添加帧,或刷新 Imported Clip 来源。",
|
||||
FrameAnimationValidationCode.FrameDurationInvalid => "将 durationMs 修改为大于 0 的整数。",
|
||||
FrameAnimationValidationCode.DefaultPlayableInvalid => "在 Graph 属性中选择有效的默认 Clip 或 Flow。",
|
||||
FrameAnimationValidationCode.ImportedClipMissingFromSource => "恢复源 Tag,或确认没有引用后删除 Missing Clip。",
|
||||
_ => "打开问题对象并修复对应字段或引用。"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationWorkspaceState
|
||||
{
|
||||
private const string Prefix = "AibisDream.FrameAnimation.Workspace.";
|
||||
|
||||
public static string Key(FrameAnimationGraph graph, string name)
|
||||
{
|
||||
var graphPath = graph != null ? AssetDatabase.GetAssetPath(graph) : string.Empty;
|
||||
var graphGuid = string.IsNullOrEmpty(graphPath) ? "none" : AssetDatabase.AssetPathToGUID(graphPath);
|
||||
var project = Hash128.Compute(Application.dataPath).ToString();
|
||||
return Prefix + project + "." + graphGuid + "." + name;
|
||||
}
|
||||
|
||||
public static float GetFloat(FrameAnimationGraph graph, string name, float fallback) =>
|
||||
EditorPrefs.GetFloat(Key(graph, name), fallback);
|
||||
|
||||
public static void SetFloat(FrameAnimationGraph graph, string name, float value) =>
|
||||
EditorPrefs.SetFloat(Key(graph, name), value);
|
||||
|
||||
public static string GetString(FrameAnimationGraph graph, string name, string fallback) =>
|
||||
EditorPrefs.GetString(Key(graph, name), fallback);
|
||||
|
||||
public static void SetString(FrameAnimationGraph graph, string name, string value) =>
|
||||
EditorPrefs.SetString(Key(graph, name), value ?? string.Empty);
|
||||
|
||||
public static bool GetBool(FrameAnimationGraph graph, string name, bool fallback) =>
|
||||
EditorPrefs.GetBool(Key(graph, name), fallback);
|
||||
|
||||
public static void SetBool(FrameAnimationGraph graph, string name, bool value) =>
|
||||
EditorPrefs.SetBool(Key(graph, name), value);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2643ad174ef237e43b98400d60c4a201
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,603 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal static class FrameAnimationFlowFocusPolicy
|
||||
{
|
||||
public static AnimationFlow FindFocusedFlow(FrameAnimationGraph graph, string focusedFlowId)
|
||||
{
|
||||
return graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId);
|
||||
}
|
||||
|
||||
public static bool Contains(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
FrameAnimationEditorSelection selection)
|
||||
{
|
||||
var flow = FindFocusedFlow(graph, focusedFlowId);
|
||||
if (flow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
|
||||
return selection.Value switch
|
||||
{
|
||||
AnimationNode node => reachable.Nodes.Contains(node),
|
||||
AnimationEdge edge => reachable.Edges.Contains(edge),
|
||||
AnimationFlow selectedFlow => selectedFlow == flow,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldExitForSelection(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
FrameAnimationEditorSelection selection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(focusedFlowId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return selection.Kind switch
|
||||
{
|
||||
FrameAnimationEditorSelectionKind.None => false,
|
||||
FrameAnimationEditorSelectionKind.Flow => false,
|
||||
FrameAnimationEditorSelectionKind.Node => !Contains(graph, focusedFlowId, selection),
|
||||
FrameAnimationEditorSelectionKind.Edge => !Contains(graph, focusedFlowId, selection),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldExitForSelectionSet(
|
||||
FrameAnimationGraph graph,
|
||||
string focusedFlowId,
|
||||
IEnumerable<FrameAnimationEditorSelection> selections)
|
||||
{
|
||||
if (string.IsNullOrEmpty(focusedFlowId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var items = (selections ?? Array.Empty<FrameAnimationEditorSelection>()).ToArray();
|
||||
return items.Length > 0 && items.Any(item =>
|
||||
(item.Kind == FrameAnimationEditorSelectionKind.Node ||
|
||||
item.Kind == FrameAnimationEditorSelectionKind.Edge)
|
||||
? !Contains(graph, focusedFlowId, item)
|
||||
: ShouldExitForSelection(graph, focusedFlowId, item));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationGraphImpact
|
||||
{
|
||||
public IReadOnlyList<AnimationFlow> Flows { get; }
|
||||
public int LostNodeCount { get; }
|
||||
|
||||
internal FrameAnimationGraphImpact(IReadOnlyList<AnimationFlow> flows, int lostNodeCount)
|
||||
{
|
||||
Flows = flows ?? Array.Empty<AnimationFlow>();
|
||||
LostNodeCount = lostNodeCount;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphImpactAnalyzer
|
||||
{
|
||||
public static FrameAnimationGraphImpact AnalyzeEdgeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var removed = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null));
|
||||
return Analyze(graph, Array.Empty<AnimationNode>(), removed);
|
||||
}
|
||||
|
||||
public static FrameAnimationGraphImpact AnalyzeNodeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes)
|
||||
{
|
||||
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null));
|
||||
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var removedEdges = new HashSet<AnimationEdge>(graph.Edges.Where(edge => edge != null &&
|
||||
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
||||
return Analyze(graph, removedNodes, removedEdges);
|
||||
}
|
||||
|
||||
public static FrameAnimationGraphImpact AnalyzeRemoval(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
||||
}
|
||||
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>())
|
||||
.Where(node => node != null));
|
||||
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var removedEdges = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>())
|
||||
.Where(edge => edge != null));
|
||||
removedEdges.UnionWith(graph.Edges.Where(edge => edge != null &&
|
||||
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
|
||||
return Analyze(graph, removedNodes, removedEdges);
|
||||
}
|
||||
|
||||
private static FrameAnimationGraphImpact Analyze(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyCollection<AnimationNode> removedNodes,
|
||||
IReadOnlyCollection<AnimationEdge> removedEdges)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
|
||||
}
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var removedNodeIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
|
||||
var affected = new List<AnimationFlow>();
|
||||
var lost = new HashSet<string>();
|
||||
foreach (var flow in graph.Flows.Where(flow => flow != null))
|
||||
{
|
||||
var before = topology.GetReachable(flow.EntryNodeId).Nodes
|
||||
.Where(node => node != null).Select(node => node.InternalId).ToHashSet();
|
||||
var after = ReachableWithout(graph, flow.EntryNodeId, removedNodeIds, removedEdges);
|
||||
var flowLost = before.Where(nodeId => !after.Contains(nodeId)).ToArray();
|
||||
if (flowLost.Length > 0)
|
||||
{
|
||||
affected.Add(flow);
|
||||
lost.UnionWith(flowLost);
|
||||
}
|
||||
}
|
||||
return new FrameAnimationGraphImpact(
|
||||
affected.OrderBy(flow => flow.Id, StringComparer.Ordinal).ToArray(),
|
||||
lost.Count);
|
||||
}
|
||||
|
||||
private static HashSet<string> ReachableWithout(
|
||||
FrameAnimationGraph graph,
|
||||
string entry,
|
||||
HashSet<string> removedNodeIds,
|
||||
IReadOnlyCollection<AnimationEdge> removedEdges)
|
||||
{
|
||||
var result = new HashSet<string>();
|
||||
if (removedNodeIds.Contains(entry ?? string.Empty))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var outgoing = graph.Edges.Where(edge => edge != null && !removedEdges.Contains(edge))
|
||||
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
var queue = new Queue<string>();
|
||||
queue.Enqueue(entry ?? string.Empty);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var id = queue.Dequeue();
|
||||
if (removedNodeIds.Contains(id) || !result.Add(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (outgoing.TryGetValue(id, out var edges))
|
||||
{
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
queue.Enqueue(edge.ToNodeId ?? string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphMutationService
|
||||
{
|
||||
private static readonly Color[] FlowColors =
|
||||
{
|
||||
new Color(0.24f, 0.65f, 1f), new Color(0.42f, 0.8f, 0.42f),
|
||||
new Color(1f, 0.62f, 0.24f), new Color(0.82f, 0.42f, 0.9f),
|
||||
new Color(1f, 0.38f, 0.52f), new Color(0.25f, 0.82f, 0.78f)
|
||||
};
|
||||
|
||||
public static bool CreateNode(
|
||||
FrameAnimationGraph graph,
|
||||
FrameClip clip,
|
||||
Vector2 position,
|
||||
out AnimationNode node,
|
||||
out string error)
|
||||
{
|
||||
node = null;
|
||||
error = string.Empty;
|
||||
if (graph == null || clip == null || !graph.Clips.Contains(clip))
|
||||
{
|
||||
error = "只能为当前 Graph 中的 FrameClip 创建节点。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Create Frame Animation Node");
|
||||
node = new AnimationNode(clip.Id,
|
||||
string.IsNullOrWhiteSpace(clip.DisplayName) ? clip.Id : clip.DisplayName);
|
||||
graph.AddNode(node);
|
||||
graph.EditorData.GetOrCreateNodeData(node.InternalId, position).SetPosition(position);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryConnect(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode from,
|
||||
AnimationNode to,
|
||||
out AnimationEdge edge,
|
||||
out string error)
|
||||
{
|
||||
edge = null;
|
||||
if (!CanConnect(graph, from, to, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Connect Frame Animation Nodes");
|
||||
edge = new AnimationEdge(from.InternalId, to.InternalId);
|
||||
graph.AddEdge(edge);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanConnect(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode from,
|
||||
AnimationNode to,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || from == null || to == null ||
|
||||
graph.Nodes.Count(node => node == from) != 1 || graph.Nodes.Count(node => node == to) != 1)
|
||||
{
|
||||
error = "连接两端必须唯一属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
if (from == to || from.InternalId == to.InternalId)
|
||||
{
|
||||
error = "节点不允许连接到自身。";
|
||||
return false;
|
||||
}
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
if (topology.GetOutgoing(from.InternalId).Count > 0)
|
||||
{
|
||||
error = "第一版每个节点最多只能有一个后继。";
|
||||
return false;
|
||||
}
|
||||
if (from.EndBehaviorOverride.HasValue)
|
||||
{
|
||||
error = "该节点已设置终点结束行为,请先清除覆盖。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Edges.Any(existing => existing != null &&
|
||||
existing.FromNodeId == from.InternalId && existing.ToNodeId == to.InternalId))
|
||||
{
|
||||
error = "该连接已经存在。";
|
||||
return false;
|
||||
}
|
||||
if (topology.WouldCreateCycle(from.InternalId, to.InternalId))
|
||||
{
|
||||
error = "该连接会形成自连接或多节点环路。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetNodeDisplayName(FrameAnimationGraph graph, AnimationNode node, string value)
|
||||
{
|
||||
Undo.RecordObject(graph, "Rename Frame Animation Node");
|
||||
node.SetDisplayName(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static void SetNodeClip(FrameAnimationGraph graph, AnimationNode node, FrameClip clip)
|
||||
{
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node Clip");
|
||||
node.SetClipId(clip != null ? clip.Id : string.Empty);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static void SetNodeSpeed(FrameAnimationGraph graph, AnimationNode node, float? value)
|
||||
{
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node Speed");
|
||||
node.SetSpeedOverride(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool SetNodeEndBehavior(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode node,
|
||||
FrameClipEndBehavior? value,
|
||||
bool removeOutgoing,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var outgoing = graph.Edges.Where(edge => edge != null && edge.FromNodeId == node.InternalId).ToArray();
|
||||
if (value.HasValue && outgoing.Length > 0 && !removeOutgoing)
|
||||
{
|
||||
error = "节点仍有后继 Edge。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Frame Animation Node End Behavior");
|
||||
if (value.HasValue)
|
||||
{
|
||||
foreach (var edge in outgoing)
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
}
|
||||
node.SetEndBehaviorOverride(value);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RemoveEdges(FrameAnimationGraph graph, IEnumerable<AnimationEdge> edges)
|
||||
{
|
||||
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
|
||||
if (graph == null || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, "Disconnect Frame Animation Nodes");
|
||||
foreach (var edge in items)
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool RemoveNodes(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
bool deleteEntryFlows,
|
||||
out string error)
|
||||
{
|
||||
return RemoveNodesAndEdges(
|
||||
graph, nodes, Array.Empty<AnimationEdge>(), deleteEntryFlows, out error);
|
||||
}
|
||||
|
||||
public static bool RemoveNodesAndEdges(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> nodes,
|
||||
IEnumerable<AnimationEdge> additionalEdges,
|
||||
bool deleteEntryFlows,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null)
|
||||
{
|
||||
error = "Graph 已失效。";
|
||||
return false;
|
||||
}
|
||||
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
|
||||
var extraEdges = (additionalEdges ?? Array.Empty<AnimationEdge>())
|
||||
.Where(edge => edge != null).Distinct().ToArray();
|
||||
var ids = new HashSet<string>(items.Select(node => node.InternalId));
|
||||
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
|
||||
if (entryFlows.Length > 0 && !deleteEntryFlows)
|
||||
{
|
||||
error = "删除项包含 Flow 入口节点。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Delete Frame Animation Graph Elements");
|
||||
foreach (var edge in graph.Edges.Where(edge => edge != null &&
|
||||
(ids.Contains(edge.FromNodeId) || ids.Contains(edge.ToNodeId) || extraEdges.Contains(edge))).ToArray())
|
||||
{
|
||||
graph.RemoveEdge(edge);
|
||||
}
|
||||
foreach (var flow in entryFlows)
|
||||
{
|
||||
graph.RemoveFlow(flow);
|
||||
graph.EditorData.RemoveFlowData(flow.Id);
|
||||
if (graph.Settings.DefaultPlayableId == flow.Id)
|
||||
{
|
||||
graph.Settings.SetDefaultPlayableId(string.Empty);
|
||||
}
|
||||
}
|
||||
foreach (var node in items)
|
||||
{
|
||||
graph.RemoveNode(node);
|
||||
graph.EditorData.RemoveNodeData(node.InternalId);
|
||||
}
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
|
||||
{
|
||||
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "_Flow";
|
||||
var candidate = root;
|
||||
var suffix = 2;
|
||||
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
||||
{
|
||||
candidate = root + suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public static bool CreateFlow(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationNode entry,
|
||||
string id,
|
||||
string displayName,
|
||||
out AnimationFlow flow,
|
||||
out string error)
|
||||
{
|
||||
flow = null;
|
||||
error = string.Empty;
|
||||
if (graph == null || entry == null || !graph.Nodes.Contains(entry))
|
||||
{
|
||||
error = "Flow 入口必须属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing.EntryNodeId == entry.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
return false;
|
||||
}
|
||||
if (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
|
||||
{
|
||||
error = "Flow id 为空或与 Clip / Flow 冲突。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Create Animation Flow");
|
||||
flow = new AnimationFlow(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName, entry.InternalId);
|
||||
graph.AddFlow(flow);
|
||||
graph.EditorData.GetOrCreateFlowData(flow.Id, FlowColors[(graph.Flows.Count - 1) % FlowColors.Length]);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool SetFlowEntry(
|
||||
FrameAnimationGraph graph,
|
||||
AnimationFlow flow,
|
||||
AnimationNode entry,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (graph == null || flow == null || entry == null ||
|
||||
!graph.Flows.Contains(flow) || !graph.Nodes.Contains(entry))
|
||||
{
|
||||
error = "Flow 或入口节点不属于当前 Graph。";
|
||||
return false;
|
||||
}
|
||||
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing != flow &&
|
||||
existing.EntryNodeId == entry.InternalId);
|
||||
if (owner != null)
|
||||
{
|
||||
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Animation Flow Entry");
|
||||
flow.SetEntryNodeId(entry.InternalId);
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetFlowColor(FrameAnimationGraph graph, AnimationFlow flow, Color value)
|
||||
{
|
||||
if (graph == null || flow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, "Set Animation Flow Color");
|
||||
graph.EditorData.GetOrCreateFlowData(flow.Id, value).SetColor(value);
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
public static bool EnsureEditorData(FrameAnimationGraph graph)
|
||||
{
|
||||
if (graph == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var missingNodes = graph.Nodes.Where(node => node != null &&
|
||||
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)).ToArray();
|
||||
var missingFlows = graph.Flows.Where(flow => flow != null &&
|
||||
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)).ToArray();
|
||||
if (missingNodes.Length == 0 && missingFlows.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Undo.RecordObject(graph, "Initialize Frame Animation Graph Layout");
|
||||
for (var index = 0; index < missingNodes.Length; index++)
|
||||
{
|
||||
var position = new Vector2((index % 4) * 300f, (index / 4) * 180f);
|
||||
graph.EditorData.GetOrCreateNodeData(missingNodes[index].InternalId, position);
|
||||
}
|
||||
for (var index = 0; index < missingFlows.Length; index++)
|
||||
{
|
||||
graph.EditorData.GetOrCreateFlowData(missingFlows[index].Id,
|
||||
FlowColors[(graph.Flows.ToList().IndexOf(missingFlows[index]) + FlowColors.Length) % FlowColors.Length]);
|
||||
}
|
||||
Dirty(graph);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Vector2 GetNodePosition(FrameAnimationGraph graph, AnimationNode node)
|
||||
{
|
||||
return graph?.EditorData.NodeEditorData.FirstOrDefault(data =>
|
||||
data != null && data.NodeId == node?.InternalId)?.Position ?? Vector2.zero;
|
||||
}
|
||||
|
||||
public static void SetNodePositions(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyDictionary<AnimationNode, Vector2> positions,
|
||||
string undoName)
|
||||
{
|
||||
if (graph == null || positions == null || positions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(graph, undoName);
|
||||
foreach (var pair in positions)
|
||||
{
|
||||
graph.EditorData.GetOrCreateNodeData(pair.Key.InternalId, pair.Value).SetPosition(pair.Value);
|
||||
}
|
||||
Dirty(graph);
|
||||
}
|
||||
|
||||
private static void Dirty(FrameAnimationGraph graph)
|
||||
{
|
||||
EditorUtility.SetDirty(graph);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameAnimationGraphLayoutService
|
||||
{
|
||||
public static IReadOnlyDictionary<AnimationNode, Vector2> Calculate(
|
||||
FrameAnimationGraph graph,
|
||||
IEnumerable<AnimationNode> scope)
|
||||
{
|
||||
var source = scope ?? (graph != null
|
||||
? graph.Nodes
|
||||
: (IEnumerable<AnimationNode>)Array.Empty<AnimationNode>());
|
||||
var nodes = source.Where(node => node != null).Distinct()
|
||||
.GroupBy(node => node.InternalId ?? string.Empty)
|
||||
.Select(group => group.First()).ToArray();
|
||||
if (graph == null || nodes.Length == 0)
|
||||
{
|
||||
return new Dictionary<AnimationNode, Vector2>();
|
||||
}
|
||||
var ids = new HashSet<string>(nodes.Select(node => node.InternalId));
|
||||
var edges = graph.Edges.Where(edge => edge != null && ids.Contains(edge.FromNodeId) && ids.Contains(edge.ToNodeId)).ToArray();
|
||||
var incoming = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
incoming[edge.ToNodeId]++;
|
||||
}
|
||||
var ranks = nodes.ToDictionary(node => node.InternalId, _ => 0);
|
||||
var queue = new Queue<AnimationNode>(nodes.Where(node => incoming[node.InternalId] == 0)
|
||||
.OrderBy(node => node.InternalId, StringComparer.Ordinal));
|
||||
var processed = new HashSet<string>();
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var node = queue.Dequeue();
|
||||
if (!processed.Add(node.InternalId)) continue;
|
||||
foreach (var edge in edges.Where(edge => edge.FromNodeId == node.InternalId))
|
||||
{
|
||||
ranks[edge.ToNodeId] = Math.Max(ranks[edge.ToNodeId], ranks[node.InternalId] + 1);
|
||||
incoming[edge.ToNodeId]--;
|
||||
if (incoming[edge.ToNodeId] == 0)
|
||||
{
|
||||
queue.Enqueue(nodes.First(item => item.InternalId == edge.ToNodeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
var oldPositions = nodes.ToDictionary(node => node,
|
||||
node => FrameAnimationGraphMutationService.GetNodePosition(graph, node));
|
||||
var originX = oldPositions.Values.Min(position => position.x);
|
||||
var originY = oldPositions.Values.Min(position => position.y);
|
||||
var result = new Dictionary<AnimationNode, Vector2>();
|
||||
foreach (var group in nodes.GroupBy(node => ranks[node.InternalId]).OrderBy(group => group.Key))
|
||||
{
|
||||
var ordered = group.OrderBy(node => oldPositions[node].y)
|
||||
.ThenBy(node => node.InternalId, StringComparer.Ordinal).ToArray();
|
||||
for (var index = 0; index < ordered.Length; index++)
|
||||
{
|
||||
result[ordered[index]] = new Vector2(originX + group.Key * 300f, originY + index * 180f);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b397ed96c5b44df89626f866c32e03dd
|
||||
@@ -1,33 +0,0 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
[CustomEditor(typeof(FrameAnimationGraph))]
|
||||
public sealed class FrameAnimationGraphEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var graph = (FrameAnimationGraph)target;
|
||||
EditorGUILayout.LabelField("FrameAnimationGraph", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.TextField("ID", graph.Id);
|
||||
EditorGUILayout.TextField("Display Name", graph.DisplayName);
|
||||
EditorGUILayout.IntField("Clips", graph.Clips.Count);
|
||||
EditorGUILayout.IntField("Nodes", graph.Nodes.Count);
|
||||
EditorGUILayout.IntField("Edges", graph.Edges.Count);
|
||||
EditorGUILayout.IntField("Flows", graph.Flows.Count);
|
||||
EditorGUILayout.IntField("Import Sources", graph.ImportSources.Count);
|
||||
EditorGUILayout.TextField("Default Playable", graph.Settings?.DefaultPlayableId ?? string.Empty);
|
||||
}
|
||||
EditorGUILayout.HelpBox(
|
||||
"Graph 的资源、来源、重命名和删除操作只能在主工作台中执行。",
|
||||
MessageType.Info);
|
||||
if (GUILayout.Button("Open Frame Animation Graph Editor"))
|
||||
{
|
||||
FrameAnimationGraphEditorWindow.Open(graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74f3b50c2650607499ea1c82a282f00f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,783 +0,0 @@
|
||||
:root {
|
||||
--fa-bg: rgb(27, 29, 32);
|
||||
--fa-canvas: rgb(30, 32, 35);
|
||||
--fa-panel: rgb(36, 39, 43);
|
||||
--fa-panel-raised: rgb(43, 46, 51);
|
||||
--fa-panel-hover: rgb(48, 52, 58);
|
||||
--fa-field: rgb(28, 31, 34);
|
||||
--fa-line: rgb(17, 19, 21);
|
||||
--fa-line-soft: rgb(62, 67, 74);
|
||||
--fa-text: rgb(215, 217, 220);
|
||||
--fa-muted: rgb(143, 150, 158);
|
||||
--fa-accent: rgb(61, 145, 212);
|
||||
--fa-accent-soft: rgb(36, 79, 112);
|
||||
--fa-success: rgb(86, 185, 123);
|
||||
--fa-warning: rgb(224, 184, 88);
|
||||
--fa-error: rgb(223, 106, 106);
|
||||
}
|
||||
|
||||
.fa-workbench {
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
background-color: var(--fa-bg);
|
||||
color: var(--fa-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fa-global-toolbar-host,
|
||||
.fa-main-host,
|
||||
.fa-bottom-host {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fa-main-host {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fa-workbench .unity-toolbar {
|
||||
min-height: 32px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
background-color: var(--fa-panel-raised);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-workbench .unity-toolbar-button,
|
||||
.fa-workbench .unity-button {
|
||||
min-height: 24px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: var(--fa-line);
|
||||
border-right-color: var(--fa-line);
|
||||
border-top-color: var(--fa-line);
|
||||
border-bottom-color: var(--fa-line);
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background-color: rgb(54, 58, 63);
|
||||
color: var(--fa-text);
|
||||
}
|
||||
|
||||
.fa-workbench .unity-toolbar-button:hover,
|
||||
.fa-workbench .unity-button:hover {
|
||||
background-color: rgb(70, 75, 81);
|
||||
}
|
||||
|
||||
.fa-button--primary {
|
||||
background-color: rgb(42, 105, 150);
|
||||
border-left-color: rgb(25, 74, 104);
|
||||
border-right-color: rgb(25, 74, 104);
|
||||
border-top-color: rgb(25, 74, 104);
|
||||
border-bottom-color: rgb(25, 74, 104);
|
||||
}
|
||||
|
||||
.fa-button--danger {
|
||||
background-color: rgb(87, 54, 56);
|
||||
color: rgb(244, 205, 207);
|
||||
}
|
||||
|
||||
.fa-toolbar-group {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fa-toolbar-separator {
|
||||
width: 1px;
|
||||
height: 21px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
background-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-panel {
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
background-color: var(--fa-panel);
|
||||
}
|
||||
|
||||
.fa-panel-header {
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
-unity-text-align: middle-left;
|
||||
-unity-font-style: bold;
|
||||
background-color: rgb(44, 47, 51);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-resizer--vertical {
|
||||
width: 4px;
|
||||
background-color: rgb(24, 26, 29);
|
||||
}
|
||||
|
||||
.fa-resizer--horizontal {
|
||||
height: 4px;
|
||||
background-color: rgb(24, 26, 29);
|
||||
}
|
||||
|
||||
.fa-resizer--vertical:hover,
|
||||
.fa-resizer--horizontal:hover {
|
||||
background-color: var(--fa-accent);
|
||||
}
|
||||
|
||||
.fa-resource-tabs {
|
||||
min-height: 34px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 3px;
|
||||
background-color: rgb(31, 34, 38);
|
||||
}
|
||||
|
||||
.fa-resource-tabs .unity-toolbar-button,
|
||||
.fa-bottom-tabs .unity-toolbar-button {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-height: 28px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: rgba(0, 0, 0, 0);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: rgb(169, 175, 182);
|
||||
}
|
||||
|
||||
.fa-resource-tabs .unity-toolbar-button:hover,
|
||||
.fa-bottom-tabs .unity-toolbar-button:hover {
|
||||
color: var(--fa-text);
|
||||
background-color: rgb(48, 52, 57);
|
||||
}
|
||||
|
||||
.fa-resource-tabs .unity-toolbar-button.fa-tab--active,
|
||||
.fa-bottom-tabs .unity-toolbar-button.fa-tab--active {
|
||||
color: white;
|
||||
background-color: var(--fa-panel-raised);
|
||||
border-bottom-color: var(--fa-accent);
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.fa-resource-tools {
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-resource-list {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
background-color: var(--fa-panel);
|
||||
}
|
||||
|
||||
.fa-resource-row {
|
||||
height: 48px;
|
||||
padding-left: 9px;
|
||||
padding-right: 7px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 4px;
|
||||
border-left-width: 3px;
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(32, 35, 39);
|
||||
}
|
||||
|
||||
.fa-resource-row:hover {
|
||||
background-color: var(--fa-panel-hover);
|
||||
}
|
||||
|
||||
.fa-resource-row--selected {
|
||||
background-color: rgb(37, 75, 103);
|
||||
border-left-color: rgb(98, 178, 234);
|
||||
}
|
||||
|
||||
.fa-resource-row--error {
|
||||
border-left-color: var(--fa-error);
|
||||
}
|
||||
|
||||
.fa-resource-row--warning {
|
||||
border-left-color: var(--fa-warning);
|
||||
}
|
||||
|
||||
.fa-resource-row__main,
|
||||
.fa-resource-row__meta,
|
||||
.fa-badge-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fa-resource-row__main {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.fa-resource-row__name {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
-unity-font-style: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fa-resource-row__id {
|
||||
max-width: 115px;
|
||||
margin-left: 5px;
|
||||
color: rgb(156, 163, 171);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fa-resource-row__meta {
|
||||
height: 17px;
|
||||
color: var(--fa-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.fa-resource-group {
|
||||
height: 27px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
-unity-text-align: middle-left;
|
||||
-unity-font-style: bold;
|
||||
color: rgb(184, 189, 195);
|
||||
background-color: rgb(41, 44, 49);
|
||||
border-top-width: 1px;
|
||||
border-top-color: rgb(52, 56, 61);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(26, 28, 31);
|
||||
}
|
||||
|
||||
.fa-badge {
|
||||
min-height: 16px;
|
||||
margin-right: 4px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
-unity-text-align: middle-center;
|
||||
font-size: 9px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
background-color: rgb(52, 56, 61);
|
||||
color: rgb(174, 180, 187);
|
||||
}
|
||||
|
||||
.fa-badge--blue { background-color: rgb(37, 63, 85); color: rgb(143, 202, 239); }
|
||||
.fa-badge--green { background-color: rgb(35, 63, 49); color: rgb(131, 209, 160); }
|
||||
.fa-badge--yellow { background-color: rgb(74, 62, 37); color: rgb(240, 200, 111); }
|
||||
.fa-badge--red { background-color: rgb(75, 41, 44); color: rgb(239, 141, 147); }
|
||||
.fa-badge--purple { background-color: rgb(59, 48, 75); color: rgb(195, 168, 232); }
|
||||
|
||||
.fa-flow-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 6px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.fa-canvas-panel {
|
||||
flex-grow: 1;
|
||||
min-width: 280px;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
background-color: var(--fa-canvas);
|
||||
}
|
||||
|
||||
.fa-canvas-toolbar {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.fa-toolbar-label {
|
||||
margin-left: 2px;
|
||||
margin-right: 4px;
|
||||
color: var(--fa-muted);
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.fa-canvas-toolbar #flow-focus-field {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.fa-flow-focus {
|
||||
min-width: 110px;
|
||||
max-width: 240px;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
-unity-font-style: bold;
|
||||
-unity-text-align: middle-left;
|
||||
border-left-width: 3px;
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.fa-empty-state {
|
||||
color: var(--fa-muted);
|
||||
}
|
||||
|
||||
.fa-node {
|
||||
min-width: 224px;
|
||||
width: 224px;
|
||||
max-width: 224px;
|
||||
min-height: 164px;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: var(--fa-line);
|
||||
border-right-color: var(--fa-line);
|
||||
border-top-color: var(--fa-line);
|
||||
border-bottom-color: var(--fa-line);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
background-color: rgb(43, 47, 52);
|
||||
}
|
||||
|
||||
.fa-node .title {
|
||||
min-height: 31px;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
background-color: rgb(46, 111, 155);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-node .title-label {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.fa-node .input,
|
||||
.fa-node .output {
|
||||
min-width: 12px;
|
||||
}
|
||||
|
||||
.fa-node__meta {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
color: rgb(175, 181, 188);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.fa-node__clip-id {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fa-node__behavior {
|
||||
margin-left: 5px;
|
||||
color: rgb(201, 205, 210);
|
||||
}
|
||||
|
||||
.fa-node__badges {
|
||||
min-height: 21px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
margin-top: 5px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fa-node__entry {
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 5px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
-unity-text-align: middle-center;
|
||||
-unity-font-style: bold;
|
||||
font-size: 9px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.fa-node__preview {
|
||||
height: 78px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.fa-node--focused {
|
||||
border-left-width: 2px;
|
||||
}
|
||||
|
||||
.fa-node.selected {
|
||||
border-left-width: 2px;
|
||||
border-right-width: 2px;
|
||||
border-top-width: 2px;
|
||||
border-bottom-width: 2px;
|
||||
border-left-color: rgb(105, 185, 237);
|
||||
border-right-color: rgb(105, 185, 237);
|
||||
border-top-color: rgb(105, 185, 237);
|
||||
border-bottom-color: rgb(105, 185, 237);
|
||||
}
|
||||
|
||||
.fa-node--preview-current {
|
||||
border-left-color: rgb(55, 174, 240);
|
||||
border-right-color: rgb(55, 174, 240);
|
||||
border-top-color: rgb(55, 174, 240);
|
||||
border-bottom-color: rgb(55, 174, 240);
|
||||
}
|
||||
|
||||
.fa-node--preview-passed { border-left-color: rgb(83, 154, 113); }
|
||||
.fa-node--preview-upcoming { opacity: 0.72; }
|
||||
|
||||
.fa-inspector-scroll,
|
||||
.fa-bottom-scroll {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
background-color: var(--fa-panel);
|
||||
}
|
||||
|
||||
.fa-inspector-title {
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-inspector-title__name {
|
||||
font-size: 15px;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.fa-inspector-title__id {
|
||||
margin-top: 3px;
|
||||
color: var(--fa-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.fa-section {
|
||||
padding-bottom: 8px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-section__title {
|
||||
height: 27px;
|
||||
padding-left: 10px;
|
||||
-unity-text-align: middle-left;
|
||||
-unity-font-style: bold;
|
||||
background-color: rgb(41, 44, 48);
|
||||
}
|
||||
|
||||
.fa-section__body {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.fa-section__body > .unity-base-field,
|
||||
.fa-section__body > .unity-button,
|
||||
.fa-section__body > .fa-readonly-row,
|
||||
.fa-section__body > .fa-callout {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.fa-readonly-row {
|
||||
min-height: 22px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fa-readonly-row__label {
|
||||
width: 118px;
|
||||
color: rgb(173, 179, 186);
|
||||
}
|
||||
|
||||
.fa-readonly-row__value {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
color: rgb(197, 202, 208);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fa-callout {
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
white-space: normal;
|
||||
border-left-width: 3px;
|
||||
border-left-color: var(--fa-accent);
|
||||
background-color: rgb(34, 48, 58);
|
||||
color: rgb(188, 209, 223);
|
||||
}
|
||||
|
||||
.fa-callout--warning { border-left-color: var(--fa-warning); background-color: rgb(57, 51, 33); color: rgb(228, 208, 161); }
|
||||
.fa-callout--error { border-left-color: var(--fa-error); background-color: rgb(63, 39, 42); color: rgb(239, 184, 188); }
|
||||
|
||||
.fa-frame-row {
|
||||
min-height: 72px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(48, 51, 55);
|
||||
}
|
||||
|
||||
.fa-frame-row__readonly {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.fa-frame-row__readonly > .unity-label {
|
||||
flex-grow: 1;
|
||||
color: var(--fa-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.fa-bottom-panel {
|
||||
flex-direction: column;
|
||||
min-height: 28px;
|
||||
background-color: rgb(32, 35, 39);
|
||||
border-top-width: 1px;
|
||||
border-top-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-bottom-toolbar {
|
||||
min-height: 31px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.fa-bottom-tabs {
|
||||
min-width: 230px;
|
||||
height: 30px;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.fa-bottom-tabs .unity-toolbar-button {
|
||||
min-width: 108px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.fa-bottom-toolbar .fa-bottom-collapse {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fa-result-controls {
|
||||
min-height: 31px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--fa-line);
|
||||
}
|
||||
|
||||
.fa-result-header,
|
||||
.fa-result-row {
|
||||
min-height: 29px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(41, 44, 48);
|
||||
}
|
||||
|
||||
.fa-result-header {
|
||||
min-height: 26px;
|
||||
color: var(--fa-muted);
|
||||
background-color: rgb(40, 43, 48);
|
||||
}
|
||||
|
||||
.fa-result-row:hover { background-color: var(--fa-panel-hover); }
|
||||
.fa-result-severity { width: 82px; -unity-font-style: bold; }
|
||||
.fa-result-kind { width: 135px; }
|
||||
.fa-result-object { width: 180px; }
|
||||
.fa-result-message { flex-grow: 1; min-width: 260px; white-space: normal; }
|
||||
.fa-severity--error { color: var(--fa-error); }
|
||||
.fa-severity--warning { color: var(--fa-warning); }
|
||||
.fa-severity--info { color: rgb(114, 189, 232); }
|
||||
|
||||
.fa-preview {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: rgb(25, 25, 25);
|
||||
border-right-color: rgb(25, 25, 25);
|
||||
border-top-color: rgb(25, 25, 25);
|
||||
border-bottom-color: rgb(25, 25, 25);
|
||||
}
|
||||
|
||||
.fa-preview__image {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.fa-preview__empty {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
-unity-text-align: middle-center;
|
||||
color: rgb(190, 190, 190);
|
||||
}
|
||||
|
||||
.fa-clip-preview-panel {
|
||||
flex-shrink: 0;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(28, 28, 28);
|
||||
}
|
||||
|
||||
.fa-selection-preview-header {
|
||||
min-height: 34px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fa-selection-preview-title {
|
||||
color: var(--fa-text);
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.fa-selection-preview-target {
|
||||
margin-top: 1px;
|
||||
color: var(--fa-muted);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fa-clip-preview-panel .fa-preview {
|
||||
height: 190px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.fa-preview-controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-height: 27px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.fa-selection-preview-panel .fa-preview-controls .fa-preview-button {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
max-width: 24px;
|
||||
height: 24px;
|
||||
min-height: 24px;
|
||||
max-height: 24px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fa-preview-button__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fa-preview-timeline {
|
||||
min-width: 28px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fa-preview-time {
|
||||
width: 82px;
|
||||
min-width: 82px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
-unity-text-align: middle-right;
|
||||
}
|
||||
|
||||
.fa-preview-settings {
|
||||
min-height: 27px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fa-preview-setting {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
margin-right: 5px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fa-preview-setting > .unity-label {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
color: var(--fa-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.fa-preview-setting__field {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fa-preview-status {
|
||||
font-size: 10px;
|
||||
color: rgb(185, 185, 185);
|
||||
margin-top: 2px;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a38dd39c699e42348667cc1264ea9076
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="True">
|
||||
<ui:VisualElement name="fa-workbench" class="fa-workbench">
|
||||
<ui:VisualElement name="global-toolbar-host" class="fa-global-toolbar-host" />
|
||||
<ui:VisualElement name="main-host" class="fa-main-host" />
|
||||
<ui:VisualElement name="bottom-host" class="fa-bottom-host" />
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -1,10 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3286d11c482fe8640a72c689bed6780c
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user