sábado, 20 de abril de 2013

Unity C# - Singleton

E ai pessoal, tudo bem?

Bom... eu ainda não posso falar sobre os projetos em que estamos trabalhando... mas como eu to com um tempinho livre, achei que podia ser legal fazer um tutorialzinho de uma coisa que é EXTREMAMENTE útil, e que eu sei que não é todo mundo que usa... Singletons.

Antes de continuar, gostaria de dizer que esse tutorialzinho básico será em C# (que diga-se de passagem eu recomendo fortemente se você usa Unity), e é bem simples, pode ser útil pra qualquer um, sem exagero.

-Singleton

De acordo com o Google Images, a definição de Singleton é:

Me vê 2 shot de Singleton por favor...

No entanto, em programação, a definição é um pouco diferente. Eu já li definições de Singleton que fazem com que você tenha medo de saber o que isso é de verdade... mas resumidamente é a coisa mais simples do mundo: É um código que garante que só exista UM objeto de sua classe e é facilmente acessível justamente por ser único.

Por exemplo... vamos supor um Tower Defense. Em um jogo desse tipo (e MUITOS outros tipos) você provavelmente vai ter uma classe que seja WaveHandler ou qualquer coisa do tipo, enfim, UM ÚNICO objeto (instancia da classe) que é responsável por controlar o spawn dos inimigos, por wave, por tempo, por seja la o que for. Essa classe mede quantos monstros estão vivos, quantos faltam pra wave acabar, cria os inimigos... enfim, esse cara sozinho eh quem manda no sistema de spawn. Então, usando um código bem simples, essa classe seria +- isso:

Um código bem tosco, não use isso como exemplo pra fazer o seu código de spawn =P
Se eu jogar esse código em um GameObject vazio, indicar pra ele qual o prefab, qual o ponto de spawn, quando eu der Play imediatamente ele vai criar 30 copias do prefab no ponto de spawn. Ai, minhas torres overpower vão matar os 30 bichos e é bem fácil saber o que vai acontecer depois.... absolutamente nada. A cada bicho morto, eu não to avisando pra minha classe WaveHandler que um bicho morreu (inimigosVivos - 1), então a checagem no update nunca vai dar 0. Pra minha classe ficar sabendo quando um inimigo morreu...você pode fazer de várias formas... criar um Array, colocar os inimigos nele e ficar vendo se eles ainda estão vivos... ou criar uma referencia a essa classe dentro de cada inimigo pra quando eles morrerem eles mesmos dizerem inimigosVivos --... pode criar uma classe estática que mantem controle dos inimigos vivos (é o que eu fazia até então)... OU, você pode simplesmente tornar sua classe um Singleton!

Pra isso, o código ficaria assim:

A implementação que eu mais gosto. Existem outros métodos de fazer um Singleton, mas esse é o mais simples.

Mas o que esse pedaço de código faz na prática? É muito simples... agora, de qualquer outro script do jogo, qualquer um, eu posso escrever isso aqui:


Entenderam a mágica? Agora eu não preciso de um array com todos os bichos pra ficar vendo se alguém morreu... não preciso de outra classe... não preciso criar uma referencia... arrastar no inspetor... basta eu dizer WaveHandler.Instance.qquerCoisaQueEuQuiser e eu estarei acessando o meu script WaveHandler, e a única coisa que eu preciso garantir é que esse script só exista UMA VEZ na cena.

E é basicamente isso! Esse é um tutorialzinho bem básico, assim que eu tiver um tempo de novo vou fazer um tutorial falando de Delegates e Eventos e como eles podem facilitar sua vida.

Espero que seja útil pra alguém, e qualquer dúvida deixa um comentário ou manda um e-mail pra gente!
Abraços,

12 comentários:

  1. Cara, acho que diz dizer único objeto e não única classe.

    ResponderExcluir
  2. Bem legal o tuto. Só queria comentar um fato específico com relação a Unity. Se vc colocar a inicialização da variável instance no Awake, permite que vc coloque esse script em diversos GameObjects quebrando todo o padrão. Recomendo nesse caso fazer com que a própria classe se instancie e controle seu próprio GameObject. Desse modo vc pode garantir que ele é o único da cena. Segue o exemplo pra ilustrar o que quero dizer: https://gist.github.com/leoviathan/5427987

    ResponderExcluir
    Respostas
    1. Oi Leoviathan,

      Eu na verdade comecei usando Singleton dessa forma que você passou, e apesar de sair mais seguro, garantindo que voce realmente só vai ter um script desse sem nem mesmo ter que arrastar pra cena em nenhum GO... o único motivo de eu ter deixado de usar esse método é que a função de busca, GameObject.FindObjetcsOfType é um tanto pesada, gera um overhead que de preferencia é melhor ficar durante o load do que em runtime, ai eu pessoalmente prefiro arrastar o script pra um GameObject chamado RequiredScripts ou sei la, e deixo ele la, no caso o IF é só rpa garantir que SE outro objeto for usar esse script no Awake dele, e por acaso ele acabar sendo chamado mais rapido que o proprio awake dos script, ele nao retorne null.

      Mas valeu pela dica, com certeza é um método mais... correto por assim dizer.

      Abs!

      Excluir
    2. E ae!

      O GameObject.FindObjectsOfType só é chamado uma única vez quando instance == null e se algum script chamar seu singleton no Awake ou no Start, ele será chamado no load e não em runtime. Também sempre há a opção de fazer um: DontDestroyOnLoad(_instance.gameObject); caso vc queira que esse Find seja apenas chamado uma única vez na vida. Assim dá pra garantir que a instância é única no projeto todo. Claro que você não vai querer carregar certos singletons nos menus e tudo mais, aí vai de uma questão de uso de memória VS usabilidade e flexibilidade do padrão. Dá pra variar as implementações segundo o tipo de singleton. Eu particularmente não uso DontDestroyOnLoad neles já que o Find é chamado só uma vez e boa.
      Na maioria dos projetos, quem monta a cena não sou eu, é um level designer. Para eles é muito fácil deixar passar essas restrições e acabar colocando o prefab mais de uma vez na cena (ainda mais se for algo como um spawner). Acho que é uma oportunidade muito grande pra criar bugs por conta do padrão não estar funcionando como deveria.

      Mas boa discussão! Continue com os bons posts de dev!
      Abraços!

      Excluir
    3. E ae Leo!

      Realmente, se outro script chamar no Awake ou Start, o resultado acaba sendo o mesmo mesmo. Agora entendi sua questão, é que eu estou tão acostumado com o nosso fluxo de trabalho por aqui (que praticamente só eu mexo no Unity) que nem levei em consideração que outra pessoa pudesse ficar responsável por isso =P.

      Agora, na verdade você me deixou com uma dúvida agora. Por exemplo, eu uso um script que é um InterfaceHandler da vida. Nele eu crio várias variaveis publicas e faço a relação de cada elemento da interface através do inspetor. Isso é na verdade por pura comodidade, pra não ter que procurar por nome ou tag cada objeto e ai acaba ficando uma restrição chata na hora de montar a cena... "putz coloquei SkillsHolder e nao SkillHolder no nome, deu pau por causa disso"... e esse meu script é um singleton... nesse caso é meio inevitável que ele esteja presente na cena "manualmente"... como que você busca esse tipo de objeto na cena, por tag mesmo?

      Valeu pelos pontos também, e valeu!
      Abraços!

      Excluir
    4. E ae Allan!

      Humm fiquei com mais dúvidas com relação ao seu caso de uso. Na atribuição da variável pública vc está colocando a referência por tipo ou por nome? Se for por nome, vc provavelmente está usando um SendMessage que está dando problemas? Em referência por tipo não há esse problema pois em tempo de compilação vc sabe exatamente todos os métodos da classe referenciada.

      Se for um problema de herança nesse caso: SkillsHolder e SkillHolder são filhos de uma classe pai e essa classe pai é o membro público de seu singleton, aí vc tem q garantir que a função chamada pelo seu singleton possui override nos filhos marcando aquela função como abstract.

      Outro ponto que pensei lendo seu caso foi no problema do singleton ter que saber muito da cena pra funcionar. Esse é o caso em que ele possui muitas variáveis atreladas à cena em si e precisa de alguma forma buscar todo mundo. Eu particularmente acho que cabe uma revisão nesse caso se realmente seu objeto precisa ser um singleton ou ele está sendo utilizado dessa forma apenas pela comodidade da chamada estática de métodos. É muito provável que se esse for o caso, a comodidade do uso está induzindo o uso do padrão. No seu caso específico do post mesmo, um spawner não precisa ser singleton. Na maioria dos projetos que eu trabalho eu tenho bem poucas coisas que realmente precisam ser únicas. Uma classe que sempre uso como singleton é um controlador de input mouse/toque. Essa classe lida com touch e mouse e repassa o input para os objetos em cena de forma abstrata. Aí sim eu quero que esse objeto seja único no projeto todo e que ele se comunique de forma anônima com os objetos. Nesse caso eu também tenho associado a ele uma interface de comunicação mesmo. Vira um command pattern no final das contas ;)

      Gostei da discussão! Se quiser, me add no skype pra trocar umas idéias: euleoo

      Abraços!

      Excluir
    5. Eae Allan e Leo!

      Gostei muito do post, e principalmente da discussão de vocês nos comentários, sobre as diferentes formas de implementação do singleton e de referencias.

      Trabalho com Unity a uns 6 meses e não tinha pensado em utilizar padrões de projeto como o singleton na mesma, por mais que já tinha estudado o singleton em c++ na faculdade, gostaria de saber sobre quais outros padrões que vocês utilizam e como vllw.

      Gostaria de manter contato também este é o meu skype pedrobacchini.

      Excluir
    6. Valeu cara!

      A verdade é que eu sou noob qdo se trata de padrões e técnicas convencionais de programação e tal, por isso que vira e mexe encontro coisas como o singleton que por mais simples estúpido que seja, ajudam pra burro e eu nem fazia ideia que existiam.

      Outro tópico bem importante que eu cheguei a escrever um post bem grande mas ainda não publiquei com medo de estar falando muita merda hahah, são delegates e eventos... coisas novas no meu mundo mas ta loco, facilitam a vida d++++... eu vou ver se acabo o post agora q to um pouco mais "vivido" com eles, mas se quiser bater um papo me adiciona no skype ou facebook msm, allansmithfhs (eu acho =P) ou Allan Michael Smith no facebook (não sei se tenho você la).

      Abs!

      Excluir
  3. Este comentário foi removido pelo autor.

    ResponderExcluir
  4. Este comentário foi removido por um administrador do blog.

    ResponderExcluir
  5. Allan,
    muito bom o tutorial, primeira vez que realmente consigo entender singleton!!

    vlw
    Abraço

    ResponderExcluir