diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ebc35..5a496ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [0.7.0] - 2021-03-12 + +- Added *NonDrawingView* to have an Image without a renderer to not add additional draw calls. +- Added *SafeAreaHelperView* to add the possibility for the *RectTransform* to adjust himself to the screen notches +- Added *AnimatedUiPresenter* to play animation on enter or closing +- Added the possibility to add *Layers* externally into the *UiService* + +**Changed**: +- Now *Canvas* are single *GameObjects* that can be controlled outside of the *UiService* + +**Fixed**: +- Fixed the issue when setting data on *UiPresenterData* not being invoked + ## [0.6.1] - 2020-09-24 - Updated dependency packages diff --git a/Editor/NonDrawingViewEditor.cs b/Editor/NonDrawingViewEditor.cs new file mode 100644 index 0000000..8a933d8 --- /dev/null +++ b/Editor/NonDrawingViewEditor.cs @@ -0,0 +1,26 @@ +using FirstLight.UiService; +using UnityEditor; +using UnityEditor.UI; +using UnityEngine; + +// ReSharper disable once CheckNamespace + +namespace FirstLightEditor.UiService +{ + /// + /// custom inspector + /// + [CanEditMultipleObjects, CustomEditor(typeof(NonDrawingView), false)] + public class NonDrawingViewEditor : GraphicEditor + { + public override void OnInspectorGUI () + { + serializedObject.Update(); + EditorGUILayout.PropertyField(m_Script, new GUILayoutOption[0]); + + // skipping AppearanceControlsGUI + RaycastControlsGUI(); + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Editor/NonDrawingViewEditor.cs.meta b/Editor/NonDrawingViewEditor.cs.meta new file mode 100644 index 0000000..e74e35e --- /dev/null +++ b/Editor/NonDrawingViewEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0d00a734d13e0448b855c59cd9381f86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/AnimatedUiPresenter.cs b/Runtime/AnimatedUiPresenter.cs new file mode 100644 index 0000000..947dc79 --- /dev/null +++ b/Runtime/AnimatedUiPresenter.cs @@ -0,0 +1,137 @@ +using System.Threading.Tasks; +using UnityEngine; + +// ReSharper disable CheckNamespace + +namespace GameLovers.UiService +{ + /// + /// + /// Allows this Presenter to have an intro and outro animation when opened and closed to provide feedback and joy for players. + /// + [RequireComponent(typeof(CanvasGroup))] + public abstract class AnimatedUiPresenter : UiCloseActivePresenter + { + [SerializeField] protected CanvasGroup _canvasGroup; + [SerializeField] protected Animation _animation; + [SerializeField] protected AnimationClip _introAnimationClip; + [SerializeField] protected AnimationClip _outroAnimationClip; + + private void OnValidate() + { + _canvasGroup = _canvasGroup ? _canvasGroup : GetComponent(); + + Debug.Assert(_animation != null, $"Presenter {gameObject.name} does not have a referenced Animation"); + OnEditorValidate(); + } + + protected override async void OnClosed() + { + _animation.clip = _outroAnimationClip; + _animation.Play(); + + await Task.Delay(Mathf.RoundToInt(_animation.clip.length * 1000)); + + if (gameObject != null) + { + gameObject.SetActive(false); + OnClosedCompleted(); + } + } + + protected override async void OnOpened() + { + _canvasGroup.alpha = 0; + _animation.clip = _introAnimationClip; + _animation.Play(); + + await Task.Yield(); + + _canvasGroup.alpha = 1; + + await Task.Delay(Mathf.RoundToInt(_animation.clip.length * 1000)); + + if (gameObject != null) + { + OnOpenedCompleted(); + } + } + + /// + /// Called in the end of this object MonoBehaviour's OnValidate() -> . + /// Override this method to have your custom extra validation. + /// + /// + /// This is Editor only call. + /// + protected virtual void OnEditorValidate() { } + + /// + /// Called in the end of this object's . + /// Override this method to have your custom extra execution when the presenter is opened. + /// + protected virtual void OnOpenedCompleted() { } + + /// + /// Called in the end of this object's . + /// Override this method to have your custom extra execution when the presenter is closed. + /// + protected virtual void OnClosedCompleted() { } + } + + /// + /// + /// Allows this Presenter to have an intro and outro animation when opened and closed to provide feedback and joy for players. + /// + [RequireComponent(typeof(Animation), typeof(CanvasGroup))] + public abstract class AnimatedUiPresenterData : UiCloseActivePresenterData where T : struct + { + [SerializeField] protected Animation _animation; + [SerializeField] protected AnimationClip _introAnimationClip; + [SerializeField] protected AnimationClip _outroAnimationClip; + + private void OnValidate() + { + Debug.Assert(_animation != null, $"Presenter {gameObject.name} does not have a referenced Animation"); + OnEditorValidate(); + } + + protected override async void OnOpened() + { + _animation.clip = _introAnimationClip; + _animation.Play(); + + await Task.Delay(Mathf.RoundToInt(_animation.clip.length * 1000)); + + if (gameObject != null) + { + OnOpenedCompleted(); + } + } + + protected override async void OnClosed() + { + _animation.clip = _outroAnimationClip; + _animation.Play(); + + await Task.Delay(Mathf.RoundToInt(_animation.clip.length * 1000)); + + if (gameObject != null) + { + gameObject.SetActive(false); + OnClosedCompleted(); + } + } + + + /// + protected virtual void OnEditorValidate() { } + + /// + protected virtual void OnOpenedCompleted() { } + + /// + protected virtual void OnClosedCompleted() { } + } +} + diff --git a/Runtime/AnimatedUiPresenter.cs.meta b/Runtime/AnimatedUiPresenter.cs.meta new file mode 100644 index 0000000..4a489ce --- /dev/null +++ b/Runtime/AnimatedUiPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 739b63b617eca498f8eefa6fb150bd57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/IUiService.cs b/Runtime/IUiService.cs index 6ce9fc6..71c9ebb 100644 --- a/Runtime/IUiService.cs +++ b/Runtime/IUiService.cs @@ -14,9 +14,9 @@ namespace GameLovers.UiService public interface IUiService { /// - /// Requests the of the given + /// Requests the root of the given /// - Canvas GetLayer(int layer); + GameObject GetLayer(int layer); /// /// Adds the given UI to the service diff --git a/Runtime/NonDrawingView.cs b/Runtime/NonDrawingView.cs new file mode 100644 index 0000000..cbf7e18 --- /dev/null +++ b/Runtime/NonDrawingView.cs @@ -0,0 +1,26 @@ +using UnityEngine.UI; + +// ReSharper disable CheckNamespace + +namespace FirstLight.UiService +{ + /// + /// A concrete subclass of the Unity UI `Graphic` class that just skips drawing. + /// Useful for providing a raycast target without actually drawing anything. + /// + public class NonDrawingView : Graphic + { + public override void SetMaterialDirty() { } + public override void SetVerticesDirty() { } + + /// + /// Probably not necessary since the chain of calls + /// `Rebuild()`->`UpdateGeometry()`->`DoMeshGeneration()`->`OnPopulateMesh()` won't happen. + /// So here really just as a fail-safe. + /// + protected override void OnPopulateMesh(VertexHelper vh) + { + vh.Clear(); + } + } +} \ No newline at end of file diff --git a/Runtime/NonDrawingView.cs.meta b/Runtime/NonDrawingView.cs.meta new file mode 100644 index 0000000..a60bb60 --- /dev/null +++ b/Runtime/NonDrawingView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6cd32b1e563d84059882296172ef8959 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SafeAreaHelperView.cs b/Runtime/SafeAreaHelperView.cs new file mode 100644 index 0000000..f17ae7b --- /dev/null +++ b/Runtime/SafeAreaHelperView.cs @@ -0,0 +1,127 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +// ReSharper disable CheckNamespace + +namespace FirstLight.UiService +{ + /// + /// This view helper translate anchored views based on device safe area (screens witch a notch) + /// + [RequireComponent(typeof(RectTransform))] + public class SafeAreaHelperView : MonoBehaviour + { + private const float _floatTolerance = 0.01f; + + [SerializeField] private RectTransform _rectTransform; + [SerializeField] private bool _ignoreWidth = true; + [SerializeField] private bool _onUpdate = false; + [SerializeField] private Vector2 _refResolution; + + private Vector2 _initAnchoredPosition; + private Rect _resolution; + private Rect _safeArea; + + internal void OnValidate() + { + _rectTransform = _rectTransform ? _rectTransform : GetComponent(); + _refResolution = transform.root.GetComponent().referenceResolution; + _initAnchoredPosition = _rectTransform.anchoredPosition; + } + + private void Awake() + { + _initAnchoredPosition = _rectTransform.anchoredPosition; + _resolution = new Rect(0,0, Screen.currentResolution.width, Screen.currentResolution.height); + _safeArea = Screen.safeArea; + } + + private void OnEnable() + { + UpdatePositions(); + } + + private void Update() + { + if (_onUpdate) + { + UpdatePositions(); + } + } + + internal void UpdatePositions() + { + var anchorMax = _rectTransform.anchorMax; + var anchorMin = _rectTransform.anchorMin; + var anchoredPosition = _initAnchoredPosition; + +#if UNITY_EDITOR + // Because Unity Device Simulator and Game View have different screen resolution configs and sometimes use Desktop resolution + _safeArea = Screen.safeArea; + _resolution = new Rect(0, 0, Screen.width, Screen.height); + _resolution = _resolution == _safeArea ? _resolution : new Rect(0,0, Screen.currentResolution.width, Screen.currentResolution.height); +#endif + + if (_safeArea == _resolution) + { + return; + } + + // Check if anchored to top or bottom + if (Math.Abs(anchorMax.y - anchorMin.y) < _floatTolerance) + { + // bottom + if (anchorMax.y < _floatTolerance) + { + anchoredPosition.y += (_safeArea.yMin - _resolution.yMin) * _refResolution.y / _resolution.height; + } + else // top + { + anchoredPosition.y += (_safeArea.yMax - _resolution.yMax) * _refResolution.y / _resolution.height; + } + } + + // Check if anchored to left or right + if (!_ignoreWidth && Math.Abs(anchorMax.x - anchorMin.x) < _floatTolerance) + { + // left + if (anchorMax.x < _floatTolerance) + { + anchoredPosition.x += (_safeArea.xMin - _resolution.xMin) * _refResolution.x / _resolution.width; + } + else // right + { + anchoredPosition.x += (_safeArea.xMax - _resolution.xMax) * _refResolution.x / _resolution.width; + } + } + + _rectTransform.anchoredPosition = anchoredPosition; + } + } + + #if UNITY_EDITOR + [UnityEditor.CustomEditor(typeof(SafeAreaHelperView))] + public class SafeAreaHelperViewEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + if (GUILayout.Button("Update Anchored Data")) + { + var view = (SafeAreaHelperView) target; + + view.OnValidate(); + } + + if (GUILayout.Button("Update Anchored View")) + { + var view = (SafeAreaHelperView) target; + + view.UpdatePositions(); + } + } + } + #endif +} \ No newline at end of file diff --git a/Runtime/SafeAreaHelperView.cs.meta b/Runtime/SafeAreaHelperView.cs.meta new file mode 100644 index 0000000..d9baaf0 --- /dev/null +++ b/Runtime/SafeAreaHelperView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c59d679905ae4b039839721dcff1375 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UiPresenter.cs b/Runtime/UiPresenter.cs index 6229ac4..cddbe4b 100644 --- a/Runtime/UiPresenter.cs +++ b/Runtime/UiPresenter.cs @@ -17,11 +17,6 @@ public abstract class UiPresenter : MonoBehaviour /// Requests the open status of the /// public bool IsOpen => gameObject.activeSelf; - - /// - /// Refreshes this opened UI - /// - public virtual void Refresh() {} /// /// Allows the ui presenter implementation to have extra behaviour when it is initialized @@ -58,13 +53,29 @@ internal void InternalOpen() OnOpened(); } - internal void InternalClose() + internal virtual void InternalClose() + { + if (this != null && gameObject != null) + { + gameObject.SetActive(false); + } + OnClosed(); + } + } + + /// + /// This type of UI Presenter closes a menu but does not disable the game object the Presenter is on. + /// The intention is for developers to implement subclasses with behaviour that turns off the game object after completing + /// some behaviour first, e.g. playing an animation or timeline. + /// + public abstract class UiCloseActivePresenter : UiPresenter + { + internal override void InternalClose() { - gameObject.SetActive(false); OnClosed(); } } - + /// /// Tags the as a to allow defining a specific state when /// opening the UI via the @@ -85,11 +96,25 @@ public abstract class UiPresenterData : UiPresenter, IUiPresenterData where T /// /// Allows the ui presenter implementation to have extra behaviour when the data defined for the presenter is set /// - protected virtual void OnSetData(T data) {} + protected virtual void OnSetData() {} internal void InternalSetData(T data) { Data = data; + + OnSetData(); + } + } + + /// + /// Tags the as a to allow defining a specific state when + /// opening the UI via the + /// + public abstract class UiCloseActivePresenterData : UiPresenterData where T : struct + { + internal override void InternalClose() + { + OnClosed(); } } } \ No newline at end of file diff --git a/Runtime/UiService.cs b/Runtime/UiService.cs index 46c721f..f93ab28 100644 --- a/Runtime/UiService.cs +++ b/Runtime/UiService.cs @@ -17,7 +17,7 @@ public class UiService : IUiServiceInit private readonly IDictionary _uiConfigs = new Dictionary(); private readonly IDictionary _uiSets = new Dictionary(); private readonly IList _visibleUiList = new List(); - private readonly IList _layers = new List(); + private readonly IList _layers = new List(); public UiService(IUiAssetLoader assetLoader) { @@ -41,8 +41,25 @@ public void Init(UiConfigs configs) } } + /// + /// Adds the given to be controlled by the . + /// Layers allow to group into the same canvas rendering order. + /// + public GameObject AddLayer(int layer) + { + for(int i = _layers.Count; i <= layer; i++) + { + var newObj = new GameObject($"Layer {i.ToString()}"); + + newObj.transform.position = Vector3.zero; + _layers.Add(newObj); + } + + return _layers[layer]; + } + /// - public Canvas GetLayer(int layer) + public GameObject GetLayer(int layer) { return _layers[layer]; } @@ -75,20 +92,7 @@ public void AddUi(T uiPresenter, int layer, bool openAfter = false) where T : Presenter = uiPresenter }; - for(int i = _layers.Count; i <= layer; i++) - { - var newObj = new GameObject($"Layer {i.ToString()}"); - var canvas = newObj.AddComponent(); - - newObj.transform.position = Vector3.zero; - canvas.renderMode = RenderMode.ScreenSpaceOverlay; - canvas.sortingOrder = i; - - _layers.Add(canvas); - } - _uiViews.Add(reference.UiType, reference); - uiPresenter.transform.SetParent(_layers[layer].transform); uiPresenter.Init(this); if (openAfter) @@ -140,8 +144,9 @@ public async Task LoadUiAsync(Type type, bool openAfter = false) { throw new KeyNotFoundException($"The UiConfig of type {type} was not added to the service. Call {nameof(AddUiConfig)} first"); } - - var gameObject = await _assetLoader.InstantiatePrefabAsync(config.AddressableAddress, null, true); + + var layer = AddLayer(config.Layer); + var gameObject = await _assetLoader.InstantiatePrefabAsync(config.AddressableAddress, layer.transform, false); var uiPresenter = gameObject.GetComponent(); gameObject.SetActive(false); diff --git a/Runtime/UiSetConfig.cs b/Runtime/UiSetConfig.cs index 7870964..2ee4a40 100644 --- a/Runtime/UiSetConfig.cs +++ b/Runtime/UiSetConfig.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; // ReSharper disable CheckNamespace diff --git a/package.json b/package.json index 750a686..906cee0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "com.gamelovers.uiservice", "displayName": "UiService", - "version": "0.6.1", - "unity": "2019.3", + "version": "0.7.0", + "unity": "2020.1", "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", "dependencies": { - "com.unity.addressables": "1.15.1" + "com.unity.addressables": "1.16.16" }, "type": "library", "hideInEditor": false