Na primeira parte, falei sobre por que a prototipagem e em geral por onde começar
-
Parte 1Na segunda parte, uma pequena amostra das principais classes
e arquitetura -
parte 2E a terceira parte - na verdade, haverá um pouco de discussão, analisaremos como os modificadores funcionam e entraremos em campo (isso não é difícil, mas há nuances). E um pouco sobre arquitetura, tentarei sem tédio.
As capturas de tela sobre o foguete geralmente são monótonas, então sugiro assistir a um vídeo de outro protótipo, que foi montado em duas semanas junto com os gráficos, e foi abandonado devido ao fato de que no gênero de plataformas não há maneira de contornar. A propósito, essa é uma das idéias principais em torno do protótipo - montar, ver se isso é uma merda, é necessário? E honestamente jogue na cesta se as respostas não parecerem convincentes para você. Mas! Isso não se aplica a projetos criativos - às vezes a criatividade acontece em prol da criatividade).
Então vidosik, você precisa olhar para os textos organizados por nível, e não para a jogabilidade):
Pequena digressão
No último artigo, recebi o código de revisão e sou grato por isso; as críticas ajudam a se desenvolver mesmo que você não concorde.
Mas quero implantar a arquitetura e a sintaxe em relação aos protótipos:- Não importa o quão legal você seja, você não pode apenas prever que não haverá hipoteca, e pode haver muita hipoteca do que não é necessário. Portanto, em qualquer caso, será necessária refatoração ou expansão. Se você não conseguir descrever os benefícios específicos do código / abordagem, é melhor não gastar muito tempo nesse código.
- Por que, na minha opinião, OOP / modelo / composição de eventos é mais fácil para protótipos do que ECS, Unity COOP, DI FrameWorks, estruturas reativas, etc. Com menos rabiscos, todas as conexões são visíveis no código, porque a principal tarefa do protótipo é responder à pergunta principal - é possível reproduzi-lo e várias secundárias - o que é melhor para a jogabilidade, isso ou aquilo. Portanto, você precisa implementar a funcionalidade necessária o mais rápido possível. Por que introduzir uma estrutura em um projeto pequeno, prescrever todo o lixo para implementar três entidades do jogo? Cada uma delas é na verdade uma classe de 50 a 100 linhas. A arquitetura deve ser pensada como parte das tarefas de protótipo e como parte de uma possível extensão para alfa, mas a segunda é necessária mais na cabeça do que no código, para que você não queime ao adicionar código
Sobre modificadores:
E finalmente sobre o próprio modificador:Aqui e antes, chamo os modificadores de campos de força em que o foguete toca e que afetam seu caminho. No protótipo, existem dois tipos deles - aceleração e deflexão.
Classe modificadorapublic class PushSideModificator : MonoBehaviour { [SerializeField] TypeOfForce typeOfForce = TypeOfForce.Push; [SerializeField] private float force; [SerializeField] DropPanelConfig dropPanelConfig; private float boundsOfCollider; private void OnTriggerEnter(Collider other) { boundsOfCollider = other.bounds.extents.x; GlobalEventAggregator.EventAggregator.Invoke(new SpaceForces { TypeOfForce = typeOfForce, Force = force, ColliderBound = boundsOfCollider, CenterOfObject = transform.position, IsAdded = true }); } private void OnTriggerExit(Collider other) { GlobalEventAggregator.EventAggregator.Invoke(new SpaceForces { CenterOfObject = transform.position, IsAdded = false }); } }
Esta é uma classe muito simples cuja tarefa é passar dois eventos:
- A entrada do jogador no campo (que na verdade é um gatilho de unidade física) e todo o contexto necessário - o tipo de modificador, sua posição, o tamanho do colisor, a força do modificador e assim por diante. Neste, além do evento do agregador, ele pode transmitir qualquer contexto às partes interessadas. Nesse sentido, este é um modelo de foguete que processa o modificador.
- O segundo evento - o jogador deixou o campo. Para remover sua influência na trajetória do jogador
Para que serve o modelo de evento? Quem mais pode precisar deste evento?
No projeto atual, isso não é implementado, mas:- Atuação por voz (recebeu um evento em que alguém entrou em campo - tocamos o som correspondente, alguém saiu - da mesma forma)
- Marcadores de interface do usuário, digamos que, para cada campo, removeremos um pouco de combustível do foguete, as dicas de ferramentas devem parecer que entramos no campo e perdemos combustível, ou ganhar pontos por cada acerto no campo; há muitas opções nas quais a interface está interessada no jogador. o campo.
- Especial efeitos - quando atingidos em um tipo diferente de campo, efeitos diferentes podem ser sobrepostos, tanto no próprio foguete quanto no espaço ao redor do foguete / campo. Especial os efeitos podem ser tratados por uma entidade / controlador separado, que também será inscrito nos eventos dos modificadores.
- Bem, isso é um mínimo de código, localizadores de serviço, agregação, dependências etc. não são necessários.
Base de jogabilidade
Neste protótipo, a essência da jogabilidade é colocar modificadores no campo de jogo, ajustando a trajetória de vôo do foguete, para contornar obstáculos e atingir o ponto / planeta de destino. Para fazer isso, temos um painel à direita, no qual estão localizados ícones de modificadores.

Classe do painel [RequireComponent (typeof(CanvasGroup))] public class DragAndDropModifiersPanel : MonoBehaviour { [SerializeField] private DropModifiersIcon iconPrfb; [SerializeField] private DropPanelConfig config; private CanvasGroup canvasGroup; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener<ButtonStartPressed>(this, RocketStarted); canvasGroup = GetComponent<CanvasGroup>(); } private void RocketStarted(ButtonStartPressed obj) { canvasGroup.DOFade(0, 1); (canvasGroup.transform as RectTransform).DOAnchorPosX(100, 1); } private void Start() { for (var x = 0; x< 3; x++) { var mod = config.GetModifierByType(TypeOfForce.Push); var go = Instantiate(iconPrfb, transform); go.Init(mod); } for (var x = 0; x< 1; x++) { var mod = config.GetModifierByType(TypeOfForce.AddSpeed); var go = Instantiate(iconPrfb, transform); go.Init(mod); } } }
Antecipando perguntas:
for (var x = 0; x< 3; x++) for (var x = 0; x< 1; x++)
3 e 1 - os chamados números mágicos que são simplesmente retirados da cabeça e inseridos no código, isso deve ser evitado, mas por que eles estão aqui? O princípio pelo qual o painel direito é formado ainda não foi determinado e foi decidido simplesmente testar o protótipo com tantos modificadores no protótipo.
Como fazer isso certo? - pelo menos coloque-o em campos serializáveis e defina as quantidades necessárias através do inspetor. Por que sou muito preguiçoso e você deveria fazê-lo? Aqui devemos proceder a partir da visão geral, uma entidade e configuração separadas ainda serão responsáveis pela formação do número necessário de modificadores, então aqui eu estava com preguiça de esperar muitas refatorações no futuro. Mas é melhor não preguiçoso! )
Sobre configurações - quando as primeiras palestras sobre ScriptableObject apareceram, gostei da ideia de armazenar dados como um ativo. Você obtém os dados necessários onde precisa, sem estar vinculado a uma instância de cópia única. Depois, houve uma palestra sobre uma abordagem para o desenvolvimento de jogos envolvendo ScriptableObject, onde eles costumavam armazenar configurações de instância. Na verdade, as predefinições / configurações de algo salvo no ativo são a configuração.
Considere a classe de configuração:Classe de configuração [CreateAssetMenu(fileName = "DropModifiersPanel", menuName = "Configs/DropModifier", order = 2)] public class DropPanelConfig : ScriptableObject { [SerializeField] private ModifierBluePrintSimple[] modifierBluePrintSimples; public DropModifier GetModifierByType(TypeOfForce typeOfModifiers) { return modifierBluePrintSimples.FirstOrDefault(x => x.GetValue.TypeOfModifier == typeOfModifiers).GetValue; } } [System.Serializable] public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; }
Qual é a essência do seu trabalho? Ele armazena uma classe de dados modificadora personalizada.
public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; }
O tipo de modificador é necessário para identificação, um ícone para a interface, o modo de jogo do objeto de jogo do modificador, o material aqui para que possa ser alterado durante a configuração. Os modificadores já podem estar localizados no campo de jogo, e digamos que o designer do jogo altere seu tipo, agora ele dá aceleração, o modificador inicia na configuração e atualiza todos os campos, incluindo o material, de acordo com esse tipo de modificador.
Trabalhar com a configuração é muito simples - passamos à configuração dos dados para um tipo específico, obtemos esses dados, visual íntimos e possíveis configurações a partir desses dados.
Onde está o lucro?O benefício é uma flexibilidade muito grande, por exemplo, você deseja alterar o material e o ícone no modificador de aceleração ou, digamos, substituir todo o projeto do jogo. Em vez de reescrever e encaminhar para os campos do inspetor, apenas alteramos esses dados em uma configuração e pronto - tudo será atualizado conosco, em todas as cenas / níveis / painéis.
E se houver vários dados para o modificador acelerador na configuração?No protótipo, é possível rastreá-lo facilmente para que os dados não sejam duplicados; em um rascunho de trabalho, você precisa de testes e validação de dados.
Do ícone ao campo de jogo
Classe de ícone do modificador public class DropModifiersIcon : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler { [SerializeField] private Image icon; [Header(" ")] [SerializeField] private RectTransform canvas; private CanvasGroup canvasGroup; private DropModifier currentModifier; private Vector3 startPoint; private Vector3 outV3; private GameObject currentDraggedObj; private void Start() { canvasGroup = GetComponent<CanvasGroup>(); startPoint = transform.position; canvas = GetComponentInParent<Canvas>().transform as RectTransform; } public void Init(DropModifier dropModifier) { icon.sprite = dropModifier.Icon; currentModifier = dropModifier; } public void OnBeginDrag(PointerEventData eventData) { BlockRaycast(false); currentDraggedObj = Instantiate(currentModifier.Modifier, WorldSpaceCoord(), Quaternion.identity); GlobalEventAggregator.EventAggregator.Invoke(new ImOnDragEvent { IsDragging = true }); } private void BlockRaycast(bool state) { canvasGroup.blocksRaycasts = state; } public void OnDrag(PointerEventData eventData) { Vector2 outV2; RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas, Input.mousePosition, null, out outV2); transform.position = canvas.transform.TransformPoint(outV2); if (currentDraggedObj != null) currentDraggedObj.transform.position = WorldSpaceCoord(); } private Vector3 WorldSpaceCoord() { RectTransformUtility.ScreenPointToWorldPointInRectangle(canvas, Input.mousePosition, Camera.main, out outV3); return outV3; } public void OnEndDrag(PointerEventData eventData) { GlobalEventAggregator.EventAggregator.Invoke(new ImOnDragEvent { IsDragging = false }); if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5) { Destroy(currentDraggedObj); transform.SetAsLastSibling(); canvasGroup.blocksRaycasts = true; } else Destroy(gameObject); } } public struct ImOnDragEvent { public bool IsDragging; }
O que está acontecendo aqui?Nós agarramos o ícone no painel, abaixo dele criamos um blog de jogo do próprio modificador. Na verdade, estamos definindo a coordenada do clique / carrinho de mão para o espaço do jogo, então movemos o modificador no espaço do jogo junto com o ícone na interface do usuário, pela maneira como aconselho a ler sobre o RectTransformUtility, esta é uma ótima classe auxiliar na qual há muitos recursos para a interface.
Digamos que mudemos de idéia sobre colocar um modificador e devolvê-lo ao painel,
if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5)
Esse trecho de código nos permite entender o que está sob o clique. Por que a verificação de camada também é indicada aqui? E por que novamente é o número mágico 5? Como lembramos da segunda parte, usamos o gráfico de rakester não apenas para a interface do usuário, mas também para o botão que está em cena; também, se adicionarmos a funcionalidade para excluir modificadores já colocados no campo ou movê-los, eles também cairão no gráfico de rake, portanto há também uma verificação adicional por pertencer à camada da interface do usuário. Essa é a camada padrão e sua ordem não muda, portanto o número 5 aqui geralmente não é um número mágico.
Como resultado, acontece que, se soltarmos o ícone acima do painel, ele retornará ao painel; se, acima do campo de jogo, o modificador permanecer no campo, o ícone será excluído.
Foi gasto 1 dia útil no código do protótipo. Além de um pouco de confusão em arquivos e gráficos. Em geral, a jogabilidade foi considerada adequada, apesar das inúmeras perguntas sobre chips de arte e design de jogos. Missão Completa.
Conclusões e recomendações
- Lay arquitetura mínima, mas ainda assim arquitetura
- Siga os princípios básicos, mas sem fanatismo)
- Escolha soluções simples
- Entre versatilidade e velocidade - é melhor escolher uma velocidade para o protótipo
- Para projetos grandes / médios, significa que é melhor reescrever do projeto. Por exemplo, agora a tendência no Unity é DOTS, você precisa escrever muitos componentes e sistemas; para pequenas tiragens, é ruim, você perde tempo, para longas tiragens - quando todos os componentes e sistemas são registrados, o ganho de tempo começa. Não acho legal gastar muito tempo na arquitetura de tendências e descobrir o que o protótipo é uma merda
Protótipos de sucesso para todos.