Search Unity

HP Bars at Runtime? (Image Masking OR Fill?)

Discussion in 'UI Toolkit' started by ModLunar, Mar 16, 2021.

  1. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    I made the following with Unity UI (from Unity 4.6 onwards):
    upload_2021-3-16_16-26-36.png

    The heart is HP and MP for my RPG.
    The red (HP) and blue (MP) are basically bars that go up and down to 0, vertically (at the bottom), and they use the UnityEngine.UI.Image's fill options for this:

    upload_2021-3-16_16-27-41.png

    Question: How do I achieve this functionality with UI Toolkit/UI Builder?

    The red HP fill of the heart just gets squashed whenever it (or its parent) is resized smaller:
    upload_2021-3-16_16-28-39.png

    (What I want is below)
    Heart HP Fill Example.gif
     
    OMGOMGXAXA likes this.
  2. MousePods

    MousePods

    Joined:
    Jul 19, 2012
    Posts:
    467
    ModLunar likes this.
  3. AlexandreT-unity

    AlexandreT-unity

    Unity Technologies

    Joined:
    Feb 1, 2018
    Posts:
    191
    You need to do the following:
    1. Assign a heart-shaped SVG asset to the background of your element, and set overflow to hidden.
    2. Add a nested element (e.g. a red rectangle) that you'll move up and down to simulate the health/mana status.

    Recently, we internally talked about the possibility of introducing a
    generateMask
    callback, that would be similar to
    generateVisualContent
    . It would be used to create a masking shape with the Mesh API (or our future Vector API). That way, you wouldn't have to use an SVG for an arbitrarily shaped mask.

    Btw, there is also a hack that you could use that leverages the specific shape of your heart. I noticed that each half looks like a cropped rotated box with rounded corners. So you could also do the following for each half:
    1. Use an axis-aligned rectangle with overflow set to hidden
    2. Add rotated child with rounded corners and overflow hidden as well (this is the background of the heart).
    3. Add nested counter-rotated rectangular child that you'll move up and down to simulate the red/blue.
     
    Last edited: Mar 17, 2021
    OMGOMGXAXA and MousePods like this.
  4. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    229
    Please consider adding soft masking in the future
     
    RunninglVlan, ModLunar and MousePods like this.
  5. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Huh...
    @AlexandreT-unity I've never created an SVG asset before, and I thought there was an Asset Store package that you needed for Unity to interpret any kind of SVG files?

    Dang, cause I was gonna redesign the UI soon to make my HP/MP bars look different, so unfortunately a hack for its current shape won't help my scenario.
    Thanks for the ideas though!

    I commented on your thread too @MousePods , thanks for sharing!
     
    MousePods likes this.
  6. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Last edited: Mar 17, 2021
  7. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Oooh!!! I think I'm really close.
    I made a custom FillableBar VisualElement C# class, and achieved this result:

    Heart HP Fill in UITOOLKIT!.gif

    I just have the following problem:
    1. It draws my background sprite twice (once as the default rendering, and once with my custom mesh... why is this?)

    In order to demonstrate it was doing something -- I turned down the Opacity -- but obviously, I need to fix the fact that it's drawing twice! (Problem (1) above)


    Cause I'm nice, here's my entire source code ;) (2 C# files)
    Just for demonstration purposes, I left in my namespace, but you can change it.
    I am using:
    - Unity 2020.2.4f1
    - UI Builder 1.0.0-preview.13
    - UI Toolkit 1.0.0-preview.14

    Code (CSharp):
    1. using System;
    2.  
    3. namespace PXG.UIElements {
    4.     /// <summary>
    5.     /// <para>Represents a <see cref="FillableBar"/>'s fill direction (horizontal or vertical orientations only).</para>
    6.     /// </summary>
    7.     [Serializable]
    8.     public enum FillDirection {
    9.         LeftToRight = 0,
    10.         RightToLeft = 1,
    11.         BottomToTop = 2,
    12.         TopToBottom = 3
    13.     }
    14. }
    15.  
    Code (CSharp):
    1. using Unity.Mathematics;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace PXG.UIElements {
    6.     /// <summary>
    7.     /// Represents a <see cref="VisualElement"/> that may be partially filled. This is useful for HP bars, for example.
    8.     /// </summary>
    9.     public class FillableBar : VisualElement {
    10.         //Must create a UxmlFactory in order to be exposed to UXML and UI Builder!
    11.         public new class UxmlFactory : UxmlFactory<FillableBar, UxmlTraits> { }
    12.  
    13.         //Use this to expose additional custom UXML attributes!
    14.         public new class UxmlTraits : VisualElement.UxmlTraits {
    15.             private UxmlFloatAttributeDescription fillAmount = new UxmlFloatAttributeDescription() {
    16.                 name = "fill-amount", //The name used for an actual UXML attribute (written in a .uxml file)
    17.                 defaultValue = 1
    18.             };
    19.             private UxmlEnumAttributeDescription<FillDirection> fillDirection = new UxmlEnumAttributeDescription<FillDirection>() {
    20.                 name = "fill-direction",
    21.                 defaultValue = FillDirection.LeftToRight
    22.             };
    23.  
    24.  
    25.             public override void Init(VisualElement visualElement, IUxmlAttributes bag, CreationContext context) {
    26.                 base.Init(visualElement, bag, context);
    27.  
    28.                 FillableBar element = visualElement as FillableBar;
    29.  
    30.                 element.FillAmount = fillAmount.GetValueFromBag(bag, context);
    31.                 element.FillDirection = fillDirection.GetValueFromBag(bag, context);
    32.             }
    33.         }
    34.  
    35.         public float FillAmount { get; set; }
    36.         public FillDirection FillDirection { get; set; }
    37.  
    38.         public FillableBar() {
    39.             generateVisualContent = GenerateVisualContent;
    40.         }
    41.  
    42.         private void GenerateVisualContent(MeshGenerationContext context) {
    43.             IResolvedStyle resolvedStyle = this.resolvedStyle;
    44.             MeshWriteData data = context.Allocate(4, 6, resolvedStyle.backgroundImage.texture);
    45.  
    46.             Rect localBound = this.localBound;
    47.             Color32 tintColor = resolvedStyle.unityBackgroundImageTintColor;
    48.  
    49.             //WARNING: VisualElement local space is (0, 0) at top-left, and goes +XY right-down (respectively).
    50.             float leftPos = localBound.xMin;
    51.             float rightPos = localBound.xMax;
    52.             float bottomPos = localBound.yMax;
    53.             float topPos = localBound.yMin;
    54.  
    55.             float fillAmount = math.saturate(FillAmount); //Saturate => keep it in range [0, 1]
    56.  
    57.             //These are coordinates using bottom-left (0, 0) like UVs
    58.             float leftUV = 0;
    59.             float rightUV = 1;
    60.             float bottomUV = 0;
    61.             float topUV = 1;
    62.  
    63.             switch (FillDirection) {
    64.                 case FillDirection.LeftToRight:
    65.                     rightUV = fillAmount;
    66.                     rightPos = math.lerp(leftPos, rightPos, fillAmount);
    67.                     break;
    68.                 case FillDirection.RightToLeft:
    69.                     leftUV = 1 - fillAmount;
    70.                     leftPos = math.lerp(rightPos, leftPos, fillAmount);
    71.                     break;
    72.                 case FillDirection.BottomToTop:
    73.                     topUV = fillAmount;
    74.                     topPos = math.lerp(bottomPos, topPos, fillAmount);
    75.                     break;
    76.                 case FillDirection.TopToBottom:
    77.                     bottomUV = 1 - fillAmount;
    78.                     bottomPos = math.lerp(topPos, bottomPos, fillAmount);
    79.                     break;
    80.             }
    81.  
    82.             data.SetNextVertex(new Vertex() {
    83.                 position = new Vector3(leftPos, bottomPos),
    84.                 tint = tintColor,
    85.                 uv = new Vector2(leftUV, bottomUV)
    86.             });
    87.  
    88.             data.SetNextVertex(new Vertex() {
    89.                 position = new Vector3(leftPos, topPos),
    90.                 tint = tintColor,
    91.                 uv = new Vector2(leftUV, topUV)
    92.             });
    93.  
    94.             data.SetNextVertex(new Vertex() {
    95.                 position = new Vector3(rightPos, bottomPos),
    96.                 tint = tintColor,
    97.                 uv = new Vector2(rightUV, bottomUV)
    98.             });
    99.  
    100.             data.SetNextVertex(new Vertex() {
    101.                 position = new Vector3(rightPos, topPos),
    102.                 tint = tintColor,
    103.                 uv = new Vector2(rightUV, topUV)
    104.             });
    105.            
    106.             data.SetNextIndex(0);
    107.             data.SetNextIndex(1);
    108.             data.SetNextIndex(2);
    109.             data.SetNextIndex(3);
    110.             data.SetNextIndex(2);
    111.             data.SetNextIndex(1);
    112.         }
    113.     }
    114. }
    115.  
    How can I fix the double-drawing problem?
     
    OMGOMGXAXA likes this.
  8. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Code (CSharp):
    1. ArgumentException: Value  is not present in the list of possible values
    2. UnityEditor.UIElements.PopupField`1[T].SetValueWithoutNotify (T newValue) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Editor/Controls/PopupField.cs:77)
    3. Unity.UI.Builder.MultiTypeField.SetTypePopupValueWithoutNotify (System.Type type) (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Utilities/MultiTypeField/MultiTypeField.cs:83)
    4. Unity.UI.Builder.BuilderInspectorStyleFields.RefreshStyleField (System.String styleName, UnityEngine.UIElements.VisualElement fieldElement) (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/Inspector/BuilderInspectorStyleFields.cs:724)
    5. Unity.UI.Builder.BuilderInspectorLocalStyles.Refresh () (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/Inspector/BuilderInspectorLocalStyles.cs:173)
    6. Unity.UI.Builder.BuilderInspector.RefreshUI () (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/Inspector/BuilderInspector.cs:470)
    7. Unity.UI.Builder.BuilderInspector.SelectionChanged () (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/Inspector/BuilderInspector.cs:517)
    8. Unity.UI.Builder.BuilderSelection.NotifyOfSelectionChange (Unity.UI.Builder.IBuilderSelectionNotifier source) (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/BuilderSelection.cs:292)
    9. Unity.UI.Builder.BuilderSelection.Select (Unity.UI.Builder.IBuilderSelectionNotifier source, UnityEngine.UIElements.VisualElement ve) (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/BuilderSelection.cs:166)
    10. Unity.UI.Builder.BuilderViewport.OnPick (UnityEngine.UIElements.MouseDownEvent evt) (at Library/PackageCache/com.unity.ui.builder@1.0.0-preview.13/Editor/Builder/Viewport/BuilderViewport.cs:352)
    11. UnityEngine.UIElements.EventCallbackFunctor`1[TEventType].Invoke (UnityEngine.UIElements.EventBase evt) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/EventCallback.cs:64)
    12. UnityEngine.UIElements.EventCallbackRegistry.InvokeCallbacks (UnityEngine.UIElements.EventBase evt) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/EventCallbackRegistry.cs:341)
    13. UnityEngine.UIElements.CallbackEventHandler.HandleEvent (UnityEngine.UIElements.EventBase evt) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/EventHandler.cs:147)
    14. UnityEngine.UIElements.EventDispatchUtilities.PropagateEvent (UnityEngine.UIElements.EventBase evt) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/IEventDispatchingStrategy.cs:147)
    15. UnityEngine.UIElements.MouseEventDispatchingStrategy.SendEventToRegularTarget (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.BaseVisualElementPanel panel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/MouseEventDispatchingStrategy.cs:35)
    16. UnityEngine.UIElements.MouseEventDispatchingStrategy.SendEventToTarget (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.BaseVisualElementPanel panel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/MouseEventDispatchingStrategy.cs:26)
    17. UnityEngine.UIElements.MouseEventDispatchingStrategy.DispatchEvent (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.IPanel iPanel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Events/MouseEventDispatchingStrategy.cs:19)
    18. UnityEngine.UIElements.EventDispatcher.ApplyDispatchingStrategies (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.IPanel panel, System.Boolean imguiEventIsInitiallyUsed) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:377)
    19. UnityEngine.UIElements.EventDispatcher.ProcessEvent (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.IPanel panel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:340)
    20. UnityEngine.UIElements.EventDispatcher.ProcessEventQueue () (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:302)
    21. UnityEngine.UIElements.EventDispatcher.OpenGate () (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:266)
    22. UnityEngine.UIElements.EventDispatcherGate.Dispose () (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:75)
    23. UnityEngine.UIElements.EventDispatcher.ProcessEvent (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.IPanel panel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:368)
    24. UnityEngine.UIElements.EventDispatcher.Dispatch (UnityEngine.UIElements.EventBase evt, UnityEngine.UIElements.IPanel panel, UnityEngine.UIElements.DispatchMode dispatchMode) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/EventDispatcher.cs:218)
    25. UnityEngine.UIElements.BaseVisualElementPanel.SendEvent (UnityEngine.UIElements.EventBase e, UnityEngine.UIElements.DispatchMode dispatchMode) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/Panel.cs:398)
    26. UnityEngine.UIElements.UIElementsUtility.DoDispatch (UnityEngine.UIElements.BaseVisualElementPanel panel) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/UIElementsUtility.cs:466)
    27. UnityEngine.UIElements.UIElementsUtility.UnityEngine.UIElements.IUIElementsUtility.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr, System.Boolean& eventHandled) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/UIElementsUtility.cs:209)
    28. UnityEngine.UIElements.UIEventRegistration.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/UIElementsUtility.cs:74)
    29. UnityEngine.UIElements.UIEventRegistration+<>c.<.cctor>b__1_2 (System.Int32 i, System.IntPtr ptr) (at Library/PackageCache/com.unity.ui@1.0.0-preview.14/Core/UIElementsUtility.cs:28)
    30. UnityEngine.GUIUtility.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr, System.Boolean& result) (at <f8197c5efb504ab8a77daf7d0e9abc5e>:0)
    31.  
    ...It's now failing upon save in the UI Builder, and looks like this:
    upload_2021-3-18_2-0-14.png

    @uDamian any ideas? -insert-thinking-emoji-here-
     
  9. AlexandreT-unity

    AlexandreT-unity

    Unity Technologies

    Joined:
    Feb 1, 2018
    Posts:
    191
    generateVisualContent is an addition to the default rendering, it's not a replacement. So you'd need to use a custom property to store your texture.

    Did you consider using an ImageElement and just changing the uv property? Or alternately, you could set the scale mode to ScaleAndCrop and modify the height. Either way, you wouldn't have to manually generate your geometry.
     
    Last edited: Mar 18, 2021
  10. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    @AlexandreT-unity Ohhh... thanks for clarifying haha!

    I don't see an ImageElement available, am I missing some types here? (I typically just use a VisualElement and put a background image style on it)
    upload_2021-3-18_15-35-41.png


    Setting the scale mode to ScaleAndCrop sounds like a really great idea, and it's so close -- but it's not aligned to a specific corner/side -- can we change that? (See below, unintended behaviour)
    Heart ScaleAndCrop.gif

    Sorry for all the posts on a seemingly simple thing to do haha. I really appreciate your time and help!
     
  11. Midiphony

    Midiphony

    Joined:
    Jan 25, 2019
    Posts:
    12
    ModLunar likes this.
  12. Midiphony

    Midiphony

    Joined:
    Jan 25, 2019
    Posts:
    12
    I tried @AlexandreT-unity 's recommended SVG method and you might want to try it (it's awesome and there's probably a lot of very cool things to do easily this way)

    upload_2021-3-19_0-52-32.png

    If you're curious to try, you need to import the Vector Graphics preview package (I had to get it as a git package with URL com.unity.vectorgraphics).
    And don't forget to import your asset as a UI Toolkit Vector Image. I couldn't set it as a background image if the asset wasn't of this type
    upload_2021-3-19_0-55-21.png
     
    AlexandreT-unity and ModLunar like this.
  13. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    199
  14. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Thanks for the detailed info!
    I wanted to avoid importing vector graphics just for this one issue, so I found a way to clone the texture and modify its raw texture data (in a NativeArray) each time an update is necessary, to achieve the visual effect of filling/emptying any arbitrary texture using the alpha channel of RGBA32 and BGRA32 textures (code may be adapted for more formats).

    Here's my solution for now! :)
    I hope this is not too bad:

    NOTE: This requires marking the texture as read-write enabled in the texture importer inspector.
    Code (CSharp):
    1. using Unity.Mathematics;
    2. using Unity.Collections;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. /// <summary>
    7. /// Represents a <see cref="VisualElement"/> that may be partially filled. This is useful for HP bars, for example.
    8. /// </summary>
    9. public class FillableBar : VisualElement {
    10.     //Must create a UxmlFactory in order to be exposed to UXML and UI Builder!
    11.     public new class UxmlFactory : UxmlFactory<FillableBar, UxmlTraits> { }
    12.  
    13.     //Use this to expose additional custom UXML attributes!
    14.     public new class UxmlTraits : VisualElement.UxmlTraits {
    15.         private UxmlFloatAttributeDescription fillAmount = new UxmlFloatAttributeDescription() {
    16.             name = "fill-amount", //The name used for an actual UXML attribute (written in a .uxml file)
    17.             defaultValue = 1,
    18.         };
    19.         private UxmlEnumAttributeDescription<FillDirection> fillDirection = new UxmlEnumAttributeDescription<FillDirection>() {
    20.             name = "fill-direction",
    21.             defaultValue = FillDirection.LeftToRight
    22.         };
    23.  
    24.         public override void Init(VisualElement visualElement, IUxmlAttributes bag, CreationContext context) {
    25.             base.Init(visualElement, bag, context);
    26.  
    27.             FillableBar element = visualElement as FillableBar;
    28.  
    29.             element.FillAmount = fillAmount.GetValueFromBag(bag, context);
    30.             element.FillDirection = fillDirection.GetValueFromBag(bag, context);
    31.         }
    32.     }
    33.  
    34.     private float fillAmount;
    35.     private FillDirection fillDirection;
    36.  
    37.     private Texture2D originalTexture;
    38.     private Texture2D copyTexture;
    39.  
    40.     /// <summary>
    41.     /// <para>The amount that this bar should be filled, in range [0, 1].</para>
    42.     /// <para>TODO: Figure out how to impose validation in the Unity editor for this value to only be within range [0, 1] (equivalent of OnValidate()?).
    43.     /// The UI Builder inspector seems to somehow set the field directly, bypassing this C# Property setter.</para>
    44.     /// </summary>
    45.     public float FillAmount {
    46.         get { return fillAmount; }
    47.         set { fillAmount = math.saturate(value); }
    48.     }
    49.  
    50.     /// <summary>
    51.     /// The direction that the fill should be based on.
    52.     /// </summary>
    53.     public FillDirection FillDirection {
    54.         get { return fillDirection; }
    55.         set { fillDirection = value; }
    56.     }
    57.  
    58.     public FillableBar() {
    59.         generateVisualContent = GenerateVisualContent;
    60.     }
    61.  
    62.     ~FillableBar() {
    63.         if (copyTexture != null)
    64.             Texture2D.Destroy(copyTexture);
    65.     }
    66.  
    67.     private void GenerateVisualContent(MeshGenerationContext context) {
    68.         IResolvedStyle resolvedStyle = this.resolvedStyle;
    69.         Texture2D backgroundTexture = resolvedStyle.backgroundImage.texture;
    70.  
    71.         if (backgroundTexture != null) {
    72.             if (backgroundTexture != copyTexture) {
    73.                 if (copyTexture != null) {
    74.                     Texture2D.Destroy(copyTexture);
    75.                     copyTexture = null;
    76.                 }
    77.                 originalTexture = backgroundTexture;
    78.                 copyTexture = CreateCopyTexture(originalTexture, FillAmount, FillDirection);
    79.  
    80.                 //Must execute this later (perhaps the next frame or so) because we shouldn't dirty this VisualElement during the generateVisualContent callback!
    81.                 //    (If we did, it would cause an infinite loop, basically-speaking)
    82.                 schedule.Execute(() => {
    83.                     style.backgroundImage = new StyleBackground(copyTexture);
    84.                 });
    85.             } else {
    86.                 UpdateTexture(copyTexture, originalTexture, FillAmount, FillDirection);
    87.             }
    88.         } else {
    89.             if (copyTexture != null) {
    90.                 Texture2D.Destroy(copyTexture);
    91.                 copyTexture = null;
    92.             }
    93.         }
    94.     }
    95.  
    96.     /// <summary>
    97.     /// Creates a copy of the <paramref name="source"/> texture, which may be modified to achieve the visual effect of a bar filling up or emptying.
    98.     /// </summary>
    99.     /// <returns>A deep copy of the texture given.</returns>
    100.     private Texture2D CreateCopyTexture(Texture2D source, float fillAmount, FillDirection fillDirection) {
    101.         //Texture2D copy = new Texture2D(source.width, source.height, source.format, false);
    102.         //source.GetRawTextureData();
    103.         Texture2D copy = Texture2D.Instantiate(source);
    104.         copy.name = source.name + " (Copy)";
    105.         UpdateTexture(copy, source, fillAmount, fillDirection);
    106.         return copy;
    107.     }
    108.  
    109.     /// <summary>
    110.     /// Updates the pixels of <paramref name="target"/> to appear as a filled image based on <paramref name="fillAmount"/> and <paramref name="fillDirection"/>.
    111.     /// Call this whenever you need to visually update the texture to be filled to a new value or direction.
    112.     /// </summary>
    113.     private void UpdateTexture(Texture2D target, Texture2D original, float fillAmount, FillDirection fillDirection) {
    114.         int width = target.width;
    115.         int height = target.height;
    116.  
    117.         Rect uvRect = CalculateFilledRect(fillAmount, fillDirection);
    118.         int2 minPixel = new int2((int) math.round(uvRect.xMin * (width - 1)), (int) math.round(uvRect.yMin * (height - 1)));
    119.         int2 maxPixel = new int2((int) math.round(uvRect.xMax * (width - 1)), (int) math.round(uvRect.yMax * (height - 1)));
    120.  
    121.         switch (target.format) {
    122.             case TextureFormat.RGBA32:
    123.             case TextureFormat.BGRA32: {
    124.                     NativeArray<Color32> pixels = target.GetRawTextureData<Color32>();
    125.                     NativeArray<Color32> originalPixels = original.GetRawTextureData<Color32>();
    126.  
    127.                     //TODO: Optimize this?
    128.                     int i = 0;
    129.                     for (int py = 0; py < height; py++) {
    130.                         for (int px = 0; px < width; px++) {
    131.                             byte alpha = originalPixels[i].a;
    132.  
    133.                             bool inFilledArea = (px >= minPixel.x && px <= maxPixel.x && py >= minPixel.y && py <= maxPixel.y);
    134.                             if (!inFilledArea)
    135.                                 alpha = 0;
    136.  
    137.                             Color32 c = pixels[i];
    138.                             c.a = alpha;
    139.                             pixels[i++] = c;
    140.                         }
    141.                     }
    142.  
    143.                     target.LoadRawTextureData(pixels);
    144.                     target.Apply();
    145.                 }
    146.                 break;
    147.             default:
    148.                 Debug.LogError("Unsupported texture format: " + target.format + "!\n" +
    149.                     "If you'd like to add support for other texture formats, edit this!");
    150.                 break;
    151.         }
    152.     }
    153.  
    154.     /// <summary>
    155.     /// Calculates the normalized area in UV-space that a filled image should be showing within, based on <paramref name="fillAmount"/> and <paramref name="fillDirection"/>.
    156.     /// </summary>
    157.     private Rect CalculateFilledRect(float fillAmount, FillDirection fillDirection) {
    158.         fillAmount = math.saturate(fillAmount); //Saturate => keep it in range [0, 1]
    159.  
    160.         //These are coordinates using bottom-left (0, 0) like UVs
    161.         float leftUV = 0;
    162.         float rightUV = 1;
    163.         float bottomUV = 0;
    164.         float topUV = 1;
    165.  
    166.         switch (fillDirection) {
    167.             case FillDirection.LeftToRight:
    168.                 rightUV = fillAmount;
    169.                 break;
    170.             case FillDirection.RightToLeft:
    171.                 leftUV = 1 - fillAmount;
    172.                 break;
    173.             case FillDirection.BottomToTop:
    174.                 topUV = fillAmount;
    175.                 break;
    176.             case FillDirection.TopToBottom:
    177.                 bottomUV = 1 - fillAmount;
    178.                 break;
    179.         }
    180.  
    181.         return new Rect(leftUV, bottomUV, (rightUV - leftUV), (topUV - bottomUV));
    182.     }
    183. }
    184.  
     
    Oneiros90 likes this.
  15. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    199
    Any reason you use 2 textures instead of 1 r/w? You know previous state and you calculate final state, why do you need intermediate?
     
  16. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Huh, I'm not sure I grasp what you mean.

    The "originalTexture" would be the asset that you set in the Inspector.
    We don't want to overwrite any of the pixels of that asset.

    We instantiate a clone of it that we can modify, and render with (called "copyTexture" in the script above).
    However, we're not filling alpha 0 or 255 in boxes -- there may be portions of the "originalTexture" that were already hidden (alpha value of 0).
    So we need to read if the original pixel (at each location) was even showing to begin with!
    Then we combine that with the fill effect -- to only show originally-showing pixels if they should also be shown in the fill effect.

    Does that make sense?
     
  17. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Ah..

    1st problem: I needed to update to Unity 2021.1+ to get Sprite support for background images.
    2nd problem: I'm noticing the script isn't responding properly to Inspector changes of the background-image property.

    [EDIT] I decided to abandon these efforts and use uGUI (Unity UI) for a while longer, even in Unity 2021.1.
    I really hope we get closer to feature parity with uGUI soon, at least including the ability to show HP bars/masking as shown in this thread!

    Otherwise, goodluck to anyone with a similar issue here with UIElements/UI Toolkit.
    As for time frame, I'll check out UIElements again in early 2023 -- goodluck to the dev team as well!
     
    Last edited: Mar 27, 2021
  18. AlexandreT-unity

    AlexandreT-unity

    Unity Technologies

    Joined:
    Feb 1, 2018
    Posts:
    191
    @ModLunar It might be late but I've thought of another simple way to achieve this:
    • A) Background of the heart
      • B) Empty rectangle element with overlow hidden
        • C) Red half heart textured element
      • D) Empty rectangle element with overlow hidden
        • E) Blue half heart textured element
    As you move B down/up, you move C up/down so it remains static. Same story for D and E.

    Instead of a heart that masks an inner colored rectangle, you would have a rectangle that clips a heart filling texture.
     
    Last edited: Mar 29, 2021
    ModLunar likes this.
  19. OMGOMGXAXA

    OMGOMGXAXA

    Joined:
    Apr 2, 2016
    Posts:
    7
    Last edited: Jul 12, 2021
    ModLunar likes this.
  20. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    You may want to create a new forum thread under the UI Toolkit forums section,

    But I'm curious to see if that's possible too!
    Would you be able to link to your new forum thread here (if you create it)? :)
     
    OMGOMGXAXA likes this.
  21. tenukii

    tenukii

    Joined:
    Mar 31, 2014
    Posts:
    12
    OMGOMGXAXA likes this.
  22. Oneiros90

    Oneiros90

    Joined:
    Apr 29, 2014
    Posts:
    18
    Your code was so helpful, thank you @ModLunar :)
    Here's a couple of improvements that I had to integrate to make it work in my case:
    1. Sometimes
      resolvedStyle.backgroundImage.texture
      returns null if the provided asset is a sprite (not sure why), so I had to add
      Code (CSharp):
      1. Sprite backgroundSprite = resolvedStyle.backgroundImage.sprite;
      2. if (backgroundTexture == null && backgroundSprite != null)
      3.     backgroundTexture = backgroundSprite.texture;
    2. FillAmount
      and
      FillDirection
      properties do not update the texture when called from code, I made your
      GenerateVisualContent
      a private void method callable by their setters
    3. I also update the texture as soon as the element is attached to a panel (through
      RegisterCallback<AttachToPanelEvent>
      ). My default
      FillAmount
      was zero but I could see the full image in the first frame.
     
    ModLunar likes this.
  23. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    290
    Yay, thanks!
    I'm glad posting here can help others out, especially with a new area of Unity's API that doesn't have many examples yet!

    Also thanks for sharing those steps you took, did that actually fix the implementation to work as intended? :D
     
unityunity