Sprawdź, czy element obiektu gry może zostać zniszczony

11

Rozwijając grę w Unity, korzystam z niej [RequireComponent(typeof(%ComponentType%))]swobodnie, aby upewnić się, że wszystkie komponenty spełniają wszystkie zależności.

Wdrażam teraz system samouczka, który wyróżnia różne obiekty interfejsu użytkownika. Aby podświetlić, odnoszę się do GameObjectsceny, następnie klonuję ją za pomocą Instantiate (), a następnie rekurencyjnie usuwam wszystkie elementy, które nie są potrzebne do wyświetlania. Problem polega na tym, że przy swobodnym stosowaniu RequireComponentw tych komponentach wielu nie można po prostu usunąć w dowolnej kolejności, ponieważ zależą one od innych komponentów, które nie zostały jeszcze usunięte.

Moje pytanie brzmi: czy istnieje sposób, aby ustalić, czy Componentmożna usunąć z GameObjectniego, do którego jest on aktualnie podłączony.

Kod, który publikuję technicznie działa, jednak generuje błędy Unity (nie wychwycone w bloku try-catch, jak widać na obrazku

wprowadź opis zdjęcia tutaj

Oto kod:

public void StripFunctionality(RectTransform rt)
{
    if (rt == null)
    {
        return;
    }

    int componentCount = rt.gameObject.GetComponents<Component>().Length;
    int safety = 10;
    int allowedComponents = 0;
    while (componentCount > allowedComponents && safety > 0)
    {
        Debug.Log("ITERATION ON "+rt.gameObject.name);
        safety--;
        allowedComponents = 0;
        foreach (Component c in rt.gameObject.GetComponents<Component>())
        {
            //Disable clicking on graphics
            if (c is Graphic)
            {
                ((Graphic)c).raycastTarget = false;
            }

            //Remove components we don't want
            if (
                !(c is Image) &&
                !(c is TextMeshProUGUI) &&
                !(c is RectTransform) &&
                !(c is CanvasRenderer)
                )
            {
                try
                {
                    DestroyImmediate(c);
                }
                catch
                {
                    //NoOp
                }
            }
            else
            {
                allowedComponents++;
            }
        }
        componentCount = rt.gameObject.GetComponents<Component>().Length;
    }

    //Recursive call to children
    foreach (RectTransform childRT in rt)
    {
        StripFunctionality(childRT);
    }
}

Tak więc sposób, w jaki ten kod wygenerował trzy ostatnie wiersze dziennika debugowania powyżej, wygląda następująco: Pobrano jako dane wejściowe GameObjectz dwoma składnikami: Buttoni PopupOpener. PopupOpenerRequies że Buttonskładnik ten występuje w tym samym GameObject'u z:

[RequireComponent(typeof(Button))]
public class PopupOpener : MonoBehaviour
{
    ...
}

Pierwsza iteracja pętli while (oznaczona tekstem „ITERATION ON Button Invite (clone)”) usiłowała usunąć Buttonpierwszą, ale nie mogła, ponieważ zależy od PopupOpenerkomponentu. to rzuciło błąd. Następnie próbował usunąć PopupOpenerkomponent, co zrobił, ponieważ nie zależy od niego nic innego. W następnej iteracji (oznaczonej drugim tekstem „ITERATION ON Button Invite (clone)”) próbował usunąć pozostały Buttonkomponent, co teraz mógł zrobić, ponieważ PopupOpenerzostał usunięty w pierwszej iteracji (po błędzie).

Moje pytanie brzmi więc, czy można z wyprzedzeniem sprawdzić, czy określony składnik można usunąć z bieżącego GameObject, bez wywoływania Destroy()lub DestroyImmediate(). Dziękuję Ci.

Liam Lime
źródło
O pierwotnym problemie - czy celem jest wykonanie nieinteraktywnej kopii (= wizualnej) elementów GUI?
wondra
Tak. Celem jest wykonanie identycznej, nieinteraktywnej kopii w tej samej pozycji, skalowanej w ten sam sposób, pokazującej ten sam tekst, co prawdziwy obiekt gry pod spodem. Powodem, dla którego wybrałem tę metodę jest to, że samouczek nie będzie wymagał modyfikacji, jeśli interfejs użytkownika zostanie edytowany w późniejszym momencie (co musiałoby się zdarzyć, gdybym pokazał zrzuty ekranu).
Liam Lime,
Nie mam doświadczenia z Unity, ale zastanawiam się, czy zamiast klonowania całości i usuwania rzeczy mógłbyś skopiować tylko potrzebne części?
Zan Lynx,
Nie testowałem tego, ale Destroyusuwa komponent na końcu ramki (w przeciwieństwie do Immediately). DestroyImmediatei tak jest uważany za zły styl, ale myślę, że przejście na Destroymoże faktycznie wyeliminować te błędy.
97 CAD
Próbowałem obu, zaczynając od Destroy (ponieważ jest to właściwy sposób), a następnie przełączyłem się na DestroyImmediate, aby sprawdzić, czy to zadziała. Destroy nadal wydaje się iść w określonej kolejności, co powoduje te same wyjątki, tylko na końcu ramki.
Liam Lime,

Odpowiedzi:

9

Jest to zdecydowanie możliwe, ale wymaga sporo pracy nóg: możesz sprawdzić, czy istnieje Componentprzyłączenie do tego, GameObjectktóre ma Attributetyp, RequireComponentktóry ma jedno z m_Type#pól przypisanych do typu komponentu, który chcesz usunąć. Wszystko zapakowane w metodę rozszerzenia:

static class GameObjectExtensions
{
    private static bool Requires(Type obj, Type requirement)
    {
        //also check for m_Type1 and m_Type2 if required
        return Attribute.IsDefined(obj, typeof(RequireComponent)) &&
               Attribute.GetCustomAttributes(obj, typeof(RequireComponent)).OfType<RequireComponent>()
               .Any(rc => rc.m_Type0.IsAssignableFrom(requirement));
    }

    internal static bool CanDestroy(this GameObject go, Type t)
    {
        return !go.GetComponents<Component>().Any(c => Requires(c.GetType(), t));
    }
}

po upuszczeniu w GameObjectExtensionsdowolnym miejscu projektu (lub alternatywnie ustaw go jako zwykłą metodę w skrypcie), możesz sprawdzić, czy składnik można usunąć, np .:

rt.gameObject.CanDestroy(c.getType());

Mogą jednak istnieć inne sposoby tworzenia nieinteraktywnego obiektu GUI - na przykład renderowanie go do tekstury, nakładanie go na prawie przezroczysty panel, który będzie przechwytywał kliknięcia lub wyłączał (zamiast niszczyć) wszystkie Componentpochodzące z MonoBehaviourlub IPointerClickHandler.

wondra
źródło
Dzięki! Zastanawiałem się, czy gotowe rozwiązanie jest dostarczane z Unity, ale chyba nie :) Dziękuję za kod!
Liam Lime,
@LiamLime Cóż, naprawdę nie mogę potwierdzić, że nie ma żadnego wbudowanego, ale jest to mało prawdopodobne, ponieważ typowy paradygmat Unity sprawia, że ​​kod jest odporny na błędy (tj. catchLoguj, kontynuuj), a nie zapobiega awariom (tzn. ifWtedy możliwe). Nie jest to jednak styl C # (taki jak ten w tej odpowiedzi). Ostatecznie twój oryginalny kod mógł być bliżej tego, jak zwykle się to robi ... a najlepsza praktyka jest kwestią osobistych preferencji.
wondra
7

Zamiast usuwać komponenty z klonu, możesz je po prostu wyłączyć, ustawiając .enabled = falseje. Nie spowoduje to sprawdzenia zależności, ponieważ komponent nie musi być włączony, aby spełnić zależność.

  • Gdy zachowanie jest wyłączone, nadal będzie znajdować się na obiekcie i możesz nawet zmieniać jego właściwości i wywoływać jego metody, ale silnik nie będzie już wywoływał tej Updatemetody.
  • Gdy moduł renderujący jest wyłączony, obiekt staje się niewidoczny.
  • Gdy zderzacz jest wyłączony, nie spowoduje to żadnych zdarzeń kolizji.
  • Gdy składnik interfejsu użytkownika jest wyłączony, odtwarzacz nie może już z nim współdziałać, ale nadal będzie renderowany, chyba że wyłączysz jego moduł renderujący.
Philipp
źródło
Komponenty nie mają pojęcia włączonego lub wyłączonego. Zachowania, Zderzaki i niektóre inne typy. Wymagałoby to więc od programisty wykonania wielu wywołań GetComponent dla różnych podklas.
DubiousPusher