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

Aconteceu que eu costumo fazer protótipos (tanto para o trabalho quanto para projetos pessoais)
e quero compartilhar minha experiência. Gostaria de escrever um artigo que seria interessante de ler, e antes de tudo, me pergunto por que uma pessoa é guiada quando toma essa ou aquela decisão na implementação do projeto, como ele inicia o projeto, geralmente é o mais difícil de começar.

Um novo exemplo que quero considerar com você é o conceito de um quebra-cabeça casual baseado em física. Às vezes, mencionarei coisas super óbvias para expandir o tópico para iniciantes.

A ideia tem esta aparência




Parte 2
Parte 3

Organizamos vários modificadores no campo de jogo, que alteram a direção do foguete, atraem, aceleram, repelem e assim por diante. A tarefa é abrir o caminho entre as estrelas para o próximo planeta que precisamos. Ganhar é considerado ao pousar / tocar nos traços do planeta. O campo de jogo é vertical, várias telas para cima, pressupõe-se que a trajetória possa ocupar o piso da tela para a esquerda / direita. Perder - se você perdeu o planeta de destino, colidiu com outro planeta, voou muito além da zona.

E espero que todos entendamos que, antes de iniciar o desenvolvimento e implementar nossos planos, devemos primeiro criar um protótipo - um produto muito rude e rápido que permita que você tente rapidamente a mecânica básica e tenha uma idéia da jogabilidade. Para que são feitos os protótipos? O processo de jogo em nossas cabeças e na prática são coisas muito diferentes, o que nos parece legal, para outros será um horror completo, e muitas vezes há momentos controversos no projeto - gerenciamento, regras de jogo, dinâmica de jogo etc. e assim por diante. Será extremamente estúpido pular o estágio do protótipo, trabalhar com a arquitetura, entregar os gráficos, ajustar os níveis e finalmente descobrir que o jogo é uma merda. Da vida - em um jogo, havia minijogos, cerca de 10 peças, depois da criação de protótipos, eles eram terrivelmente entediantes, metade foi jogada fora, metade foi refeita.

Dica - além da mecânica básica, isole lugares controversos, anote as expectativas específicas do protótipo. Isso é feito para recarregar em momentos difíceis. Por exemplo, uma das tarefas que enfrentou esse protótipo foi o quão conveniente e compreensível o campo de jogo consiste em várias telas e eles precisam ser executados. Foi necessário implementar o plano A - svayp. Plano B - a capacidade de ampliar o campo de jogo (se necessário). Havia também várias opções para modificadores. A primeira ideia é visível na captura de tela - expomos o modificador e a direção de sua influência. Como resultado, os modificadores foram substituídos simplesmente por esferas que mudam a direção do foguete quando tocam na esfera. Decidimos que isso seria mais casual, sem trajetórias, etc.

A funcionalidade geral que implementamos:

  1. Você pode definir a trajetória inicial do foguete, limitando o grau de desvio da perpendicular (o foguete não pode ser girado para o lado em mais de um grau)
  2. Deveria haver um botão Iniciar, pelo qual enviamos o foguete na estrada
  3. Rolar a tela ao colocar o modificador (para iniciar)
  4. Movimento da câmera atrás do player (após o início)
  5. Painel de interface com o qual os modificadores de arrastar e soltar serão implementados em campo
  6. O protótipo deve ter dois modificadores - repulsão e aceleração
  7. Deve haver planetas quando você toca no que você morre
  8. Deve haver um planeta quando você toca e ganha

Arquitetura


Um ponto muito importante: em grandes desenvolvimentos, é geralmente aceito que o protótipo seja escrito da maneira mais horrível possível, mas que o projeto é simplesmente escrito do zero. Na dura realidade de muitos projetos, as pernas crescem do protótipo, o que é ruim para o grande desenvolvimento - arquitetura de curvas, código legado, dívida técnica, tempo extra para refatoração. Bem, e o desenvolvimento indie como um todo flui suavemente do protótipo para o beta. Por que sou? É necessário colocar a arquitetura em um protótipo, mesmo que seja primitivo, para que você não chore mais tarde ou core na frente dos colegas.

Antes de qualquer início, sempre repetimos - SÓLIDO, BEIJO, SECO, YAGNI. Até programadores experientes esquecem Kiss e Yagni).

Em qual arquitetura básica devo me ater

Há um GameController gameobject vazio na cena com a tag correspondente, componentes / mono-bikes estão pendurados nele, é melhor torná-lo uma pré-fabricada, em seguida, basta adicionar componentes conforme necessário à pré-fabricada:

  1. GameController - (responsável pelo estado do jogo, diretamente para a lógica (ganho, perda, quanta vida, etc.)
  2. InputController - tudo relacionado ao gerenciamento de jogadores, rastreamento de tachi, cliques, quem clicou, status de controle etc.
  3. TransformManager - nos jogos, você muitas vezes precisa saber quem está onde, vários dados relacionados à posição do jogador / inimigos. Por exemplo, se sobrevoamos um planeta, o jogador é derrotado, o controlador do jogo é responsável por isso, mas ele deve saber a posição do jogador de onde. O gerenciador de transformação é exatamente a essência que sabe sobre as coisas
  4. AudioController - aqui está claro, trata-se de sons
  5. InterfacesController - e aqui está claro, isso é sobre interface do usuário

A imagem geral emerge - para cada tarefa compreensível, você tem seu próprio controlador / entidade que resolve esses problemas, isso ajudará a evitar objetos godlayk, fornece uma idéia de onde cavar, dos controladores que enviamos dados, sempre podemos alterar a implementação de recebimento de dados. Campos públicos não são permitidos, fornecemos dados apenas por meio de propriedades / métodos públicos. Calculamos / alteramos dados localmente.

Às vezes acontece que o GameController é inflado, devido a várias lógicas e cálculos específicos. Se precisarmos processar dados - para isso, é melhor criar uma classe separada GameControllerModel e fazê-lo lá.

E assim o código começou


Classe base para foguetes
using GlobalEventAggregator; using UnityEngine; using UnityEngine.Assertions; namespace PlayerRocket { public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper { [SerializeField] protected RocketConfig config; protected Rigidbody rigidbody; protected InputController inputController; protected RocketHolder rocketHolder; protected RocketState rocketState; public Transform Transform => transform; public Rigidbody RigidbodyForForce => rigidbody; RocketState IPlayerHelper.RocketState => rocketState; protected ForceModel<IUseForces> forceModel; protected virtual void Awake() { Injections(); EventAggregator.AddListener<ButtonStartPressed>(this, StartEventReact); EventAggregator.AddListener<EndGameEvent>(this, EndGameReact); EventAggregator.AddListener<CollideWithPlanetEvent>(this, DestroyRocket); rigidbody = GetComponent<Rigidbody>(); Assert.IsNotNull(rigidbody, "    " + gameObject.name); forceModel = new ForceModel<IUseForces>(this); } protected virtual void Start() { Injections(); } private void DestroyRocket(CollideWithPlanetEvent obj) { Destroy(gameObject); } private void EndGameReact(EndGameEvent obj) { Debug.Log("     "); rocketState = RocketState.STOP; } private void Injections() { EventAggregator.Invoke(new InjectEvent<InputController> { inject = (InputController obj) => inputController = obj}); EventAggregator.Invoke(new InjectEvent<RocketHolder> { inject = (RocketHolder holder) => rocketHolder = holder }); } protected abstract void StartEventReact(ButtonStartPressed buttonStartPressed); } public interface IPlayerHelper { Transform Transform { get; } RocketState RocketState { get; } } } 


Vamos analisar a classe:

  [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper 

Primeiro, por que a classe é abstrata? Não sabemos que tipo de foguetes teremos, como eles se moverão, como serão animados, quais recursos de jogabilidade estarão disponíveis (por exemplo, a possibilidade de um foguete saltar para o lado). Portanto, tornamos a classe base abstrata, colocamos dados padrão lá e colocamos os métodos abstratos, cuja implementação para mísseis específicos pode variar.

Também pode ser visto que a classe já possui uma implementação de interfaces e um atributo que trava na jogabilidade o componente desejado sem o qual o foguete não é um foguete.

 [SerializeField] protected RocketConfig config; 

Esse atributo nos diz que o inspetor possui um campo serializável no qual o objeto é inserido, na maioria das lições, incluindo o Unity, esses campos são divulgados ao público. Se você é um desenvolvedor independente, não faça isso. Use campos particulares e este atributo. Aqui, quero discutir um pouco sobre o que é essa classe e o que ela faz.

Rocketconfig
 using UnityEngine; namespace PlayerRocket { [CreateAssetMenu(fileName = "RocketConfig", menuName = "Configs/RocketConfigs", order = 1)] public class RocketConfig : ScriptableObject { [SerializeField] private float speed; [SerializeField] private float fuel; public float Speed => speed; public float Fuel => fuel; } } 


Este é um ScriptableObject que armazena configurações de foguetes. Isso leva o pool de dados de que os designers de jogos precisam - além da classe. Assim, os designers de jogos não precisam mexer e configurar um projeto de jogo específico com um foguete específico; eles podem apenas consertar essa configuração, que é armazenada em um ativo / arquivo separado. Eles podem configurar a configuração de tempo de execução e ela será salva, também é possível comprar skins diferentes para o foguete e os parâmetros serem os mesmos - a configuração apenas vai para onde você precisa. Essa abordagem está se expandindo bem - você pode adicionar quaisquer dados, escrever editores personalizados etc.

 protected ForceModel<IUseForces> forceModel; 

Também quero me debruçar sobre isso, esta é uma classe genérica para aplicar modificadores a um objeto.

Forcemodel
 using System.Collections.Generic; using System.Linq; using UnityEngine; public enum TypeOfForce { Push = 0, AddSpeed = 1, } public class ForceModel<T> where T : IUseForces { readonly private T forceUser; private List<SpaceForces> forces = new List<SpaceForces>(); protected bool IsHaveAdditionalForces; public ForceModel(T user) { GlobalEventAggregator.EventAggregator.AddListener<SpaceForces>(this, ChangeModificatorsList); forceUser = user; } private void ChangeModificatorsList(SpaceForces obj) { if (obj.IsAdded) forces.Add(obj); else forces.Remove(forces.FirstOrDefault(x => x.CenterOfObject == obj.CenterOfObject)); if (forces.Count > 0) IsHaveAdditionalForces = true; else IsHaveAdditionalForces = false; } public void AddModificator() { if (!IsHaveAdditionalForces) return; foreach (var f in forces) { switch (f.TypeOfForce) { case TypeOfForce.Push: AddDirectionForce(f); break; case TypeOfForce.AddSpeed: forceUser.RigidbodyForForce.AddRelativeForce(Vector3.up*f.Force); break; } } } private void AddDirectionForce(SpaceForces spaceForces) { //Debug.Log(""); //var t = AngleDir(forceUser.TransformForForce.position, spaceForces.CenterOfObject); forceUser.RigidbodyForForce.AddForce(Push(spaceForces)); } private Vector3 Push(SpaceForces spaceForces) { var dist = Vector2.Distance(forceUser.Transform.position, spaceForces.CenterOfObject); var coeff = 1 - (spaceForces.ColliderBound / dist); if (forceUser.Transform.position.x > spaceForces.CenterOfObject.x) return (Vector3.right * spaceForces.Force) * coeff; else return (-Vector3.right * spaceForces.Force) * coeff; } public static float AngleDir(Vector2 A, Vector2 B) { return -Ax * By + Ay * Bx; } } public interface IUseForces { Transform Transform { get; } Rigidbody RigidbodyForForce { get; } } public struct SpaceForces { public TypeOfForce TypeOfForce; public Vector3 CenterOfObject; public Vector3 Direction; public float Force; public float ColliderBound; public bool IsAdded; } 


Foi o que escrevi acima - se você precisar fazer algum tipo de computação / lógica complexa, coloque-a em uma classe separada. Existe uma lógica muito simples - há uma lista de forças que se aplicam a um foguete. Nós iteramos a lista, analisamos que tipo de poder é e aplicamos um método específico. A lista é atualizada por eventos, eventos acontecem ao entrar / sair no campo modificador. O sistema é bastante flexível, primeiramente trabalha com uma interface (oi encapsulamento), os usuários de modificadores podem ser não apenas rockets / players. Em segundo lugar, um genérico - você pode estender IUseForces com vários descendentes para necessidades / experimentos e ainda usar essa classe / modelo.

O suficiente para a primeira parte. Na segunda parte, consideraremos um sistema de eventos, injeções de dependência, um controlador de entrada, uma própria classe de foguete e tentaremos iniciá-lo.

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


All Articles