Dio la casualidad de que a menudo hago prototipos (tanto para el trabajo como para proyectos personales)
y quiero compartir mi experiencia Me gustaría escribir un artículo que sería interesante de leer y, en primer lugar, me pregunto por qué se guía a una persona cuando toma esta o aquella decisión en la implementación del proyecto, cómo comienza el proyecto, a menudo es el más difícil de comenzar.
Un nuevo ejemplo que quiero considerar contigo es el concepto de un rompecabezas casual basado en la física.
A veces mencionaré cosas súper obvias para expandir el tema para principiantes.La idea se ve así
Parte 2Parte 3Organizamos varios modificadores en el campo de juego, que cambian la dirección del cohete, atraen, aceleran, repelen, etc. La tarea es allanar el camino entre las estrellas para el próximo planeta que necesitamos. Ganar se considera al aterrizar / tocar el rastro del planeta. El campo de juego es vertical, varias pantallas arriba, se supone que la trayectoria puede ocupar el piso de la pantalla izquierda / derecha. Perder: si se perdió el planeta de destino, chocó con otro planeta, voló mucho más allá de la zona.
Y espero que todos comprendamos que antes de entrar en el desarrollo e implementar nuestros planes, primero debemos hacer un prototipo, un producto muy crudo y rápido que le permitirá probar rápidamente la mecánica básica y tener una idea del juego. ¿Para qué están hechos los prototipos? El proceso del juego en nuestras cabezas y en la práctica son cosas muy diferentes, lo que nos parece genial, para otros será un completo horror, además, a menudo hay momentos controvertidos en el proyecto: administración, reglas del juego, dinámica del juego, etc. Y así sucesivamente. Será extremadamente estúpido saltear la etapa del prototipo, trabajar a través de la arquitectura, entregar los gráficos, ajustar los niveles y finalmente descubrir que el juego es una mierda. De la vida: en un juego había minijuegos, alrededor de 10 piezas, después de la creación de prototipos resultó que eran terriblemente aburridas, la mitad fueron expulsadas, la mitad fueron rehechas.
Consejo: además de la mecánica básica, aislar lugares controvertidos, anotar expectativas específicas del prototipo. Esto se hace para recargar en momentos difíciles. Por ejemplo, una de las tareas a las que se enfrentó este prototipo fue lo conveniente y comprensible que el campo de juego consta de varias pantallas y necesitan ser enrolladas. Fue necesario implementar el plan A - svayp. Plan B: la capacidad de ampliar el campo de juego (si es necesario). También había varias opciones para modificadores. La primera idea es visible en la captura de pantalla: exponemos el modificador y la dirección de su influencia. Como resultado, los modificadores fueron reemplazados simplemente por esferas que cambian la dirección del cohete cuando tocan la esfera. Decidimos que esto sería más informal, sin trayectorias, etc.
La funcionalidad general que implementamos:- Puede establecer la trayectoria inicial del cohete, limitando el grado de desviación de la perpendicular (el cohete no puede girarse hacia un lado más de un grado)
- Debería haber un botón de inicio, por el cual enviamos el cohete en el camino
- Desplazar la pantalla al colocar el modificador (para comenzar)
- Movimiento de la cámara detrás del jugador (después del inicio)
- Panel de interfaz con el que se implementarán modificadores de arrastrar y soltar en el campo
- El prototipo debe tener dos modificadores: repulsión y aceleración.
- Debe haber planetas cuando tocas y mueres
- Debe haber un planeta cuando tocas y ganas
Arquitectura
Un punto muy importante, en grandes desarrollos, generalmente se acepta que el prototipo está escrito tan horrible como sería más rápido, luego el proyecto simplemente está escrito desde cero. En la dura realidad de muchos proyectos, las piernas crecen fuera del prototipo, lo que es malo para un gran desarrollo: arquitectura de curvas, código heredado, deuda técnica, tiempo extra para refactorizar. Bueno, y el desarrollo independiente en su conjunto fluye suavemente desde el prototipo a la versión beta. ¿Por qué lo soy? Es necesario colocar la arquitectura incluso en un prototipo, incluso si es primitivo, para que no llores más tarde o te ruborices frente a tus colegas.
Antes de comenzar, siempre repetimos: SÓLIDO, BESO, SECO, YAGNI. Incluso los programadores experimentados se olvidan de Kiss y Yagni).
¿A qué arquitectura básica me apego?Hay un GameController Gameobject vacío en la escena con la etiqueta correspondiente, los componentes / monobahs cuelgan en él, es mejor hacerlo prefabricado, luego simplemente agregue los componentes necesarios al prefabricado:
- GameController: (responsable del estado del juego, directamente a la lógica (ganado, perdido, cuánta vida, etc.)
- InputController: todo lo relacionado con la gestión del jugador, el seguimiento de tachi, los clics, quién hizo clic, el estado del control, etc.
- TransformManager: en los juegos, a menudo necesitas saber quién está dónde, varios datos relacionados con la posición del jugador / enemigos. Por ejemplo, si sobrevolamos un planeta, el jugador es derrotado, el controlador del juego es responsable de esto, pero debe conocer la posición del jugador desde dónde. El administrador de transformación es exactamente la esencia que sabe de las cosas.
- AudioController: aquí está claro, se trata de sonidos
- InterfacesController, y aquí está claro, se trata de UI
La imagen general emerge: para cada tarea comprensible, se crea un controlador / entidad que resuelve estos problemas, esto permitirá evitar objetos como google, da una idea de dónde cavar, damos datos de los controladores, siempre podemos cambiar la implementación de la recepción de datos. Los campos públicos no están permitidos, solo proporcionamos datos a través de propiedades / métodos públicos. Calculamos / cambiamos datos localmente.
A veces sucede que el GameController está inflado, debido a varios cálculos y lógica específica. Si necesitamos procesar datos, para esto es mejor crear una clase separada GameControllerModel y hacerlo allí.
Y así comenzó el código
Clase base para cohetesusing 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; } } }
Repasemos la clase:
[RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper
Primero, ¿por qué la clase es abstracta? No sabemos qué tipo de cohetes tendremos, cómo se moverán, cómo se animarán, qué características de juego estarán disponibles (por ejemplo, la posibilidad de que un cohete rebote hacia un lado). Por lo tanto, hacemos el resumen de la clase base, colocamos datos estándar allí y colocamos los métodos abstractos, cuya implementación para misiles específicos puede variar.
También se puede ver que la clase ya tiene una implementación de interfaces y un atributo que cuelga en el juego el componente deseado sin el cual el cohete no es un cohete.
[SerializeField] protected RocketConfig config;
Este atributo nos dice que el inspector tiene un campo serializable donde el objeto está repleto, en la mayoría de las lecciones, incluida Unity, dichos campos se hacen públicos, si usted es un desarrollador independiente, no haga esto. Use campos privados y este atributo. Aquí quiero detenerme un poco en qué es esta clase y qué hace.
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 es un ScriptableObject que almacena la configuración de cohetes. Esto lleva el conjunto de datos que los diseñadores de juegos necesitan, más allá de la clase. Por lo tanto, los diseñadores de juegos no necesitan perder el tiempo y configurar un proyecto de juego específico con un cohete específico, solo pueden arreglar esta configuración, que se almacena en un archivo / activo separado. Pueden configurar la configuración de tiempo de ejecución y se guardará, también es posible si es posible comprar diferentes máscaras para el cohete, y los parámetros son los mismos: la configuración simplemente va a donde lo necesite. Este enfoque se está expandiendo bien: puede agregar cualquier dato, escribir editores personalizados, etc.
protected ForceModel<IUseForces> forceModel;
También quiero detenerme en esto, esta es una clase genérica para aplicar modificadores a un 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) {
Esto es lo que escribí anteriormente: si necesita hacer algún tipo de cálculo / lógica compleja, colóquelo en una clase separada. Hay una lógica muy simple: hay una lista de fuerzas que se aplican a un cohete. Repetimos la lista, observamos qué tipo de poder es y aplicamos un método específico. La lista se actualiza por eventos, los eventos suceden al entrar / salir en el campo modificador. El sistema es bastante flexible, en primer lugar funciona con una interfaz (alta encapsulación), los usuarios de modificadores pueden ser no solo cohetes / jugadores. En segundo lugar, genérico: puede extender IUseForces con diferentes descendientes para necesidades / experimentos, y aún usar esta clase / modelo.
Suficiente para la primera parte. En la segunda parte, consideraremos un sistema de eventos, inyecciones de dependencia, un controlador de entrada, una clase de cohete y trataremos de lanzarlo.