ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Unity][UI Toolkit] uss에서 first-child, last-child 구현하기 [USS]
    앱 개발/Unity, C# 2024. 11. 29. 20:07

    UI Toolkit -> UGUI (Nova) -> 다시 UI Toolkit으로 migration 하는 중...

    유니티 버전을 2021에서 2022로 업그레이드 하면서 UI Toolkit도 많이 안정되었구나 싶긴 했는데,

    그래도 여전히 기존 css에서 구현되는 기능이 다 사용 가능한 건 아니라 한계를 다시 체험하고 있다.

     

    그 중에 제일 답답했던 것이 child 사이의 거리를 설정할 수 없는 것...

    웹을 할 때는 css에서 flex 속성과 함께 gap 속성을 주면 간단하고 깔끔하게 정렬할 수 있었는데,

    유니티 uss에는 해당 속성이 없어서 자식의 margin을 하나씩 다 설정해주는 방식으로 가야한다.

     

    이 기능을 자동으로 해주는 스크립트를 작성하고 싶었다.

    그러기 위해서는 last-child를 알아야 했는데, 역시 속성이 존재하지 않아 괴로워하던 중

    인터넷에서 미리 괴로워한 사람의 코드를 발견했다.

    이번 포스팅은 그 코드를 샤라웃만 하고 넘어가겠다.

     

    참고 코드

    아래의 코드를 아주 살짝 변경하였다 (OnChildChange 메서드 private -> protected)

     

    Fake CSS pseudo classes :first-child and :last-child in Unity's UIToolkit with regular USS classes .first-child and .last-child.

    Fake CSS pseudo classes :first-child and :last-child in Unity's UIToolkit with regular USS classes .first-child and .last-child. - ChildAnnotator.cs

    gist.github.com

    더보기
    /* Original code[1] Copyright (c) 2022 Shane Celis[1]
       Licensed under the MIT License[1]
       [1]: https://gist.github.com/shanecelis/1ab175c46313da401138ccacceeb0c90
       [1]: https://twitter.com/shanecelis
       [1]: https://opensource.org/licenses/MIT
    */
    
    using UnityEngine.Scripting;
    using UnityEngine.UIElements;
    
    namespace UI.Common
    {
      /** This event represents a change in the children of a VisualElement. */
      public class ChildChangeEvent : EventBase<ChildChangeEvent>, IChangeEvent {
        public int previousValue { get; protected set; }
    
        public int newValue { get; protected set; }
    
        protected override void Init() {
          base.Init();
          this.LocalInit();
        }
    
        private void LocalInit() {
          this.bubbles = false;
          this.tricklesDown = false;
        }
    
        public static ChildChangeEvent GetPooled(int previousValue, int newValue) {
          ChildChangeEvent pooled = EventBase<ChildChangeEvent>.GetPooled();
          pooled.previousValue = previousValue;
          pooled.newValue = newValue;
          return pooled;
        }
    
        public ChildChangeEvent() => this.LocalInit();
      }
    
      /** Make `ChildChangeEvent` part of the events a VisualElement receives. There
          are two principle ways to trigger the ChildChangeEvent:
          a) Call CheckChildChange() which emits an event if the child count has
          changed. A manual poll.
          b) Set `checkInterval` to some milliseconds and `CheckChildChange()` will be
          called on that interval.
          Note: It would be nice to not have to do this with a poll. Internally
          `VisualElement` has a version and hierarchy change event, but we have no
          access to it that I know of. In the future, hopefully polling will not be
          required. If that happens, I'll try to add a note about it here and mark
          this class obsolete.
       */
      public class ChildChangeManipulator : IManipulator {
        private int lastChildCount;
    
        private IVisualElementScheduledItem task;
        private int _checkInterval;
        /** Set up a poll to check the child counts every so many milliseconds. */
        public int checkInterval {
          get => _checkInterval;
          set {
            task?.Pause();
            if (_checkInterval == value)
              return;
            _checkInterval = value;
            if (_checkInterval > 0)
              task = target?.schedule.Execute(CheckChildChange)
                         .Every(_checkInterval);
          }
        }
    
        private VisualElement _target;
        public VisualElement target {
          get => _target;
          set => _target = value;
        }
    
        /** Check whether the child count differs from the last time an event was
            sent. If the counts differ, send a `ChildChangeEvent`.
            This may have false negatives meaning that if one adds and removes from
            the elements in equal amounts, they won't be caught. In that case you might
            want to compute a hash of the children to check for differences.
         */
        public void CheckChildChange() {
          if (target?.childCount != lastChildCount)
            SendChildChange();
        }
    
        public void SendChildChange() {
          if (target == null)
            return;
          var e = ChildChangeEvent.GetPooled(lastChildCount, target.childCount);
          e.target = target;
          lastChildCount = target.childCount;
          target.SendEvent(e);
        }
      }
    
      /** Fake CSS pseudo classes :first-child and :last-child as USS regular classes
          .first-child and .last-child, i.e., the first child will have a .first-child
          USS class, and the last child will have a .last-child USS class. Knowing the
          first and last child helps with styling elements.
          If the children are fixed, no further setup ought to be required.
          If the children are changing, one must setup the `childChanger`; either
          calling `childChanger.CheckChildChange()` or `childChanger.checkInterval =
          1000` to check children every second for instance. Finally one can set that
          interval as a UXML attribute `check-interval`.
          Note: Hopefully Unity will add pseudo class support so one doesn't need to
          resort to this.
          Or hopefully Unity exposes some event for detecting the addition or
          removal of elements. They have one internally but nothing is exposed.
       */
      public class ChildAnnotator : VisualElement {
        public readonly ChildChangeManipulator childChanger;
        
        private VisualElement _firstChild;
        protected VisualElement firstChild {
          get => _firstChild;
          set {
            if (_firstChild != value) {
              _firstChild?.RemoveFromClassList("first-child");
              _firstChild = value;
              _firstChild?.AddToClassList("first-child");
            }
          }
        }
        private VisualElement _lastChild;
        protected VisualElement lastChild {
          get => _lastChild;
          set {
            if (_lastChild != value) {
              _lastChild?.RemoveFromClassList("last-child");
              _lastChild = value;
              _lastChild?.AddToClassList("last-child");
            }
          }
        }
    
        public ChildAnnotator() {
          this.AddManipulator(childChanger = new ChildChangeManipulator());
          RegisterCallback<ChildChangeEvent>(OnChildChange);
          schedule.Execute(() => childChanger.CheckChildChange());
        }
        
        protected virtual void OnChildChange(ChildChangeEvent evt) {
          if (childCount == 0) {
            firstChild = null;
            lastChild = null;
            return;
          }
    
          if (childCount > 0) {
            firstChild = this[0];
            lastChild = this[childCount - 1];
          }
        }
    
        [Preserve]
        public new class UxmlFactory : UxmlFactory<ChildAnnotator, UxmlTraits> { }
    
        [Preserve]
        public new class UxmlTraits : VisualElement.UxmlTraits {
          private readonly UxmlIntAttributeDescription checkInterval = new UxmlIntAttributeDescription { name = "check-interval", defaultValue = 0 };
    
          public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) {
            base.Init(ve, bag, cc);
            var item = (ChildAnnotator) ve;
    
            item.childChanger.checkInterval = checkInterval.GetValueFromBag(bag, cc);
          }
        }
      }
    }



    사용 방법

    아래와 같이 클래스를 연결하여 스타일 적용 가능하다.

     

    끗~