Vérifiez si le composant d'un objet de jeu peut être détruit

11

En développant un jeu dans Unity, je fais un usage libéral de [RequireComponent(typeof(%ComponentType%))]pour assurer que les composants ont toutes les dépendances respectées.

J'implémente maintenant un système de tutoriel qui met en évidence divers objets d'interface utilisateur. Pour faire la mise en surbrillance, je prends une référence au GameObjectdans la scène, puis je le clone avec Instantiate (), puis je supprime récursivement tous les composants qui ne sont pas nécessaires pour l'affichage. Le problème est qu'avec l'utilisation libérale de RequireComponentdans ces composants, beaucoup ne peuvent pas être simplement supprimés dans n'importe quel ordre, car ils dépendent d'autres composants, qui n'ont pas encore été supprimés.

Ma question est la suivante: existe-t-il un moyen de déterminer si un Componentpeut être supprimé du fichier auquel GameObjectil est actuellement attaché?

Le code que je publie fonctionne techniquement, mais il génère des erreurs Unity (pas pris dans le bloc try-catch, comme on le voit dans l'image

entrez la description de l'image ici

Voici le code:

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);
    }
}

Ainsi, la façon dont ce code a généré les trois dernières lignes du journal de débogage ci-dessus est la suivante: il a pris comme entrée a GameObjectavec deux composants: Buttonet PopupOpener. PopupOpenerrequiert qu'un Buttoncomposant soit présent dans le même GameObject avec:

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

La première itération de la boucle while (indiquée par le texte "ITERATION ON Button Invite (clone)") a tenté de supprimer la Buttonpremière, mais n'a pas pu car elle dépend du PopupOpenercomposant. cela a jeté une erreur. Ensuite, il a tenté de supprimer le PopupOpenercomposant, ce qu'il a fait, car il ne dépend d'aucune autre chose. Dans l'itération suivante (désignée par le deuxième texte "ITERATION ON Button Invite (clone)"), il a tenté de supprimer le Buttoncomposant restant , ce qu'il pouvait maintenant faire, car il PopupOpeneravait été supprimé lors de la première itération (après l'erreur).

Ma question est donc de savoir s'il est possible de vérifier à l'avance si un composant spécifié peut être supprimé de son courant GameObject, sans invoquer Destroy()ou DestroyImmediate(). Je vous remercie.

Liam Lime
la source
À propos du problème d'origine - l'objectif est-il de faire une copie non interactive (= visuelle) des éléments GUI?
wondra
Oui. Le but est de faire une copie identique et non interactif dans la même position, mise à l'échelle de la même manière, montrant le même texte que le véritable objet de jeu en dessous. La raison pour laquelle j'ai choisi cette méthode est que le didacticiel n'aurait pas besoin de modifications si l'interface utilisateur était modifiée ultérieurement (ce qui devrait se produire si je montrais des captures d'écran).
Liam Lime
Je n'ai pas d'expérience avec Unity, mais je me demande si, au lieu de cloner le tout et de supprimer des éléments, pourriez-vous uniquement copier les parties dont vous avez besoin?
Zan Lynx
Je ne l'ai pas testé, mais Destroysupprime le composant à la fin du cadre (par opposition à Immediately). DestroyImmediateest considéré comme un mauvais style de toute façon, mais je pense que le passage à Destroypeut effectivement éliminer ces erreurs.
CAD97
J'ai essayé les deux, en commençant par Destroy (car c'est la bonne façon), puis je suis passé à DestroyImmediate pour voir si cela fonctionnerait. Destroy semble toujours aller dans l'ordre spécifié, ce qui provoque les mêmes exceptions, juste à la fin de la trame.
Liam Lime

Réponses:

9

C'est certainement possible, mais cela demande beaucoup de travail: vous pouvez vérifier s'il y a un Componentattaché au GameObjectqui a un Attributetype RequireComponentqui a un de ses m_Type#champs attribuable au type de composant que vous êtes sur le point de supprimer. Le tout enveloppé dans une méthode d'extension:

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));
    }
}

après avoir déposé GameObjectExtensionsn'importe où dans le projet (ou en faire une méthode régulière sur le script), vous pouvez vérifier si un composant peut être supprimé, par exemple:

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

Cependant, il peut y avoir d'autres moyens de créer un objet GUI non interactif - par exemple, le rendre en texture, le superposer avec un panneau presque transparent qui interceptera les clics ou désactivera (au lieu de détruire) tous les Components dérivant de MonoBehaviourou IPointerClickHandler.

Wondra
la source
Merci! Je me demandais si une solution prête à l'emploi est livrée avec Unity, mais je suppose que non :) Merci pour votre code!
Liam Lime
@LiamLime Eh bien, je ne peux pas vraiment confirmer qu'il n'y en a pas, mais c'est improbable car le paradigme Unity typique rend le code tolérant aux pannes (c.-à-d catch. Journal, continue) plutôt que de prévenir les pannes (c.-à-d. ifPossibles, alors). Ce n'est cependant pas beaucoup de style C # (comme celui de cette réponse). En fin de compte, votre code d'origine aurait pu être plus proche de la façon dont les choses sont généralement faites ... et les meilleures pratiques sont une question de préférence personnelle.
wondra
7

Au lieu de supprimer les composants du clone, vous pouvez simplement les désactiver en les paramétrant .enabled = false. Cela ne déclenchera pas les vérifications de dépendance, car un composant n'a pas besoin d'être activé pour remplir la dépendance.

  • Lorsqu'un comportement est désactivé, il sera toujours sur l'objet et vous pouvez même modifier ses propriétés et appeler ses méthodes, mais le moteur n'appellera plus sa Updateméthode.
  • Lorsqu'un rendu est désactivé, l'objet devient invisible.
  • Lorsqu'un collisionneur est désactivé, aucun événement de collision ne se produit.
  • Lorsqu'un composant d'interface utilisateur est désactivé, le joueur ne peut plus interagir avec lui, mais il sera toujours rendu, sauf si vous désactivez également son moteur de rendu.
Philipp
la source
Les composants n'ont pas la notion d'activer ou de désactiver. Les comportements, les collisionneurs et certains autres types le font. Cela nécessiterait donc que le programmeur effectue plusieurs appels à GetComponent pour les différentes sous-classes.
DubiousPusher