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 2Parte 3Organizamos 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:- 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)
- Deveria haver um botão Iniciar, pelo qual enviamos o foguete na estrada
- Rolar a tela ao colocar o modificador (para iniciar)
- Movimento da câmera atrás do player (após o início)
- Painel de interface com o qual os modificadores de arrastar e soltar serão implementados em campo
- O protótipo deve ter dois modificadores - repulsão e aceleração
- Deve haver planetas quando você toca no que você morre
- 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 aterHá 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:
- GameController - (responsável pelo estado do jogo, diretamente para a lógica (ganho, perda, quanta vida, etc.)
- InputController - tudo relacionado ao gerenciamento de jogadores, rastreamento de tachi, cliques, quem clicou, status de controle etc.
- 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
- AudioController - aqui está claro, trata-se de sons
- 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 foguetesusing 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) {
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.