Compare commits
10
Commits
2b2a74da42
...
be96b326de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be96b326de | ||
|
|
750baeb219 | ||
|
|
15df36659a | ||
|
|
f1fc356d99 | ||
|
|
2a6a0b1861 | ||
|
|
2dcb7a961a | ||
|
|
d25f237e76 | ||
|
|
f264329751 | ||
|
|
1b37297585 | ||
|
|
458f344e83 |
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: no-game-references
|
||||||
|
description: '编写或修改 C# 代码时加载此规则:确保命名、注释、Tooltip、Header 中不出现其他游戏的专有名称(如 HK / Hollow Knight / Silksong / Ori 等)或明确引用。触发场景:新建 .cs 文件、修改已有 .cs 文件、Code Review .cs 代码是否合规。'
|
||||||
|
---
|
||||||
|
|
||||||
|
# 代码命名与注释:禁止引用其他游戏
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
在编写或修改任何 C# 代码时,**命名(变量名、方法名、类名)、注释(`//`、`///`)、`[Tooltip]`、`[Header]` 字段标签** 中均不得出现以下内容:
|
||||||
|
|
||||||
|
- 其他游戏的**专有名称**(如:空洞骑士、丝之歌、奥日,以及对应英文 Hollow Knight、HK、Silksong、Ori 等)
|
||||||
|
- 其他游戏中的**专属机制/道具名称**(如:Mothwing Cloak、Monarch Wings、暗影披风 等)
|
||||||
|
- "对齐 XX 游戏"、"仿 XX 风格"、"参考 XX"、"类比 XX"等措辞中明确出现游戏名
|
||||||
|
|
||||||
|
## 正确做法:描述功能特点
|
||||||
|
|
||||||
|
若某设计参考了其他游戏的机制,**只描述该机制的功能特点**,不写出游戏名。
|
||||||
|
|
||||||
|
| ❌ 禁止 | ✅ 替换为 |
|
||||||
|
|---|---|
|
||||||
|
| `// 对齐空洞骑士手感` | `// 下落比上升更快,手感紧实` |
|
||||||
|
| `[Header("跳跃(对齐空洞骑士手感)")]` | `[Header("跳跃")]` |
|
||||||
|
| `[Tooltip("HK ~0.12s")]` | `[Tooltip("推荐 0.12s")]` |
|
||||||
|
| `// 类比 HK Monarch Wings,随时可二段跳` | `// 上升阶段即可触发二段跳` |
|
||||||
|
| `// 仿丝之歌风格 Lookahead` | `// 速度自适应 Lookahead:速度越快,预测距离越大` |
|
||||||
|
| `/// 模拟空洞骑士的标志性相机手感` | `/// 实现下落快、起跳缓的非对称相机追随手感` |
|
||||||
|
| `// 对齐 HK:碰到实体墙不会压墙卡住` | `// 碰到实体墙立即中止,避免压墙卡住` |
|
||||||
|
| `private void ApplyHKComposerDefaults(...)` | `private void ApplyComposerDefaults(...)` |
|
||||||
|
| `[Tooltip("对齐空洞骑士 = 1 格血")]` | `[Tooltip("建议设为 1,确保任意触碰即扣一格血量")]` |
|
||||||
|
|
||||||
|
## 允许保留的通用术语
|
||||||
|
|
||||||
|
以下术语为业界通用设计词汇,**不视为游戏专名**,可正常使用:
|
||||||
|
|
||||||
|
- `Pogo` / `pogo`(踩踏弹跳,通用机制术语)
|
||||||
|
- `Charm`(可收集护符,本项目自有道具类型)
|
||||||
|
- `platformer`、`横版平台游戏`(泛指游戏类型)
|
||||||
|
- `Metroidvania`(泛指游戏类型)
|
||||||
|
- `手感`、`风格`(描述本项目自身功能的通用词)
|
||||||
@@ -45,6 +45,7 @@ MonoBehaviour:
|
|||||||
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
|
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
|
||||||
m_GroupAssets:
|
m_GroupAssets:
|
||||||
- {fileID: 11400000, guid: c22627c324f1c25498607e9b6e157457, type: 2}
|
- {fileID: 11400000, guid: c22627c324f1c25498607e9b6e157457, type: 2}
|
||||||
|
- {fileID: 11400000, guid: dd7101d419030164a8916786fa463075, type: 2}
|
||||||
- {fileID: 11400000, guid: 0123a97ef69d06c429118db4ea81ab47, type: 2}
|
- {fileID: 11400000, guid: 0123a97ef69d06c429118db4ea81ab47, type: 2}
|
||||||
m_BuildSettings:
|
m_BuildSettings:
|
||||||
m_CompileScriptsInVirtualMode: 0
|
m_CompileScriptsInVirtualMode: 0
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||||
|
m_Name: Scenes
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
m_GroupName: Scenes
|
||||||
|
m_Data:
|
||||||
|
m_SerializedData: []
|
||||||
|
m_GUID: bb063dcc2b7a95445ba989917306f188
|
||||||
|
m_SerializeEntries:
|
||||||
|
- m_GUID: 6372e5b8e07d7ae4eb37a184fc8e912d
|
||||||
|
m_Address: Scene_Persistent
|
||||||
|
m_ReadOnly: 0
|
||||||
|
m_SerializedLabels: []
|
||||||
|
FlaggedDuringContentUpdateRestriction: 0
|
||||||
|
m_ReadOnly: 0
|
||||||
|
m_Settings: {fileID: 11400000, guid: ea6262c4e52d79d41ab2c167b19171ff, type: 2}
|
||||||
|
m_SchemaSet:
|
||||||
|
m_Schemas:
|
||||||
|
- {fileID: 11400000, guid: dee8776f24b4f904a8a61b38c06a59a6, type: 2}
|
||||||
|
- {fileID: 11400000, guid: bb24fcbbf8d393540a320bb108625c55, type: 2}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dd7101d419030164a8916786fa463075
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||||
|
m_Name: Scenes_BundledAssetGroupSchema
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
m_Group: {fileID: 11400000, guid: dd7101d419030164a8916786fa463075, type: 2}
|
||||||
|
m_InternalBundleIdMode: 1
|
||||||
|
m_Compression: 1
|
||||||
|
m_IncludeAddressInCatalog: 1
|
||||||
|
m_IncludeGUIDInCatalog: 1
|
||||||
|
m_IncludeLabelsInCatalog: 1
|
||||||
|
m_InternalIdNamingMode: 0
|
||||||
|
m_CacheClearBehavior: 0
|
||||||
|
m_IncludeInBuild: 1
|
||||||
|
m_BundledAssetProviderType:
|
||||||
|
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||||
|
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||||
|
m_ForceUniqueProvider: 0
|
||||||
|
m_UseAssetBundleCache: 1
|
||||||
|
m_UseAssetBundleCrc: 1
|
||||||
|
m_UseAssetBundleCrcForCachedBundles: 1
|
||||||
|
m_UseUWRForLocalBundles: 0
|
||||||
|
m_Timeout: 0
|
||||||
|
m_ChunkedTransfer: 0
|
||||||
|
m_RedirectLimit: -1
|
||||||
|
m_RetryCount: 0
|
||||||
|
m_BuildPath:
|
||||||
|
m_Id: 768ef6c5b40cf3841a6e6188ed781ca3
|
||||||
|
m_LoadPath:
|
||||||
|
m_Id: a9ed4dec0ec20c2459505631f5a924dd
|
||||||
|
m_BundleMode: 0
|
||||||
|
m_AssetBundleProviderType:
|
||||||
|
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||||
|
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||||
|
m_BundleNaming: 0
|
||||||
|
m_AssetLoadMode: 0
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dee8776f24b4f904a8a61b38c06a59a6
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||||
|
m_Name: Scenes_ContentUpdateGroupSchema
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
m_Group: {fileID: 11400000, guid: dd7101d419030164a8916786fa463075, type: 2}
|
||||||
|
m_StaticContent: 0
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bb24fcbbf8d393540a320bb108625c55
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 16
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 18bd840b706853d4a934ec3199f63a41, type: 3}
|
||||||
|
m_Name: Animancer Settings
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
_Data:
|
||||||
|
- rid: 8370433886431215616
|
||||||
|
- rid: 8370433886431215617
|
||||||
|
- rid: 8370433886431215618
|
||||||
|
- rid: 8370433886431215619
|
||||||
|
- rid: 8370433886431215620
|
||||||
|
- rid: 8370433886431215621
|
||||||
|
references:
|
||||||
|
version: 2
|
||||||
|
RefIds:
|
||||||
|
- rid: 8370433886431215616
|
||||||
|
type: {class: AnimancerComponentPreviewSettings, ns: Animancer.Editor.Previews, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
_RepaintRate: 30
|
||||||
|
- rid: 8370433886431215617
|
||||||
|
type: {class: AnimancerGraphControls, ns: Animancer.Editor, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
_FrameStep: 0.02
|
||||||
|
- rid: 8370433886431215618
|
||||||
|
type: {class: TransitionPreviewSettings, ns: Animancer.Editor.Previews, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
_AutoClose: 1
|
||||||
|
_SceneLighting: 0
|
||||||
|
_ShowSkybox: 0
|
||||||
|
_FrameStep: 0.02
|
||||||
|
_SceneEnvironment: {fileID: 0}
|
||||||
|
_Models: []
|
||||||
|
- rid: 8370433886431215619
|
||||||
|
type: {class: SerializableEventSequenceDrawerSettings, ns: Animancer.Editor, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
_HideEventCallbacks: 0
|
||||||
|
- rid: 8370433886431215620
|
||||||
|
type: {class: AnimationTimeAttributeSettings, ns: Animancer.Units.Editor, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
showApproximations: 1
|
||||||
|
showNormalized: 1
|
||||||
|
showSeconds: 1
|
||||||
|
showFrames: 1
|
||||||
|
- rid: 8370433886431215621
|
||||||
|
type: {class: GenerateSpriteAnimationsSettings, ns: Animancer.Editor.Tools, asm: Kybernetik.Animancer.Editor}
|
||||||
|
data:
|
||||||
|
_FrameRate: 12
|
||||||
|
_HierarchyPath:
|
||||||
|
_TargetType:
|
||||||
|
_QualifiedName: UnityEngine.SpriteRenderer, UnityEngine.CoreModule, Version=0.0.0.0,
|
||||||
|
Culture=neutral, PublicKeyToken=null
|
||||||
|
_PropertyName: m_Sprite
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3415867d5a98b1440b2a492fb6b02c78
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6a0880f172d9d6846bd2e98118d3803c
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 5ec15df6b0d345c4f92ba459e89dc02f, type: 3}
|
|
||||||
m_Name: PLY_PlayerAnimationConfig
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
Idle: {fileID: 0}
|
|
||||||
Run: {fileID: 0}
|
|
||||||
Jump: {fileID: 0}
|
|
||||||
Fall: {fileID: 0}
|
|
||||||
Dash: {fileID: 0}
|
|
||||||
WallSlide: {fileID: 0}
|
|
||||||
Hurt: {fileID: 0}
|
|
||||||
Dead: {fileID: 0}
|
|
||||||
UseSpring: {fileID: 0}
|
|
||||||
GroundAttacks: []
|
|
||||||
AirAttack: {fileID: 0}
|
|
||||||
UpAttack: {fileID: 0}
|
|
||||||
DownAttack: {fileID: 0}
|
|
||||||
ParryStart: {fileID: 0}
|
|
||||||
ParrySuccess: {fileID: 0}
|
|
||||||
-18
@@ -9,7 +9,6 @@ GameObject:
|
|||||||
serializedVersion: 6
|
serializedVersion: 6
|
||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 6453743960439743923}
|
- component: {fileID: 6453743960439743923}
|
||||||
- component: {fileID: 8467938703123916945}
|
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: MMDefaultPostProcessingVolume
|
m_Name: MMDefaultPostProcessingVolume
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -32,20 +31,3 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!114 &8467938703123916945
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 5003270845818652397}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 8b9a305e18de0c04dbd257a21cd47087, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
sharedProfile: {fileID: 11400000, guid: c1bea42a5942a4c09a3ecd393f6054aa, type: 2}
|
|
||||||
isGlobal: 1
|
|
||||||
blendDistance: 0
|
|
||||||
weight: 1
|
|
||||||
priority: 0
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2d7a6732ad4a5b94cb233e39690bfec4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b52e0d522f65164499cf8492ceb64ba1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c58e502017a33d04bb7c64ae8348898d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace LWGUI.CustomGUISample
|
||||||
|
{
|
||||||
|
public static class CustomFooter
|
||||||
|
{
|
||||||
|
public static void DoCustomFooter(LWGUI lwgui)
|
||||||
|
{
|
||||||
|
// Draw your custom gui...
|
||||||
|
|
||||||
|
// Debug.Log(lwgui.shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[InitializeOnLoadMethod]
|
||||||
|
private static void RegisterEvent()
|
||||||
|
{
|
||||||
|
LWGUI.onDrawCustomFooter += DoCustomFooter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 421a56cca25d486298053d5eeca0c669
|
||||||
|
timeCreated: 1688612200
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace LWGUI.CustomGUISample
|
||||||
|
{
|
||||||
|
public static class CustomHeader
|
||||||
|
{
|
||||||
|
public static void DoCustomHeader(LWGUI lwgui)
|
||||||
|
{
|
||||||
|
// Draw your custom gui...
|
||||||
|
|
||||||
|
// Debug.Log(lwgui.shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[InitializeOnLoadMethod]
|
||||||
|
private static void RegisterEvent()
|
||||||
|
{
|
||||||
|
LWGUI.onDrawCustomHeader += DoCustomHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 12f48d96821d4f33aa8d9a7618a9ae0b
|
||||||
|
timeCreated: 1688613396
|
||||||
@@ -0,0 +1,934 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Misc Function
|
||||||
|
/// </summary>
|
||||||
|
public class Helper
|
||||||
|
{
|
||||||
|
#region Engine Misc
|
||||||
|
|
||||||
|
public static void ObsoleteWarning(string obsoleteStr, string newStr)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("'" + obsoleteStr + "' is Obsolete! Please use '" + newStr + "'!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool PropertyValueEquals(MaterialProperty prop1, MaterialProperty prop2)
|
||||||
|
{
|
||||||
|
if (prop1.textureValue == prop2.textureValue
|
||||||
|
&& prop1.vectorValue == prop2.vectorValue
|
||||||
|
&& prop1.colorValue == prop2.colorValue
|
||||||
|
&& prop1.floatValue == prop2.floatValue
|
||||||
|
#if UNITY_2021_1_OR_NEWER
|
||||||
|
&& prop1.intValue == prop2.intValue
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsPropertyHideInInspector(MaterialProperty prop)
|
||||||
|
{
|
||||||
|
return (prop.flags & MaterialProperty.PropFlags.HideInInspector) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetKeyWord(string keyWord, string propName)
|
||||||
|
{
|
||||||
|
string k;
|
||||||
|
if (string.IsNullOrEmpty(keyWord) || keyWord == "__")
|
||||||
|
{
|
||||||
|
k = propName.ToUpperInvariant() + "_ON";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
k = keyWord.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetShaderKeyWord(Object[] materials, string keyWord, bool isEnable)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keyWord) || string.IsNullOrEmpty(keyWord)) return;
|
||||||
|
|
||||||
|
foreach (Material m in materials)
|
||||||
|
{
|
||||||
|
// delete "_" keywords
|
||||||
|
if (keyWord == "_")
|
||||||
|
{
|
||||||
|
if (m.IsKeywordEnabled(keyWord))
|
||||||
|
{
|
||||||
|
m.DisableKeyword(keyWord);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.IsKeywordEnabled(keyWord))
|
||||||
|
{
|
||||||
|
if (!isEnable) m.DisableKeyword(keyWord);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (isEnable) m.EnableKeyword(keyWord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetShaderKeyWord(Object[] materials, string[] keyWords, int index)
|
||||||
|
{
|
||||||
|
Debug.Assert(keyWords.Length >= 1 && index < keyWords.Length && index >= 0,
|
||||||
|
"KeyWords Length: " + keyWords.Length + " or Index: " + index + " Error! ");
|
||||||
|
for (int i = 0; i < keyWords.Length; i++)
|
||||||
|
{
|
||||||
|
SetShaderKeyWord(materials, keyWords[i], index == i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetShaderPassEnabled(Object[] materials, string[] lightModeNames, bool enabled)
|
||||||
|
{
|
||||||
|
if (lightModeNames.Length == 0) return;
|
||||||
|
|
||||||
|
foreach (Material material in materials)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < lightModeNames.Length; i++)
|
||||||
|
{
|
||||||
|
material.SetShaderPassEnabled(lightModeNames[i], enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// make Drawer can get all current Material props by customShaderGUI
|
||||||
|
/// Unity 2019.2+
|
||||||
|
/// </summary>
|
||||||
|
public static LWGUI GetLWGUI(MaterialEditor editor)
|
||||||
|
{
|
||||||
|
var customShaderGUI = ReflectionHelper.GetCustomShaderGUI(editor);
|
||||||
|
if (customShaderGUI != null && customShaderGUI is LWGUI)
|
||||||
|
{
|
||||||
|
LWGUI gui = customShaderGUI as LWGUI;
|
||||||
|
return gui;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("Please add \"CustomEditor \"LWGUI.LWGUI\"\" to the end of your shader!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AdaptiveFieldWidth(GUIStyle style, GUIContent content, float extraWidth = 0)
|
||||||
|
{
|
||||||
|
var extraTextWidth = Mathf.Max(0, style.CalcSize(content).x + extraWidth - EditorGUIUtility.fieldWidth);
|
||||||
|
EditorGUIUtility.labelWidth -= extraTextWidth;
|
||||||
|
EditorGUIUtility.fieldWidth += extraTextWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void BeginProperty(Rect rect, MaterialProperty property, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
#if UNITY_2022_1_OR_NEWER
|
||||||
|
MaterialEditor.BeginProperty(rect, property);
|
||||||
|
foreach (var extraPropName in lwgui.perShaderData.propertyDatas[property.name].extraPropNames)
|
||||||
|
MaterialEditor.BeginProperty(rect, lwgui.perFrameData.propertyDatas[extraPropName].property);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EndProperty(LWGUI lwgui, MaterialProperty property)
|
||||||
|
{
|
||||||
|
#if UNITY_2022_1_OR_NEWER
|
||||||
|
MaterialEditor.EndProperty();
|
||||||
|
foreach (var extraPropName in lwgui.perShaderData.propertyDatas[property.name].extraPropNames)
|
||||||
|
MaterialEditor.EndProperty();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool EndChangeCheck(LWGUI lwgui, MaterialProperty property)
|
||||||
|
{
|
||||||
|
return lwgui.perFrameData.EndChangeCheck(property.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Math
|
||||||
|
|
||||||
|
public static float PowPreserveSign(float f, float p)
|
||||||
|
{
|
||||||
|
float num = Mathf.Pow(Mathf.Abs(f), p);
|
||||||
|
if ((double)f < 0.0)
|
||||||
|
return -num;
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region GUI Styles
|
||||||
|
|
||||||
|
// Tips: Use properties to fix null reference errors
|
||||||
|
|
||||||
|
private static GUIStyle _guiStyles_IconButton;
|
||||||
|
public static GUIStyle guiStyles_IconButton
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_guiStyles_IconButton == null)
|
||||||
|
{
|
||||||
|
_guiStyles_IconButton = new GUIStyle(
|
||||||
|
#if UNITY_2021_2_OR_NEWER
|
||||||
|
EditorStyles.iconButton
|
||||||
|
#else
|
||||||
|
"iconButton"
|
||||||
|
#endif
|
||||||
|
) { fixedHeight = 0, fixedWidth = 0 };
|
||||||
|
}
|
||||||
|
return _guiStyles_IconButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _guiStyle_Foldout;
|
||||||
|
|
||||||
|
public static GUIStyle guiStyle_Foldout
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_guiStyle_Foldout == null)
|
||||||
|
{
|
||||||
|
_guiStyle_Foldout =
|
||||||
|
new GUIStyle(EditorStyles.miniButton)
|
||||||
|
{
|
||||||
|
contentOffset = new Vector2(22, 0),
|
||||||
|
fixedHeight = 27,
|
||||||
|
alignment = TextAnchor.MiddleLeft,
|
||||||
|
font = EditorStyles.boldLabel.font,
|
||||||
|
fontSize = EditorStyles.boldLabel.fontSize
|
||||||
|
#if UNITY_2019_4_OR_NEWER
|
||||||
|
+ 1,
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _guiStyle_Foldout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _guiStyle_Helpbox;
|
||||||
|
|
||||||
|
public static GUIStyle guiStyle_Helpbox
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_guiStyle_Helpbox == null)
|
||||||
|
{
|
||||||
|
_guiStyle_Helpbox = new GUIStyle(EditorStyles.helpBox) { fontSize = 12 };
|
||||||
|
}
|
||||||
|
return _guiStyle_Helpbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _guiStyles_ToolbarSearchTextFieldPopup;
|
||||||
|
public static GUIStyle guiStyles_ToolbarSearchTextFieldPopup
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_guiStyles_ToolbarSearchTextFieldPopup == null)
|
||||||
|
{
|
||||||
|
string toolbarSeachTextFieldPopupStr = "ToolbarSeachTextFieldPopup";
|
||||||
|
{
|
||||||
|
// ToolbarSeachTextFieldPopup has renamed at Unity 2021.3.28+
|
||||||
|
#if !UNITY_2022_3_OR_NEWER
|
||||||
|
string[] versionParts = Application.unityVersion.Split('.');
|
||||||
|
int majorVersion = int.Parse(versionParts[0]);
|
||||||
|
int minorVersion = int.Parse(versionParts[1]);
|
||||||
|
Match patchVersionMatch = Regex.Match(versionParts[2], @"\d+");
|
||||||
|
int patchVersion = int.Parse(patchVersionMatch.Value);
|
||||||
|
if (majorVersion >= 2021 && minorVersion >= 3 && patchVersion >= 28)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
toolbarSeachTextFieldPopupStr = "ToolbarSearchTextFieldPopup";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_guiStyles_ToolbarSearchTextFieldPopup = new GUIStyle(toolbarSeachTextFieldPopupStr);
|
||||||
|
}
|
||||||
|
return _guiStyles_ToolbarSearchTextFieldPopup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Draw GUI for Drawer
|
||||||
|
|
||||||
|
// TODO: use Reflection
|
||||||
|
// copy and edit of https://github.com/GucioDevs/SimpleMinMaxSlider/blob/master/Assets/SimpleMinMaxSlider/Scripts/Editor/MinMaxSliderDrawer.cs
|
||||||
|
public static Rect[] SplitRect(Rect rectToSplit, int n)
|
||||||
|
{
|
||||||
|
Rect[] rects = new Rect[n];
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
rects[i] = new Rect(rectToSplit.position.x + (i * rectToSplit.width / n), rectToSplit.position.y,
|
||||||
|
rectToSplit.width / n, rectToSplit.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
int padding = (int)rects[0].width - 50; // use 50, enough to show 0.xx (2 digits)
|
||||||
|
int space = 5;
|
||||||
|
|
||||||
|
rects[0].width -= padding + space;
|
||||||
|
rects[2].width -= padding + space;
|
||||||
|
|
||||||
|
rects[1].x -= padding;
|
||||||
|
rects[1].width += padding * 2;
|
||||||
|
|
||||||
|
rects[2].x += padding + space;
|
||||||
|
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool DrawFoldout(Rect rect, ref bool isFolding, bool toggleValue, bool hasToggle, GUIContent label)
|
||||||
|
{
|
||||||
|
var toggleRect = new Rect(rect.x + 8f, rect.y + 7f, 13f, 13f);
|
||||||
|
|
||||||
|
// Toggle Event
|
||||||
|
if (hasToggle)
|
||||||
|
{
|
||||||
|
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && toggleRect.Contains(Event.current.mousePosition))
|
||||||
|
{
|
||||||
|
toggleValue = !toggleValue;
|
||||||
|
Event.current.Use();
|
||||||
|
GUI.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button
|
||||||
|
{
|
||||||
|
// Cancel Right Click
|
||||||
|
if (Event.current.type == EventType.MouseDown && Event.current.button == 1 && rect.Contains(Event.current.mousePosition))
|
||||||
|
Event.current.Use();
|
||||||
|
|
||||||
|
var enabled = GUI.enabled;
|
||||||
|
GUI.enabled = true;
|
||||||
|
var guiColor = GUI.backgroundColor;
|
||||||
|
GUI.backgroundColor = isFolding ? Color.white : new Color(0.85f, 0.85f, 0.85f);
|
||||||
|
if (GUI.Button(rect, label, guiStyle_Foldout))
|
||||||
|
{
|
||||||
|
isFolding = !isFolding;
|
||||||
|
GUI.changed = false;
|
||||||
|
}
|
||||||
|
GUI.backgroundColor = guiColor;
|
||||||
|
GUI.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Icon
|
||||||
|
if (hasToggle)
|
||||||
|
{
|
||||||
|
EditorGUI.Toggle(toggleRect, string.Empty, toggleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toggleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Draw GUI for Material
|
||||||
|
|
||||||
|
public static void DrawSplitLine()
|
||||||
|
{
|
||||||
|
var rect = EditorGUILayout.GetControlRect(true, 1);
|
||||||
|
rect.x = 0;
|
||||||
|
rect.width = EditorGUIUtility.currentViewWidth;
|
||||||
|
EditorGUI.DrawRect(rect, new Color(0, 0, 0, 0.45f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Texture2D _helpboxIcon = EditorGUIUtility.IconContent("console.infoicon").image as Texture2D;
|
||||||
|
|
||||||
|
public static void DrawHelpbox(PropertyStaticData propertyStaticData, PropertyDynamicData propertyDynamicData)
|
||||||
|
{
|
||||||
|
var helpboxStr = propertyStaticData.helpboxMessages;
|
||||||
|
if (!string.IsNullOrEmpty(helpboxStr))
|
||||||
|
{
|
||||||
|
var content = new GUIContent(helpboxStr, _helpboxIcon);
|
||||||
|
var helpboxRect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(true, guiStyle_Helpbox.CalcHeight(content, EditorGUIUtility.currentViewWidth)));
|
||||||
|
helpboxRect.xMax -= RevertableHelper.revertButtonWidth;
|
||||||
|
GUI.Label(helpboxRect, content, guiStyle_Helpbox);
|
||||||
|
// EditorGUI.HelpBox(helpboxRect, helpboxStr, MessageType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Texture _logo = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("26b9d845eb7b1a747bf04dc84e5bcc2c"));
|
||||||
|
private static GUIContent _logoGuiContent = new GUIContent(string.Empty, _logo,
|
||||||
|
"LWGUI (Light Weight Shader GUI)\n\n"
|
||||||
|
+ "A Lightweight, Flexible, Powerful Unity Shader GUI system.\n\n"
|
||||||
|
+ "Copyright (c) Jason Ma");
|
||||||
|
|
||||||
|
public static void DrawLogo()
|
||||||
|
{
|
||||||
|
var logoRect = EditorGUILayout.GetControlRect(false, _logo.height);
|
||||||
|
var w = logoRect.width;
|
||||||
|
logoRect.xMin += w * 0.5f - _logo.width * 0.5f;
|
||||||
|
logoRect.xMax -= w * 0.5f - _logo.width * 0.5f;
|
||||||
|
|
||||||
|
if (EditorGUIUtility.currentViewWidth >= logoRect.width && GUI.Button(logoRect, _logoGuiContent, guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
Application.OpenURL("https://github.com/JasonMa0012/LWGUI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Toolbar Buttons
|
||||||
|
private static Material _copiedMaterial;
|
||||||
|
private static List<string> _copiedProps = new List<string>();
|
||||||
|
|
||||||
|
private static Texture _iconCopy = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("9cdef444d18d2ce4abb6bbc4fed4d109"));
|
||||||
|
private static Texture _iconPaste = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("8e7a78d02e4c3574998524a0842a8ccb"));
|
||||||
|
private static Texture _iconSelect = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("6f44e40b24300974eb607293e4224ecc"));
|
||||||
|
private static Texture _iconCheckout = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("72488141525eaa8499e65e52755cb6d0"));
|
||||||
|
private static Texture _iconExpand = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("2382450e7f4ddb94c9180d6634c41378"));
|
||||||
|
private static Texture _iconCollapse = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("929b6e5dfacc42b429d715a3e1ca2b57"));
|
||||||
|
private static Texture _iconVisibility = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("9576e23a695b35d49a9fc55c9a948b4f"));
|
||||||
|
|
||||||
|
private static GUIContent _guiContentCopy = new GUIContent("", _iconCopy, "Copy Material Properties");
|
||||||
|
private static GUIContent _guiContentPaste = new GUIContent("", _iconPaste, "Paste Material Properties\n\nRight-click to paste values by type.");
|
||||||
|
private static GUIContent _guiContentSelect = new GUIContent("", _iconSelect, "Select the Material Asset\n\nUsed to jump from a Runtime Material Instance to a Material Asset.");
|
||||||
|
private static GUIContent _guiContentChechout = new GUIContent("", _iconCheckout, "Checkout selected Material Assets");
|
||||||
|
private static GUIContent _guiContentExpand = new GUIContent("", _iconExpand, "Expand All Groups");
|
||||||
|
private static GUIContent _guiContentCollapse = new GUIContent("", _iconCollapse, "Collapse All Groups");
|
||||||
|
private static GUIContent _guiContentVisibility = new GUIContent("", _iconVisibility, "Display Mode");
|
||||||
|
|
||||||
|
private static string[] _materialInstanceNameEnd = new[] { "_Instantiated", " (Instance)" };
|
||||||
|
|
||||||
|
private enum CopyMaterialValueMask
|
||||||
|
{
|
||||||
|
Float = 1 << 0,
|
||||||
|
Vector = 1 << 1,
|
||||||
|
Texture = 1 << 2,
|
||||||
|
Keyword = 1 << 3,
|
||||||
|
RenderQueue = 1 << 4,
|
||||||
|
Number = Float | Vector,
|
||||||
|
All = (1 << 5) - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIContent[] _pasteMaterialMenus = new[]
|
||||||
|
{
|
||||||
|
new GUIContent("Paste Number Values"),
|
||||||
|
new GUIContent("Paste Texture Values"),
|
||||||
|
new GUIContent("Paste Keywords"),
|
||||||
|
new GUIContent("Paste RenderQueue"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static uint[] _pasteMaterialMenuValueMasks = new[]
|
||||||
|
{
|
||||||
|
(uint)CopyMaterialValueMask.Number,
|
||||||
|
(uint)CopyMaterialValueMask.Texture,
|
||||||
|
(uint)CopyMaterialValueMask.Keyword,
|
||||||
|
(uint)CopyMaterialValueMask.RenderQueue,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void DoPasteMaterialProperties(LWGUI lwgui , uint valueMask)
|
||||||
|
{
|
||||||
|
if (!_copiedMaterial)
|
||||||
|
{
|
||||||
|
Debug.LogError("Please copy Material Properties first!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (Material material in lwgui.materialEditor.targets)
|
||||||
|
{
|
||||||
|
if (!VersionControlHelper.Checkout(material))
|
||||||
|
{
|
||||||
|
Debug.LogError("Material: '" + lwgui.material.name + "' unable to write!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Undo.RecordObject(material, "Paste Material Properties");
|
||||||
|
for (int i = 0; i < ShaderUtil.GetPropertyCount(_copiedMaterial.shader); i++)
|
||||||
|
{
|
||||||
|
var name = ShaderUtil.GetPropertyName(_copiedMaterial.shader, i);
|
||||||
|
var type = ShaderUtil.GetPropertyType(_copiedMaterial.shader, i);
|
||||||
|
PastePropertyValueToMaterial(material, name, name, type, valueMask);
|
||||||
|
}
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.Keyword) != 0)
|
||||||
|
material.shaderKeywords = _copiedMaterial.shaderKeywords;
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.RenderQueue) != 0)
|
||||||
|
material.renderQueue = _copiedMaterial.renderQueue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PastePropertyValueToMaterial(Material material, string srcName, string dstName)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ShaderUtil.GetPropertyCount(_copiedMaterial.shader); i++)
|
||||||
|
{
|
||||||
|
var name = ShaderUtil.GetPropertyName(_copiedMaterial.shader, i);
|
||||||
|
if (name == srcName)
|
||||||
|
{
|
||||||
|
var type = ShaderUtil.GetPropertyType(_copiedMaterial.shader, i);
|
||||||
|
PastePropertyValueToMaterial(material, srcName, dstName, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PastePropertyValueToMaterial(Material material, string srcName, string dstName, ShaderUtil.ShaderPropertyType type, uint valueMask = (uint)CopyMaterialValueMask.All)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ShaderUtil.ShaderPropertyType.Color:
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.Vector) != 0)
|
||||||
|
material.SetColor(dstName, _copiedMaterial.GetColor(srcName));
|
||||||
|
break;
|
||||||
|
case ShaderUtil.ShaderPropertyType.Vector:
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.Vector) != 0)
|
||||||
|
material.SetVector(dstName, _copiedMaterial.GetVector(srcName));
|
||||||
|
break;
|
||||||
|
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.Texture) != 0)
|
||||||
|
material.SetTexture(dstName, _copiedMaterial.GetTexture(srcName));
|
||||||
|
break;
|
||||||
|
// Float
|
||||||
|
default:
|
||||||
|
if ((valueMask & (uint)CopyMaterialValueMask.Float) != 0)
|
||||||
|
material.SetFloat(dstName, _copiedMaterial.GetFloat(srcName));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DrawToolbarButtons(ref Rect toolBarRect, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
// Copy
|
||||||
|
var buttonRectOffset = toolBarRect.height + 2;
|
||||||
|
var buttonRect = new Rect(toolBarRect.x, toolBarRect.y, toolBarRect.height, toolBarRect.height);
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentCopy, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
_copiedMaterial = UnityEngine.Object.Instantiate(lwgui.material);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paste
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
// Right Click
|
||||||
|
if (Event.current.type == EventType.MouseDown
|
||||||
|
&& Event.current.button == 1
|
||||||
|
&& buttonRect.Contains(Event.current.mousePosition))
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayCustomMenu(new Rect(Event.current.mousePosition.x, Event.current.mousePosition.y, 0, 0), _pasteMaterialMenus, -1,
|
||||||
|
(data, options, selected) =>
|
||||||
|
{
|
||||||
|
DoPasteMaterialProperties(lwgui, _pasteMaterialMenuValueMasks[selected]);
|
||||||
|
}, null);
|
||||||
|
Event.current.Use();
|
||||||
|
}
|
||||||
|
// Left Click
|
||||||
|
if (GUI.Button(buttonRect, _guiContentPaste, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
DoPasteMaterialProperties(lwgui, (uint)CopyMaterialValueMask.All);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select Material Asset, jump from a Runtime Material Instance to a Material Asset
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentSelect, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
if (AssetDatabase.Contains(lwgui.material))
|
||||||
|
{
|
||||||
|
Selection.activeObject = lwgui.material;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get Material Asset name
|
||||||
|
var name = lwgui.material.name;
|
||||||
|
foreach (var nameEnd in _materialInstanceNameEnd)
|
||||||
|
{
|
||||||
|
if (name.EndsWith(nameEnd))
|
||||||
|
{
|
||||||
|
name = name.Substring(0, name.Length - nameEnd.Length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get path
|
||||||
|
var guids = AssetDatabase.FindAssets("t:Material " + name);
|
||||||
|
var paths = guids.Select(((guid, i) =>
|
||||||
|
{
|
||||||
|
var filePath = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
var fileName = System.IO.Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
return (fileName == name && filePath.EndsWith(".mat")) ? filePath : null;
|
||||||
|
})).Where((s => !string.IsNullOrEmpty(s))).ToArray();
|
||||||
|
|
||||||
|
// Select Asset
|
||||||
|
if (paths.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("Can not find Material Assets with name: " + name);
|
||||||
|
}
|
||||||
|
else if (paths.Length > 1)
|
||||||
|
{
|
||||||
|
var str = string.Empty;
|
||||||
|
foreach (string path in paths)
|
||||||
|
{
|
||||||
|
str += "\n" + path;
|
||||||
|
}
|
||||||
|
Debug.LogWarning("Multiple Material Assets with the same name have been found, select only the first one:" + str);
|
||||||
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<Material>(paths[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<Material>(paths[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentChechout, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
foreach (var material in lwgui.materialEditor.targets)
|
||||||
|
{
|
||||||
|
VersionControlHelper.Checkout(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentExpand, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
foreach (var propertyStaticDataPair in lwgui.perShaderData.propertyDatas)
|
||||||
|
{
|
||||||
|
if (propertyStaticDataPair.Value.isMain || propertyStaticDataPair.Value.isAdvancedHeader)
|
||||||
|
propertyStaticDataPair.Value.isExpanding = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentCollapse, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
foreach (var propertyStaticDataPair in lwgui.perShaderData.propertyDatas)
|
||||||
|
{
|
||||||
|
if (propertyStaticDataPair.Value.isMain || propertyStaticDataPair.Value.isAdvancedHeader)
|
||||||
|
propertyStaticDataPair.Value.isExpanding = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Mode
|
||||||
|
buttonRect.x += buttonRectOffset;
|
||||||
|
toolBarRect.xMin += buttonRectOffset;
|
||||||
|
var color = GUI.color;
|
||||||
|
if (!lwgui.perShaderData.displayModeData.IsDefaultDisplayMode())
|
||||||
|
GUI.color = Color.yellow;
|
||||||
|
if (GUI.Button(buttonRect, _guiContentVisibility, Helper.guiStyles_IconButton))
|
||||||
|
{
|
||||||
|
string[] displayModeMenus = new[]
|
||||||
|
{
|
||||||
|
"Show All Advanced (" + lwgui.perShaderData.displayModeData.advancedCount + " of " + lwgui.perShaderData.propertyDatas.Count + ")",
|
||||||
|
"Show All Hidden (" + lwgui.perShaderData.displayModeData.hiddenCount + " of " + lwgui.perShaderData.propertyDatas.Count + ")",
|
||||||
|
"Show Only Modified (" + lwgui.perFrameData.modifiedCount + " of " + lwgui.perShaderData.propertyDatas.Count + ")",
|
||||||
|
};
|
||||||
|
bool[] enabled = new[] { true, true, true };
|
||||||
|
bool[] separator = new bool[3];
|
||||||
|
int[] selected = new[]
|
||||||
|
{
|
||||||
|
lwgui.perShaderData.displayModeData.showAllAdvancedProperties ? 0 : -1,
|
||||||
|
lwgui.perShaderData.displayModeData.showAllHiddenProperties ? 1 : -1,
|
||||||
|
lwgui.perShaderData.displayModeData.showOnlyModifiedProperties ? 2 : -1,
|
||||||
|
};
|
||||||
|
ReflectionHelper.DisplayCustomMenuWithSeparators(new Rect(Event.current.mousePosition.x, Event.current.mousePosition.y, 0, 0),
|
||||||
|
displayModeMenus, enabled, separator, selected,
|
||||||
|
(data, options, selectedIndex) =>
|
||||||
|
{
|
||||||
|
switch (selectedIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
lwgui.perShaderData.displayModeData.showAllAdvancedProperties = !lwgui.perShaderData.displayModeData.showAllAdvancedProperties;
|
||||||
|
lwgui.perShaderData.ToggleShowAllAdvancedProperties();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
lwgui.perShaderData.displayModeData.showAllHiddenProperties = !lwgui.perShaderData.displayModeData.showAllHiddenProperties;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
lwgui.perShaderData.displayModeData.showOnlyModifiedProperties = !lwgui.perShaderData.displayModeData.showOnlyModifiedProperties;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
GUI.color = color;
|
||||||
|
|
||||||
|
toolBarRect.xMin += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Search Field
|
||||||
|
private static readonly int s_TextFieldHash = "EditorTextField".GetHashCode();
|
||||||
|
private static readonly GUIContent[] _searchModeMenus =
|
||||||
|
(new GUIContent[(int)SearchMode.Num]).Select(((guiContent, i) =>
|
||||||
|
{
|
||||||
|
if (i == (int)SearchMode.Num)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new GUIContent(((SearchMode)i).ToString());
|
||||||
|
})).ToArray();
|
||||||
|
|
||||||
|
|
||||||
|
/// <returns>is has changed?</returns>
|
||||||
|
public static bool DrawSearchField(Rect rect, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
bool hasChanged = false;
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
|
||||||
|
var revertButtonRect = RevertableHelper.SplitRevertButtonRect(ref rect);
|
||||||
|
|
||||||
|
// Get internal TextField ControlID
|
||||||
|
int controlId = GUIUtility.GetControlID(s_TextFieldHash, FocusType.Keyboard, rect) + 1;
|
||||||
|
|
||||||
|
// searching mode
|
||||||
|
Rect modeRect = new Rect(rect);
|
||||||
|
modeRect.width = 20f;
|
||||||
|
if (Event.current.type == UnityEngine.EventType.MouseDown && modeRect.Contains(Event.current.mousePosition))
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayCustomMenu(rect, _searchModeMenus, (int)lwgui.perShaderData.searchMode,
|
||||||
|
(data, options, selected) =>
|
||||||
|
{
|
||||||
|
lwgui.perShaderData.searchMode = (SearchMode)selected;
|
||||||
|
hasChanged = true;
|
||||||
|
}, null);
|
||||||
|
Event.current.Use();
|
||||||
|
}
|
||||||
|
|
||||||
|
lwgui.perShaderData.searchString = EditorGUI.TextField(rect, String.Empty, lwgui.perShaderData.searchString, guiStyles_ToolbarSearchTextFieldPopup);
|
||||||
|
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
hasChanged = true;
|
||||||
|
|
||||||
|
// revert button
|
||||||
|
if (!string.IsNullOrEmpty(lwgui.perShaderData.searchString)
|
||||||
|
&& RevertableHelper.DrawRevertButton(revertButtonRect))
|
||||||
|
{
|
||||||
|
lwgui.perShaderData.searchString = string.Empty;
|
||||||
|
hasChanged = true;
|
||||||
|
GUIUtility.keyboardControl = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// display search mode
|
||||||
|
if (GUIUtility.keyboardControl != controlId
|
||||||
|
&& string.IsNullOrEmpty(lwgui.perShaderData.searchString)
|
||||||
|
&& Event.current.type == UnityEngine.EventType.Repaint)
|
||||||
|
{
|
||||||
|
using (new EditorGUI.DisabledScope(true))
|
||||||
|
{
|
||||||
|
#if UNITY_2019_2_OR_NEWER
|
||||||
|
var disableTextRect = new Rect(rect.x, rect.y, rect.width,
|
||||||
|
guiStyles_ToolbarSearchTextFieldPopup.fixedHeight > 0.0
|
||||||
|
? guiStyles_ToolbarSearchTextFieldPopup.fixedHeight
|
||||||
|
: rect.height);
|
||||||
|
#else
|
||||||
|
var disableTextRect = rect;
|
||||||
|
disableTextRect.yMin -= 3f;
|
||||||
|
#endif
|
||||||
|
disableTextRect = guiStyles_ToolbarSearchTextFieldPopup.padding.Remove(disableTextRect);
|
||||||
|
int fontSize = EditorStyles.label.fontSize;
|
||||||
|
EditorStyles.label.fontSize = guiStyles_ToolbarSearchTextFieldPopup.fontSize;
|
||||||
|
EditorStyles.label.Draw(disableTextRect, new GUIContent(lwgui.perShaderData.searchMode.ToString()), false, false, false, false);
|
||||||
|
EditorStyles.label.fontSize = fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanged) lwgui.perShaderData.UpdateSearchFilter();
|
||||||
|
|
||||||
|
return hasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Context Menu
|
||||||
|
|
||||||
|
private static void EditPresetEvent(string mode, ShaderPropertyPreset presetAsset, ShaderPropertyPreset.Preset activePreset, MaterialProperty prop, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
if (!VersionControlHelper.Checkout(presetAsset))
|
||||||
|
{
|
||||||
|
Debug.LogError("Can not edit the preset: " + presetAsset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case "Add":
|
||||||
|
case "Update":
|
||||||
|
activePreset.AddOrUpdateIncludeExtraProperties(lwgui, prop);
|
||||||
|
break;
|
||||||
|
case "Remove":
|
||||||
|
activePreset.RemoveIncludeExtraProperties(lwgui, prop.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
EditorUtility.SetDirty(presetAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DoPropertyContextMenus(Rect rect, MaterialProperty prop, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
if (Event.current.type != EventType.ContextClick || !rect.Contains(Event.current.mousePosition)) return;
|
||||||
|
|
||||||
|
Event.current.Use();
|
||||||
|
var propStaticData = lwgui.perShaderData.propertyDatas[prop.name];
|
||||||
|
var menus = new GenericMenu();
|
||||||
|
|
||||||
|
// 2022+ Material Varant Menus
|
||||||
|
#if UNITY_2022_1_OR_NEWER
|
||||||
|
ReflectionHelper.HandleApplyRevert(menus, prop);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Copy
|
||||||
|
menus.AddItem(new GUIContent("Copy"), false, () =>
|
||||||
|
{
|
||||||
|
_copiedMaterial = UnityEngine.Object.Instantiate(lwgui.material);
|
||||||
|
_copiedProps.Clear();
|
||||||
|
_copiedProps.Add(prop.name);
|
||||||
|
foreach (var extraPropName in propStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
_copiedProps.Add(extraPropName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Children
|
||||||
|
foreach (var childPropStaticData in propStaticData.children)
|
||||||
|
{
|
||||||
|
_copiedProps.Add(childPropStaticData.name);
|
||||||
|
foreach (var extraPropName in childPropStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
_copiedProps.Add(extraPropName);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var childChildPropStaticData in childPropStaticData.children)
|
||||||
|
{
|
||||||
|
_copiedProps.Add(childChildPropStaticData.name);
|
||||||
|
foreach (var extraPropName in childChildPropStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
_copiedProps.Add(extraPropName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paste
|
||||||
|
GenericMenu.MenuFunction pasteAction = () =>
|
||||||
|
{
|
||||||
|
foreach (Material material in prop.targets)
|
||||||
|
{
|
||||||
|
if (!VersionControlHelper.Checkout(material))
|
||||||
|
{
|
||||||
|
Debug.LogError("Material: '" + lwgui.material.name + "' unable to write!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Undo.RecordObject(material, "Paste Material Properties");
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], prop.name);
|
||||||
|
foreach (var extraPropName in propStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
if (index == _copiedProps.Count) break;
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], extraPropName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paste Children
|
||||||
|
foreach (var childPropStaticData in propStaticData.children)
|
||||||
|
{
|
||||||
|
if (index == _copiedProps.Count) break;
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], childPropStaticData.name);
|
||||||
|
foreach (var extraPropName in childPropStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
if (index == _copiedProps.Count) break;
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], extraPropName);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var childChildPropStaticData in childPropStaticData.children)
|
||||||
|
{
|
||||||
|
if (index == _copiedProps.Count) break;
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], childChildPropStaticData.name);
|
||||||
|
foreach (var extraPropName in childChildPropStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
if (index == _copiedProps.Count) break;
|
||||||
|
PastePropertyValueToMaterial(material, _copiedProps[index++], extraPropName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (_copiedMaterial != null && _copiedProps.Count > 0 && GUI.enabled)
|
||||||
|
menus.AddItem(new GUIContent("Paste"), false, pasteAction);
|
||||||
|
else
|
||||||
|
menus.AddDisabledItem(new GUIContent("Paste"));
|
||||||
|
|
||||||
|
menus.AddSeparator("");
|
||||||
|
|
||||||
|
// Copy Display Name
|
||||||
|
menus.AddItem(new GUIContent("Copy Display Name"), false, () =>
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = propStaticData.displayName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy Property Names
|
||||||
|
menus.AddItem(new GUIContent("Copy Property Names"), false, () =>
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = prop.name;
|
||||||
|
foreach (var extraPropName in propStaticData.extraPropNames)
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer += ", " + extraPropName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// menus.AddSeparator("");
|
||||||
|
//
|
||||||
|
// // Add to Favorites
|
||||||
|
// menus.AddItem(new GUIContent("Add to Favorites"), false, () =>
|
||||||
|
// {
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // Remove from Favorites
|
||||||
|
// menus.AddItem(new GUIContent("Remove from Favorites"), false, () =>
|
||||||
|
// {
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Preset
|
||||||
|
if (GUI.enabled)
|
||||||
|
{
|
||||||
|
menus.AddSeparator("");
|
||||||
|
foreach (var activePresetData in lwgui.perFrameData.activePresets)
|
||||||
|
{
|
||||||
|
if (activePresetData.property == prop) continue;
|
||||||
|
|
||||||
|
var activePreset = activePresetData.preset;
|
||||||
|
var presetAsset = lwgui.perShaderData.propertyDatas[activePresetData.property.name].propertyPresetAsset;
|
||||||
|
var presetPropDisplayName = lwgui.perShaderData.propertyDatas[activePresetData.property.name].displayName;
|
||||||
|
|
||||||
|
if (activePreset.GetPropertyValue(prop.name) != null)
|
||||||
|
{
|
||||||
|
menus.AddItem(new GUIContent("Update to Preset/" + presetPropDisplayName + "/" + activePreset.presetName), false, () => EditPresetEvent("Update", presetAsset, activePreset, prop, lwgui));
|
||||||
|
menus.AddItem(new GUIContent("Remove from Preset/" + presetPropDisplayName + "/" + activePreset.presetName), false, () => EditPresetEvent("Remove", presetAsset, activePreset, prop, lwgui));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
menus.AddItem(new GUIContent("Add to Preset/" + presetPropDisplayName + "/" + activePreset.presetName), false, () => EditPresetEvent("Add", presetAsset, activePreset, prop, lwgui));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menus.ShowAsContext();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bc93109edd264b6aa0265609cb38cc58
|
||||||
|
timeCreated: 1687336811
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public delegate void LWGUICustomGUIEvent(LWGUI lwgui);
|
||||||
|
|
||||||
|
public class LWGUI : ShaderGUI
|
||||||
|
{
|
||||||
|
public MaterialProperty[] props;
|
||||||
|
public MaterialEditor materialEditor;
|
||||||
|
public Material material;
|
||||||
|
public Shader shader;
|
||||||
|
public PerShaderData perShaderData;
|
||||||
|
public PerFrameData perFrameData;
|
||||||
|
|
||||||
|
public static LWGUICustomGUIEvent onDrawCustomHeader;
|
||||||
|
public static LWGUICustomGUIEvent onDrawCustomFooter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when switch to a new Material Window, each window has a LWGUI instance
|
||||||
|
/// </summary>
|
||||||
|
public LWGUI() { }
|
||||||
|
|
||||||
|
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
|
||||||
|
{
|
||||||
|
this.props = props;
|
||||||
|
this.materialEditor = materialEditor;
|
||||||
|
this.material = materialEditor.target as Material;
|
||||||
|
this.shader = this.material.shader;
|
||||||
|
this.perShaderData = MetaDataHelper.BuildPerShaderData(shader, props);
|
||||||
|
this.perFrameData = MetaDataHelper.BuildPerFrameData(shader, material, props);
|
||||||
|
|
||||||
|
|
||||||
|
// Custom Header
|
||||||
|
if (onDrawCustomHeader != null)
|
||||||
|
onDrawCustomHeader(this);
|
||||||
|
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
bool enabled = GUI.enabled;
|
||||||
|
GUI.enabled = true;
|
||||||
|
var toolBarRect = EditorGUILayout.GetControlRect();
|
||||||
|
toolBarRect.xMin = 2;
|
||||||
|
|
||||||
|
Helper.DrawToolbarButtons(ref toolBarRect, this);
|
||||||
|
|
||||||
|
Helper.DrawSearchField(toolBarRect, this);
|
||||||
|
|
||||||
|
GUILayoutUtility.GetRect(0, 0); // Space(0)
|
||||||
|
GUI.enabled = enabled;
|
||||||
|
Helper.DrawSplitLine();
|
||||||
|
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
{
|
||||||
|
// move fields left to make rect for Revert Button
|
||||||
|
materialEditor.SetDefaultGUIWidths();
|
||||||
|
RevertableHelper.InitRevertableGUIWidths();
|
||||||
|
|
||||||
|
// start drawing properties
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propStaticData = perShaderData.propertyDatas[prop.name];
|
||||||
|
var propDynamicData = perFrameData.propertyDatas[prop.name];
|
||||||
|
|
||||||
|
// Visibility
|
||||||
|
{
|
||||||
|
if (!MetaDataHelper.GetPropertyVisibility(prop, material, this))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (propStaticData.parent != null
|
||||||
|
&& (!MetaDataHelper.GetParentPropertyVisibility(propStaticData.parent, material, this)
|
||||||
|
|| !MetaDataHelper.GetParentPropertyVisibility(propStaticData.parent.parent, material, this)))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indent
|
||||||
|
var indentLevel = EditorGUI.indentLevel;
|
||||||
|
if (propStaticData.isAdvancedHeader)
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
if (propStaticData.parent != null)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
if (propStaticData.parent.parent != null)
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced Header
|
||||||
|
if (propStaticData.isAdvancedHeader && !propStaticData.isAdvancedHeaderProperty)
|
||||||
|
{
|
||||||
|
DrawAdvancedHeader(propStaticData, prop);
|
||||||
|
|
||||||
|
if (!propStaticData.isExpanding)
|
||||||
|
{
|
||||||
|
RevertableHelper.SetRevertableGUIWidths();
|
||||||
|
EditorGUI.indentLevel = indentLevel;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawProperty(prop);
|
||||||
|
|
||||||
|
RevertableHelper.SetRevertableGUIWidths();
|
||||||
|
EditorGUI.indentLevel = indentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
materialEditor.SetDefaultGUIWidths();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
Helper.DrawSplitLine();
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
|
||||||
|
// Render settings
|
||||||
|
#if UNITY_2019_4_OR_NEWER
|
||||||
|
if (SupportedRenderingFeatures.active.editableMaterialRenderQueue)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
materialEditor.RenderQueueField();
|
||||||
|
}
|
||||||
|
materialEditor.EnableInstancingField();
|
||||||
|
materialEditor.LightmapEmissionProperty();
|
||||||
|
materialEditor.DoubleSidedGIField();
|
||||||
|
|
||||||
|
|
||||||
|
// Custom Footer
|
||||||
|
if (onDrawCustomFooter != null)
|
||||||
|
onDrawCustomFooter(this);
|
||||||
|
|
||||||
|
|
||||||
|
// LOGO
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
Helper.DrawLogo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAdvancedHeader(PropertyStaticData propStaticData, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
var rect = EditorGUILayout.GetControlRect();
|
||||||
|
var revertButtonRect = RevertableHelper.SplitRevertButtonRect(ref rect);
|
||||||
|
var label = string.IsNullOrEmpty(propStaticData.advancedHeaderString) ? "Advanced" : propStaticData.advancedHeaderString;
|
||||||
|
propStaticData.isExpanding = EditorGUI.Foldout(rect, propStaticData.isExpanding, label);
|
||||||
|
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && rect.Contains(Event.current.mousePosition))
|
||||||
|
propStaticData.isExpanding = !propStaticData.isExpanding;
|
||||||
|
RevertableHelper.DrawRevertableProperty(revertButtonRect, prop, this, true);
|
||||||
|
Helper.DoPropertyContextMenus(rect, prop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProperty(MaterialProperty prop)
|
||||||
|
{
|
||||||
|
var propStaticData = perShaderData.propertyDatas[prop.name];
|
||||||
|
var propDynamicData = perFrameData.propertyDatas[prop.name];
|
||||||
|
|
||||||
|
Helper.DrawHelpbox(propStaticData, propDynamicData);
|
||||||
|
|
||||||
|
var label = new GUIContent(propStaticData.displayName, MetaDataHelper.GetPropertyTooltip(propStaticData, propDynamicData));
|
||||||
|
var height = materialEditor.GetPropertyHeight(prop, label.text);
|
||||||
|
var rect = EditorGUILayout.GetControlRect(true, height);
|
||||||
|
|
||||||
|
var revertButtonRect = RevertableHelper.SplitRevertButtonRect(ref rect);
|
||||||
|
|
||||||
|
Helper.BeginProperty(rect, prop, this);
|
||||||
|
Helper.DoPropertyContextMenus(rect, prop, this);
|
||||||
|
RevertableHelper.FixGUIWidthMismatch(prop.type, materialEditor);
|
||||||
|
if (propStaticData.isAdvancedHeaderProperty)
|
||||||
|
propStaticData.isExpanding = EditorGUI.Foldout(rect, propStaticData.isExpanding, string.Empty);
|
||||||
|
RevertableHelper.DrawRevertableProperty(revertButtonRect, prop, this, propStaticData.isMain || propStaticData.isAdvancedHeaderProperty);
|
||||||
|
materialEditor.ShaderProperty(rect, prop, label);
|
||||||
|
Helper.EndProperty(this, prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} //namespace LWGUI
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: b358a30ac16c6a34fb673ede0a288e48
|
guid: b44ae323525d0c64c92721bfbd1ad8c5
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -0,0 +1,630 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public enum SearchMode
|
||||||
|
{
|
||||||
|
Auto = 0, // Search by group first, and search by property when there are no results
|
||||||
|
Property = 1, // Search by property
|
||||||
|
Group = 2, // Search by group
|
||||||
|
Num = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LogicalOperator
|
||||||
|
{
|
||||||
|
And,
|
||||||
|
Or
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DisplayModeData
|
||||||
|
{
|
||||||
|
public bool showAllAdvancedProperties;
|
||||||
|
public bool showAllHiddenProperties;
|
||||||
|
public bool showOnlyModifiedProperties;
|
||||||
|
|
||||||
|
public int advancedCount;
|
||||||
|
public int hiddenCount;
|
||||||
|
|
||||||
|
public bool IsDefaultDisplayMode() { return !(showAllAdvancedProperties || showAllHiddenProperties || showOnlyModifiedProperties); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ShowIfData
|
||||||
|
{
|
||||||
|
public LogicalOperator logicalOperator = LogicalOperator.And;
|
||||||
|
public string targetPropertyName = string.Empty;
|
||||||
|
public CompareFunction compareFunction = CompareFunction.Equal;
|
||||||
|
public float value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All static metadata for a Property, determined after the Shader is compiled.
|
||||||
|
/// </summary>
|
||||||
|
public class PropertyStaticData
|
||||||
|
{
|
||||||
|
public string name = string.Empty;
|
||||||
|
public string displayName = string.Empty; // Decoded displayName (Helpbox and Tooltip are encoded in displayName)
|
||||||
|
|
||||||
|
// Structure
|
||||||
|
public string groupName = string.Empty; // [Group(groupName)] / [Sub(groupName)] / [Advanced(groupName)]
|
||||||
|
public bool isMain = false; // [Group]
|
||||||
|
public bool isAdvanced = false; // [Advanced]
|
||||||
|
public bool isAdvancedHeader = false; // the first [Advanced] in the same group
|
||||||
|
public bool isAdvancedHeaderProperty = false;
|
||||||
|
public string advancedHeaderString = string.Empty;
|
||||||
|
public PropertyStaticData parent = null;
|
||||||
|
public List<PropertyStaticData> children = new List<PropertyStaticData>();
|
||||||
|
|
||||||
|
// Visibility
|
||||||
|
public string conditionalDisplayKeyword = string.Empty; // [Group(groupName_conditionalDisplayKeyword)]
|
||||||
|
public bool isSearchMatched = true; // Draws when the search match is successful
|
||||||
|
public bool isExpanding = false; // Draws when the group is expanding
|
||||||
|
public bool isHidden = false; // [Hidden]
|
||||||
|
public List<ShowIfData> showIfDatas = new List<ShowIfData>(); // [ShowIf()]
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
public List<string> extraPropNames = new List<string>(); // Other Props that have been associated
|
||||||
|
public string helpboxMessages = string.Empty;
|
||||||
|
public string tooltipMessages = string.Empty;
|
||||||
|
public ShaderPropertyPreset propertyPresetAsset = null; // The Referenced Preset Asset
|
||||||
|
|
||||||
|
public void AddExtraProperty(string propName)
|
||||||
|
{
|
||||||
|
if (!extraPropNames.Contains(propName)) extraPropNames.Add(propName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consistent metadata across different material instances of the same Shader.
|
||||||
|
/// </summary>
|
||||||
|
public class PerShaderData
|
||||||
|
{
|
||||||
|
public Dictionary<string, PropertyStaticData> propertyDatas = new Dictionary<string, PropertyStaticData>();
|
||||||
|
public SearchMode searchMode = SearchMode.Auto;
|
||||||
|
public string searchString = string.Empty;
|
||||||
|
public List<string> favoriteproperties = new List<string>();
|
||||||
|
public DisplayModeData displayModeData = new DisplayModeData();
|
||||||
|
|
||||||
|
|
||||||
|
public void BuildPropertyStaticData(Shader shader, MaterialProperty[] props)
|
||||||
|
{
|
||||||
|
// Get Property Static Data
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propStaticData = new PropertyStaticData(){ name = prop.name };
|
||||||
|
propertyDatas[prop.name] = propStaticData;
|
||||||
|
|
||||||
|
List<MaterialPropertyDrawer> decoratorDrawers;
|
||||||
|
var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers);
|
||||||
|
if (decoratorDrawers != null && decoratorDrawers.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var decoratorDrawer in decoratorDrawers)
|
||||||
|
{
|
||||||
|
if (decoratorDrawer is IBaseDrawer)
|
||||||
|
(decoratorDrawer as IBaseDrawer).BuildStaticMetaData(shader, prop, props, propStaticData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (drawer != null)
|
||||||
|
{
|
||||||
|
if (drawer is IBaseDrawer)
|
||||||
|
(drawer as IBaseDrawer).BuildStaticMetaData(shader, prop, props, propStaticData);
|
||||||
|
}
|
||||||
|
|
||||||
|
DecodeMetaDataFromDisplayName(prop, propStaticData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Data
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propStaticData = propertyDatas[prop.name];
|
||||||
|
propStaticData.extraPropNames.RemoveAll((extraPropName =>
|
||||||
|
string.IsNullOrEmpty(extraPropName) || !propertyDatas.ContainsKey(extraPropName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Property Structure
|
||||||
|
{
|
||||||
|
var groupToMainPropertyDic = new Dictionary<string, MaterialProperty>();
|
||||||
|
|
||||||
|
// Collection Groups
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propData = propertyDatas[prop.name];
|
||||||
|
if (propData.isMain
|
||||||
|
&& !string.IsNullOrEmpty(propData.groupName)
|
||||||
|
&& !groupToMainPropertyDic.ContainsKey(propData.groupName))
|
||||||
|
groupToMainPropertyDic.Add(propData.groupName, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register SubProps
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propData = propertyDatas[prop.name];
|
||||||
|
if (!propData.isMain
|
||||||
|
&& !string.IsNullOrEmpty(propData.groupName))
|
||||||
|
{
|
||||||
|
foreach (var groupName in groupToMainPropertyDic.Keys)
|
||||||
|
{
|
||||||
|
if (propData.groupName.StartsWith(groupName))
|
||||||
|
{
|
||||||
|
// Update Structure
|
||||||
|
var mainProp = groupToMainPropertyDic[groupName];
|
||||||
|
propData.parent = propertyDatas[mainProp.name];
|
||||||
|
propertyDatas[mainProp.name].children.Add(propData);
|
||||||
|
|
||||||
|
// Split groupName and conditional display keyword
|
||||||
|
if (propData.groupName.Length > groupName.Length)
|
||||||
|
{
|
||||||
|
propData.conditionalDisplayKeyword =
|
||||||
|
propData.groupName.Substring(groupName.Length, propData.groupName.Length - groupName.Length).ToUpper();
|
||||||
|
propData.groupName = groupName;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Display Mode Data
|
||||||
|
{
|
||||||
|
PropertyStaticData lastPropData = null;
|
||||||
|
PropertyStaticData lastHeaderPropData = null;
|
||||||
|
for (int i = 0; i < props.Length; i++)
|
||||||
|
{
|
||||||
|
var prop = props[i];
|
||||||
|
var propStaticData = propertyDatas[prop.name];
|
||||||
|
|
||||||
|
// Counting
|
||||||
|
if (propStaticData.isHidden
|
||||||
|
|| (propStaticData.parent != null
|
||||||
|
&& (propStaticData.parent.isHidden
|
||||||
|
|| (propStaticData.parent.parent != null && propStaticData.parent.parent.isHidden))))
|
||||||
|
displayModeData.hiddenCount++;
|
||||||
|
if (propStaticData.isAdvanced
|
||||||
|
|| (propStaticData.parent != null
|
||||||
|
&& (propStaticData.parent.isAdvanced
|
||||||
|
|| (propStaticData.parent.parent != null && propStaticData.parent.parent.isAdvanced))))
|
||||||
|
displayModeData.advancedCount++;
|
||||||
|
|
||||||
|
// Build Advanced Structure
|
||||||
|
if (propStaticData.isAdvanced)
|
||||||
|
{
|
||||||
|
// If it is the first prop in a Advanced Block, set to Header
|
||||||
|
if (lastPropData == null
|
||||||
|
|| !lastPropData.isAdvanced
|
||||||
|
|| propStaticData.isAdvancedHeaderProperty
|
||||||
|
|| (!string.IsNullOrEmpty(propStaticData.advancedHeaderString)
|
||||||
|
&& propStaticData.advancedHeaderString != lastPropData.advancedHeaderString))
|
||||||
|
{
|
||||||
|
propStaticData.isAdvancedHeader = true;
|
||||||
|
lastHeaderPropData = propStaticData;
|
||||||
|
}
|
||||||
|
// Else set to child
|
||||||
|
else
|
||||||
|
{
|
||||||
|
propStaticData.parent = lastHeaderPropData;
|
||||||
|
lastHeaderPropData.children.Add(propStaticData);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPropData = propStaticData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string _tooltipSplitter = "#";
|
||||||
|
private static readonly string _helpboxSplitter = "%";
|
||||||
|
|
||||||
|
public void DecodeMetaDataFromDisplayName(MaterialProperty prop, PropertyStaticData propStaticData)
|
||||||
|
{
|
||||||
|
var tooltips = prop.displayName.Split(new String[] { _tooltipSplitter }, StringSplitOptions.None);
|
||||||
|
if (tooltips.Length > 1)
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= tooltips.Length - 1; i++)
|
||||||
|
{
|
||||||
|
var str = tooltips[i];
|
||||||
|
var helpboxIndex = tooltips[i].IndexOf(_helpboxSplitter, StringComparison.Ordinal);
|
||||||
|
if (helpboxIndex > 0)
|
||||||
|
str = tooltips[i].Substring(0, helpboxIndex);
|
||||||
|
propStaticData.tooltipMessages += str + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var helpboxes = prop.displayName.Split(new String[] { _helpboxSplitter }, StringSplitOptions.None);
|
||||||
|
if (helpboxes.Length > 1)
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= helpboxes.Length - 1; i++)
|
||||||
|
{
|
||||||
|
var str = helpboxes[i];
|
||||||
|
var tooltipIndex = helpboxes[i].IndexOf(_tooltipSplitter, StringComparison.Ordinal);
|
||||||
|
if (tooltipIndex > 0)
|
||||||
|
str = tooltips[i].Substring(0, tooltipIndex);
|
||||||
|
propStaticData.helpboxMessages += str + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propStaticData.helpboxMessages.EndsWith("\n"))
|
||||||
|
propStaticData.helpboxMessages = propStaticData.helpboxMessages.Substring(0, propStaticData.helpboxMessages.Length - 1);
|
||||||
|
|
||||||
|
propStaticData.displayName = prop.displayName.Split(new String[] { _tooltipSplitter, _helpboxSplitter }, StringSplitOptions.None)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSearchFilter()
|
||||||
|
{
|
||||||
|
var isSearchStringEmpty = string.IsNullOrEmpty(searchString);
|
||||||
|
var searchStringLower = searchString.ToLower();
|
||||||
|
var searchKeywords = searchStringLower.Split(' ', ',', ';', '|', ',', ';'); // Some possible separators
|
||||||
|
|
||||||
|
// The First Search
|
||||||
|
foreach (var propertyData in propertyDatas)
|
||||||
|
{
|
||||||
|
propertyData.Value.isSearchMatched = isSearchStringEmpty
|
||||||
|
? true
|
||||||
|
: IsWholeWordMatch(propertyData.Value.displayName, propertyData.Key, searchKeywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Further adjust visibility
|
||||||
|
if (!isSearchStringEmpty)
|
||||||
|
{
|
||||||
|
var searchModeTemp = searchMode;
|
||||||
|
// Auto: search by group first, and search by property when there are no results
|
||||||
|
if (searchModeTemp == SearchMode.Auto)
|
||||||
|
{
|
||||||
|
// if has no group
|
||||||
|
if (!propertyDatas.Any((propertyData => propertyData.Value.isSearchMatched && propertyData.Value.isMain)))
|
||||||
|
searchModeTemp = SearchMode.Property;
|
||||||
|
else
|
||||||
|
searchModeTemp = SearchMode.Group;
|
||||||
|
}
|
||||||
|
|
||||||
|
// search by property
|
||||||
|
if (searchModeTemp == SearchMode.Property)
|
||||||
|
{
|
||||||
|
// when a SubProp is displayed, the MainProp is also displayed
|
||||||
|
foreach (var propertyData in propertyDatas)
|
||||||
|
{
|
||||||
|
if (propertyData.Value.isMain && propertyData.Value.children.Any((childPropertyData => childPropertyData.isSearchMatched)))
|
||||||
|
propertyData.Value.isSearchMatched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// search by group
|
||||||
|
else if (searchModeTemp == SearchMode.Group)
|
||||||
|
{
|
||||||
|
// when search by group, all SubProps should display with MainProp
|
||||||
|
foreach (var propertyData in propertyDatas)
|
||||||
|
{
|
||||||
|
if (propertyData.Value.isMain)
|
||||||
|
foreach (var childPropertyData in propertyData.Value.children)
|
||||||
|
childPropertyData.isSearchMatched = propertyData.Value.isSearchMatched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWholeWordMatch(string displayName, string propertyName, string[] searchingKeywords)
|
||||||
|
{
|
||||||
|
bool contains = true;
|
||||||
|
displayName = displayName.ToLower();
|
||||||
|
var name = propertyName.ToLower();
|
||||||
|
|
||||||
|
foreach (var keyword in searchingKeywords)
|
||||||
|
{
|
||||||
|
var isMatch = false;
|
||||||
|
isMatch |= displayName.Contains(keyword);
|
||||||
|
isMatch |= name.Contains(keyword);
|
||||||
|
contains &= isMatch;
|
||||||
|
}
|
||||||
|
return contains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleShowAllAdvancedProperties()
|
||||||
|
{
|
||||||
|
foreach (var propertyStaticDataPair in propertyDatas)
|
||||||
|
{
|
||||||
|
if (propertyStaticDataPair.Value.isAdvancedHeader)
|
||||||
|
propertyStaticDataPair.Value.isExpanding = displayModeData.showAllAdvancedProperties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Property metadata dynamically generated perframe
|
||||||
|
/// </summary>
|
||||||
|
public class PropertyDynamicData
|
||||||
|
{
|
||||||
|
public MaterialProperty property;
|
||||||
|
public MaterialProperty defualtProperty; // Default values may be overridden by Preset
|
||||||
|
public string defaultValueDescription = string.Empty; // Description of the default values used in Tooltip
|
||||||
|
public bool hasModified = false; // Are properties modified in the material?
|
||||||
|
public bool hasChildrenModified = false; // Are Children properties modified in the material?
|
||||||
|
public bool hasRevertChanged = false; // Used to call property EndChangeCheck()
|
||||||
|
public bool isShowing = true; // ShowIf() result
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PersetDynamicData
|
||||||
|
{
|
||||||
|
public ShaderPropertyPreset.Preset preset;
|
||||||
|
public MaterialProperty property;
|
||||||
|
|
||||||
|
public PersetDynamicData(ShaderPropertyPreset.Preset preset, MaterialProperty property)
|
||||||
|
{
|
||||||
|
this.preset = preset;
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Each frame of each material may have different metadata.
|
||||||
|
/// </summary>
|
||||||
|
public class PerFrameData
|
||||||
|
{
|
||||||
|
public Dictionary<string, PropertyDynamicData> propertyDatas = new Dictionary<string, PropertyDynamicData>();
|
||||||
|
public List<PersetDynamicData> activePresets = new List<PersetDynamicData>();
|
||||||
|
|
||||||
|
public int modifiedCount = 0;
|
||||||
|
|
||||||
|
public void BuildPerFrameData(Shader shader, Material material, MaterialProperty[] props, PerShaderData perShaderData)
|
||||||
|
{
|
||||||
|
// Get active presets
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
List<MaterialPropertyDrawer> decoratorDrawers;
|
||||||
|
var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers);
|
||||||
|
|
||||||
|
// Get Presets
|
||||||
|
if (drawer != null)
|
||||||
|
{
|
||||||
|
if (drawer is IBasePresetDrawer)
|
||||||
|
{
|
||||||
|
var activePreset = (drawer as IBasePresetDrawer).GetActivePreset(prop, perShaderData.propertyDatas[prop.name].propertyPresetAsset);
|
||||||
|
if (activePreset != null)
|
||||||
|
{
|
||||||
|
activePresets.Add(new PersetDynamicData(activePreset, prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply presets to default material
|
||||||
|
{
|
||||||
|
var defaultMaterial =
|
||||||
|
#if UNITY_2022_1_OR_NEWER
|
||||||
|
material.parent ? UnityEngine.Object.Instantiate(material.parent) :
|
||||||
|
#endif
|
||||||
|
new Material(shader);
|
||||||
|
|
||||||
|
foreach (var activePreset in activePresets)
|
||||||
|
activePreset.preset.ApplyToDefaultMaterial(defaultMaterial);
|
||||||
|
|
||||||
|
var defaultProperties = MaterialEditor.GetMaterialProperties(new[] { defaultMaterial });
|
||||||
|
Debug.Assert(defaultProperties.Length == props.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < props.Length; i++)
|
||||||
|
{
|
||||||
|
Debug.Assert(props[i].name == defaultProperties[i].name);
|
||||||
|
Debug.Assert(!propertyDatas.ContainsKey(props[i].name));
|
||||||
|
|
||||||
|
var hasModified = !Helper.PropertyValueEquals(props[i], defaultProperties[i]);
|
||||||
|
if (hasModified) modifiedCount++;
|
||||||
|
propertyDatas.Add(props[i].name, new PropertyDynamicData()
|
||||||
|
{
|
||||||
|
property = props[i],
|
||||||
|
defualtProperty = defaultProperties[i],
|
||||||
|
hasModified = hasModified
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
var propStaticData = perShaderData.propertyDatas[prop.name];
|
||||||
|
var propDynamicData = propertyDatas[prop.name];
|
||||||
|
|
||||||
|
// Override parent hasModified
|
||||||
|
if (propDynamicData.hasModified)
|
||||||
|
{
|
||||||
|
var parentPropData = propStaticData.parent;
|
||||||
|
if (parentPropData != null)
|
||||||
|
{
|
||||||
|
propertyDatas[parentPropData.name].hasChildrenModified = true;
|
||||||
|
if (parentPropData.parent != null)
|
||||||
|
propertyDatas[parentPropData.parent.name].hasChildrenModified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default value descriptions
|
||||||
|
List<MaterialPropertyDrawer> decoratorDrawers;
|
||||||
|
var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers);
|
||||||
|
{
|
||||||
|
if (decoratorDrawers != null && decoratorDrawers.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var decoratorDrawer in decoratorDrawers)
|
||||||
|
{
|
||||||
|
if (decoratorDrawer is IBaseDrawer)
|
||||||
|
(decoratorDrawer as IBaseDrawer).GetDefaultValueDescription(shader, prop, perShaderData, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (drawer != null)
|
||||||
|
{
|
||||||
|
if (drawer is IBaseDrawer)
|
||||||
|
(drawer as IBaseDrawer).GetDefaultValueDescription(shader, prop, perShaderData, this);
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(propDynamicData.defaultValueDescription))
|
||||||
|
propDynamicData.defaultValueDescription =
|
||||||
|
RevertableHelper.GetPropertyDefaultValueText(propDynamicData.defualtProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ShowIf() results
|
||||||
|
foreach (var showIfData in propStaticData.showIfDatas)
|
||||||
|
{
|
||||||
|
var propCurrentValue = propertyDatas[showIfData.targetPropertyName].property.floatValue;
|
||||||
|
bool compareResult;
|
||||||
|
|
||||||
|
switch (showIfData.compareFunction)
|
||||||
|
{
|
||||||
|
case CompareFunction.Less:
|
||||||
|
compareResult = propCurrentValue < showIfData.value;
|
||||||
|
break;
|
||||||
|
case CompareFunction.LessEqual:
|
||||||
|
compareResult = propCurrentValue <= showIfData.value;
|
||||||
|
break;
|
||||||
|
case CompareFunction.Greater:
|
||||||
|
compareResult = propCurrentValue > showIfData.value;
|
||||||
|
break;
|
||||||
|
case CompareFunction.NotEqual:
|
||||||
|
compareResult = propCurrentValue != showIfData.value;
|
||||||
|
break;
|
||||||
|
case CompareFunction.GreaterEqual:
|
||||||
|
compareResult = propCurrentValue >= showIfData.value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
compareResult = propCurrentValue == showIfData.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (showIfData.logicalOperator)
|
||||||
|
{
|
||||||
|
case LogicalOperator.And:
|
||||||
|
propDynamicData.isShowing &= compareResult;
|
||||||
|
break;
|
||||||
|
case LogicalOperator.Or:
|
||||||
|
propDynamicData.isShowing |= compareResult;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MaterialProperty GetProperty(string propName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(propName) && propertyDatas.ContainsKey(propName))
|
||||||
|
return propertyDatas[propName].property;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MaterialProperty GetDefaultProperty(string propName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(propName) && propertyDatas.ContainsKey(propName))
|
||||||
|
return propertyDatas[propName].defualtProperty;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EndChangeCheck(string propName = null)
|
||||||
|
{
|
||||||
|
var result = EditorGUI.EndChangeCheck();
|
||||||
|
if (!string.IsNullOrEmpty(propName))
|
||||||
|
{
|
||||||
|
result |= propertyDatas[propName].hasRevertChanged;
|
||||||
|
propertyDatas[propName].hasRevertChanged = false;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MetaDataHelper
|
||||||
|
{
|
||||||
|
private static Dictionary<Shader, PerShaderData> _shaderDataDic = new Dictionary<Shader, PerShaderData>();
|
||||||
|
|
||||||
|
public static PerShaderData BuildPerShaderData(Shader shader, MaterialProperty[] props)
|
||||||
|
{
|
||||||
|
if (!_shaderDataDic.ContainsKey(shader))
|
||||||
|
{
|
||||||
|
var perShaderData = new PerShaderData();
|
||||||
|
perShaderData.BuildPropertyStaticData(shader, props);
|
||||||
|
_shaderDataDic.Add(shader, perShaderData);
|
||||||
|
}
|
||||||
|
return _shaderDataDic[shader];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ForceRebuildPerShaderData(Shader shader)
|
||||||
|
{
|
||||||
|
if (shader && _shaderDataDic.ContainsKey(shader))
|
||||||
|
_shaderDataDic.Remove(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PerFrameData BuildPerFrameData(Shader shader, Material material, MaterialProperty[] props)
|
||||||
|
{
|
||||||
|
var perFrameData = new PerFrameData();
|
||||||
|
perFrameData.BuildPerFrameData(shader, material, props, _shaderDataDic[shader]);
|
||||||
|
return perFrameData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPropertyTooltip(PropertyStaticData propertyStaticData, PropertyDynamicData propertyDynamicData)
|
||||||
|
{
|
||||||
|
var str = propertyStaticData.tooltipMessages;
|
||||||
|
if (!string.IsNullOrEmpty(str))
|
||||||
|
str += "\n\n";
|
||||||
|
str += "Property Name: " + propertyDynamicData.property.name + "\n";
|
||||||
|
str += "Default Value: " + propertyDynamicData.defaultValueDescription;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string _tooltipString = "#";
|
||||||
|
private static readonly string _helpboxString = "%";
|
||||||
|
|
||||||
|
public static string GetPropertyDisplayName(Shader shader, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
var tooltipIndex = prop.displayName.IndexOf(_tooltipString, StringComparison.Ordinal);
|
||||||
|
var helpboxIndex = prop.displayName.IndexOf(_helpboxString, StringComparison.Ordinal);
|
||||||
|
var minIndex = tooltipIndex == -1 ? helpboxIndex : tooltipIndex;
|
||||||
|
if (tooltipIndex != -1 && helpboxIndex != -1)
|
||||||
|
minIndex = Mathf.Min(minIndex, helpboxIndex);
|
||||||
|
if (minIndex == -1)
|
||||||
|
return prop.displayName;
|
||||||
|
else if (minIndex == 0)
|
||||||
|
return string.Empty;
|
||||||
|
else
|
||||||
|
return prop.displayName.Substring(0, minIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool GetPropertyVisibility(MaterialProperty prop, Material material, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
var propertyStaticData = lwgui.perShaderData.propertyDatas[prop.name];
|
||||||
|
var propertyDynamicData = lwgui.perFrameData.propertyDatas[prop.name];
|
||||||
|
var displayModeData = lwgui.perShaderData.displayModeData;
|
||||||
|
|
||||||
|
if ( // if HideInInspector
|
||||||
|
Helper.IsPropertyHideInInspector(prop)
|
||||||
|
// if Search Filtered
|
||||||
|
|| !propertyStaticData.isSearchMatched
|
||||||
|
// if the Conditional Display Keyword is not active
|
||||||
|
|| (!string.IsNullOrEmpty(propertyStaticData.conditionalDisplayKeyword) && !material.shaderKeywords.Any((str => str == propertyStaticData.conditionalDisplayKeyword)))
|
||||||
|
|| (!displayModeData.showAllHiddenProperties && propertyStaticData.isHidden)
|
||||||
|
// if show modified only
|
||||||
|
|| (displayModeData.showOnlyModifiedProperties && !(propertyDynamicData.hasModified || propertyDynamicData.hasChildrenModified))
|
||||||
|
// ShowIf() == false
|
||||||
|
|| !propertyDynamicData.isShowing
|
||||||
|
)
|
||||||
|
{
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool GetParentPropertyVisibility(PropertyStaticData parentPropStaticData, Material material, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
if (parentPropStaticData != null
|
||||||
|
&& (!lwgui.perShaderData.propertyDatas[parentPropStaticData.name].isExpanding
|
||||||
|
|| !MetaDataHelper.GetPropertyVisibility(lwgui.perFrameData.propertyDatas[parentPropStaticData.name].property, material, lwgui)))
|
||||||
|
{
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 481e689c921a4e138739b26b793b41f6
|
||||||
|
timeCreated: 1687336811
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public class PresetHelper
|
||||||
|
{
|
||||||
|
private static Dictionary<string /*FileName*/, ShaderPropertyPreset> _loadedPresets = new Dictionary<string, ShaderPropertyPreset>();
|
||||||
|
|
||||||
|
private static bool _isInitComplete;
|
||||||
|
|
||||||
|
public static bool IsInitComplete { get { return _isInitComplete; } }
|
||||||
|
|
||||||
|
public static void Init()
|
||||||
|
{
|
||||||
|
if (!_isInitComplete)
|
||||||
|
{
|
||||||
|
ForceInit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ForceInit()
|
||||||
|
{
|
||||||
|
_loadedPresets.Clear();
|
||||||
|
_isInitComplete = false;
|
||||||
|
var GUIDs = AssetDatabase.FindAssets("t:" + typeof(ShaderPropertyPreset));
|
||||||
|
foreach (var GUID in GUIDs)
|
||||||
|
{
|
||||||
|
var preset = AssetDatabase.LoadAssetAtPath<ShaderPropertyPreset>(AssetDatabase.GUIDToAssetPath(GUID));
|
||||||
|
AddPreset(preset);
|
||||||
|
}
|
||||||
|
_isInitComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddPreset(ShaderPropertyPreset preset)
|
||||||
|
{
|
||||||
|
if (!preset) return;
|
||||||
|
if (!_loadedPresets.ContainsKey(preset.name))
|
||||||
|
{
|
||||||
|
_loadedPresets.Add(preset.name, preset);
|
||||||
|
// Debug.Log(preset.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShaderPropertyPreset GetPresetFile(string presetFileName)
|
||||||
|
{
|
||||||
|
if (!_loadedPresets.ContainsKey(presetFileName) || !_loadedPresets[presetFileName])
|
||||||
|
ForceInit();
|
||||||
|
|
||||||
|
if (!_loadedPresets.ContainsKey(presetFileName) || !_loadedPresets[presetFileName])
|
||||||
|
{
|
||||||
|
Debug.LogError("Invalid ShaderPropertyPreset: ‘" + presetFileName + "’ !");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _loadedPresets[presetFileName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 48e58de7087c42f88a2a18c52b6ce131
|
||||||
|
timeCreated: 1687336811
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public class RampHelper
|
||||||
|
{
|
||||||
|
#region RampEditor
|
||||||
|
[Serializable]
|
||||||
|
public class GradientObject : ScriptableObject
|
||||||
|
{
|
||||||
|
[SerializeField] public Gradient gradient = new Gradient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly string projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - 6);
|
||||||
|
|
||||||
|
public static string lastSavePath
|
||||||
|
{
|
||||||
|
get { return EditorPrefs.GetString("LWGUI_GradientSavePath_" + Application.version, Application.dataPath); }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.Contains(projectPath))
|
||||||
|
EditorPrefs.SetString("LWGUI_GradientSavePath_" + Application.version, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly GUIContent _iconAdd = new GUIContent(EditorGUIUtility.IconContent("d_Toolbar Plus").image, "Add"),
|
||||||
|
_iconEdit = new GUIContent(EditorGUIUtility.IconContent("editicon.sml").image, "Edit"),
|
||||||
|
_iconDiscard = new GUIContent(EditorGUIUtility.IconContent("d_TreeEditor.Refresh").image, "Discard"),
|
||||||
|
_iconSave = new GUIContent(EditorGUIUtility.IconContent("SaveActive").image, "Save");
|
||||||
|
|
||||||
|
public static bool RampEditor(
|
||||||
|
Rect buttonRect,
|
||||||
|
MaterialProperty prop,
|
||||||
|
SerializedProperty serializedProperty,
|
||||||
|
bool isDirty,
|
||||||
|
string defaultFileName,
|
||||||
|
string rootPath,
|
||||||
|
int defaultWidth,
|
||||||
|
int defaultHeight,
|
||||||
|
out Texture2D newTexture,
|
||||||
|
out bool doSave,
|
||||||
|
out bool doDiscard)
|
||||||
|
{
|
||||||
|
newTexture = null;
|
||||||
|
var hasChange = false;
|
||||||
|
var shouldCreate = false;
|
||||||
|
var singleButtonWidth = buttonRect.width * 0.25f;
|
||||||
|
var editRect = new Rect(buttonRect.x + singleButtonWidth * 0, buttonRect.y, singleButtonWidth, buttonRect.height);
|
||||||
|
var saveRect = new Rect(buttonRect.x + singleButtonWidth * 1, buttonRect.y, singleButtonWidth, buttonRect.height);
|
||||||
|
var addRect = new Rect(buttonRect.x + singleButtonWidth * 2, buttonRect.y, singleButtonWidth, buttonRect.height);
|
||||||
|
var discardRect = new Rect(buttonRect.x + singleButtonWidth * 3, buttonRect.y, singleButtonWidth, buttonRect.height);
|
||||||
|
|
||||||
|
// Edit button event
|
||||||
|
var currEvent = Event.current;
|
||||||
|
if (currEvent.type == EventType.MouseDown && editRect.Contains(currEvent.mousePosition))
|
||||||
|
{
|
||||||
|
// if the current edited texture is null, create new one
|
||||||
|
if (prop.textureValue == null)
|
||||||
|
{
|
||||||
|
shouldCreate = true;
|
||||||
|
currEvent.Use();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Undo.RecordObject(prop.textureValue, "Edit Gradient");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gradient Editor
|
||||||
|
var gradientPropertyRect = new Rect(editRect.x + 2, editRect.y + 2, editRect.width - 2, editRect.height - 2);
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
EditorGUI.PropertyField(gradientPropertyRect, serializedProperty, GUIContent.none);
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
{
|
||||||
|
hasChange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit button overlay
|
||||||
|
if (currEvent.type == EventType.Repaint)
|
||||||
|
{
|
||||||
|
var isHover = editRect.Contains(currEvent.mousePosition);
|
||||||
|
(new GUIStyle("button")).Draw(editRect, _iconEdit, isHover, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button
|
||||||
|
if (GUI.Button(addRect, _iconAdd) || shouldCreate)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(projectPath + rootPath))
|
||||||
|
Directory.CreateDirectory(projectPath + rootPath);
|
||||||
|
|
||||||
|
var absPath = EditorUtility.SaveFilePanel("Create New Ramp Texture", rootPath, defaultFileName, "png");
|
||||||
|
|
||||||
|
if (absPath.StartsWith(projectPath + rootPath))
|
||||||
|
{
|
||||||
|
//Create texture and save PNG
|
||||||
|
var saveUnityPath = absPath.Replace(projectPath, String.Empty);
|
||||||
|
CreateAndSaveNewGradientTexture(defaultWidth, defaultHeight, saveUnityPath);
|
||||||
|
// VersionControlHelper.Add(saveUnityPath);
|
||||||
|
//Load created texture
|
||||||
|
newTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(saveUnityPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (absPath != String.Empty)
|
||||||
|
{
|
||||||
|
var retry = EditorUtility.DisplayDialog("Invalid Path", "Please select the subdirectory of '" + projectPath + rootPath + "'", "Retry", "Cancel");
|
||||||
|
if (!retry) break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save button
|
||||||
|
var color = GUI.color;
|
||||||
|
if (isDirty) GUI.color = Color.yellow;
|
||||||
|
doSave = GUI.Button(saveRect, _iconSave);
|
||||||
|
GUI.color = color;
|
||||||
|
|
||||||
|
// Discard button
|
||||||
|
doDiscard = GUI.Button(discardRect, _iconDiscard);
|
||||||
|
|
||||||
|
return hasChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasGradient(AssetImporter assetImporter) { return assetImporter.userData.Contains("LWGUI");}
|
||||||
|
|
||||||
|
public static Gradient GetGradientFromTexture(Texture texture, out bool isDirty, bool doReimport = false)
|
||||||
|
{
|
||||||
|
isDirty = false;
|
||||||
|
if (texture == null) return null;
|
||||||
|
|
||||||
|
var assetImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture));
|
||||||
|
if (assetImporter != null && HasGradient(assetImporter))
|
||||||
|
{
|
||||||
|
GradientObject savedGradientObject, editingGradientObject;
|
||||||
|
isDirty = DecodeGradientFromJSON(assetImporter.userData, out savedGradientObject, out editingGradientObject);
|
||||||
|
return doReimport ? savedGradientObject.gradient : editingGradientObject.gradient;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("Can not find texture: "
|
||||||
|
+ texture.name
|
||||||
|
+ " or it's userData on disk! \n"
|
||||||
|
+ "If you are moving or copying the Ramp Map, make sure your .meta file is not lost!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetGradientToTexture(Texture texture, GradientObject gradientObject, bool doSaveToDisk = false)
|
||||||
|
{
|
||||||
|
if (texture == null || gradientObject.gradient == null) return;
|
||||||
|
|
||||||
|
var texture2D = (Texture2D)texture;
|
||||||
|
// Save to texture
|
||||||
|
var path = AssetDatabase.GetAssetPath(texture);
|
||||||
|
var pixels = GetPixelsFromGradient(gradientObject.gradient, texture.width, texture.height);
|
||||||
|
texture2D.SetPixels32(pixels);
|
||||||
|
texture2D.Apply();
|
||||||
|
|
||||||
|
// Save gradient JSON to userData
|
||||||
|
var assetImporter = AssetImporter.GetAtPath(path);
|
||||||
|
GradientObject savedGradientObject, editingGradientObject;
|
||||||
|
DecodeGradientFromJSON(assetImporter.userData, out savedGradientObject, out editingGradientObject);
|
||||||
|
assetImporter.userData = EncodeGradientToJSON(doSaveToDisk ? gradientObject : savedGradientObject, gradientObject);
|
||||||
|
|
||||||
|
// Save texture to disk
|
||||||
|
if (doSaveToDisk)
|
||||||
|
{
|
||||||
|
var systemPath = projectPath + path;
|
||||||
|
VersionControlHelper.Checkout(path);
|
||||||
|
File.WriteAllBytes(systemPath, texture2D.EncodeToPNG());
|
||||||
|
assetImporter.SaveAndReimport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EncodeGradientToJSON(GradientObject savedGradientObject, GradientObject editingGradientObject)
|
||||||
|
{
|
||||||
|
string savedJSON = " ", editingJSON = " ";
|
||||||
|
if (savedGradientObject != null)
|
||||||
|
savedJSON = EditorJsonUtility.ToJson(savedGradientObject);
|
||||||
|
if (editingGradientObject != null)
|
||||||
|
editingJSON = EditorJsonUtility.ToJson(editingGradientObject);
|
||||||
|
|
||||||
|
return savedJSON + "#" + editingJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DecodeGradientFromJSON(string json, out GradientObject savedGradientObject, out GradientObject editingGradientObject)
|
||||||
|
{
|
||||||
|
var subJSONs = json.Split('#');
|
||||||
|
savedGradientObject = ScriptableObject.CreateInstance<GradientObject>();
|
||||||
|
if (subJSONs[0] != " ")
|
||||||
|
EditorJsonUtility.FromJsonOverwrite(subJSONs[0], savedGradientObject);
|
||||||
|
editingGradientObject = ScriptableObject.CreateInstance<GradientObject>();
|
||||||
|
if (subJSONs[1] != " ")
|
||||||
|
EditorJsonUtility.FromJsonOverwrite(subJSONs[1], editingGradientObject);
|
||||||
|
return subJSONs[0] != subJSONs[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CreateAndSaveNewGradientTexture(int width, int height, string unityPath)
|
||||||
|
{
|
||||||
|
var gradientObject = ScriptableObject.CreateInstance<GradientObject>();
|
||||||
|
gradientObject.gradient = new Gradient();
|
||||||
|
gradientObject.gradient.colorKeys = new[] { new GradientColorKey(Color.black, 0.0f), new GradientColorKey(Color.white, 1.0f) };
|
||||||
|
gradientObject.gradient.alphaKeys = new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) };
|
||||||
|
|
||||||
|
var ramp = CreateGradientTexture(gradientObject.gradient, width, height);
|
||||||
|
var png = ramp.EncodeToPNG();
|
||||||
|
Object.DestroyImmediate(ramp);
|
||||||
|
|
||||||
|
var systemPath = projectPath + unityPath;
|
||||||
|
File.WriteAllBytes(systemPath, png);
|
||||||
|
|
||||||
|
AssetDatabase.ImportAsset(unityPath);
|
||||||
|
var textureImporter = AssetImporter.GetAtPath(unityPath) as TextureImporter;
|
||||||
|
textureImporter.wrapMode = TextureWrapMode.Clamp;
|
||||||
|
textureImporter.isReadable = true;
|
||||||
|
textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
textureImporter.alphaSource = TextureImporterAlphaSource.FromInput;
|
||||||
|
textureImporter.mipmapEnabled = false;
|
||||||
|
|
||||||
|
var platformTextureSettings = textureImporter.GetDefaultPlatformTextureSettings();
|
||||||
|
platformTextureSettings.format = TextureImporterFormat.ARGB32;
|
||||||
|
platformTextureSettings.textureCompression = TextureImporterCompression.Uncompressed;
|
||||||
|
textureImporter.SetPlatformTextureSettings(platformTextureSettings);
|
||||||
|
|
||||||
|
//Gradient data embedded in userData
|
||||||
|
textureImporter.userData = EncodeGradientToJSON(gradientObject, gradientObject);
|
||||||
|
textureImporter.SaveAndReimport();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture2D CreateGradientTexture(Gradient gradient, int width, int height)
|
||||||
|
{
|
||||||
|
var ramp = new Texture2D(width, height, TextureFormat.RGBA32, true, true);
|
||||||
|
var colors = GetPixelsFromGradient(gradient, width, height);
|
||||||
|
ramp.SetPixels32(colors);
|
||||||
|
ramp.Apply();
|
||||||
|
return ramp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color32[] GetPixelsFromGradient(Gradient gradient, int width, int height)
|
||||||
|
{
|
||||||
|
var pixels = new Color32[width * height];
|
||||||
|
for (var x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
var delta = x / (float)width;
|
||||||
|
if (delta < 0) delta = 0;
|
||||||
|
if (delta > 1) delta = 1;
|
||||||
|
var col = gradient.Evaluate(delta);
|
||||||
|
for (int i = 0; i < height; i++)
|
||||||
|
{
|
||||||
|
pixels[x + i * width] = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region RampSelector
|
||||||
|
public delegate void SwitchRampMapEvent(Texture2D selectedRamp);
|
||||||
|
|
||||||
|
public static void RampSelector(Rect rect, string rootPath, SwitchRampMapEvent switchRampMapEvent)
|
||||||
|
{
|
||||||
|
var e = Event.current;
|
||||||
|
if (e.type == UnityEngine.EventType.MouseDown && rect.Contains(e.mousePosition))
|
||||||
|
{
|
||||||
|
e.Use();
|
||||||
|
var textureGUIDs = AssetDatabase.FindAssets("t:Texture2D", new[] { rootPath });
|
||||||
|
var rampMaps = textureGUIDs.Select((GUID) =>
|
||||||
|
{
|
||||||
|
var path = AssetDatabase.GUIDToAssetPath(GUID);
|
||||||
|
var assetImporter = AssetImporter.GetAtPath(path);
|
||||||
|
if (HasGradient(assetImporter))
|
||||||
|
{
|
||||||
|
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}).ToArray();
|
||||||
|
RampSelectorWindow.ShowWindow(rect, rampMaps, switchRampMapEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RampSelectorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private Texture2D[] _rampMaps;
|
||||||
|
private Vector2 _scrollPosition;
|
||||||
|
private RampHelper.SwitchRampMapEvent _switchRampMapEvent;
|
||||||
|
|
||||||
|
public static void ShowWindow(Rect rect, Texture2D[] rampMaps, RampHelper.SwitchRampMapEvent switchRampMapEvent)
|
||||||
|
{
|
||||||
|
RampSelectorWindow window = ScriptableObject.CreateInstance<RampSelectorWindow>();
|
||||||
|
window.titleContent = new GUIContent("Ramp Selector");
|
||||||
|
window.minSize = new Vector2(400, 500);
|
||||||
|
window._rampMaps = rampMaps;
|
||||||
|
window._switchRampMapEvent = switchRampMapEvent;
|
||||||
|
window.ShowAuxWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||||
|
|
||||||
|
foreach (Texture2D rampMap in _rampMaps)
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
if (rampMap != null)
|
||||||
|
{
|
||||||
|
var guiContent = new GUIContent(rampMap.name);
|
||||||
|
var rect = EditorGUILayout.GetControlRect();
|
||||||
|
var buttonWidth = Mathf.Min(300f, Mathf.Max(GUI.skin.button.CalcSize(guiContent).x, rect.width * 0.35f));
|
||||||
|
var buttonRect = new Rect(rect.x + rect.width - buttonWidth, rect.y, buttonWidth, rect.height);
|
||||||
|
var previewRect = new Rect(rect.x, rect.y, rect.width - buttonWidth - 3.0f, rect.height);
|
||||||
|
if (GUI.Button(buttonRect, guiContent) && _switchRampMapEvent != null)
|
||||||
|
{
|
||||||
|
_switchRampMapEvent(rampMap);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
EditorGUI.DrawPreviewTexture(previewRect, rampMap);
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4581bed83f9b41e18444cb08b59ce839
|
||||||
|
timeCreated: 1687336811
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public class ReflectionHelper
|
||||||
|
{
|
||||||
|
private static Assembly UnityEditor_Assembly = Assembly.GetAssembly(typeof(Editor));
|
||||||
|
|
||||||
|
#region MaterialPropertyHandler
|
||||||
|
private static Type MaterialPropertyHandler_Type = UnityEditor_Assembly.GetType("UnityEditor.MaterialPropertyHandler");
|
||||||
|
private static MethodInfo MaterialPropertyHandler_GetHandler_Method = MaterialPropertyHandler_Type.GetMethod("GetHandler", BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
private static PropertyInfo MaterialPropertyHandler_PropertyDrawer_Property = MaterialPropertyHandler_Type.GetProperty("propertyDrawer");
|
||||||
|
private static FieldInfo MaterialPropertyHandler_DecoratorDrawers_Field = MaterialPropertyHandler_Type.GetField("m_DecoratorDrawers", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
public static MaterialPropertyDrawer GetPropertyDrawer(Shader shader, MaterialProperty prop, out List<MaterialPropertyDrawer> decoratorDrawers)
|
||||||
|
{
|
||||||
|
decoratorDrawers = new List<MaterialPropertyDrawer>();
|
||||||
|
var handler = MaterialPropertyHandler_GetHandler_Method.Invoke(null, new System.Object[] { shader, prop.name });
|
||||||
|
if (handler != null && handler.GetType() == MaterialPropertyHandler_Type)
|
||||||
|
{
|
||||||
|
decoratorDrawers = MaterialPropertyHandler_DecoratorDrawers_Field.GetValue(handler) as List<MaterialPropertyDrawer>;
|
||||||
|
return MaterialPropertyHandler_PropertyDrawer_Property.GetValue(handler, null) as MaterialPropertyDrawer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MaterialPropertyDrawer GetPropertyDrawer(Shader shader, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
List<MaterialPropertyDrawer> decoratorDrawers;
|
||||||
|
return GetPropertyDrawer(shader, prop, out decoratorDrawers);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MaterialEditor
|
||||||
|
private static Type MaterialEditor_Type = typeof(MaterialEditor);
|
||||||
|
private static MethodInfo MaterialEditor_DoPowerRangeProperty_Method = MaterialEditor_Type.GetMethod("DoPowerRangeProperty", BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
private static MethodInfo MaterialEditor_DefaultShaderPropertyInternal_Method = MaterialEditor_Type.GetMethod("DefaultShaderPropertyInternal", BindingFlags.NonPublic | BindingFlags.Instance, null,
|
||||||
|
new []{typeof(Rect), typeof(MaterialProperty), typeof(GUIContent)}, null);
|
||||||
|
#if !UNITY_2019_2_OR_NEWER
|
||||||
|
private static FieldInfo MaterialEditor_CustomShaderGUI_Field = MaterialEditor_Type.GetField("m_CustomShaderGUI", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public static ShaderGUI GetCustomShaderGUI(MaterialEditor editor)
|
||||||
|
{
|
||||||
|
#if !UNITY_2019_2_OR_NEWER
|
||||||
|
return MaterialEditor_CustomShaderGUI_Field.GetValue(editor) as ShaderGUI;
|
||||||
|
#else
|
||||||
|
return editor.customShaderGUI;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float DoPowerRangeProperty(Rect position, MaterialProperty prop, GUIContent label, float power)
|
||||||
|
{
|
||||||
|
return (float)MaterialEditor_DoPowerRangeProperty_Method.Invoke(null, new System.Object[] { position, prop, label, power });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DefaultShaderPropertyInternal(MaterialEditor editor, Rect position, MaterialProperty prop, GUIContent label)
|
||||||
|
{
|
||||||
|
MaterialEditor_DefaultShaderPropertyInternal_Method.Invoke(editor, new System.Object[] { position, prop, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region EditorUtility
|
||||||
|
private static Type EditorUtility_Type = typeof(EditorUtility);
|
||||||
|
private static MethodInfo EditorUtility_DisplayCustomMenuWithSeparators_Method = EditorUtility_Type.GetMethod("DisplayCustomMenuWithSeparators", BindingFlags.NonPublic | BindingFlags.Static, null,
|
||||||
|
new []{typeof(Rect), typeof(string[]), typeof(bool[]), typeof(bool[]), typeof(int[]), typeof(EditorUtility.SelectMenuItemFunction), typeof(object), typeof(bool)}, null);
|
||||||
|
|
||||||
|
public static void DisplayCustomMenuWithSeparators(
|
||||||
|
Rect position,
|
||||||
|
string[] options,
|
||||||
|
bool[] enabled,
|
||||||
|
bool[] separator,
|
||||||
|
int[] selected,
|
||||||
|
EditorUtility.SelectMenuItemFunction callback,
|
||||||
|
object userData = null,
|
||||||
|
bool showHotkey = false)
|
||||||
|
{
|
||||||
|
EditorUtility_DisplayCustomMenuWithSeparators_Method.Invoke(null, new System.Object[] { position, options, enabled, separator, selected, callback, userData, showHotkey });
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MaterialEnumDrawer
|
||||||
|
// UnityEditor.MaterialEnumDrawer(string enumName)
|
||||||
|
private static System.Type[] _types;
|
||||||
|
|
||||||
|
public static System.Type[] GetAllTypes()
|
||||||
|
{
|
||||||
|
if (_types == null)
|
||||||
|
{
|
||||||
|
_types = ((IEnumerable<Assembly>)AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
.SelectMany<Assembly, System.Type>((Func<Assembly, IEnumerable<System.Type>>)
|
||||||
|
(assembly =>
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
return (IEnumerable<System.Type>)(new System.Type[0]);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (IEnumerable<System.Type>)assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
Debug.LogError(ex);
|
||||||
|
return (IEnumerable<System.Type>)(new System.Type[0]);
|
||||||
|
}
|
||||||
|
})).ToArray<System.Type>();
|
||||||
|
}
|
||||||
|
return _types;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Ramp
|
||||||
|
private static Type EditorWindow_Type = typeof(EditorWindow);
|
||||||
|
private static MethodInfo EditorWindow_ShowModal_Method = EditorWindow_Type.GetMethod("ShowModal", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// UnityEditor.EditorWindow.ShowModal
|
||||||
|
// Other windows will not be accessible and any script recompilation will not happen until this window is closed
|
||||||
|
// https://docs.unity3d.com/ScriptReference/EditorWindow.ShowModal.html
|
||||||
|
public static void ShowModal(EditorWindow window)
|
||||||
|
{
|
||||||
|
#if UNITY_2019_3_OR_NEWER
|
||||||
|
window.ShowModal();
|
||||||
|
#else
|
||||||
|
EditorWindow_ShowModal_Method.Invoke(window, null);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MaterialProperty.PropertyData
|
||||||
|
#if UNITY_2022_1_OR_NEWER
|
||||||
|
private static Type MaterialProperty_Type = typeof(MaterialProperty);
|
||||||
|
private static Type PropertyData_Type = MaterialProperty_Type.GetNestedType("PropertyData", BindingFlags.NonPublic);
|
||||||
|
// MergeStack(out bool lockedInChildren, out bool lockedByAncestor, out bool overriden)
|
||||||
|
private static MethodInfo PropertyData_MergeStack_Method = PropertyData_Type.GetMethod("MergeStack", BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
// HandleApplyRevert(GenericMenu menu, bool singleEditing, UnityEngine.Object[] targets)
|
||||||
|
private static MethodInfo PropertyData_HandleApplyRevert_Method = PropertyData_Type.GetMethod("HandleApplyRevert", BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
public static void HandleApplyRevert(GenericMenu menu, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
System.Object[] parameters = new System.Object[3];
|
||||||
|
ReflectionHelper.PropertyData_MergeStack_Method.Invoke(null, parameters);
|
||||||
|
bool lockedInChildren = (bool)parameters[0];
|
||||||
|
bool lockedByAncestor = (bool)parameters[1];
|
||||||
|
bool overriden = (bool)parameters[2];
|
||||||
|
bool singleEditing = prop.targets.Length == 1;
|
||||||
|
|
||||||
|
if (overriden)
|
||||||
|
{
|
||||||
|
ReflectionHelper.PropertyData_HandleApplyRevert_Method.Invoke(null, new System.Object[]{menu, singleEditing, prop.targets});
|
||||||
|
menu.AddSeparator("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3451be150a8485bbd83e66540024f35
|
||||||
|
timeCreated: 1687336811
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helpers for drawing Unreal Style Revertable Shader GUI
|
||||||
|
/// </summary>
|
||||||
|
public class RevertableHelper
|
||||||
|
{
|
||||||
|
public static readonly float revertButtonWidth = 15f;
|
||||||
|
public static float fieldWidth;
|
||||||
|
public static float labelWidth;
|
||||||
|
|
||||||
|
#region GUI Setting
|
||||||
|
|
||||||
|
public static void IndentRect(ref Rect rect)
|
||||||
|
{
|
||||||
|
rect.xMax -= RevertableHelper.revertButtonWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rect SplitRevertButtonRect(ref Rect rect, bool isCallInDrawer = false)
|
||||||
|
{
|
||||||
|
float defaultHeightWithoutDrawers = EditorGUIUtility.singleLineHeight;
|
||||||
|
var revertButtonRect = GetRevertButtonRect(defaultHeightWithoutDrawers, rect, isCallInDrawer);
|
||||||
|
IndentRect(ref rect);
|
||||||
|
return revertButtonRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rect GetRevertButtonRect(float propHeight, Rect rect, bool isCallInDrawer = false)
|
||||||
|
{
|
||||||
|
if (isCallInDrawer) rect.xMax += revertButtonWidth;
|
||||||
|
var revertButtonRect = new Rect(rect.xMax - revertButtonWidth + 2f,
|
||||||
|
rect.yMax - propHeight * 0.5f - revertButtonWidth * 0.5f,
|
||||||
|
revertButtonWidth - 2f,
|
||||||
|
revertButtonWidth - 3f);
|
||||||
|
return revertButtonRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InitRevertableGUIWidths()
|
||||||
|
{
|
||||||
|
EditorGUIUtility.fieldWidth += RevertableHelper.revertButtonWidth;
|
||||||
|
EditorGUIUtility.labelWidth -= RevertableHelper.revertButtonWidth;
|
||||||
|
RevertableHelper.fieldWidth = EditorGUIUtility.fieldWidth;
|
||||||
|
RevertableHelper.labelWidth = EditorGUIUtility.labelWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetRevertableGUIWidths()
|
||||||
|
{
|
||||||
|
EditorGUIUtility.fieldWidth = RevertableHelper.fieldWidth;
|
||||||
|
EditorGUIUtility.labelWidth = RevertableHelper.labelWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FixGUIWidthMismatch(MaterialProperty.PropType propType, MaterialEditor materialEditor)
|
||||||
|
{
|
||||||
|
switch (propType)
|
||||||
|
{
|
||||||
|
case MaterialProperty.PropType.Texture:
|
||||||
|
case MaterialProperty.PropType.Range:
|
||||||
|
materialEditor.SetDefaultGUIWidths();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RevertableHelper.SetRevertableGUIWidths();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Property Handle
|
||||||
|
|
||||||
|
public static void SetPropertyToDefault(MaterialProperty defaultProp, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
prop.vectorValue = defaultProp.vectorValue;
|
||||||
|
prop.colorValue = defaultProp.colorValue;
|
||||||
|
prop.floatValue = defaultProp.floatValue;
|
||||||
|
prop.textureValue = defaultProp.textureValue;
|
||||||
|
#if UNITY_2021_1_OR_NEWER
|
||||||
|
prop.intValue = defaultProp.intValue;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPropertyDefaultValueText(MaterialProperty defaultProp)
|
||||||
|
{
|
||||||
|
string defaultText = String.Empty;
|
||||||
|
switch (defaultProp.type)
|
||||||
|
{
|
||||||
|
case MaterialProperty.PropType.Color:
|
||||||
|
defaultText += defaultProp.colorValue;
|
||||||
|
break;
|
||||||
|
case MaterialProperty.PropType.Float:
|
||||||
|
case MaterialProperty.PropType.Range:
|
||||||
|
defaultText += defaultProp.floatValue;
|
||||||
|
break;
|
||||||
|
#if UNITY_2021_1_OR_NEWER
|
||||||
|
case MaterialProperty.PropType.Int:
|
||||||
|
defaultText += defaultProp.intValue;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case MaterialProperty.PropType.Texture:
|
||||||
|
defaultText += defaultProp.textureValue != null ? defaultProp.textureValue.name : "None";
|
||||||
|
break;
|
||||||
|
case MaterialProperty.PropType.Vector:
|
||||||
|
defaultText += defaultProp.vectorValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return defaultText;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Draw revert button
|
||||||
|
|
||||||
|
public static bool DrawRevertableProperty(Rect position, MaterialProperty prop, LWGUI lwgui, bool isHeader = false)
|
||||||
|
{
|
||||||
|
bool hasModified = prop.hasMixedValue;
|
||||||
|
|
||||||
|
var propDynamicData = lwgui.perFrameData.propertyDatas[prop.name];
|
||||||
|
var propStaticData = lwgui.perShaderData.propertyDatas[prop.name];
|
||||||
|
if (!hasModified)
|
||||||
|
hasModified = propDynamicData.hasModified;
|
||||||
|
|
||||||
|
if (!hasModified && isHeader)
|
||||||
|
hasModified = propDynamicData.hasChildrenModified;
|
||||||
|
|
||||||
|
var extraPropNames = lwgui.perShaderData.propertyDatas[prop.name].extraPropNames;
|
||||||
|
if (!hasModified && extraPropNames.Count > 0)
|
||||||
|
hasModified = extraPropNames.Any((extraPropName => lwgui.perFrameData.propertyDatas[extraPropName].hasModified));
|
||||||
|
|
||||||
|
if (!hasModified)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Rect rect = position;
|
||||||
|
if (DrawRevertButton(rect))
|
||||||
|
{
|
||||||
|
DoRevertProperty(prop, lwgui);
|
||||||
|
|
||||||
|
foreach (var childStaticData in propStaticData.children)
|
||||||
|
{
|
||||||
|
DoRevertProperty(lwgui.perFrameData.propertyDatas[childStaticData.name].property, lwgui);
|
||||||
|
foreach (var childChildStaticData in childStaticData.children)
|
||||||
|
DoRevertProperty(lwgui.perFrameData.propertyDatas[childChildStaticData.name].property, lwgui);
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh keywords
|
||||||
|
MaterialEditor.ApplyMaterialPropertyDrawers(lwgui.materialEditor.targets);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoRevertProperty(MaterialProperty prop, LWGUI lwgui)
|
||||||
|
{
|
||||||
|
var propDynamicData = lwgui.perFrameData.propertyDatas[prop.name];
|
||||||
|
var extraPropNames = lwgui.perShaderData.propertyDatas[prop.name].extraPropNames;
|
||||||
|
propDynamicData.hasRevertChanged = true;
|
||||||
|
SetPropertyToDefault(propDynamicData.defualtProperty, prop);
|
||||||
|
foreach (var extraPropName in extraPropNames)
|
||||||
|
{
|
||||||
|
var extraPropDynamicData = lwgui.perFrameData.propertyDatas[extraPropName];
|
||||||
|
extraPropDynamicData.hasRevertChanged = true;
|
||||||
|
SetPropertyToDefault(extraPropDynamicData.defualtProperty, extraPropDynamicData.property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Texture _icon = AssetDatabase.LoadAssetAtPath<Texture>(AssetDatabase.GUIDToAssetPath("e7bc1130858d984488bca32b8512ca96"));
|
||||||
|
|
||||||
|
public static bool DrawRevertButton(Rect rect)
|
||||||
|
{
|
||||||
|
if (_icon == null) Debug.LogError("RevertIcon.png + meta is missing!");
|
||||||
|
GUI.DrawTexture(rect, _icon);
|
||||||
|
var e = Event.current;
|
||||||
|
if (e.type == UnityEngine.EventType.MouseDown && rect.Contains(e.mousePosition))
|
||||||
|
{
|
||||||
|
e.Use();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 259535769835488e9edaf21cbe36465d
|
||||||
|
timeCreated: 1687336811
|
||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: af7e12583264b8c4da8dcd69df274793
|
guid: b7661dfeec673f340b66ac769fa42d18
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public class ShaderModifyListener : AssetPostprocessor
|
||||||
|
{
|
||||||
|
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
|
||||||
|
{
|
||||||
|
foreach (var assetPath in importedAssets)
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(assetPath).Equals(".shader", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var shader = AssetDatabase.LoadAssetAtPath<Shader>(assetPath);
|
||||||
|
MetaDataHelper.ForceRebuildPerShaderData(shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6143a217ed5c4b709eb585de3e8026d0
|
||||||
|
timeCreated: 1693983094
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
// Copyright (c) 2022 Jason Ma
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(fileName = "LWGUI_ShaderPropertyPreset.asset", menuName = "LWGUI/Shader Property Preset")]
|
||||||
|
public class ShaderPropertyPreset : ScriptableObject
|
||||||
|
{
|
||||||
|
public enum PropertyType
|
||||||
|
{
|
||||||
|
Color,
|
||||||
|
Vector,
|
||||||
|
Float,
|
||||||
|
Range,
|
||||||
|
Texture,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class PropertyValue
|
||||||
|
{
|
||||||
|
public PropertyValue(MaterialProperty prop)
|
||||||
|
{
|
||||||
|
CopyFromMaterialProperty(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string propertyName;
|
||||||
|
public PropertyType propertyType;
|
||||||
|
public float floatValue;
|
||||||
|
public Color colorValue;
|
||||||
|
public Vector4 vectorValue;
|
||||||
|
public Texture textureValue;
|
||||||
|
|
||||||
|
private int propertyNameID = -1;
|
||||||
|
|
||||||
|
public void Apply(Material material, bool isDefaultMaterial, PerFrameData perFrameData = null)
|
||||||
|
{
|
||||||
|
if (propertyNameID == -1 || !material.HasProperty(propertyNameID))
|
||||||
|
propertyNameID = Shader.PropertyToID(propertyName);
|
||||||
|
if (!material.HasProperty(propertyNameID))
|
||||||
|
{
|
||||||
|
var propertyNameLower = propertyName.ToLower();
|
||||||
|
switch (propertyNameLower)
|
||||||
|
{
|
||||||
|
case "renderqueue":
|
||||||
|
material.renderQueue = (int)floatValue;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
// Debug.LogWarning("Unable to find Preset Property: " + propertyName + " in Material: " + material + "!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPropertyOtherMaterials = !isDefaultMaterial && perFrameData == null;
|
||||||
|
if (isPropertyOtherMaterials || isDefaultMaterial)
|
||||||
|
{
|
||||||
|
switch (propertyType)
|
||||||
|
{
|
||||||
|
case PropertyType.Color:
|
||||||
|
material.SetColor(propertyNameID, colorValue);
|
||||||
|
break;
|
||||||
|
case PropertyType.Vector:
|
||||||
|
material.SetVector(propertyNameID, vectorValue);
|
||||||
|
break;
|
||||||
|
case PropertyType.Float:
|
||||||
|
case PropertyType.Range:
|
||||||
|
material.SetFloat(propertyNameID, floatValue);
|
||||||
|
break;
|
||||||
|
case PropertyType.Texture:
|
||||||
|
material.SetTexture(propertyNameID, textureValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPropertyOtherMaterials)
|
||||||
|
MaterialEditor.ApplyMaterialPropertyDrawers(material);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// is Property Primary Material
|
||||||
|
{
|
||||||
|
var propDynamicData = perFrameData.propertyDatas[propertyName];
|
||||||
|
var prop = propDynamicData.property;
|
||||||
|
switch (propertyType)
|
||||||
|
{
|
||||||
|
case PropertyType.Color:
|
||||||
|
prop.colorValue = colorValue;
|
||||||
|
break;
|
||||||
|
case PropertyType.Vector:
|
||||||
|
prop.vectorValue = vectorValue;
|
||||||
|
break;
|
||||||
|
case PropertyType.Float:
|
||||||
|
case PropertyType.Range:
|
||||||
|
prop.floatValue = floatValue;
|
||||||
|
break;
|
||||||
|
case PropertyType.Texture:
|
||||||
|
prop.textureValue = textureValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
propDynamicData.hasRevertChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyFromMaterialProperty(MaterialProperty prop)
|
||||||
|
{
|
||||||
|
propertyName = prop.name;
|
||||||
|
switch (prop.type)
|
||||||
|
{
|
||||||
|
case MaterialProperty.PropType.Color:
|
||||||
|
propertyType = PropertyType.Color;
|
||||||
|
colorValue = prop.colorValue;
|
||||||
|
break;
|
||||||
|
case MaterialProperty.PropType.Vector:
|
||||||
|
propertyType = PropertyType.Vector;
|
||||||
|
vectorValue = prop.vectorValue;
|
||||||
|
break;
|
||||||
|
#if UNITY_2021_1_OR_NEWER
|
||||||
|
case MaterialProperty.PropType.Int:
|
||||||
|
#endif
|
||||||
|
case MaterialProperty.PropType.Float:
|
||||||
|
propertyType = PropertyType.Float;
|
||||||
|
floatValue = prop.floatValue;
|
||||||
|
break;
|
||||||
|
case MaterialProperty.PropType.Range:
|
||||||
|
propertyType = PropertyType.Range;
|
||||||
|
floatValue = prop.floatValue;
|
||||||
|
break;
|
||||||
|
case MaterialProperty.PropType.Texture:
|
||||||
|
propertyType = PropertyType.Texture;
|
||||||
|
textureValue = prop.textureValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnValidate()
|
||||||
|
{
|
||||||
|
propertyNameID = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class Preset
|
||||||
|
{
|
||||||
|
public string presetName;
|
||||||
|
public List<PropertyValue> propertyValues = new List<PropertyValue>();
|
||||||
|
|
||||||
|
public void ApplyToDefaultMaterial(Material material)
|
||||||
|
{
|
||||||
|
foreach (var propertyValue in propertyValues)
|
||||||
|
propertyValue.Apply(material, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyValue GetPropertyValue(string propName)
|
||||||
|
{
|
||||||
|
PropertyValue result = null;
|
||||||
|
if (propertyValues != null)
|
||||||
|
{
|
||||||
|
foreach (var propertyValue in propertyValues)
|
||||||
|
{
|
||||||
|
if (propertyValue.propertyName == propName)
|
||||||
|
{
|
||||||
|
result = propertyValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOrUpdate(MaterialProperty prop)
|
||||||
|
{
|
||||||
|
var propertyValue = GetPropertyValue(prop.name);
|
||||||
|
if (propertyValue != null)
|
||||||
|
propertyValue.CopyFromMaterialProperty(prop);
|
||||||
|
else
|
||||||
|
propertyValues.Add(new PropertyValue(prop));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOrUpdateIncludeExtraProperties(LWGUI lwgui, MaterialProperty prop)
|
||||||
|
{
|
||||||
|
AddOrUpdate(prop);
|
||||||
|
foreach (var extraPropName in lwgui.perShaderData.propertyDatas[prop.name].extraPropNames)
|
||||||
|
{
|
||||||
|
AddOrUpdate(lwgui.perFrameData.propertyDatas[extraPropName].property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string propName)
|
||||||
|
{
|
||||||
|
var propertyValue = GetPropertyValue(propName);
|
||||||
|
if (propertyValue != null)
|
||||||
|
propertyValues.Remove(propertyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveIncludeExtraProperties(LWGUI lwgui, string propName)
|
||||||
|
{
|
||||||
|
Remove(propName);
|
||||||
|
foreach (var extraPropName in lwgui.perShaderData.propertyDatas[propName].extraPropNames)
|
||||||
|
{
|
||||||
|
Remove(lwgui.perFrameData.propertyDatas[extraPropName].property.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Preset> presets;
|
||||||
|
|
||||||
|
// private void Awake()
|
||||||
|
// {
|
||||||
|
// Debug.Log($"{this.name} Awake");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void OnDestroy()
|
||||||
|
// {
|
||||||
|
// Debug.Log($"{this.name} OnDestroy");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void OnDisable()
|
||||||
|
// {
|
||||||
|
// Debug.Log($"{this.name} OnDisable");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void Reset()
|
||||||
|
// {
|
||||||
|
// Debug.Log($"{this.name} Reset");
|
||||||
|
// }
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
// Debug.Log($"{this.name} OnValidate");
|
||||||
|
PresetHelper.ForceInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Debug.Log($"{this.name} OnEnable");
|
||||||
|
// manually added when a preset is manually created
|
||||||
|
if (PresetHelper.IsInitComplete)
|
||||||
|
PresetHelper.AddPreset(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToMaterials(UnityEngine.Object[] materials, int presetIndex, PerFrameData perFrameData)
|
||||||
|
{
|
||||||
|
if (this.presets.Count == 0) return;
|
||||||
|
|
||||||
|
int index = (int)Mathf.Min(this.presets.Count - 1, Mathf.Max(0, presetIndex));
|
||||||
|
foreach (var propertyValue in this.presets[index].propertyValues)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < materials.Length; i++)
|
||||||
|
{
|
||||||
|
var material = materials[i] as Material;
|
||||||
|
propertyValue.Apply(material, false, i == 0 ? perFrameData : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 28fbcbca3fb14507af6ed5c104c40b84
|
||||||
|
timeCreated: 1667657706
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) Jason Ma
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.VersionControl;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace LWGUI
|
||||||
|
{
|
||||||
|
public class VersionControlHelper
|
||||||
|
{
|
||||||
|
public static bool isVCEnabled { get { return Provider.enabled && Provider.isActive; } }
|
||||||
|
|
||||||
|
public static bool Checkout(UnityEngine.Object obj)
|
||||||
|
{
|
||||||
|
if (AssetDatabase.Contains(obj))
|
||||||
|
{
|
||||||
|
var path = AssetDatabase.GetAssetPath(obj);
|
||||||
|
return Checkout(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Checkout(string projectRelativedPath)
|
||||||
|
{
|
||||||
|
if (isVCEnabled)
|
||||||
|
{
|
||||||
|
var vcAsset = Provider.GetAssetByPath(projectRelativedPath);
|
||||||
|
if (vcAsset != null)
|
||||||
|
{
|
||||||
|
var statusTask = Provider.Status(vcAsset);
|
||||||
|
statusTask.Wait();
|
||||||
|
if (Provider.CheckoutIsValid(statusTask.assetList))
|
||||||
|
{
|
||||||
|
var checkOutTask = Provider.Checkout(vcAsset, CheckoutMode.Both);
|
||||||
|
checkOutTask.Wait();
|
||||||
|
if (checkOutTask.success)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Provider.IsOpenForEdit(vcAsset))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogError("Checkout '" + projectRelativedPath + "' failure!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Add(string projectRelativedPath)
|
||||||
|
{
|
||||||
|
if (isVCEnabled)
|
||||||
|
{
|
||||||
|
var vcAsset = Provider.GetAssetByPath(projectRelativedPath);
|
||||||
|
if (vcAsset != null)
|
||||||
|
{
|
||||||
|
var statusTask = Provider.Status(vcAsset);
|
||||||
|
statusTask.Wait();
|
||||||
|
if (Provider.AddIsValid(statusTask.assetList))
|
||||||
|
{
|
||||||
|
var addTask = Provider.Add(vcAsset, false);
|
||||||
|
addTask.Wait();
|
||||||
|
if (addTask.success)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogError("Add '" + projectRelativedPath + "' failure!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f8e9e3181ad14421a67f641ab4aadc5c
|
||||||
|
timeCreated: 1687333459
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8dc030a56b7172d40934e0f5c7f186a6
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,286 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!1 &3419683162514475710
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 7063479915286433301}
|
|
||||||
- component: {fileID: 1913988771854760432}
|
|
||||||
- component: {fileID: 7242343237021918190}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: Layer1
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &7063479915286433301
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3419683162514475710}
|
|
||||||
serializedVersion: 2
|
|
||||||
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: 8299643768118427544}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1839735485 &1913988771854760432
|
|
||||||
Tilemap:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3419683162514475710}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_Tiles:
|
|
||||||
- first: {x: -1, y: -2, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 4
|
|
||||||
m_TileSpriteIndex: 4
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
- first: {x: 0, y: -2, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 5
|
|
||||||
m_TileSpriteIndex: 5
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
- first: {x: -1, y: -1, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 2
|
|
||||||
m_TileSpriteIndex: 2
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
- first: {x: 0, y: -1, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 3
|
|
||||||
m_TileSpriteIndex: 3
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
- first: {x: -1, y: 0, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 0
|
|
||||||
m_TileSpriteIndex: 0
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
- first: {x: 0, y: 0, z: 0}
|
|
||||||
second:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_TileIndex: 1
|
|
||||||
m_TileSpriteIndex: 1
|
|
||||||
m_TileMatrixIndex: 0
|
|
||||||
m_TileColorIndex: 0
|
|
||||||
m_TileObjectToInstantiateIndex: 65535
|
|
||||||
dummyAlignment: 0
|
|
||||||
m_AllTileFlags: 1073741825
|
|
||||||
m_AnimatedTiles: {}
|
|
||||||
m_TileAssetArray:
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: 4ca4f5908b7d5364cb0287c046251970, type: 2}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: c1a00617dda76b941b08860cf7752bda, type: 2}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: 40d643345af17ba4a8ae9853163f97bf, type: 2}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: a75c0ec63e8d0614ca0ec8f41dd73214, type: 2}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: 188b71f83e9bdfd48aad2a153881b1cb, type: 2}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 11400000, guid: 41c28c158dd92564ea3cc8370f13763d, type: 2}
|
|
||||||
m_TileSpriteArray:
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: -1728761622, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: -842361761, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: -1180015184, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 428007125, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: 1676777230, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
- m_RefCount: 1
|
|
||||||
m_Data: {fileID: -1797864058, guid: fe57cc6797ae6534ca109ccaa9bb1a93, type: 3}
|
|
||||||
m_TileMatrixArray:
|
|
||||||
- m_RefCount: 6
|
|
||||||
m_Data:
|
|
||||||
e00: 1
|
|
||||||
e01: 0
|
|
||||||
e02: 0
|
|
||||||
e03: 0
|
|
||||||
e10: 0
|
|
||||||
e11: 1
|
|
||||||
e12: 0
|
|
||||||
e13: 0
|
|
||||||
e20: 0
|
|
||||||
e21: 0
|
|
||||||
e22: 1
|
|
||||||
e23: 0
|
|
||||||
e30: 0
|
|
||||||
e31: 0
|
|
||||||
e32: 0
|
|
||||||
e33: 1
|
|
||||||
m_TileColorArray:
|
|
||||||
- m_RefCount: 6
|
|
||||||
m_Data: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_TileObjectToInstantiateArray: []
|
|
||||||
m_AnimationFrameRate: 1
|
|
||||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_Origin: {x: -1, y: -2, z: 0}
|
|
||||||
m_Size: {x: 2, y: 3, z: 1}
|
|
||||||
m_TileAnchor: {x: 0.5, y: 0.5, z: 0}
|
|
||||||
m_TileOrientation: 0
|
|
||||||
m_TileOrientationMatrix:
|
|
||||||
e00: 1
|
|
||||||
e01: 0
|
|
||||||
e02: 0
|
|
||||||
e03: 0
|
|
||||||
e10: 0
|
|
||||||
e11: 1
|
|
||||||
e12: 0
|
|
||||||
e13: 0
|
|
||||||
e20: 0
|
|
||||||
e21: 0
|
|
||||||
e22: 1
|
|
||||||
e23: 0
|
|
||||||
e30: 0
|
|
||||||
e31: 0
|
|
||||||
e32: 0
|
|
||||||
e33: 1
|
|
||||||
--- !u!483693784 &7242343237021918190
|
|
||||||
TilemapRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3419683162514475710}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_CastShadows: 0
|
|
||||||
m_ReceiveShadows: 0
|
|
||||||
m_DynamicOccludee: 0
|
|
||||||
m_StaticShadowCaster: 0
|
|
||||||
m_MotionVectors: 1
|
|
||||||
m_LightProbeUsage: 0
|
|
||||||
m_ReflectionProbeUsage: 0
|
|
||||||
m_RayTracingMode: 0
|
|
||||||
m_RayTraceProcedural: 0
|
|
||||||
m_RenderingLayerMask: 1
|
|
||||||
m_RendererPriority: 0
|
|
||||||
m_Materials:
|
|
||||||
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
|
|
||||||
m_StaticBatchInfo:
|
|
||||||
firstSubMesh: 0
|
|
||||||
subMeshCount: 0
|
|
||||||
m_StaticBatchRoot: {fileID: 0}
|
|
||||||
m_ProbeAnchor: {fileID: 0}
|
|
||||||
m_LightProbeVolumeOverride: {fileID: 0}
|
|
||||||
m_ScaleInLightmap: 1
|
|
||||||
m_ReceiveGI: 1
|
|
||||||
m_PreserveUVs: 0
|
|
||||||
m_IgnoreNormalsForChartDetection: 0
|
|
||||||
m_ImportantGI: 0
|
|
||||||
m_StitchLightmapSeams: 1
|
|
||||||
m_SelectedEditorRenderState: 0
|
|
||||||
m_MinimumChartSize: 4
|
|
||||||
m_AutoUVMaxDistance: 0.5
|
|
||||||
m_AutoUVMaxAngle: 89
|
|
||||||
m_LightmapParameters: {fileID: 0}
|
|
||||||
m_SortingLayerID: 0
|
|
||||||
m_SortingLayer: 0
|
|
||||||
m_SortingOrder: 0
|
|
||||||
m_ChunkSize: {x: 32, y: 32, z: 32}
|
|
||||||
m_ChunkCullingBounds: {x: 0, y: 0, z: 0}
|
|
||||||
m_MaxChunkCount: 16
|
|
||||||
m_MaxFrameAge: 16
|
|
||||||
m_SortOrder: 0
|
|
||||||
m_Mode: 0
|
|
||||||
m_DetectChunkCullingBounds: 0
|
|
||||||
m_MaskInteraction: 0
|
|
||||||
--- !u!1 &6022835947839004827
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 8299643768118427544}
|
|
||||||
- component: {fileID: 6367259315250269812}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: Test
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &8299643768118427544
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 6022835947839004827}
|
|
||||||
serializedVersion: 2
|
|
||||||
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:
|
|
||||||
- {fileID: 7063479915286433301}
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!156049354 &6367259315250269812
|
|
||||||
Grid:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 6022835947839004827}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_CellSize: {x: 1, y: 1, z: 0}
|
|
||||||
m_CellGap: {x: 0, y: 0, z: 0}
|
|
||||||
m_CellLayout: 0
|
|
||||||
m_CellSwizzle: 0
|
|
||||||
--- !u!114 &8034214933598048606
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 12395, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name: Palette Settings
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
cellSizing: 0
|
|
||||||
m_TransparencySortMode: 0
|
|
||||||
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
[CreateAssetMenu(menuName = "Camera/CameraConfig")]
|
|
||||||
public class CameraConfigSO : ScriptableObject
|
|
||||||
{
|
|
||||||
[Header("跟随")]
|
|
||||||
public float FollowDamping = 0.15f;
|
|
||||||
public float LookAheadTime = 0.3f;
|
|
||||||
public float LookAheadSmoothing = 0.1f;
|
|
||||||
public Vector2 DeadZoneSize = new Vector2(1f, 0.5f);
|
|
||||||
public Vector2 SoftZoneSize = new Vector2(2.5f, 2f);
|
|
||||||
|
|
||||||
[Header("偏移")]
|
|
||||||
public float LookDownOffset = -1.5f;
|
|
||||||
public float LookUpOffset = 1.5f;
|
|
||||||
|
|
||||||
[Header("画面抖动默认强度")]
|
|
||||||
public float DefaultImpulseStrength = 0.3f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unity.Cinemachine;
|
|
||||||
using BaseGames.Core;
|
|
||||||
|
|
||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 相机状态单例控制器。管理房间相机切换、限位器更新与屏幕抖动。
|
|
||||||
/// 须放置在持久化场景中。
|
|
||||||
/// </summary>
|
|
||||||
[DefaultExecutionOrder(-100)]
|
|
||||||
public class CameraStateController : MonoBehaviour, ICameraService
|
|
||||||
{
|
|
||||||
[Header("引用")]
|
|
||||||
[SerializeField] private CinemachineBrain _brain;
|
|
||||||
[SerializeField] private CinemachineImpulseSource _impulseSource;
|
|
||||||
|
|
||||||
[Header("默认混合配置")]
|
|
||||||
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
|
|
||||||
|
|
||||||
// ── 注册表 ────────────────────────────────────────────────────────────
|
|
||||||
private readonly HashSet<RoomCamera> _registeredCameras = new HashSet<RoomCamera>();
|
|
||||||
private RoomCamera _activeCamera;
|
|
||||||
|
|
||||||
// ── Unity Lifecycle ───────────────────────────────────────────────────
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
if (ServiceLocator.GetOrDefault<ICameraService>() != null) { Destroy(gameObject); return; }
|
|
||||||
ServiceLocator.Register<ICameraService>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
ServiceLocator.Unregister<ICameraService>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 公开 API ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>向控制器注册一个房间相机(可选,也可由触发器直接调用 SwitchRoom)。</summary>
|
|
||||||
public void RegisterRoomCamera(RoomCamera camera)
|
|
||||||
{
|
|
||||||
if (camera != null) _registeredCameras.Add(camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>注销房间相机。</summary>
|
|
||||||
public void UnregisterRoomCamera(RoomCamera camera)
|
|
||||||
{
|
|
||||||
if (camera != null) _registeredCameras.Remove(camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>切换到目标房间相机,并应用对应的混合配置。</summary>
|
|
||||||
public void SwitchRoom(RoomCamera targetCamera)
|
|
||||||
{
|
|
||||||
if (targetCamera == null || targetCamera == _activeCamera) return;
|
|
||||||
|
|
||||||
// 应用混合配置到 Brain
|
|
||||||
if (_brain != null)
|
|
||||||
{
|
|
||||||
var profile = targetCamera.BlendProfile ?? _defaultBlendProfile;
|
|
||||||
if (profile != null)
|
|
||||||
_brain.DefaultBlend = profile.ToBlendDefinition();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 禁用旧相机、启用新相机
|
|
||||||
_activeCamera?.Deactivate();
|
|
||||||
_activeCamera = targetCamera;
|
|
||||||
_activeCamera.Activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>触发屏幕抖动。</summary>
|
|
||||||
public void TriggerImpulse(Vector3 velocity)
|
|
||||||
{
|
|
||||||
if (_impulseSource != null)
|
|
||||||
_impulseSource.GenerateImpulse(velocity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>以默认强度触发屏幕抖动。</summary>
|
|
||||||
public void TriggerImpulse(float strength = 0.3f)
|
|
||||||
=> TriggerImpulse(Vector3.down * strength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using BaseGames.Core;
|
|
||||||
|
|
||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 相机区域切换触发器。玩家进入时通知 CameraStateController 切换到目标房间相机。
|
|
||||||
/// [ExecuteAlways] 确保编辑器中 Gizmo 立即更新。
|
|
||||||
/// </summary>
|
|
||||||
[ExecuteAlways]
|
|
||||||
[RequireComponent(typeof(BoxCollider2D))]
|
|
||||||
public class CameraTriggerZone : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private RoomCamera _targetCamera;
|
|
||||||
[SerializeField] private string _playerTag = "Player";
|
|
||||||
|
|
||||||
private BoxCollider2D _collider;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_collider = GetComponent<BoxCollider2D>();
|
|
||||||
_collider.isTrigger = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTriggerEnter2D(Collider2D other)
|
|
||||||
{
|
|
||||||
if (!Application.isPlaying) return;
|
|
||||||
if (!other.CompareTag(_playerTag)) return;
|
|
||||||
if (_targetCamera != null)
|
|
||||||
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchRoom(_targetCamera);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
|
||||||
{
|
|
||||||
if (_collider == null) _collider = GetComponent<BoxCollider2D>();
|
|
||||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.25f);
|
|
||||||
Gizmos.matrix = transform.localToWorldMatrix;
|
|
||||||
Gizmos.DrawCube(_collider.offset, _collider.size);
|
|
||||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.8f);
|
|
||||||
Gizmos.DrawWireCube(_collider.offset, _collider.size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 相机服务接口。供 RoomController / CameraTriggerZone 等调用,
|
|
||||||
/// 通过 ServiceLocator.Get<ICameraService>() 访问,无需直接依赖 CameraStateController。
|
|
||||||
/// </summary>
|
|
||||||
public interface ICameraService
|
|
||||||
{
|
|
||||||
/// <summary>切换到目标房间相机。</summary>
|
|
||||||
void SwitchRoom(RoomCamera targetCamera);
|
|
||||||
|
|
||||||
/// <summary>注册一个房间相机到控制器注册表。</summary>
|
|
||||||
void RegisterRoomCamera(RoomCamera camera);
|
|
||||||
|
|
||||||
/// <summary>注销一个房间相机。</summary>
|
|
||||||
void UnregisterRoomCamera(RoomCamera camera);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using Unity.Cinemachine;
|
|
||||||
|
|
||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 单房间虚拟相机。激活时提升优先级,停用时降为 0。
|
|
||||||
/// 挂载在每个房间的 CinemachineCamera GameObject 上。
|
|
||||||
/// </summary>
|
|
||||||
[RequireComponent(typeof(CinemachineCamera))]
|
|
||||||
public class RoomCamera : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("房间设置")]
|
|
||||||
[SerializeField] private RoomVisibleArea _visibleArea;
|
|
||||||
[SerializeField] private Vector2 _cameraOffset = Vector2.zero;
|
|
||||||
[SerializeField] private CameraBlendProfileSO _blendProfile;
|
|
||||||
[SerializeField] private int _activePriority = 15;
|
|
||||||
|
|
||||||
private CinemachineCamera _vcam;
|
|
||||||
|
|
||||||
private void Awake() => _vcam = GetComponent<CinemachineCamera>();
|
|
||||||
private void OnEnable() => _vcam.Priority = _activePriority;
|
|
||||||
private void OnDisable() => _vcam.Priority = 0;
|
|
||||||
|
|
||||||
public PolygonCollider2D ConfinerCollider => _visibleArea?.Collider;
|
|
||||||
public Vector2 CameraOffset => _cameraOffset;
|
|
||||||
public CameraBlendProfileSO BlendProfile => _blendProfile;
|
|
||||||
|
|
||||||
/// <summary>在 CameraStateController 管理的激活流程中调用。</summary>
|
|
||||||
public void Activate() => gameObject.SetActive(true);
|
|
||||||
public void Deactivate() => gameObject.SetActive(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Camera
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标记房间的可见区域(多边形)。供 CinemachineConfiner2D 使用。
|
|
||||||
/// [ExecuteAlways] 确保编辑器中碰撞体立即更新。
|
|
||||||
/// </summary>
|
|
||||||
[ExecuteAlways]
|
|
||||||
[RequireComponent(typeof(PolygonCollider2D))]
|
|
||||||
public class RoomVisibleArea : MonoBehaviour
|
|
||||||
{
|
|
||||||
private PolygonCollider2D _collider;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_collider = GetComponent<PolygonCollider2D>();
|
|
||||||
_collider.isTrigger = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PolygonCollider2D Collider
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_collider == null) _collider = GetComponent<PolygonCollider2D>();
|
|
||||||
return _collider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Camera { }
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Cutscene { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1cbf67024d1a0404183a606fe02f3e48
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,904 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using BaseGames.Audio;
|
|
||||||
using BaseGames.Camera;
|
|
||||||
using BaseGames.Combat;
|
|
||||||
using BaseGames.Core;
|
|
||||||
using BaseGames.Core.Events;
|
|
||||||
using BaseGames.Core.Pool;
|
|
||||||
using BaseGames.Enemies;
|
|
||||||
using BaseGames.Input;
|
|
||||||
using BaseGames.Player;
|
|
||||||
using BaseGames.Player.States;
|
|
||||||
using BaseGames.UI;
|
|
||||||
using BaseGames.UI.HUD;
|
|
||||||
using BaseGames.UI.Menus;
|
|
||||||
using BaseGames.World;
|
|
||||||
using PathBerserker2d;
|
|
||||||
using Unity.Cinemachine;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.SceneManagement;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
using UnityEngine.Tilemaps;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
namespace BaseGames.Editor
|
|
||||||
{
|
|
||||||
public static class SceneScaffoldTools
|
|
||||||
{
|
|
||||||
|
|
||||||
[MenuItem("BaseGames/Tools/Scaffold Persistent Scene")]
|
|
||||||
public static void ScaffoldPersistentScene()
|
|
||||||
{
|
|
||||||
var report = new List<string>();
|
|
||||||
|
|
||||||
EnsureEventChannelAssets(report);
|
|
||||||
|
|
||||||
GameObject root = GetOrCreateRoot("[Persistent]");
|
|
||||||
Transform services = GetOrCreateChild(root.transform, "[Services]");
|
|
||||||
Transform input = GetOrCreateChild(root.transform, "[Input]");
|
|
||||||
Transform camera = GetOrCreateChild(root.transform, "[Camera]");
|
|
||||||
Transform ui = GetOrCreateChild(root.transform, "[UI]");
|
|
||||||
|
|
||||||
GameObject registrarGo = GetOrCreateChild(services, "GameServiceRegistrar").gameObject;
|
|
||||||
GameObject deathRespawnGo = GetOrCreateChild(services, "DeathRespawnService").gameObject;
|
|
||||||
GameObject sceneServiceGo = GetOrCreateChild(services, "SceneService").gameObject;
|
|
||||||
GameObject sceneLoaderGo = GetOrCreateChild(services, "SceneLoader").gameObject;
|
|
||||||
GameObject registryGo = GetOrCreateChild(services, "EventChannelRegistry").gameObject;
|
|
||||||
GameObject settingsGo = GetOrCreateChild(services, "SettingsManager").gameObject;
|
|
||||||
GameObject poolGo = GetOrCreateChild(services, "GlobalObjectPool").gameObject;
|
|
||||||
GameObject gameManagerGo = GetOrCreateChild(services, "GameManager").gameObject;
|
|
||||||
GameObject audioManagerGo = GetOrCreateChild(services, "AudioManager").gameObject;
|
|
||||||
|
|
||||||
GameServiceRegistrar registrar = GetOrAddComponent<GameServiceRegistrar>(registrarGo);
|
|
||||||
DeathRespawnService deathRespawnService = GetOrAddComponent<DeathRespawnService>(deathRespawnGo);
|
|
||||||
SceneService sceneService = GetOrAddComponent<SceneService>(sceneServiceGo);
|
|
||||||
SceneLoader sceneLoader = GetOrAddComponent<SceneLoader>(sceneLoaderGo);
|
|
||||||
EventChannelRegistry registry = GetOrAddComponent<EventChannelRegistry>(registryGo);
|
|
||||||
SettingsManager settingsManager = GetOrAddComponent<SettingsManager>(settingsGo);
|
|
||||||
GetOrAddComponent<GlobalObjectPool>(poolGo);
|
|
||||||
GameManager gameManager = GetOrAddComponent<GameManager>(gameManagerGo);
|
|
||||||
AudioManager audioManager = GetOrAddComponent<AudioManager>(audioManagerGo);
|
|
||||||
|
|
||||||
GameObject inputHolderGo = GetOrCreateChild(input, "InputReaderHolder").gameObject;
|
|
||||||
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
|
|
||||||
if (inputReaderAsset == null)
|
|
||||||
inputReaderAsset = EnsureInputReaderAsset(report);
|
|
||||||
|
|
||||||
InputReaderBootstrap inputBootstrap = GetOrAddComponent<InputReaderBootstrap>(inputHolderGo);
|
|
||||||
AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report);
|
|
||||||
if (inputReaderAsset != null)
|
|
||||||
{
|
|
||||||
AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType<VoidEventChannelSO>("EVT_PauseRequested"), report);
|
|
||||||
AssignReference(inputReaderAsset, "_inputActions", FindFirstAssetWithExtension(".inputactions", "PlayerInputActions", "InputActions"), report);
|
|
||||||
}
|
|
||||||
if (inputReaderAsset == null)
|
|
||||||
report.Add("未找到 InputReaderSO 资产,InputReaderBootstrap 将保持空引用。请补齐 Assets/Data/Input/InputReader.asset。");
|
|
||||||
|
|
||||||
GameObject mainCameraGo = GetOrCreateChild(camera, "Main Camera").gameObject;
|
|
||||||
UnityEngine.Camera mainCamera = GetOrAddComponent<UnityEngine.Camera>(mainCameraGo);
|
|
||||||
mainCamera.orthographic = false;
|
|
||||||
mainCamera.fieldOfView = 60f;
|
|
||||||
mainCameraGo.tag = "MainCamera";
|
|
||||||
GetOrAddComponent<AudioListener>(mainCameraGo);
|
|
||||||
CinemachineBrain brain = GetOrAddComponent<CinemachineBrain>(mainCameraGo);
|
|
||||||
|
|
||||||
GameObject cameraStateGo = GetOrCreateChild(camera, "CameraStateController").gameObject;
|
|
||||||
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
|
|
||||||
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
|
|
||||||
|
|
||||||
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
|
|
||||||
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
|
|
||||||
|
|
||||||
GameObject hudCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "HUD Canvas", 0);
|
|
||||||
GameObject hudRootGo = GetOrCreateChild(hudCanvasGo.transform, "HUDRoot").gameObject;
|
|
||||||
HUDController hudController = GetOrAddComponent<HUDController>(hudRootGo);
|
|
||||||
|
|
||||||
GameObject pauseRootGo = GetOrCreateChild(uiRootGo.transform, "PauseMenuRoot").gameObject;
|
|
||||||
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
|
|
||||||
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
|
|
||||||
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
|
|
||||||
pauseRootGo.SetActive(false);
|
|
||||||
settingsRootGo.SetActive(false);
|
|
||||||
mapRootGo.SetActive(false);
|
|
||||||
shopRootGo.SetActive(false);
|
|
||||||
|
|
||||||
GameObject deathCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "DeathScreen Canvas", 10);
|
|
||||||
GameObject deathRootGo = GetOrCreateChild(deathCanvasGo.transform, "DeathScreenRoot").gameObject;
|
|
||||||
DeathScreenController deathScreenController = GetOrAddComponent<DeathScreenController>(deathRootGo);
|
|
||||||
deathRootGo.SetActive(false);
|
|
||||||
GameObject respawnButtonGo = GetOrCreateChild(deathRootGo.transform, "RespawnButton").gameObject;
|
|
||||||
GetOrAddComponent<Image>(respawnButtonGo);
|
|
||||||
Button respawnButton = GetOrAddComponent<Button>(respawnButtonGo);
|
|
||||||
|
|
||||||
EnsureAudioSources(audioManagerGo, audioManager, report);
|
|
||||||
|
|
||||||
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
|
|
||||||
AssignReference(registrar, "_sceneService", sceneService);
|
|
||||||
AssignReference(registrar, "_eventChannelRegistry", registry);
|
|
||||||
|
|
||||||
AssignReference(gameManager, "_settingsManager", settingsManager);
|
|
||||||
AssignReference(gameManager, "_deathRespawnService", deathRespawnService);
|
|
||||||
AssignReference(gameManager, "_sceneService", sceneService);
|
|
||||||
AssignAsset(gameManager, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
|
||||||
AssignAsset(gameManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
|
||||||
AssignAsset(gameManager, "_onResumeRequested", report, false, "EVT_ResumeRequested", "EVT_PauseResumed");
|
|
||||||
AssignAsset(gameManager, "_onBossFightStarted", report, false, "EVT_BossFightStarted", "EVT_BossFight");
|
|
||||||
AssignAsset(gameManager, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
|
||||||
AssignAsset(gameManager, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
|
||||||
AssignAsset(gameManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
|
||||||
AssignAsset(gameManager, "_onPlayerRespawned", report, false, "EVT_PlayerRespawned", "EVT_PlayerRespawn");
|
|
||||||
|
|
||||||
AssignAsset(sceneService, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
|
||||||
AssignAsset(sceneService, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
|
||||||
AssignAsset(sceneService, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
|
||||||
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
|
||||||
|
|
||||||
AssignAsset(sceneLoader, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
|
||||||
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
|
||||||
|
|
||||||
AssignAsset(deathRespawnService, "_onRespawnStarted", report, false, "EVT_RespawnStarted");
|
|
||||||
AssignAsset(deathRespawnService, "_onRespawnCompleted", report, false, "EVT_RespawnCompleted");
|
|
||||||
AssignAsset(deathRespawnService, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
|
||||||
|
|
||||||
AssignAsset(settingsManager, "_defaultSettings", report, false, "SET_GlobalSettings");
|
|
||||||
|
|
||||||
AssignAsset(audioManager, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
|
||||||
|
|
||||||
AssignReference(cameraStateController, "_brain", brain);
|
|
||||||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
|
||||||
|
|
||||||
AssignReference(uiManager, "_hudRoot", hudRootGo);
|
|
||||||
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
|
|
||||||
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
|
|
||||||
AssignReference(uiManager, "_settingsRoot", settingsRootGo);
|
|
||||||
AssignReference(uiManager, "_mapRoot", mapRootGo);
|
|
||||||
AssignReference(uiManager, "_shopRoot", shopRootGo);
|
|
||||||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
|
||||||
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
|
||||||
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
|
||||||
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
|
|
||||||
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
|
|
||||||
|
|
||||||
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
|
|
||||||
AssignAsset(deathScreenController, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
|
||||||
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
|
||||||
|
|
||||||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
|
||||||
|
|
||||||
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureEventChannelAssets(List<string> report)
|
|
||||||
{
|
|
||||||
bool hasCoreSet =
|
|
||||||
FindFirstAsset("EVT_PlayerDied") != null &&
|
|
||||||
FindFirstAsset("EVT_DeathScreenConfirmed") != null &&
|
|
||||||
(FindFirstAsset("EVT_GameStateChanged") != null || FindFirstAsset("EVT_GameState") != null) &&
|
|
||||||
FindFirstAsset("EVT_PauseRequested") != null &&
|
|
||||||
FindFirstAsset("EVT_SceneLoadRequest") != null;
|
|
||||||
|
|
||||||
if (hasCoreSet)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CreateEventChannelAssets.CreateAll();
|
|
||||||
report?.Add("检测到关键事件频道缺失,已自动执行 Create Event Channel Assets。");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[MenuItem("BaseGames/Tools/Scaffold Test Room")]
|
|
||||||
public static void ScaffoldTestRoom()
|
|
||||||
{
|
|
||||||
var report = new List<string>();
|
|
||||||
|
|
||||||
EnsureEventChannelAssets(report);
|
|
||||||
|
|
||||||
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
|
|
||||||
if (inputReaderAsset == null)
|
|
||||||
inputReaderAsset = EnsureInputReaderAsset(report);
|
|
||||||
|
|
||||||
GameObject root = GetOrCreateRoot("[TestRoom]");
|
|
||||||
Transform environment = GetOrCreateChild(root.transform, "[Environment]");
|
|
||||||
Transform playerRoot = GetOrCreateChild(root.transform, "[Player]");
|
|
||||||
Transform enemyRoot = GetOrCreateChild(root.transform, "[Enemy]");
|
|
||||||
Transform savePointRoot = GetOrCreateChild(root.transform, "[SavePoint]");
|
|
||||||
Transform cameraRoot = GetOrCreateChild(root.transform, "[Camera]");
|
|
||||||
|
|
||||||
DisableRenderCamerasUnderRoot(root.transform, report);
|
|
||||||
|
|
||||||
Object playerStatsConfigAsset = EnsurePlayerStatsConfigAsset(report);
|
|
||||||
Object playerMovementConfigAsset = EnsurePlayerMovementConfigAsset(report);
|
|
||||||
Object playerAnimationConfigAsset = EnsurePlayerAnimationConfigAsset(report);
|
|
||||||
|
|
||||||
GameObject groundGridGo = GetOrCreateChild(environment, "GroundGrid").gameObject;
|
|
||||||
GetOrAddComponent<Grid>(groundGridGo);
|
|
||||||
GameObject groundGo = GetOrCreateChild(groundGridGo.transform, "Ground").gameObject;
|
|
||||||
TilemapCollider2D tilemapCollider = GetOrAddComponent<TilemapCollider2D>(groundGo);
|
|
||||||
tilemapCollider.usedByComposite = true;
|
|
||||||
GetOrAddComponent<Tilemap>(groundGo);
|
|
||||||
GetOrAddComponent<TilemapRenderer>(groundGo);
|
|
||||||
Rigidbody2D groundBody = GetOrAddComponent<Rigidbody2D>(groundGo);
|
|
||||||
groundBody.bodyType = RigidbodyType2D.Static;
|
|
||||||
GetOrAddComponent<CompositeCollider2D>(groundGo);
|
|
||||||
SetLayer(groundGo, "Ground", report);
|
|
||||||
|
|
||||||
GameObject fallbackFloorGo = GetOrCreateChild(environment, "GroundFallback").gameObject;
|
|
||||||
fallbackFloorGo.transform.position = new Vector3(0f, -2f, 0f);
|
|
||||||
BoxCollider2D fallbackFloorCollider = GetOrAddComponent<BoxCollider2D>(fallbackFloorGo);
|
|
||||||
fallbackFloorCollider.size = new Vector2(40f, 1f);
|
|
||||||
Rigidbody2D fallbackFloorBody = GetOrAddComponent<Rigidbody2D>(fallbackFloorGo);
|
|
||||||
fallbackFloorBody.bodyType = RigidbodyType2D.Static;
|
|
||||||
SetLayer(fallbackFloorGo, "Ground", report);
|
|
||||||
EnsureVisualSprite(fallbackFloorGo.transform, "Visual", new Color(0.25f, 0.7f, 0.3f, 1f), new Vector2(40f, 1f), -10);
|
|
||||||
AddScaffoldNote(fallbackFloorGo, "保底地板用于测试:若 Tilemap 资源未提供碰撞形状,角色仍不会穿地。", report);
|
|
||||||
|
|
||||||
GameObject navSurfaceGo = GetOrCreateChild(environment, "NavSurfaceRoot").gameObject;
|
|
||||||
GetOrAddComponent<NavSurface>(navSurfaceGo);
|
|
||||||
AddScaffoldNote(navSurfaceGo, "NavSurface 已创建,仍需在 Unity Inspector 中点击 Bake。", report);
|
|
||||||
|
|
||||||
GameObject playerGo = GetOrCreateChild(playerRoot, "Player").gameObject;
|
|
||||||
RemoveMissingScripts(playerGo, recursive: true, report);
|
|
||||||
playerGo.transform.position = new Vector3(0f, 1f, 0f);
|
|
||||||
playerGo.tag = "Player";
|
|
||||||
SetLayer(playerGo, "Player", report);
|
|
||||||
PlayerController playerController = GetOrAddComponent<PlayerController>(playerGo);
|
|
||||||
InputBuffer inputBuffer = GetOrAddComponent<InputBuffer>(playerGo);
|
|
||||||
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(playerGo);
|
|
||||||
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(playerGo);
|
|
||||||
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(playerGo);
|
|
||||||
FormController formController = GetOrAddComponent<FormController>(playerGo);
|
|
||||||
WeaponManager weaponManager = GetOrAddComponent<WeaponManager>(playerGo);
|
|
||||||
SkillManager skillManager = GetOrAddComponent<SkillManager>(playerGo);
|
|
||||||
SpringSystem springSystem = GetOrAddComponent<SpringSystem>(playerGo);
|
|
||||||
Component parrySystem = AddComponentIfTypeExists(playerGo, "BaseGames.Parry.ParrySystem", report);
|
|
||||||
ShieldComponent shieldComponent = GetOrAddComponent<ShieldComponent>(playerGo);
|
|
||||||
Rigidbody2D playerBody = GetOrAddComponent<Rigidbody2D>(playerGo);
|
|
||||||
playerBody.bodyType = RigidbodyType2D.Dynamic;
|
|
||||||
playerBody.gravityScale = 2f;
|
|
||||||
playerBody.constraints = RigidbodyConstraints2D.FreezeRotation;
|
|
||||||
GetOrAddComponent<CapsuleCollider2D>(playerGo);
|
|
||||||
Animancer.AnimancerComponent playerAnimancer = GetOrAddComponent<Animancer.AnimancerComponent>(playerGo);
|
|
||||||
EnsureVisualSprite(playerGo.transform, "Visual", new Color(0.3f, 0.7f, 1f, 1f), new Vector2(0.8f, 1.6f), 20);
|
|
||||||
|
|
||||||
GameObject groundCheckGo = GetOrCreateChild(playerGo.transform, "GroundCheck").gameObject;
|
|
||||||
groundCheckGo.transform.localPosition = new Vector3(0f, -0.5f, 0f);
|
|
||||||
|
|
||||||
GameObject hitBoxGroundGo = GetOrCreateChild(playerGo.transform, "HitBoxGround").gameObject;
|
|
||||||
BoxCollider2D hitBoxGroundCollider = GetOrAddComponent<BoxCollider2D>(hitBoxGroundGo);
|
|
||||||
hitBoxGroundCollider.isTrigger = true;
|
|
||||||
HitBox hitBoxGround = GetOrAddComponent<HitBox>(hitBoxGroundGo);
|
|
||||||
SetLayer(hitBoxGroundGo, "PlayerHitBox", report);
|
|
||||||
|
|
||||||
GameObject hitBoxUpGo = GetOrCreateChild(playerGo.transform, "HitBoxUp").gameObject;
|
|
||||||
BoxCollider2D hitBoxUpCollider = GetOrAddComponent<BoxCollider2D>(hitBoxUpGo);
|
|
||||||
hitBoxUpCollider.isTrigger = true;
|
|
||||||
HitBox hitBoxUp = GetOrAddComponent<HitBox>(hitBoxUpGo);
|
|
||||||
SetLayer(hitBoxUpGo, "PlayerHitBox", report);
|
|
||||||
|
|
||||||
GameObject hitBoxDownGo = GetOrCreateChild(playerGo.transform, "HitBoxDown").gameObject;
|
|
||||||
BoxCollider2D hitBoxDownCollider = GetOrAddComponent<BoxCollider2D>(hitBoxDownGo);
|
|
||||||
hitBoxDownCollider.isTrigger = true;
|
|
||||||
HitBox hitBoxDown = GetOrAddComponent<HitBox>(hitBoxDownGo);
|
|
||||||
SetLayer(hitBoxDownGo, "PlayerHitBox", report);
|
|
||||||
|
|
||||||
GameObject hitBoxAirGo = GetOrCreateChild(playerGo.transform, "HitBoxAir").gameObject;
|
|
||||||
BoxCollider2D hitBoxAirCollider = GetOrAddComponent<BoxCollider2D>(hitBoxAirGo);
|
|
||||||
hitBoxAirCollider.isTrigger = true;
|
|
||||||
HitBox hitBoxAir = GetOrAddComponent<HitBox>(hitBoxAirGo);
|
|
||||||
SetLayer(hitBoxAirGo, "PlayerHitBox", report);
|
|
||||||
|
|
||||||
GameObject playerHurtBoxGo = GetOrCreateChild(playerGo.transform, "HurtBox").gameObject;
|
|
||||||
CapsuleCollider2D playerHurtCollider = GetOrAddComponent<CapsuleCollider2D>(playerHurtBoxGo);
|
|
||||||
playerHurtCollider.isTrigger = true;
|
|
||||||
HurtBox playerHurtBox = GetOrAddComponent<HurtBox>(playerHurtBoxGo);
|
|
||||||
SetLayer(playerHurtBoxGo, "PlayerHurtBox", report);
|
|
||||||
|
|
||||||
GameObject enemyGo = GetOrCreateChild(enemyRoot, "BasicEnemy").gameObject;
|
|
||||||
SetLayer(enemyGo, "Enemy", report);
|
|
||||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(enemyGo);
|
|
||||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(enemyGo);
|
|
||||||
GetOrAddComponent<Rigidbody2D>(enemyGo).bodyType = RigidbodyType2D.Dynamic;
|
|
||||||
GetOrAddComponent<CapsuleCollider2D>(enemyGo);
|
|
||||||
GetOrAddComponent<Animancer.AnimancerComponent>(enemyGo);
|
|
||||||
EnsureVisualSprite(enemyGo.transform, "Visual", new Color(1f, 0.45f, 0.45f, 1f), new Vector2(0.8f, 1.4f), 15);
|
|
||||||
AddComponentIfTypeExists(enemyGo, "PathBerserker2d.NavAgent", report);
|
|
||||||
|
|
||||||
GameObject enemyHurtBoxGo = GetOrCreateChild(enemyGo.transform, "HurtBox").gameObject;
|
|
||||||
CapsuleCollider2D enemyHurtCollider = GetOrAddComponent<CapsuleCollider2D>(enemyHurtBoxGo);
|
|
||||||
enemyHurtCollider.isTrigger = true;
|
|
||||||
HurtBox enemyHurtBox = GetOrAddComponent<HurtBox>(enemyHurtBoxGo);
|
|
||||||
SetLayer(enemyHurtBoxGo, "EnemyHurtBox", report);
|
|
||||||
|
|
||||||
GameObject savePointGo = GetOrCreateChild(savePointRoot, "SavePointObject").gameObject;
|
|
||||||
SavePoint savePoint = GetOrAddComponent<SavePoint>(savePointGo);
|
|
||||||
BoxCollider2D savePointCollider = GetOrAddComponent<BoxCollider2D>(savePointGo);
|
|
||||||
savePointCollider.isTrigger = true;
|
|
||||||
SetLayer(savePointGo, "TriggerZone", report);
|
|
||||||
EnsureVisualSprite(savePointGo.transform, "Visual", new Color(1f, 0.9f, 0.3f, 0.9f), new Vector2(0.8f, 1.2f), 10);
|
|
||||||
AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated");
|
|
||||||
AssignAsset(savePoint, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
|
||||||
|
|
||||||
GameObject roomCameraGo = GetOrCreateChild(cameraRoot, "RoomCamera").gameObject;
|
|
||||||
CinemachineCamera roomCameraComponent = GetOrAddComponent<CinemachineCamera>(roomCameraGo);
|
|
||||||
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(roomCameraGo);
|
|
||||||
UnityEngine.Camera roomRenderCamera = roomCameraGo.GetComponent<UnityEngine.Camera>();
|
|
||||||
if (roomRenderCamera != null)
|
|
||||||
{
|
|
||||||
roomRenderCamera.orthographic = false;
|
|
||||||
roomRenderCamera.fieldOfView = 60f;
|
|
||||||
roomRenderCamera.enabled = false;
|
|
||||||
report.Add("RoomCamera 上的 Unity Camera 已禁用(Additive 测试时仅保留 Persistent/Main Camera 渲染)。");
|
|
||||||
}
|
|
||||||
AudioListener roomAudioListener = roomCameraGo.GetComponent<AudioListener>();
|
|
||||||
if (roomAudioListener != null)
|
|
||||||
{
|
|
||||||
Undo.DestroyObjectImmediate(roomAudioListener);
|
|
||||||
report.Add("RoomCamera 上的 AudioListener 已移除(Additive 测试时由 Persistent/Main Camera 保留唯一监听器)。");
|
|
||||||
}
|
|
||||||
GameObject roomBoundaryGo = GetOrCreateChild(roomCameraGo.transform, "RoomBoundary").gameObject;
|
|
||||||
RemoveMissingScripts(roomBoundaryGo, recursive: true, report);
|
|
||||||
RoomVisibleArea roomVisibleArea = GetOrAddComponent<RoomVisibleArea>(roomBoundaryGo);
|
|
||||||
CinemachineConfiner2D confiner2D = GetOrAddComponent<CinemachineConfiner2D>(roomCameraGo);
|
|
||||||
AssignReference(roomCamera, "_visibleArea", roomVisibleArea);
|
|
||||||
AssignReferenceByCandidates(roomCameraComponent, playerGo.transform, report, "Follow", "m_Follow");
|
|
||||||
AssignReferenceByCandidates(confiner2D, roomVisibleArea.Collider, report, "BoundingShape2D", "m_BoundingShape2D");
|
|
||||||
AddScaffoldNote(roomBoundaryGo, "RoomBoundary 已挂 RoomVisibleArea,需要手工编辑 PolygonCollider2D 顶点定义房间边界。", report);
|
|
||||||
|
|
||||||
if (inputReaderAsset != null)
|
|
||||||
{
|
|
||||||
AssignReference(playerController, "_inputReader", inputReaderAsset, report);
|
|
||||||
AssignReference(inputBuffer, "_inputReader", inputReaderAsset, report);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
report.Add("未找到 InputReader 资产,PlayerController/InputBuffer 的 _inputReader 未能绑定。");
|
|
||||||
}
|
|
||||||
|
|
||||||
AssignReference(playerController, "_movement", playerMovement, report);
|
|
||||||
AssignReference(playerController, "_animancer", playerAnimancer, report);
|
|
||||||
AssignReference(playerController, "_combat", playerCombat, report);
|
|
||||||
AssignReference(playerController, "_formController", formController, report);
|
|
||||||
AssignReference(playerController, "_weaponManager", weaponManager, report);
|
|
||||||
AssignReference(playerController, "_skillManager", skillManager, report);
|
|
||||||
AssignReference(playerController, "_springSystem", springSystem, report);
|
|
||||||
AssignReference(playerController, "_parrySystem", parrySystem, report);
|
|
||||||
AssignReference(playerController, "_shield", shieldComponent, report);
|
|
||||||
AssignReference(playerController, "_statsConfig", playerStatsConfigAsset, report);
|
|
||||||
AssignReference(playerController, "_movementConfig", playerMovementConfigAsset, report);
|
|
||||||
AssignReference(playerController, "_animConfig", playerAnimationConfigAsset, report);
|
|
||||||
AssignReference(playerStats, "_config", playerStatsConfigAsset, report);
|
|
||||||
AssignReference(playerMovement, "_config", playerMovementConfigAsset, report);
|
|
||||||
AssignReference(playerMovement, "_groundCheck", groundCheckGo.transform, report);
|
|
||||||
AssignLayerMask(playerMovement, "_groundLayer", "Ground", report);
|
|
||||||
AssignReference(playerCombat, "_weaponManager", weaponManager, report);
|
|
||||||
AssignReference(playerCombat, "_hitBoxGround", hitBoxGround, report);
|
|
||||||
AssignReference(playerCombat, "_hitBoxUp", hitBoxUp, report);
|
|
||||||
AssignReference(playerCombat, "_hitBoxDown", hitBoxDown, report);
|
|
||||||
AssignReference(playerCombat, "_hitBoxAir", hitBoxAir, report);
|
|
||||||
AssignAsset(playerController, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
|
||||||
AssignAsset(playerController, "_onHPChanged", report, false, "EVT_HPChanged");
|
|
||||||
AssignReference(playerController, "_stats", playerStats);
|
|
||||||
AssignReference(playerController, "_hurtBox", playerHurtBox);
|
|
||||||
|
|
||||||
AssignAsset(playerStats, "_onHPChanged", report, false, "EVT_HPChanged");
|
|
||||||
AssignAsset(playerStats, "_onMaxHPChanged", report, false, "EVT_MaxHPChanged");
|
|
||||||
AssignAsset(playerStats, "_onSoulPowerChanged", report, false, "EVT_SoulPowerChanged");
|
|
||||||
AssignAsset(playerStats, "_onSpiritPowerChanged", report, false, "EVT_SpiritPowerChanged");
|
|
||||||
AssignAsset(playerStats, "_onSpringChargesChanged", report, false, "EVT_SpringChargesChanged");
|
|
||||||
AssignAsset(playerStats, "_onGeoChanged", report, false, "EVT_GeoChanged");
|
|
||||||
AssignAsset(playerStats, "_onAbilityUnlocked", report, false, "EVT_AbilityUnlocked");
|
|
||||||
AssignAsset(playerStats, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
|
||||||
|
|
||||||
AssignAsset(playerHurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
|
||||||
AssignAsset(playerHurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
|
||||||
AssignAsset(enemyHurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
|
||||||
AssignAsset(enemyHurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
|
||||||
|
|
||||||
AssignReference(enemyBase, "_stats", enemyStats);
|
|
||||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
|
||||||
AssignAsset(enemyBase, "_statsSO", report, false, "BasicEnemyStats", "EnemyStatsSO");
|
|
||||||
|
|
||||||
AddScaffoldNote(playerGo, "Player 已生成基础控制节点。PlayerMovement、Combat、Form、Weapon、Skill 等复杂依赖需按实际 prefab/配置继续补齐。", report);
|
|
||||||
AddScaffoldNote(enemyGo, "Enemy 已生成基础节点。行为树、导航参数、动画配置和战斗组件仍需手工配置。", report);
|
|
||||||
|
|
||||||
MarkDirtyAndLog("TestRoom 场景脚手架", root, report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureAudioSources(GameObject audioManagerGo, AudioManager audioManager, List<string> report)
|
|
||||||
{
|
|
||||||
GameObject bgmAGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source A").gameObject;
|
|
||||||
GameObject bgmBGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source B").gameObject;
|
|
||||||
GameObject sfxRootGo = GetOrCreateChild(audioManagerGo.transform, "SFX Sources").gameObject;
|
|
||||||
|
|
||||||
AudioSource bgmA = GetOrAddComponent<AudioSource>(bgmAGo);
|
|
||||||
AudioSource bgmB = GetOrAddComponent<AudioSource>(bgmBGo);
|
|
||||||
bgmA.playOnAwake = false;
|
|
||||||
bgmB.playOnAwake = false;
|
|
||||||
bgmA.loop = true;
|
|
||||||
bgmB.loop = true;
|
|
||||||
|
|
||||||
var sfxSources = new AudioSource[6];
|
|
||||||
for (int i = 0; i < sfxSources.Length; i++)
|
|
||||||
{
|
|
||||||
GameObject sfxGo = GetOrCreateChild(sfxRootGo.transform, $"SFX Source {i + 1}").gameObject;
|
|
||||||
AudioSource sfxSource = GetOrAddComponent<AudioSource>(sfxGo);
|
|
||||||
sfxSource.playOnAwake = false;
|
|
||||||
sfxSources[i] = sfxSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
AssignReference(audioManager, "_bgmSourceA", bgmA);
|
|
||||||
AssignReference(audioManager, "_bgmSourceB", bgmB);
|
|
||||||
AssignArrayReferences(audioManager, "_sfxSources", sfxSources, report);
|
|
||||||
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source,AudioMixer 仍需手工指定。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GameObject GetOrCreateRoot(string name)
|
|
||||||
{
|
|
||||||
Scene scene = SceneManager.GetActiveScene();
|
|
||||||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
|
||||||
{
|
|
||||||
if (rootObject.name == name)
|
|
||||||
return rootObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameObject root = new GameObject(name);
|
|
||||||
Undo.RegisterCreatedObjectUndo(root, $"Create {name}");
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Transform GetOrCreateChild(Transform parent, string name)
|
|
||||||
{
|
|
||||||
Transform child = parent.Find(name);
|
|
||||||
if (child != null)
|
|
||||||
return child;
|
|
||||||
|
|
||||||
GameObject go = new GameObject(name);
|
|
||||||
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
|
|
||||||
go.transform.SetParent(parent, false);
|
|
||||||
return go.transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
|
||||||
{
|
|
||||||
T component = go.GetComponent<T>();
|
|
||||||
if (component != null)
|
|
||||||
return component;
|
|
||||||
|
|
||||||
return Undo.AddComponent<T>(go);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Component AddComponentIfTypeExists(GameObject go, string typeName, List<string> report)
|
|
||||||
{
|
|
||||||
System.Type type = FindType(typeName);
|
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
report.Add($"未找到类型 {typeName},已跳过对应组件挂载。");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Component component = go.GetComponent(type);
|
|
||||||
if (component != null)
|
|
||||||
return component;
|
|
||||||
|
|
||||||
return Undo.AddComponent(go, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static System.Type FindType(string typeName)
|
|
||||||
{
|
|
||||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
|
||||||
{
|
|
||||||
System.Type type = assembly.GetType(typeName);
|
|
||||||
if (type != null)
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GameObject GetOrCreateCanvas(Transform parent, string name, int sortOrder)
|
|
||||||
{
|
|
||||||
GameObject canvasGo = GetOrCreateChild(parent, name).gameObject;
|
|
||||||
Canvas canvas = GetOrAddComponent<Canvas>(canvasGo);
|
|
||||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
||||||
canvas.sortingOrder = sortOrder;
|
|
||||||
GetOrAddComponent<CanvasScaler>(canvasGo);
|
|
||||||
GetOrAddComponent<GraphicRaycaster>(canvasGo);
|
|
||||||
return canvasGo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignReference(Object target, string propertyName, Object value)
|
|
||||||
{
|
|
||||||
AssignReference(target, propertyName, value, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignReference(Object target, string propertyName, Object value, List<string> report)
|
|
||||||
{
|
|
||||||
SerializedObject serializedObject = new SerializedObject(target);
|
|
||||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
|
||||||
if (property == null)
|
|
||||||
{
|
|
||||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入引用。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
property.objectReferenceValue = value;
|
|
||||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignReferenceByCandidates(Object target, Object value, List<string> report, params string[] candidates)
|
|
||||||
{
|
|
||||||
if (target == null || candidates == null || candidates.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (string candidate in candidates)
|
|
||||||
{
|
|
||||||
if (TryAssignSerializedReference(target, candidate, value))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (TryAssignMemberReference(target, candidate, value))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
report?.Add($"{target.GetType().Name} 未找到可写引用字段: {string.Join(" / ", candidates)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryAssignSerializedReference(Object target, string propertyName, Object value)
|
|
||||||
{
|
|
||||||
SerializedObject serializedObject = new SerializedObject(target);
|
|
||||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
|
||||||
if (property == null || property.propertyType != SerializedPropertyType.ObjectReference)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
property.objectReferenceValue = value;
|
|
||||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryAssignMemberReference(Object target, string memberName, Object value)
|
|
||||||
{
|
|
||||||
System.Type targetType = target.GetType();
|
|
||||||
|
|
||||||
PropertyInfo property = targetType.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
||||||
if (property != null && property.CanWrite && typeof(Object).IsAssignableFrom(property.PropertyType))
|
|
||||||
{
|
|
||||||
if (value == null || property.PropertyType.IsAssignableFrom(value.GetType()))
|
|
||||||
{
|
|
||||||
property.SetValue(target, value);
|
|
||||||
EditorUtility.SetDirty(target);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldInfo field = targetType.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
||||||
if (field != null && typeof(Object).IsAssignableFrom(field.FieldType))
|
|
||||||
{
|
|
||||||
if (value == null || field.FieldType.IsAssignableFrom(value.GetType()))
|
|
||||||
{
|
|
||||||
field.SetValue(target, value);
|
|
||||||
EditorUtility.SetDirty(target);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignArrayReferences(Object target, string propertyName, IReadOnlyList<Object> values, List<string> report)
|
|
||||||
{
|
|
||||||
SerializedObject serializedObject = new SerializedObject(target);
|
|
||||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
|
||||||
if (property == null || !property.isArray)
|
|
||||||
{
|
|
||||||
report.Add($"{target.GetType().Name}.{propertyName} 不是可写数组字段。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
property.arraySize = values.Count;
|
|
||||||
for (int i = 0; i < values.Count; i++)
|
|
||||||
property.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
|
||||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignAsset(Object target, string propertyName, List<string> report, bool required, params string[] candidates)
|
|
||||||
{
|
|
||||||
Object asset = FindFirstAsset(candidates);
|
|
||||||
if (asset == null && required)
|
|
||||||
report.Add($"未找到 {target.GetType().Name}.{propertyName} 需要的资产: {string.Join(" / ", candidates)}");
|
|
||||||
|
|
||||||
AssignReference(target, propertyName, asset, report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object FindFirstAsset(params string[] candidates)
|
|
||||||
{
|
|
||||||
foreach (string candidate in candidates)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(candidate))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string[] guids = AssetDatabase.FindAssets(candidate);
|
|
||||||
foreach (string guid in guids)
|
|
||||||
{
|
|
||||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
||||||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
|
||||||
if (asset != null && asset.name == candidate)
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object FindFirstAssetByType<T>(params string[] candidates) where T : Object
|
|
||||||
{
|
|
||||||
foreach (string candidate in candidates)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(candidate))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string[] guids = AssetDatabase.FindAssets(candidate);
|
|
||||||
foreach (string guid in guids)
|
|
||||||
{
|
|
||||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
||||||
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
|
||||||
if (asset != null && asset.name == candidate)
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object FindFirstAssetWithExtension(string extension, params string[] candidates)
|
|
||||||
{
|
|
||||||
foreach (string candidate in candidates)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(candidate))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string[] guids = AssetDatabase.FindAssets(candidate);
|
|
||||||
foreach (string guid in guids)
|
|
||||||
{
|
|
||||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
||||||
if (string.IsNullOrEmpty(path) || !path.EndsWith(extension, System.StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
|
||||||
if (asset != null && asset.name == candidate)
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object EnsureInputReaderAsset(List<string> report)
|
|
||||||
{
|
|
||||||
string[] existing = AssetDatabase.FindAssets("t:InputReaderSO");
|
|
||||||
if (existing != null && existing.Length > 0)
|
|
||||||
{
|
|
||||||
string firstPath = AssetDatabase.GUIDToAssetPath(existing[0]);
|
|
||||||
Object found = AssetDatabase.LoadMainAssetAtPath(firstPath);
|
|
||||||
if (found != null)
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
const string inputFolder = "Assets/Data/Input";
|
|
||||||
if (!AssetDatabase.IsValidFolder("Assets/Data"))
|
|
||||||
AssetDatabase.CreateFolder("Assets", "Data");
|
|
||||||
if (!AssetDatabase.IsValidFolder(inputFolder))
|
|
||||||
AssetDatabase.CreateFolder("Assets/Data", "Input");
|
|
||||||
|
|
||||||
const string assetPath = "Assets/Data/Input/InputReader.asset";
|
|
||||||
InputReaderSO created = ScriptableObject.CreateInstance<InputReaderSO>();
|
|
||||||
AssetDatabase.CreateAsset(created, assetPath);
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
|
|
||||||
report?.Add("未找到 InputReaderSO,已自动创建 Assets/Data/Input/InputReader.asset。");
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object EnsurePlayerStatsConfigAsset(List<string> report)
|
|
||||||
{
|
|
||||||
Object existing = FindFirstAssetByType<PlayerStatsSO>("PlayerStats", "PLY_PlayerStats", "PlayerStatsSO");
|
|
||||||
if (existing != null)
|
|
||||||
return existing;
|
|
||||||
|
|
||||||
const string folder = "Assets/Data/Player";
|
|
||||||
EnsureFolder(folder);
|
|
||||||
const string assetPath = "Assets/Data/Player/PLY_PlayerStats.asset";
|
|
||||||
PlayerStatsSO created = ScriptableObject.CreateInstance<PlayerStatsSO>();
|
|
||||||
AssetDatabase.CreateAsset(created, assetPath);
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
report?.Add("未找到 PlayerStatsSO,已自动创建 Assets/Data/Player/PLY_PlayerStats.asset。");
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object EnsurePlayerMovementConfigAsset(List<string> report)
|
|
||||||
{
|
|
||||||
Object existing = FindFirstAssetByType<PlayerMovementConfigSO>("PlayerMovementConfig", "PLY_PlayerMovementConfig", "PlayerMovementConfigSO");
|
|
||||||
if (existing != null)
|
|
||||||
return existing;
|
|
||||||
|
|
||||||
const string folder = "Assets/Data/Player";
|
|
||||||
EnsureFolder(folder);
|
|
||||||
const string assetPath = "Assets/Data/Player/PLY_PlayerMovementConfig.asset";
|
|
||||||
PlayerMovementConfigSO created = ScriptableObject.CreateInstance<PlayerMovementConfigSO>();
|
|
||||||
AssetDatabase.CreateAsset(created, assetPath);
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
report?.Add("未找到 PlayerMovementConfigSO,已自动创建 Assets/Data/Player/PLY_PlayerMovementConfig.asset。");
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object EnsurePlayerAnimationConfigAsset(List<string> report)
|
|
||||||
{
|
|
||||||
Object existing = FindFirstAssetByType<PlayerAnimationConfigSO>("PlayerAnimationConfig", "PLY_PlayerAnimationConfig", "PlayerAnimationConfigSO");
|
|
||||||
if (existing != null)
|
|
||||||
return existing;
|
|
||||||
|
|
||||||
const string folder = "Assets/Data/Player";
|
|
||||||
EnsureFolder(folder);
|
|
||||||
const string assetPath = "Assets/Data/Player/PLY_PlayerAnimationConfig.asset";
|
|
||||||
PlayerAnimationConfigSO created = ScriptableObject.CreateInstance<PlayerAnimationConfigSO>();
|
|
||||||
AssetDatabase.CreateAsset(created, assetPath);
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
report?.Add("未找到 PlayerAnimationConfigSO,已自动创建 Assets/Data/Player/PLY_PlayerAnimationConfig.asset(动画片段需后续手工绑定)。");
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureFolder(string fullPath)
|
|
||||||
{
|
|
||||||
string[] parts = fullPath.Split('/');
|
|
||||||
if (parts.Length == 0 || parts[0] != "Assets")
|
|
||||||
return;
|
|
||||||
|
|
||||||
string current = "Assets";
|
|
||||||
for (int i = 1; i < parts.Length; i++)
|
|
||||||
{
|
|
||||||
string next = current + "/" + parts[i];
|
|
||||||
if (!AssetDatabase.IsValidFolder(next))
|
|
||||||
AssetDatabase.CreateFolder(current, parts[i]);
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RemoveMissingScripts(GameObject go, bool recursive, List<string> report)
|
|
||||||
{
|
|
||||||
if (go == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int removed = 0;
|
|
||||||
if (!recursive)
|
|
||||||
{
|
|
||||||
removed = RemoveMissingScriptsOnSingleObject(go);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var stack = new Stack<Transform>();
|
|
||||||
stack.Push(go.transform);
|
|
||||||
while (stack.Count > 0)
|
|
||||||
{
|
|
||||||
Transform current = stack.Pop();
|
|
||||||
removed += RemoveMissingScriptsOnSingleObject(current.gameObject);
|
|
||||||
foreach (Transform child in current)
|
|
||||||
stack.Push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0)
|
|
||||||
report?.Add($"{go.name}: 已清理 Missing Behaviour x{removed}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int RemoveMissingScriptsOnSingleObject(GameObject go)
|
|
||||||
{
|
|
||||||
int before = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
|
|
||||||
if (before <= 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
Undo.RegisterCompleteObjectUndo(go, "Remove Missing Scripts");
|
|
||||||
GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
|
|
||||||
int after = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
|
|
||||||
return before - after;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetLayer(GameObject go, string layerName, List<string> report)
|
|
||||||
{
|
|
||||||
int layer = LayerMask.NameToLayer(layerName);
|
|
||||||
if (layer < 0)
|
|
||||||
{
|
|
||||||
report.Add($"Layer '{layerName}' 不存在,{go.name} 保持默认 Layer。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
go.layer = layer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssignLayerMask(Object target, string propertyName, string layerName, List<string> report)
|
|
||||||
{
|
|
||||||
int layer = LayerMask.NameToLayer(layerName);
|
|
||||||
if (layer < 0)
|
|
||||||
{
|
|
||||||
report?.Add($"Layer '{layerName}' 不存在,{target.GetType().Name}.{propertyName} 无法写入。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SerializedObject serializedObject = new SerializedObject(target);
|
|
||||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
|
||||||
if (property == null)
|
|
||||||
{
|
|
||||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入 LayerMask。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
property.intValue = 1 << layer;
|
|
||||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureVisualSprite(Transform parent, string childName, Color color, Vector2 size, int sortingOrder)
|
|
||||||
{
|
|
||||||
Transform visualTransform = GetOrCreateChild(parent, childName);
|
|
||||||
SpriteRenderer renderer = GetOrAddComponent<SpriteRenderer>(visualTransform.gameObject);
|
|
||||||
renderer.sprite = GetBuiltinDefaultSprite();
|
|
||||||
renderer.color = color;
|
|
||||||
renderer.drawMode = SpriteDrawMode.Sliced;
|
|
||||||
renderer.size = size;
|
|
||||||
renderer.sortingOrder = sortingOrder;
|
|
||||||
visualTransform.localPosition = Vector3.zero;
|
|
||||||
visualTransform.localRotation = Quaternion.identity;
|
|
||||||
visualTransform.localScale = Vector3.one;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Sprite GetBuiltinDefaultSprite()
|
|
||||||
=> AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
|
|
||||||
|
|
||||||
private static void AddScaffoldNote(GameObject go, string message)
|
|
||||||
{
|
|
||||||
AddScaffoldNote(go, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DisableRenderCamerasUnderRoot(Transform root, List<string> report)
|
|
||||||
{
|
|
||||||
if (root == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UnityEngine.Camera[] cameras = root.GetComponentsInChildren<UnityEngine.Camera>(true);
|
|
||||||
foreach (UnityEngine.Camera camera in cameras)
|
|
||||||
{
|
|
||||||
if (camera == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (camera.enabled)
|
|
||||||
{
|
|
||||||
camera.enabled = false;
|
|
||||||
report?.Add($"已禁用 TestRoom 内渲染相机: {camera.gameObject.name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddScaffoldNote(GameObject go, string message, List<string> report)
|
|
||||||
{
|
|
||||||
// 注意:不再添加 MonoBehaviour 组件,避免 Editor 程序集组件在 Play 模式下出现 Missing Script
|
|
||||||
report?.Add($"{go.name}: {message}");
|
|
||||||
Debug.Log($"[SceneScaffold] {go.name}: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
|
|
||||||
{
|
|
||||||
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
|
||||||
Selection.activeGameObject = root;
|
|
||||||
|
|
||||||
if (report.Count == 0)
|
|
||||||
{
|
|
||||||
Debug.Log($"[SceneScaffoldTools] {scaffoldName} 完成。所有可自动补齐的对象与引用均已生成。", root);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogWarning($"[SceneScaffoldTools] {scaffoldName} 完成,但仍有 {report.Count} 项需要手工确认:\n- {string.Join("\n- ", report)}", root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 0ed4f9a0a8f4ccb4595bc120c7203eb1
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Editor { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: efa269d575c20564089e0ae4009e7157
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c504654ba79100742a30b448b0233d63
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Enemies.AI { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e18876511cb4b9e4ab43cbce1215c72c
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Enemies.Boss.Patterns { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 17b436090127a5d4b88e51b077f2cdb7
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Enemies
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12)。
|
|
||||||
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem,
|
|
||||||
/// 以批处理方式接收 LOS(Line of Sight)检测结果。
|
|
||||||
/// </summary>
|
|
||||||
public interface ILOSRequester
|
|
||||||
{
|
|
||||||
/// <summary>射线起点(通常是眼部位置)。</summary>
|
|
||||||
Vector2 LOSOrigin { get; }
|
|
||||||
|
|
||||||
/// <summary>射线终点(通常是玩家位置)。</summary>
|
|
||||||
Vector2 LOSTarget { get; }
|
|
||||||
|
|
||||||
/// <summary>遮挡 LOS 的物理图层。</summary>
|
|
||||||
LayerMask LOSBlockingMask { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 接收 LOS 检测结果。
|
|
||||||
/// <paramref name="hasLineOfSight"/>: true = 有视线,false = 被遮挡。
|
|
||||||
/// </summary>
|
|
||||||
void ReceiveLOSResult(bool hasLineOfSight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Enemies.Navigation { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d27858cf5a8d193489516e0cc5bcc39a
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Enemies { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e2f632c03ad8b734e953d5fce3d5b048
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Equipment { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: da2dd3b6ba3b910429dff21eaf870278
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Feedback { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e48c25f6e427fea4facf869cec33a9e3
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Input
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 在运行时启用 InputReaderSO 的 ActionMap。
|
|
||||||
/// 挂在 Persistent 场景的 InputReaderHolder 上。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class InputReaderBootstrap : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private InputReaderSO _inputReader;
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
if (_inputReader == null)
|
|
||||||
{
|
|
||||||
_inputReader = FindDefaultInputReader();
|
|
||||||
if (_inputReader == null)
|
|
||||||
Debug.LogError("[InputReaderBootstrap] Could not find InputReaderSO by name or assignment!");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
|
|
||||||
if (_inputReader != null)
|
|
||||||
{
|
|
||||||
_inputReader.LoadBindingOverrides(); // 从 PlayerPrefs 恢复用户自定义绑定
|
|
||||||
_inputReader.EnableGameplayInput();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogError("[InputReaderBootstrap.Start] _inputReader is NULL!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
_inputReader?.DisableAllInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InputReaderSO FindDefaultInputReader()
|
|
||||||
{
|
|
||||||
InputReaderSO[] readers = Resources.FindObjectsOfTypeAll<InputReaderSO>();
|
|
||||||
foreach (InputReaderSO reader in readers)
|
|
||||||
{
|
|
||||||
if (reader != null && reader.name == "InputReader")
|
|
||||||
return reader;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Localization { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 403ac6da4659e9948b5954277b21571a
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Parry { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 521bdf598b4b7c046a783ff8f0258c5e
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
[CreateAssetMenu(menuName = "Player/AnimationConfig")]
|
|
||||||
public class PlayerAnimationConfigSO : ScriptableObject
|
|
||||||
{
|
|
||||||
[Header("移动")]
|
|
||||||
public AnimationClip Idle;
|
|
||||||
public AnimationClip Run;
|
|
||||||
public AnimationClip Jump;
|
|
||||||
public AnimationClip Fall;
|
|
||||||
public AnimationClip Dash;
|
|
||||||
|
|
||||||
[Header("墙")]
|
|
||||||
public AnimationClip WallSlide;
|
|
||||||
|
|
||||||
[Header("受伤 / 死亡")]
|
|
||||||
public AnimationClip Hurt;
|
|
||||||
public AnimationClip Dead;
|
|
||||||
[Tooltip("受击硬直最短持续时间(秒),动画早于此时间结束也不会提前退出)")]
|
|
||||||
public float HurtDuration = 0.4f;
|
|
||||||
|
|
||||||
[Header("弹簧")]
|
|
||||||
public AnimationClip UseSpring;
|
|
||||||
|
|
||||||
[Header("地面攻击(连招序列)")]
|
|
||||||
public AnimationClip[] GroundAttacks;
|
|
||||||
|
|
||||||
/// <summary>每段连击的 HitBox 开启/关闭时间点(归一化 0-1),与 GroundAttacks 索引对应。</summary>
|
|
||||||
[System.Serializable]
|
|
||||||
public struct AttackTimings
|
|
||||||
{
|
|
||||||
[Tooltip("HitBox 开启时间点(归一化 0-1)")]
|
|
||||||
[UnityEngine.Range(0f, 1f)] public float HitBoxEnter;
|
|
||||||
[Tooltip("HitBox 关闭时间点(归一化 0-1)")]
|
|
||||||
[UnityEngine.Range(0f, 1f)] public float HitBoxExit;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Header("地面攻击 HitBox 激活窗口(归一化时间 0-1)")]
|
|
||||||
[Tooltip("每段连击 HitBox 开启/关闭时间点,与 GroundAttacks 索引对应")]
|
|
||||||
public AttackTimings[] GroundAttackTimings = { new AttackTimings { HitBoxEnter = 0.3f, HitBoxExit = 0.6f } };
|
|
||||||
|
|
||||||
[Header("空中攻击")]
|
|
||||||
public AnimationClip AirAttack;
|
|
||||||
public AnimationClip UpAttack;
|
|
||||||
public AnimationClip DownAttack; // 戳击 (Pogo)
|
|
||||||
|
|
||||||
[Header("弹反")]
|
|
||||||
public AnimationClip ParryStart;
|
|
||||||
public AnimationClip ParrySuccess;
|
|
||||||
|
|
||||||
[Header("游泳")]
|
|
||||||
public AnimationClip SwimIdle;
|
|
||||||
public AnimationClip SwimMove;
|
|
||||||
|
|
||||||
/// <summary>按连招步骤取地面攻击动画,越界自动取最后一个。</summary>
|
|
||||||
public AnimationClip GetAttackClip(int step)
|
|
||||||
{
|
|
||||||
if (GroundAttacks == null || GroundAttacks.Length == 0) return null;
|
|
||||||
return step < GroundAttacks.Length ? GroundAttacks[step] : GroundAttacks[^1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using BaseGames.Combat;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 玩家战斗组件。
|
|
||||||
/// 架构 05_PlayerModule §5:HitBox 直接挂在 Player Prefab 子节点上,不经过 WeaponInstance。
|
|
||||||
/// 节点:[HitBoxGround]、[HitBoxUp]、[HitBoxDown]、[HitBoxAir]。
|
|
||||||
/// </summary>
|
|
||||||
public class PlayerCombat : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private WeaponManager _weaponManager;
|
|
||||||
|
|
||||||
[Header("HitBox(Player Prefab 子节点)")]
|
|
||||||
[SerializeField] private HitBox _hitBoxGround;
|
|
||||||
[SerializeField] private HitBox _hitBoxUp;
|
|
||||||
[SerializeField] private HitBox _hitBoxDown;
|
|
||||||
[SerializeField] private HitBox _hitBoxAir;
|
|
||||||
|
|
||||||
private PlayerStats _stats;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_stats = GetComponentInParent<PlayerStats>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
if (_weaponManager != null)
|
|
||||||
_weaponManager.OnWeaponChanged += RefreshWeaponData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (_weaponManager != null)
|
|
||||||
_weaponManager.OnWeaponChanged -= RefreshWeaponData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshWeaponData(WeaponSO weapon)
|
|
||||||
{
|
|
||||||
// 武器切换时可在此更新 HitBox 默认尺寸等
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 连击段伤害来源切换 ────────────────────────────────────────────────
|
|
||||||
/// <summary>
|
|
||||||
/// 根据当前连招段切换 HitBox 的 DamageSource(由 AttackState 在每段开始时调用)。
|
|
||||||
/// </summary>
|
|
||||||
public void SetComboSegmentSource(int comboIndex)
|
|
||||||
{
|
|
||||||
WeaponSO w = _weaponManager?.ActiveWeapon;
|
|
||||||
if (w == null) return;
|
|
||||||
|
|
||||||
DamageSourceSO src = comboIndex switch
|
|
||||||
{
|
|
||||||
0 => w.attack1Source,
|
|
||||||
1 => w.attack2Source,
|
|
||||||
2 => w.attack3Source,
|
|
||||||
_ => w.attack1Source,
|
|
||||||
};
|
|
||||||
_hitBoxGround?.SetDamageSource(src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── HitBox 激活(由 State / AnimationEvent 调用)─────────────────────
|
|
||||||
public void EnableWeaponHitBox(AttackDirection dir)
|
|
||||||
{
|
|
||||||
var source = _weaponManager?.ActiveWeapon?.GetSourceByDir(dir);
|
|
||||||
GetHitBox(dir)?.Activate(source, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableWeaponHitBox(AttackDirection dir)
|
|
||||||
=> GetHitBox(dir)?.Deactivate();
|
|
||||||
|
|
||||||
public void DisableAllWeaponHitBoxes()
|
|
||||||
{
|
|
||||||
_hitBoxGround?.Deactivate();
|
|
||||||
_hitBoxUp?.Deactivate();
|
|
||||||
_hitBoxDown?.Deactivate();
|
|
||||||
_hitBoxAir?.Deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>命中确认回调:增加灵力(由 HurtBox.ReceiveDamage 步骤 7 的 HitConfirmed 事件订阅)。</summary>
|
|
||||||
public void OnHitConfirmed(DamageInfo info)
|
|
||||||
{
|
|
||||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
|
||||||
_stats?.AddSoulPower(gain);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HitBox GetHitBox(AttackDirection dir) => dir switch
|
|
||||||
{
|
|
||||||
AttackDirection.Ground => _hitBoxGround,
|
|
||||||
AttackDirection.Up => _hitBoxUp,
|
|
||||||
AttackDirection.Down => _hitBoxDown,
|
|
||||||
AttackDirection.Air => _hitBoxAir,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
|
|
||||||
|
|
||||||
/// </summary>
|
|
||||||
[RequireComponent(typeof(Rigidbody2D))]
|
|
||||||
public class PlayerMovement : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("配置")]
|
|
||||||
[SerializeField] private PlayerMovementConfigSO _config;
|
|
||||||
|
|
||||||
[Header("地面检测")]
|
|
||||||
[SerializeField] private Transform _groundCheck;
|
|
||||||
[SerializeField] private Vector2 _groundCheckSize = new Vector2(0.8f, 0.05f);
|
|
||||||
[SerializeField] private LayerMask _groundLayer;
|
|
||||||
|
|
||||||
[Header("朝向")]
|
|
||||||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
|
||||||
|
|
||||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
|
||||||
private Rigidbody2D _rb;
|
|
||||||
private float _coyoteTimer;
|
|
||||||
private bool _isGrounded;
|
|
||||||
private bool _isWallLeft;
|
|
||||||
private bool _isWallRight;
|
|
||||||
private bool _onOneWayPlatform;
|
|
||||||
private int _facingDirection = 1;
|
|
||||||
private bool _cancelWindowOpen;
|
|
||||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
|
||||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
|
||||||
|
|
||||||
public bool IsGrounded => _isGrounded;
|
|
||||||
public bool HasCoyoteTime => _coyoteTimer > 0f;
|
|
||||||
public bool IsWallLeft => _isWallLeft;
|
|
||||||
public bool IsWallRight => _isWallRight;
|
|
||||||
public bool OnOneWayPlatform => _onOneWayPlatform;
|
|
||||||
public int FacingDirection => _facingDirection;
|
|
||||||
public Rigidbody2D Rb => _rb;
|
|
||||||
public bool CancelWindowOpen => _cancelWindowOpen;
|
|
||||||
public SurfaceType CurrentSurface => _currentSurface;
|
|
||||||
/// <summary>垂直速度大于零(处于上升弧段)。供 WallJumpState 判断起跳是否结束。</summary>
|
|
||||||
public bool IsRising => _rb.velocity.y > 0f;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
|
||||||
_rb = GetComponent<Rigidbody2D>();
|
|
||||||
if (_spriteRenderer == null)
|
|
||||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FixedUpdate()
|
|
||||||
{
|
|
||||||
CheckGrounded();
|
|
||||||
CheckWalls();
|
|
||||||
|
|
||||||
if (_isGrounded)
|
|
||||||
_coyoteTimer = _config.CoyoteTime;
|
|
||||||
else
|
|
||||||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 移动 ──────────────────────────────────────────────────────────────
|
|
||||||
public void Move(float speedX)
|
|
||||||
{
|
|
||||||
float target = speedX;
|
|
||||||
float current = _rb.velocity.x;
|
|
||||||
float accel = Mathf.Abs(speedX) > 0.01f ? _config.Acceleration : _config.Deceleration;
|
|
||||||
float newX = Mathf.MoveTowards(current, target, accel * Time.fixedDeltaTime);
|
|
||||||
_rb.velocity = new Vector2(newX, _rb.velocity.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 跳跃 ──────────────────────────────────────────────────────────────
|
|
||||||
public void Jump(bool isVariable = true)
|
|
||||||
{
|
|
||||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
|
||||||
_coyoteTimer = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CutJump()
|
|
||||||
{
|
|
||||||
if (_rb.velocity.y > 0f)
|
|
||||||
_rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
|
||||||
public void SetGravityScale(float scale) => _rb.gravityScale = scale;
|
|
||||||
|
|
||||||
// ── 击退 ──────────────────────────────────────────────────────────────
|
|
||||||
public void ApplyKnockback(Vector2 direction, float force)
|
|
||||||
=> _rb.velocity = direction.normalized * force;
|
|
||||||
|
|
||||||
// ── 速度控制 ──────────────────────────────────────────────────────────
|
|
||||||
public void ZeroVelocity() => _rb.velocity = Vector2.zero;
|
|
||||||
public void ZeroHorizontalVelocity() => _rb.velocity = new Vector2(0f, _rb.velocity.y);
|
|
||||||
|
|
||||||
// ── 朝向 ──────────────────────────────────────────────────────────────
|
|
||||||
public void UpdateFacing()
|
|
||||||
{
|
|
||||||
float vx = _rb.velocity.x;
|
|
||||||
if (Mathf.Abs(vx) < 0.1f) return;
|
|
||||||
int dir = vx > 0f ? 1 : -1;
|
|
||||||
if (dir == _facingDirection) return;
|
|
||||||
_facingDirection = dir;
|
|
||||||
if (_spriteRenderer != null)
|
|
||||||
_spriteRenderer.flipX = dir < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 取消窗口 ──────────────────────────────────────────────────────────
|
|
||||||
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
|
|
||||||
|
|
||||||
// ── 冲刺 ──────────────────────────────────────────────────────────────
|
|
||||||
/// <summary>
|
|
||||||
/// 施加冲刺速度(DashState/AerialDashState 调用)。
|
|
||||||
/// direction 应为归一化水平向量(±1, 0)。
|
|
||||||
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
|
|
||||||
/// </summary>
|
|
||||||
public void Dash(Vector2 direction, float speed)
|
|
||||||
{
|
|
||||||
_rb.velocity = new Vector2(direction.x * speed, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(向下缓慢滑动)。
|
|
||||||
/// WallSlideState.OnStateFixedUpdate 每帧调用。
|
|
||||||
/// </summary>
|
|
||||||
public void ApplyWallSlide()
|
|
||||||
{
|
|
||||||
float targetY = -_config.WallSlideSpeed;
|
|
||||||
if (_rb.velocity.y < targetY)
|
|
||||||
_rb.velocity = new Vector2(_rb.velocity.x, targetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
|
|
||||||
/// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
|
|
||||||
/// </summary>
|
|
||||||
public void WallJump(int wallDir)
|
|
||||||
{
|
|
||||||
float forceX = -wallDir * _config.WallJumpForceX;
|
|
||||||
float forceY = _config.WallJumpForceY;
|
|
||||||
_rb.velocity = new Vector2(forceX, forceY);
|
|
||||||
_coyoteTimer = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>单向平台穿透(输入下行 + 跳跃键时触发)。</summary>
|
|
||||||
public void DropThroughPlatform() { }
|
|
||||||
|
|
||||||
// ── Physics 检测 ──────────────────────────────────────────────────────
|
|
||||||
private void CheckGrounded()
|
|
||||||
{
|
|
||||||
bool wasGrounded = _isGrounded;
|
|
||||||
Vector2 origin = _groundCheck != null
|
|
||||||
? (Vector2)_groundCheck.position
|
|
||||||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
|
||||||
|
|
||||||
_isGrounded = Physics2D.OverlapBoxNonAlloc(origin, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
|
|
||||||
|
|
||||||
if (_isGrounded && !wasGrounded)
|
|
||||||
_coyoteTimer = _config.CoyoteTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckWalls()
|
|
||||||
{
|
|
||||||
float len = _config.WallRayLength;
|
|
||||||
float offY = _config.WallRayOffsetY;
|
|
||||||
Vector2 pos = (Vector2)transform.position + Vector2.up * offY;
|
|
||||||
|
|
||||||
_isWallLeft = Physics2D.Raycast(pos, Vector2.left, len, _groundLayer);
|
|
||||||
_isWallRight = Physics2D.Raycast(pos, Vector2.right, len, _groundLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
|
||||||
{
|
|
||||||
if (_groundCheck == null) return;
|
|
||||||
Gizmos.color = Color.green;
|
|
||||||
Gizmos.DrawWireCube(_groundCheck.position, _groundCheckSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>当前所在地面类型(用于脚步声等反馈)。</summary>
|
|
||||||
public enum SurfaceType
|
|
||||||
{
|
|
||||||
Ground,
|
|
||||||
OneWayPlatform,
|
|
||||||
Slope,
|
|
||||||
Ice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
[CreateAssetMenu(menuName = "Player/MovementConfig")]
|
|
||||||
public class PlayerMovementConfigSO : ScriptableObject
|
|
||||||
{
|
|
||||||
[Header("地面移动")]
|
|
||||||
public float RunSpeed = 7f;
|
|
||||||
public float Acceleration = 50f;
|
|
||||||
public float Deceleration = 80f;
|
|
||||||
|
|
||||||
[Header("跳跃")]
|
|
||||||
public float JumpForce = 18f;
|
|
||||||
public float CoyoteTime = 0.12f;
|
|
||||||
public float FallGravityMult = 2.5f;
|
|
||||||
public float MaxFallSpeed = 20f;
|
|
||||||
|
|
||||||
[Header("冲刺")]
|
|
||||||
public float DashSpeed = 20f;
|
|
||||||
public float DashDuration = 0.18f;
|
|
||||||
public float DashCooldown = 0.4f;
|
|
||||||
public int MaxAerialDashes = 1;
|
|
||||||
|
|
||||||
[Header("蹬墙 / 壁滑")]
|
|
||||||
public float WallSlideSpeed = 2f;
|
|
||||||
public float WallJumpForceX = 12f;
|
|
||||||
public float WallJumpForceY = 16f;
|
|
||||||
public float WallRayLength = 0.55f;
|
|
||||||
public float WallRayOffsetY = 0.2f;
|
|
||||||
public float WallGrabMaxHeightGain = 0.5f;
|
|
||||||
public float WallGrabReleaseDelay = 0.08f;
|
|
||||||
public float WallJumpBackForceX = 14f;
|
|
||||||
public float WallJumpAwayForceX = 10f;
|
|
||||||
public float WallJumpAwayForceY = 18f;
|
|
||||||
public float WallJumpInputLockDuration = 0.15f;
|
|
||||||
|
|
||||||
[Header("重力")]
|
|
||||||
public float DefaultGravityScale = 3f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 独立墙壁检测组件(架构 05_PlayerModule §13)。
|
|
||||||
/// ⚠️ 不嵌入 PlayerMovement,以保持单一职责。
|
|
||||||
/// 每侧发两根射线(Top + Bottom),两根均命中才视为接触墙壁(防卡角误判)。
|
|
||||||
/// WallSlideState / WallJumpState 通过 PlayerController.WallDetector 访问。
|
|
||||||
/// </summary>
|
|
||||||
[RequireComponent(typeof(PlayerMovement))]
|
|
||||||
public class PlayerWallDetector : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private PlayerMovementConfigSO _config;
|
|
||||||
|
|
||||||
[Header("墙壁 Layer(默认使用 \"Wall\" + \"Ground\")")]
|
|
||||||
[SerializeField] private LayerMask _wallLayer;
|
|
||||||
|
|
||||||
/// <summary>当前是否正在触碰墙壁。</summary>
|
|
||||||
public bool IsTouchingWall { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>触碰到的墙壁方向:+1 = 右墙,-1 = 左墙,0 = 无墙。</summary>
|
|
||||||
public int WallDirection { get; private set; }
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FixedUpdate()
|
|
||||||
{
|
|
||||||
bool rightWall = CheckSide(Vector2.right);
|
|
||||||
bool leftWall = CheckSide(Vector2.left);
|
|
||||||
|
|
||||||
IsTouchingWall = rightWall || leftWall;
|
|
||||||
WallDirection = rightWall ? 1 : (leftWall ? -1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。
|
|
||||||
/// </summary>
|
|
||||||
private bool CheckSide(Vector2 dir)
|
|
||||||
{
|
|
||||||
Vector2 center = transform.position;
|
|
||||||
float len = _config.WallRayLength;
|
|
||||||
float oy = _config.WallRayOffsetY;
|
|
||||||
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Ground");
|
|
||||||
|
|
||||||
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
|
||||||
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);
|
|
||||||
return top && bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
|
||||||
{
|
|
||||||
if (_config == null) return;
|
|
||||||
float len = _config.WallRayLength;
|
|
||||||
float oy = _config.WallRayOffsetY;
|
|
||||||
Vector2 center = transform.position;
|
|
||||||
|
|
||||||
Gizmos.color = IsTouchingWall ? Color.red : Color.cyan;
|
|
||||||
// 右侧两根射线
|
|
||||||
Gizmos.DrawRay(center + Vector2.up * oy, Vector2.right * len);
|
|
||||||
Gizmos.DrawRay(center + Vector2.down * oy, Vector2.right * len);
|
|
||||||
// 左侧两根射线
|
|
||||||
Gizmos.DrawRay(center + Vector2.up * oy, Vector2.left * len);
|
|
||||||
Gizmos.DrawRay(center + Vector2.down * oy, Vector2.left * len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 治愈弹簧系统(架构 05_PlayerModule §7)。
|
|
||||||
/// PlayerStats 中已预留 CurrentSpringCharges / MaxSpringCharges / SpringKillPoints 字段。
|
|
||||||
/// TODO: 实现弹簧充能逻辑:
|
|
||||||
/// - 击杀敌人时调用 PlayerStats.AddSpringKillPoint() 积累充能
|
|
||||||
/// - 按下治愈键时消耗充能槽并恢复玩家 HP
|
|
||||||
/// - 充能满格时触发特殊强化状态(视设计而定)
|
|
||||||
/// </summary>
|
|
||||||
public class SpringSystem : MonoBehaviour { }
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 空中冲刺状态(架构 05_PlayerModule §12)。
|
|
||||||
/// 与地面 DashState 独立,消耗 MaxAerialDashes 次数;
|
|
||||||
/// 空中冲刺可向任意方向(使用移动输入方向,无输入则使用朝向)。
|
|
||||||
/// </summary>
|
|
||||||
public class AerialDashState : PlayerStateBase
|
|
||||||
{
|
|
||||||
private float _timer;
|
|
||||||
private int _aerialDashesLeft;
|
|
||||||
private int _facingDir;
|
|
||||||
|
|
||||||
public bool HasAerialDash => _aerialDashesLeft > 0;
|
|
||||||
|
|
||||||
public AerialDashState(PlayerController owner) : base(owner)
|
|
||||||
{
|
|
||||||
_aerialDashesLeft = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
|
|
||||||
_facingDir = Owner.FacingDirection;
|
|
||||||
_timer = Cfg.DashDuration;
|
|
||||||
|
|
||||||
// 无敌帧
|
|
||||||
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
|
|
||||||
|
|
||||||
// 关闭重力,施加冲刺速度(空中冲刺不改变垂直速度)
|
|
||||||
Move?.SetGravityScale(0f);
|
|
||||||
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
|
|
||||||
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
|
|
||||||
|
|
||||||
// 播放冲刺动画(复用地面冲刺动画)
|
|
||||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
_timer -= Time.deltaTime;
|
|
||||||
if (_timer <= 0f)
|
|
||||||
{
|
|
||||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
|
||||||
{
|
|
||||||
// 冲刺期间锁定速度
|
|
||||||
if (_timer > 0f)
|
|
||||||
{
|
|
||||||
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
|
|
||||||
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>着地时重置空中冲刺次数(由 PlayerController 在着地时调用)。</summary>
|
|
||||||
public void ResetAerialDashes()
|
|
||||||
{
|
|
||||||
_aerialDashesLeft = Cfg.MaxAerialDashes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 38830e1649a9eb548b20540a737bdf09
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using BaseGames.Combat;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 空中攻击状态(架构 05_PlayerModule §2)。
|
|
||||||
/// 由 FallState / JumpState 接收攻击输入后转入;
|
|
||||||
/// Animancer 帧事件驱动 HitBoxAir 激活;结束后回到 FallState。
|
|
||||||
/// </summary>
|
|
||||||
public class AirAttackState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public AirAttackState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
Owner.Combat?.SetComboSegmentSource(0);
|
|
||||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
|
||||||
|
|
||||||
var clip = Owner.Weapon?.ActiveWeapon?.airAttackClip;
|
|
||||||
if (clip != null && clip.Clip != null)
|
|
||||||
{
|
|
||||||
var state = Anim.Play(clip);
|
|
||||||
state.Events(this).OnEnd = OnClipEnd;
|
|
||||||
}
|
|
||||||
else if (AnimCfg?.AirAttack != null)
|
|
||||||
{
|
|
||||||
var state = Anim.Play(AnimCfg.AirAttack);
|
|
||||||
state.Events(this).OnEnd = OnClipEnd;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnClipEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClipEnd()
|
|
||||||
{
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using BaseGames.Combat;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 地面攻击状态(3 段连击)。
|
|
||||||
/// 由 PlayerController 实例化,AttackEvent 触发切换。
|
|
||||||
/// 通过 Animancer 帧事件驱动 HitBox 激活/关闭。
|
|
||||||
/// </summary>
|
|
||||||
public class AttackState : PlayerStateBase
|
|
||||||
{
|
|
||||||
private int _comboIndex;
|
|
||||||
|
|
||||||
public AttackState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
_comboIndex = 0;
|
|
||||||
PlayAttackClip();
|
|
||||||
Input.AttackEvent += OnAttackInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Input.AttackEvent -= OnAttackInput;
|
|
||||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate() { }
|
|
||||||
public override void OnStateFixedUpdate() { }
|
|
||||||
|
|
||||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void PlayAttackClip()
|
|
||||||
{
|
|
||||||
// ⚠️ 字段名 GroundAttacks(非 AttackChainClips)
|
|
||||||
var clip = AnimCfg.GroundAttacks[_comboIndex];
|
|
||||||
var state = Anim.Play(clip);
|
|
||||||
var events = state.Events(this);
|
|
||||||
events.OnEnd = OnClipEnd;
|
|
||||||
|
|
||||||
// HitBox 由 Animancer 归一化时间事件驱动(时间点配置于 PlayerAnimationConfigSO)
|
|
||||||
var timings = AnimCfg?.GroundAttackTimings;
|
|
||||||
float enterTime = timings != null && _comboIndex < timings.Length
|
|
||||||
? timings[_comboIndex].HitBoxEnter : 0.3f;
|
|
||||||
float exitTime = timings != null && _comboIndex < timings.Length
|
|
||||||
? timings[_comboIndex].HitBoxExit : 0.6f;
|
|
||||||
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
|
|
||||||
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClipEnd()
|
|
||||||
{
|
|
||||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
|
||||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAttackInput()
|
|
||||||
{
|
|
||||||
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
|
|
||||||
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
|
|
||||||
if (_comboIndex < maxCombo - 1)
|
|
||||||
{
|
|
||||||
_comboIndex++;
|
|
||||||
PlayAttackClip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 地面冲刺状态(架构 05_PlayerModule §2 + §12)。
|
|
||||||
/// 施加水平位移 + 无敌帧;冲刺期间重力归零,结束后恢复;
|
|
||||||
/// 需解锁 AbilityType.Dash 才能进入(PlayerController 负责条件检查)。
|
|
||||||
/// </summary>
|
|
||||||
public class DashState : PlayerStateBase
|
|
||||||
{
|
|
||||||
private float _timer;
|
|
||||||
private float _cooldownTimer;
|
|
||||||
private int _facingDir;
|
|
||||||
|
|
||||||
public bool CanDash => _cooldownTimer <= 0f;
|
|
||||||
|
|
||||||
/// <summary>冲刺状态期间应视为无敌,PlayerController 据此跳过受击硬直。</summary>
|
|
||||||
public override bool IsInvincible => true;
|
|
||||||
|
|
||||||
public DashState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
Debug.Assert(Cfg != null, "[DashState] MovementConfig 未配置,请在 PlayerController Inspector 中绑定 MovementConfig SO。", _owner);
|
|
||||||
|
|
||||||
_facingDir = Owner.FacingDirection;
|
|
||||||
_timer = Cfg.DashDuration;
|
|
||||||
|
|
||||||
// 无敌帧
|
|
||||||
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
|
|
||||||
|
|
||||||
// 关闭重力,施加冲刺速度
|
|
||||||
Move?.SetGravityScale(0f);
|
|
||||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
|
||||||
|
|
||||||
// 播放冲刺动画
|
|
||||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
_timer -= Time.deltaTime;
|
|
||||||
if (_timer <= 0f)
|
|
||||||
EndDash();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
// 恢复默认重力
|
|
||||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
|
||||||
_cooldownTimer = Cfg.DashCooldown;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
|
||||||
{
|
|
||||||
// 冲刺期间保持冲刺速度(防止摩擦力减速)
|
|
||||||
if (_timer > 0f)
|
|
||||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndDash()
|
|
||||||
{
|
|
||||||
if (Move != null && Move.IsGrounded)
|
|
||||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
|
||||||
else
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>每帧减少冷却(由 PlayerController.Update 调用)。</summary>
|
|
||||||
public void TickCooldown(float dt)
|
|
||||||
{
|
|
||||||
if (_cooldownTimer > 0f) _cooldownTimer -= dt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>下落状态。着地后转为 Idle 或 Run,持有郊狼时间允许跳跃。</summary>
|
|
||||||
public class FallState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public FallState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
if (AnimCfg?.Fall != null)
|
|
||||||
Anim.Play(AnimCfg.Fall);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
// 郊狼时间跳跃
|
|
||||||
if (Buffer.ConsumeJump() && Move.HasCoyoteTime)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 着地
|
|
||||||
if (Move.IsGrounded)
|
|
||||||
{
|
|
||||||
Move.ZeroVelocity();
|
|
||||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
|
||||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
|
||||||
else
|
|
||||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 空中水平移动
|
|
||||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
|
||||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
|
||||||
{
|
|
||||||
// 增强下落重力
|
|
||||||
if (Move.Rb.velocity.y < 0f)
|
|
||||||
{
|
|
||||||
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
|
|
||||||
Move.Rb.velocity = new Vector2(
|
|
||||||
Move.Rb.velocity.x,
|
|
||||||
Mathf.Max(Move.Rb.velocity.y + extraGrav, -Cfg.MaxFallSpeed));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>闲置状态。默认入口状态,播放 Idle 动画。</summary>
|
|
||||||
public class IdleState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public IdleState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
if (AnimCfg?.Idle != null)
|
|
||||||
Anim.Play(AnimCfg.Idle);
|
|
||||||
Move?.ZeroHorizontalVelocity();
|
|
||||||
// 着地时重置空中冲刺次数
|
|
||||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
if (!Move.IsGrounded)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Buffer.ConsumeJump())
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>跳跃状态。在 OnStateEnter 触发跳跃,速度降为零时转为 FallState。</summary>
|
|
||||||
public class JumpState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public JumpState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
if (AnimCfg?.Jump != null)
|
|
||||||
Anim.Play(AnimCfg.Jump);
|
|
||||||
Move.Jump();
|
|
||||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
// 上升结束时转为下落
|
|
||||||
if (Move.Rb.velocity.y <= 0f)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 水平移动
|
|
||||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
|
||||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnJumpCancelled() => Move.CutJump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>跑步状态。播放 Run 动画并驱动水平移动。</summary>
|
|
||||||
public class RunState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public RunState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
if (AnimCfg?.Run != null)
|
|
||||||
Anim.Play(AnimCfg.Run);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
if (!Move.IsGrounded)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Buffer.ConsumeJump())
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
|
|
||||||
{
|
|
||||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
|
||||||
{
|
|
||||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using BaseGames.Combat;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 上劈状态(架构 05_PlayerModule §2)。
|
|
||||||
/// 激活 HitBoxUp;结束后回到 Idle(地面)或 FallState(空中)。
|
|
||||||
/// </summary>
|
|
||||||
public class UpAttackState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public UpAttackState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up);
|
|
||||||
|
|
||||||
var clip = Owner.Weapon?.ActiveWeapon?.upAttackClip;
|
|
||||||
if (clip != null && clip.Clip != null)
|
|
||||||
{
|
|
||||||
var state = Anim.Play(clip);
|
|
||||||
state.Events(this).OnEnd = OnClipEnd;
|
|
||||||
}
|
|
||||||
else if (AnimCfg?.UpAttack != null)
|
|
||||||
{
|
|
||||||
var state = Anim.Play(AnimCfg.UpAttack);
|
|
||||||
state.Events(this).OnEnd = OnClipEnd;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnClipEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClipEnd()
|
|
||||||
{
|
|
||||||
if (Move.IsGrounded)
|
|
||||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
|
||||||
else
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 蹬墙跳状态(架构 05_PlayerModule §2)。
|
|
||||||
/// 从 WallSlideState 进入;施加背墙方向的水平 + 垂直速度;
|
|
||||||
/// 短暂锁定水平输入后转为 FallState。
|
|
||||||
/// </summary>
|
|
||||||
public class WallJumpState : PlayerStateBase
|
|
||||||
{
|
|
||||||
private float _inputLockTimer;
|
|
||||||
private int _wallDir;
|
|
||||||
|
|
||||||
public WallJumpState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
// 记录墙壁方向(跳跃反向)
|
|
||||||
_wallDir = Owner.WallDetector != null ? Owner.WallDetector.WallDirection : 0;
|
|
||||||
if (_wallDir == 0) _wallDir = -Owner.FacingDirection;
|
|
||||||
|
|
||||||
// 施加蹬墙跳速度
|
|
||||||
Move?.WallJump(_wallDir);
|
|
||||||
|
|
||||||
// 锁定水平输入
|
|
||||||
_inputLockTimer = Cfg.WallJumpInputLockDuration;
|
|
||||||
|
|
||||||
// 播放跳跃动画(复用跳跃动画)
|
|
||||||
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
|
|
||||||
|
|
||||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
_inputLockTimer -= Time.deltaTime;
|
|
||||||
|
|
||||||
// 上升结束 → 下落
|
|
||||||
if (!Move.IsRising)
|
|
||||||
{
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输入锁结束后允许水平控制
|
|
||||||
if (_inputLockTimer <= 0f && Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
|
||||||
Move?.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnJumpCancelled() => Move?.CutJump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace BaseGames.Player.States
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 壁滑状态(架构 05_PlayerModule §2)。
|
|
||||||
/// 需有 PlayerWallDetector.IsTouchingWall == true 才能进入;
|
|
||||||
/// 限制下落速度为 WallSlideSpeed;按跳跃则切换到 WallJumpState。
|
|
||||||
/// </summary>
|
|
||||||
public class WallSlideState : PlayerStateBase
|
|
||||||
{
|
|
||||||
public WallSlideState(PlayerController owner) : base(owner) { }
|
|
||||||
|
|
||||||
public override void OnStateEnter()
|
|
||||||
{
|
|
||||||
if (AnimCfg?.WallSlide != null)
|
|
||||||
Anim?.Play(AnimCfg.WallSlide);
|
|
||||||
|
|
||||||
Input.JumpStartedEvent += OnJumpPressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateExit()
|
|
||||||
{
|
|
||||||
Input.JumpStartedEvent -= OnJumpPressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateUpdate()
|
|
||||||
{
|
|
||||||
// 离开墙壁 → 下落
|
|
||||||
if (Owner.WallDetector == null || !Owner.WallDetector.IsTouchingWall)
|
|
||||||
{
|
|
||||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 着地 → 闲置
|
|
||||||
if (Move.IsGrounded)
|
|
||||||
{
|
|
||||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnStateFixedUpdate()
|
|
||||||
{
|
|
||||||
// 限制下落速度(壁滑缓慢下落)
|
|
||||||
Move?.ApplyWallSlide();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnJumpPressed()
|
|
||||||
{
|
|
||||||
Owner.TransitionTo(Owner.GetState<WallJumpState>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Player.States { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: ee579de5ae36a9448b7463976699bc20
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
using Animancer;
|
|
||||||
using BaseGames.Combat;
|
|
||||||
using BaseGames.Feedback;
|
|
||||||
|
|
||||||
namespace BaseGames.Player
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 武器数据 SO(纯数据,不含 Prefab 引用)。
|
|
||||||
/// 每个攻击方向对应一个 DamageSourceSO 和 ClipTransition。
|
|
||||||
/// HitBox 直接挂载在 Player Prefab 上(架构 05_PlayerModule §7)。
|
|
||||||
/// </summary>
|
|
||||||
[CreateAssetMenu(menuName = "Player/Weapon")]
|
|
||||||
public class WeaponSO : ScriptableObject
|
|
||||||
{
|
|
||||||
[Header("基础信息")]
|
|
||||||
public string weaponId; // 全局唯一 ID,如 "Weapon_SkyBlade"
|
|
||||||
public string displayName; // 显示名,如 "天裂刃"
|
|
||||||
public Sprite icon;
|
|
||||||
public WeaponType weaponType;
|
|
||||||
|
|
||||||
[Header("连击动画(Animancer ClipTransition)")]
|
|
||||||
public ClipTransition attack1Clip;
|
|
||||||
public ClipTransition attack2Clip;
|
|
||||||
public ClipTransition attack3Clip;
|
|
||||||
public ClipTransition airAttackClip;
|
|
||||||
public ClipTransition upAttackClip;
|
|
||||||
public ClipTransition downAttackClip;
|
|
||||||
|
|
||||||
[Header("伤害来源(每段独立 DamageSourceSO)")]
|
|
||||||
public DamageSourceSO attack1Source;
|
|
||||||
public DamageSourceSO attack2Source;
|
|
||||||
public DamageSourceSO attack3Source;
|
|
||||||
public DamageSourceSO airAttackSource;
|
|
||||||
public DamageSourceSO upAttackSource;
|
|
||||||
public DamageSourceSO downAttackSource;
|
|
||||||
|
|
||||||
[Header("HitBox 覆盖(零向量 = 使用 PlayerCombat 默认尺寸)")]
|
|
||||||
public Vector2 hitBoxSizeOverride;
|
|
||||||
public Vector2 hitBoxOffsetOverride;
|
|
||||||
|
|
||||||
[Header("武器特效")]
|
|
||||||
public WeaponVFXConfig vfxConfig;
|
|
||||||
|
|
||||||
[Header("战斗参数")]
|
|
||||||
[Tooltip("命中确认时增加的灵力值(覆盖 PlayerCombat 默认值 10)")]
|
|
||||||
[Min(0)]
|
|
||||||
public int soulPowerGain = 10;
|
|
||||||
|
|
||||||
// ── 方向查询 ──────────────────────────────────────────────────────────
|
|
||||||
public DamageSourceSO GetSourceByDir(AttackDirection dir) => dir switch
|
|
||||||
{
|
|
||||||
AttackDirection.Ground => attack1Source,
|
|
||||||
AttackDirection.Up => upAttackSource,
|
|
||||||
AttackDirection.Down => downAttackSource,
|
|
||||||
AttackDirection.Air => airAttackSource,
|
|
||||||
_ => attack1Source,
|
|
||||||
};
|
|
||||||
|
|
||||||
public ClipTransition GetClipByCombo(int comboIndex) => comboIndex switch
|
|
||||||
{
|
|
||||||
0 => attack1Clip,
|
|
||||||
1 => attack2Clip,
|
|
||||||
2 => attack3Clip,
|
|
||||||
_ => attack1Clip,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 武器类型枚举(架构 05 §7)──────────────────────────────────────────────
|
|
||||||
public enum WeaponType
|
|
||||||
{
|
|
||||||
SkyBlade, // 天魂:裂空刃(高频轻击)
|
|
||||||
EarthHammer, // 地魂:地震锤(低频重击,范围大)
|
|
||||||
LifeScythe, // 命魂:命镰(穿透,直线斩)
|
|
||||||
Custom, // 护符替换或未来扩展武器
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 武器特效配置([Serializable] 内嵌,架构 05 §7)────────────────────────
|
|
||||||
[Serializable]
|
|
||||||
public class WeaponVFXConfig
|
|
||||||
{
|
|
||||||
[Tooltip("切换到此武器时播放的特效预设 ID(对应 IFeedbackPlayer.TriggerPreset)")]
|
|
||||||
public string onEquipPresetId;
|
|
||||||
|
|
||||||
[Tooltip("武器挥斩拖尾 Prefab(null = 不显示拖尾)")]
|
|
||||||
public GameObject weaponTrailPrefab;
|
|
||||||
|
|
||||||
public Color trailColor = Color.white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Placeholder to prevent asmdef-no-scripts warning.
|
|
||||||
namespace BaseGames.Progression { }
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d780bf52980df49418bfbb9521410cd9
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user