O que (não) você precisa saber para criar jogos no Unity



O Unity é um mecanismo de jogo com um limite de entrada longe de zero (comparando com o mesmo Game Maker Studio) e, neste artigo, mostrarei quais problemas encontrei ao começar a estudá-lo e quais soluções encontrei. Descreverei esses momentos com o exemplo do meu jogo de quebra-cabeça 2D para Android (que espero que seja lançado em breve no Play Market).

Eu não finjo ser verdade, e não exorto que você repita o processo, se você souber o melhor caminho, apenas mostrarei como fazê-lo, e talvez alguém que esteja começando a se familiarizar com o Unity crie sua própria obra-prima independente com menos trabalho.

Sou engenheiro de projeto de usina, mas sempre me interessei por codificação e conheço algumas linguagens de programação. Portanto, concordamos que, para criar jogos no Unity:
  • Você precisa conhecer um pouco de C # ou JavaScript (pelo menos a sintaxe em forma de C).

Tudo o que será escrito abaixo não é um tutorial do Unity, do qual foram criados suficientes na rede sem mim. A seguir, serão coletados os momentos difíceis que podem ocorrer ao criar seu primeiro projeto no Unity.

Vale a pena advertir que os scripts fornecidos omitem a maior parte da lógica do jogo (representando "segredos comerciais"), mas seu desempenho como exemplos foi verificado.

Problema Um - Orientação



Trava de orientação
A primeira dificuldade que surgiu em mim foi não prestar a devida atenção à otimização da interface visual para orientação da tela. A solução é a mais simples - se você não precisar alterar a orientação da tela para jogar, é melhor bloqueá-la. Não há necessidade de flexibilidade excessiva, você está escrevendo um jogo independente, não um projeto do outro lado de um milhão de dólares. Por que toneladas de transições condicionais e mudança de âncoras se o jogo parece melhor em Retrato (por exemplo). Você pode bloquear a orientação da tela aqui:
Editar> Configurações do projeto> Player


Permissões diferentes
Também é importante testar a interface visual em diferentes resoluções na orientação selecionada e, ao testar, não se esqueça da existência de dispositivos com proporções de 4: 3 (bom ou 3: 4), para que possamos adicionar com segurança 768x1024 (ou 1024x768).

Melhor posicionamento
É melhor usar o Rect Transform para ajustar o posicionamento e a escala dos objetos do jogo.


Problema Dois - COMUNICAÇÃO


Eu tive um problema semelhante devido ao fato de ter conhecido o desenvolvedor do jogo pelo Game Maker Studio, onde o script é uma parte completa do objeto do jogo e ele tem acesso total a todos os componentes do objeto imediatamente. O Unity possui scripts comuns, e apenas instâncias deles são adicionados ao objeto. Falando de maneira simplista e figurativa, o script não sabe diretamente em qual objeto está executando no momento. Portanto, ao escrever scripts, você deve levar em consideração a inicialização das interfaces para trabalhar com os componentes de um objeto ou com os componentes de outros objetos.

Nós treinamos em gatos
No meu jogo, há um objeto GameField; no palco, há apenas uma instância, há também um script com o mesmo nome. O objeto é responsável por exibir a pontuação do jogo e por reproduzir todo o som do jogo, portanto, na minha opinião, é mais econômico para a memória (em geral, o jogo possui apenas três fontes de áudio - uma música de fundo e dois outros efeitos sonoros). O script resolve os problemas de armazenamento de uma conta de jogo, escolhendo o AudioClip para reproduzir som e para alguma lógica do jogo.

Vamos nos debruçar sobre o som com mais detalhes, pois este exemplo mostra facilmente a interação do script com os componentes do objeto.

Naturalmente, o objeto deve ter o próprio script GameField.cs e o componente AudioSource, no meu caso, dois inteiros (mais tarde ficará claro o porquê).

Como mencionado anteriormente, o script "não sabe" que o objeto possui um componente AudioSource, portanto, declaramos e inicializamos a interface (por enquanto, consideramos que o AudioSource é apenas um):
private AudioSource Sound; void Start(){ Sound = GetComponent<AudioSource> (); } 

O método GetComponent <component_type> () retornará o primeiro componente do tipo especificado do objeto.

Além do AudioSource, você precisará de vários AudioClip:
 [Header ("Audio clips")] [SerializeField] private AudioClip OnStart; [SerializeField] private AudioClip OnEfScore; [SerializeField] private AudioClip OnHighScore; [SerializeField] private AudioClip OnMainTimer; [SerializeField] private AudioClip OnBubbMarker; [SerializeField] private AudioClip OnScoreUp; 

A seguir, os comandos entre colchetes são necessários para o Inspector`a, mais detalhes aqui .



Agora, o script no Inspector possui novos campos nos quais arrastamos os sons necessários.


Em seguida, crie um método SoundPlay no script que recebe o AudioClip:
 public void PlaySound(AudioClip Clip = null){ Sound.clip = Clip; Sound.Play (); } 

Para reproduzir som no jogo, chamamos no momento certo esse método com o clipe.

Há um ponto negativo menos significativo dessa abordagem, apenas um som pode ser reproduzido por vez, mas durante o jogo pode ser necessário reproduzir dois ou mais sons, com exceção da música de fundo tocando constantemente.

Para evitar a cacofonia, eu recomendo evitar a possibilidade de reprodução simultânea de mais de 4-5 sons (de preferência um máximo de 2-3). Quero dizer sons breves de primeira ordem do jogo (salto, moeda, tiro do jogador ...); para o ruído de fundo, é melhor criar sua própria fonte som no objeto que produz esse ruído (se você precisar de som 2d-3d) ou em um objeto responsável por todo o ruído de fundo (se “volume” não for necessário).

No meu jogo, não há necessidade de reproduzir simultaneamente mais de dois clipes de áudio. Para uma reprodução garantida dos dois sons hipotéticos, adicionei dois AudioSource ao objeto GameField. Para determinar os componentes no script, usamos o método
 GetComponents<_>() 

que retorna uma matriz de todos os componentes do tipo especificado do objeto.

O código ficará assim:
 private AudioSource[] Sound; //    void Start(){ Sound = GetComponents<AudioSource> (); //  GetComponents } 

A maioria das alterações afetará o método PlaySound. Eu vejo duas versões desse método: “universal” (para qualquer número de AudioSource em um objeto) e “desajeitado” (para 2-3 AudioSource, não o mais elegante, mas com menos recursos).

A opção "desajeitada" para dois AudioSource (eu usei)
 private void PlaySound(AudioClip Clip = null){ if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } 

Você pode estender para três ou mais AudioSource, mas o número de condições consumirá toda a economia de desempenho.

Opção "universal"
 private void PlaySound(AudioClip Clip = null){ foreach (AudioSource _Sound in Sound) { if (!_Sound.isPlaying) { _Sound.clip = Clip; _Sound.Play (); break; } } } 


Acesso a um componente externo
No campo de jogo, há várias instâncias da pré-fabricada Fishka, como um chip de jogo. É construído assim:
  • Objeto pai com seu SpriteRenderer;
    • Objetos filho com seu SpriteRenderer.

Os objetos filhos são responsáveis ​​por desenhar o corpo do chip, sua cor, elementos mutáveis ​​adicionais. O pai desenha uma borda de marcador ao redor do chip (o chip ativo deve ser destacado no jogo). O script está apenas no objeto pai. Portanto, para gerenciar sprites filhos, o script pai precisa especificar esses sprites. Organizei assim - no script, criei interfaces para acessar os filhos SpriteRenderer:
 [Header ("Graphic objects")] public SpriteRenderer Marker; [SerializeField] private SpriteRenderer Base; [Space] [SerializeField] private SpriteRenderer Center_Red; [SerializeField] private SpriteRenderer Center_Green; [SerializeField] private SpriteRenderer Center_Blue; 

Agora, o script no Inspector possui campos adicionais:


Arrastar e soltar filhos nos campos correspondentes nos dá acesso a eles no script.

Exemplo de uso:
 void OnMouseDown(){ //        Marker.enabled = !Marker.enabled; } 


Chamando o script de outra pessoa
Além de manipular componentes externos, você também pode acessar o script de um objeto de terceiros, trabalhar com suas variáveis ​​Públicas, métodos e subclasses.

Vou dar um exemplo do já conhecido objeto GameField.

O script GameField possui o método público FishkiMarkerDisabled (), necessário para "remover" um marcador de todos os chips no campo e é usado no processo de definir um marcador ao clicar em um chip, pois só pode haver um ativo.

No script Fishka.cs, ​​o SpriteRenderer Marker é público, ou seja, pode ser acessado de outro script. Para fazer isso, adicione a declaração e a inicialização de interfaces para todas as instâncias da classe Fishka no script GameField.cs (ao criar um script, a classe com o mesmo nome é criada nele), semelhante à maneira como é feita para vários AudioSource:
 private Fishka[] Fishki; void Start(){ Fishki = GameObject.FindObjectsOfType (typeof(Fishka)) as Fishka[]; } public void FishkiMarkerDisabled(){ foreach (Fishka _Fishka in Fishki) { _Fishka .Marker.enabled = false; } } 

No script Fishka.cs, ​​adicione a declaração e a inicialização da interface da instância da classe GameField e, quando clicarmos no objeto, chamaremos o método FishkiMarkerDisabled () dessa classe:
 private GameField gf; void Start(){ gf = GameObject.FindObjectOfType (typeof(GameField)) as GameField; } void OnMouseDown(){ gf.FishkiMarkerDisabled(); Marker.enabled = !Marker.enabled; } 

Assim, você pode interagir entre scripts (ou melhor, classes) de diferentes objetos.


Problema Três - MANTENADORES


Detentor de conta
Assim que algo parecido com uma conta aparece no jogo, o problema imediato é o armazenamento, tanto durante o jogo quanto fora dele, também quero manter um registro para incentivar o jogador a superá-lo.

Não considerarei opções quando todo o jogo (menu, jogo, retirada de conta) for construído em uma cena, porque, em primeiro lugar, essa não é a melhor maneira de criar o primeiro projeto e, em segundo lugar, na minha opinião, a cena de carregamento inicial deve ser . Portanto, concordamos que há quatro cenas no projeto:
  1. loader - uma cena na qual o objeto de música de fundo é inicializado (mais será mais tarde) e carregando as configurações do salvamento;
  2. menu - uma cena com um menu;
  3. cena do jogo;
  4. score - a cena da pontuação, registro e placar.


Nota: A ordem de carregamento da cena é definida em Arquivo> Configurações da compilação.

Os pontos acumulados durante o jogo são armazenados na variável Score da classe GameField. Para ter acesso aos dados ao ir para a cena de pontuações, crie uma classe estática pública ScoreHolder, na qual declaramos uma variável para armazenar o valor e uma propriedade para obter e definir o valor dessa variável (o método foi espionado por apocatastas ):
 using UnityEngine; public static class ScoreHolder{ private static int _Score = 0; public static int Score { get{ return _Score; } set{ _Score = value; } } } 

Uma classe estática pública não precisa ser adicionada a nenhum objeto; ela está disponível imediatamente em qualquer cena de qualquer script.

Exemplo de uso na classe GameField nas pontuações do método de transição de cena:
 using UnityEngine.SceneManagement; public class GameField : MonoBehaviour { private int Score = 0; //     ,         Scores void GotoScores(){ ScoreHolder.Score = Score; //   ScoreHolder.Score   SceneManager.LoadScene (“scores”); } } 

Da mesma forma, você pode adicionar uma conta de registro ao ScoreHolder durante o jogo, mas ela não será salva na saída.

Guarda-redes
Considere o exemplo de salvar o valor da variável booleana SoundEffectsMute, dependendo do estado em que o jogo tem ou não efeitos sonoros.

A variável em si é armazenada na classe estática pública SettingsHolder:
 using UnityEngine; public static class SettingsHolder{ private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; } } } 

A classe é semelhante ao ScoreHolder, você pode até combiná-los em um, mas, na minha opinião, isso é falta de educação.

Como você pode ver no script, por padrão _SoundEffectsMute é declarado falso, portanto, toda vez que o jogo é iniciado, SettingsHolder.SoundEffectsMute retornará false, independentemente de o usuário o ter alterado antes ou não (é alterado usando o botão na cena do menu).

Salvando Variáveis
O melhor para um aplicativo Android será usar o método PlayerPrefs.SetInt para salvar (para mais detalhes, consulte a documentação oficial ). Existem duas opções para manter o valor de SettingsHolder.SoundEffectsMute no PlayerPrefs, vamos chamá-los de "simples" e "elegantes".

A maneira "simples" (para mim assim) está no método OnMouseDown () da classe do botão acima mencionado. O valor armazenado é carregado na mesma classe, mas no método Start ():
 using UnityEngine; public class ButtonSoundMute : MonoBehaviour { void Start(){ //    ,  PlayerPrefs    bool switch (PlayerPrefs.GetInt ("SoundEffectsMute")) { case 0: SettingsHolder.SoundEffectsMute = false; break; case 1: SettingsHolder.SoundEffectsMute = true; break; default: //    default SettingsHolder.SoundEffectsMute = true; break; } } void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; //    ,  PlayerPrefs    bool if (SettingsHolder.SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } 


O método "elegante", na minha opinião, não é o mais correto, porque complicar a manutenção do código, mas há algo nele, e não posso deixar de compartilhá-lo. Um recurso desse método é que o configurador da propriedade SettingsHolder.SoundEffectsMute é chamado em um momento que não requer alto desempenho e pode ser carregado (oh, horror) usando PlayerPrefs (leitura / gravação em um arquivo). Altere a classe estática pública SettingsHolder:

 using UnityEngine; public static class SettingsHolder { private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; if (_SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } } 

O método OnMouseDown da classe ButtonSoundMute será simplificado para:
 void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; } 


Não vale a pena carregar o getter com a leitura de um arquivo, pois ele está envolvido em um processo crítico de desempenho - no método PlaySound () da classe GameField:
 private void PlaySound(AudioClip Clip = null){ if (!SettingsHolder.SoundEffectsMute) { //      “”  (. ) if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } } 


Da maneira acima, você pode organizar o armazenamento no jogo de quaisquer variáveis.


Quinto Problema - UM POR TODOS


Essa música será eterna
Cedo ou tarde, todos enfrentam esse problema, e eu não fui exceção. Conforme planejado, a música de fundo começa a tocar mesmo na cena do menu e, se não estiver desativada, toca o menu, o jogo e as pontuações nas cenas sem interrupção. Mas se o objeto "tocando" a música de fundo estiver instalada na cena do menu, quando você for para a cena do jogo, ela será destruída e o som desaparecerá. Se você colocar o mesmo objeto na cena do jogo, depois da transição, a música será reproduzida primeiro. A solução acabou sendo o uso do método DontDestroyOnLoad (Object target) colocado no método Start () da classe cuja instância de script o objeto de música possui. Para fazer isso, crie o script DontDestroyThis.cs:
 using UnityEngine; public class DontDestroyThis: MonoBehaviour { void Start(){ DontDestroyOnLoad(this.gameObject); } } 

Para que tudo funcione, o objeto “musical” deve ser raiz (no mesmo nível hierárquico da câmera principal).

Por que música de fundo no carregador
A captura de tela mostra que o objeto “musical” não está localizado na cena do menu, mas na cena do carregador. Essa é uma medida causada pelo fato de que a cena do menu pode ser carregada mais de uma vez (após a cena das partituras, a transição para a cena do menu) e, cada vez que é carregada, outro objeto “musical” será criado e o antigo não será excluído. Isso pode ser feito como no exemplo da documentação oficial , mas eu decidi aproveitar o fato de que a cena do carregador é garantida para carregar apenas uma vez.

Com isso, os principais problemas que encontrei ao desenvolver meu primeiro jogo no Unity, antes de fazer o upload para o Play Market (ainda não registrei uma conta de desenvolvedor), foram encerrados com êxito.

PS
Se a informação foi útil, você pode apoiar o autor e ele finalmente registrará uma conta de desenvolvedor do Android.

Source: https://habr.com/ru/post/pt458914/


All Articles