Eu acho que muitos no processo de desenvolvimento de um jogo para iOS tiveram que enfrentar o fato de que é necessário usar uma ou outra funcionalidade nativa. Em relação ao Unity3D, muitos problemas podem surgir nesta edição: para implementar algum tipo de recurso, você deve procurar plugins nativos escritos no Objective-C. Alguém neste momento imediatamente se desespera e abandona a idéia. Alguém está procurando soluções prontas no AssetStore ou em fóruns, esperando que já exista uma solução pronta. Se não houver soluções prontas, o mais persistente de nós não vê outra maneira senão mergulhar no abismo da programação iOS e da interação do Unity3D com o código Objective-C.
Aqueles que escolherem o último caminho (embora, eu acho, eles mesmos saibam), enfrentarão muitos problemas nesse caminho difícil e espinhoso:
- O iOS é um ecossistema absolutamente desconhecido e isolado, desenvolvendo-se à sua maneira. No mínimo, você terá que gastar muito tempo para entender como chegar ao aplicativo e onde, nas profundezas do projeto Xcode gerado automaticamente, está o código para o mecanismo Unity3D interagir com o componente nativo do aplicativo.
- Objective-C é uma linguagem de programação bastante separada e pouco parecida. E quando se trata de interagir com o código C ++ do aplicativo Unity3D, o “dialeto” dessa linguagem, chamado Objective-C ++, entra em cena. Há muito pouca informação sobre ele, a maior parte é antiga e arquivística.
- O protocolo de interação entre o Unity3D e o aplicativo iOS é pouco descrito. Você deve confiar apenas nos tutoriais de entusiastas da rede que escrevem como desenvolver o plugin nativo mais simples. Ao mesmo tempo, poucas pessoas abordam questões mais profundas e problemas decorrentes da necessidade de fazer algo complicado.
Aqueles que querem aprender sobre os mecanismos de interação do Unity3D com um aplicativo iOS, por favor, em cat.
Para esclarecer o gargalo sombrio da interação do Unity3D com o código nativo, este artigo descreve os aspectos de interação de um aplicativo iOS delegado com o código Unity3D, com o qual as ferramentas C ++ e Objective-C são implementadas e como você mesmo delegar o aplicativo. Essas informações podem ser úteis tanto para uma melhor compreensão dos mecanismos de ligação do Unity3D + iOS quanto para o uso prático.
Interação entre iOS e aplicativo
Como introdução, vamos ver como a interação do aplicativo com o sistema é implementada no iOS e vice-versa. Esquematicamente, o lançamento de um aplicativo iOS se parece com o seguinte:

Para estudar esse mecanismo do ponto de vista do código, é adequado um novo aplicativo criado no Xcode usando o modelo "Aplicativo de visualização única".

Ao selecionar este modelo, a saída fornecerá o aplicativo iOS mais simples que pode ser executado em um dispositivo ou emulador e mostrará uma tela branca. O Xcode ajudará a criar um projeto no qual haverá apenas 5 arquivos com código-fonte (com 2 deles sendo arquivos .h de cabeçalho) e vários arquivos auxiliares que não são interessantes para nós (composição, configurações, ícones).

Vamos ver pelo que os arquivos de código fonte são responsáveis:
- ViewController.m / ViewController.h - códigos-fonte não muito interessantes para nós. Como seu aplicativo possui uma View (que não é representada pelo código, mas usando o Storyboard), você precisará da classe Controller, que controlará essa View. Em geral, dessa maneira o próprio Xcode nos incentiva a usar o padrão MVC. O projeto que gera o Unity3D não terá esses arquivos de origem.
- AppDelegate.m / AppDelegate.h é o delegado do seu aplicativo. O ponto de interesse no aplicativo em que começa o trabalho do código do aplicativo customizado.
- main.m - o ponto de partida do aplicativo. Como qualquer aplicativo C / C ++, ele contém a função principal, com a qual o programa é iniciado.
Agora, vamos ver o código começando com o arquivo
main.m :
int main(int argc, char * argv[]) {
Com a linha 1, tudo está claro e sem explicação, vamos para a linha 2. Indica que o ciclo de vida do aplicativo ocorrerá dentro do pool de Autorelease. O uso do pool de liberação automática nos diz que confiaremos o gerenciamento de memória do aplicativo a esse pool específico, ou seja, ele lidará com problemas quando for necessário liberar memória para uma variável específica. A história sobre gerenciamento de memória no iOS está além do escopo desta história, portanto, não faz sentido aprofundar-se neste tópico. Para aqueles interessados neste tópico, você pode encontrar, por exemplo,
este artigo .
Vamos para a linha 3. Chama a função
UIApplicationMain . Os parâmetros de inicialização do programa (argc, argv) são passados para ele. Em seguida, nesta função, é indicada qual classe usar como classe principal do aplicativo, sua instância é criada. E, finalmente, é indicada qual classe usar como delegado do aplicativo, sua instância é criada, as conexões entre a instância da classe do aplicativo e seu delegado são configuradas.
Em nosso exemplo, nil é passado como a classe que representará a instância do aplicativo - grosso modo, o analógico local é nulo. Além de nulo, você pode passar uma classe específica herdada do
UIApplication lá . Se nil for especificado, o UIApplication será usado. Esta classe é um ponto centralizado para gerenciar e coordenar o trabalho de um aplicativo no iOS e é um singleton. Com ele, você pode aprender quase tudo sobre o estado atual do aplicativo, notificações, janelas, eventos que ocorreram no próprio sistema que afetam esse aplicativo e muito mais. Essa classe quase nunca herda. Vamos abordar a criação da classe Application Delegate em mais detalhes.
Criar Delegado de Aplicativo
Uma indicação de qual classe usar como delegado do aplicativo ocorre em uma chamada de função
NSStringFromClass([AppDelegate class])
Vamos analisar esta chamada em partes.
[AppDelegate class]
Essa construção retorna um objeto da classe AppDelegate (que é declarada em AppDelegate.h / .m) e a função
NSStringFromClass retorna o nome da classe como uma seqüência de caracteres. Simplesmente passamos o nome da string da classe a ser criada e usada como representante da função UIApplicationMain. Para um melhor entendimento, a linha 3 no arquivo
main.m pode ser substituída pelo seguinte:
return UIApplicationMain(argc, argv, nil, @"AppDelegate");
E o resultado de sua implementação seria idêntico à versão original. Aparentemente, os desenvolvedores decidiram adotar essa abordagem para não usar uma constante de string. Com uma abordagem padrão, se você renomear uma classe delegada, o analisador lançará imediatamente um erro. No caso de usar a linha usual, o código será compilado com êxito e você receberá um erro apenas iniciando o aplicativo.
Um mecanismo semelhante para criar uma classe, usando apenas o nome da string da classe, pode lembrá-lo do Reflection from C #. O Objective-C e seu tempo de execução são muito mais poderosos que o Reflection in C #. Este é um ponto muito importante no contexto deste artigo, mas levaria muito tempo para descrever todos os recursos. No entanto, ainda encontraremos "Reflexão" no Objetivo-C abaixo. Resta entender o conceito do delegado do aplicativo e suas funções.
Delegado do aplicativo
Toda interação do aplicativo com o iOS ocorre na classe UIApplication. Essa classe assume muitas responsabilidades - notifica sobre a origem dos eventos, o estado do aplicativo e muito mais. Na maior parte, seu papel é notificar. Mas quando algo acontece no sistema, devemos ser capazes de responder de alguma forma a essa alteração, para executar algum tipo de funcionalidade personalizada. Se uma instância da classe UIApplication fizer isso, essa prática começará a se parecer com uma abordagem chamada
Objeto Divino . Portanto, vale a pena pensar em libertar essa classe de parte de suas responsabilidades.
É para esses fins que o ecossistema do iOS usa algo como delegado de aplicativo. A partir do próprio nome, podemos concluir que estamos lidando com um padrão de design como
Delegação . Em resumo, simplesmente transferimos a responsabilidade pelo processamento da resposta a determinados eventos do aplicativo ao delegado do aplicativo. Para esse fim, em nosso exemplo, a classe AppDelegate foi criada na qual podemos escrever funcionalidades personalizadas, deixando a classe UIApplication para trabalhar no modo de caixa preta. Essa abordagem pode parecer controversa para alguém em termos de beleza do design arquitetônico, mas os próprios autores do iOS estão nos pressionando para essa abordagem e a grande maioria dos desenvolvedores (se não todos) a usa.
Para verificar visualmente com que frequência, durante o trabalho do aplicativo, o delegado do aplicativo recebe uma mensagem específica, veja o diagrama:

Os retângulos amarelos indicam as chamadas de um ou outro método delegado em resposta a determinados eventos da vida útil do aplicativo (ciclo de vida do aplicativo). Este diagrama ilustra apenas eventos relacionados a alterações no estado do aplicativo e não exibe muitos outros aspectos da responsabilidade do delegado, como aceitar notificações ou interagir com estruturas.
Aqui estão alguns exemplos em que podemos precisar acessar um representante de aplicativo do Unity3D:
- manipulação de notificações push e locais
- Registrando Eventos de Ativação do Aplicativo no Analytics
- determinação de como iniciar o aplicativo - “limpo” ou sair do plano de fundo
- como o aplicativo foi iniciado - por tach para notificação, usando Ações rápidas da tela inicial ou apenas por tach on incon
- interação com WatchKit ou HealthKit
- abrir e processar URLs de outro aplicativo. Se esse URL se aplicar ao seu aplicativo, você poderá processá-lo em vez de deixar o sistema abrir esse URL em um navegador
Esta não é a lista completa de cenários. Além disso, é importante notar que o delegado modifica muitos sistemas de análise e publicidade em seus plugins nativos.
Como o Unity3D implementa um delegado de aplicativo
Vamos agora analisar o projeto Xcode gerado pelo Unity3D e descobrir como o delegado do aplicativo é implementado no Unity3D. Ao criar para a plataforma iOS, o Unity3D gera automaticamente um projeto Xcode para você, que usa muito código padrão. Este código de modelo também inclui o código de Delegado de Aplicativo. Dentro de qualquer projeto gerado, você pode encontrar os arquivos
UnityAppController.he UnityAppController.mm . Esses arquivos contêm o código da classe UnityAppController que nos interessa.
De fato, o Unity3D usa uma versão modificada do modelo "Aplicativo de exibição única". Somente neste modelo, o Unity3D usa o delegado do aplicativo não apenas para manipular eventos do iOS, mas também para inicializar o próprio mecanismo, preparar componentes gráficos e muito mais. Isso é muito fácil de entender se você observar o método.
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
no código da classe UnityAppController. Esse método é chamado no momento da inicialização do aplicativo, quando você pode transferir o controle para o seu código personalizado. Dentro deste método, por exemplo, você pode encontrar as seguintes linhas:
UnityInitApplicationNoGraphics([[[NSBundle mainBundle] bundlePath] UTF8String]); [self selectRenderingAPI]; [UnityRenderingView InitializeForAPI: self.renderingAPI]; _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds]; _unityView = [self createUnityView]; [DisplayManager Initialize]; _mainDisplay = [DisplayManager Instance].mainDisplay; [_mainDisplay createWithWindow: _window andView: _unityView]; [self createUI]; [self preStartUnity];
Sem sequer entrar em detalhes sobre o que esses desafios representam, você pode adivinhar que eles estão relacionados à preparação do Unity3D para o trabalho. Acontece o seguinte cenário:
- A função principal é chamada de main.mm
- As classes de instância do aplicativo e seu representante são criadas.
- O delegado do aplicativo prepara e lança o mecanismo Unity3D
- Seu código personalizado começa a funcionar. Se você usa o il2cpp, seu código é convertido de C # para IL e depois para código C ++, que entra diretamente no projeto Xcode.
Esse script parece bastante simples e lógico, mas traz um problema em potencial: como podemos modificar o delegado do aplicativo se não tivermos acesso ao código fonte ao trabalhar no Unity3D?
O Unity3D afetado para modificar o delegado do aplicativo
Podemos dar uma olhada nos arquivos
AppDelegateListener.mm/.h . Eles contêm macros que permitem registrar qualquer classe como ouvinte de eventos para o delegado do aplicativo. Essa é uma boa abordagem, não precisamos modificar o código existente, mas apenas adicionar um novo. Mas tem uma desvantagem significativa: nem todos os eventos do aplicativo são suportados e não há como obter informações sobre o lançamento do aplicativo.
A saída mais óbvia, porém, inaceitável é alterar o código-fonte delegado manualmente depois que o Unity3D cria o projeto Xcode. O problema dessa abordagem é óbvio - a opção é adequada se você fizer montagens com as mãos e não ficar confuso com a necessidade de modificar o código manualmente após cada montagem. No caso de usar construtores (Unity Cloud Build ou qualquer outra máquina de compilação), essa opção é absolutamente inaceitável. Para esses propósitos, os desenvolvedores do Unity3D nos deixaram uma brecha.
O arquivo
UnityAppController.h , além de declarar variáveis e métodos, também contém uma definição de macro:
#define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ...
Essa macro apenas permite substituir o delegado do aplicativo. Para fazer isso, você precisa executar algumas etapas simples:
- Escreva seu próprio representante de aplicativo no Objective-C
- Em algum lugar dentro do código fonte, adicione a seguinte linha
IMPL_APP_CONTROLLER_SUBCLASS(___)
- Coloque essa fonte na pasta Plugins / iOS do seu projeto Unity3D
Agora você receberá um projeto no qual o delegado padrão do aplicativo Unity3D será substituído pelo seu personalizado.
Como funciona a macro de substituição de delegado
Vamos dar uma olhada no código fonte completo da macro:
#define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ... @interface ClassName(OverrideAppDelegate) \ { \ } \ +(void)load; \ @end \ @implementation ClassName(OverrideAppDelegate) \ +(void)load \ { \ extern const char* AppControllerClassName; \ AppControllerClassName = #ClassName; \ } \ @end
O uso dessa macro na sua fonte adicionará o código descrito na macro ao corpo da sua fonte no estágio de compilação. Essa macro faz o seguinte. Primeiro, ele adicionará o método load à interface da sua classe. Uma interface no contexto do Objective-C pode ser pensada como uma coleção de campos e métodos públicos. Em C #, um método de carregamento estático aparecerá na sua classe que não retorna nada. A seguir, a implementação desse método de carregamento será adicionada ao código da sua classe. Nesse método, a variável AppControllerClassName será declarada, que é uma matriz do tipo char e, em seguida, essa variável receberá um valor. Este valor é o nome da string da sua classe. Obviamente, essas informações não são suficientes para entender o mecanismo de operação dessa macro; portanto, devemos entender o que é esse método de "carga" e por que uma variável é declarada.
A
documentação oficial diz que o carregamento é um método especial chamado uma vez para cada classe (especificamente a classe, não suas instâncias) no estágio inicial do lançamento do aplicativo, mesmo antes da chamada da função principal. O ambiente de tempo de execução Objective-c (runtime) na inicialização do aplicativo registrará todas as classes que serão usadas durante a operação do aplicativo e chamará o método load nele, se implementado. Acontece que, mesmo antes do início de qualquer código em nosso aplicativo, a variável AppControllerClassName será adicionada à sua classe.
Então você pode pensar: “E qual é o sentido de ter essa variável se ela for declarada dentro do método e será excluída da memória quando você sair desse método?”. A resposta a esta pergunta está um pouco além dos limites do Objective-C.
E onde está o C ++?
Vamos dar uma outra olhada na declaração dessa variável
extern const char* AppControllerClassName;
A única coisa que pode ser incompreensível nesta declaração é o modificador externo. Se você tentar usar esse modificador no Objective-C puro, o Xcode emitirá um erro. O fato é que esse modificador não faz parte do Objective-C; é implementado em C ++. O Objetivo-C pode ser descrito sucintamente dizendo que é "linguagem C com classes". É uma extensão da linguagem C e permite o uso ilimitado do código C intercalado com o código Objective-C.
No entanto, para usar recursos externos e outros recursos do C ++, você precisa fazer algum truque - use o Objective-C ++. Praticamente não há informações sobre essa linguagem, devido ao fato de ser apenas o código Objective-C que permite a inserção do código C ++. Para que o compilador considere que algum arquivo de origem deve ser compilado como Objective-C ++, e não Objective-C, você só precisa alterar a extensão desse arquivo de
.m para
.mm .
O próprio modificador externo é usado para declarar uma variável global. Mais precisamente, para dizer ao compilador “Acredite em mim, essa variável existe, mas a memória para ela foi alocada não aqui, mas em outra fonte. E ela também tem um valor, eu garanto. ” Assim, nossa linha de código simplesmente cria uma variável global e armazena nela o nome de nossa classe personalizada. Resta apenas entender onde essa variável pode ser usada.
Voltar ao menu principal
Lembramos o que foi dito anteriormente - o delegado do aplicativo é criado especificando o nome da classe. Se o delegado foi criado usando o valor constante [classe myClass] no modelo de projeto regular do Xcode, aparentemente os caras do Unity decidiram que esse valor deveria ser envolvido em uma variável. Usando o método de cutucada científica, pegamos o projeto Xcode gerado pelo Unity3D e vamos para o arquivo
main.mm.Nele vemos código mais complexo do que antes, parte desse código está faltando como desnecessário:
Aqui vemos a declaração dessa variável e a criação do aplicativo delegado com sua ajuda.
Se criamos um delegado personalizado, a variável necessária existe e já importa - o nome da nossa classe. Declarar e inicializar a variável antes da função principal garante que ela tenha um valor padrão - UnityAppController.
Agora, com esta decisão, tudo deve ficar bem claro.
Problema de macro
Obviamente, para a grande maioria das situações, o uso dessa macro é uma ótima solução. Mas vale a pena notar que há uma grande armadilha: você não pode ter mais de um delegado personalizado. Isso acontece porque se 2 ou mais classes usam a macro IMPL_APP_CONTROLLER_SUBCLASS (ClassName), para a primeira delas o valor da variável que precisamos será atribuído e outras atribuições serão ignoradas. E essa variável é uma sequência, ou seja, não pode ser atribuída mais de um valor.
Você pode pensar que esse problema é degenerado e improvável na prática. Mas, este artigo não teria acontecido se esse problema não tivesse realmente ocorrido, e mesmo sob circunstâncias muito estranhas. A situação pode ser a seguinte. Você tem um projeto no qual utiliza muitos serviços de análise e publicidade. Muitos desses serviços possuem componentes Objective-C. Eles estão no seu projeto há muito tempo e você não conhece os problemas com eles. Aqui você precisa escrever um representante personalizado. Você usa uma macro mágica projetada para evitar problemas, criar um projeto e obter um relatório sobre o sucesso da montagem. Execute o projeto no dispositivo, e sua funcionalidade não funciona e você não recebe um único erro.
E pode ser que um dos plugins de publicidade ou análise use a mesma macro. Por exemplo, no plug-in do
AppsFlyer, essa macro é usada.
Qual é o valor da variável externa no caso de múltiplas declarações?
É interessante descobrir se a mesma variável externa é declarada em vários arquivos e eles são inicializados da maneira que nossa macro (no método de carregamento); então, como podemos entender qual valor a variável terá? Para entender o padrão, um aplicativo de teste simples foi criado, cujo código pode ser encontrado
aqui .
A essência do aplicativo é simples. Existem 2 classes A e B, em ambas as classes a variável externa AexternVar é declarada, é atribuído um valor específico. Os valores da variável nas classes são definidos de maneira diferente. Na função principal, o valor desta variável é registrado. Constatou-se experimentalmente que o valor da variável depende da ordem em que as fontes são adicionadas ao projeto. A ordem na qual o tempo de execução Objective-C registra classes durante a execução do aplicativo depende disso. Se você deseja repetir a experiência, abra o projeto e selecione a guia Fases de construção nas configurações do projeto. Como o projeto é pequeno e de teste, ele possui apenas 8 códigos-fonte. Todos eles estão presentes na guia Build Fhases na lista Compile Sources.

Se nesta lista a fonte da classe A for maior que a fonte da classe B, a variável receberá um valor da classe B. Caso contrário, a variável receberá um valor da classe A.
Imagine quantos problemas isso teoricamente pode causar é uma pequena nuance. Especialmente se o projeto for grande, gerado automaticamente e você não souber em quais classes essa variável é declarada.
Resolução de problemas
No início do artigo, foi dito que o Objective-C ajudaria a refletir o C #. Especificamente, para resolver nosso problema, você pode usar o mecanismo chamado
Método Swizzling . A essência desse mecanismo é que temos a oportunidade de substituir a implementação de um método de qualquer classe por outro durante a aplicação. Assim, podemos substituir o método de interesse no UnityAppController por um personalizado. Tomamos a implementação existente e complementamos o código que precisamos. Estamos escrevendo um código que substitui a implementação existente do método pela que precisamos. Durante o trabalho do aplicativo, o delegado que usa a macro funcionará como antes, chamando a implementação básica do UnityAppController, e aí nosso método personalizado entrará em ação e alcançaremos o resultado desejado. Essa abordagem está bem escrita e ilustrada
neste artigo . Com essa técnica, podemos criar uma classe auxiliar - um análogo de um delegado personalizado.
Nesta classe, escreveremos todo o código personalizado, tornando a classe personalizada um tipo de Wrapper para chamar a funcionalidade de outras classes. Essa abordagem funcionará, mas é extremamente implícita devido ao fato de ser difícil rastrear onde o método é substituído e quais consequências ele levará.Outra solução para o problema
O principal aspecto do problema que aconteceu é que há muitos delegados personalizados, ou você pode ter apenas um ou substituí-lo parcialmente por um segundo. Ao mesmo tempo, não há como garantir que o código dos delegados personalizados não ocorra em arquivos de origem diferentes. Acontece que a situação pode ser considerada como uma referência quando há apenas um delegado no aplicativo, você precisa criar classes personalizadas quantas desejar, enquanto nenhuma dessas classes usa a macro para evitar problemas.O problema é pequeno, resta determinar como isso pode ser feito usando o Unity3D, deixando a capacidade de criar um projeto usando uma máquina de compilação. O algoritmo da solução é o seguinte:- Escrevemos delegados personalizados na quantidade necessária, dividindo a lógica dos plugins em diferentes classes, observando os princípios do SOLID e não recorrendo à sofisticação.
- UnityAppController XCode . UnityAppController .
- UnityAppController Unity .
- XCode UnityAppController ,
O item mais difícil desta lista é sem dúvida o último. No entanto, esse recurso pode ser implementado no Unity3D usando o script de construção pós-processo. Esse script foi escrito em uma linda noite, você pode assisti-lo no GitHub .Esse pós-processo é bastante fácil de usar, escolha-o em um projeto do Unity. Olhe na janela Inspetor e veja um campo chamado NewDelegateFile. Arraste e solte seu UnityAppController modificado neste campo e salve.
Ao criar um projeto iOS, o delegado padrão será substituído por um modificado, e nenhuma intervenção manual é necessária. Agora, ao adicionar novos delegados personalizados ao projeto, você só precisa modificar a opção UnityAppController existente no projeto do Unity.PS
Obrigado a todos que chegaram ao final, o artigo acabou sendo extremamente longo. Espero que a informação pintada seja útil.