com.wallstop-studios.unity-helpers 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/DATA_STRUCTURES.md +7 -7
- package/Docs/EFFECTS_SYSTEM.md +836 -8
- package/Docs/EFFECTS_SYSTEM_TUTORIAL.md +77 -18
- package/Docs/HULLS.md +2 -2
- package/Docs/RANDOM_PERFORMANCE.md +1 -1
- package/Docs/REFLECTION_HELPERS.md +1 -1
- package/Docs/RELATIONAL_COMPONENTS.md +51 -6
- package/Docs/SERIALIZATION.md +1 -1
- package/Docs/SINGLETONS.md +2 -2
- package/Docs/SPATIAL_TREES_2D_GUIDE.md +3 -3
- package/Docs/SPATIAL_TREES_3D_GUIDE.md +3 -3
- package/Docs/SPATIAL_TREE_SEMANTICS.md +7 -7
- package/Editor/CustomDrawers/WShowIfPropertyDrawer.cs +131 -41
- package/Editor/Utils/ScriptableObjectSingletonCreator.cs +175 -18
- package/README.md +17 -3
- package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +4 -2
- package/Runtime/Tags/Attribute.cs +144 -24
- package/Runtime/Tags/AttributeEffect.cs +278 -11
- package/Runtime/Tags/AttributeModification.cs +59 -29
- package/Runtime/Tags/AttributeUtilities.cs +465 -0
- package/Runtime/Tags/AttributesComponent.cs +20 -0
- package/Runtime/Tags/EffectBehavior.cs +171 -0
- package/Runtime/Tags/EffectBehavior.cs.meta +4 -0
- package/Runtime/Tags/EffectHandle.cs +5 -0
- package/Runtime/Tags/EffectHandler.cs +564 -39
- package/Runtime/Tags/EffectStackKey.cs +79 -0
- package/Runtime/Tags/EffectStackKey.cs.meta +4 -0
- package/Runtime/Tags/PeriodicEffectDefinition.cs +102 -0
- package/Runtime/Tags/PeriodicEffectDefinition.cs.meta +4 -0
- package/Runtime/Tags/PeriodicEffectRuntimeState.cs +40 -0
- package/Runtime/Tags/PeriodicEffectRuntimeState.cs.meta +4 -0
- package/Runtime/Tags/TagHandler.cs +375 -21
- package/Samples~/DI - Zenject/README.md +0 -2
- package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs +285 -0
- package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs.meta +11 -0
- package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +2 -2
- package/Tests/Editor/Utils/ScriptableObjectSingletonTests.cs +41 -0
- package/Tests/Runtime/Serialization/JsonSerializationTest.cs +4 -3
- package/Tests/Runtime/Tags/AttributeEffectTests.cs +135 -0
- package/Tests/Runtime/Tags/AttributeEffectTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/AttributeModificationTests.cs +137 -0
- package/Tests/Runtime/Tags/AttributeTests.cs +192 -0
- package/Tests/Runtime/Tags/AttributeTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/AttributeUtilitiesTests.cs +245 -0
- package/Tests/Runtime/Tags/CosmeticAndCollisionTests.cs +1 -1
- package/Tests/Runtime/Tags/EffectBehaviorTests.cs +184 -0
- package/Tests/Runtime/Tags/EffectBehaviorTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/EffectHandlerTests.cs +809 -0
- package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs +89 -0
- package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs.meta +4 -0
- package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs +92 -0
- package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/TagHandlerTests.cs +130 -6
- package/package.json +1 -1
- package/scripts/lint-doc-links.ps1 +156 -11
- package/Tests/Runtime/Tags/AttributeDataTests.cs +0 -312
- package/node_modules.meta +0 -8
- /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
|
@@ -61,8 +61,9 @@ public class PlayerStats : AttributesComponent
|
|
|
61
61
|
{
|
|
62
62
|
// Define attributes that effects can modify
|
|
63
63
|
public Attribute Speed = 5f;
|
|
64
|
-
public Attribute
|
|
65
|
-
public Attribute
|
|
64
|
+
public Attribute MaxHealth = 100f;
|
|
65
|
+
public Attribute AttackDamage = 10f;
|
|
66
|
+
public Attribute Defense = 5f;
|
|
66
67
|
|
|
67
68
|
void Start()
|
|
68
69
|
{
|
|
@@ -80,6 +81,15 @@ public class PlayerStats : AttributesComponent
|
|
|
80
81
|
- Calculates final value automatically (Add → Multiply → Override)
|
|
81
82
|
- Raises events when value changes
|
|
82
83
|
|
|
84
|
+
**⚠️ Important: Use Attributes for "max" or "rate" values, NOT "current" depleting values!**
|
|
85
|
+
|
|
86
|
+
- ✅ **MaxHealth** - modified by buffs (good)
|
|
87
|
+
- ❌ **CurrentHealth** - modified by damage/healing from many systems (bad - causes state conflicts)
|
|
88
|
+
- ✅ **AttackDamage** - modified by strength buffs (good)
|
|
89
|
+
- ✅ **Speed** - modified by haste/slow effects (good)
|
|
90
|
+
|
|
91
|
+
If a value is frequently modified by systems outside the effects system (like health being reduced by damage), use a regular field instead. See the main documentation for details.
|
|
92
|
+
|
|
83
93
|
---
|
|
84
94
|
|
|
85
95
|
## Step 2: Add Stats to Your Player (30 seconds)
|
|
@@ -88,8 +98,9 @@ public class PlayerStats : AttributesComponent
|
|
|
88
98
|
2. Add Component → `PlayerStats`
|
|
89
99
|
3. Set values in Inspector:
|
|
90
100
|
- Speed: `5`
|
|
91
|
-
-
|
|
92
|
-
-
|
|
101
|
+
- MaxHealth: `100`
|
|
102
|
+
- AttackDamage: `10`
|
|
103
|
+
- Defense: `5`
|
|
93
104
|
|
|
94
105
|
That's it! Your player now has modifiable attributes.
|
|
95
106
|
|
|
@@ -282,8 +293,8 @@ One effect can modify multiple attributes:
|
|
|
282
293
|
**Create "Berserker Rage" effect:**
|
|
283
294
|
|
|
284
295
|
- Modification 1: Speed × 1.3
|
|
285
|
-
- Modification 2:
|
|
286
|
-
- Modification 3:
|
|
296
|
+
- Modification 2: AttackDamage × 2.0
|
|
297
|
+
- Modification 3: Defense × 0.5 (trade-off - more damage but less defense!)
|
|
287
298
|
- Duration: 10 seconds
|
|
288
299
|
- Tags: `"Berserker"`, `"Buff"`
|
|
289
300
|
|
|
@@ -312,23 +323,63 @@ if (handle.HasValue)
|
|
|
312
323
|
|
|
313
324
|
```csharp
|
|
314
325
|
// Create "Poison" effect:
|
|
315
|
-
// -
|
|
326
|
+
// - periodicEffects: interval = 1s, maxTicks = 10, modifications = []
|
|
327
|
+
// - behaviors: PoisonDamageBehavior (below)
|
|
316
328
|
// - Duration: 10 seconds
|
|
317
|
-
// - Tags: "
|
|
329
|
+
// - Tags: "Poisoned", "DoT", "Debuff"
|
|
330
|
+
|
|
331
|
+
void ApplyPoison(GameObject target)
|
|
332
|
+
{
|
|
333
|
+
target.ApplyEffect(poisonEffect);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
[CreateAssetMenu(menuName = "Combat/Effects/Poison Damage")]
|
|
337
|
+
public sealed class PoisonDamageBehavior : EffectBehavior
|
|
338
|
+
{
|
|
339
|
+
[SerializeField]
|
|
340
|
+
private float damagePerTick = 2f;
|
|
341
|
+
|
|
342
|
+
public override void OnPeriodicTick(
|
|
343
|
+
EffectBehaviorContext context,
|
|
344
|
+
PeriodicEffectTickContext tickContext
|
|
345
|
+
)
|
|
346
|
+
{
|
|
347
|
+
if (!context.Target.TryGetComponent(out PlayerHealth health))
|
|
348
|
+
{
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
318
351
|
|
|
319
|
-
|
|
320
|
-
|
|
352
|
+
health.ApplyDamage(damagePerTick);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
321
355
|
|
|
322
|
-
|
|
323
|
-
void Update()
|
|
356
|
+
public sealed class PlayerHealth : MonoBehaviour
|
|
324
357
|
{
|
|
325
|
-
|
|
358
|
+
[SerializeField]
|
|
359
|
+
private float currentHealth = 100f;
|
|
360
|
+
|
|
361
|
+
public float CurrentHealth => currentHealth;
|
|
362
|
+
|
|
363
|
+
public void ApplyDamage(float amount)
|
|
326
364
|
{
|
|
327
|
-
|
|
365
|
+
currentHealth -= amount;
|
|
366
|
+
|
|
367
|
+
if (currentHealth <= 0f)
|
|
368
|
+
{
|
|
369
|
+
currentHealth = 0f;
|
|
370
|
+
Die();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private void Die()
|
|
375
|
+
{
|
|
376
|
+
// Handle player death
|
|
328
377
|
}
|
|
329
378
|
}
|
|
330
379
|
```
|
|
331
380
|
|
|
381
|
+
This keeps `CurrentHealth` as a regular gameplay field while the effect system triggers damage through behaviours.
|
|
382
|
+
|
|
332
383
|
### Cooldown Reduction
|
|
333
384
|
|
|
334
385
|
```csharp
|
|
@@ -376,6 +427,13 @@ void TryApplyBuff(AttributeEffect effect)
|
|
|
376
427
|
|
|
377
428
|
## Troubleshooting
|
|
378
429
|
|
|
430
|
+
### "Should I use CurrentHealth as an Attribute?"
|
|
431
|
+
|
|
432
|
+
- **No!** Use `MaxHealth` as an Attribute (modified by buffs), but keep `CurrentHealth` as a regular field (modified by damage/healing)
|
|
433
|
+
- **Why:** CurrentHealth is modified by many systems (combat, regeneration, etc.). Using it as an Attribute causes state conflicts when effects and other systems both try to modify it
|
|
434
|
+
- **Pattern:** Attribute for max/cap, regular field for current/depleting value
|
|
435
|
+
- **See:** "Understanding Attributes: What to Model and What to Avoid" in main documentation
|
|
436
|
+
|
|
379
437
|
### "Attribute 'Speed' not found"
|
|
380
438
|
|
|
381
439
|
- **Cause:** Attribute name in effect doesn't match field name in AttributesComponent
|
|
@@ -409,11 +467,12 @@ You now have a complete buff/debuff system! Here are some ideas to expand:
|
|
|
409
467
|
|
|
410
468
|
### Create More Effects
|
|
411
469
|
|
|
412
|
-
- **Shield:**
|
|
470
|
+
- **Shield:** MaxHealth × 1.5, visual shield sprite
|
|
413
471
|
- **Slow:** Speed × 0.5, "Slowed" tag
|
|
414
|
-
- **Critical:**
|
|
415
|
-
- **Invisibility:** Just tags ("Invisible"), no stat changes
|
|
416
|
-
- **
|
|
472
|
+
- **Critical Strike:** AttackDamage × 2.0, "CriticalHit" tag, brief flash effect
|
|
473
|
+
- **Invisibility:** Just tags ("Invisible"), no stat changes, transparency effect
|
|
474
|
+
- **Armor Buff:** Defense + 10, metallic sheen cosmetic
|
|
475
|
+
- **Strength Potion:** AttackDamage × 1.5, red particle aura
|
|
417
476
|
|
|
418
477
|
### Build Systems Around Tags
|
|
419
478
|
|
package/Docs/HULLS.md
CHANGED
|
@@ -18,7 +18,7 @@ This guide explains convex and concave hulls, when to use each, and how they dif
|
|
|
18
18
|
|
|
19
19
|
Illustration:
|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
23
|
## Concave Hull
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@ Illustration:
|
|
|
31
31
|
|
|
32
32
|
Illustration:
|
|
33
33
|
|
|
34
|
-

|
|
35
35
|
|
|
36
36
|
## Choosing Between Them
|
|
37
37
|
|
|
@@ -71,7 +71,7 @@ Threading
|
|
|
71
71
|
|
|
72
72
|
Visual
|
|
73
73
|
|
|
74
|
-

|
|
75
75
|
|
|
76
76
|
This document contains performance benchmarks for the various random number generators included in Unity Helpers.
|
|
77
77
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Visual
|
|
9
9
|
|
|
10
|
-

|
|
11
11
|
|
|
12
12
|
ReflectionHelpers is a set of utilities for high‑performance reflection in Unity projects. It generates and caches delegates to access fields and properties, call methods and constructors, and quickly create common collections — with safe fallbacks when dynamic IL isn’t available.
|
|
13
13
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Visual
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
Auto-wire components in your hierarchy without `GetComponent` boilerplate. These attributes make common relationships explicit, robust, and easy to maintain.
|
|
8
8
|
|
|
@@ -10,7 +10,14 @@ Auto-wire components in your hierarchy without `GetComponent` boilerplate. These
|
|
|
10
10
|
- `ParentComponent` — up the transform hierarchy
|
|
11
11
|
- `ChildComponent` — down the transform hierarchy (breadth-first)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**Collection Type Support:** Each attribute works with:
|
|
14
|
+
|
|
15
|
+
- Single fields (e.g., `Transform`)
|
|
16
|
+
- Arrays (e.g., `Collider2D[]`)
|
|
17
|
+
- **Lists** (e.g., `List<Rigidbody2D>`)
|
|
18
|
+
- **HashSets** (e.g., `HashSet<Renderer>`)
|
|
19
|
+
|
|
20
|
+
All attributes support optional assignment, filters (tag/name), depth limits, max results, and interface/base-type resolution.
|
|
14
21
|
|
|
15
22
|
Having issues? Jump to Troubleshooting: see [Troubleshooting](#troubleshooting).
|
|
16
23
|
|
|
@@ -197,7 +204,8 @@ Examples:
|
|
|
197
204
|
[SiblingComponent] private Animator animator; // required by default
|
|
198
205
|
[SiblingComponent(Optional = true)] private Rigidbody2D rb; // optional
|
|
199
206
|
[SiblingComponent(TagFilter = "Visual", NameFilter = "Sprite")] private Component[] visuals;
|
|
200
|
-
[SiblingComponent(MaxCount = 2)] private List<Collider2D> firstTwo;
|
|
207
|
+
[SiblingComponent(MaxCount = 2)] private List<Collider2D> firstTwo; // List<T> supported
|
|
208
|
+
[SiblingComponent] private HashSet<Renderer> allRenderers; // HashSet<T> supported
|
|
201
209
|
```
|
|
202
210
|
|
|
203
211
|
### ParentComponent
|
|
@@ -232,7 +240,10 @@ Examples:
|
|
|
232
240
|
// First matching descendant with a tag
|
|
233
241
|
[ChildComponent(OnlyDescendants = true, TagFilter = "Weapon")] private Collider2D weaponCollider;
|
|
234
242
|
|
|
235
|
-
// Gather into a
|
|
243
|
+
// Gather into a List (preserves insertion order)
|
|
244
|
+
[ChildComponent(OnlyDescendants = true)] private List<MeshRenderer> childRenderers;
|
|
245
|
+
|
|
246
|
+
// Gather into a HashSet (unique results, no duplicates) and limit count
|
|
236
247
|
[ChildComponent(OnlyDescendants = true, MaxCount = 10)] private HashSet<Rigidbody2D> firstTenRigidbodies;
|
|
237
248
|
```
|
|
238
249
|
|
|
@@ -261,6 +272,39 @@ Examples:
|
|
|
261
272
|
- `AllowInterfaces` (default: true)
|
|
262
273
|
- If `true`, can assign by interface or base type; set `false` to restrict to concrete types
|
|
263
274
|
|
|
275
|
+
### Choosing the Right Collection Type
|
|
276
|
+
|
|
277
|
+
**Use Arrays (`T[]`)** when:
|
|
278
|
+
|
|
279
|
+
- Collection size is fixed or rarely changes
|
|
280
|
+
- Need the smallest memory footprint
|
|
281
|
+
- Interoperating with APIs that require arrays
|
|
282
|
+
|
|
283
|
+
**Use Lists (`List<T>`)** when:
|
|
284
|
+
|
|
285
|
+
- Need insertion order preserved
|
|
286
|
+
- Plan to add/remove elements after assignment
|
|
287
|
+
- Want indexed access with `[]` operator
|
|
288
|
+
- Need compatibility with most LINQ operations
|
|
289
|
+
|
|
290
|
+
**Use HashSets (`HashSet<T>`)** when:
|
|
291
|
+
|
|
292
|
+
- Need guaranteed uniqueness (no duplicates)
|
|
293
|
+
- Performing frequent membership tests (`Contains()`)
|
|
294
|
+
- Order doesn't matter
|
|
295
|
+
- Want O(1) lookup performance
|
|
296
|
+
|
|
297
|
+
```csharp
|
|
298
|
+
// Arrays: Fixed size, minimal overhead
|
|
299
|
+
[ChildComponent] private Collider2D[] colliders;
|
|
300
|
+
|
|
301
|
+
// Lists: Dynamic, ordered, index-based access
|
|
302
|
+
[ChildComponent] private List<Renderer> renderers;
|
|
303
|
+
|
|
304
|
+
// HashSets: Unique, fast lookups, unordered
|
|
305
|
+
[ChildComponent] private HashSet<AudioSource> audioSources;
|
|
306
|
+
```
|
|
307
|
+
|
|
264
308
|
## Recipes
|
|
265
309
|
|
|
266
310
|
- UI hierarchy references
|
|
@@ -564,7 +608,8 @@ Common pitfalls and how to avoid them
|
|
|
564
608
|
|
|
565
609
|
**DI Integration Samples:**
|
|
566
610
|
|
|
567
|
-
- [VContainer Integration](Samples~/DI%20-%20VContainer/README.md) - Complete VContainer setup guide
|
|
568
|
-
- [Zenject Integration](Samples~/DI%20-%20Zenject/README.md) - Complete Zenject setup guide
|
|
611
|
+
- [VContainer Integration](../Samples~/DI%20-%20VContainer/README.md) - Complete VContainer setup guide
|
|
612
|
+
- [Zenject Integration](../Samples~/DI%20-%20Zenject/README.md) - Complete Zenject setup guide
|
|
613
|
+
- [Reflex Integration](../Samples~/DI%20-%20Reflex/README.md) - Complete Reflex setup guide
|
|
569
614
|
|
|
570
615
|
**Need help?** [Open an issue](https://github.com/wallstop/unity-helpers/issues) | [Troubleshooting](#troubleshooting)
|
package/Docs/SERIALIZATION.md
CHANGED
package/Docs/SINGLETONS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Visual
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
This package includes two lightweight, production‑ready singleton helpers that make global access patterns safe, consistent, and testable:
|
|
8
8
|
|
|
@@ -403,7 +403,7 @@ Use alternatives when one or more of these apply:
|
|
|
403
403
|
|
|
404
404
|
Use this chart to pick an approach based on constraints:
|
|
405
405
|
|
|
406
|
-

|
|
407
407
|
|
|
408
408
|
<a id="troubleshooting"></a>
|
|
409
409
|
|
|
@@ -188,7 +188,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
|
|
|
188
188
|
- Pros: Simple structure; predictable performance; incremental updates straightforward.
|
|
189
189
|
- Cons: Data hotspots deepen local trees; nearest neighbors slower than KDTree.
|
|
190
190
|
|
|
191
|
-
Diagram: 
|
|
192
192
|
|
|
193
193
|
### KDTree2D
|
|
194
194
|
|
|
@@ -197,7 +197,7 @@ Diagram: 
|
|
|
197
197
|
- Pros: Strong NN performance; balanced variant gives consistent query time.
|
|
198
198
|
- Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.
|
|
199
199
|
|
|
200
|
-
Diagram: 
|
|
201
201
|
|
|
202
202
|
### RTree2D
|
|
203
203
|
|
|
@@ -206,7 +206,7 @@ Diagram: 
|
|
|
206
206
|
- Pros: Great for large bounds queries; matches bounds semantics.
|
|
207
207
|
- Cons: Overlapping MBRs can increase node visits; not optimal for point NN.
|
|
208
208
|
|
|
209
|
-
Diagram: 
|
|
210
210
|
|
|
211
211
|
## Choosing a Structure
|
|
212
212
|
|
|
@@ -178,7 +178,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
|
|
|
178
178
|
- Pros: Good spatial locality; intuitive partitioning; balanced performance.
|
|
179
179
|
- Cons: Nearest neighbors slower than KDTree on pure point data.
|
|
180
180
|
|
|
181
|
-

|
|
182
182
|
|
|
183
183
|
### KDTree3D
|
|
184
184
|
|
|
@@ -187,7 +187,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
|
|
|
187
187
|
- Pros: Strong NN performance; balanced variant gives consistent query time.
|
|
188
188
|
- Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.
|
|
189
189
|
|
|
190
|
-

|
|
191
191
|
|
|
192
192
|
### RTree3D
|
|
193
193
|
|
|
@@ -196,7 +196,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
|
|
|
196
196
|
- Pros: Great for large bounds queries; matches volumetric semantics.
|
|
197
197
|
- Cons: Overlapping boxes can increase node visits; not optimal for point NN.
|
|
198
198
|
|
|
199
|
-

|
|
200
200
|
|
|
201
201
|
## Choosing a Structure
|
|
202
202
|
|
|
@@ -16,19 +16,19 @@ This page explains how the 2D and 3D spatial structures compare in terms of corr
|
|
|
16
16
|
|
|
17
17
|
Illustrations:
|
|
18
18
|
|
|
19
|
-

|
|
20
20
|
|
|
21
|
-

|
|
22
22
|
|
|
23
|
-

|
|
24
24
|
|
|
25
25
|
3D Variants
|
|
26
26
|
|
|
27
|
-

|
|
28
28
|
|
|
29
|
-

|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
Diagram notes
|
|
34
34
|
|
|
@@ -91,7 +91,7 @@ Key reasons and scenarios:
|
|
|
91
91
|
|
|
92
92
|
## Boundary Semantics
|
|
93
93
|
|
|
94
|
-

|
|
95
95
|
|
|
96
96
|
Tips
|
|
97
97
|
|
|
@@ -42,63 +42,153 @@ namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers
|
|
|
42
42
|
SerializedProperty conditionProperty = property.serializedObject.FindProperty(
|
|
43
43
|
showIf.conditionField
|
|
44
44
|
);
|
|
45
|
-
if (conditionProperty
|
|
45
|
+
if (conditionProperty != null)
|
|
46
46
|
{
|
|
47
|
-
if (conditionProperty
|
|
47
|
+
if (TryEvaluateCondition(conditionProperty, showIf, out bool serializedResult))
|
|
48
48
|
{
|
|
49
|
-
return
|
|
49
|
+
return serializedResult;
|
|
50
50
|
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
54
|
+
object enclosingObject = property.GetEnclosingObject(out _);
|
|
55
|
+
if (enclosingObject == null)
|
|
56
|
+
{
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
60
|
+
Type type = enclosingObject.GetType();
|
|
61
|
+
Dictionary<string, Func<object, object>> cachedFields = CachedFields.GetOrAdd(type);
|
|
62
|
+
if (!cachedFields.TryGetValue(showIf.conditionField, out Func<object, object> accessor))
|
|
63
|
+
{
|
|
64
|
+
FieldInfo field = type.GetField(
|
|
65
|
+
showIf.conditionField,
|
|
66
|
+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
|
|
67
|
+
);
|
|
68
|
+
if (field == null)
|
|
67
69
|
{
|
|
68
|
-
|
|
69
|
-
showIf.conditionField
|
|
70
|
-
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
|
|
70
|
+
Debug.LogError(
|
|
71
|
+
$"Failed to find conditional field {showIf.conditionField} on {type.Name}!"
|
|
71
72
|
);
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
Debug.LogError(
|
|
75
|
-
$"Failed to find conditional field {showIf.conditionField} on {type.Name}!"
|
|
76
|
-
);
|
|
77
|
-
accessor = _ => null;
|
|
78
|
-
}
|
|
79
|
-
else
|
|
80
|
-
{
|
|
81
|
-
accessor = ReflectionHelpers.GetFieldGetter(field);
|
|
82
|
-
}
|
|
83
|
-
cachedFields[showIf.conditionField] = accessor;
|
|
73
|
+
accessor = _ => null;
|
|
84
74
|
}
|
|
85
|
-
|
|
86
|
-
if (fieldValue is bool maybeCondition)
|
|
75
|
+
else
|
|
87
76
|
{
|
|
88
|
-
|
|
77
|
+
accessor = ReflectionHelpers.GetFieldGetter(field);
|
|
89
78
|
}
|
|
79
|
+
cachedFields[showIf.conditionField] = accessor;
|
|
80
|
+
}
|
|
81
|
+
object fieldValue = accessor(enclosingObject);
|
|
82
|
+
return !TryEvaluateCondition(fieldValue, showIf, out bool reflectedResult)
|
|
83
|
+
? true
|
|
84
|
+
: reflectedResult;
|
|
85
|
+
}
|
|
90
86
|
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
private static bool TryEvaluateCondition(
|
|
88
|
+
SerializedProperty conditionProperty,
|
|
89
|
+
WShowIfAttribute showIf,
|
|
90
|
+
out bool shouldShow
|
|
91
|
+
)
|
|
92
|
+
{
|
|
93
|
+
if (conditionProperty == null)
|
|
94
|
+
{
|
|
95
|
+
shouldShow = true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (conditionProperty.propertyType == SerializedPropertyType.Boolean)
|
|
100
|
+
{
|
|
101
|
+
bool condition = conditionProperty.boolValue;
|
|
102
|
+
shouldShow = showIf.inverse ? !condition : condition;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
object conditionValue = conditionProperty.GetTargetObjectWithField(out _);
|
|
107
|
+
return TryEvaluateCondition(conditionValue, showIf, out shouldShow);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static bool TryEvaluateCondition(
|
|
111
|
+
object conditionValue,
|
|
112
|
+
WShowIfAttribute showIf,
|
|
113
|
+
out bool shouldShow
|
|
114
|
+
)
|
|
115
|
+
{
|
|
116
|
+
if (conditionValue is bool boolean)
|
|
117
|
+
{
|
|
118
|
+
shouldShow = showIf.inverse ? !boolean : boolean;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
object[] expectedValues = showIf.expectedValues;
|
|
123
|
+
if (expectedValues == null || expectedValues.Length == 0)
|
|
124
|
+
{
|
|
125
|
+
shouldShow = true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bool match = false;
|
|
130
|
+
for (int i = 0; i < expectedValues.Length; ++i)
|
|
131
|
+
{
|
|
132
|
+
if (ValuesEqual(conditionValue, expectedValues[i]))
|
|
93
133
|
{
|
|
94
|
-
|
|
134
|
+
match = true;
|
|
135
|
+
break;
|
|
95
136
|
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
shouldShow = showIf.inverse ? !match : match;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private static bool ValuesEqual(object actual, object expected)
|
|
144
|
+
{
|
|
145
|
+
if (ReferenceEquals(actual, expected))
|
|
146
|
+
{
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (actual == null || expected == null)
|
|
151
|
+
{
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (actual.Equals(expected))
|
|
156
|
+
{
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Type actualType = actual.GetType();
|
|
161
|
+
Type expectedType = expected.GetType();
|
|
162
|
+
|
|
163
|
+
try
|
|
164
|
+
{
|
|
165
|
+
if (actualType.IsEnum || expectedType.IsEnum)
|
|
166
|
+
{
|
|
167
|
+
long actualValue = Convert.ToInt64(actual);
|
|
168
|
+
long expectedValue = Convert.ToInt64(expected);
|
|
169
|
+
return actualValue == expectedValue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch
|
|
173
|
+
{
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
96
176
|
|
|
97
|
-
|
|
177
|
+
if (actual is IConvertible && expected is IConvertible)
|
|
178
|
+
{
|
|
179
|
+
try
|
|
180
|
+
{
|
|
181
|
+
double actualValue = Convert.ToDouble(actual);
|
|
182
|
+
double expectedValue = Convert.ToDouble(expected);
|
|
183
|
+
return Math.Abs(actualValue - expectedValue) < double.Epsilon;
|
|
184
|
+
}
|
|
185
|
+
catch
|
|
186
|
+
{
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
98
189
|
}
|
|
99
190
|
|
|
100
|
-
|
|
101
|
-
return showIf.inverse ? !condition : condition;
|
|
191
|
+
return false;
|
|
102
192
|
}
|
|
103
193
|
}
|
|
104
194
|
#endif
|