Skip to content

Commit a445c63

Browse files
author
Miguel Cartier
committed
feat(runtime): add multi-instance support for UI presenters
1 parent 744d33f commit a445c63

File tree

7 files changed

+482
-150
lines changed

7 files changed

+482
-150
lines changed

Editor/UiConfigsEditor.cs

Lines changed: 113 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ public abstract class UiConfigsEditor<TSet> : Editor
6161

6262
private const string UiSetExplanation =
6363
"UI Set Configurations\n\n" +
64-
"UI Sets group multiple presenters that should be displayed together. " +
64+
"UI Sets group multiple presenter instances that should be displayed together. " +
6565
"When a set is activated via UiService, all its presenters are loaded and shown simultaneously. " +
66-
"Presenters are loaded in the order listed (top to bottom).";
66+
"Presenters are loaded in the order listed (top to bottom).\n\n" +
67+
"You can add the same UI type multiple times with different instance addresses for multi-instance support.";
6768

6869
private Dictionary<string, string> _assetPathLookup;
6970
private List<string> _uiConfigsAddress;
71+
private Dictionary<string, Type> _uiTypesByAddress; // Maps addressable address to Type
7072
private UiConfigs _scriptableObject;
7173
private SerializedProperty _configsProperty;
7274
private SerializedProperty _setsProperty;
@@ -214,12 +216,14 @@ private VisualElement CreateConfigElement()
214216
container.style.paddingTop = 2;
215217
container.style.paddingBottom = 2;
216218

219+
// UiPresenter Addressable Address
217220
var label = new Label();
218221
label.style.flexGrow = 1;
219222
label.style.paddingLeft = 5;
220223
label.style.unityTextAlign = TextAnchor.MiddleLeft;
221224
container.Add(label);
222225

226+
// Layer field
223227
var layerField = new IntegerField();
224228
layerField.style.width = 80;
225229
layerField.style.marginRight = 5;
@@ -245,14 +249,23 @@ private VisualElement CreateSetPresenterElement()
245249
dragHandle.tooltip = "Drag to reorder";
246250
container.Add(dragHandle);
247251

248-
// Dropdown for selecting UI presenter
252+
// Dropdown for selecting UI presenter type
249253
var dropdown = new DropdownField();
250254
dropdown.choices = new List<string>(_uiConfigsAddress ?? new List<string>());
251255
dropdown.style.flexGrow = 1;
252256
dropdown.style.paddingTop = 3;
253257
dropdown.style.paddingBottom = 3;
258+
dropdown.name = "ui-type-dropdown";
254259
container.Add(dropdown);
255260

261+
// Instance address field (optional)
262+
var instanceField = new TextField();
263+
instanceField.style.width = 120;
264+
instanceField.style.marginLeft = 5;
265+
instanceField.tooltip = "Optional instance address (leave empty for default instance)";
266+
instanceField.name = "instance-address-field";
267+
container.Add(instanceField);
268+
256269
// Delete button
257270
var deleteButton = new Button { text = "×" };
258271
deleteButton.style.width = 25;
@@ -286,9 +299,9 @@ private VisualElement CreateSetElement(string setName, int setIndex)
286299
header.style.marginBottom = 5;
287300
setContainer.Add(header);
288301

289-
// Get the property for this set's UI configs
302+
// Get the property for this set's UI entries
290303
var setProperty = _setsProperty.GetArrayElementAtIndex(setIndex);
291-
var uiConfigsAddressProperty = setProperty.FindPropertyRelative(nameof(UiSetConfigSerializable.UiConfigsAddress));
304+
var uiEntriesProperty = setProperty.FindPropertyRelative(nameof(UiSetConfigSerializable.UiEntries));
292305

293306
// ListView for presenters in this set
294307
var presenterListView = new ListView
@@ -301,13 +314,13 @@ private VisualElement CreateSetElement(string setName, int setIndex)
301314
fixedItemHeight = 28
302315
};
303316

304-
presenterListView.BindProperty(uiConfigsAddressProperty);
317+
presenterListView.BindProperty(uiEntriesProperty);
305318

306319
presenterListView.makeItem = CreateSetPresenterElement;
307-
presenterListView.bindItem = (element, index) => BindSetPresenterElement(element, index, uiConfigsAddressProperty, presenterListView);
320+
presenterListView.bindItem = (element, index) => BindSetPresenterElement(element, index, uiEntriesProperty, presenterListView);
308321

309322
// Register callbacks to save changes when items are added, removed, or reordered
310-
presenterListView.itemsAdded += indices => OnPresenterItemsAdded(indices, uiConfigsAddressProperty);
323+
presenterListView.itemsAdded += indices => OnPresenterItemsAdded(indices, uiEntriesProperty);
311324
presenterListView.itemsRemoved += _ => SaveSetChanges();
312325
presenterListView.itemIndexChanged += (_, _) => SaveSetChanges();
313326

@@ -316,21 +329,40 @@ private VisualElement CreateSetElement(string setName, int setIndex)
316329
return setContainer;
317330
}
318331

319-
private void BindSetPresenterElement(VisualElement element, int index, SerializedProperty uiConfigsAddressProperty, ListView listView)
332+
private void BindSetPresenterElement(VisualElement element, int index, SerializedProperty uiEntriesProperty, ListView listView)
320333
{
321-
if (index >= uiConfigsAddressProperty.arraySize)
334+
if (index >= uiEntriesProperty.arraySize)
322335
return;
323336

324-
var dropdown = element.Q<DropdownField>();
325-
if (dropdown == null)
337+
var dropdown = element.Q<DropdownField>("ui-type-dropdown");
338+
var instanceField = element.Q<TextField>("instance-address-field");
339+
if (dropdown == null || instanceField == null)
326340
return;
327341

328-
var itemProperty = uiConfigsAddressProperty.GetArrayElementAtIndex(index);
342+
var entryProperty = uiEntriesProperty.GetArrayElementAtIndex(index);
343+
var typeNameProperty = entryProperty.FindPropertyRelative(nameof(UiSetEntry.UiTypeName));
344+
var instanceAddressProperty = entryProperty.FindPropertyRelative(nameof(UiSetEntry.InstanceAddress));
345+
346+
// Find the matching address for this type
347+
var currentTypeName = typeNameProperty.stringValue;
348+
Type currentType = string.IsNullOrEmpty(currentTypeName) ? null : Type.GetType(currentTypeName);
349+
350+
// Find the address that matches this type
351+
string matchingAddress = null;
352+
if (currentType != null && _uiTypesByAddress != null)
353+
{
354+
foreach (var kvp in _uiTypesByAddress)
355+
{
356+
if (kvp.Value == currentType)
357+
{
358+
matchingAddress = kvp.Key;
359+
break;
360+
}
361+
}
362+
}
329363

330-
// Find the index in our address list
331-
var currentAddress = itemProperty.stringValue;
332-
var selectedIndex = string.IsNullOrEmpty(currentAddress) ? 0 :
333-
_uiConfigsAddress.FindIndex(address => address == currentAddress);
364+
var selectedIndex = string.IsNullOrEmpty(matchingAddress) ? 0 :
365+
_uiConfigsAddress.FindIndex(address => address == matchingAddress);
334366

335367
if (selectedIndex < 0)
336368
selectedIndex = 0;
@@ -339,45 +371,87 @@ private void BindSetPresenterElement(VisualElement element, int index, Serialize
339371
{
340372
// Unbind to prevent stale property references
341373
dropdown.Unbind();
374+
instanceField.Unbind();
342375

343-
// Set the current value
376+
// Set the current values
344377
dropdown.index = selectedIndex;
378+
instanceField.value = instanceAddressProperty.stringValue ?? string.Empty;
345379

346-
// Register callback to store address when changed
380+
// Register callback to store type when changed
347381
dropdown.RegisterValueChangedCallback(evt =>
348382
{
349383
var newIndex = dropdown.index;
350384
if (newIndex >= 0 && newIndex < _uiConfigsAddress.Count)
351385
{
352-
itemProperty.stringValue = _uiConfigsAddress[newIndex];
353-
SaveSetChanges();
386+
var selectedAddress = _uiConfigsAddress[newIndex];
387+
if (_uiTypesByAddress.TryGetValue(selectedAddress, out var selectedType))
388+
{
389+
typeNameProperty.stringValue = selectedType.AssemblyQualifiedName;
390+
SaveSetChanges();
391+
}
354392
}
355393
});
356394

395+
// Register callback for instance address field
396+
instanceField.RegisterValueChangedCallback(evt =>
397+
{
398+
instanceAddressProperty.stringValue = evt.newValue ?? string.Empty;
399+
SaveSetChanges();
400+
});
401+
357402
// Set initial value if property is empty
358-
if (string.IsNullOrEmpty(itemProperty.stringValue) && selectedIndex < _uiConfigsAddress.Count)
403+
if (string.IsNullOrEmpty(typeNameProperty.stringValue) && selectedIndex < _uiConfigsAddress.Count)
404+
{
405+
var address = _uiConfigsAddress[selectedIndex];
406+
if (_uiTypesByAddress.TryGetValue(address, out var type))
407+
{
408+
typeNameProperty.stringValue = type.AssemblyQualifiedName;
409+
serializedObject.ApplyModifiedProperties();
410+
}
411+
}
412+
}
413+
414+
// Setup delete button to remove this item from the set
415+
var deleteButton = element.Q<Button>("delete-button");
416+
if (deleteButton != null)
417+
{
418+
// Store the click handler in userData to unregister it later if needed
419+
if (deleteButton.userData is EventCallback<ClickEvent> previousCallback)
359420
{
360-
itemProperty.stringValue = _uiConfigsAddress[selectedIndex];
361-
serializedObject.ApplyModifiedProperties();
421+
deleteButton.UnregisterCallback(previousCallback);
362422
}
423+
424+
EventCallback<ClickEvent> clickHandler = _ =>
425+
{
426+
uiEntriesProperty.DeleteArrayElementAtIndex(index);
427+
SaveSetChanges();
428+
};
429+
430+
deleteButton.userData = clickHandler;
431+
deleteButton.RegisterCallback(clickHandler);
363432
}
364433
}
365434

366-
private void OnPresenterItemsAdded(IEnumerable<int> indices, SerializedProperty uiConfigsAddressProperty)
435+
private void OnPresenterItemsAdded(IEnumerable<int> indices, SerializedProperty uiEntriesProperty)
367436
{
368-
if (_uiConfigsAddress == null || _uiConfigsAddress.Count == 0)
437+
if (_uiConfigsAddress == null || _uiConfigsAddress.Count == 0 || _uiTypesByAddress == null)
369438
{
370439
return;
371440
}
372441

373442
var defaultAddress = _uiConfigsAddress[0];
443+
Type defaultType = _uiTypesByAddress.TryGetValue(defaultAddress, out var type) ? type : null;
374444

375445
foreach (var index in indices)
376446
{
377-
if (index < uiConfigsAddressProperty.arraySize)
447+
if (index < uiEntriesProperty.arraySize)
378448
{
379-
var itemProperty = uiConfigsAddressProperty.GetArrayElementAtIndex(index);
380-
itemProperty.stringValue = defaultAddress;
449+
var entryProperty = uiEntriesProperty.GetArrayElementAtIndex(index);
450+
var typeNameProperty = entryProperty.FindPropertyRelative(nameof(UiSetEntry.UiTypeName));
451+
var instanceAddressProperty = entryProperty.FindPropertyRelative(nameof(UiSetEntry.InstanceAddress));
452+
453+
typeNameProperty.stringValue = defaultType?.AssemblyQualifiedName ?? string.Empty;
454+
instanceAddressProperty.stringValue = string.Empty;
381455
}
382456
}
383457

@@ -437,6 +511,16 @@ private void SyncConfigsWithAddressables()
437511
_scriptableObject.Configs = configs;
438512
_uiConfigsAddress = uiConfigsAddress;
439513
_assetPathLookup = assetPathLookup;
514+
515+
// Build Type lookup dictionary
516+
_uiTypesByAddress = new Dictionary<string, Type>();
517+
foreach (var config in configs)
518+
{
519+
if (!string.IsNullOrEmpty(config.AddressableAddress) && config.UiType != null)
520+
{
521+
_uiTypesByAddress[config.AddressableAddress] = config.UiType;
522+
}
523+
}
440524

441525
EditorUtility.SetDirty(_scriptableObject);
442526
AssetDatabase.SaveAssets();

Runtime/IUiService.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,22 @@ namespace GameLovers.UiService
1010
{
1111
/// <summary>
1212
/// This service provides an abstraction layer to interact with the game's UI <seealso cref="UiPresenter"/>
13-
/// The Ui Service is organized by layers. The higher the layer the more close is to the camera viewport
13+
/// The Ui Service is organized by layers. The higher the layer the more close is to the camera viewport.
14+
/// Supports multiple instances of the same UI type through the UiInstanceId system.
1415
/// </summary>
1516
public interface IUiService
1617
{
1718
/// <summary>
18-
/// Gets a read-only dictionary of the Presenters currently Loaded in memory by the UI service.
19+
/// Gets a read-only dictionary of all Presenters currently loaded in memory by the UI service.
20+
/// Keys are UiInstanceId which contain both the Type and optional instance name.
1921
/// </summary>
20-
IReadOnlyDictionary<Type, UiPresenter> LoadedPresenters { get; }
22+
IReadOnlyDictionary<UiInstanceId, UiPresenter> LoadedPresenters { get; }
2123

2224
/// <summary>
23-
/// Gets a read-only list of the Presenters currently currently visible and were opened by the UI service.
25+
/// Gets a read-only list of all Presenter instances currently visible.
26+
/// Each entry is a UiInstanceId containing the Type and instance name.
2427
/// </summary>
25-
IReadOnlyList<Type> VisiblePresenters { get; }
26-
27-
/// <summary>
28-
/// Gets a read-only dictionary of the layers used by the UI service.
29-
/// </summary>
30-
IReadOnlyDictionary<int, GameObject> Layers { get; }
28+
IReadOnlyList<UiInstanceId> VisiblePresenters { get; }
3129

3230
/// <summary>
3331
/// Gets a read-only dictionary of the containers of UI, called 'Ui Set' maintained by the UI service.

Runtime/UiConfigs.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,17 @@ public List<UiConfig> Configs
4343
/// <summary>
4444
/// Gets the list of UI set configurations
4545
/// </summary>
46-
public List<UiSetConfig> Sets => _sets.ConvertAll(element => UiSetConfigSerializable.ToUiSetConfig(element, _configs));
46+
public List<UiSetConfig> Sets => _sets.ConvertAll(element => UiSetConfigSerializable.ToUiSetConfig(element));
4747

4848
/// <summary>
4949
/// Sets the new size of this scriptable object <seealso cref="UiSetConfig"/> list.
50-
/// The UiConfigSets have the same id value that the index in the list
50+
/// The UiConfigSets have the same id value that the index in the list.
51+
/// Validates entries against available UI configs.
5152
/// </summary>
5253
/// <param name="size">The new size of the list</param>
5354
public void SetSetsSize(int size)
5455
{
55-
var validAddresses = new HashSet<string>(_configs.Select(c => c.AddressableAddress));
56+
var validTypeNames = new HashSet<string>(_configs.Select(c => c.UiType));
5657

5758
if (size < _sets.Count)
5859
{
@@ -64,12 +65,20 @@ public void SetSetsSize(int size)
6465
if (i < _sets.Count)
6566
{
6667
var set = _sets[i];
67-
set.UiConfigsAddress.RemoveAll(address => !validAddresses.Contains(address));
68+
69+
// Initialize UiEntries if null
70+
if (set.UiEntries == null)
71+
{
72+
set.UiEntries = new List<UiSetEntry>();
73+
}
74+
75+
// Remove entries that reference non-existent UI types
76+
set.UiEntries.RemoveAll(entry => !validTypeNames.Contains(entry.UiTypeName));
6877
_sets[i] = set;
6978
continue;
7079
}
7180

72-
_sets.Add(new UiSetConfigSerializable { SetId = i, UiConfigsAddress = new List<string>() });
81+
_sets.Add(new UiSetConfigSerializable { SetId = i, UiEntries = new List<UiSetEntry>() });
7382
}
7483
}
7584

0 commit comments

Comments
 (0)