Prototypage d'un jeu mobile, par où commencer et comment le faire. Partie 1

Il m'est arrivé de faire souvent des prototypes (à la fois pour le travail et pour des projets personnels)
et je veux partager mon expérience. Je voudrais écrire un article qui serait intéressant à lire moi-même, et je me demande tout d'abord ce qui guide une personne lorsqu'elle prend telle ou telle décision dans la mise en œuvre du projet, comment il démarre le projet, c'est souvent le plus difficile à démarrer.

Un nouvel exemple que je veux considérer avec vous est le concept d'un puzzle décontracté basé sur la physique. Parfois, je mentionnerai des choses super évidentes afin d'élargir le sujet pour les débutants.

L'idée ressemble à ceci




2e partie
3e partie

Nous organisons divers modificateurs sur le terrain de jeu, qui changent la direction de la fusée, attirent, accélèrent, repoussent, etc. La tâche consiste à ouvrir la voie entre les étoiles à la prochaine planète dont nous avons besoin. La victoire est considérée lors de l'atterrissage / toucher la trace de la planète. Le terrain de jeu est vertical, plusieurs écrans vers le haut, on suppose que la trajectoire peut occuper le plancher de l'écran gauche / droite. Perdre - si vous avez raté la planète de destination, heurté une autre planète, volé bien au-delà de la zone.

Et j'espère que nous comprenons tous qu'avant de se lancer dans le développement et la mise en œuvre de nos plans, nous devons d'abord faire un prototype - un produit très brut et rapide qui vous permettra d'essayer rapidement les mécanismes de base et d'avoir une idée du gameplay. À quoi servent les prototypes? Le processus de jeu dans nos têtes et dans la pratique sont des choses très différentes, ce qui nous semble cool, pour d'autres ce sera une horreur totale, et souvent il y a des moments controversés dans le projet - gestion, règles du jeu, dynamique du jeu, etc. et ainsi de suite. Il sera extrêmement stupide de sauter l'étape du prototype, de travailler sur l'architecture, de livrer les graphiques, d'ajuster les niveaux et enfin de découvrir que le jeu est de la merde. De la vie - dans un jeu, il y avait des mini-jeux, environ 10 pièces, après le prototypage, il s'est avéré qu'ils étaient terriblement ennuyeux, la moitié ont été jetés, la moitié a été refaite.

Astuce - en plus de la mécanique de base, isolez les endroits controversés, notez les attentes spécifiques du prototype. Cela se fait afin de recharger les moments difficiles. Par exemple, l'une des tâches auxquelles a été confronté ce prototype était de savoir à quel point il était pratique et compréhensible que le terrain de jeu se compose de plusieurs écrans et qu'ils doivent être enroulés. Il était nécessaire de mettre en œuvre le plan A - svayp. Plan B - la possibilité de zoomer sur le terrain de jeu (si nécessaire). Il y avait également plusieurs options pour les modificateurs. La première idée est visible dans la capture d'écran - nous exposons le modificateur et la direction de son influence. En conséquence, les modificateurs ont été remplacés simplement par des sphères qui changent la direction de la fusée lorsqu'elles touchent la sphère. Nous avons décidé que ce serait plus désinvolte, sans trajectoire, etc.

Les fonctionnalités générales que nous mettons en œuvre:

  1. Vous pouvez définir la trajectoire initiale de la fusée, le degré de déviation par rapport à la perpendiculaire étant limité (la fusée ne peut pas être tournée de plus d'un degré sur le côté)
  2. Il devrait y avoir un bouton de démarrage, par lequel nous envoyons la fusée sur la route
  3. Défilement de l'écran lors du placement du modificateur (pour commencer)
  4. Mouvement de la caméra derrière le joueur (après le démarrage)
  5. Panneau d'interface avec lequel les modificateurs de glisser-déposer seront implémentés sur le terrain
  6. Le prototype doit avoir deux modificateurs - répulsion et accélération
  7. Il doit y avoir des planètes quand vous touchez et vous mourrez
  8. Il doit y avoir une planète lorsque vous touchez que vous gagnez

L'architecture


Un point très important, dans un grand développement, il est généralement admis que le prototype est écrit aussi horrible qu'il serait plus rapide, puis le projet est simplement écrit à partir de zéro. Dans la dure réalité de nombreux projets, les jambes sortent du prototype, ce qui est mauvais pour le grand développement - architecture de courbe, code hérité, dette technique, temps supplémentaire pour le refactoring. Eh bien, et le développement indépendant dans son ensemble se déroule en douceur du prototype à la version bêta. Pourquoi suis-je? Il faut placer l'architecture même dans un prototype, même si c'est primitif, pour ne pas pleurer plus tard, ou rougir devant des collègues.

Avant tout départ, nous répétons toujours - SOLID, KISS, DRY, YAGNI. Même les programmeurs expérimentés oublient Kiss et Yagni).

À quelle architecture de base dois-je me conformer

Il y a un gameobject GameController vide dans la scène avec le tag correspondant, les composants / monobahs s'accrochent dessus, il est préférable d'en faire un préfabriqué, puis ajoutez simplement les composants nécessaires au préfabriqué:

  1. GameController - (responsable de l'état du jeu, directement à la logique (gagné, perdu, combien de vie, etc.)
  2. InputController - tout ce qui concerne la gestion des joueurs, le suivi des tachi, des clics, qui a cliqué, contrôler l'état, etc.
  3. TransformManager - dans les jeux, vous devez souvent savoir qui est où, diverses données liées à la position du joueur / des ennemis. Par exemple, si nous survolons une planète, le joueur est vaincu, le contrôleur de jeu en est responsable, mais il doit connaître la position du joueur d'où. Le gestionnaire de transformation est exactement l'essence qui connaît les choses
  4. AudioController - ici c'est clair, il s'agit de sons
  5. InterfacesController - et ici, c'est clair, il s'agit de l'interface utilisateur

L'image globale émerge - pour chaque tâche compréhensible, un contrôleur / entité est créé qui résout ces problèmes, cela permettra d'éviter des objets comme google, il permet de comprendre où creuser, nous donnons des données des contrôleurs, nous pouvons toujours changer la mise en œuvre de la réception des données. Les champs publics ne sont pas autorisés, nous ne donnons des données que via des propriétés / méthodes publiques. Nous calculons / modifions les données localement.

Il arrive parfois que le GameController soit gonflé, en raison de diverses logiques et calculs spécifiques. Si nous devons traiter des données - pour cela, il est préférable de créer une classe GameControllerModel distincte et de le faire là-bas.

Et donc le code a commencé


Classe de base pour les fusées
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; } } } 


Passons en revue la classe:

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

D'abord, pourquoi l'abstrait de classe? Nous ne savons pas quel type de fusée nous aurons, comment elles se déplaceront, comment elles seront animées, quelles fonctionnalités de gameplay seront disponibles (par exemple, la possibilité d'une fusée rebondissant sur le côté). Par conséquent, nous rendons la classe de base abstraite, y mettons des données standard et définissons les méthodes abstraites, dont la mise en œuvre pour des missiles spécifiques peut varier.

On peut également voir que la classe a déjà une implémentation d'interfaces et un attribut qui accroche au gameplay le composant souhaité sans lequel la fusée n'est pas une fusée.

 [SerializeField] protected RocketConfig config; 

Cet attribut nous indique que l'inspecteur a un champ sérialisable dans lequel l'objet est entassé, dans la plupart des leçons, y compris Unity, ces champs sont rendus publics, si vous êtes un développeur indépendant, ne le faites pas. Utilisez des champs privés et cet attribut. Ici, je veux m'arrêter un peu sur ce qu'est cette classe et ce qu'elle fait.

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; } } 


Il s'agit d'un ScriptableObject qui stocke les paramètres de la fusée. Cela prend le pool de données dont les concepteurs de jeux ont besoin - au-delà de la classe. Ainsi, les concepteurs de jeux n'ont pas besoin de jouer et de configurer un projet de jeu spécifique avec une fusée spécifique, ils ne peuvent que réparer cette configuration, qui est stockée dans un fichier / élément distinct. Ils peuvent configurer la configuration d'exécution et celle-ci sera enregistrée, il est également possible s'il sera possible d'acheter des skins différents pour la fusée, et les paramètres sont les mêmes - la configuration se déplace à l'endroit où vous en avez besoin. Cette approche se développe bien - vous pouvez ajouter des données, écrire des éditeurs personnalisés, etc.

 protected ForceModel<IUseForces> forceModel; 

Je veux également m'attarder sur ce point, il s'agit d'une classe générique pour appliquer des modificateurs à un objet.

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; } 


C'est ce que j'ai écrit ci-dessus - si vous avez besoin de faire une sorte de calcul / logique complexe, mettez-le dans une classe séparée. Il y a une logique très simple - il y a une liste de forces qui s'appliquent à une fusée. Nous parcourons la liste, examinons de quel type de pouvoir il s'agit et appliquons une méthode spécifique. La liste est mise à jour par les événements, les événements se produisent lors de l'entrée / sortie dans le champ du modificateur. Le système est assez flexible, tout d'abord il fonctionne avec une interface (encapsulation hi), les utilisateurs de modificateurs peuvent être non seulement des roquettes / player. Deuxièmement, générique - vous pouvez étendre IUseForces avec différents descendants pour les besoins / expériences, et toujours utiliser cette classe / modèle.

Assez pour la première partie. Dans la deuxième partie, nous considérerons un système d'événements, des injections de dépendances, un contrôleur d'entrée, une classe de fusée elle-même et tenterons de la lancer.

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


All Articles