Verifique se o componente de um objeto de jogo pode ser destruído

11

Ao desenvolver um jogo no Unity, estou fazendo uso liberal [RequireComponent(typeof(%ComponentType%))]para garantir que os componentes tenham todas as dependências atendidas.

Agora estou implementando um sistema de tutorial que destaca vários objetos da interface do usuário. Para fazer o realce, estou fazendo uma referência à GameObjectcena, clono-a com Instantiate () e retiro recursivamente todos os componentes que não são necessários para exibição. O problema é que, com o uso liberal RequireComponentdesses componentes, muitos não podem ser simplesmente removidos em nenhuma ordem, pois dependem de outros componentes, que ainda não foram excluídos.

Minha pergunta é: Existe uma maneira de determinar se um Componentpode ser removido do GameObjectque está atualmente anexado.

O código que estou postando funciona tecnicamente, no entanto, gera erros do Unity (não capturados no bloco try-catch, como visto na imagem

insira a descrição da imagem aqui

Aqui está o código:

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

Portanto, a maneira como esse código gerou as últimas três linhas do log de depuração acima é o seguinte: Ele tomou como entrada a GameObjectcom dois componentes: Buttone PopupOpener. PopupOpenerrequer que um Buttoncomponente esteja presente no mesmo GameObject com:

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

A primeira iteração do loop while (denotada pelo texto "ITERATION ON Button Invite (clone)") tentou excluir a Buttonprimeira, mas não pôde, pois depende do PopupOpenercomponente. isso gerou um erro. Em seguida, tentou excluir o PopupOpenercomponente, o que foi feito, pois não é dependente de mais nada. Na próxima iteração (indicada pelo segundo texto "ITERATION ON Button Invite (clone)"), ele tentou excluir o Buttoncomponente restante , o que agora poderia fazer, pois PopupOpenerfoi excluído na primeira iteração (após o erro).

Portanto, minha pergunta é se é possível verificar antecipadamente se um componente especificado pode ser removido de sua corrente GameObject, sem chamar Destroy()ou DestroyImmediate(). Obrigado.

Liam Lime
fonte
Sobre o problema original - o objetivo é fazer uma cópia não interativa (= visual) do (s) elemento (s) da GUI?
Wondra
Sim. O objetivo é fazer uma cópia idêntica, não interativa, na mesma posição, dimensionada da mesma maneira, mostrando o mesmo texto que o objeto de jogo real embaixo. A razão pela qual segui esse método é que o tutorial não precisaria de modificações se a interface do usuário fosse editada posteriormente (o que aconteceria se eu mostrasse capturas de tela).
Liam Lime
Não tenho experiência com o Unity, mas estou imaginando se, em vez de clonar a coisa toda e remover coisas, você poderia apenas copiar as partes necessárias?
Zan Lynx
Eu não testei, mas Destroyremove o componente no final do quadro (ao contrário de Immediately). DestroyImmediateé considerado um estilo ruim, mas acho que mudar para Destroypode realmente eliminar esses erros.
CAD97
Eu tentei os dois, começando com Destroy (já que esse é o caminho correto) e depois mudei para DestroyImmediate para ver se funcionaria. A destruição ainda parece ocorrer na ordem especificada, o que causa as mesmas exceções, apenas no final do quadro.
Liam Lime

Respostas:

9

Definitivamente, é possível, mas exige bastante trabalho: você pode verificar se há um Componentanexo ao GameObjectqual possui um Attributetipo RequireComponentque possui um de seus m_Type#campos atribuíveis ao tipo de componente que você está prestes a remover. Tudo envolvido em um método de extensão:

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

depois de soltar GameObjectExtensionsem qualquer lugar do projeto (ou alternativamente torná-lo um método regular no script), você pode verificar se um componente pode ser removido, por exemplo:

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

No entanto, pode haver outros meios para criar um objeto GUI não interativo - por exemplo, renderizando-o em textura, cobrindo-o com um painel quase transparente que interceptará cliques ou desativará (em vez de destruir) todos os Componentderivados de MonoBehaviourou IPointerClickHandler.

wondra
fonte
Obrigado! Eu queria saber se uma solução pronta é fornecida com o Unity, mas acho que não :) Obrigado pelo seu código!
Liam Lime
@LiamLime Bem, eu realmente não posso confirmar que não há nada embutido, mas é improvável, pois o paradigma típico do Unity está tornando o código tolerante a falhas (ou seja catch, log, continue) ao invés de evitar falhas (ou seja if, possível). No entanto, isso não é muito estilo C # (como um nesta resposta). No final, seu código original pode ter sido mais próximo de como as coisas geralmente são feitas ... e as melhores práticas são uma questão de preferência pessoal.
Wondra
7

Em vez de remover os componentes no clone, você pode apenas desativá-los, definindo .enabled = false-os. Isso não acionará as verificações de dependência, porque um componente não precisa ser ativado para atender a dependência.

  • Quando um Comportamento está desativado, ele ainda estará no objeto e você poderá alterar suas propriedades e chamar seus métodos, mas o mecanismo não chamará mais seu Updatemétodo.
  • Quando um Renderer é desativado, o objeto fica invisível.
  • Quando um colisor é desativado, ele não causa nenhum evento de colisão.
  • Quando um componente da interface do usuário é desativado, o player não pode mais interagir com ele, mas ainda será renderizado, a menos que você também desative o renderizador.
Philipp
fonte
Os componentes não têm uma noção de ativado ou desativado. Comportamentos, colisores e alguns outros tipos sim. Portanto, isso exigiria que o programador fizesse várias chamadas para GetComponent para as várias subclasses.
DubiousPusher