Compare commits
35 Commits
9c43d8dbe9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 80220367ae | |||
| be0290372f | |||
|
|
29c981eb58 | ||
|
|
dbe7d99a90 | ||
| c16c2480ff | |||
| 9ce3173697 | |||
| 13750487cf | |||
| a835e8321b | |||
|
|
8985d98653 | ||
| fdc20cd66f | |||
|
|
879f1e49b8 | ||
|
|
bdb3d40287 | ||
|
|
7f49082975 | ||
| 67320b445f | |||
|
|
eedbe9d41f | ||
|
|
150f157725 | ||
|
|
6e1ce6f6f7 | ||
|
|
f9694c5992 | ||
|
|
625b8e79f9 | ||
|
|
83f3608eae | ||
|
|
fd75502f1a | ||
| 5158c529f7 | |||
| a6f24c4258 | |||
| 2146fdf881 | |||
| 93b085f3cd | |||
| 78fc7fb468 | |||
| c7ba1a4c03 | |||
|
|
7d2afc7001 | ||
|
|
404b2985bf | ||
|
|
81a037637b | ||
|
|
2e1a6f99d2 | ||
|
|
a5862579ff | ||
|
|
545bfae0db | ||
|
|
d24bdfb4c3 | ||
|
|
644c1cc04c |
6
.vsconfig
Normal file
6
.vsconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedGame"
|
||||
]
|
||||
}
|
||||
13
AGENTS.md
Normal file
13
AGENTS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Ichni Workspace Agent Standards
|
||||
|
||||
本文件记录 `ichni Official` 工作区的永久工作标准。除非用户明确要求修改工作标准,否则所有后续工作都必须继续遵守本文件。
|
||||
|
||||
1. 本工作区内的所有项目均遵循统一的编码规范。
|
||||
2. 文档及共享资源应供所有工作区成员查阅。
|
||||
3. `ichni Official` 是游戏本体项目;`ichniCreatorStudio` 位于 `D:\Projects\IchniCreatorStudio`,是游戏的关卡编辑器。二者遵循相似的项目结构和脚本结构,并共享序列化存档类。类名后缀为 `_BM` 的类称为 `BM` 类。
|
||||
4. 当用户要求编写新的游戏元素,也就是继承自 `BaseElement` 的元素,或修改现有游戏元素脚本时,如果条件允许,需要同时在 `ichni Official` 和 `ichniCreatorStudio` 中编写或修改适合对应项目的脚本,并向用户提醒跨项目同步情况。
|
||||
5. `ichni Official` 使用 `Wwise 2024.1.9.8920` 作为音频中间件。
|
||||
6. `ichniCreatorStudio` 因为需要自定义音频加载,使用 Unity 原生音频。
|
||||
7. `ichni Official` 使用 `Yarn Spinner` 制作游戏剧情。允许修改和扩展 `Yarn Spinner`。
|
||||
8. 回复用户时使用简体中文。代码、关键词、API 名称、类名、路径、特殊术语等需要使用英文时,应保留英文。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b64bd0799a2d9a4caade060fa09c19c
|
||||
guid: b620e892c4e6cef42b58998db5e25480
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
116
Assets/000_assets/Anim/DTM_Trail.controller
Normal file
116
Assets/000_assets/Anim/DTM_Trail.controller
Normal file
@@ -0,0 +1,116 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!91 &9100000
|
||||
AnimatorController:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: DTM_Trail
|
||||
serializedVersion: 5
|
||||
m_AnimatorParameters: []
|
||||
m_AnimatorLayers:
|
||||
- serializedVersion: 5
|
||||
m_Name: Base Layer
|
||||
m_StateMachine: {fileID: 5902170936534288239}
|
||||
m_Mask: {fileID: 0}
|
||||
m_Motions: []
|
||||
m_Behaviours: []
|
||||
m_BlendingMode: 0
|
||||
m_SyncedLayerIndex: -1
|
||||
m_DefaultWeight: 0
|
||||
m_IKPass: 0
|
||||
m_SyncedLayerAffectsTiming: 0
|
||||
m_Controller: {fileID: 9100000}
|
||||
--- !u!1109 &1372388572982127020
|
||||
AnimatorTransition:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name:
|
||||
m_Conditions: []
|
||||
m_DstStateMachine: {fileID: 0}
|
||||
m_DstState: {fileID: 1613106866484612845}
|
||||
m_Solo: 0
|
||||
m_Mute: 0
|
||||
m_IsExit: 0
|
||||
serializedVersion: 1
|
||||
--- !u!1102 &1613106866484612845
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Empty
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions: []
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 0}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
--- !u!1107 &5902170936534288239
|
||||
AnimatorStateMachine:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Base Layer
|
||||
m_ChildStates:
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 1613106866484612845}
|
||||
m_Position: {x: 260, y: 50, z: 0}
|
||||
- serializedVersion: 1
|
||||
m_State: {fileID: 7058488031596627784}
|
||||
m_Position: {x: 490, y: 50, z: 0}
|
||||
m_ChildStateMachines: []
|
||||
m_AnyStateTransitions: []
|
||||
m_EntryTransitions:
|
||||
- {fileID: 1372388572982127020}
|
||||
m_StateMachineTransitions: {}
|
||||
m_StateMachineBehaviours: []
|
||||
m_AnyStatePosition: {x: 50, y: 20, z: 0}
|
||||
m_EntryPosition: {x: 50, y: 120, z: 0}
|
||||
m_ExitPosition: {x: 800, y: 120, z: 0}
|
||||
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
|
||||
m_DefaultState: {fileID: 1613106866484612845}
|
||||
--- !u!1102 &7058488031596627784
|
||||
AnimatorState:
|
||||
serializedVersion: 6
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: HitPoint_Show01
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions: []
|
||||
m_StateMachineBehaviours: []
|
||||
m_Position: {x: 50, y: 50, z: 0}
|
||||
m_IKOnFeet: 0
|
||||
m_WriteDefaultValues: 1
|
||||
m_Mirror: 0
|
||||
m_SpeedParameterActive: 0
|
||||
m_MirrorParameterActive: 0
|
||||
m_CycleOffsetParameterActive: 0
|
||||
m_TimeParameterActive: 0
|
||||
m_Motion: {fileID: 7400000, guid: 96eebd1d5b544e74ca55536541f943f2, type: 2}
|
||||
m_Tag:
|
||||
m_SpeedParameter:
|
||||
m_MirrorParameter:
|
||||
m_CycleOffsetParameter:
|
||||
m_TimeParameter:
|
||||
8
Assets/000_assets/Anim/DTM_Trail.controller.meta
Normal file
8
Assets/000_assets/Anim/DTM_Trail.controller.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 518b73ce1b7c250478e9e8283dfcbee6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 9100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
434
Assets/000_assets/Anim/HitPoint_Show01.anim
Normal file
434
Assets/000_assets/Anim/HitPoint_Show01.anim
Normal file
@@ -0,0 +1,434 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!74 &7400000
|
||||
AnimationClip:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: HitPoint_Show01
|
||||
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.33333334
|
||||
value: 0.033
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._cube_radius
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- 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.33333334
|
||||
value: 0.05
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._cube_width
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- 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.5
|
||||
value: 0.5
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._ring_radius
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0.16666667
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.28333333
|
||||
value: 0.11
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.5
|
||||
value: 0.01
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._ring_width
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0.9
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.5
|
||||
value: 0.9
|
||||
inSlope: 0
|
||||
outSlope: -0.43721995
|
||||
tangentMode: 1
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.09370208
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0.5
|
||||
inSlope: -0.03507681
|
||||
outSlope: -0.03507681
|
||||
tangentMode: 0
|
||||
weightedMode: 1
|
||||
inWeight: 0.8706604
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._gapsize
|
||||
path: Particle System
|
||||
classID: 199
|
||||
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: 922205677
|
||||
attribute: 2312236205
|
||||
script: {fileID: 0}
|
||||
typeID: 199
|
||||
customType: 22
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
- serializedVersion: 2
|
||||
path: 922205677
|
||||
attribute: 2357554547
|
||||
script: {fileID: 0}
|
||||
typeID: 199
|
||||
customType: 22
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
- serializedVersion: 2
|
||||
path: 922205677
|
||||
attribute: 2401919657
|
||||
script: {fileID: 0}
|
||||
typeID: 199
|
||||
customType: 22
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
- serializedVersion: 2
|
||||
path: 922205677
|
||||
attribute: 2385326758
|
||||
script: {fileID: 0}
|
||||
typeID: 199
|
||||
customType: 22
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
- serializedVersion: 2
|
||||
path: 922205677
|
||||
attribute: 2345372462
|
||||
script: {fileID: 0}
|
||||
typeID: 199
|
||||
customType: 22
|
||||
isPPtrCurve: 0
|
||||
isIntCurve: 0
|
||||
isSerializeReferenceCurve: 0
|
||||
pptrCurveMapping: []
|
||||
m_AnimationClipSettings:
|
||||
serializedVersion: 2
|
||||
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||
m_AdditiveReferencePoseTime: 0
|
||||
m_StartTime: 0
|
||||
m_StopTime: 1
|
||||
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.33333334
|
||||
value: 0.033
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._cube_radius
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- 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.33333334
|
||||
value: 0.05
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._cube_width
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- 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.5
|
||||
value: 0.5
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._ring_radius
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0.16666667
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.28333333
|
||||
value: 0.11
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.5
|
||||
value: 0.01
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._ring_width
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
- serializedVersion: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0.9
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 136
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 0.5
|
||||
value: 0.9
|
||||
inSlope: 0
|
||||
outSlope: -0.43721995
|
||||
tangentMode: 1
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.09370208
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0.5
|
||||
inSlope: -0.03507681
|
||||
outSlope: -0.03507681
|
||||
tangentMode: 0
|
||||
weightedMode: 1
|
||||
inWeight: 0.8706604
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
attribute: material._gapsize
|
||||
path: Particle System
|
||||
classID: 199
|
||||
script: {fileID: 0}
|
||||
flags: 0
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_Events: []
|
||||
8
Assets/000_assets/Anim/HitPoint_Show01.anim.meta
Normal file
8
Assets/000_assets/Anim/HitPoint_Show01.anim.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96eebd1d5b544e74ca55536541f943f2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Assets/Audio/hold end.wav
Normal file
BIN
Assets/Audio/hold end.wav
Normal file
Binary file not shown.
23
Assets/Audio/hold end.wav.meta
Normal file
23
Assets/Audio/hold end.wav.meta
Normal file
@@ -0,0 +1,23 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eeef95cf1b0383642a5b27217fa0e681
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 8
|
||||
defaultSettings:
|
||||
serializedVersion: 2
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
preloadAudioData: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Audio/hold start.wav
Normal file
BIN
Assets/Audio/hold start.wav
Normal file
Binary file not shown.
23
Assets/Audio/hold start.wav.meta
Normal file
23
Assets/Audio/hold start.wav.meta
Normal file
@@ -0,0 +1,23 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6fe5a0c30cc449445b852ff0463a4205
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 8
|
||||
defaultSettings:
|
||||
serializedVersion: 2
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
preloadAudioData: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -278,18 +278,21 @@ MonoBehaviour:
|
||||
threshold:
|
||||
m_OverrideState: 1
|
||||
m_Value: 1.1
|
||||
softKnee:
|
||||
m_OverrideState: 1
|
||||
m_Value: 0.5
|
||||
clamp:
|
||||
m_OverrideState: 1
|
||||
m_Value: 65472
|
||||
scatter:
|
||||
m_OverrideState: 1
|
||||
m_Value: 0.7
|
||||
clamp:
|
||||
m_OverrideState: 1
|
||||
m_Value: 65472
|
||||
diffusion:
|
||||
m_OverrideState: 1
|
||||
m_Value: 6
|
||||
initialDownscaleShift:
|
||||
m_OverrideState: 1
|
||||
m_Value: 1
|
||||
kernelScale:
|
||||
m_OverrideState: 1
|
||||
m_Value: 1
|
||||
tint:
|
||||
m_OverrideState: 1
|
||||
m_Value: {r: 1, g: 1, b: 1, a: 1}
|
||||
@@ -700,10 +703,10 @@ MonoBehaviour:
|
||||
m_Value: 0
|
||||
strengthX:
|
||||
m_OverrideState: 1
|
||||
m_Value: 1920
|
||||
m_Value: 1
|
||||
strengthY:
|
||||
m_OverrideState: 1
|
||||
m_Value: 1080
|
||||
m_Value: 1
|
||||
--- !u!114 &3182184296376474618
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 3
|
||||
|
||||
@@ -228,8 +228,9 @@ namespace Dreamteck.Splines
|
||||
Generate();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
protected override void LateRun()
|
||||
{
|
||||
base.LateRun();
|
||||
if (multithreaded && trs.hasChanged)
|
||||
{
|
||||
_trsRight = trs.right;
|
||||
|
||||
334
Assets/Editor/MetaFileCleaner.cs
Normal file
334
Assets/Editor/MetaFileCleaner.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Unity Editor 工具:识别并清理项目中没有对应资源文件的孤立 .meta 文件。
|
||||
///
|
||||
/// 功能:
|
||||
/// - 扫描 Assets 目录下所有 .meta 文件
|
||||
/// - 检查每个 .meta 文件对应的资源是否存在
|
||||
/// - 列出所有孤立的 .meta 文件
|
||||
/// - 支持一键删除所有孤立 .meta 文件
|
||||
/// - 也可通过 Assets 右键菜单对选中文件夹进行扫描
|
||||
///
|
||||
/// 菜单位置: Tools/Ichni/Clean Unused Meta Files
|
||||
/// 右键菜单: Assets/Clean Unused Meta Files (in folder)
|
||||
/// </summary>
|
||||
public static class MetaFileCleaner
|
||||
{
|
||||
private const string MENU_ROOT = "Tools/Ichni/Clean Unused Meta Files";
|
||||
private const string ASSETS_CONTEXT_MENU = "Assets/Clean Unused Meta Files";
|
||||
|
||||
/// <summary>
|
||||
/// 扫描整个 Assets 目录
|
||||
/// </summary>
|
||||
[MenuItem(MENU_ROOT)]
|
||||
private static void CleanUnusedMetaFiles()
|
||||
{
|
||||
string assetsPath = Application.dataPath;
|
||||
ScanAndCleanMetaFiles(assetsPath, "Entire Assets");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在项目窗口(Project窗口)中选中文件夹时,右键菜单项
|
||||
/// </summary>
|
||||
[MenuItem(ASSETS_CONTEXT_MENU)]
|
||||
private static void CleanUnusedMetaFilesFromSelection()
|
||||
{
|
||||
// 获取当前选中的资源路径(相对路径)
|
||||
string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||||
|
||||
if (string.IsNullOrEmpty(selectedPath))
|
||||
{
|
||||
EditorUtility.DisplayDialog("No Selection", "Please select a folder in the Project window.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
string fullPath = Path.GetFullPath(selectedPath);
|
||||
if (!Directory.Exists(fullPath))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Not a Folder", "Please select a folder, not a file.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
ScanAndCleanMetaFiles(fullPath, selectedPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证右键菜单是否可用:仅当选中的是文件夹时启用
|
||||
/// </summary>
|
||||
[MenuItem(ASSETS_CONTEXT_MENU, true)]
|
||||
private static bool ValidateCleanUnusedMetaFilesFromSelection()
|
||||
{
|
||||
string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||||
if (string.IsNullOrEmpty(selectedPath))
|
||||
return false;
|
||||
|
||||
string fullPath = Path.GetFullPath(selectedPath);
|
||||
return Directory.Exists(fullPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心扫描与清理逻辑
|
||||
/// </summary>
|
||||
private static void ScanAndCleanMetaFiles(string scanDirectory, string displayName)
|
||||
{
|
||||
// 收集所有 .meta 文件
|
||||
string[] allMetaFiles = Directory.GetFiles(scanDirectory, "*.meta", SearchOption.AllDirectories);
|
||||
|
||||
List<string> orphanedMetaFiles = new List<string>();
|
||||
|
||||
// 进度条
|
||||
for (int i = 0; i < allMetaFiles.Length; i++)
|
||||
{
|
||||
string metaFile = allMetaFiles[i];
|
||||
|
||||
if (EditorUtility.DisplayCancelableProgressBar(
|
||||
"Scanning .meta files",
|
||||
$"Checking: {metaFile}",
|
||||
(float)i / allMetaFiles.Length))
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
Debug.LogWarning("[MetaFileCleaner] Scan cancelled by user.");
|
||||
return;
|
||||
}
|
||||
|
||||
// .meta 文件对应的原始资源路径
|
||||
// 例如: "Assets/Textures/foo.png.meta" → "Assets/Textures/foo.png"
|
||||
string assetPath = metaFile.Substring(0, metaFile.Length - ".meta".Length);
|
||||
|
||||
// 检查原始资源是否存在(文件或目录)
|
||||
if (!File.Exists(assetPath) && !Directory.Exists(assetPath))
|
||||
{
|
||||
orphanedMetaFiles.Add(metaFile);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
// 显示结果
|
||||
if (orphanedMetaFiles.Count == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Clean Unused Meta Files",
|
||||
$"Scan completed: {displayName}\n\nNo orphaned .meta files found. ({allMetaFiles.Length} total .meta files checked)",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
// 有孤立文件,显示详细窗口
|
||||
OrphanedMetaWindow.ShowWindow(orphanedMetaFiles, displayName);
|
||||
}
|
||||
|
||||
#region Orphaned Meta Window
|
||||
|
||||
private class OrphanedMetaWindow : EditorWindow
|
||||
{
|
||||
private List<string> orphanedFiles;
|
||||
private Vector2 scrollPos;
|
||||
private string displayName;
|
||||
private bool[] selected;
|
||||
|
||||
public static void ShowWindow(List<string> orphanedFiles, string displayName)
|
||||
{
|
||||
OrphanedMetaWindow window = GetWindow<OrphanedMetaWindow>(true, "Orphaned .meta Files", true);
|
||||
window.orphanedFiles = orphanedFiles;
|
||||
window.displayName = displayName;
|
||||
window.selected = new bool[orphanedFiles.Count];
|
||||
for (int i = 0; i < window.selected.Length; i++)
|
||||
window.selected[i] = true; // 默认全选
|
||||
window.minSize = new Vector2(600, 300);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
GUILayout.Label($"Orphaned .meta files found in: {displayName}", EditorStyles.boldLabel);
|
||||
GUILayout.Label($"Total: {orphanedFiles.Count} orphaned .meta files (out of all files scanned)",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.Space(4);
|
||||
|
||||
// 统计选中数量
|
||||
int selectedCount = selected.Count(s => s);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Select All", GUILayout.Width(100)))
|
||||
{
|
||||
for (int i = 0; i < selected.Length; i++)
|
||||
selected[i] = true;
|
||||
}
|
||||
if (GUILayout.Button("Deselect All", GUILayout.Width(100)))
|
||||
{
|
||||
for (int i = 0; i < selected.Length; i++)
|
||||
selected[i] = false;
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.color = new Color(0.8f, 0.2f, 0.2f);
|
||||
if (GUILayout.Button($"Delete Selected ({selectedCount})", GUILayout.Width(180)))
|
||||
{
|
||||
ConfirmAndDelete();
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
|
||||
if (GUILayout.Button("Cancel", GUILayout.Width(80)))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
|
||||
GUILayout.Space(8);
|
||||
|
||||
// 文件列表
|
||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||
for (int i = 0; i < orphanedFiles.Count; i++)
|
||||
{
|
||||
string relativePath = GetRelativePath(orphanedFiles[i]);
|
||||
selected[i] = EditorGUILayout.ToggleLeft(relativePath, selected[i]);
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
GUILayout.Space(4);
|
||||
GUILayout.Label("Tip: Hover over an item to see full path in tooltip", EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
private void ConfirmAndDelete()
|
||||
{
|
||||
List<string> toDelete = new List<string>();
|
||||
for (int i = 0; i < orphanedFiles.Count; i++)
|
||||
{
|
||||
if (selected[i])
|
||||
toDelete.Add(orphanedFiles[i]);
|
||||
}
|
||||
|
||||
if (toDelete.Count == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Nothing Selected", "Please select at least one file to delete.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算可读的文件大小总计
|
||||
long totalBytes = 0;
|
||||
foreach (string file in toDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
totalBytes += new FileInfo(file).Length;
|
||||
}
|
||||
catch { /* skip */ }
|
||||
}
|
||||
|
||||
string sizeStr = FormatBytes(totalBytes);
|
||||
|
||||
bool confirm = EditorUtility.DisplayDialog(
|
||||
"Confirm Delete",
|
||||
$"Are you sure you want to delete {toDelete.Count} orphaned .meta file(s)?\n\n" +
|
||||
$"Total size: {sizeStr}\n\n" +
|
||||
"This operation cannot be undone. It is recommended to have a backup.\n\n" +
|
||||
"Note: If these .meta files are related to assets you plan to restore, " +
|
||||
"deleting them may cause GUID conflicts. Only proceed if you are sure the " +
|
||||
"original assets are no longer needed.",
|
||||
"Delete",
|
||||
"Cancel");
|
||||
|
||||
if (!confirm)
|
||||
return;
|
||||
|
||||
DeleteFiles(toDelete);
|
||||
}
|
||||
|
||||
private void DeleteFiles(List<string> files)
|
||||
{
|
||||
int deleted = 0;
|
||||
int failed = 0;
|
||||
long recoveredBytes = 0;
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
string file = files[i];
|
||||
|
||||
if (EditorUtility.DisplayCancelableProgressBar(
|
||||
"Deleting orphaned .meta files",
|
||||
$"Deleting: {GetRelativePath(file)} ({i + 1}/{files.Count})",
|
||||
(float)i / files.Count))
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
Debug.LogWarning($"[MetaFileCleaner] Deletion cancelled after {deleted} files deleted.");
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FileInfo fi = new FileInfo(file);
|
||||
long size = fi.Length;
|
||||
|
||||
File.Delete(file);
|
||||
|
||||
// 同时删除 .meta 对应的资源文件(如果存在空的残留文件)
|
||||
string assetPath = file.Substring(0, file.Length - ".meta".Length);
|
||||
if (File.Exists(assetPath) && new FileInfo(assetPath).Length == 0)
|
||||
{
|
||||
File.Delete(assetPath);
|
||||
Debug.Log($"[MetaFileCleaner] Also deleted empty residual file: {assetPath}");
|
||||
}
|
||||
|
||||
deleted++;
|
||||
recoveredBytes += size;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[MetaFileCleaner] Failed to delete: {file}\n{ex.Message}");
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
string summary = $"Deleted: {deleted} file(s).\n" +
|
||||
$"Failed: {failed} file(s).\n" +
|
||||
$"Space recovered: {FormatBytes(recoveredBytes)}";
|
||||
|
||||
Debug.Log($"[MetaFileCleaner] Cleanup complete.\n{summary}");
|
||||
|
||||
EditorUtility.DisplayDialog("Cleanup Complete", summary, "OK");
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private string GetRelativePath(string fullPath)
|
||||
{
|
||||
string assetsIndex = "Assets";
|
||||
int idx = fullPath.IndexOf(assetsIndex);
|
||||
if (idx >= 0)
|
||||
return fullPath.Substring(idx);
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utilities
|
||||
|
||||
/// <summary>
|
||||
/// 可读的文件大小格式化
|
||||
/// </summary>
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
if (bytes < 1024)
|
||||
return $"{bytes} B";
|
||||
if (bytes < 1024 * 1024)
|
||||
return $"{bytes / 1024.0:F1} KB";
|
||||
return $"{bytes / (1024.0 * 1024.0):F2} MB";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Editor/MetaFileCleaner.cs.meta
Normal file
2
Assets/Editor/MetaFileCleaner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5efd05e1466c4da49884d6f4ab9588aa
|
||||
323951
Assets/FR2_Cache.asset
323951
Assets/FR2_Cache.asset
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -163,8 +163,7 @@ MonoBehaviour:
|
||||
type: 3}
|
||||
QuickMoveObj: {fileID: 2260653761532832587, guid: dba2d8dbaab1439469ac4924d33e3397,
|
||||
type: 3}
|
||||
NodeEditor: {fileID: 6091547238259645747, guid: 55d0ca39526c5e94ebac2e6da0c8d80c,
|
||||
type: 3}
|
||||
NodeEditor: {fileID: 0}
|
||||
defaultBackground: {fileID: 21300000, guid: fc6c02e75b66345c29e8a25e2e2bda9c, type: 3}
|
||||
defaultSkyboxMaterial: {fileID: 2100000, guid: 06a20df987b317044a05ea694c976948,
|
||||
type: 2}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3330,6 +3330,143 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 571415733}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &572953708
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 572953709}
|
||||
- component: {fileID: 572953711}
|
||||
- component: {fileID: 572953710}
|
||||
m_Layer: 5
|
||||
m_Name: BiliBoard
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &572953709
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 572953708}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4337614255172217536}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -651, y: 281}
|
||||
m_SizeDelta: {x: 701.8, y: 50}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!114 &572953710
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 572953708}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: "7/10:\u6CE8\u610F\uFF1A\u8BE5\u7248\u672C\u4E4B\u524D\u7684\u8C31\u9762\u7684crossTrack\u70B9\u4F1A\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u8FDB\u884C\u4E00\u6B21paste"
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 36
|
||||
m_fontSizeBase: 36
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 1
|
||||
m_VerticalAlignment: 256
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!222 &572953711
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 572953708}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &574654335
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -9854,6 +9991,7 @@ MonoBehaviour:
|
||||
gridColor: {r: 0.3, g: 0.3, b: 0.3, a: 0.5}
|
||||
pointSize: 15
|
||||
tangentHandleLength: 40
|
||||
lineThickness: 2
|
||||
curve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
@@ -9896,7 +10034,9 @@ MonoBehaviour:
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
rawImage: {fileID: 0}
|
||||
curveLine: {fileID: 0}
|
||||
gridLines: {fileID: 0}
|
||||
borderLine: {fileID: 0}
|
||||
--- !u!114 &2086226218
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -17225,6 +17365,7 @@ RectTransform:
|
||||
- {fileID: 4337614254719278960}
|
||||
- {fileID: 4337614255231150676}
|
||||
- {fileID: 4337614254241459141}
|
||||
- {fileID: 572953709}
|
||||
- {fileID: 4337614253782299621}
|
||||
- {fileID: 4337614254594138373}
|
||||
- {fileID: 4337614253628246447}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor.Commands
|
||||
@@ -14,6 +15,8 @@ namespace Ichni.Editor.Commands
|
||||
/// </summary>
|
||||
public static void ExecuteCommand(ICommand command)
|
||||
{
|
||||
if (command == null) return;
|
||||
|
||||
if (undoStack.Count > 0)
|
||||
{
|
||||
var topCommand = undoStack.Peek();
|
||||
@@ -28,7 +31,14 @@ namespace Ichni.Editor.Commands
|
||||
// 新操作彻底切断原本可能存在的重做未来
|
||||
command.Execute();
|
||||
undoStack.Push(command);
|
||||
redoStack.Clear();
|
||||
redoStack.Clear();
|
||||
}
|
||||
|
||||
public static GameElement ExecuteCreate(Func<GameElement> createAction)
|
||||
{
|
||||
var command = new CreateElementCommand(createAction);
|
||||
ExecuteCommand(command);
|
||||
return command.CreatedElement;
|
||||
}
|
||||
|
||||
public static void Undo()
|
||||
|
||||
245
Assets/Scripts/CommandSystem/GameElementStructuralCommands.cs
Normal file
245
Assets/Scripts/CommandSystem/GameElementStructuralCommands.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.RhythmGame;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor.Commands
|
||||
{
|
||||
internal sealed class ElementTreeSnapshot
|
||||
{
|
||||
private readonly List<BaseElement_BM> elements = new List<BaseElement_BM>();
|
||||
private readonly Guid rootGuid;
|
||||
private readonly Guid parentGuid;
|
||||
private readonly int parentChildIndex;
|
||||
|
||||
private ElementTreeSnapshot(GameElement root)
|
||||
{
|
||||
rootGuid = root.elementGuid;
|
||||
parentGuid = root.parentElement?.elementGuid ?? Guid.Empty;
|
||||
parentChildIndex = root.parentElement != null
|
||||
? root.parentElement.childElementList.IndexOf(root)
|
||||
: -1;
|
||||
|
||||
RegisterExistingAncestorChain(root.parentElement);
|
||||
|
||||
foreach (GameElement element in root.GetAllGameElementsFromThis())
|
||||
{
|
||||
if (element == null) continue;
|
||||
element.SaveBM();
|
||||
if (element.matchedBM != null)
|
||||
elements.Add(element.matchedBM);
|
||||
|
||||
element.submoduleList.RemoveAll(submodule => submodule == null);
|
||||
foreach (SubmoduleBase submodule in element.submoduleList)
|
||||
{
|
||||
submodule.SaveBM();
|
||||
if (submodule.matchedBM != null)
|
||||
elements.Add(submodule.matchedBM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ElementTreeSnapshot Capture(GameElement root)
|
||||
{
|
||||
return root == null ? null : new ElementTreeSnapshot(root);
|
||||
}
|
||||
|
||||
public GameElement Restore()
|
||||
{
|
||||
if (elements.Count == 0) return null;
|
||||
|
||||
GameElement parent = parentGuid == Guid.Empty ? null : FindExistingElement(parentGuid);
|
||||
RegisterExistingAncestorChain(parent);
|
||||
RepairRootAttachment();
|
||||
|
||||
foreach (GameElement_BM gameElementBM in elements.OfType<GameElement_BM>())
|
||||
{
|
||||
GameElement_BM.identifier[gameElementBM.elementGuid] = gameElementBM;
|
||||
}
|
||||
|
||||
foreach (BaseElement_BM elementBM in elements)
|
||||
{
|
||||
elementBM.ExecuteBM();
|
||||
}
|
||||
|
||||
EditorManager.instance.beatmapContainer.ExecuteLowPriorityActions();
|
||||
|
||||
GameElement root = GameElement_BM.GetElement(rootGuid);
|
||||
if (root == null) return null;
|
||||
|
||||
RestoreParentOrder(root, parent);
|
||||
|
||||
foreach (GameElement element in root.GetAllGameElementsFromThis())
|
||||
{
|
||||
if (element == null) continue;
|
||||
element.AfterInitialize();
|
||||
element.Refresh();
|
||||
}
|
||||
|
||||
EditorManager.instance.uiManager.hierarchy.FindTab(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
private GameElement FindExistingElement(Guid elementGuid)
|
||||
{
|
||||
GameElement element = GameElement_BM.GetElement(elementGuid);
|
||||
if (element != null) return element;
|
||||
|
||||
return EditorManager.instance.beatmapContainer.gameElementList
|
||||
.FirstOrDefault(gameElement => gameElement != null && gameElement.elementGuid == elementGuid);
|
||||
}
|
||||
|
||||
private void RepairRootAttachment()
|
||||
{
|
||||
if (parentGuid == Guid.Empty) return;
|
||||
|
||||
GameElement_BM rootBM = elements
|
||||
.OfType<GameElement_BM>()
|
||||
.FirstOrDefault(gameElementBM => gameElementBM.elementGuid == rootGuid);
|
||||
|
||||
if (rootBM != null)
|
||||
{
|
||||
rootBM.attachedElementGuid = parentGuid;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterExistingAncestorChain(GameElement parent)
|
||||
{
|
||||
if (parent == null) return;
|
||||
|
||||
List<GameElement> ancestors = new List<GameElement>();
|
||||
for (GameElement current = parent; current != null; current = current.parentElement)
|
||||
{
|
||||
ancestors.Add(current);
|
||||
}
|
||||
|
||||
ancestors.Reverse();
|
||||
foreach (GameElement ancestor in ancestors)
|
||||
{
|
||||
ancestor.SaveBM();
|
||||
if (ancestor.matchedBM is not GameElement_BM ancestorBM) continue;
|
||||
|
||||
GameElement_BM.identifier[ancestor.elementGuid] = ancestorBM;
|
||||
ancestorBM.matchedElement = ancestor;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreParentOrder(GameElement root, GameElement parent)
|
||||
{
|
||||
if (parent == null || parentChildIndex < 0) return;
|
||||
|
||||
parent.childElementList.Remove(root);
|
||||
int insertIndex = Mathf.Clamp(parentChildIndex, 0, parent.childElementList.Count);
|
||||
parent.childElementList.Insert(insertIndex, root);
|
||||
root.transform.SetSiblingIndex(insertIndex);
|
||||
parent.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DeleteElementCommand : ICommand
|
||||
{
|
||||
private readonly ElementTreeSnapshot snapshot;
|
||||
private GameElement target;
|
||||
|
||||
public DeleteElementCommand(GameElement target)
|
||||
{
|
||||
this.target = target;
|
||||
snapshot = ElementTreeSnapshot.Capture(target);
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
DeleteCurrentTarget();
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
target = snapshot?.Restore();
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
DeleteCurrentTarget();
|
||||
}
|
||||
|
||||
public bool TryMerge(ICommand other) => false;
|
||||
|
||||
private void DeleteCurrentTarget()
|
||||
{
|
||||
if (target == null) return;
|
||||
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(target);
|
||||
target = null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PasteElementCommand : ICommand
|
||||
{
|
||||
private readonly CopyPasteDeleteModule module;
|
||||
private readonly GameElement source;
|
||||
private readonly GameElement parent;
|
||||
private ElementTreeSnapshot snapshot;
|
||||
private GameElement pastedRoot;
|
||||
|
||||
public PasteElementCommand(CopyPasteDeleteModule module, GameElement source, GameElement parent)
|
||||
{
|
||||
this.module = module;
|
||||
this.source = source;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
pastedRoot = module.PasteElementRaw(source, parent);
|
||||
snapshot ??= ElementTreeSnapshot.Capture(pastedRoot);
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
if (pastedRoot == null) return;
|
||||
module.DeleteElementRaw(pastedRoot, false);
|
||||
pastedRoot = null;
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
pastedRoot = snapshot?.Restore();
|
||||
}
|
||||
|
||||
public bool TryMerge(ICommand other) => false;
|
||||
}
|
||||
|
||||
public sealed class CreateElementCommand : ICommand
|
||||
{
|
||||
private readonly Func<GameElement> createAction;
|
||||
private ElementTreeSnapshot snapshot;
|
||||
|
||||
public GameElement CreatedElement { get; private set; }
|
||||
|
||||
public CreateElementCommand(Func<GameElement> createAction)
|
||||
{
|
||||
this.createAction = createAction;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
CreatedElement = createAction?.Invoke();
|
||||
snapshot ??= ElementTreeSnapshot.Capture(CreatedElement);
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
if (CreatedElement == null) return;
|
||||
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(CreatedElement, false);
|
||||
CreatedElement = null;
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
CreatedElement = snapshot?.Restore();
|
||||
}
|
||||
|
||||
public bool TryMerge(ICommand other) => false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96273157236b1a54fbf492d88d4c540f
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using Ichni.RhythmGame;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse;
|
||||
using Sirenix.Utilities;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -737,9 +738,10 @@ namespace Ichni.Editor
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void NoteScale(float scale)
|
||||
public static void NoteScale(float scale, bool All = false)
|
||||
{
|
||||
var noteBases = inspector.connectedGameElement.GetAllGameElementsFromThis().OfType<NoteBase>().ToList();
|
||||
var noteBases = (All ? EditorManager.instance.beatmapContainer.gameElementList :
|
||||
inspector.connectedGameElement.GetAllGameElementsFromThis()).OfType<NoteBase>().ToList();
|
||||
foreach (var note in noteBases)
|
||||
{
|
||||
if (note.noteVisual != null)
|
||||
@@ -962,7 +964,14 @@ namespace Ichni.Editor
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static void CloseAllTrackPreviewLine()
|
||||
{
|
||||
EditorManager.instance.beatmapContainer.gameElementList.OfType<Track>().ForEach(i =>
|
||||
{
|
||||
i.trackPathSubmodule.isShowingDisplay = false;
|
||||
i.Refresh();
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加 Tag Matcher 到 TagManager
|
||||
/// </summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
@@ -784,29 +785,32 @@ namespace Ichni.Editor
|
||||
|
||||
builder
|
||||
.Button("Displacement", () =>
|
||||
Displacement.GenerateElement("New Displacement", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => Displacement.GenerateElement("New Displacement", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true)))
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true))))
|
||||
.Button("Swirl", () =>
|
||||
Swirl.GenerateElement("New Swirl", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => Swirl.GenerateElement("New Swirl", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true)))
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true))))
|
||||
.Button("Scale", () =>
|
||||
Scale.GenerateElement("New Scale", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => Scale.GenerateElement("New Scale", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true)))
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true))))
|
||||
.Button("Look At", () =>
|
||||
LookAt.GenerateElement("New Look At", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, new FlexibleBool()))
|
||||
CommandManager.ExecuteCreate(() => LookAt.GenerateElement("New Look At", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, new FlexibleBool())))
|
||||
.Button("Position Sync", () =>
|
||||
CommandManager.ExecuteCreate(() => PositionSync.GenerateElement("New Position Sync", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, new FlexibleBool())))
|
||||
.Button("Displacement Tracker", () =>
|
||||
DisplacementTracker.GenerateElement("New Displacement Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f))
|
||||
CommandManager.ExecuteCreate(() => DisplacementTracker.GenerateElement("New Displacement Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f)))
|
||||
.Button("Swirl Tracker", () =>
|
||||
SwirlTracker.GenerateElement("New Swirl Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f))
|
||||
CommandManager.ExecuteCreate(() => SwirlTracker.GenerateElement("New Swirl Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f)))
|
||||
.Button("Scale Tracker", () =>
|
||||
ScaleTracker.GenerateElement("New Scale Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f));
|
||||
CommandManager.ExecuteCreate(() => ScaleTracker.GenerateElement("New Scale Tracker", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, 0f)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
@@ -34,17 +34,19 @@ namespace Ichni.Editor
|
||||
if (minInputField != null) minInputField.interactable = true;
|
||||
if (maxInputField != null) maxInputField.interactable = true;
|
||||
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.wholeNumbers = wholeNumbers;
|
||||
|
||||
// [对象池安全] 精准解绑业务代理
|
||||
// [对象池安全] 精准解绑业务代理 — 必须在修改 slider range 之前完成,
|
||||
// 否则 Unity Slider 在 maxValue < 旧 value 时会自动触发 onValueChanged,
|
||||
// 导致旧监听器用旧的 parameterName 将错误值写回对象。
|
||||
slider.onValueChanged.RemoveListener(OnSliderValueChanged);
|
||||
valueInputField?.onEndEdit.RemoveListener(OnValueInputEndEdit);
|
||||
minInputField?.onEndEdit.RemoveListener(OnMinInputEndEdit);
|
||||
maxInputField?.onEndEdit.RemoveListener(OnMaxInputEndEdit);
|
||||
customAction = null;
|
||||
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.wholeNumbers = wholeNumbers;
|
||||
|
||||
base.Initialize(baseElement, title, parameterName);
|
||||
|
||||
if (parameterName != string.Empty)
|
||||
@@ -58,13 +60,13 @@ namespace Ichni.Editor
|
||||
{
|
||||
Debug.LogWarning($"[DynamicUI] 尝试绑定 {title} ({parameterName}) 失败,由于其值或路径无效。");
|
||||
}
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
}
|
||||
else
|
||||
{
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
}
|
||||
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
valueInputField?.onEndEdit.AddListener(OnValueInputEndEdit);
|
||||
minInputField?.onEndEdit.AddListener(OnMinInputEndEdit);
|
||||
maxInputField?.onEndEdit.AddListener(OnMaxInputEndEdit);
|
||||
|
||||
SyncAllInputFields();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DG.Tweening;
|
||||
using Lean.Pool;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
@@ -24,7 +25,8 @@ namespace Ichni.Editor
|
||||
tabList = new List<HierarchyTab>();
|
||||
addFolderButton.onClick.AddListener(() =>
|
||||
{
|
||||
ElementFolder.GenerateElement("New Folder", Guid.NewGuid(), new List<string>(), true, null);
|
||||
CommandManager.ExecuteCreate(() =>
|
||||
ElementFolder.GenerateElement("New Folder", Guid.NewGuid(), new List<string>(), true, null));
|
||||
});
|
||||
expandButtom.onClick.AddListener(Expand);
|
||||
rectTransform = this.GetComponent<RectTransform>();
|
||||
@@ -201,4 +203,4 @@ namespace Ichni.Editor
|
||||
upLoadElement = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ namespace Ichni.Editor
|
||||
targetElement.connectedTab = this;
|
||||
this.isExpanded = false;
|
||||
this.isSelected = false;
|
||||
if (BgImage != null)
|
||||
{
|
||||
BgImage.color = new Color(0.5f, 0.5f, 0.5f, 0);
|
||||
}
|
||||
this.childTabList = new List<HierarchyTab>();
|
||||
|
||||
// 清除旧的缩进线
|
||||
@@ -453,4 +457,4 @@ namespace Ichni.Editor
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Ichni.Editor
|
||||
this.parameterName = parameterName;
|
||||
unitList = new List<DynamicUICompositeUnit>();
|
||||
|
||||
InitializeWindow(titleText, ApplyParameters);
|
||||
InitializeWindow(titleText, () => ApplyParameters?.Invoke());
|
||||
|
||||
|
||||
}
|
||||
@@ -450,4 +450,4 @@ namespace Ichni.Editor
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,15 @@ namespace Ichni.Editor
|
||||
{
|
||||
public partial class ResolutionHints : MonoBehaviour
|
||||
{
|
||||
public Image phoneFrame;
|
||||
public Image iPadFrame;
|
||||
public Image safeAreaFrame;
|
||||
public GameObject judgeLineHints;
|
||||
}
|
||||
|
||||
public partial class ResolutionHints
|
||||
{
|
||||
public void SetPhoneFrame()
|
||||
public void SetJudgeLineHints()
|
||||
{
|
||||
phoneFrame.gameObject.SetActive(!phoneFrame.gameObject.activeSelf);
|
||||
}
|
||||
|
||||
public void SetIPadFrame()
|
||||
{
|
||||
iPadFrame.gameObject.SetActive(!iPadFrame.gameObject.activeSelf);
|
||||
judgeLineHints.SetActive(!judgeLineHints.gameObject.activeSelf);
|
||||
}
|
||||
|
||||
public void SetSafeAreaFrame()
|
||||
|
||||
@@ -176,13 +176,7 @@ namespace Ichni.Editor
|
||||
|
||||
private void GenerateSaveClipWindow()
|
||||
{
|
||||
GameElement currentElement = EditorManager.instance.operationManager.currentSelectedElements[0];
|
||||
|
||||
if (currentElement == null)
|
||||
{
|
||||
LogWindow.Log("No Game Element selected.", Color.red);
|
||||
return;
|
||||
}
|
||||
if (!TryGetSingleSelectedElementForClip(out GameElement currentElement)) return;
|
||||
|
||||
if (clipManagementWindow != null)
|
||||
{
|
||||
@@ -208,15 +202,7 @@ namespace Ichni.Editor
|
||||
|
||||
private void GenerateLoadClipWindow()
|
||||
{
|
||||
GameElement currentElement = EditorManager.instance.operationManager.currentSelectedElements[0];
|
||||
|
||||
if (currentElement == null)
|
||||
{
|
||||
LogWindow.Log("No Game Element selected.", Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
GameElement loadTarget = currentElement == EditorManager.instance ? null : currentElement.parentElement;
|
||||
if (!TryGetSingleSelectedElementForClip(out GameElement currentElement)) return;
|
||||
|
||||
if (clipManagementWindow != null)
|
||||
{
|
||||
@@ -240,6 +226,26 @@ namespace Ichni.Editor
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryGetSingleSelectedElementForClip(out GameElement currentElement)
|
||||
{
|
||||
currentElement = null;
|
||||
var selectedElements = EditorManager.instance.operationManager.currentSelectedElements;
|
||||
if (selectedElements == null || selectedElements.Count != 1)
|
||||
{
|
||||
LogWindow.Log("Please select only one Game Element for clip operation.", Color.red);
|
||||
return false;
|
||||
}
|
||||
|
||||
currentElement = selectedElements[0];
|
||||
if (currentElement == null)
|
||||
{
|
||||
LogWindow.Log("No Game Element selected.", Color.red);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GenerateMergeWindow()
|
||||
{
|
||||
GeneralSecondaryWindow mergeWindow = Instantiate(EditorManager.instance.basePrefabs.generalSecondaryWindow,
|
||||
@@ -312,15 +318,16 @@ namespace Ichni.Editor
|
||||
{
|
||||
if (hold.noteAudioSubmodule.generalJudgeAudioList.Contains("DefaultTap"))
|
||||
{
|
||||
hold.submoduleList.Remove(hold.noteAudioSubmodule);
|
||||
hold.noteAudioSubmodule = null;
|
||||
hold.noteAudioSubmodule = new NoteAudioSubmodule(hold,
|
||||
new List<string>() { "DefaultEndHold" },
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>() { "DefaultStartHold" });
|
||||
}
|
||||
}
|
||||
hold.submoduleList.Remove(hold.noteAudioSubmodule);
|
||||
hold.noteAudioSubmodule = null;
|
||||
hold.noteAudioSubmodule = new NoteAudioSubmodule(hold,
|
||||
new List<string>(),
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>() { "DefaultStartHold" },
|
||||
new List<string>() { "DefaultEndHold" });
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Flick flick in allFlicks)
|
||||
{
|
||||
@@ -339,4 +346,4 @@ namespace Ichni.Editor
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
615
Assets/Scripts/DynamicUI/Timeline/AudioMelSpectrogram.cs
Normal file
615
Assets/Scripts/DynamicUI/Timeline/AudioMelSpectrogram.cs
Normal file
@@ -0,0 +1,615 @@
|
||||
using System.Threading;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 非 MonoBehaviour 的 Mel 频谱图渲染器。
|
||||
///
|
||||
/// === 架构 ===
|
||||
/// PathNodeCurveDrawer.Update()
|
||||
/// └─ AudioMelSpectrogram.UpdateView(curT, maxT)
|
||||
/// ├─ 首次检测 AudioClip → 后台线程预计算全曲 FFT
|
||||
/// ├─ 每帧从缓存取出当前可见时间段 → 颜色 LUT → Color32[] → SetPixelData → Apply
|
||||
/// └─ 视角未变时整帧跳过
|
||||
///
|
||||
/// 后台线程:PCM 读取(主线程) → FFT + Mel 滤波 + Log(子线程) → 缓存 Log 能量
|
||||
/// 主线程: 按 visibleStart/End 映射到帧范围 → 逐列查 LUT → 写像素 → GPU upload
|
||||
/// </summary>
|
||||
public class AudioMelSpectrogram
|
||||
{
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [公共参数] 由 PathNodeCurveDrawer 每帧通过字段赋值同步
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>渲染目标的 RawImage。尺寸从此读取 × resolutionScale 决定纹理大小。</summary>
|
||||
public RawImage targetImage;
|
||||
|
||||
/// <summary>纹理分辨率缩放。0.5 = 半分辨率(宽/高各砍一半,像素数 1/4)。</summary>
|
||||
[Range(0.1f, 1f)] public float resolutionScale = 0.5f;
|
||||
|
||||
/// <summary>颜色渐变:归一化能量 [0,1] → Color。Inspector 修改实时生效。</summary>
|
||||
public Gradient colorGradient;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [FFT / Mel 常量] 改动需要重新预计算才会生效
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>FFT 窗口大小(采样点数)。越大频率分辨率越高、时间分辨率越低。
|
||||
/// 4096 @ 44100Hz ≈ 93ms 窗口,频率分辨率 ≈ 10.8Hz。
|
||||
/// 配合 512 帧移(87.5% overlap)维持时间分辨率 ~11.6ms。</summary>
|
||||
private const int FFT_SIZE = 4096;
|
||||
|
||||
/// <summary>帧移(相邻 FFT 窗口的起始采样偏移量)。
|
||||
/// 512 = 50% overlap(HOP_SIZE = FFT_SIZE / 2)。
|
||||
/// 每帧对应时间 = HOP_SIZE / sampleRate ≈ 11.6ms。</summary>
|
||||
private const int HOP_SIZE = 512;
|
||||
|
||||
/// <summary>Mel 滤波器组数量。也是缓存和纹理的高度方向分辨率。
|
||||
/// 128 个三角滤波器在 Mel 尺度上等距分布,覆盖 0 ~ sampleRate/2 Hz。
|
||||
/// 更高 → 频率细节更多,但低频段相邻 bin 间距更窄(~40Hz),
|
||||
/// 对钢琴相邻半音(~2Hz @ 440Hz)仍偏粗,但已是实用上限。</summary>
|
||||
private const int MEL_BINS = 128;
|
||||
|
||||
/// <summary>Mel 尺度常数:m = 2595 × log10(1 + f/700)。</summary>
|
||||
private const float MEL_A = 2595f;
|
||||
|
||||
/// <summary>Mel 尺度常数:f = 700 × (10^(m/2595) - 1)。</summary>
|
||||
private const float MEL_B = 700f;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [预计算缓存] 后台线程写入 → 主线程只读
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>当前已缓存的 AudioClip 引用。检测 clip 变化时触发重新预计算。</summary>
|
||||
private AudioClip _cachedClip;
|
||||
|
||||
/// <summary>预计算完成后的 Log-Mel 能量缓存。
|
||||
/// 布局:flat[ frameIndex × MEL_BINS + melBin ],每个元素 = Log(能量)。
|
||||
/// 存 Log 值避免每帧重复 Mathf.Log。volatile 保证主线程读到最新值。</summary>
|
||||
private volatile float[] _logMelData;
|
||||
|
||||
/// <summary>后台线程正在写入的临时缓存。完成后通过 _cacheReady 交给主线程。</summary>
|
||||
private float[] _pendingLogData;
|
||||
|
||||
/// <summary>后台线程完成标记。主线程检测到此标志后切 _pendingLogData → data。</summary>
|
||||
private volatile bool _cacheReady;
|
||||
|
||||
/// <summary>总帧数。由 (totalSamples - FFT_SIZE) / HOP_SIZE + 1 计算。
|
||||
/// 决定了 data 的第一维长度。</summary>
|
||||
private int _totalFrames;
|
||||
|
||||
/// <summary>每帧对应的时间(秒)= HOP_SIZE / sampleRate。
|
||||
/// frame f 的中心时间 = (f + 1) × _timePerFrame。</summary>
|
||||
private float _timePerFrame;
|
||||
|
||||
/// <summary>Mel 三角滤波器组。_melFilters[m][i] = 第 m 个 mel bin 在 FFT bin i 上的权重。
|
||||
/// 在预计算开始时构建,之后只读。后台线程安全(写入完成后才启动线程)。</summary>
|
||||
private float[][] _melFilters;
|
||||
|
||||
/// <summary>AudioClip 的采样率(Hz)。用于 _timePerFrame 和 Mel 滤波器组的频率轴计算。</summary>
|
||||
private int _sampleRate;
|
||||
|
||||
/// <summary>后台线程正在运行的标记。防止重复启动线程。</summary>
|
||||
private volatile bool _computing;
|
||||
|
||||
/// <summary>后台线程已计算完成的帧数。主线程用此判断可以读 _pendingLogData 的前 N 帧。
|
||||
/// 后台线程每算完一批(每 16 帧)通过 Volatile.Write 更新,
|
||||
/// 主线程通过 Volatile.Read 读取,保证写 release/读 acquire 语义。</summary>
|
||||
private volatile int _framesComputed;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [运行时纹理]
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>实际写入的 Texture2D。尺寸 = RawImage 像素尺寸 × resolutionScale。
|
||||
/// 纹理格式 RGBA32,FilterMode 可切换为 Point 避免插值模糊。</summary>
|
||||
private Texture2D _texture;
|
||||
|
||||
/// <summary>像素缓冲区(CPU 侧)。每帧通过 unsafe 指针直接写入,
|
||||
/// 然后 SetPixelData 整块提交到 GPU。SetPixelData 比 SetPixels 快因为走原始字节拷贝。</summary>
|
||||
private Color32[] _pixels32;
|
||||
|
||||
/// <summary>当前纹理尺寸。用于检测 RawImage 尺寸变化并重建纹理。</summary>
|
||||
private int _texW, _texH;
|
||||
|
||||
/// <summary>上一帧渲染的 FFT 帧范围。用于帧级抖动过滤:
|
||||
/// 当 songTime 在帧边界附近振荡时,startF/endF 不变就不重绘,
|
||||
/// 避免子帧级变化导致整张图反复左右偏移。</summary>
|
||||
private int _prevStartF = -1, _prevEndF = -1;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [颜色 LUT] 256 级预计算查找表
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>256 级颜色查找表。norm[0,1] → LUT[(int)(norm × 255)]。
|
||||
/// 避免每像素调用 Gradient.Evaluate(函数调用 + 键插值开销)。</summary>
|
||||
private Color32[] _colorLut;
|
||||
|
||||
/// <summary>上次构建 LUT 时使用的 Gradient 引用。
|
||||
/// 为 null 或与当前 colorGradient 不同时重建。
|
||||
/// 注意:Inspector 修改 Gradient 时不改变引用,所以用了直接引用 + 用户 Gradient 每次重建的策略。</summary>
|
||||
private Gradient _appliedGradient;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// [Bin 查找表] y → melBin 的预计算映射
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>纹理行 y → Mel 滤波器索引。_binForY[y] = round(y / h × MEL_BINS)。
|
||||
/// 避免每像素重复计算浮点除法和 round。</summary>
|
||||
private int[] _binForY;
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 每帧入口
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>每帧由 PathNodeCurveDrawer.Update() 调用。</summary>
|
||||
/// <param name="audioSource">从 ModuleTime 获取的 AudioSource,用于检测 AudioClip。</param>
|
||||
/// <param name="visibleStart">可见范围起点(秒),通常 = ModuleTime.SmoothTime(与 CurrentBeatF 同源)。</param>
|
||||
/// <param name="visibleEnd">可见范围终点(秒),由 PathNodeCurveDrawer 按 beatRange × 60/BPM 换算。</param>
|
||||
public void UpdateView(AudioSource audioSource, float visibleStart, float visibleEnd, bool forceUpdate = false)
|
||||
{
|
||||
if (targetImage == null) return;
|
||||
|
||||
// ── 检测新 AudioClip → 启动后台预计算 ──────────────────────────
|
||||
// _cachedClip 记录上次预计算的 clip。只要 clip 引用变了就触发重新计算。
|
||||
// _computing 防重入,后台线程运行期间不重复启动。
|
||||
if (audioSource != null && audioSource.clip != null && audioSource.clip != _cachedClip)
|
||||
{
|
||||
if (!_computing)
|
||||
StartPreCompute(audioSource.clip);
|
||||
}
|
||||
|
||||
// ── 后台线程完成 → 切缓存 ──────────────────────────────────────
|
||||
// 必须放在 _computing 检查之前:先完成缓存切换,再让 _computing check 通过。
|
||||
if (_cacheReady)
|
||||
{
|
||||
_logMelData = _pendingLogData;
|
||||
_pendingLogData = null;
|
||||
_totalFrames = _logMelData.Length / MEL_BINS;
|
||||
_cachedClip = audioSource.clip;
|
||||
_cacheReady = false;
|
||||
_computing = false;
|
||||
}
|
||||
|
||||
// ── 完全态:全缓存就绪,正常渲染 ────────────────────────────
|
||||
if (!_computing && _logMelData != null && _totalFrames > 0)
|
||||
{
|
||||
RenderCurrentView(visibleStart, visibleEnd, forceUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 部分态:后台计算中,但已有部分帧可供显示 ────────────────────
|
||||
if (_computing && _pendingLogData != null && _framesComputed > 0)
|
||||
{
|
||||
// 用 _pendingLogData 渲染已完成部分
|
||||
RenderCurrentView(visibleStart, visibleEnd, true,
|
||||
_pendingLogData, _framesComputed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 渲染当前视图(被完全态和部分态共用)
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>根据 visibleStart/End 计算帧范围,渲染到纹理并提交 GPU。</summary>
|
||||
/// <param name="data">数据源:完全态用 data,部分态用 _pendingLogData。</param>
|
||||
/// <param name="framesAvail">可用帧数:完全态 = _totalFrames,部分态 = _framesComputed。</param>
|
||||
private void RenderCurrentView(float visibleStart, float visibleEnd, bool forceUpdate,
|
||||
float[] data = null, int framesAvail = 0)
|
||||
{
|
||||
// 默认走完全态 _logMelData / _totalFrames
|
||||
if (data == null) { data = _logMelData; framesAvail = _totalFrames; }
|
||||
if (data == null || framesAvail == 0) return;
|
||||
|
||||
// ── 可见时间范围 → FFT 帧范围 ─────────────────────────────────
|
||||
// FFT 帧 f 的时间中心 = (f × HOP_SIZE + FFT_SIZE/2) / sampleRate
|
||||
// 当 FFT_SIZE=4096, HOP_SIZE=512: 帧 f 中心在 (f + 4) × _timePerFrame
|
||||
float frameOffset = FFT_SIZE / (2f * HOP_SIZE);
|
||||
// 先算帧号,后判断跳过。帧级检测能过滤 songTime 在帧边界附近的振荡,
|
||||
// 避免 startF/endF 没变时整张图反复左右偏移。
|
||||
int startF = Mathf.Max(0, Mathf.RoundToInt(visibleStart / _timePerFrame - frameOffset));
|
||||
int endF = Mathf.Max(1, Mathf.RoundToInt(visibleEnd / _timePerFrame - frameOffset));
|
||||
|
||||
// ── 帧号未变 → 跳过重绘(部分态 forceUpdate=true 不会跳过)──
|
||||
if (!forceUpdate && startF == _prevStartF && endF == _prevEndF && _pixels32 != null)
|
||||
return;
|
||||
|
||||
int visFrames = endF - startF;
|
||||
float dur = visibleEnd - visibleStart;
|
||||
if (dur <= 0f) return;
|
||||
|
||||
// ── 纹理尺寸 ──
|
||||
var rect = targetImage.rectTransform.rect;
|
||||
int w = Mathf.Max(1, Mathf.RoundToInt(rect.width * resolutionScale));
|
||||
int h = Mathf.Max(1, Mathf.RoundToInt(rect.height * resolutionScale));
|
||||
bool sizeChanged = _texture == null || _texture.width != w || _texture.height != h;
|
||||
|
||||
if (sizeChanged)
|
||||
InitTexture(w, h);
|
||||
|
||||
// ── 全量重绘 ──
|
||||
BuildColorLut();
|
||||
RenderColumns(startF, visFrames, w, h, 0, w - 1, data, framesAvail);
|
||||
_texture.SetPixelData(_pixels32, 0);
|
||||
_texture.Apply(false);
|
||||
|
||||
_prevStartF = startF;
|
||||
_prevEndF = endF;
|
||||
_texW = w;
|
||||
_texH = h;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 列渲染(热路径)
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>渲染 colFrom~colTo 列的像素。被 UpdateView 每帧调用(全量重绘)。</summary>
|
||||
/// <param name="startF">可见范围起点帧号</param>
|
||||
/// <param name="visFrames">可见范围总帧数</param>
|
||||
/// <param name="w">纹理像素宽度</param>
|
||||
/// <param name="h">纹理像素高度</param>
|
||||
/// <param name="colFrom">起始列(含)</param>
|
||||
/// <param name="colTo">结束列(含)</param>
|
||||
private void RenderColumns(int startF, int visFrames, int w, int h,
|
||||
int colFrom, int colTo,
|
||||
float[] data, int totalFrames)
|
||||
{
|
||||
// 在堆上分配每列的临时缓冲区。
|
||||
// 相比 stackalloc 有少量 GC 压力,但在 h ≤ 128、每帧最多调用一次的情况下可以忽略。
|
||||
// 用途:一趟读 data 同时写入 colVals + 扫描 min/max,
|
||||
// 第二趟从 colVals 读(避免第二趟再访存托管数组)。
|
||||
float[] colVals = new float[h];
|
||||
|
||||
for (int x = colFrom; x <= colTo; x++)
|
||||
{
|
||||
// 计算当前列对应的 FFT 帧号
|
||||
float t = (float)x / w; // 归一化列位置 [0,1]
|
||||
int frameIdx = startF + (int)math.round(t * visFrames);
|
||||
// 超出已计算帧 → 跳过该列,维持透明背景
|
||||
if (frameIdx >= totalFrames) continue;
|
||||
if (frameIdx < 0) continue;
|
||||
int rowBase = frameIdx * MEL_BINS; // 该帧在 data 中的起始偏移
|
||||
|
||||
// ── 一趟读入 stack + 扫描 min/max ──
|
||||
// 遍历 h 行,每行用 _binForY 映射到 Mel bin,
|
||||
// 从 data 取值 → 存入 colVals[y] → 同时更新 mn/mx。
|
||||
float mn = float.MaxValue, mx = float.MinValue;
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
float v = data[rowBase + _binForY[y]];
|
||||
colVals[y] = v;
|
||||
if (v < mn) mn = v;
|
||||
if (v > mx) mx = v;
|
||||
}
|
||||
// 防止全静音帧导致除零。invRange 在后续乘 norm,不额外乘除。
|
||||
float invRange = 1f / math.max(mx - mn, 0.001f);
|
||||
|
||||
// ── 从 stack 渲染 ──
|
||||
// norm[0,1] → idx[0,255] → LUT[idx] → 写入像素。
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
float norm = (colVals[y] - mn) * invRange;
|
||||
int idx = (int)(norm * 255);
|
||||
if (idx > 255) idx = 255;
|
||||
_pixels32[y * w + x] = _colorLut[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 后台预计算
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>启动后台线程预计算。
|
||||
/// 主线程:读取 PCM + 混单声道 + 构建 Mel 滤波器组。
|
||||
/// 子线程:Hann 加窗 → FFT → 功率谱 → Mel 三角滤波 → Log → 写入 _pendingLogData。</summary>
|
||||
private void StartPreCompute(AudioClip clip)
|
||||
{
|
||||
_computing = true;
|
||||
_cacheReady = false;
|
||||
_framesComputed = 0; // 重置进度计数器
|
||||
_cachedClip = clip;
|
||||
_sampleRate = clip.frequency;
|
||||
_timePerFrame = (float)HOP_SIZE / _sampleRate; // ≈ 11.6ms @44100Hz
|
||||
|
||||
// 构建 Mel 滤波器组(主线程,因为 _melFilters 不是 volatile)
|
||||
BuildMelFilterBank();
|
||||
|
||||
// ── 主线程:读取 PCM 并混为单声道 ──
|
||||
// AudioClip.GetData() 必须在主线程调用(Unity API 限制)。
|
||||
// clip.samples = 每声道的采样数,totalSamples × channels = 总 float 数。
|
||||
int totalSamples = clip.samples;
|
||||
int channels = clip.channels;
|
||||
float[] samples = new float[totalSamples * channels];
|
||||
clip.GetData(samples, 0);
|
||||
|
||||
// 多声道 → 单声道:取各声道平均值。
|
||||
float[] mono = new float[totalSamples];
|
||||
if (channels == 1)
|
||||
System.Array.Copy(samples, mono, totalSamples);
|
||||
else
|
||||
for (int i = 0; i < totalSamples; i++)
|
||||
{
|
||||
float sum = 0f;
|
||||
for (int c = 0; c < channels; c++)
|
||||
sum += samples[i * channels + c];
|
||||
mono[i] = sum / channels;
|
||||
}
|
||||
|
||||
// 总帧数 = 能容纳的 HOP_SIZE 步进数
|
||||
// 最后一帧的起始偏移 = totalSamples - FFT_SIZE,不足一帧时返回至少 1 帧。
|
||||
int totalFrames = Mathf.Max(1, (totalSamples - FFT_SIZE) / HOP_SIZE + 1);
|
||||
float[] pending = new float[totalFrames * MEL_BINS];
|
||||
_pendingLogData = pending; // 立即暴露给主线程读取,线程写内容 + 更新 _framesComputed
|
||||
|
||||
// ── 后台线程:FFT + Mel 滤波 + Log ──
|
||||
// 所有数据(mono, window, _melFilters)在线程启动前已就绪,不需要同步。
|
||||
// 只写 pending[],不碰任何 Unity API。
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
// Blackman-Harris(4 项最小旁瓣窗):旁瓣 ≤ -92dB,远优于 Hann 的 -32dB。
|
||||
// 代价:主瓣宽度比 Hann 宽约 60%,但 FFT_SIZE=4096 下仍只有 ~22Hz,
|
||||
// 远窄于 Mel 滤波器间距,不影响频率分辨率。
|
||||
float[] window = new float[FFT_SIZE];
|
||||
float bhA0 = 0.35875f, bhA1 = 0.48829f, bhA2 = 0.14128f, bhA3 = 0.01168f;
|
||||
float invN1 = 1f / (FFT_SIZE - 1);
|
||||
for (int i = 0; i < FFT_SIZE; i++)
|
||||
{
|
||||
float p = Mathf.PI * 2f * i * invN1;
|
||||
window[i] = bhA0 - bhA1 * Mathf.Cos(p) + bhA2 * Mathf.Cos(2f * p) - bhA3 * Mathf.Cos(3f * p);
|
||||
}
|
||||
|
||||
float[] real = new float[FFT_SIZE];
|
||||
float[] imag = new float[FFT_SIZE];
|
||||
float[] power = new float[FFT_SIZE / 2];
|
||||
|
||||
for (int f = 0; f < totalFrames; f++)
|
||||
{
|
||||
int offset = f * HOP_SIZE;
|
||||
|
||||
// 加窗:mono[offset + i] × window[i] → real[i]
|
||||
for (int i = 0; i < FFT_SIZE; i++)
|
||||
{
|
||||
int idx = offset + i;
|
||||
real[i] = idx < totalSamples ? mono[idx] * window[i] : 0f;
|
||||
imag[i] = 0f;
|
||||
}
|
||||
|
||||
// 原位 Cooley-Tukey radix-2 FFT → real/imag 为复频谱
|
||||
FFT(real, imag);
|
||||
|
||||
// 功率谱 |X|² = real² + imag²(只取前 N/2 个正频率 bin)
|
||||
for (int i = 0; i < FFT_SIZE / 2; i++)
|
||||
power[i] = real[i] * real[i] + imag[i] * imag[i];
|
||||
|
||||
// Mel 三角滤波:每个 Mel bin 是若干 FFT bin 的加权和
|
||||
int row = f * MEL_BINS;
|
||||
for (int m = 0; m < MEL_BINS; m++)
|
||||
{
|
||||
float e = 0f;
|
||||
var filter = _melFilters[m];
|
||||
for (int i = 0; i < FFT_SIZE / 2; i++)
|
||||
e += power[i] * filter[i];
|
||||
// 存 Log(energy) 而不是 raw energy!
|
||||
// 人耳对声压的感知是对数的,且 Log 压缩后帧循环不再需要 Mathf.Log。
|
||||
pending[row + m] = Mathf.Log(Mathf.Max(e, 1e-10f));
|
||||
}
|
||||
|
||||
// 每算完 16 帧(≈ 186ms @11.6ms/帧),更新进度计数器。
|
||||
// Volatile.Write 保证对主线程可见,同时隐含 release 语义,
|
||||
// 确保写 pending[] 的 stores 在写 _framesComputed 之前完成。
|
||||
if ((f & 15) == 0 || f == totalFrames - 1)
|
||||
_framesComputed = f + 1; // volatile 写入,主线程立即可见
|
||||
}
|
||||
|
||||
_cacheReady = true; // 通知主线程
|
||||
});
|
||||
thread.IsBackground = true; // 应用程序退出时自动终止
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Mel 三角滤波器组
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>构建 Mel 三角滤波器组。
|
||||
/// 在 Mel 尺度上均匀放置 MEL_BINS 个三角滤波器,每个覆盖 3 个 Mel 点(左/中心/右)。
|
||||
/// 频率范围:0 ~ sampleRate/2 Hz。
|
||||
/// 三角峰值为 1(归一化),相邻滤波器间交叠 ≈ 50%。</summary>
|
||||
private void BuildMelFilterBank()
|
||||
{
|
||||
int half = FFT_SIZE / 2;
|
||||
float freqMax = _sampleRate / 2f; // Nyquist 频率
|
||||
float melMin = FreqToMel(0f);
|
||||
float melMax = FreqToMel(freqMax);
|
||||
|
||||
// 每个 FFT bin 的频率(Hz)
|
||||
float[] binFreq = new float[half];
|
||||
for (int i = 0; i < half; i++)
|
||||
binFreq[i] = (float)i / FFT_SIZE * _sampleRate;
|
||||
|
||||
_melFilters = new float[MEL_BINS][];
|
||||
for (int m = 0; m < MEL_BINS; m++)
|
||||
{
|
||||
// 在 Mel 尺度上等距取三个点:左 / 中心 / 右
|
||||
float mL = melMin + (melMax - melMin) * (m) / (MEL_BINS + 1);
|
||||
float mC = melMin + (melMax - melMin) * (m + 1) / (MEL_BINS + 1);
|
||||
float mR = melMin + (melMax - melMin) * (m + 2) / (MEL_BINS + 1);
|
||||
float fL = MelToFreq(mL), fC = MelToFreq(mC), fR = MelToFreq(mR);
|
||||
|
||||
// 三角滤波器:左→中心上升段,中心→右下降段,其余为 0
|
||||
var filter = new float[half];
|
||||
for (int i = 0; i < half; i++)
|
||||
{
|
||||
float f = binFreq[i];
|
||||
if (f >= fL && f < fC)
|
||||
filter[i] = (f - fL) / (fC - fL); // 上升
|
||||
else if (f >= fC && f < fR)
|
||||
filter[i] = (fR - f) / (fR - fC); // 下降
|
||||
// else: 0
|
||||
}
|
||||
_melFilters[m] = filter;
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// FFT — 实序列原位 Cooley-Tukey radix-2 DIT(时域抽取)
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>一维实序列 FFT(正变换)。
|
||||
/// 输入:real[i] = 实部(时域信号),imag[i] = 0。
|
||||
/// 输出:real[i] + j×imag[i] = 复频谱(0 ~ sampleRate,对称)。
|
||||
/// 只需要前 N/2 个 bin(0 ~ Nyquist)做功率谱计算。
|
||||
///
|
||||
/// radix-2 = 输入长度必须为 2 的幂(FFT_SIZE = 1024 ✅)。
|
||||
/// DIT = 先 bit-reversal 重排,再逐级蝶形运算。</summary>
|
||||
private static void FFT(float[] real, float[] imag)
|
||||
{
|
||||
int n = real.Length;
|
||||
|
||||
// 1. Bit-reversal 重排:使蝶形运算的输入自然有序
|
||||
for (int i = 1, j = 0; i < n; i++)
|
||||
{
|
||||
int bit = n >> 1;
|
||||
for (; j >= bit; bit >>= 1) j -= bit;
|
||||
j += bit;
|
||||
if (i < j)
|
||||
{
|
||||
(real[i], real[j]) = (real[j], real[i]);
|
||||
(imag[i], imag[j]) = (imag[j], imag[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 蝶形运算:逐级合并,len = 2 → 4 → 8 → ... → n
|
||||
for (int len = 2; len <= n; len <<= 1)
|
||||
{
|
||||
float ang = 2f * Mathf.PI / len; // 旋转因子角度
|
||||
float wlenR = Mathf.Cos(ang);
|
||||
float wlenI = -Mathf.Sin(ang); // 负号 = 正变换
|
||||
|
||||
for (int i = 0; i < n; i += len)
|
||||
{
|
||||
float wr = 1f, wi = 0f; // 初始旋转因子 = 1
|
||||
int half = len >> 1;
|
||||
for (int j = 0; j < half; j++)
|
||||
{
|
||||
int i1 = i + j; // 偶序号
|
||||
int i2 = i + j + half; // 奇序号
|
||||
// t = W × X[i2]
|
||||
float tr = wr * real[i2] - wi * imag[i2];
|
||||
float ti = wr * imag[i2] + wi * real[i2];
|
||||
// 蝶形:X[i1] = X[i1] + t, X[i2] = X[i1] - t
|
||||
real[i2] = real[i1] - tr;
|
||||
imag[i2] = imag[i1] - ti;
|
||||
real[i1] += tr;
|
||||
imag[i1] += ti;
|
||||
// 更新旋转因子
|
||||
(wr, wi) = (wr * wlenR - wi * wlenI, wr * wlenI + wi * wlenR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Mel 尺度转换
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>频率(Hz)→ Mel 值。m = 2595 × log10(1 + f/700)。</summary>
|
||||
private static float FreqToMel(float f) => MEL_A * Mathf.Log10(1f + f / MEL_B);
|
||||
|
||||
/// <summary>Mel 值 → 频率(Hz)。f = 700 × (10^(m/2595) - 1)。</summary>
|
||||
private static float MelToFreq(float m) => MEL_B * (Mathf.Pow(10f, m / MEL_A) - 1f);
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 纹理 / LUT / Bin 表
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>创建或重建纹理与辅助表。</summary>
|
||||
private void InitTexture(int w, int h)
|
||||
{
|
||||
// 纹理格式 RGBA32 = 每像素 4 字节(R/G/B/A × byte),与 Color32 结构一致。
|
||||
// mipChain: false = 不生成 mipmap,节省 GPU 内存和 upload 带宽。
|
||||
_texture = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||||
_texture.filterMode = FilterMode.Point; // 无插值,每格锐利显示频率带
|
||||
_texture.wrapMode = TextureWrapMode.Clamp; // 边缘不循环
|
||||
|
||||
_pixels32 = new Color32[w * h];
|
||||
var bg = (Color32)Color.clear; // 全透明背景
|
||||
for (int i = 0; i < _pixels32.Length; i++)
|
||||
_pixels32[i] = bg;
|
||||
_texture.SetPixelData(_pixels32, 0);
|
||||
_texture.Apply(false);
|
||||
|
||||
if (targetImage != null)
|
||||
targetImage.texture = _texture;
|
||||
|
||||
// 预计算 y → bin 索引:每行 y 映射到最近的 Mel 滤波器
|
||||
_binForY = new int[h];
|
||||
for (int y = 0; y < h; y++)
|
||||
_binForY[y] = Mathf.Clamp(
|
||||
Mathf.RoundToInt((float)y / h * MEL_BINS),
|
||||
0, MEL_BINS - 1);
|
||||
|
||||
_appliedGradient = null; // 强制下次重建 LUT
|
||||
}
|
||||
|
||||
/// <summary>构建 256 级颜色 LUT。
|
||||
/// 用户设置了 Gradient → 每次重建(Inspector 修改不改变引用)。
|
||||
/// 使用默认 Gradient → 缓存到静态引用。</summary>
|
||||
private void BuildColorLut()
|
||||
{
|
||||
var grad = colorGradient != null ? colorGradient : GetDefaultGradient();
|
||||
|
||||
// 只在「使用默认 Gradient 且已缓存」时跳过重建
|
||||
if (colorGradient == null && _appliedGradient == grad && _colorLut != null)
|
||||
return;
|
||||
|
||||
_colorLut = new Color32[256];
|
||||
for (int i = 0; i < 256; i++)
|
||||
_colorLut[i] = (Color32)grad.Evaluate(i / 255f);
|
||||
_appliedGradient = grad;
|
||||
}
|
||||
|
||||
/// <summary>默认半透渐变:低能透明 → 深紫 → 青蓝 → 青绿 → 金黄 → 白。
|
||||
/// 透明度逐渐升高,叠在曲线下方也不会遮挡。</summary>
|
||||
private static Gradient _defaultGradient;
|
||||
|
||||
private static Gradient GetDefaultGradient()
|
||||
{
|
||||
if (_defaultGradient != null) return _defaultGradient;
|
||||
|
||||
_defaultGradient = new Gradient();
|
||||
_defaultGradient.SetKeys(
|
||||
new GradientColorKey[]
|
||||
{
|
||||
new GradientColorKey(new Color(0.4f, 0.1f, 0.6f), 0f),
|
||||
new GradientColorKey(new Color(0f, 0.6f, 0.8f), 0.35f),
|
||||
new GradientColorKey(new Color(0.2f, 0.9f, 0.5f), 0.6f),
|
||||
new GradientColorKey(new Color(1f, 0.85f,0.3f), 0.8f),
|
||||
new GradientColorKey(Color.white, 1f),
|
||||
},
|
||||
new GradientAlphaKey[]
|
||||
{
|
||||
new GradientAlphaKey(0f, 0f),
|
||||
new GradientAlphaKey(0.5f, 0.15f),
|
||||
new GradientAlphaKey(0.75f,0.4f),
|
||||
new GradientAlphaKey(0.9f, 0.7f),
|
||||
new GradientAlphaKey(1f, 1f),
|
||||
}
|
||||
);
|
||||
return _defaultGradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 890ea908890460d418719dd90440863b
|
||||
@@ -36,7 +36,16 @@ namespace Ichni.Editor
|
||||
|
||||
private string BeatText;
|
||||
private string TimeText;
|
||||
private AudioMelSpectrogram _audioMelSpectrogram = new();
|
||||
|
||||
/// <summary>Mel 频谱图渲染目标 RawImage。请在 Inspector 中指定 Timeline 下的 RawImage 子对象。</summary>
|
||||
public RawImage spectrogramTarget;
|
||||
|
||||
/// <summary>可选的频谱图颜色渐变。为 null 时使用 AudioMelSpectrogram 内置默认渐变。</summary>
|
||||
public Gradient spectrogramGradient;
|
||||
|
||||
/// <summary>频谱图纹理分辨率缩放。0.5 = 半分辨率。</summary>
|
||||
[Range(0.1f, 1f)] public float spectrogramResolutionScale = 0.5f;
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
@@ -50,6 +59,14 @@ namespace Ichni.Editor
|
||||
UpdateTime();
|
||||
});
|
||||
|
||||
// ── 初始化 Mel 频谱图 ──
|
||||
if (_audioMelSpectrogram != null)
|
||||
{
|
||||
_audioMelSpectrogram.targetImage = spectrogramTarget;
|
||||
_audioMelSpectrogram.resolutionScale = spectrogramResolutionScale;
|
||||
if (spectrogramGradient != null)
|
||||
_audioMelSpectrogram.colorGradient = spectrogramGradient;
|
||||
}
|
||||
}
|
||||
public void Update()
|
||||
{
|
||||
@@ -65,7 +82,16 @@ namespace Ichni.Editor
|
||||
DetectPointer();
|
||||
}
|
||||
|
||||
|
||||
// ── Mel 频谱图每帧刷新 ──
|
||||
if (_audioMelSpectrogram != null && musicPlayer != null && musicPlayer.audioSource != null && timePointerModule != null)
|
||||
{
|
||||
float visibleWidth = spectrogramTarget != null
|
||||
? spectrogramTarget.rectTransform.rect.width
|
||||
: GetComponent<RectTransform>().rect.width;
|
||||
float visTimeRange = Mathf.Max(1f, visibleWidth / timePointerModule.timePointerInterval * timePerBeat);
|
||||
_audioMelSpectrogram.UpdateView(musicPlayer.audioSource,
|
||||
songTime, songTime + visTimeRange);
|
||||
}
|
||||
}
|
||||
private void DetectSetRange()
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ using UnityEngine.InputSystem;
|
||||
|
||||
namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符合C#规范
|
||||
{
|
||||
public partial class FastNoteTracker : GameElement, IBeChangeInExport
|
||||
public partial class FastNoteTracker : GameElement
|
||||
{
|
||||
public Track TrackedTrack { get => parentElement as Track; }
|
||||
private bool _isEnabled = false;
|
||||
@@ -58,8 +58,6 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
|
||||
}
|
||||
}
|
||||
private bool _showNotePreview = false;
|
||||
BaseElement_BM IBeChangeInExport.MatchingExportElement { get => null; set { } }
|
||||
|
||||
// NotePreview和TextHint的容器
|
||||
private Transform _previewRoot;
|
||||
private Transform PreviewRoot
|
||||
@@ -535,10 +533,6 @@ namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写,符
|
||||
// 后面的 Inspector 和 Export 代码逻辑暂时不需要大改,只要注意变量名 Beatdiver -> BeatDiver
|
||||
public partial class FastNoteTracker
|
||||
{
|
||||
public BaseElement_BM MatchingExportElement { get => null; set => throw new System.NotImplementedException(); }
|
||||
|
||||
public void SaveExportBM() { }
|
||||
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
base.SetUpInspector();
|
||||
|
||||
@@ -28,6 +28,7 @@ public partial class EventPoint : MonoBehaviour
|
||||
public TMP_Text ViewText;
|
||||
|
||||
public static bool Locked = false;
|
||||
private UILineRenderer _overflowLine;
|
||||
public int BeatDeviver => FatherTab.BeatDeviver;
|
||||
public void Initialize(AnimatedFloat animatedFloat)
|
||||
{
|
||||
@@ -63,10 +64,8 @@ public partial class EventPoint : MonoBehaviour
|
||||
Linerender.rectTransform.pivot = EvDrawimage.rectTransform.pivot;
|
||||
Linerender.rectTransform.anchoredPosition = EvDrawimage.rectTransform.anchoredPosition;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 创建/同步溢出红色线条
|
||||
SyncOverflowLine();
|
||||
}
|
||||
|
||||
public float value => FatherTab.scalevalue;
|
||||
@@ -234,6 +233,30 @@ public partial class EventPoint : MonoBehaviour
|
||||
animatedFloat.animationCurveType
|
||||
);
|
||||
}
|
||||
|
||||
private void SyncOverflowLine()
|
||||
{
|
||||
if (Linerender == null) return;
|
||||
|
||||
if (_overflowLine == null)
|
||||
{
|
||||
GameObject go = new GameObject("OverflowLine", typeof(RectTransform));
|
||||
go.transform.SetParent(Linerender.transform.parent, false);
|
||||
|
||||
_overflowLine = go.AddComponent<UILineRenderer>();
|
||||
_overflowLine.color = Color.red;
|
||||
_overflowLine.RelativeSize = true;
|
||||
_overflowLine.LineThickness = Linerender.LineThickness;
|
||||
_overflowLine.raycastTarget = false;
|
||||
}
|
||||
|
||||
// 每次同步位置/大小,确保跟随 EventPoint 变化
|
||||
_overflowLine.rectTransform.anchorMin = Linerender.rectTransform.anchorMin;
|
||||
_overflowLine.rectTransform.anchorMax = Linerender.rectTransform.anchorMax;
|
||||
_overflowLine.rectTransform.pivot = Linerender.rectTransform.pivot;
|
||||
_overflowLine.rectTransform.sizeDelta = Linerender.rectTransform.sizeDelta;
|
||||
_overflowLine.rectTransform.anchoredPosition = Linerender.rectTransform.anchoredPosition;
|
||||
}
|
||||
}
|
||||
public partial class EventPoint//显示?
|
||||
{
|
||||
@@ -244,19 +267,66 @@ public partial class EventPoint//显示?
|
||||
int width = Mathf.Max(2, (int)Linerender.rectTransform.sizeDelta.x / 5);
|
||||
int height = Mathf.Max(1, (int)Linerender.rectTransform.sizeDelta.y / 5);
|
||||
|
||||
// 使用 UILineRenderer 直接绘制曲线(替代原来的纹理生成)
|
||||
Linerender.RelativeSize = true;
|
||||
Linerender.color = Color.green;
|
||||
Vector2[] points = new Vector2[width];
|
||||
// 计算原始 y 值(未裁剪)
|
||||
float[] rawY = new float[width];
|
||||
for (int i = 0; i < width; i++)
|
||||
{
|
||||
float t = (float)i / (width - 1);
|
||||
float curveVal = animatedFloat.startValue * value + ((animatedFloat.endValue - animatedFloat.startValue)
|
||||
* AnimationCurveEvaluator.Evaluate(animatedFloat.animationCurveType, t) * value);
|
||||
float yNorm = 0.5f + curveVal / height;
|
||||
points[i] = new Vector2(t, yNorm);
|
||||
rawY[i] = 0.5f + curveVal / height;
|
||||
}
|
||||
|
||||
// 分割为绿色连续段(框内)和红色连续段(框外Y循环)
|
||||
var greenSegs = new List<Vector2[]>();
|
||||
var redSegs = new List<Vector2[]>();
|
||||
var curGreen = new List<Vector2>();
|
||||
var curRed = new List<Vector2>();
|
||||
|
||||
for (int i = 0; i < width; i++)
|
||||
{
|
||||
float t = (float)i / (width - 1);
|
||||
float y = rawY[i];
|
||||
bool inBounds = y >= 0f && y <= 1f;
|
||||
|
||||
if (inBounds)
|
||||
{
|
||||
if (curRed.Count > 0)
|
||||
{
|
||||
if (curRed.Count >= 2) redSegs.Add(curRed.ToArray());
|
||||
curRed.Clear();
|
||||
}
|
||||
curGreen.Add(new Vector2(t, y));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (curGreen.Count > 0)
|
||||
{
|
||||
if (curGreen.Count >= 2) greenSegs.Add(curGreen.ToArray());
|
||||
curGreen.Clear();
|
||||
}
|
||||
// Y 循环回框内
|
||||
float wrappedY = y;
|
||||
while (wrappedY < 0f) wrappedY += 1f;
|
||||
while (wrappedY > 1f) wrappedY -= 1f;
|
||||
curRed.Add(new Vector2(t, wrappedY));
|
||||
}
|
||||
}
|
||||
|
||||
if (curGreen.Count >= 2) greenSegs.Add(curGreen.ToArray());
|
||||
if (curRed.Count >= 2) redSegs.Add(curRed.ToArray());
|
||||
|
||||
// 设置绿色主线和红色溢出线
|
||||
Linerender.RelativeSize = true;
|
||||
Linerender.color = Color.green;
|
||||
Linerender.Segments = greenSegs.Count > 0 ? greenSegs : null;
|
||||
|
||||
if (_overflowLine != null)
|
||||
{
|
||||
_overflowLine.RelativeSize = true;
|
||||
_overflowLine.color = Color.red;
|
||||
_overflowLine.Segments = redSegs.Count > 0 ? redSegs : null;
|
||||
}
|
||||
Linerender.Points = points;
|
||||
|
||||
// 其余的非纹理相关代码保持不变
|
||||
if (NextEventPoint != null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
@@ -35,5 +36,15 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
(matchedElement as DisplacementTracker).targetDisplacement = GetElement(targetDisplacementGuid) as ICanBeTrackedDisplacement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不应通过子模块路径到达此处——Tracker_BM 由 IExportOverride 在父级处理。
|
||||
/// 如被执行说明数据流异常,报错以便排查。
|
||||
/// </summary>
|
||||
public void OnExport(BaseElement_BM parentBM)
|
||||
{
|
||||
Debug.LogError($"DisplacementTracker_BM 不应出现在导出子模块中!" +
|
||||
$"应通过 IExportOverride.GetExportElement() 在父级处理。element: {elementName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
@@ -35,5 +36,15 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
(matchedElement as ScaleTracker).targetScale = GetElement(targetScaleGuid) as ICanBeTrackedScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不应通过子模块路径到达此处——Tracker_BM 由 IExportOverride 在父级处理。
|
||||
/// 如被执行说明数据流异常,报错以便排查。
|
||||
/// </summary>
|
||||
public void OnExport(BaseElement_BM parentBM)
|
||||
{
|
||||
Debug.LogError($"ScaleTracker_BM 不应出现在导出子模块中!" +
|
||||
$"应通过 IExportOverride.GetExportElement() 在父级处理。element: {elementName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
@@ -35,5 +36,15 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
(matchedElement as SwirlTracker).targetSwirl = GetElement(targetSwirlGuid) as ICanBeTrackedSwirl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不应通过子模块路径到达此处——Tracker_BM 由 IExportOverride 在父级处理。
|
||||
/// 如被执行说明数据流异常,报错以便排查。
|
||||
/// </summary>
|
||||
public void OnExport(BaseElement_BM parentBM)
|
||||
{
|
||||
Debug.LogError($"SwirlTracker_BM 不应出现在导出子模块中!" +
|
||||
$"应通过 IExportOverride.GetExportElement() 在父级处理。element: {elementName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,64 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
return Vector3Interferometer.GenerateElement(elementName, Guid.NewGuid(), tags, false,
|
||||
parent as AnimationBase, InterferomType, InterferomValueVector3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出预处理:将自己的干涉仪效果烘焙到父元素的动画数据中。
|
||||
/// </summary>
|
||||
public void OnExport(BaseElement_BM parentBM)
|
||||
{
|
||||
// 将干涉仪值按分量应用到父元素的 FlexibleFloat_BM 数据上
|
||||
if (parentBM is Swirl_BM swirl)
|
||||
{
|
||||
ApplyToFlexibleFloat(swirl.eulerAngleX, 0);
|
||||
ApplyToFlexibleFloat(swirl.eulerAngleY, 1);
|
||||
ApplyToFlexibleFloat(swirl.eulerAngleZ, 2);
|
||||
}
|
||||
else if (parentBM is Scale_BM scale)
|
||||
{
|
||||
ApplyToFlexibleFloat(scale.scaleX, 0);
|
||||
ApplyToFlexibleFloat(scale.scaleY, 1);
|
||||
ApplyToFlexibleFloat(scale.scaleZ, 2);
|
||||
}
|
||||
else if (parentBM is Displacement_BM disp)
|
||||
{
|
||||
ApplyToFlexibleFloat(disp.positionX, 0);
|
||||
ApplyToFlexibleFloat(disp.positionY, 1);
|
||||
ApplyToFlexibleFloat(disp.positionZ, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToFlexibleFloat(FlexibleFloat_BM flex, int axis)
|
||||
{
|
||||
if (flex == null) return;
|
||||
float value = InterferomValueVector3[axis];
|
||||
foreach (var anim in flex.animatedFloatList)
|
||||
{
|
||||
switch (InterferomType)
|
||||
{
|
||||
case InterferomType.Additive:
|
||||
anim.startValue += value;
|
||||
anim.endValue += value;
|
||||
break;
|
||||
case InterferomType.Multiplicative:
|
||||
anim.startValue *= value;
|
||||
anim.endValue *= value;
|
||||
break;
|
||||
case InterferomType.Override:
|
||||
anim.startValue = value;
|
||||
anim.endValue = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICanNotInExport { }
|
||||
public interface ICanNotInExport
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出时预处理:将自身效果烘焙到 parentBM 上,或执行其他必要变换。
|
||||
/// 调用后该元素将被从导出结果中移除。
|
||||
/// </summary>
|
||||
void OnExport(BaseElement_BM parentBM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
public class PositionSync_BM : GameElement_BM
|
||||
{
|
||||
public FlexibleBool_BM enabling;
|
||||
public Guid syncTargetObjectGuid;
|
||||
|
||||
public PositionSync_BM()
|
||||
{
|
||||
}
|
||||
|
||||
public PositionSync_BM(string elementName, Guid elementGuid, List<string> tags,
|
||||
GameElement_BM attachedElement, FlexibleBool_BM enabling, Guid syncTargetObjectGuid)
|
||||
: base(elementName, elementGuid, tags, attachedElement)
|
||||
{
|
||||
this.enabling = enabling;
|
||||
this.syncTargetObjectGuid = syncTargetObjectGuid;
|
||||
}
|
||||
|
||||
public override void ExecuteBM()
|
||||
{
|
||||
matchedElement = PositionSync.GenerateElement(elementName, elementGuid, tags, false,
|
||||
GetElement(attachedElementGuid), GetElement(syncTargetObjectGuid), enabling.ConvertToGameType());
|
||||
matchedElement.matchedBM = this;
|
||||
}
|
||||
|
||||
public override GameElement DuplicateBM(GameElement parent)
|
||||
{
|
||||
return PositionSync.GenerateElement(elementName, Guid.NewGuid(), tags, false, parent,
|
||||
GetElement(syncTargetObjectGuid), enabling.ConvertToGameType());
|
||||
}
|
||||
|
||||
public override void AfterExecute()
|
||||
{
|
||||
(matchedElement as PositionSync).syncTargetObject = GetElement(syncTargetObjectGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95b5b1be02575774f9c399b637cfcf12
|
||||
@@ -11,6 +11,7 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
public List<string> badAudioList;
|
||||
public List<string> missAudioList;
|
||||
public List<string> holdStartAudioList;
|
||||
public List<string> holdEndAudioList;
|
||||
|
||||
public NoteAudioSubmodule_BM()
|
||||
{
|
||||
@@ -25,20 +26,23 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
badAudioList = noteAudioSubmodule.badAudioList;
|
||||
missAudioList = noteAudioSubmodule.missAudioList;
|
||||
holdStartAudioList = noteAudioSubmodule.holdStartAudioList;
|
||||
holdEndAudioList = noteAudioSubmodule.holdEndAudioList;
|
||||
}
|
||||
|
||||
public override void ExecuteBM()
|
||||
{
|
||||
attachedElement = GameElement_BM.GetElement(attachedElementGuid);
|
||||
(attachedElement as NoteBase).noteAudioSubmodule = new NoteAudioSubmodule((attachedElement as NoteBase),
|
||||
generalJudgeAudioList, perfectAudioList, goodAudioList, badAudioList, missAudioList, holdStartAudioList);
|
||||
generalJudgeAudioList, perfectAudioList, goodAudioList, badAudioList, missAudioList,
|
||||
holdStartAudioList, holdEndAudioList);
|
||||
}
|
||||
|
||||
#region [Editor 独有接口] Editor Interfaces
|
||||
public override void DuplicateBM(GameElement attached)
|
||||
{
|
||||
(attached as NoteBase).noteAudioSubmodule = new NoteAudioSubmodule((attached as NoteBase),
|
||||
generalJudgeAudioList, perfectAudioList, goodAudioList, badAudioList, missAudioList, holdStartAudioList);
|
||||
generalJudgeAudioList, perfectAudioList, goodAudioList, badAudioList, missAudioList,
|
||||
holdStartAudioList, holdEndAudioList);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
8
Assets/Scripts/Game/DataCore/DataTypes/ES3Types.meta
Normal file
8
Assets/Scripts/Game/DataCore/DataTypes/ES3Types.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8085560939c7d8943af1b052b47fe39b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using ES3Types;
|
||||
|
||||
namespace ES3Types
|
||||
{
|
||||
/// <summary>
|
||||
/// 兼容旧存档:FlexibleBool_BM 在旧数据中存为 "animations" 字段名(Runtime 遗留),
|
||||
/// 新数据使用 "animatedBoolList"。Read 过程自动识别两种格式。
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.Preserve]
|
||||
public class ES3Type_FlexibleBool_BM : ES3ObjectType
|
||||
{
|
||||
public ES3Type_FlexibleBool_BM() : base(typeof(Ichni.RhythmGame.Beatmap.FlexibleBool_BM))
|
||||
{
|
||||
priority = 1;
|
||||
}
|
||||
|
||||
protected override void WriteObject(object obj, ES3Writer writer)
|
||||
{
|
||||
var instance = (Ichni.RhythmGame.Beatmap.FlexibleBool_BM)obj;
|
||||
writer.WriteProperty("animatedBoolList", instance.animatedBoolList);
|
||||
}
|
||||
|
||||
protected override object ReadObject<T>(ES3Reader reader)
|
||||
{
|
||||
var instance = new Ichni.RhythmGame.Beatmap.FlexibleBool_BM();
|
||||
foreach (string propertyName in reader.Properties)
|
||||
{
|
||||
if (propertyName == "animatedBoolList" || propertyName == "animations")
|
||||
{
|
||||
instance.animatedBoolList = reader.Read<List<Ichni.RhythmGame.AnimatedBool>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6f9c73884bda4242b47ff6c46ddcccc
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using ES3Types;
|
||||
|
||||
namespace ES3Types
|
||||
{
|
||||
/// <summary>
|
||||
/// 兼容旧存档:FlexibleFloat_BM 在旧数据中存为 "animations" 字段名(Runtime 遗留),
|
||||
/// 新数据使用 "animatedFloatList"。Read 过程自动识别两种格式。
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.Preserve]
|
||||
public class ES3Type_FlexibleFloat_BM : ES3ObjectType
|
||||
{
|
||||
public ES3Type_FlexibleFloat_BM() : base(typeof(Ichni.RhythmGame.Beatmap.FlexibleFloat_BM))
|
||||
{
|
||||
priority = 1;
|
||||
}
|
||||
|
||||
protected override void WriteObject(object obj, ES3Writer writer)
|
||||
{
|
||||
var instance = (Ichni.RhythmGame.Beatmap.FlexibleFloat_BM)obj;
|
||||
writer.WriteProperty("animatedFloatList", instance.animatedFloatList);
|
||||
}
|
||||
|
||||
protected override object ReadObject<T>(ES3Reader reader)
|
||||
{
|
||||
var instance = new Ichni.RhythmGame.Beatmap.FlexibleFloat_BM();
|
||||
foreach (string propertyName in reader.Properties)
|
||||
{
|
||||
if (propertyName == "animatedFloatList" || propertyName == "animations")
|
||||
{
|
||||
instance.animatedFloatList = reader.Read<List<Ichni.RhythmGame.AnimatedFloat>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af78df6a5dc19dd44acc3e39caf5d57a
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using ES3Types;
|
||||
|
||||
namespace ES3Types
|
||||
{
|
||||
/// <summary>
|
||||
/// 兼容旧存档:FlexibleInt_BM 在旧数据中存为 "animations" 字段名(Runtime 遗留),
|
||||
/// 新数据使用 "animatedIntList"。Read 过程自动识别两种格式。
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.Preserve]
|
||||
public class ES3Type_FlexibleInt_BM : ES3ObjectType
|
||||
{
|
||||
public ES3Type_FlexibleInt_BM() : base(typeof(Ichni.RhythmGame.Beatmap.FlexibleInt_BM))
|
||||
{
|
||||
priority = 1; // 优先级高于反射自动生成的 ES3ReflectedObjectType
|
||||
}
|
||||
|
||||
protected override void WriteObject(object obj, ES3Writer writer)
|
||||
{
|
||||
var instance = (Ichni.RhythmGame.Beatmap.FlexibleInt_BM)obj;
|
||||
writer.WriteProperty("animatedIntList", instance.animatedIntList);
|
||||
}
|
||||
|
||||
protected override object ReadObject<T>(ES3Reader reader)
|
||||
{
|
||||
var instance = new Ichni.RhythmGame.Beatmap.FlexibleInt_BM();
|
||||
foreach (string propertyName in reader.Properties)
|
||||
{
|
||||
if (propertyName == "animatedIntList" || propertyName == "animations")
|
||||
{
|
||||
instance.animatedIntList = reader.Read<List<Ichni.RhythmGame.AnimatedInt>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e28d98af48261e4a8b77a74c63ce72c
|
||||
@@ -6,8 +6,8 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
public class PixelateEffect_BM : EffectBase_BM
|
||||
{
|
||||
[System.Obsolete] public float duration;
|
||||
public float bottomX;
|
||||
public float bottomY;
|
||||
[Range(0f, 1f)] public float bottomX;
|
||||
[Range(0f, 1f)] public float bottomY;
|
||||
public AnimationCurve intensityCurve;
|
||||
|
||||
public PixelateEffect_BM()
|
||||
@@ -19,8 +19,8 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
this.effectTime = duration;
|
||||
this.duration = duration;
|
||||
this.bottomX = bottomX;
|
||||
this.bottomY = bottomY;
|
||||
this.bottomX = Mathf.Clamp01(bottomX);
|
||||
this.bottomY = Mathf.Clamp01(bottomY);
|
||||
this.intensityCurve = intensityCurve;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
{
|
||||
public class CrossTrackPoint_BM : GameElement_BM
|
||||
{
|
||||
public FlexibleInt trackSwitch;
|
||||
public FlexibleFloat trackPercent;
|
||||
public FlexibleInt_BM trackSwitch;
|
||||
public FlexibleFloat_BM trackPercent;
|
||||
public bool MotionAngles = false;
|
||||
|
||||
public CrossTrackPoint_BM()
|
||||
@@ -15,7 +15,7 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
}
|
||||
|
||||
public CrossTrackPoint_BM(string elementName, Guid elementGuid, List<string> tags,
|
||||
GameElement_BM attachedElement, FlexibleInt trackSwitch, FlexibleFloat trackPercent)
|
||||
GameElement_BM attachedElement, FlexibleInt_BM trackSwitch, FlexibleFloat_BM trackPercent)
|
||||
: base(elementName, elementGuid, tags, attachedElement)
|
||||
{
|
||||
this.trackSwitch = trackSwitch;
|
||||
@@ -25,7 +25,9 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
public override void ExecuteBM()
|
||||
{
|
||||
var element = CrossTrackPoint.GenerateElement(elementName, elementGuid, tags, false,
|
||||
GetElement(attachedElementGuid) as ElementFolder, trackSwitch, trackPercent);
|
||||
GetElement(attachedElementGuid) as ElementFolder,
|
||||
trackSwitch?.ConvertToGameType() ?? new FlexibleInt(),
|
||||
trackPercent?.ConvertToGameType() ?? new FlexibleFloat());
|
||||
matchedElement = element;
|
||||
// 还原MotionAngles
|
||||
if (element is CrossTrackPoint ctp && this != null)
|
||||
@@ -38,7 +40,9 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
public override GameElement DuplicateBM(GameElement parent)
|
||||
{
|
||||
var newElement = CrossTrackPoint.GenerateElement(elementName, Guid.NewGuid(), tags, false,
|
||||
parent as ElementFolder, trackSwitch, trackPercent);
|
||||
parent as ElementFolder,
|
||||
trackSwitch?.ConvertToGameType() ?? new FlexibleInt(),
|
||||
trackPercent?.ConvertToGameType() ?? new FlexibleFloat());
|
||||
// 复制MotionAngles
|
||||
if (newElement is CrossTrackPoint ctp)
|
||||
{
|
||||
|
||||
@@ -38,34 +38,52 @@ namespace Ichni.RhythmGame.Beatmap
|
||||
e.SaveBM();
|
||||
e.submoduleList.RemoveAll(s => s == null);
|
||||
e.submoduleList.ForEach(s => s.SaveBM());
|
||||
if (forExport && e is IBeChangeInExport changeInExport)
|
||||
{
|
||||
changeInExport.SaveExportBM();
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var gameElement in gameElementList)
|
||||
{
|
||||
if (gameElement.matchedBM == null) continue;
|
||||
|
||||
if (forExport && gameElement is IBeChangeInExport changeInExport)
|
||||
BaseElement_BM elementToAdd = gameElement.matchedBM;
|
||||
|
||||
// 检查是否有导出替换元素,有则使用替代元素
|
||||
if (forExport && gameElement is IExportOverride exportOverride)
|
||||
{
|
||||
if (changeInExport.MatchingExportElement != null)
|
||||
BaseElement_BM exportElement = exportOverride.GetExportElement();
|
||||
if (exportElement != null)
|
||||
{
|
||||
elementList.Add(changeInExport.MatchingExportElement);
|
||||
List<BaseElement_BM> submodules = gameElement.submoduleList.ConvertAll(s => s.matchedBM);
|
||||
submodules.RemoveAll(s => s == null);
|
||||
submodules.ForEach(e => { e.attachedElementGuid = ((GameElement_BM)changeInExport.MatchingExportElement).elementGuid; });
|
||||
elementList.AddRange(submodules);
|
||||
elementToAdd = exportElement;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// 导出时跳过不应出现在最终结果中的主元素(如干涉仪 BM、Tracker BM)
|
||||
// 这些元素的效果已在父级 GetExportElement() 中烘焙完毕
|
||||
if (forExport && elementToAdd is ICanNotInExport)
|
||||
{
|
||||
elementList.Add(gameElement.matchedBM);
|
||||
List<BaseElement_BM> submodules = gameElement.submoduleList.ConvertAll(s => s.matchedBM);
|
||||
submodules.RemoveAll(s => s == null);
|
||||
elementList.AddRange(submodules);
|
||||
continue;
|
||||
}
|
||||
|
||||
elementList.Add(elementToAdd);
|
||||
List<BaseElement_BM> submodules = gameElement.submoduleList.ConvertAll(s => s.matchedBM);
|
||||
submodules.RemoveAll(s => s == null);
|
||||
|
||||
// 导出时:ICanNotInExport 子模块先执行导出策略(如将干涉仪效果烘焙到父元素),再移除
|
||||
if (forExport)
|
||||
{
|
||||
foreach (var sub in submodules.OfType<ICanNotInExport>())
|
||||
{
|
||||
sub.OnExport(elementToAdd);
|
||||
}
|
||||
submodules.RemoveAll(s => s is ICanNotInExport);
|
||||
}
|
||||
|
||||
// 当使用了替代元素时,将其 Guid 附给子模块
|
||||
if (elementToAdd != gameElement.matchedBM)
|
||||
{
|
||||
submodules.ForEach(s => { s.attachedElementGuid = ((GameElement_BM)elementToAdd).elementGuid; });
|
||||
}
|
||||
|
||||
elementList.AddRange(submodules);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Ichni.Editor;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -16,13 +19,49 @@ namespace Ichni.RhythmGame
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
base.SetUpInspector();
|
||||
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
|
||||
Inspector inspector = EditorManager.instance.uiManager.inspector;
|
||||
IHaveInspection insp = inspector;
|
||||
|
||||
// ── 组件下拉菜单 ──
|
||||
List<string> componentOptions = new List<string>();
|
||||
if (animatedObject != null)
|
||||
{
|
||||
var components = animatedObject.gameObject.GetComponents<Component>();
|
||||
foreach (var c in components)
|
||||
{
|
||||
string fullName = c.GetType().FullName;
|
||||
if (!componentOptions.Contains(fullName))
|
||||
componentOptions.Add(fullName);
|
||||
}
|
||||
componentOptions.Sort();
|
||||
}
|
||||
|
||||
// ── 属性/字段下拉菜单(基于当前 componentName) ──
|
||||
List<string> propertyOptions = new List<string>();
|
||||
if (!string.IsNullOrEmpty(componentName))
|
||||
{
|
||||
Type compType = ResolveComponentType(componentName);
|
||||
if (compType != null)
|
||||
{
|
||||
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.CanWrite && prop.PropertyType == typeof(Color))
|
||||
propertyOptions.Add(prop.Name);
|
||||
}
|
||||
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (field.FieldType == typeof(Color) && !propertyOptions.Contains(field.Name))
|
||||
propertyOptions.Add(field.Name);
|
||||
}
|
||||
propertyOptions.Sort();
|
||||
}
|
||||
}
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Property Animation Color")
|
||||
.InputField(nameof(componentName), "Component Name")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.InputField(nameof(propertyName), "Property Name")
|
||||
.Dropdown(nameof(componentName), componentOptions, "Component")
|
||||
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
|
||||
.Dropdown(nameof(propertyName), propertyOptions, "Property")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Color",
|
||||
new FlexibleFloat[] { propertyValueR, propertyValueG, propertyValueB, propertyValueA },
|
||||
@@ -33,5 +72,15 @@ namespace Ichni.RhythmGame
|
||||
.Button("Color A", () => insp.GenerateCompositeParameterWindow(this, "Color A", nameof(propertyValueA)).SetAsFlexibleFloat())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static Type ResolveComponentType(string typeName)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type type = assembly.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Ichni.Editor;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -15,18 +18,64 @@ namespace Ichni.RhythmGame
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
base.SetUpInspector();
|
||||
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
|
||||
Inspector inspector = EditorManager.instance.uiManager.inspector;
|
||||
IHaveInspection insp = inspector;
|
||||
|
||||
// ── 组件下拉菜单 ──
|
||||
List<string> componentOptions = new List<string>();
|
||||
if (animatedObject != null)
|
||||
{
|
||||
var components = animatedObject.gameObject.GetComponents<Component>();
|
||||
foreach (var c in components)
|
||||
{
|
||||
string fullName = c.GetType().FullName;
|
||||
if (!componentOptions.Contains(fullName))
|
||||
componentOptions.Add(fullName);
|
||||
}
|
||||
componentOptions.Sort();
|
||||
}
|
||||
|
||||
// ── 属性/字段下拉菜单(基于当前 componentName) ──
|
||||
List<string> propertyOptions = new List<string>();
|
||||
if (!string.IsNullOrEmpty(componentName))
|
||||
{
|
||||
Type compType = ResolveComponentType(componentName);
|
||||
if (compType != null)
|
||||
{
|
||||
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.CanWrite && prop.PropertyType == typeof(float))
|
||||
propertyOptions.Add(prop.Name);
|
||||
}
|
||||
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (field.FieldType == typeof(float) && !propertyOptions.Contains(field.Name))
|
||||
propertyOptions.Add(field.Name);
|
||||
}
|
||||
propertyOptions.Sort();
|
||||
}
|
||||
}
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Property Animation Float")
|
||||
.InputField(nameof(componentName), "Component Name")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.InputField(nameof(propertyName), "Property Name")
|
||||
.Dropdown(nameof(componentName), componentOptions, "Component")
|
||||
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
|
||||
.Dropdown(nameof(propertyName), propertyOptions, "Property")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Float",
|
||||
new FlexibleFloat[] { propertyValue }, new string[] { "Value" }))
|
||||
.Button("Float Value", () => insp.GenerateCompositeParameterWindow(this, "Float Value", nameof(propertyValue)).SetAsFlexibleFloat())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static Type ResolveComponentType(string typeName)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type type = assembly.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Ichni.Editor;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -15,13 +18,49 @@ namespace Ichni.RhythmGame
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
base.SetUpInspector();
|
||||
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
|
||||
Inspector inspector = EditorManager.instance.uiManager.inspector;
|
||||
IHaveInspection insp = inspector;
|
||||
|
||||
// ── 组件下拉菜单 ──
|
||||
List<string> componentOptions = new List<string>();
|
||||
if (animatedObject != null)
|
||||
{
|
||||
var components = animatedObject.gameObject.GetComponents<Component>();
|
||||
foreach (var c in components)
|
||||
{
|
||||
string fullName = c.GetType().FullName;
|
||||
if (!componentOptions.Contains(fullName))
|
||||
componentOptions.Add(fullName);
|
||||
}
|
||||
componentOptions.Sort();
|
||||
}
|
||||
|
||||
// ── 属性/字段下拉菜单(基于当前 componentName) ──
|
||||
List<string> propertyOptions = new List<string>();
|
||||
if (!string.IsNullOrEmpty(componentName))
|
||||
{
|
||||
Type compType = ResolveComponentType(componentName);
|
||||
if (compType != null)
|
||||
{
|
||||
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.CanWrite && prop.PropertyType == typeof(Vector3))
|
||||
propertyOptions.Add(prop.Name);
|
||||
}
|
||||
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
if (field.FieldType == typeof(Vector3) && !propertyOptions.Contains(field.Name))
|
||||
propertyOptions.Add(field.Name);
|
||||
}
|
||||
propertyOptions.Sort();
|
||||
}
|
||||
}
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Property Animation Vector3")
|
||||
.InputField(nameof(componentName), "Component Name")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.InputField(nameof(propertyName), "Property Name")
|
||||
.Dropdown(nameof(componentName), componentOptions, "Component")
|
||||
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
|
||||
.Dropdown(nameof(propertyName), propertyOptions, "Property")
|
||||
.OnChanged(() => AfterInitialize())
|
||||
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Vector3",
|
||||
new FlexibleFloat[] { propertyValueX, propertyValueY, propertyValueZ }, new string[] { "X", "Y", "Z" }))
|
||||
@@ -30,5 +69,15 @@ namespace Ichni.RhythmGame
|
||||
.Button("Vector Z", () => insp.GenerateCompositeParameterWindow(this, "Vector Z", nameof(propertyValueZ)).SetAsFlexibleFloat())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static Type ResolveComponentType(string typeName)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type type = assembly.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -37,8 +38,8 @@ namespace Ichni.RhythmGame
|
||||
.InputField(nameof(TimeOffset), "Offset")
|
||||
.Button("Interferometer", () =>
|
||||
{
|
||||
Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero);
|
||||
CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero));
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -37,8 +38,8 @@ namespace Ichni.RhythmGame
|
||||
.InputField(nameof(TimeOffset), "Offset")
|
||||
.Button("Interferometer", () =>
|
||||
{
|
||||
Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero);
|
||||
CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero));
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -37,8 +38,8 @@ namespace Ichni.RhythmGame
|
||||
.InputField(nameof(TimeOffset), "Offset")
|
||||
.Button("Interferometer", () =>
|
||||
{
|
||||
Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero);
|
||||
CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer", Guid.NewGuid(),
|
||||
new List<string>(), true, this, InterferomType.Additive, Vector3.zero));
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -21,8 +22,8 @@ namespace Ichni.RhythmGame
|
||||
new FlexibleFloat[] { positionX, positionY, positionZ }, new string[] { "PosX", "PosY", "PosZ" }))
|
||||
.ParameterText(nameof(PreviewValue), "value:", autoUpdate: true)
|
||||
.Span(3)
|
||||
.Button("Interferometer", () => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero))
|
||||
.Button("Interferometer", () => CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero)))
|
||||
.Span(3)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Linq;
|
||||
using Ichni.Editor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class PositionSync
|
||||
{
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
Inspector inspectorMain = EditorManager.instance.uiManager.inspector;
|
||||
IHaveInspection inspection = inspectorMain;
|
||||
|
||||
var nameRef = new ElementRef<DynamicUIInputField>();
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Position Sync")
|
||||
.UnboundInputField("Try Get Element").WithRef(nameRef)
|
||||
.Button("Connect Transform Element", () =>
|
||||
{
|
||||
GameElement found = EditorManager.instance.beatmapContainer.gameElementList
|
||||
.Where(e => e is IHaveTransformSubmodule)
|
||||
.FirstOrDefault(e => e.elementName == nameRef.Value?.GetValue<string>());
|
||||
if (found == null)
|
||||
{
|
||||
LogWindow.Log("Transform Game Element not found.", Color.yellow);
|
||||
return;
|
||||
}
|
||||
syncTargetObject = found;
|
||||
inspectorMain.SetInspector(this);
|
||||
})
|
||||
.HintText(() => syncTargetObject == null
|
||||
? "No Transform Game Element Connected"
|
||||
: "Connected With: " + syncTargetObject.elementName)
|
||||
.Button("Enabling", () =>
|
||||
{
|
||||
inspection.GenerateCompositeParameterWindow(this, "Enabling", nameof(enabling))
|
||||
.SetAsFlexibleBool();
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ee62caf923c281469adfc5cff298929
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -22,8 +23,8 @@ namespace Ichni.RhythmGame
|
||||
new string[] { "ScaleX", "ScaleY", "ScaleZ" }))
|
||||
.ParameterText(nameof(PreviewValue), "value:", autoUpdate: true)
|
||||
.Span(3)
|
||||
.Button("Interferometer", () => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero))
|
||||
.Button("Interferometer", () => CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero)))
|
||||
.Span(3)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -22,8 +23,8 @@ namespace Ichni.RhythmGame
|
||||
new string[] { "EulerX", "EulerY", "EulerZ" }))
|
||||
.ParameterText(nameof(PreviewValue), "value:", autoUpdate: true)
|
||||
.Span(3)
|
||||
.Button("Interferometer", () => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero))
|
||||
.Button("Interferometer", () => CommandManager.ExecuteCreate(() => Vector3Interferometer.GenerateElement("New Vector3 Interferometer",
|
||||
Guid.NewGuid(), new List<string>(), true, this, InterferomType.Additive, Vector3.zero)))
|
||||
.Span(3)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -23,41 +24,41 @@ namespace Ichni.RhythmGame
|
||||
var builder = InspectorBuilder.For(this)
|
||||
.Section("Generate")
|
||||
.Button("Environment Object", () =>
|
||||
TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(),
|
||||
new List<string>(), true, this))
|
||||
CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(),
|
||||
new List<string>(), true, this)))
|
||||
.Toggle(nameof(isStatic), "Is Static")
|
||||
.OnChanged(() => gameObject.isStatic = isStatic)
|
||||
.Preset(InspectorBuilder.TransformPreset)
|
||||
.Section("Generate Animations")
|
||||
.Button("Base Color Change", () =>
|
||||
BaseColorChange.GenerateElement("New Base Color Change", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => BaseColorChange.GenerateElement("New Base Color Change", Guid.NewGuid(), new List<string>(), true,
|
||||
this,
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.r, colorSubmodule.originalBaseColor.r, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.g, colorSubmodule.originalBaseColor.g, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.b, colorSubmodule.originalBaseColor.b, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.a, colorSubmodule.originalBaseColor.a, animationCurveType: AnimationCurveType.Linear) })
|
||||
));
|
||||
)));
|
||||
|
||||
if (haveEmissionColor)
|
||||
{
|
||||
builder.Button("Emission Color Change", () =>
|
||||
EmissionColorChange.GenerateElement("New Emission Color Change", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => EmissionColorChange.GenerateElement("New Emission Color Change", Guid.NewGuid(),
|
||||
new List<string>(), true, this,
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat()));
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat())));
|
||||
}
|
||||
|
||||
builder
|
||||
.Button("Property Animation Color", () =>
|
||||
PropertyAnimationColor.GenerateElement("New Property Animation Color", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => PropertyAnimationColor.GenerateElement("New Property Animation Color", Guid.NewGuid(),
|
||||
new List<string>(), true, this, GetType().FullName, "",
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat()))
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat())))
|
||||
.Button("Property Animation Float", () =>
|
||||
PropertyAnimationFloat.GenerateElement("New Property Animation Float", Guid.NewGuid(),
|
||||
new List<string>(), true, this, GetType().FullName, "", new FlexibleFloat()))
|
||||
CommandManager.ExecuteCreate(() => PropertyAnimationFloat.GenerateElement("New Property Animation Float", Guid.NewGuid(),
|
||||
new List<string>(), true, this, GetType().FullName, "", new FlexibleFloat())))
|
||||
.Button("Property Animation Vector3", () =>
|
||||
PropertyAnimationVector3.GenerateElement("New Property Animation Vector3", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => PropertyAnimationVector3.GenerateElement("New Property Animation Vector3", Guid.NewGuid(),
|
||||
new List<string>(), true, this, GetType().FullName, "",
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat()))
|
||||
new FlexibleFloat(), new FlexibleFloat(), new FlexibleFloat())))
|
||||
.Build();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse;
|
||||
using Sirenix.OdinInspector;
|
||||
@@ -26,29 +27,29 @@ namespace Ichni.RhythmGame
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Generate", sectionOrder: 10)
|
||||
.Button("Folder", () => ElementFolder.GenerateElement("New Folder", Guid.NewGuid(), new List<string>(), true, this))
|
||||
.Button("Track", () => Track.GenerateElement("New Track", Guid.NewGuid(), new List<string>(), true, this))
|
||||
.Button("Camera", () => GameCamera.GenerateElement("New Camera", Guid.NewGuid(), new List<string>(), true, this, GameCamera.CameraViewType.Perspective, 60, 10))
|
||||
.Button("Cross Track Point", () => CrossTrackPoint.GenerateElement("New Cross Track Point", Guid.NewGuid(), new List<string>(), true, this, new FlexibleInt(), new FlexibleFloat()))
|
||||
.Button("Folder", () => CommandManager.ExecuteCreate(() => ElementFolder.GenerateElement("New Folder", Guid.NewGuid(), new List<string>(), true, this)))
|
||||
.Button("Track", () => CommandManager.ExecuteCreate(() => Track.GenerateElement("New Track", Guid.NewGuid(), new List<string>(), true, this)))
|
||||
.Button("Camera", () => CommandManager.ExecuteCreate(() => GameCamera.GenerateElement("New Camera", Guid.NewGuid(), new List<string>(), true, this, GameCamera.CameraViewType.Perspective, 60, 10)))
|
||||
.Button("Cross Track Point", () => CommandManager.ExecuteCreate(() => CrossTrackPoint.GenerateElement("New Cross Track Point", Guid.NewGuid(), new List<string>(), true, this, new FlexibleInt(), new FlexibleFloat())))
|
||||
.Build();
|
||||
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Generate Elements", sectionOrder: 15)
|
||||
.Preset(InspectorBuilder.TransformPreset)
|
||||
.Section("Generate Notes", sectionOrder: 20)
|
||||
.Button("Tap", () => Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, this, 0f))
|
||||
.Button("Stay", () => Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, this, 0f))
|
||||
.Button("Hold", () => Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, this, 0f, 1f))
|
||||
.Button("Flick", () => Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, this, 0f, new List<Vector2>()))
|
||||
.Button("Tap", () => CommandManager.ExecuteCreate(() => Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, this, 0f)))
|
||||
.Button("Stay", () => CommandManager.ExecuteCreate(() => Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, this, 0f)))
|
||||
.Button("Hold", () => CommandManager.ExecuteCreate(() => Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, this, 0f, 1f)))
|
||||
.Button("Flick", () => CommandManager.ExecuteCreate(() => Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, this, 0f, new List<Vector2>())))
|
||||
.Section("Generate Environment", sectionOrder: 30)
|
||||
.Button("Environment Object", () => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(), true, this))
|
||||
.Button("Time Effects Collection", () => TimeEffectsCollection.GenerateElement("New Time Effects Collection", Guid.NewGuid(),
|
||||
new List<string>(), true, this, 0))
|
||||
.Button("Environment Object", () => CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(), true, this)))
|
||||
.Button("Time Effects Collection", () => CommandManager.ExecuteCreate(() => TimeEffectsCollection.GenerateElement("New Time Effects Collection", Guid.NewGuid(),
|
||||
new List<string>(), true, this, 0)))
|
||||
.Button("Particle Emitter", () =>
|
||||
{
|
||||
ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
this, "", "", false, 0, 1, ParticleSystemSimulationSpace.World,
|
||||
10, 5, 1, 1, true, Vector3.zero);
|
||||
10, 5, 1, 1, true, Vector3.zero));
|
||||
})
|
||||
|
||||
.Section("Tools", sectionOrder: 40)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -25,14 +26,14 @@ namespace Ichni.RhythmGame
|
||||
.Preset(InspectorBuilder.TransformPreset)
|
||||
.Section("Generate")
|
||||
.Button("Field of View", () =>
|
||||
CameraFieldOfView.GenerateElement("New Field of View", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => CameraFieldOfView.GenerateElement("New Field of View", Guid.NewGuid(),
|
||||
new List<string>(), true, this, new FlexibleFloat(new List<AnimatedFloat>
|
||||
{
|
||||
new AnimatedFloat(0f, 1f, 60f, 60f, AnimationCurveType.Linear)
|
||||
})))
|
||||
}))))
|
||||
.Button("Extension", () =>
|
||||
GameCameraExtension.GenerateElement("New Extension", Guid.NewGuid(),
|
||||
new List<string>(), true, this, 1000f))
|
||||
CommandManager.ExecuteCreate(() => GameCameraExtension.GenerateElement("New Extension", Guid.NewGuid(),
|
||||
new List<string>(), true, this, 1000f)))
|
||||
.Build();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -13,14 +13,16 @@ namespace Ichni.RhythmGame
|
||||
#region [层级从属结构与UI映射] Hierarchy & UI Mapping
|
||||
//与游戏物体连接的Tab
|
||||
public HierarchyTab connectedTab;
|
||||
|
||||
|
||||
public Action RefreshAction;
|
||||
|
||||
public class EnableType : IBaseElement
|
||||
{
|
||||
public Type type;
|
||||
public bool enable;
|
||||
public BaseElement_BM matchedBM { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
|
||||
public List<EnableType> enableTypes;
|
||||
#endregion
|
||||
|
||||
@@ -30,6 +32,7 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
if (connectedTab != null) connectedTab.tabButtonText.text = this.elementName;
|
||||
gameObject.name = elementName;
|
||||
RefreshAction?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +68,7 @@ namespace Ichni.RhythmGame
|
||||
Destroy(gameObject); //销毁
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
#region [编辑器面板绘制] Inspector UI
|
||||
|
||||
/// <summary>
|
||||
@@ -134,7 +137,7 @@ namespace Ichni.RhythmGame
|
||||
#endregion
|
||||
|
||||
#region [编辑器辅助功能与节点树检索] Utility Nodes Retrieving
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有子GameElement
|
||||
/// </summary>
|
||||
@@ -171,7 +174,7 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
if (et.enable) enabledTypes.Add(et.type);
|
||||
}
|
||||
|
||||
|
||||
return childElementList.FindAll(child =>
|
||||
child != null && enabledTypes.Any(t => t.IsAssignableFrom(child.GetType()))
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Ichni.RhythmGame
|
||||
.OnChanged(() => inspectorMain.SetInspector(this))
|
||||
.Button("Generate", () =>
|
||||
{
|
||||
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElement(this);
|
||||
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(this);
|
||||
inspectorMain.ClearInspector();
|
||||
SubstantialObject.GenerateElement(elementName, elementGuid, tags, true,
|
||||
themeBundleName, objectName, parentElement);
|
||||
|
||||
@@ -11,8 +11,8 @@ namespace Ichni.RhythmGame
|
||||
InspectorBuilder.For(this)
|
||||
.Section("Pixelate Effect")
|
||||
.InputField(nameof(effectTime), "Effect Time")
|
||||
.InputField(nameof(bottomX), "Bottom X")
|
||||
.InputField(nameof(bottomY), "Bottom Y")
|
||||
.InputField(nameof(bottomX), "Bottom X (0-1)")
|
||||
.InputField(nameof(bottomY), "Bottom Y (0-1)")
|
||||
.Button("Intensity Curve", () => insp.GenerateCompositeParameterWindow(this, "Intensity Curve", nameof(intensityCurve)).SetAsCustomCurve())
|
||||
.Button("Clear Pixelate", Recover)
|
||||
.Button("Remove", () => { nowEffectState = EffectState.Before; AccommodatingList.Remove(this); EditorManager.instance.uiManager.inspector.SetInspector(attachedGameElement); })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -84,9 +85,9 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
if (skyboxSubsetter == null)
|
||||
{
|
||||
SkyboxSubsetter.GenerateElement("New Skybox Subsetter", Guid.NewGuid(),
|
||||
CommandManager.ExecuteCreate(() => SkyboxSubsetter.GenerateElement("New Skybox Subsetter", Guid.NewGuid(),
|
||||
new List<string>(), true, this,
|
||||
new List<string>(), new List<string>(), new List<float>(), new List<float>());
|
||||
new List<string>(), new List<string>(), new List<float>(), new List<float>()));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace Ichni.RhythmGame
|
||||
builder.Button("Hold Start", () =>
|
||||
inspection.GenerateCompositeParameterWindow(this, "Hold Start Sound List",
|
||||
nameof(holdStartAudioList)).SetAsStringList());
|
||||
builder.Button("Hold End", () =>
|
||||
inspection.GenerateCompositeParameterWindow(this, "Hold End Sound List",
|
||||
nameof(holdEndAudioList)).SetAsStringList());
|
||||
}
|
||||
|
||||
builder.Build();
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -75,7 +76,7 @@ namespace Ichni.RhythmGame
|
||||
.Section("Note Visual")
|
||||
.Button("Generate Note Visual", () =>
|
||||
{
|
||||
TemporaryObject.GenerateElement("New Note Visual", Guid.NewGuid(), new List<string>(), true, this);
|
||||
CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Note Visual", Guid.NewGuid(), new List<string>(), true, this));
|
||||
})
|
||||
.EnabledIf(() => noteVisual == null)
|
||||
.Build();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -38,13 +39,13 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public void GenerateBaseColorChange()
|
||||
{
|
||||
BaseColorChange.GenerateElement("New Base Color Change", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => BaseColorChange.GenerateElement("New Base Color Change", Guid.NewGuid(), new List<string>(), true,
|
||||
this,
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.r, colorSubmodule.originalBaseColor.r, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.g, colorSubmodule.originalBaseColor.g, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.b, colorSubmodule.originalBaseColor.b, animationCurveType: AnimationCurveType.Linear) }),
|
||||
new FlexibleFloat(new List<AnimatedFloat> { new AnimatedFloat(0f, 0.1f, colorSubmodule.originalBaseColor.a, colorSubmodule.originalBaseColor.a, animationCurveType: AnimationCurveType.Linear) })
|
||||
);
|
||||
));
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -12,7 +13,7 @@ namespace Ichni.RhythmGame
|
||||
public override void SaveBM()
|
||||
{
|
||||
matchedBM = new CrossTrackPoint_BM(elementName, elementGuid, tags,
|
||||
parentElement.matchedBM as GameElement_BM, trackSwitch, trackPercent)
|
||||
parentElement.matchedBM as GameElement_BM, trackSwitch?.ConvertToBM(), trackPercent?.ConvertToBM())
|
||||
{
|
||||
MotionAngles = this.MotionAngles
|
||||
};
|
||||
@@ -36,36 +37,67 @@ namespace Ichni.RhythmGame
|
||||
.Toggle(nameof(MotionAngles), "Motion With Angles")
|
||||
.Section("Generate")
|
||||
.Button("Game Camera", () =>
|
||||
GameCamera.GenerateElement("New Game Camera", Guid.NewGuid(), new List<string>(),
|
||||
true, this, GameCamera.CameraViewType.Perspective, 60, 10))
|
||||
CommandManager.ExecuteCreate(() => GameCamera.GenerateElement("New Game Camera", Guid.NewGuid(), new List<string>(),
|
||||
true, this, GameCamera.CameraViewType.Perspective, 60, 10)))
|
||||
.Button("Trail", () =>
|
||||
Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(),
|
||||
CommandManager.ExecuteCreate(() => Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(),
|
||||
true, this, 1, true, 1,
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", ""))
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", "")))
|
||||
.Button("Environment Object", () =>
|
||||
TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this))
|
||||
CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this)))
|
||||
.Button("Generate Particle Emitter", () =>
|
||||
ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
this, "", "", false, 0, 1, ParticleSystemSimulationSpace.World,
|
||||
10, 5, 1, 1, true, Vector3.zero))
|
||||
10, 5, 1, 1, true, Vector3.zero)))
|
||||
.Build();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [工具方法] Tool Methods
|
||||
|
||||
public Action PasteAction = null;
|
||||
private void PasteTrackList()
|
||||
{
|
||||
List<Track> trackList = (parentElement as ElementFolder).trackList;
|
||||
if (parentElement is not ElementFolder folder)
|
||||
{
|
||||
LogWindow.Log("Paste Track List failed: parent element is not an ElementFolder.", Color.yellow);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Track> trackList = folder.trackList;
|
||||
if (trackList == null || trackList.Count == 0)
|
||||
{
|
||||
LogWindow.Log("Paste Track List failed: no tracks found in parent folder.", Color.yellow);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PasteAction == null)
|
||||
{
|
||||
PasteAction = () =>
|
||||
{
|
||||
PasteTrackList();
|
||||
};
|
||||
}
|
||||
trackSwitch = new FlexibleInt();
|
||||
trackPercent = new FlexibleFloat();
|
||||
foreach (Track track in trackList)
|
||||
{
|
||||
if (track == null) continue;
|
||||
|
||||
TrackTimeSubmoduleMovable trackTimeSubmodule = track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
|
||||
if (trackTimeSubmodule == null)
|
||||
{
|
||||
LogWindow.Log("Paste Track List skipped a track without movable time submodule.", Color.yellow);
|
||||
continue;
|
||||
}
|
||||
|
||||
trackSwitch.animations.Add(new AnimatedInt(trackTimeSubmodule.trackStartTime,
|
||||
trackList.IndexOf(track)));
|
||||
trackPercent.animations.Add(new AnimatedFloat(trackTimeSubmodule.trackStartTime,
|
||||
trackTimeSubmodule.trackEndTime, 0, 1, trackTimeSubmodule.animationCurveType));
|
||||
track.RefreshAction -= PasteAction;
|
||||
track.RefreshAction += PasteAction;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -29,16 +30,16 @@ namespace Ichni.RhythmGame
|
||||
.OnChanged(() => trackPositioner.motion.rotationOffset = motionEulerAngles)
|
||||
.Section("Generate")
|
||||
.Button("Generate Trail", () =>
|
||||
Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(), true,
|
||||
this, 1, true, 1,
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", ""))
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", "")))
|
||||
.Button("Environment Object", () =>
|
||||
TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this))
|
||||
CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this)))
|
||||
.Button("Generate Particle Emitter", () =>
|
||||
ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
this, "", "", false, 0, 1, ParticleSystemSimulationSpace.World,
|
||||
10, 5, 1, 1, true, Vector3.zero))
|
||||
10, 5, 1, 1, true, Vector3.zero)))
|
||||
.Build();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -33,16 +34,16 @@ namespace Ichni.RhythmGame
|
||||
.Toggle(nameof(MotionAngles), "Motion With Angles")
|
||||
.Section("Generate")
|
||||
.Button("Generate Trail", () =>
|
||||
Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => Trail.GenerateElement("New Trail", Guid.NewGuid(), new List<string>(), true,
|
||||
this, 1, true, 1,
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", ""))
|
||||
AnimationCurve.Constant(0, 1, 1), ColorExtensions.DefaultGradient(), "", "")))
|
||||
.Button("Environment Object", () =>
|
||||
TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this))
|
||||
CommandManager.ExecuteCreate(() => TemporaryObject.GenerateElement("New Environment Object", Guid.NewGuid(), new List<string>(),
|
||||
true, this)))
|
||||
.Button("Generate Particle Emitter", () =>
|
||||
ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
CommandManager.ExecuteCreate(() => ParticleEmitter.GenerateElement("New Particle Emitter", Guid.NewGuid(), new List<string>(), true,
|
||||
this, "", "", false, 0, 1, ParticleSystemSimulationSpace.World,
|
||||
10, 5, 1, 1, true, Vector3.zero))
|
||||
10, 5, 1, 1, true, Vector3.zero)))
|
||||
.Build();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
@@ -27,7 +28,7 @@ namespace Ichni.RhythmGame
|
||||
.OnChanged(ClosePath)
|
||||
.Button("Generate Path Node", () =>
|
||||
{
|
||||
PathNode.GenerateElement("New Path Node", Guid.NewGuid(), new List<string>(), true, track, true);
|
||||
CommandManager.ExecuteCreate(() => PathNode.GenerateElement("New Path Node", Guid.NewGuid(), new List<string>(), true, track, true));
|
||||
})
|
||||
.Toggle(nameof(isShowingDisplay), "Show Display")
|
||||
.OnChanged(() => SetDisplay(isShowingDisplay))
|
||||
|
||||
@@ -14,8 +14,10 @@ namespace Ichni.RhythmGame
|
||||
/// </summary>
|
||||
/// <param name="unitPositionOffset">单位位置整体偏移</param>
|
||||
/// <param name="unitTimeOffset">单位时间偏移</param>
|
||||
/// <param name="unitRotationOffset">单位旋转偏移(欧拉角)</param>
|
||||
/// <param name="unitScaleOffset">单位缩放偏移</param>
|
||||
/// <param name="iteration">迭代次数,即产生几个粘贴的Track</param>
|
||||
private void QuickCopy(Vector3 unitPositionOffset, float unitTimeOffset, bool includeAnimations = true, int iteration = 1, bool MovingTrack = false)
|
||||
private void QuickCopy(Vector3 unitPositionOffset, float unitTimeOffset, bool includeAnimations = true, int iteration = 1, bool MovingTrack = false, Vector3 unitRotationOffset = default, Vector3 unitScaleOffset = default)
|
||||
{
|
||||
if (iteration <= 0) return;
|
||||
|
||||
@@ -23,23 +25,29 @@ namespace Ichni.RhythmGame
|
||||
cpd.CopyElement(this);
|
||||
for (int i = 1; i <= iteration; i++)
|
||||
{
|
||||
cpd.PasteElement(parentElement);
|
||||
cpd.PasteElementRaw(this, parentElement);
|
||||
Track newTrack = cpd.pastedElementList[0] as Track;
|
||||
Vector3 positionOffset = unitPositionOffset * i;
|
||||
float timeOffset = unitTimeOffset * i;
|
||||
Vector3 rotationOffset = unitRotationOffset * i;
|
||||
Vector3 scaleOffset = unitScaleOffset * i;
|
||||
|
||||
//以下:对Track的所有有效的Submodule和子GameElement进行偏移 TODO: 需要思考是否有统一时间偏移的方法?
|
||||
if (MovingTrack)
|
||||
{
|
||||
newTrack.transformSubmodule.originalPosition += positionOffset;
|
||||
newTrack.transformSubmodule.originalEulerAngles += rotationOffset;
|
||||
newTrack.transformSubmodule.originalScale += scaleOffset;
|
||||
newTrack.transformSubmodule.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
//PathNode,位置偏移
|
||||
//PathNode,位置/旋转/缩放偏移
|
||||
newTrack.trackPathSubmodule.pathNodeList.ForEach(pn =>
|
||||
{
|
||||
pn.transformSubmodule.originalPosition += positionOffset;
|
||||
pn.transformSubmodule.originalEulerAngles += rotationOffset;
|
||||
pn.transformSubmodule.originalScale += scaleOffset;
|
||||
pn.transformSubmodule.Refresh();
|
||||
});
|
||||
}
|
||||
@@ -161,4 +169,4 @@ namespace Ichni.RhythmGame
|
||||
trackPathSubmodule.pathNodeList.ForEach(pn => pn.SetPathNodeSphere(isShowing));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ichni.RhythmGame;
|
||||
using Ichni.Editor;
|
||||
using Ichni.Editor.Commands;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
@@ -79,41 +80,45 @@ namespace Ichni.RhythmGame
|
||||
.Section("Generate Elements", sectionOrder: 20)
|
||||
.Button("Path Node", () =>
|
||||
{
|
||||
PathNode.GenerateElement("New Path Node", Guid.NewGuid(), new List<string>(), true, this, true);
|
||||
CommandManager.ExecuteCreate(() => PathNode.GenerateElement("New Path Node", Guid.NewGuid(), new List<string>(), true, this, true));
|
||||
})
|
||||
.EnabledIf(() => trackPathSubmodule != null)
|
||||
.Button("Track Percent Point", () =>
|
||||
{
|
||||
var a = TrackPercentPoint.GenerateElement("New Track Percent Point", Guid.NewGuid(), new List<string>(), true, this, new FlexibleFloat());
|
||||
if (trackTimeSubmodule != null && trackTimeSubmodule is TrackTimeSubmoduleMovable trackTimeSubmoduleMovable)
|
||||
a.trackPercent.Add(
|
||||
new AnimatedFloat(trackTimeSubmoduleMovable.trackStartTime, trackTimeSubmoduleMovable.trackEndTime, 0, 1, AnimationCurveType.Linear));
|
||||
CommandManager.ExecuteCreate(() =>
|
||||
{
|
||||
var a = TrackPercentPoint.GenerateElement("New Track Percent Point", Guid.NewGuid(), new List<string>(), true, this, new FlexibleFloat());
|
||||
if (trackTimeSubmodule != null && trackTimeSubmodule is TrackTimeSubmoduleMovable trackTimeSubmoduleMovable)
|
||||
a.trackPercent.Add(
|
||||
new AnimatedFloat(trackTimeSubmoduleMovable.trackStartTime, trackTimeSubmoduleMovable.trackEndTime, 0, 1, AnimationCurveType.Linear));
|
||||
return a;
|
||||
});
|
||||
})
|
||||
.Button("Track Head Point", () =>
|
||||
{
|
||||
TrackHeadPoint.GenerateElement("New Track Head Point", Guid.NewGuid(), new List<string>(), true, this, false, Vector3.zero);
|
||||
CommandManager.ExecuteCreate(() => TrackHeadPoint.GenerateElement("New Track Head Point", Guid.NewGuid(), new List<string>(), true, this, false, Vector3.zero));
|
||||
})
|
||||
.EnabledIf(() => trackTimeSubmodule is TrackTimeSubmoduleMovable)
|
||||
.Button("Tap", () => Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, this, 0f))
|
||||
.Button("Stay", () => Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, this, 0f))
|
||||
.Button("Hold", () => Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, this, 0f, 1f))
|
||||
.Button("Flick", () => Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, this, 0f, new List<Vector2>()))
|
||||
.Button("Tap", () => CommandManager.ExecuteCreate(() => Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, this, 0f)))
|
||||
.Button("Stay", () => CommandManager.ExecuteCreate(() => Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, this, 0f)))
|
||||
.Button("Hold", () => CommandManager.ExecuteCreate(() => Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, this, 0f, 1f)))
|
||||
.Button("Flick", () => CommandManager.ExecuteCreate(() => Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, this, 0f, new List<Vector2>())))
|
||||
.Button("Particle Tracker", () =>
|
||||
{
|
||||
ParticleTracker.GenerateElement("New Particle Tracker", Guid.NewGuid(), new List<string>(), true, this,
|
||||
string.Empty, string.Empty, false, 0, 1, false, 10, Vector3.right, 10, 5, true, Vector3.zero);
|
||||
CommandManager.ExecuteCreate(() => ParticleTracker.GenerateElement("New Particle Tracker", Guid.NewGuid(), new List<string>(), true, this,
|
||||
string.Empty, string.Empty, false, 0, 1, false, 10, Vector3.right, 10, 5, true, Vector3.zero));
|
||||
})
|
||||
.Button("Object Tracker", () =>
|
||||
{
|
||||
ObjectTracker.GenerateElement("New Object Tracker", Guid.NewGuid(), new List<string>(), true, this,
|
||||
CommandManager.ExecuteCreate(() => ObjectTracker.GenerateElement("New Object Tracker", Guid.NewGuid(), new List<string>(), true, this,
|
||||
string.Empty, string.Empty, 10, Vector2.zero, Vector2.zero, string.Empty,
|
||||
false, Vector3.zero, Vector3.zero, string.Empty,
|
||||
false, Vector3.zero, Vector3.zero, string.Empty);
|
||||
false, Vector3.zero, Vector3.zero, string.Empty));
|
||||
})
|
||||
.Button("Track Global Color Change", () =>
|
||||
{
|
||||
TrackGlobalColorChange.GenerateElement("New Track Global Color Change", Guid.NewGuid(), new List<string>(), true, this,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
|
||||
CommandManager.ExecuteCreate(() => TrackGlobalColorChange.GenerateElement("New Track Global Color Change", Guid.NewGuid(), new List<string>(), true, this,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true)));
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -126,22 +131,44 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
IHaveInspection qcWindow = inspectorMain.GenerateSecondaryWindow(this, elementName + "'s Quick Copy");
|
||||
var qcContainer = qcWindow.GenerateContainer();
|
||||
var qcSubcontainer = qcContainer.GenerateSubcontainer(3);
|
||||
var xField = qcWindow.GenerateInputField(qcSubcontainer, "X offset", "0");
|
||||
var yField = qcWindow.GenerateInputField(qcSubcontainer, "Y offset", "0");
|
||||
var zField = qcWindow.GenerateInputField(qcSubcontainer, "Z offset", "0");
|
||||
var timeField = qcWindow.GenerateInputField(qcSubcontainer, "Time offset", "0");
|
||||
var iterationField = qcWindow.GenerateInputField(qcSubcontainer, "Iteration", "0");
|
||||
var includeAnimationToggle = qcWindow.GenerateToggle(null, qcSubcontainer, "Include Animation", string.Empty);
|
||||
var movingTrackToggle = qcWindow.GenerateToggle(null, qcSubcontainer, "Moving Track or PathNode", string.Empty);
|
||||
qcWindow.GenerateButton(this, qcSubcontainer, "Copy", () =>
|
||||
|
||||
// 位置偏移
|
||||
var posSub = qcContainer.GenerateSubcontainer(3);
|
||||
var xField = qcWindow.GenerateInputField(posSub, "X offset", "0");
|
||||
var yField = qcWindow.GenerateInputField(posSub, "Y offset", "0");
|
||||
var zField = qcWindow.GenerateInputField(posSub, "Z offset", "0");
|
||||
|
||||
// 旋转偏移(欧拉角)
|
||||
var rotSub = qcContainer.GenerateSubcontainer(3);
|
||||
var rotXField = qcWindow.GenerateInputField(rotSub, "Rot X", "0");
|
||||
var rotYField = qcWindow.GenerateInputField(rotSub, "Rot Y", "0");
|
||||
var rotZField = qcWindow.GenerateInputField(rotSub, "Rot Z", "0");
|
||||
|
||||
// 缩放偏移
|
||||
var scaleSub = qcContainer.GenerateSubcontainer(3);
|
||||
var scaleXField = qcWindow.GenerateInputField(scaleSub, "Scale X", "0");
|
||||
var scaleYField = qcWindow.GenerateInputField(scaleSub, "Scale Y", "0");
|
||||
var scaleZField = qcWindow.GenerateInputField(scaleSub, "Scale Z", "0");
|
||||
|
||||
// 时间 & 迭代
|
||||
var timeSub = qcContainer.GenerateSubcontainer(3);
|
||||
var timeField = qcWindow.GenerateInputField(timeSub, "Time offset", "0");
|
||||
var iterationField = qcWindow.GenerateInputField(timeSub, "Iteration", "1");
|
||||
|
||||
// 选项 & 按钮
|
||||
var toggleSub = qcContainer.GenerateSubcontainer(3);
|
||||
var includeAnimationToggle = qcWindow.GenerateToggle(null, toggleSub, "Include Animation", string.Empty);
|
||||
var movingTrackToggle = qcWindow.GenerateToggle(null, toggleSub, "Move Track", string.Empty);
|
||||
qcWindow.GenerateButton(this, toggleSub, "Copy", () =>
|
||||
{
|
||||
Vector3 positionOffset = new Vector3(xField.GetValue<float>(), yField.GetValue<float>(), zField.GetValue<float>());
|
||||
Vector3 rotationOffset = new Vector3(rotXField.GetValue<float>(), rotYField.GetValue<float>(), rotZField.GetValue<float>());
|
||||
Vector3 scaleOffset = new Vector3(scaleXField.GetValue<float>(), scaleYField.GetValue<float>(), scaleZField.GetValue<float>());
|
||||
float timeOffset = timeField.GetValue<float>();
|
||||
int iteration = iterationField.GetValue<int>();
|
||||
bool includeAnimation = includeAnimationToggle.toggle.isOn;
|
||||
bool movingTrack = movingTrackToggle.toggle.isOn;
|
||||
this.QuickCopy(positionOffset, timeOffset, includeAnimation, iteration, movingTrack);
|
||||
this.QuickCopy(positionOffset, timeOffset, includeAnimation, iteration, movingTrack, rotationOffset, scaleOffset);
|
||||
});
|
||||
})
|
||||
.Button("Whole Track Move", () =>
|
||||
@@ -198,7 +225,7 @@ namespace Ichni.RhythmGame
|
||||
.Span(3)
|
||||
.Button("Fast Note Tracker", () =>
|
||||
{
|
||||
FastNoteTracker.GenerateElement("Fast Note Tracker", Guid.NewGuid(), new List<string>(), true, this);
|
||||
CommandManager.ExecuteCreate(() => FastNoteTracker.GenerateElement("Fast Note Tracker", Guid.NewGuid(), new List<string>(), true, this));
|
||||
})
|
||||
.Span(3)
|
||||
.Build();
|
||||
@@ -225,7 +252,7 @@ namespace Ichni.RhythmGame
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
|
||||
[Button("Check Notes")]
|
||||
public void checkNotes()
|
||||
{
|
||||
@@ -252,7 +279,7 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Button("Rebuild")]
|
||||
public void Rebuild()
|
||||
{
|
||||
|
||||
@@ -26,10 +26,11 @@ namespace Ichni.RhythmGame
|
||||
bool isFirstGenerated, GameElement parentElement)
|
||||
{
|
||||
base.Initialize(name, elementGuid, tags, isFirstGenerated, parentElement);
|
||||
if (this.parentElement.submoduleList.Count == 0 ||
|
||||
!this.parentElement.submoduleList.Exists(e => e is TransformSubmodule))
|
||||
if ((this is Displacement || this is Swirl || this is Scale || this is LookAt) && (this.parentElement.submoduleList.Count == 0 ||
|
||||
!this.parentElement.submoduleList.Exists(e => e is TransformSubmodule)))
|
||||
{
|
||||
LogWindow.Log("AnimationBase must be attached to a GameElement with TransformSubmodule", Color.red);
|
||||
LogWindow.Log("Transform animation must be attached to a GameElement with TransformSubmodule", Color.red);
|
||||
LogWindow.Log(this.elementName + " " + this.elementGuid.ToString(), Color.red);
|
||||
}
|
||||
|
||||
// 向 ElementUpdateScheduler 注册 Phase.Animation
|
||||
|
||||
@@ -17,19 +17,19 @@ namespace Ichni.RhythmGame
|
||||
private Component targetComponent;
|
||||
private FieldInfo targetField;
|
||||
private PropertyInfo targetProperty;
|
||||
|
||||
|
||||
// 高性能赋值委托
|
||||
private Action<Color> colorSetterDelegate;
|
||||
#endregion
|
||||
|
||||
#region [生命周期与工厂] Lifecycle & Factory
|
||||
public static PropertyAnimationColor GenerateElement(string elementName, Guid id, List<string> tags, bool isFirstGenerated,
|
||||
GameElement animatedObject, string componentName, string propertyName,
|
||||
GameElement animatedObject, string componentName, string propertyName,
|
||||
FlexibleFloat propertyValueR, FlexibleFloat propertyValueG, FlexibleFloat propertyValueB, FlexibleFloat propertyValueA)
|
||||
{
|
||||
PropertyAnimationColor animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
|
||||
.AddComponent<PropertyAnimationColor>();
|
||||
|
||||
|
||||
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
|
||||
|
||||
animation.animatedObject = animatedObject;
|
||||
@@ -64,10 +64,10 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
Debug.LogWarning($"[PropertyAnimationColor] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
|
||||
}
|
||||
|
||||
|
||||
base.AfterInitialize();
|
||||
}
|
||||
|
||||
|
||||
private Type GetTypeFromAllAssemblies(string typeName)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
@@ -125,7 +125,7 @@ namespace Ichni.RhythmGame
|
||||
|
||||
if (colorSetterDelegate != null)
|
||||
{
|
||||
colorSetterDelegate(targetColor);
|
||||
colorSetterDelegate(targetColor);
|
||||
}
|
||||
else if (targetProperty != null)
|
||||
{
|
||||
@@ -135,20 +135,24 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
targetField.SetValue(targetComponent, targetColor);
|
||||
}
|
||||
|
||||
|
||||
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
|
||||
{
|
||||
dirtyTarget.dirtyMarkSubmodule.MarkDirty(propertyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
animatedObject.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void ApplyTimeOffset(float offset)
|
||||
{
|
||||
base.ApplyTimeOffset(offset);
|
||||
void ApplyOffset(FlexibleFloat ff)
|
||||
{
|
||||
if (ff == null || ff.animations == null) return;
|
||||
foreach(var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
|
||||
foreach (var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
|
||||
}
|
||||
ApplyOffset(propertyValueR);
|
||||
ApplyOffset(propertyValueG);
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ichni.RhythmGame
|
||||
private Component targetComponent;
|
||||
private FieldInfo targetField;
|
||||
private PropertyInfo targetProperty;
|
||||
|
||||
|
||||
// 我们尝试通过原生的 Action 来降低反射 SetValue 带来的性能损耗(装箱与GC)
|
||||
private Action<float> floatSetterDelegate;
|
||||
#endregion
|
||||
@@ -28,7 +28,7 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
PropertyAnimationFloat animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
|
||||
.AddComponent<PropertyAnimationFloat>();
|
||||
|
||||
|
||||
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
|
||||
|
||||
animation.animatedObject = animatedObject;
|
||||
@@ -60,10 +60,10 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
Debug.LogWarning($"[PropertyAnimationFloat] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
|
||||
}
|
||||
|
||||
|
||||
base.AfterInitialize();
|
||||
}
|
||||
|
||||
|
||||
private Type GetTypeFromAllAssemblies(string typeName)
|
||||
{
|
||||
// 对于跨程序集的搜索
|
||||
@@ -125,7 +125,7 @@ namespace Ichni.RhythmGame
|
||||
|
||||
if (floatSetterDelegate != null)
|
||||
{
|
||||
floatSetterDelegate(value);
|
||||
floatSetterDelegate(value);
|
||||
}
|
||||
else if (targetProperty != null)
|
||||
{
|
||||
@@ -135,19 +135,23 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
targetField.SetValue(targetComponent, value);
|
||||
}
|
||||
|
||||
|
||||
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
|
||||
{
|
||||
dirtyTarget.dirtyMarkSubmodule?.MarkDirty(propertyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
animatedObject.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void ApplyTimeOffset(float offset)
|
||||
{
|
||||
base.ApplyTimeOffset(offset);
|
||||
if (propertyValue != null && propertyValue.animations != null)
|
||||
{
|
||||
foreach(var a in propertyValue.animations)
|
||||
foreach (var a in propertyValue.animations)
|
||||
{
|
||||
a.startTime += offset;
|
||||
a.endTime += offset;
|
||||
|
||||
@@ -17,19 +17,19 @@ namespace Ichni.RhythmGame
|
||||
private Component targetComponent;
|
||||
private FieldInfo targetField;
|
||||
private PropertyInfo targetProperty;
|
||||
|
||||
|
||||
// 高性能赋值委托
|
||||
private Action<Vector3> vectorSetterDelegate;
|
||||
#endregion
|
||||
|
||||
#region [生命周期与工厂] Lifecycle & Factory
|
||||
public static PropertyAnimationVector3 GenerateElement(string elementName, Guid id, List<string> tags, bool isFirstGenerated,
|
||||
GameElement animatedObject, string componentName, string propertyName,
|
||||
GameElement animatedObject, string componentName, string propertyName,
|
||||
FlexibleFloat propertyValueX, FlexibleFloat propertyValueY, FlexibleFloat propertyValueZ)
|
||||
{
|
||||
PropertyAnimationVector3 animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
|
||||
.AddComponent<PropertyAnimationVector3>();
|
||||
|
||||
|
||||
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
|
||||
|
||||
animation.animatedObject = animatedObject;
|
||||
@@ -63,10 +63,10 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
Debug.LogWarning($"[PropertyAnimationVector3] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
|
||||
}
|
||||
|
||||
|
||||
base.AfterInitialize();
|
||||
}
|
||||
|
||||
|
||||
private Type GetTypeFromAllAssemblies(string typeName)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
@@ -121,7 +121,7 @@ namespace Ichni.RhythmGame
|
||||
|
||||
if (vectorSetterDelegate != null)
|
||||
{
|
||||
vectorSetterDelegate(targetVector);
|
||||
vectorSetterDelegate(targetVector);
|
||||
}
|
||||
else if (targetProperty != null)
|
||||
{
|
||||
@@ -131,20 +131,24 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
targetField.SetValue(targetComponent, targetVector);
|
||||
}
|
||||
|
||||
|
||||
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
|
||||
{
|
||||
dirtyTarget.dirtyMarkSubmodule.MarkDirty(propertyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
animatedObject.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void ApplyTimeOffset(float offset)
|
||||
{
|
||||
base.ApplyTimeOffset(offset);
|
||||
void ApplyOffset(FlexibleFloat ff)
|
||||
{
|
||||
if (ff == null || ff.animations == null) return;
|
||||
foreach(var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
|
||||
foreach (var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
|
||||
}
|
||||
ApplyOffset(propertyValueX);
|
||||
ApplyOffset(propertyValueY);
|
||||
|
||||
@@ -13,27 +13,22 @@ namespace Ichni.RhythmGame
|
||||
Override
|
||||
}
|
||||
|
||||
public abstract class InterferometerBase : GameElement, IHaveTimeDurationSubmodule, IBeChangeInExport
|
||||
public abstract class InterferometerBase : GameElement, IHaveTimeDurationSubmodule
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public AnimationBase parentAnimationElement;
|
||||
public TimeDurationSubmodule timeDurationSubmodule { get; set; }
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
public InterferomType InterferomType;
|
||||
#endregion
|
||||
|
||||
#region [导出接口] Export Interface
|
||||
public virtual void SaveExportBM()
|
||||
{
|
||||
MatchingExportElement = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public interface IBeChangeInExport
|
||||
/// <summary>
|
||||
/// 提供导出时替换当前元素的替代 BM 元素。返回 null 表示不需要替换,使用默认逻辑。
|
||||
/// 实现方在 GetExportElement() 中完成准备工作(SaveBM、干涉仪、烘焙),然后返回替代元素。
|
||||
/// </summary>
|
||||
public interface IExportOverride
|
||||
{
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
void SaveExportBM();
|
||||
BaseElement_BM GetExportElement();
|
||||
}
|
||||
|
||||
public interface IHaveInterferometer
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class DisplacementTracker : AnimationBase, IBeChangeInExport, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedDisplacement
|
||||
public partial class DisplacementTracker : AnimationBase, IExportOverride, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedDisplacement
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
private TransformSubmodule targetTransformSubmodule;
|
||||
@@ -14,7 +14,7 @@ namespace Ichni.RhythmGame
|
||||
private FlexibleFloat positionX, positionY, positionZ;
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
public float TimeOffset;
|
||||
public BaseElement_BM MatchingExportElement { get; set; } = null;
|
||||
public bool MaybeDeadLoop = true;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new();
|
||||
#endregion
|
||||
|
||||
@@ -159,9 +159,9 @@ namespace Ichni.RhythmGame
|
||||
public override Vector3 getValue(float time)
|
||||
{
|
||||
Vector3 currentPosition = new Vector3(
|
||||
positionX.GetValue(time + TimeOffset),
|
||||
positionY.GetValue(time + TimeOffset),
|
||||
positionZ.GetValue(time + TimeOffset)
|
||||
positionX.GetValue(time),
|
||||
positionY.GetValue(time),
|
||||
positionZ.GetValue(time)
|
||||
);
|
||||
((IHaveVector3Interferometer)this).ApplyVector3Interferometers(ref currentPosition);
|
||||
return currentPosition;
|
||||
@@ -169,12 +169,15 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public Displacement GetOriginDisplacementWithOffset(ref float timeOffset)
|
||||
{
|
||||
return null;
|
||||
timeOffset += TimeOffset;
|
||||
if (targetDisplacement is Displacement disp) return disp;
|
||||
else if (targetDisplacement is DisplacementTracker dispTracker) return dispTracker.GetOriginDisplacementWithOffset(ref timeOffset);
|
||||
else throw new Exception("WTF type of tracked displacement.");
|
||||
}
|
||||
|
||||
public (FlexibleReturnType, bool)[] GetCurrentReturnTypes(float time)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return targetDisplacement.GetCurrentReturnTypes(time + TimeOffset);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -185,15 +188,17 @@ namespace Ichni.RhythmGame
|
||||
((GameElement)targetDisplacement).elementGuid, TimeOffset);
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
Refresh();
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
parentElement.SaveBM();
|
||||
var a = new Displacement_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
var result = new Displacement_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
positionX.ConvertToBM(), positionY.ConvertToBM(), positionZ.ConvertToBM());
|
||||
MatchingExportElement = a;
|
||||
// 将 Tracker 自身的干涉仪效果烘焙到导出数据中
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
result.positionX, result.positionY, result.positionZ);
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class ScaleTracker : AnimationBase, IBeChangeInExport, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedScale
|
||||
public partial class ScaleTracker : AnimationBase, IExportOverride, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedScale
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
private TransformSubmodule targetTransformSubmodule;
|
||||
@@ -14,7 +14,6 @@ namespace Ichni.RhythmGame
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
public float TimeOffset;
|
||||
public bool MaybeDeadLoop = true;
|
||||
public BaseElement_BM MatchingExportElement { get; set; } = null;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new();
|
||||
private FlexibleFloat scaleX, scaleY, scaleZ;
|
||||
#endregion
|
||||
@@ -147,15 +146,18 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public (FlexibleReturnType, bool)[] GetCurrentReturnTypes(float time)
|
||||
{
|
||||
return targetScale.GetCurrentReturnTypes(time + TimeOffset);
|
||||
(FlexibleReturnType, bool) x = scaleX.getReturnType(time);
|
||||
(FlexibleReturnType, bool) y = scaleY.getReturnType(time);
|
||||
(FlexibleReturnType, bool) z = scaleZ.getReturnType(time);
|
||||
return new (FlexibleReturnType, bool)[] { x, y, z };
|
||||
}
|
||||
|
||||
public override Vector3 getValue(float time)
|
||||
{
|
||||
Vector3 currentScale = new Vector3(
|
||||
scaleX.GetValue(time + TimeOffset),
|
||||
scaleY.GetValue(time + TimeOffset),
|
||||
scaleZ.GetValue(time + TimeOffset)
|
||||
scaleX.GetValue(time),
|
||||
scaleY.GetValue(time),
|
||||
scaleZ.GetValue(time)
|
||||
);
|
||||
((IHaveVector3Interferometer)this).ApplyVector3Interferometers(ref currentScale);
|
||||
return currentScale;
|
||||
@@ -177,15 +179,17 @@ namespace Ichni.RhythmGame
|
||||
((GameElement)targetScale).elementGuid, TimeOffset);
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
Refresh();
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
parentElement.SaveBM();
|
||||
var a = new Scale_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
var result = new Scale_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
scaleX.ConvertToBM(), scaleY.ConvertToBM(), scaleZ.ConvertToBM());
|
||||
MatchingExportElement = a;
|
||||
// 将 Tracker 自身的干涉仪效果烘焙到导出数据中
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
result.scaleX, result.scaleY, result.scaleZ);
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class SwirlTracker : AnimationBase, IBeChangeInExport, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedSwirl
|
||||
public partial class SwirlTracker : AnimationBase, IExportOverride, IHaveVector3Interferometer, IHaveTimeDurationSubmodule, ICanBeTrackedSwirl
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
private TransformSubmodule targetTransformSubmodule;
|
||||
@@ -14,7 +14,6 @@ namespace Ichni.RhythmGame
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
public float TimeOffset;
|
||||
public bool MaybeDeadLoop = true;
|
||||
public BaseElement_BM MatchingExportElement { get; set; } = null;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new();
|
||||
private FlexibleFloat eulerAngleX, eulerAngleY, eulerAngleZ;
|
||||
#endregion
|
||||
@@ -147,15 +146,18 @@ namespace Ichni.RhythmGame
|
||||
|
||||
public (FlexibleReturnType, bool)[] GetCurrentReturnTypes(float time)
|
||||
{
|
||||
return targetSwirl.GetCurrentReturnTypes(time + TimeOffset);
|
||||
(FlexibleReturnType, bool) x = eulerAngleX.getReturnType(time);
|
||||
(FlexibleReturnType, bool) y = eulerAngleY.getReturnType(time);
|
||||
(FlexibleReturnType, bool) z = eulerAngleZ.getReturnType(time);
|
||||
return new (FlexibleReturnType, bool)[] { x, y, z };
|
||||
}
|
||||
|
||||
public override Vector3 getValue(float time)
|
||||
{
|
||||
Vector3 currentEulerAngles = new Vector3(
|
||||
eulerAngleX.GetValue(time + TimeOffset),
|
||||
eulerAngleY.GetValue(time + TimeOffset),
|
||||
eulerAngleZ.GetValue(time + TimeOffset)
|
||||
eulerAngleX.GetValue(time),
|
||||
eulerAngleY.GetValue(time),
|
||||
eulerAngleZ.GetValue(time)
|
||||
);
|
||||
((IHaveVector3Interferometer)this).ApplyVector3Interferometers(ref currentEulerAngles);
|
||||
return currentEulerAngles;
|
||||
@@ -177,15 +179,17 @@ namespace Ichni.RhythmGame
|
||||
((GameElement)targetSwirl).elementGuid, TimeOffset);
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
Refresh();
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
parentElement.SaveBM();
|
||||
var a = new Swirl_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
var result = new Swirl_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
eulerAngleX.ConvertToBM(), eulerAngleY.ConvertToBM(), eulerAngleZ.ConvertToBM());
|
||||
MatchingExportElement = a;
|
||||
// 将 Tracker 自身的干涉仪效果烘焙到导出数据中
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
result.eulerAngleX, result.eulerAngleY, result.eulerAngleZ);
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -7,14 +7,13 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class Displacement : AnimationBase, IHaveVector3Interferometer, ICanBeTrackedDisplacement
|
||||
public partial class Displacement : AnimationBase, IHaveVector3Interferometer, IExportOverride, ICanBeTrackedDisplacement
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
private TransformSubmodule targetTransformSubmodule;
|
||||
public FlexibleFloat positionX, positionY, positionZ;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new List<InterferometerBase>();
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
@@ -113,14 +112,14 @@ namespace Ichni.RhythmGame
|
||||
positionX.ConvertToBM(), positionY.ConvertToBM(), positionZ.ConvertToBM());
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
(MatchingExportElement as Displacement_BM).positionX,
|
||||
(MatchingExportElement as Displacement_BM).positionY,
|
||||
(MatchingExportElement as Displacement_BM).positionZ);
|
||||
(matchedBM as Displacement_BM).positionX,
|
||||
(matchedBM as Displacement_BM).positionY,
|
||||
(matchedBM as Displacement_BM).positionZ);
|
||||
return matchedBM;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronizes this element's world position to another transform-bearing element.
|
||||
/// The target world position is converted back into this element parent's local space.
|
||||
/// </summary>
|
||||
public partial class PositionSync : AnimationBase, IExportOverride
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public TransformSubmodule targetTransformSubmodule;
|
||||
public GameElement syncTargetObject;
|
||||
public FlexibleBool enabling;
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
public static PositionSync GenerateElement(string elementName, Guid id,
|
||||
List<string> tags, bool isFirstGenerated, GameElement animatedObject,
|
||||
GameElement syncTargetObject, FlexibleBool enabling)
|
||||
{
|
||||
PositionSync sync = Instantiate(EditorManager.instance.basePrefabs.emptyObject).AddComponent<PositionSync>();
|
||||
sync.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
|
||||
sync.animatedObject = animatedObject;
|
||||
sync.syncTargetObject = syncTargetObject;
|
||||
sync.enabling = enabling;
|
||||
sync.animationReturnType = FlexibleReturnType.Before;
|
||||
sync.targetTransformSubmodule = (animatedObject as IHaveTransformSubmodule)?.transformSubmodule;
|
||||
|
||||
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, sync);
|
||||
return sync;
|
||||
}
|
||||
|
||||
public override void SetDefaultSubmodules()
|
||||
{
|
||||
timeDurationSubmodule = new TimeDurationSubmodule(this);
|
||||
}
|
||||
|
||||
public override void OnDelete()
|
||||
{
|
||||
base.OnDelete();
|
||||
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [动画更新] Animation Update
|
||||
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
|
||||
{
|
||||
base.ScheduledUpdate(phase, songTime);
|
||||
|
||||
if (phase == UpdatePhase.Effect && enabling != null && enabling.value)
|
||||
{
|
||||
ApplyPositionSync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAnimation(float songTime)
|
||||
{
|
||||
if (enabling == null)
|
||||
{
|
||||
animationReturnType = FlexibleReturnType.None;
|
||||
return;
|
||||
}
|
||||
|
||||
enabling.UpdateFlexibleBool(songTime);
|
||||
|
||||
if (enabling.value)
|
||||
{
|
||||
animationReturnType = FlexibleReturnType.MiddleExecuting;
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
}
|
||||
}
|
||||
else if (animationReturnType != FlexibleReturnType.MiddleInterval)
|
||||
{
|
||||
animationReturnType = FlexibleReturnType.MiddleInterval;
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPositionSync()
|
||||
{
|
||||
if (targetTransformSubmodule == null || syncTargetObject == null) return;
|
||||
if (syncTargetObject is not IHaveTransformSubmodule) return;
|
||||
|
||||
Transform self = targetTransformSubmodule.attachedGameElement.transform;
|
||||
Transform parent = self.parent;
|
||||
Vector3 targetWorldPosition = syncTargetObject.transform.position;
|
||||
Vector3 desiredLocalPosition = parent != null
|
||||
? parent.InverseTransformPoint(targetWorldPosition)
|
||||
: targetWorldPosition;
|
||||
Vector3 existingLocalOffset = targetTransformSubmodule.currentPosition - targetTransformSubmodule.originalPosition;
|
||||
Vector3 syncOffset = desiredLocalPosition - targetTransformSubmodule.originalPosition + existingLocalOffset;
|
||||
|
||||
targetTransformSubmodule.positionOffset += syncOffset;
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
(targetTransformSubmodule.attachedGameElement as IHaveTransformSubmodule)?.UpdateTransform(false);
|
||||
}
|
||||
|
||||
public override void ApplyTimeOffset(float offset)
|
||||
{
|
||||
base.ApplyTimeOffset(offset);
|
||||
enabling?.animations.ForEach(anim => anim.ApplyTimeOffset(offset));
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (targetTransformSubmodule != null)
|
||||
{
|
||||
targetTransformSubmodule.positionDirtyMark = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [存档与序列化] Serialize & BM
|
||||
public override void SaveBM()
|
||||
{
|
||||
matchedBM = new PositionSync_BM(elementName, elementGuid, tags, parentElement.matchedBM as GameElement_BM,
|
||||
enabling.ConvertToBM(), syncTargetObject != null ? syncTargetObject.elementGuid : Guid.Empty);
|
||||
}
|
||||
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
string message = $"PositionSync cannot be exported before baking is implemented. Element: {elementName}";
|
||||
LogWindow.Log(message, Color.red);
|
||||
Debug.LogError(message);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8e0717920cb3b547966d49d9d686146
|
||||
@@ -6,13 +6,12 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class Scale : AnimationBase, IHaveVector3Interferometer, IBeChangeInExport, ICanBeTrackedScale
|
||||
public partial class Scale : AnimationBase, IHaveVector3Interferometer, IExportOverride, ICanBeTrackedScale
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public TransformSubmodule targetTransformSubmodule;
|
||||
public FlexibleFloat scaleX, scaleY, scaleZ;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new List<InterferometerBase>();
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
#endregion
|
||||
|
||||
@@ -134,14 +133,14 @@ namespace Ichni.RhythmGame
|
||||
scaleX.ConvertToBM(), scaleY.ConvertToBM(), scaleZ.ConvertToBM());
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
(MatchingExportElement as Scale_BM).scaleX,
|
||||
(MatchingExportElement as Scale_BM).scaleY,
|
||||
(MatchingExportElement as Scale_BM).scaleZ);
|
||||
(matchedBM as Scale_BM).scaleX,
|
||||
(matchedBM as Scale_BM).scaleY,
|
||||
(matchedBM as Scale_BM).scaleZ);
|
||||
return matchedBM;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public partial class Swirl : AnimationBase, IHaveVector3Interferometer, IBeChangeInExport, ICanBeTrackedSwirl
|
||||
public partial class Swirl : AnimationBase, IHaveVector3Interferometer, IExportOverride, ICanBeTrackedSwirl
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
private TransformSubmodule targetTransformSubmodule;
|
||||
public FlexibleFloat eulerAngleX, eulerAngleY, eulerAngleZ;
|
||||
public List<InterferometerBase> Interferometers { get; set; } = new List<InterferometerBase>();
|
||||
public Vector3 PreviewValue = Vector3.zero;
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
@@ -129,14 +128,14 @@ namespace Ichni.RhythmGame
|
||||
eulerAngleX.ConvertToBM(), eulerAngleY.ConvertToBM(), eulerAngleZ.ConvertToBM());
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
SaveBM();
|
||||
MatchingExportElement = matchedBM;
|
||||
((IHaveVector3Interferometer)this).ApplyVector3InterferometersBM(
|
||||
(MatchingExportElement as Swirl_BM).eulerAngleX,
|
||||
(MatchingExportElement as Swirl_BM).eulerAngleY,
|
||||
(MatchingExportElement as Swirl_BM).eulerAngleZ);
|
||||
(matchedBM as Swirl_BM).eulerAngleX,
|
||||
(matchedBM as Swirl_BM).eulerAngleY,
|
||||
(matchedBM as Swirl_BM).eulerAngleZ);
|
||||
return matchedBM;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using UnityEngine.Events;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public class BeatmapContainer : IBaseElement, IBeChangeInExport
|
||||
public class BeatmapContainer : IBaseElement, IExportOverride
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public List<GameElement> gameElementList;
|
||||
@@ -18,7 +18,6 @@ namespace Ichni.RhythmGame
|
||||
public List<UnityAction> lowPriorityActions;
|
||||
|
||||
public BaseElement_BM matchedBM { get; set; }
|
||||
public BaseElement_BM MatchingExportElement { get; set; }
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
@@ -76,13 +75,15 @@ namespace Ichni.RhythmGame
|
||||
matchedBM = new BeatmapContainer_BM(gameElementList);
|
||||
}
|
||||
|
||||
public void SaveExportBM()
|
||||
public BaseElement_BM GetExportElement()
|
||||
{
|
||||
MatchingExportElement = new BeatmapContainer_BM(gameElementList, true);
|
||||
if (((BeatmapContainer_BM)MatchingExportElement).elementList.Any(i => i is ICanNotInExport))
|
||||
var export = new BeatmapContainer_BM(gameElementList, true);
|
||||
if (export.elementList.Any(i => i is ICanNotInExport))
|
||||
{
|
||||
Debug.LogError("Export Error!");
|
||||
Debug.LogError($"导出失败:elementList 仍包含 ICanNotInExport 元素。" +
|
||||
$"这些元素本应在 BeatmapContainer_BM 构造时被过滤。");
|
||||
}
|
||||
return export;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
".mp3" => LoadMP3(songLocation),
|
||||
".ogg" => ES3.LoadAudio(songLocation, AudioType.OGGVORBIS),
|
||||
".wav" => ES3.LoadAudio(songLocation, AudioType.WAV),
|
||||
".wav" => LoadWav(songLocation),
|
||||
_ => throw new Exception("Unsupported audio format: " + extension)
|
||||
};
|
||||
|
||||
@@ -75,6 +75,84 @@ namespace Ichni.RhythmGame
|
||||
position => { mpegFile = new MpegFile(filepath); });
|
||||
return ac;
|
||||
}
|
||||
|
||||
/// <summary>手动解码 WAV 文件为 PCM AudioClip。
|
||||
/// 支持 8/16/24/32-bit PCM 以及 32-bit IEEE float(formatTag=3)。
|
||||
/// Unity 的 AudioType.WAV 不支持 32-bit WAV(导出版本 GetData 返回全零),
|
||||
/// 所以需要自己解析 WAV 头 + 转换采样到 float PCM。</summary>
|
||||
private AudioClip LoadWav(string filepath)
|
||||
{
|
||||
string filename = Path.GetFileNameWithoutExtension(filepath);
|
||||
using var stream = File.OpenRead(filepath);
|
||||
using var reader = new BinaryReader(stream);
|
||||
|
||||
// ── RIFF 头 ──
|
||||
if (new string(reader.ReadChars(4)) != "RIFF")
|
||||
throw new Exception("Not a valid WAV file");
|
||||
reader.ReadInt32(); // file size
|
||||
if (new string(reader.ReadChars(4)) != "WAVE")
|
||||
throw new Exception("Not a valid WAV file");
|
||||
|
||||
int channels = 0, sampleRate = 0, bitsPerSample = 0;
|
||||
short formatTag = 1; // 1 = PCM, 3 = IEEE float
|
||||
float[] samples = null;
|
||||
|
||||
// ── 逐 chunk 解析 ──
|
||||
while (stream.Position < stream.Length - 8)
|
||||
{
|
||||
string chunkId = new string(reader.ReadChars(4));
|
||||
int chunkSize = reader.ReadInt32();
|
||||
long chunkEnd = stream.Position + chunkSize;
|
||||
|
||||
switch (chunkId)
|
||||
{
|
||||
case "fmt ":
|
||||
formatTag = reader.ReadInt16();
|
||||
channels = reader.ReadInt16();
|
||||
sampleRate = reader.ReadInt32();
|
||||
reader.ReadInt32(); // avgBytesPerSec
|
||||
reader.ReadInt16(); // blockAlign
|
||||
bitsPerSample = reader.ReadInt16();
|
||||
break;
|
||||
|
||||
case "data":
|
||||
int bytesPerSample = bitsPerSample / 8;
|
||||
int totalFloats = chunkSize / bytesPerSample;
|
||||
samples = new float[totalFloats];
|
||||
|
||||
for (int i = 0; i < totalFloats; i++)
|
||||
{
|
||||
if (formatTag == 3 && bitsPerSample == 32) // IEEE float
|
||||
samples[i] = reader.ReadSingle();
|
||||
else if (bitsPerSample == 32) // 32-bit PCM
|
||||
samples[i] = reader.ReadInt32() / 2147483648f;
|
||||
else if (bitsPerSample == 24) // 24-bit PCM
|
||||
{
|
||||
int s = reader.ReadByte() | (reader.ReadByte() << 8) | (reader.ReadByte() << 16);
|
||||
if ((s & 0x800000) != 0) s |= ~0xFFFFFF; // sign extend
|
||||
samples[i] = s / 8388607f;
|
||||
}
|
||||
else if (bitsPerSample == 16) // 16-bit PCM
|
||||
samples[i] = reader.ReadInt16() / 32767f;
|
||||
else if (bitsPerSample == 8) // 8-bit PCM (unsigned)
|
||||
samples[i] = (reader.ReadByte() - 128) / 128f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 跳到 chunk 末尾(含 word alignment padding)
|
||||
stream.Position = chunkEnd;
|
||||
if ((chunkSize & 1) != 0) stream.Position++;
|
||||
}
|
||||
|
||||
if (samples == null)
|
||||
throw new Exception("No data chunk found in WAV file");
|
||||
|
||||
int totalSamples = samples.Length / channels;
|
||||
AudioClip ac = AudioClip.Create(filename, totalSamples, channels, sampleRate, false);
|
||||
ac.SetData(samples, 0);
|
||||
return ac;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [编辑器界面] Inspector
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Ichni.RhythmGame
|
||||
{ "Vignette", () => new VignetteEffect(1, 1, 0.4f, Color.black, CustomCurvePresets.Parabolic(1, 0, 1)) },
|
||||
{ "SetInteger", () => new SetIntegerEffect("New Variable", 0, false, 0, 1) },
|
||||
{ "EnableControl", () => new EnableControlEffect(null, "New Variable", 0, false, "") },
|
||||
{ "Pixelate", () => new PixelateEffect(1, 320, 180, CustomCurvePresets.Instant()) },
|
||||
{ "Pixelate", () => new PixelateEffect(1, 1f / 6f, 1f / 6f, CustomCurvePresets.Instant()) },
|
||||
{ "LowPassFilter", () => new LowPassFilterEffect(1, 20, true, CustomCurvePresets.Parabolic(1, 0, 1)) },
|
||||
{ "HighPassFilter", () => new HighPassFilterEffect(1, 20000, true, CustomCurvePresets.Parabolic(1, 0, 1)) },
|
||||
{ "SpeedLines", () => new SpeedLinesEffect(1f, CustomCurvePresets.Parabolic(1, 0, 1), new Color(1f, 1f, 1f, 0.5f), peakRemap: 0f) },
|
||||
@@ -205,6 +205,7 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
if (nowEffectState == EffectState.Before)
|
||||
{
|
||||
PostProcessingManager.EnsureActiveCameraPostProcessing();
|
||||
PreExecute();
|
||||
}
|
||||
|
||||
@@ -294,4 +295,4 @@ namespace Ichni.RhythmGame
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using SLSUtilities.Rendering.PostProcessing;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
@@ -10,7 +11,7 @@ namespace Ichni.RhythmGame
|
||||
public float peak;
|
||||
public AnimationCurve intensityCurve;
|
||||
|
||||
private Bloom _bloom;
|
||||
private AnimeBloom _bloom;
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
@@ -36,6 +37,7 @@ namespace Ichni.RhythmGame
|
||||
if (_bloom == null && PostProcessingManager.GlobalVolume != null)
|
||||
{
|
||||
PostProcessingManager.GlobalVolume.profile.TryGet(out _bloom);
|
||||
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Ichni.RhythmGame
|
||||
public partial class PixelateEffect : EffectBase
|
||||
{
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public float bottomX;
|
||||
public float bottomY;
|
||||
[Range(0f, 1f)] public float bottomX;
|
||||
[Range(0f, 1f)] public float bottomY;
|
||||
public AnimationCurve intensityCurve;
|
||||
|
||||
private PixelateVolume _pixelateVolume;
|
||||
@@ -18,8 +18,8 @@ namespace Ichni.RhythmGame
|
||||
public PixelateEffect(float duration, float bottomX, float bottomY, AnimationCurve intensityCurve)
|
||||
: base(duration)
|
||||
{
|
||||
this.bottomX = bottomX;
|
||||
this.bottomY = bottomY;
|
||||
this.bottomX = Mathf.Clamp01(bottomX);
|
||||
this.bottomY = Mathf.Clamp01(bottomY);
|
||||
this.intensityCurve = intensityCurve;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ namespace Ichni.RhythmGame
|
||||
if (other is PixelateEffect otherEffect)
|
||||
{
|
||||
this.effectTime = otherEffect.effectTime;
|
||||
this.bottomX = otherEffect.bottomX;
|
||||
this.bottomY = otherEffect.bottomY;
|
||||
this.bottomX = Mathf.Clamp01(otherEffect.bottomX);
|
||||
this.bottomY = Mathf.Clamp01(otherEffect.bottomY);
|
||||
this.intensityCurve = otherEffect.intensityCurve != null ? new AnimationCurve(otherEffect.intensityCurve.keys) : null;
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,8 @@ namespace Ichni.RhythmGame
|
||||
if (_pixelateVolume != null)
|
||||
{
|
||||
_pixelateVolume.forceActive.value = true;
|
||||
_pixelateVolume.strengthX.value = Screen.width;
|
||||
_pixelateVolume.strengthY.value = Screen.height;
|
||||
_pixelateVolume.strengthX.value = 1f;
|
||||
_pixelateVolume.strengthY.value = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ namespace Ichni.RhythmGame
|
||||
{
|
||||
if (_pixelateVolume != null)
|
||||
{
|
||||
float x = Mathf.Lerp(Screen.width, bottomX, intensityCurve.Evaluate(effectProgressPercent));
|
||||
float y = Mathf.Lerp(Screen.height, bottomY, intensityCurve.Evaluate(effectProgressPercent));
|
||||
float progress = intensityCurve == null ? effectProgressPercent : intensityCurve.Evaluate(effectProgressPercent);
|
||||
float x = Mathf.Lerp(1f, Mathf.Clamp01(bottomX), progress);
|
||||
float y = Mathf.Lerp(1f, Mathf.Clamp01(bottomY), progress);
|
||||
_pixelateVolume.strengthX.value = x;
|
||||
_pixelateVolume.strengthY.value = y;
|
||||
}
|
||||
@@ -75,8 +76,8 @@ namespace Ichni.RhythmGame
|
||||
if (_pixelateVolume != null)
|
||||
{
|
||||
_pixelateVolume.forceActive.value = false;
|
||||
_pixelateVolume.strengthX.value = Screen.width;
|
||||
_pixelateVolume.strengthY.value = Screen.height;
|
||||
_pixelateVolume.strengthX.value = 1f;
|
||||
_pixelateVolume.strengthY.value = 1f;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user