Skip to content

Instantly share code, notes, and snippets.

@loopyd
Last active May 27, 2023 00:53
Show Gist options
  • Save loopyd/824e45c51d1ed72fd398f89f435add02 to your computer and use it in GitHub Desktop.
Save loopyd/824e45c51d1ed72fd398f89f435add02 to your computer and use it in GitHub Desktop.
[Unity 3D] [Corgi Engine] [NoesisGUI] [Component] NoesisView Manager - Corgi Singleton
/* **********************************************************************************************************************
NoesisViewManager.cs - A lightweight Noesis singleton ribbon for NoesisView pool management - Release 1.1.0
Copyright (C) 2023 DeityDurg / LoopyD / Saber7ooth
DBAD Licensed -> Please read LICENSE: section for details
**********************************************************************************************************************
FEATURES (current):
- Single component on the Manager prefab does it all
- Queries accelerated by LINQ compile time optimizations (can handle a lot of NoesisView components)
- Generic implementation for future NoesisGUI updates
- NoesisView pool management: AddNoesisView, RemoveNoesisView
- Show and hide controls: ShowNoesisView, HideNoesisView
- NoesisView bulk operations: SoloNoesisView
- Get Pages or UserControls (single or bulk): GetNoesisView, GetNoesisViews method patterns
- Get User Controls (single or bulk): GetNoesisView, GetNoesisViews alternate method patterns
- Get Data Contexts (single or bulk): GetNoesisDataContext, GetNoesisDataContexts
- Enable instantiate multiple NoesisViews of the same type: AllowInstantiate, DenyInstantiate, CanInstantiate
PLANNED FOR PUBLIC RELEASE ( if (love) contribute_more(); ):
- Full MMEventListener and MMEvent integration for all methods.
- MMF_Player and MMF_Feedback auto-integration for Corgi Engine for NoesisGUIManager ShowNoesisView, HideNoesisView
- NoesisView rive triggering via DataTriggers: NoesisViewRiveTrigger, NoesisViewRiveTriggerBulk
PREREQUISATES:
1. You should have Corgi Engine installed and activated -> https://corgi-engine.moremountains.com/
2. You should have NoesisGUI installed into your project and activated -> https://www.noesisengine.com/
3. Generate a blend project and put some XAML goodies in it
4. Ensure that clr-namespace, NoesisGUIExtensions, class, and DataContext are defined in the resources section
of your XAML, or at the root! (dont put them on the Gird control, this script doesnt like that, at all....)
**********************************************************************************************************************
INSTALLATION:
1. Create a new script and paste the code.
2. Change my namespace. MetroEngine is not public, but this code is.
3. Put it on the Corgi Manager prefab by going into Prefab edit mode for the "Manager", and click "Add Component",
put this in there.
4. Follow NoesisGUI method to create a Camera Stack, and ensure you have a GameObject named
"UI Camera" or "UICamera", as this script looks for its parent GameObject to start spawning in the pool.
**********************************************************************************************************************
INTEGRATE:
1. Profit because you now just saved yourself a lot of time, and can spawn in NoesisViews in a managed way!
2. You can now begin overriding corgi classes like GUIManager.cs, store a reference to this class, and you
can call its methods to work with NoesisGUI easily! Toss out much of Corgis HUD code, you wont need it!
**********************************************************************************************************************
LICENSE:
LICENSED UNDER DBAD (Dont be a dick) LICENSE -> https://dbad-license.org/
Version 1.1, December 2016
Copyright (C) 2023 DeityDurg / LoopyD / Saber7ooth
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document.
> DON'T BE A DICK PUBLIC LICENSE
> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
1. Do whatever you like with the original work, just don't be a dick.
Being a dick includes - but is not limited to - the following instances:
1a. Outright copyright infringement - Don't just copy this and change the name.
1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick.
1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick.
2. If you become rich through modifications, related works/services, or supporting the original work,
share the love. Only a dick would make loads off this work and not buy the original work's
creator(s) a pint.
You can SHARE THE LOVE by donating: https://www.paypal.com/paypalme/snowflowerwolf
3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes
you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back.
********************************************************************************************************************** */
using System;
using System.Collections.Generic;
using System.Linq;
using MoreMountains.Tools;
using Noesis;
using UnityEngine;
using UnityEngine.InputSystem;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace MetroidVania2D.MetroEngine
{
/// <summary>
/// Enumorator that defines the type of NoesisView controls we can bind to.
/// </summary>
public enum NoesisViewBoundType
{
None,
NoesisPage,
NoesisUserControl
}
/// <summary>
/// Configuration struct for the NoesisViewBoundData element.
/// </summary>
public struct NoesisViewBoundConfig
{
public bool CanInstantiate;
}
/// <summary>
/// This class contains the bidning information for a NoesisViewBoundData element, along with
/// some type helpers to aide in LINQ compile-time optimization.
/// </summary>
public class NoesisViewBoundTypeInfo
{
#region Fields
public object NoesisBoundClass;
public Type NoesisBoundClassType => NoesisBoundClass.GetType();
public bool IsBound => NoesisBoundClass != null;
public NoesisViewBoundType BoundType {
get {
switch (NoesisBoundClassType) {
case Type page when page.IsSubclassOf(typeof(Page)):
return NoesisViewBoundType.NoesisPage;
case Type userControl when userControl.IsSubclassOf(typeof(UserControl)):
return NoesisViewBoundType.NoesisUserControl;
default:
return NoesisViewBoundType.None;
}
}
}
public NoesisViewBoundConfig NoesisBoundConfig;
#endregion
#region Constructors
public NoesisViewBoundTypeInfo (object noeisisBoundClass, NoesisViewBoundConfig config)
{
NoesisBoundClass = noeisisBoundClass;
NoesisBoundConfig = config;
}
public NoesisViewBoundTypeInfo (object noeisisBoundClass)
{
NoesisBoundClass = noeisisBoundClass;
}
public NoesisViewBoundTypeInfo ()
{
NoesisBoundClass = null;
}
#endregion
/// <summary>
/// Returns a boolean that summarizes if a class is bound to a NoesisViewBoundTypeInfo correctly.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView bound object (the root Framework element in XAML)</typeparam>
/// <returns>true or false, if bidning has been completed properly.</returns>
#region Methods
public bool IsBoundToClassOfType<T1>() where T1 : FrameworkElement
{
if (IsBound && NoesisBoundClass is T1 && BoundType == NoesisViewBoundType.NoesisUserControl || BoundType == NoesisViewBoundType.NoesisPage) {
return true;
}
return false;
}
public bool CanInstantiate () => NoesisBoundConfig.CanInstantiate;
public void SetCanInstantiate (bool canInstantiate) => NoesisBoundConfig.CanInstantiate = canInstantiate;
#endregion
}
/// <summary>
/// This class contains the data for a NoesisViewBoundData element.
/// </summary>
public class NoesisViewBoundData
{
#region Fields
public GUID NoesisBoundHandle;
public NoesisViewBoundTypeInfo NoesisBoundType;
public Component NeosisViewRef;
public bool IsBoundToView => NeosisViewRef != null;
/// TODO: I have no idea why I put this here, it will probably be removed.
public UnityEngine.Transform transform { get; set; }
#endregion
#region Constructors
public NoesisViewBoundData ()
{
NoesisBoundHandle = new GUID();
NoesisBoundType = new NoesisViewBoundTypeInfo();
NeosisViewRef = null;
}
public NoesisViewBoundData (NoesisViewBoundTypeInfo noesisBoundType, NoesisView noesisViewRef)
{
NoesisBoundHandle = new GUID();
NoesisBoundType = noesisBoundType;
NeosisViewRef = noesisViewRef;
}
#endregion
#region Methods
/// <summary>
/// Returns a boolean that summarizes if a NoesisView is bound to a NoesisViewBoundData correctly.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView bound object (the root Framework element in XAML)</typeparam>
/// <returns>true or false, if bidning has been completed properly.</returns>
public bool IsBoundToViewWithType<T1> () where T1 : FrameworkElement
{
return IsBoundToView && NeosisViewRef is NoesisView && NoesisBoundType.IsBound && NoesisBoundType.IsBoundToClassOfType<T1>();
}
#endregion
}
/// <summary>
/// NoesisViewManager is a convenient pool for NoesisGUI NoesisView components in Unity.
/// </summary>
[Serializable]
public class NoesisViewManager : MMSingleton<NoesisViewManager>
{
#region Classes
#endregion
#region Fields
[SerializeField] public InputActionAsset InputActions;
[SerializeField] public GameObject UICameraRef;
private List<NoesisViewBoundData> NeosisBoundData;
#endregion
#region Event Arguments
public class NoesisViewEventArgs : System.EventArgs
{
public NoesisViewEventArgs (NoesisView noesisView, NoesisViewBoundData noesisViewBoundData)
{
NoesisView = noesisView;
NoesisViewBoundData = noesisViewBoundData;
}
public NoesisView NoesisView { get; private set; }
public NoesisViewBoundData NoesisViewBoundData { get; private set; }
}
#endregion
#region Delegates
public delegate void NoesisViewEventDelegate (System.Object sender, NoesisViewEventArgs e);
#endregion
#region Events
private event NoesisViewEventDelegate NoesisViewAddedEvent;
private event NoesisViewEventDelegate NoesisViewRemovedEvent;
#endregion
#region Monobehaviours
/// <summary>
/// The awake property gets called when the object is initialized, before the game starts.
/// </summary>
protected override void Awake()
{
base.Awake();
}
#endregion
#region Methods
/// <summary>
/// Subscribe an event to the NoesisViewAdded event.
/// </summary>
/// <param name="noesisViewEvent">The reference to the <paramref name="noesisViewEvent"/></param>
/// <returns>void</returns>
public void SubscribeNoeisisViewAdded (NoesisViewEventDelegate noesisViewEvent) => NoesisViewAddedEvent += noesisViewEvent;
/// <summary>
/// Unsubscribe an event to the NoesisViewAdded event.
/// </summary>
/// <param name="noesisViewEvent">The reference to the <paramref name="noesisViewEvent"/></param>
/// <returns>void</returns>
public void UnsubscribeNoeisisViewAdded (NoesisViewEventDelegate noesisViewEvent) => NoesisViewAddedEvent -= noesisViewEvent;
/// <summary>
/// Subscribe an event to the NoesisViewRemoved event.
/// </summary>
/// <param name="noesisViewEvent">The reference to the <paramref name="noesisViewEvent"/></param>
/// <returns>void</returns>
public void SubscribeNoeisisViewRemoved (NoesisViewEventDelegate noesisViewEvent) => NoesisViewRemovedEvent += noesisViewEvent;
/// <summary>
/// Unsubscribe an event to the NoesisViewRemoved event.
/// </summary>
public void UnsubscribeNoeisisViewRemoved (NoesisViewEventDelegate noesisViewEvent) => NoesisViewRemovedEvent -= noesisViewEvent;
/// <summary>
/// Add a NoesisView to the referenced GameObject (loaded into UICameraRZef on Awake())
/// </summary>
/// <typeparam name="T1">The class type of your NoesisView.</typeparam>
/// <param name="xaml">The xaml we would like to use</param>
public void AddNoesisView<T1> (NoesisXaml xaml) where T1 : FrameworkElement
{
int controlCount = NeosisBoundData.Where(x => x.NoesisBoundType.IsBoundToClassOfType<T1>() && (x.NeosisViewRef as NoesisView).Xaml.name == xaml.name).ToList().Count;
if ((CanInstantiate<T1>() == false && controlCount == 0) || (CanInstantiate<T1>() == true && controlCount > 0)) {
NoesisView noesisView = UICameraRef.AddComponent<NoesisView>();
noesisView.Xaml = xaml;
noesisView.LoadXaml(true);
if (noesisView.Xaml != null) {
noesisView.EnableActions = true;
noesisView.RenderFlags = RenderFlags.PPAA;
noesisView.Actions = InputActions;
}
var noesisBoundData = new NoesisViewBoundData(new NoesisViewBoundTypeInfo(NoesisViewBoundType.NoesisUserControl), noesisView);
NeosisBoundData.Add(noesisBoundData);
NoesisViewAddedEvent?.Invoke(this, new NoesisViewEventArgs(
noesisView,
noesisBoundData
));
}
}
/// <summary>
/// Get the raw bound Noesis View Data (useful for working on the NoesisView directly)
/// of the specified type. This method returns multiple results.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch (root element in XAML DOM)</typeparam>
/// <returns>A A List object containing the requested information.</returns>
public List<NoesisViewBoundData> GetNoesisViews<T1>() where T1 : FrameworkElement
=> NeosisBoundData.Where(x => x.NoesisBoundType.IsBoundToClassOfType<T1>()).ToList();
/// <summary>
/// Get the raw bound Noesis View Data (useful for working on the NoesisView directly) by its
/// GUID handle.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch (root element in XAML DOM)</typeparam>
/// <param name="noesisHandle">The specific NeosisViewManager GUID handle to fetch</param>
/// <returns>A NoesisViewBoundData object containing the requested information.</returns>
public NoesisViewBoundData GetNoesisView<T1>(GUID noesisHandle) where T1 : FrameworkElement => NeosisBoundData.Where(x => x.IsBoundToViewWithType<T1>() && x.NoesisBoundHandle == noesisHandle).FirstOrDefault();
/// <summary>
/// Allow instantiation (multiple of) for the specified Noesis View type.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to modify (root element in XAML DOM)</typeparam>
public void AllowInstantiate<T1> () where T1 : FrameworkElement => NeosisBoundData.Where(s => s.IsBoundToViewWithType<T1>()).ToList().ForEach(x => x.NoesisBoundType.NoesisBoundConfig.CanInstantiate = true);
/// <summary>
/// Deny instantiation (multiple of) for the specified Noesis View type.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to modify (root element in XAML DOM)</typeparam>
public void DenyInstantiate<T1>() where T1 : FrameworkElement => NeosisBoundData.Where(s => s.IsBoundToViewWithType<T1>()).ToList().ForEach(x => x.NoesisBoundType.NoesisBoundConfig.CanInstantiate = false);
/// <summary>
/// Returns whether or not the specified Noesis View type can be instantiated
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to query (root element in XAML DOM)</typeparam>
/// <returns>A boolean which represents this case.</returns>
public bool CanInstantiate<T1>() where T1 : FrameworkElement => NeosisBoundData.Where( s =>
s.IsBoundToViewWithType<T1>() &&
s.NoesisBoundType.CanInstantiate() == true).ToList().Count > 0;
/// <summary>
/// Remove all NoesisViews of the specified type from the game.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to remove (root element in XAML DOM)</typeparam>
public void RemoveNoesisViews<T1> () where T1 : FrameworkElement => GetNoesisViews<T1>()?.ForEach( (x) => {
Destroy(x.NeosisViewRef);
NeosisBoundData.Remove(x);
NoesisViewRemovedEvent?.Invoke(this, new NoesisViewEventArgs(
x.NeosisViewRef as NoesisView,
x
));
});
/// <summary>
/// Remove a specific NoesisView of the specified type from the game. Here you can target
/// a single NoesisView by its NoesisGUIManager GUID handle.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to remove (root element in XAML DOM)</typeparam>
/// <param name="noesisHandle">The specific NeosisViewManager GUID handle to remove</param>
public void RemoveNoesisView<T1> (GUID noesisHandle) where T1 : FrameworkElement
{
var result = GetNoesisView<T1>(noesisHandle);
if (result != null) {
Destroy(result.NeosisViewRef);
NeosisBoundData.Remove(result);
NoesisViewRemovedEvent?.Invoke(this, new NoesisViewEventArgs (
result.NeosisViewRef as NoesisView,
result
));
}
}
/// <summary>
/// Get all user controls from the NoesisView of the specified type, with the specified name.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch from (root element in XAML DOM)</typeparam>
/// <typeparam name="T2">The user control you'd like to get</typeparam>
/// <param name="name">The name of the user control (as specified in XAML)</param>
/// <returns>A list of UserControl objects of the specified type</returns>
public List<T2> GetNoesisUserControls<T1, T2>(string name) where T1 : FrameworkElement where T2 : UserControl => GetNoesisViews<T1>()
.Select(data => ((data.NoesisBoundType.NoesisBoundClass as T1).FindName(name)) as T2)
.OfType<T2>()
.ToList();
/// <summary>
/// Get the user control from the NoesisView of the specified type, with the specified name.
/// Use the NeosisViewManager GUID handle to specify which specific NoesisView you want to get the
/// user control from.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch from (root element in XAML DOM)</typeparam>
/// <typeparam name="T2">The type of the User Control to fetch from</typeparam>
/// <param name="noesisHandle">The specific NeosisViewManager GUID handle to fetch from</param>
/// <param name="name">The name of the UserControl (as specified in XAML)</param>
/// <returns>A UserControl object of the specified type.</returns>
public T2 GetNoesisUserControl<T1, T2>(GUID noesisHandle, string name) where T1 : FrameworkElement where T2 : UserControl
{
var result = GetNoesisView<T1>(noesisHandle);
if (result != null) {
return (result.NoesisBoundType.NoesisBoundClass as T1).FindName(name) as T2;
}
return null;
}
/// <summary>
/// Get all the DataContexts of the NoesisView of the specified type (useful for working on the data
/// context directly, grabbing properties, or firing methods)
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch from (root element in XAML DOM)</typeparam>
/// <typeparam name="T2">The recieving type of the DataContext</typeparam>
/// <returns>A list of DataContext objects of the specified type (for ViewModels, primarily)</returns>
public List<T2> GetNoesisDataContexts<T1, T2>() where T1 : FrameworkElement where T2 : class => GetNoesisViews<T1>()
.Select(data => (data.NoesisBoundType.NoesisBoundClass as T1).DataContext as T2)
.OfType<T2>()
.ToList();
/// <summary>
/// Get the DataContext of the NoesisView of the specified type (useful for working on the data
/// directly, for example, grabbing properties or firing methods)
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch from (root element in XAML DOM)</typeparam>
/// <typeparam name="T2">The recieving type of the DataContext</typeparam>
/// <param name="noesisHandle">The specific NeosisViewManager GUID handle to fetch from</param>
/// <returns>A ataContext object of the specified type (for ViewModels, primarily)</returns>
public T2 GetNoesisDataContext<T1, T2>(GUID noesisHandle) where T1: FrameworkElement where T2 : class
{
var result = GetNoesisView<T1>(noesisHandle);
if (result != null) {
return (result.NoesisBoundType.NoesisBoundClass as T1).DataContext as T2;
}
return null;
}
/// <summary>
/// Get the DataContexts of the user control of the specified type on the NoesisView of the
/// specified type (useful for working on the data directly exposed from a UserControl.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to fetch from (root element in XAML DOM)</typeparam>
/// <typeparam name="T2">The type of UserControl to fetch from</typeparam>
/// <typeparam name="T3">The recieving type of the DataContext</typeparam>
/// <param name="name">The name of the UserControl (specified in XAML)</param>
/// <returns>A list of DataContext objects of the specified type</returns>
public List<T3> GetNoesisDataContexts<T1, T2, T3> (string name) where T1 : FrameworkElement where T2 : UserControl where T3 : class => GetNoesisUserControls<T1, T2>(name)
.Select(data => data.DataContext as T3)
.OfType<T3>()
.ToList();
/// <summary>
/// Shows the specific NoesisView of the specified type using the
/// Neosis GUID handle.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView control to show (root element in XAML DOM)</typeparam>
/// <param name="noesisHandle">The NeosisViewManager GUID Handle</param>
public void ShowNoesisView<T1> (GUID noesisHandle) where T1 : FrameworkElement
{
var result = (GetNoesisView<T1>(noesisHandle)?.NoesisBoundType.NoesisBoundClass as T1);
result.Visibility = Visibility.Visible;
}
/// <summary>
/// Hides the NoesisView of the specified type.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView to hide.</typeparam>
public void HideNoesisView<T1>(GUID noesisHandle) where T1 : FrameworkElement
{
var result = (GetNoesisView<T1>(noesisHandle)?.NoesisBoundType.NoesisBoundClass as T1);
result.Visibility = Visibility.Hidden;
}
/// <summary>
/// Causes the NoesisView of the specified type to be the only one visible.
/// </summary>
/// <typeparam name="T1">The type of the NoesisView to solo.</typeparam>
public void SoloNoesisView<T1> (GUID noesisHandle) where T1 : FrameworkElement
{
GetNoesisViews<T1>().ForEach(
x =>
{
(x.NoesisBoundType.NoesisBoundClass as FrameworkElement).Visibility = Visibility.Hidden;
});
var result = GetNoesisView<T1>(noesisHandle)?.NoesisBoundType.NoesisBoundClass as T1;
result.Visibility = Visibility.Visible;
}
#endregion
}
}
@loopyd
Copy link
Author

loopyd commented May 27, 2023

Updated to version 1.1.0 with subscribable events, you can call methods:

  • SubscribeNoeisisViewAdded
  • UnubscribeNoeisisViewAdded
  • SubscribeNoesisViewRemoved
  • UnSubscribeNoesisViewRemoved

You can subscribe your own events that match the NoesisViewEventArgs / NoesisViewEventDelegate pattern to getr notified in another class when elements are added or removed from the pool.

I should also make it clear that this is a singleton manager intended to be installed into your Managers prefab in corgi scene to make it much easier to integrate Corgi Engine with NoesisiGUI. Access to DataContext with the pooll allows you to do things like trigger rive controls and such.

This is not a Unity DOTS solution as NoeisisView will not work with DOTS Hybrid Renderer and it is independent of the DOTS world. You should use other assets like DamageNumbersPro in that case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment