It so happened that I often do prototypes (both for work and for personal projects)
and I want to share my experience. I would like to write an article that I would be interested to read myself, and first of all I wondered what a person is guided by when he takes this or that decision in the project implementation, as he starts the project, often to start is the most difficult thing.
A fresh example I want to consider with you is the concept of a casual puzzle based on physics.
Sometimes I will mention over the obvious things in order to expand the topic for beginners.
Idea looks like this
')
Part 2
Part 3
We place various modifiers on the playing field, which change the direction of the rocket, attract, accelerate, repel and so on. The task is to pave the way among the stars to the next planet we need. Winnings are counted upon landing / touching the trail of the planet. The playing field is vertical, several screens up, there is an assumption that the trajectory can occupy the floor of the screen left / right. Loss - if missed past the destination planet, collided with another planet, flew far beyond the zone.
And I hope we all understand that before rushing into development and implementing our plans, you must first make a prototype - a very raw and fast product that will allow you to quickly try basic mechanics and get an idea of the gameplay. What are prototypes made for? The gameplay in our heads and in practice are very different things, what seems cool to us, there will be complete horror for others, plus there are often controversial moments in the project - management, rules of the game, dynamics of the game and so on. etc. It would be extremely stupid to skip the prototype stage, work through the architecture, bring the graphics, adjust the levels and eventually find out that the game is shitty. From life - in one game there were mini-games, about 10 pieces, after prototyping, it turned out that they were awfully boring, threw out half, and remade half.
Advice - in addition to the basic mechanics isolate controversial places, write down specific expectations from the prototype. This is done in order to perezlozhitsya on difficult moments. For example, one of the tasks that this prototype faced was how comfortable and understandable that the playing field consists of several screens and they need to be shaken. It was necessary to implement plan A - svayp. Plan B - the ability to zoom the playing field (if necessary). There were also several options for modifiers. The screenshot shows the first idea - set the modifier and the direction of its influence. As a result, the modifiers were replaced by simply spheres that change the direction of the rocket when the sphere is touched. We decided that it would be more casual, without trajectories, and so on.
The general functionality that we implement:
- You can set the initial trajectory of the rocket, with limited deviations from the perpendicular to the degree (the rocket can not be turned to the side for more than a degree)
- There should be a start button to send a rocket on the way.
- Scrolling the screen when placing the modifier (before the start)
- Camera after player movement (after launch)
- Interface panel with which the field will be drags and drop modifiers
- The prototype should have two modifiers - repulsion and acceleration.
- There must be planets when you die
- Must have a planet when you win
Architecture
A very important point, in a large development, it is considered that the prototype is written as it were, if only faster, then the project is simply written from scratch. In the harsh reality of many projects, legs grow from a prototype, which is bad for a large development - curve architecture, code, technical debt, additional time for refactoring. Well, indie development as a whole smoothly flows from the prototype into beta. Why am I, you need to lay the architecture even in the prototype, even if it’s primitive, so that you don’t have to cry, or even blush in front of your colleagues.
Before any start, always repeat - SOLID, KISS, DRY, YAGNI. Even experienced programmers forget about Kiss and Yagni).
What basic architecture do I follow?
In the scene there is an empty gameobject GameController with the corresponding tag, it has components / mono-tags hanging on it, it’s better to make it a prefab, then just add components to the prefab as necessary:
- GameController - (responsible for the state of the game, directly logic (won, lost, how much life, etc.)
- InputController - everything related to player control, tracking of tachets, clicks, clicked, control status, and so on.
- TransformManager - in games it is often necessary to know who is where, various data related to the position of the player / enemies. For example, if we fly past the planet, then we count the defeat of the player, the game controller is responsible for this, but from where he should know the position of the player. The Transform Manager is the very entity that knows the state of affairs.
- AudioController - here it is clear, this is about the sounds
- InterfacesController - and here it is clear, this is about UI
The overall picture emerges - for each understandable piece of tasks, a controller / entity is created that solves these tasks, it will allow to avoid Godlike objects, gives an understanding where to dig, we give out data from controllers, we can always change the implementation of data acquisition. Public fields can not be, we give the data only through public properties / methods. Data is calculated / changed locally.
Sometimes it happens that GameController swells up, due to different specific logic and calculations. If we need to process data - for this it is better to have a separate GameControllerModel class and do it there.
And so the code began
Rocket Base Classusing 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; } } }
Let 's
run through the class:
[RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper
First, why is the class abstract? We do not know what kind of rockets we will have, how they will move, how they will be animated, what gameplay features will be available (for example, the possibility of a rocket bounce to the side). Therefore, we make the base class abstract, lay standard data there, and lay down abstract methods, the implementation of which for specific missiles may differ.
You can also see that the class already has an interface implementation and an attribute that will attach the necessary component to the gamebug without which the rocket is not a rocket.
[SerializeField] protected RocketConfig config;
This attribute tells us that the inspector has a serializable field where the object is being pushed, in most of the lessons, including Unity, such fields are made public, if you are an indie developer, don’t do so. Use private fields and this attribute. Here I want to dwell a little on what kind of class it is and what it does.
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; } }
This is a ScriptableObject that stores rocket settings. This brings the data pool that game designers need - beyond the limits of the class. Thus, game designers do not need to swarm and configure a specific game object with a specific rocket, they can only fix this config, which is stored in a separate asset / file. They can configure the config time and it will continue, also let's say if you can buy different skins for the rocket, and the parameters are the same and the same - the config just roams where necessary. This approach expands well - you can add any data, write custom editors, and so on.
protected ForceModel<IUseForces> forceModel;
I also want to dwell on this, this is a generic class for applying modifiers to an object.
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) {
This is what I wrote above - if you need to do some kind of calculations / complex logic, put it into a separate class. There is a very simple logic - there is a list of forces that apply to the rocket. We iterate the list, see what kind of power it is and apply a specific method. The list is updated on events, events happen when entering / leaving the modifier field. The system is quite flexible, in the first place it works with the interface (hi encapsulation), users of modifiers can be not only rockets / player. Secondly, a generic - you can extend IUseForces with various descendants for needs / experiments, and still use this class / model.
For the first part, that's enough. In the second part, we will consider the event system, dependency injection, input controller, the rocket class itself and try to launch it.