Skip to content

Commit 43b9e00

Browse files
author
Miguel Cartier
committed
feat(editor): add custom GameUiConfigsEditor for UiConfigs
- Add GameUiConfigsEditor extending UiConfigsEditor<UiSetId> - Enable custom inspector visualization for UiConfigs asset - Provides type-safe UI configuration editor for game-specific UiSetId enum - Update the README and CHANGELOF documentation - Bumped the project version to 1.0.0
1 parent 7fd89a1 commit 43b9e00

File tree

7 files changed

+130
-80
lines changed

7 files changed

+130
-80
lines changed

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@ All notable changes to this package will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html)
66

7+
## [1.0.0] - 2025-11-01
8+
9+
**New**:
10+
- Added `IUiAnalytics` interface and `UiAnalytics` implementation for performance tracking
11+
- Added three editor windows: `UiAnalyticsWindow`, `UiServiceHierarchyWindow`, `UiLayerVisualizerWindow`
12+
- Added `UiPresenterSceneGizmos` for visual debugging in Scene view
13+
- Added `UiPresenterEditor` custom inspector with quick open/close buttons
14+
- Added comprehensive sample scenes with README for all major features
15+
- Added Performance Optimization section to README with best practices
16+
- Added Troubleshooting section to README with common issues and solutions
17+
18+
**Changed**:
19+
- Replaced `Task.Delay` with `UniTask.Delay` throughout for better performance and WebGL compatibility
20+
- Updated `CloseAllUi` to avoid modifying collection during iteration
21+
- Enhanced `UiService.Dispose()` with proper cleanup of all presenters, layers, and asset loader
22+
- `LoadUiAsync`, `OpenUiAsync` methods now accept optional `CancellationToken` parameter
23+
- Updated the README with a complete information of the project
24+
25+
**Fixed**:
26+
- **CRITICAL**: Fixed `PresenterDelayerBase.CloseWithDelay` using wrong delay property (was using `OpenDelayInSeconds` instead of `CloseDelayInSeconds`)
27+
- **CRITICAL**: Fixed `AnimationDelayer` incorrect time unit conversion (removed `* 1000` multiplications)
28+
- **CRITICAL**: Fixed `GetOrLoadUiAsync` returning null when loading new UI (now properly assigns return value)
29+
- **CRITICAL**: Fixed `DelayUiPresenterData<T>` inheriting from wrong base class (now inherits from `UiPresenter<T>`)
30+
- Fixed missing null checks in `AnimationDelayer` for animation clips
31+
- Fixed exception handling in `UnloadUi` with proper `TryGetValue` checks
32+
- Fixed exception handling in `RemoveUiSet` with proper `TryGetValue` checks
33+
- Fixed redundant operations in `CloseAllUi` logic
34+
735
## [0.13.1] - 2025-09-28
836

937
**New**:

Editor/UiConfigsEditor.cs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ public abstract class UiConfigsEditor<TSet> : Editor
6666
"Presenters are loaded in the order listed (top to bottom).";
6767

6868
private Dictionary<string, string> _assetPathLookup;
69-
private List<string> _uiConfigsType;
70-
private string[] _uiConfigsAddress;
69+
private List<string> _uiConfigsAddress;
7170
private UiConfigs _scriptableObject;
7271
private SerializedProperty _configsProperty;
7372
private SerializedProperty _setsProperty;
@@ -245,7 +244,7 @@ private VisualElement CreateSetPresenterElement()
245244

246245
// Dropdown for selecting UI presenter
247246
var dropdown = new DropdownField();
248-
dropdown.choices = new List<string>(_uiConfigsAddress ?? new string[0]);
247+
dropdown.choices = new List<string>(_uiConfigsAddress ?? new List<string>());
249248
dropdown.style.flexGrow = 1;
250249
dropdown.style.paddingTop = 3;
251250
dropdown.style.paddingBottom = 3;
@@ -286,7 +285,7 @@ private VisualElement CreateSetElement(string setName, int setIndex)
286285

287286
// Get the property for this set's UI configs
288287
var setProperty = _setsProperty.GetArrayElementAtIndex(setIndex);
289-
var uiConfigsTypeProperty = setProperty.FindPropertyRelative(nameof(UiConfigs.UiSetConfigSerializable.UiConfigsType));
288+
var uiConfigsAddressProperty = setProperty.FindPropertyRelative(nameof(UiSetConfigSerializable.UiConfigsAddress));
290289

291290
// ListView for presenters in this set
292291
var presenterListView = new ListView
@@ -299,13 +298,13 @@ private VisualElement CreateSetElement(string setName, int setIndex)
299298
fixedItemHeight = 28
300299
};
301300

302-
presenterListView.BindProperty(uiConfigsTypeProperty);
301+
presenterListView.BindProperty(uiConfigsAddressProperty);
303302

304303
presenterListView.makeItem = CreateSetPresenterElement;
305-
presenterListView.bindItem = (element, index) => BindSetPresenterElement(element, index, uiConfigsTypeProperty, presenterListView);
304+
presenterListView.bindItem = (element, index) => BindSetPresenterElement(element, index, uiConfigsAddressProperty, presenterListView);
306305

307306
// Register callbacks to save changes when items are added, removed, or reordered
308-
presenterListView.itemsAdded += indices => OnPresenterItemsAdded(indices, uiConfigsTypeProperty);
307+
presenterListView.itemsAdded += indices => OnPresenterItemsAdded(indices, uiConfigsAddressProperty);
309308
presenterListView.itemsRemoved += _ => SaveSetChanges();
310309
presenterListView.itemIndexChanged += (_, _) => SaveSetChanges();
311310

@@ -314,53 +313,68 @@ private VisualElement CreateSetElement(string setName, int setIndex)
314313
return setContainer;
315314
}
316315

317-
private void BindSetPresenterElement(VisualElement element, int index, SerializedProperty uiConfigsTypeProperty, ListView listView)
316+
private void BindSetPresenterElement(VisualElement element, int index, SerializedProperty uiConfigsAddressProperty, ListView listView)
318317
{
319-
if (index >= uiConfigsTypeProperty.arraySize)
318+
if (index >= uiConfigsAddressProperty.arraySize)
320319
return;
321320

322321
var dropdown = element.Q<DropdownField>();
323322
if (dropdown == null)
324323
return;
325324

326-
var itemProperty = uiConfigsTypeProperty.GetArrayElementAtIndex(index);
325+
var itemProperty = uiConfigsAddressProperty.GetArrayElementAtIndex(index);
327326

328-
// Find the index in our type list
329-
var currentType = itemProperty.stringValue;
330-
var selectedIndex = string.IsNullOrEmpty(currentType) ? 0 :
331-
_uiConfigsType.FindIndex(type => type == currentType);
327+
// Find the index in our address list
328+
var currentAddress = itemProperty.stringValue;
329+
var selectedIndex = string.IsNullOrEmpty(currentAddress) ? 0 :
330+
_uiConfigsAddress.FindIndex(address => address == currentAddress);
332331

333332
if (selectedIndex < 0)
334333
selectedIndex = 0;
335334

336-
if (_uiConfigsAddress != null && _uiConfigsAddress.Length > 0)
335+
if (_uiConfigsAddress != null && _uiConfigsAddress.Count > 0)
337336
{
338337
// Unbind to prevent stale property references
339338
dropdown.Unbind();
340339

341340
// Set the current value
342341
dropdown.index = selectedIndex;
343342

344-
// Bind to the property - this handles change tracking automatically
345-
dropdown.BindProperty(itemProperty);
343+
// Register callback to store address when changed
344+
dropdown.RegisterValueChangedCallback(evt =>
345+
{
346+
var newIndex = dropdown.index;
347+
if (newIndex >= 0 && newIndex < _uiConfigsAddress.Count)
348+
{
349+
itemProperty.stringValue = _uiConfigsAddress[newIndex];
350+
SaveSetChanges();
351+
}
352+
});
353+
354+
// Set initial value if property is empty
355+
if (string.IsNullOrEmpty(itemProperty.stringValue) && selectedIndex < _uiConfigsAddress.Count)
356+
{
357+
itemProperty.stringValue = _uiConfigsAddress[selectedIndex];
358+
serializedObject.ApplyModifiedProperties();
359+
}
346360
}
347361
}
348362

349-
private void OnPresenterItemsAdded(IEnumerable<int> indices, SerializedProperty uiConfigsTypeProperty)
363+
private void OnPresenterItemsAdded(IEnumerable<int> indices, SerializedProperty uiConfigsAddressProperty)
350364
{
351-
if (_uiConfigsAddress == null || _uiConfigsAddress.Length == 0)
365+
if (_uiConfigsAddress == null || _uiConfigsAddress.Count == 0)
352366
{
353367
return;
354368
}
355369

356-
var defaultType = _uiConfigsAddress[0];
370+
var defaultAddress = _uiConfigsAddress[0];
357371

358372
foreach (var index in indices)
359373
{
360-
if (index < uiConfigsTypeProperty.arraySize)
374+
if (index < uiConfigsAddressProperty.arraySize)
361375
{
362-
var itemProperty = uiConfigsTypeProperty.GetArrayElementAtIndex(index);
363-
itemProperty.stringValue = defaultType;
376+
var itemProperty = uiConfigsAddressProperty.GetArrayElementAtIndex(index);
377+
itemProperty.stringValue = defaultAddress;
364378
}
365379
}
366380

@@ -383,9 +397,7 @@ private void SyncConfigsWithAddressables()
383397
var assetList = GetAssetList();
384398
var configs = new List<UiConfig>();
385399
var uiConfigsAddress = new List<string>();
386-
var uiConfigsType = new List<string>();
387400
var assetPathLookup = new Dictionary<string, string>();
388-
389401
var existingConfigs = _scriptableObject.Configs;
390402

391403
foreach (var asset in assetList)
@@ -416,13 +428,11 @@ private void SyncConfigsWithAddressables()
416428

417429
configs.Add(config);
418430
uiConfigsAddress.Add(asset.address);
419-
uiConfigsType.Add(presenterType.AssemblyQualifiedName);
420431
assetPathLookup[asset.address] = asset.AssetPath;
421432
}
422433

423434
_scriptableObject.Configs = configs;
424-
_uiConfigsAddress = uiConfigsAddress.ToArray();
425-
_uiConfigsType = uiConfigsType;
435+
_uiConfigsAddress = uiConfigsAddress;
426436
_assetPathLookup = assetPathLookup;
427437

428438
EditorUtility.SetDirty(_scriptableObject);
@@ -491,4 +501,5 @@ private static List<AddressableAssetEntry> GetAssetList()
491501
return assetList;
492502
}
493503
}
494-
}
504+
}
505+

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Unity Version](https://img.shields.io/badge/Unity-6000.0%2B-blue.svg)](https://unity3d.com/get-unity/download)
44
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5-
[![Version](https://img.shields.io/badge/version-0.13.0-green.svg)](CHANGELOG.md)
5+
[![Version](https://img.shields.io/badge/version-1.0.0-green.svg)](CHANGELOG.md)
66

77
A powerful and flexible UI management system for Unity that provides a robust abstraction layer for handling game UI with support for layers, async loading, and UI sets. This service streamlines UI development by managing the complete lifecycle of UI presenters, from loading and initialization to display and cleanup.
88

Runtime/IUiService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,15 @@ public interface IUiServiceInit : IUiService, IDisposable
257257
/// <remarks>
258258
/// To help configure the game's UI, you need to create a UiConfigs Scriptable object by:
259259
/// - Right Click on the Project View > Create > ScriptableObjects > Configs > UiConfigs
260+
/// - Duplicate UI configs or UI sets will log warnings but will not throw exceptions
261+
/// - Layer numbers below 0 or above 1000 will log warnings
262+
/// - Empty addressable addresses or null UI types will throw ArgumentException
260263
/// </remarks>
264+
/// <exception cref="ArgumentNullException">
265+
/// Thrown if <paramref name="configs"/> is null.
266+
/// </exception>
261267
/// <exception cref="ArgumentException">
262-
/// Thrown if any of the <see cref="UiConfig"/> in the given <paramref name="configs"/> is duplicated.
268+
/// Thrown if any UI config has an empty addressable address or null UI type.
263269
/// </exception>
264270
void Init(UiConfigs configs);
265271
}

Runtime/UiConfigs.cs

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public List<UiConfig> Configs
4242
/// <summary>
4343
/// Gets the list of UI set configurations
4444
/// </summary>
45-
public List<UiSetConfig> Sets => _sets.ConvertAll(element => (UiSetConfig)element);
45+
public List<UiSetConfig> Sets => _sets.ConvertAll(element => UiSetConfigSerializable.ToUiSetConfig(element, _configs));
4646

4747
/// <summary>
4848
/// Sets the new size of this scriptable object <seealso cref="UiSetConfig"/> list.
@@ -60,23 +60,23 @@ public void SetSetsSize(int size)
6060
{
6161
if (i < _sets.Count)
6262
{
63-
var cleanedConfigList = new List<string>(_sets[i].UiConfigsType.Count);
63+
var cleanedConfigList = new List<string>(_sets[i].UiConfigsAddress.Count);
6464

65-
foreach (var uiConfig in _sets[i].UiConfigsType)
65+
foreach (var address in _sets[i].UiConfigsAddress)
6666
{
67-
if (_configs.FindIndex(config => config.UiType == uiConfig) > -1)
67+
if (_configs.FindIndex(config => config.AddressableAddress == address) > -1)
6868
{
69-
cleanedConfigList.Add(uiConfig);
69+
cleanedConfigList.Add(address);
7070
}
7171
}
7272

7373
var set = _sets[i];
74-
set.UiConfigsType = cleanedConfigList;
74+
set.UiConfigsAddress = cleanedConfigList;
7575
_sets[i] = set;
7676
continue;
7777
}
7878

79-
_sets.Add(new UiSetConfigSerializable { SetId = i, UiConfigsType = new List<string>() });
79+
_sets.Add(new UiSetConfigSerializable { SetId = i, UiConfigsAddress = new List<string>() });
8080
}
8181
}
8282

@@ -112,46 +112,5 @@ public static implicit operator UiConfigSerializable(UiConfig config)
112112
}
113113
}
114114

115-
/// <summary>
116-
/// Necessary to serialize the data in scriptable object
117-
/// </summary>
118-
[Serializable]
119-
public struct UiSetConfigSerializable
120-
{
121-
public int SetId;
122-
public List<string> UiConfigsType;
123-
124-
public static implicit operator UiSetConfig(UiSetConfigSerializable serializable)
125-
{
126-
var configs = new List<Type>();
127-
128-
foreach (var uiConfig in serializable.UiConfigsType)
129-
{
130-
configs.Add(Type.GetType(uiConfig));
131-
}
132-
133-
return new UiSetConfig
134-
{
135-
SetId = serializable.SetId,
136-
UiConfigsType = configs.AsReadOnly()
137-
};
138-
}
139-
140-
public static implicit operator UiSetConfigSerializable(UiSetConfig config)
141-
{
142-
var configTypes = new List<string>();
143-
144-
foreach (var type in config.UiConfigsType)
145-
{
146-
configTypes.Add(type.AssemblyQualifiedName);
147-
}
148-
149-
return new UiSetConfigSerializable
150-
{
151-
SetId = config.SetId,
152-
UiConfigsType = configTypes
153-
};
154-
}
155-
}
156115
}
157116
}

Runtime/UiSetConfig.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,50 @@ public struct UiSetConfig
1515
public int SetId;
1616
public IReadOnlyList<Type> UiConfigsType;
1717
}
18+
19+
/// <summary>
20+
/// Necessary to serialize the data in scriptable object
21+
/// </summary>
22+
[Serializable]
23+
public struct UiSetConfigSerializable
24+
{
25+
public int SetId;
26+
public List<string> UiConfigsAddress;
27+
28+
public static UiSetConfig ToUiSetConfig(UiSetConfigSerializable serializable, List<UiConfigs.UiConfigSerializable> configs)
29+
{
30+
var types = new List<Type>();
31+
foreach (var address in serializable.UiConfigsAddress)
32+
{
33+
var config = configs.Find(c => c.AddressableAddress == address);
34+
if (!string.IsNullOrEmpty(config.UiType))
35+
{
36+
types.Add(Type.GetType(config.UiType));
37+
}
38+
}
39+
return new UiSetConfig
40+
{
41+
SetId = serializable.SetId,
42+
UiConfigsType = types.AsReadOnly()
43+
};
44+
}
45+
46+
public static UiSetConfigSerializable FromUiSetConfig(UiSetConfig config, List<UiConfigs.UiConfigSerializable> configs)
47+
{
48+
var addresses = new List<string>();
49+
foreach (var type in config.UiConfigsType)
50+
{
51+
var uiConfig = configs.Find(c => c.UiType == type.AssemblyQualifiedName);
52+
if (!string.IsNullOrEmpty(uiConfig.AddressableAddress))
53+
{
54+
addresses.Add(uiConfig.AddressableAddress);
55+
}
56+
}
57+
return new UiSetConfigSerializable
58+
{
59+
SetId = config.SetId,
60+
UiConfigsAddress = addresses
61+
};
62+
}
63+
}
1864
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "com.gamelovers.uiservice",
33
"displayName": "UiService",
44
"author": "Miguel Tomas",
5-
"version": "0.13.1",
5+
"version": "1.0.0",
66
"unity": "6000.0",
77
"license": "MIT",
88
"description": "This package provides a service to help manage an Unity's, game UI.\nIt allows to open, close, load, unload and request any Ui Configured in the game.\nThe package provides a Ui Set that allows to group a set of Ui Presenters to help load, open and close multiple Uis at the same time.\n\nTo help configure the game's UI you need to create a UiConfigs Scriptable object by:\n- Right Click on the Project View > Create > ScriptableObjects > Configs > UiConfigs",

0 commit comments

Comments
 (0)