Prototipar um jogo para celular, por onde começar e como fazê-lo. Parte 2

Para quem perdeu a primeira parte - Parte 1
Próxima parte - parte 3

Se alguém estiver interessado em ler sobre o agregador usado pelo evento, aqui está você, mas isso não é necessário.

Então, começamos a coletar tudo em uma pilha




Foguete:

Classe de foguete base
using DG.Tweening; using GlobalEventAggregator; using UnityEngine; namespace PlayerRocket { public class Rocket : PlayerRocketBase { [SerializeField] private float pathorrectionTime = 10; private Vector3 movingUp = new Vector3(0, 1, 0); protected override void StartEventReact(ButtonStartPressed buttonStartPressed) { transform.SetParent(null); rocketState = RocketState.MOVE; transform.DORotate(Vector3.zero, pathorrectionTime); } protected override void Start() { base.Start(); EventAggregator.Invoke(new RegisterUser { playerHelper = this }); if (rocketState == RocketState.WAITFORSTART) return; RocketBehaviour(); } private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } } } 


O que precisamos para um foguete decolar? No espaço de jogo, precisamos de um planeta condicional com o qual começamos, um botão de partida e um foguete. O que um foguete deve ser capaz de fazer?

  1. Aguarde o início
  2. Voar
  3. Seja afetado por modificadores
  4. Para parar

Ou seja, temos um comportamento / estado diferente do foguete, dependendo do estado atual, o foguete deve fornecer um comportamento diferente. Na programação, somos constantemente confrontados com uma situação em que um objeto pode ter muitos comportamentos radicalmente diferentes.

Para comportamento complexo de objetos - é melhor usar padrões comportamentais, por exemplo, um padrão de estado. Para os mais simples, os programadores iniciantes costumam usar muito, se for o caso. Eu recomendo usar switch e enum. Em primeiro lugar, essa é uma divisão mais clara da lógica em estágios específicos; graças a isso, saberemos exatamente em que estado estamos agora e o que está acontecendo, há menos oportunidades de transformar o código em um macarrão de dezenas de exceções.

Como funciona:

Primeiro começamos enum com os estados que precisamos:

  public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } 

Na classe pai, temos um campo -
 protected RocketState rocketState; 

Por padrão, o primeiro valor é atribuído a ele. O próprio Enum define os valores padrão, mas para dados que podem ser alterados de cima ou configurados por designers de jogos, eu defino manualmente os valores, para quê? Para poder agregar outro valor ao inam em qualquer lugar e não violar os dados armazenados. Também aconselho a estudar flag enum.

Seguinte:

Definimos o próprio comportamento em uma atualização, dependendo do valor do campo rocketState

  private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } 

Vou decifrar o que está acontecendo:

  1. Quando esperamos, simplesmente giramos o foguete em direção ao cursor do mouse, definindo assim a trajetória inicial
  2. O segundo estado - voamos, aceleramos o foguete na direção certa e atualizamos o modelo de modificadores para a aparência de objetos que afetam a trajetória
  3. O terceiro estado é quando a equipe chega até nós para parar, aqui trabalhamos tudo para que o foguete pare e se traduza no estado - paramos completamente.
  4. O último estado é que não estamos fazendo nada.

A conveniência do padrão atual - tudo é facilmente expansível e ajustável, mas há uma coisa, mas um elo fraco - é quando podemos ter um estado que combina vários outros estados. Aqui, ou um sinalizador inam, com uma complicação de processamento, ou já mude para padrões mais "pesados".

Nós descobrimos o foguete. O próximo passo é um objeto simples, mas divertido - o botão Iniciar.

Botão Iniciar


A funcionalidade a seguir é necessária para ela clicar, ela notificou que eles clicaram nela.

Classe do botão Iniciar
 using UnityEngine; using UnityEngine.EventSystems; public class StartButton : MonoBehaviour, IPointerDownHandler { private bool isTriggered; private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } } public struct ButtonStartPressed { } 


De acordo com o design do jogo, este é um objeto 3D no palco, o botão deve ser integrado ao design do planeta inicial. Bem, ok, há uma nuance - como rastrear um clique em um objeto em uma cena?

Se pesquisarmos no Google, encontraremos vários métodos OnMouse, entre os quais haverá um clique. Parece uma escolha fácil, mas é muito ruim, começando com o fato de que muitas vezes funciona torto (existem muitas nuances para rastrear cliques), "querido", terminando com o fato de que ele não fornece toneladas de pães que estão no UnityEngine.EventSystems.

No final, eu recomendo usar o UnityEngine.EventSystems e as interfaces IPointerDownHandler, IPointerClickHandler. Nos métodos deles, percebemos a reação à pressão, mas há várias nuances.

  1. Um EventSystem deve estar presente na cena; esse é um objeto / classe / componente da unidade, geralmente criado quando criamos a tela para a interface, mas você também pode criá-lo.
  2. O RayCaster de física deve estar presente na câmera (isto é para 3D, para gráficos 2D, existe um racaster separado)
  3. Deve haver um colisor na instalação

No projeto, fica assim:



Agora o objeto rastreia o clique e esse método é chamado:

 public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } 

O que está acontecendo aqui:

Temos um campo booleano no qual rastreamos se o botão foi pressionado ou não (isso é proteção contra pressionamentos repetidos para que não tenhamos um script inicial executado a cada vez).

Em seguida, chamamos o evento - o botão é pressionado, ao qual a classe de foguete está inscrita, e coloca o foguete em um estado de movimento.

Avançando um pouco - por que está aqui e ali para eventos? Essa é uma programação orientada a eventos: primeiro, um modelo de evento é mais barato que o processamento contínuo de dados para descobrir suas alterações. Em segundo lugar, essa é a conexão mais fraca, não precisamos saber no foguete que existe um botão, que alguém o pressionou e assim por diante, apenas sabemos que há um evento para começar, recebemos e estamos agindo. Além disso, este evento é interessante não apenas para o foguete, por exemplo, um painel com modificadores é assinado para o mesmo evento, está oculto no início do foguete. Além disso, esse evento pode ser de interesse para o controlador de entrada - e a entrada do usuário não pode ser processada ou processada de maneira diferente após o lançamento do foguete.

Por que muitos programadores não gostam do paradigma de eventos? Como uma tonelada de eventos e inscrições para esses eventos transformam facilmente o código em macarrão, no qual não está claro por onde começar e se terminará em algum lugar, sem mencionar o fato de que você também precisa monitorar seu cancelamento de assinatura / assinatura e manter todos os objetos ativos.

E é por isso que, para a implementação de eventos, uso meu agregador de eventos, que na verdade não transmite eventos, mas os contêineres de dados através de eventos e classes assinam os dados que lhes interessam. Além disso, o próprio agregador monitora objetos ativos e lança objetos mortos para fora dos assinantes. Graças à transferência do contêiner, a injeção também é possível; você pode passar um link para a classe de seu interesse. Pelo contêiner, você pode acompanhar facilmente quem processa e envia esses dados. Prototipar é uma coisa ótima.

Rotação de foguetes para determinar o caminho inicial



De acordo com o design do jogo, o foguete deve poder girar ao redor do planeta para determinar a trajetória inicial, mas não mais do que um certo ângulo. A rotação é realizada pelo toque - o foguete simplesmente segue o dedo e é sempre direcionado para o local onde cutucamos a tela. A propósito, é apenas o protótipo que tornou possível determinar que esse é um ponto fraco e que existem muitos episódios associados ao gerenciamento que limitam essa funcionalidade.

Mas em ordem:

  1. Precisamos que o foguete gire em relação ao planeta na direção do carrinho de mão
  2. Precisamos prender o ângulo de rotação

Quanto à rotação em relação ao planeta - você pode girar astutamente em torno do eixo e calcular o eixo de rotação, ou você pode simplesmente criar um objeto com um manequim centrado dentro do planeta, mover o foguete para lá e girar silenciosamente o manequim em torno do eixo Z, o manequim terá uma classe que determinará o comportamento do objeto. O foguete irá girar com ele. O objeto que eu chamei de RocketHolder. Nós descobrimos isso.

Agora, sobre as restrições de virar e girar na direção do carrinho de mão:

classe RocketHolder
 using UnityEngine; public class RocketHolder : MonoBehaviour { [SerializeField] private float clampAngle = 45; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> obj) => obj.inject(this)); } private float ClampAngle(float angle, float from, float to) { if (angle < 0f) angle = 360 + angle; if (angle > 180f) return Mathf.Max(angle, 360 + from); return Mathf.Min(angle, to); } private Vector3 ClampRotationVectorZ (Vector3 rotation ) { return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle)); } public void RotateHolder(Vector3 targetPosition) { var diff = targetPosition - transform.position; diff.Normalize(); float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90); transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles); } } 


Apesar do fato de o jogo ser teoricamente 3D, mas toda a lógica e a jogabilidade são na verdade 2D. E nós apenas precisamos apertar o foguete em torno do eixo Z na direção do local da pressão. No final do método, fixamos o grau de rotação pelo valor especificado no inspetor. No método Awake, você pode ver a implementação mais correta de uma injeção de classe por meio de um agregador.

Controlador de entrada


Uma das classes mais importantes, é ele quem coleta e processa o comportamento do usuário. Pressionando teclas de atalho, botões do gamepad, teclados, etc. Eu tenho uma entrada bastante simples no protótipo, na verdade você precisa saber apenas três coisas:

  1. Existe um clique e suas coordenadas
  2. Existe um deslize vertical e quanto deslizar
  3. Opero com interface / modificadores

classe InputController
 using System; using UnityEngine; using UnityEngine.EventSystems; public class InputController : MonoBehaviour { public const float DirectionRange = 10; private Vector3 clickedPosition; [Header("     ")] [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f; [Header("  ")] [SerializeField] private float speedOfVerticalScroll = 2; public ReactiveValue<float> ReactiveVerticalScroll { get; private set; } public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition); public bool OnTouch { get; private set; } public bool OnDrag { get; private set; } // Start is called before the first frame update private void Awake() { ReactiveVerticalScroll = new ReactiveValue<float>(); GlobalEventAggregator.EventAggregator.AddListener(this, (ImOnDragEvent obj) => OnDrag = obj.IsDragging); GlobalEventAggregator.EventAggregator.AddListener<InjectEvent<InputController>>(this, InjectReact); } private void InjectReact(InjectEvent<InputController> obj) { obj.inject(this); } private void OnEnable() { GlobalEventAggregator.EventAggregator.Invoke(this); } void Start() { GlobalEventAggregator.EventAggregator.Invoke(this); } private void MouseInput() { if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.gameObject.layer == 5) return; if (Input.GetKeyDown(KeyCode.Mouse0)) clickedPosition = Input.mousePosition; if (Input.GetKey(KeyCode.Mouse0)) { if (OnDrag) return; VerticalMove(); OnTouch = true; return; } OnTouch = false; ReactiveVerticalScroll.CurrentValue = 0; } private void VerticalMove() { if ( Math.Abs(Input.mousePosition.y-clickedPosition.y) < afterThisDistanceWeGonnaDoSwipe) return; var distance = clickedPosition.y + Input.mousePosition.y * speedOfVerticalScroll; if (Input.mousePosition.y > clickedPosition.y) ReactiveVerticalScroll.CurrentValue = distance; else if (Input.mousePosition.y < clickedPosition.y) ReactiveVerticalScroll.CurrentValue = -distance; else ReactiveVerticalScroll.CurrentValue = 0; } // Update is called once per frame void Update() { MouseInput(); } } } 


Está tudo na testa e sem problemas, interessante pode ser a implementação primitiva do proprietário reativo - quando eu estava apenas começando a programar, sempre foi interessante descobrir como os dados foram alterados, sem ventilação constante dos dados. Bem, é isso.

É assim:

classe ReactiveValue
 public class ReactiveValue<T> where T: struct { private T currentState; public Action<T> OnChange; public T CurrentValue { get => currentState; set { if (value.Equals(currentState)) return; else { currentState = value; OnChange?.Invoke(currentState); } } } } 


Assinamos o OnChange e agitamos se apenas o valor tiver sido alterado.

Com relação à prototipagem e arquitetura - as dicas são as mesmas, somente propriedades e métodos públicos, todos os dados devem ser alterados apenas localmente. Qualquer processamento e cálculos - adicione de acordo com métodos separados. Como resultado, você sempre pode alterar a implementação / cálculos, e isso não afetará os usuários externos da classe. Por enquanto, é a terceira parte final - sobre modificadores e interface (arrastar e soltar). E pretendo colocar o projeto no git para que eu possa ver / sentir. Se você tiver dúvidas sobre prototipagem, pergunte, tentarei responder com clareza.

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


All Articles