Prototypage d'un jeu mobile, par où commencer et comment le faire. Partie 3 (finale)

Dans la première partie, j'ai expliqué pourquoi le prototypage et en général par où commencer
- Partie 1

Dans la deuxième partie, un petit tour d'horizon des classes clés
et architecture - Partie 2

Et la troisième partie - il y aura en fait une petite discussion, nous analyserons le fonctionnement des modificateurs, et nous laisser tomber sur le terrain de jeu (ce n'est pas difficile, mais il y a des nuances). Et un peu d'architecture, je vais essayer sans ennui.

Les captures d'écran sur la fusée sont généralement ternes, je suggère donc de regarder une vidéo d'un autre prototype, qui a été assemblé en 2 semaines avec les graphiques, et a été abandonné car il n'y a aucun moyen de contourner le genre de jeu de plateforme. Soit dit en passant, c'est l'une des idées clés autour du prototype - assembler, voir si c'est de la merde, est-ce nécessaire? Et jetez honnêtement le panier si les réponses ne vous semblent pas convaincantes. Mais! Cela ne s'applique pas aux projets créatifs - parfois la créativité se produit pour la créativité).

Alors vidosik, vous devez regarder les textes classés par niveau, et non pas le gameplay):



Petite digression


Dans le dernier article, j'ai reçu le code de révision et j'en suis reconnaissant; la critique aide à se développer même si vous n'êtes pas d'accord avec elle.

Mais je veux déployer pour l'architecture et la syntaxe concernant les prototypes:
  1. Peu importe à quel point vous êtes cool, vous ne pouvez pas simplement prévoir qu'il ne sera pas hypothéqué, et il peut y avoir beaucoup d'hypothèques de ce qui n'est pas nécessaire. Par conséquent, dans tous les cas, une refactorisation ou une extension sera nécessaire. Si vous ne pouvez pas décrire les avantages spécifiques du code / de l'approche, il est préférable de ne pas consacrer beaucoup de temps à ce code.
  2. Pourquoi à mon avis, la POO / modèle d'événement / composition est plus facile pour les prototypes que ECS, Unity COOP, DI FrameWorks, Frameworks réactifs, etc. Moins de gribouillis, toutes les connexions sont visibles dans le code, car la tâche principale du prototype est de répondre à la question principale - est-il possible d'y jouer, et un certain nombre de secondaires - ce qui est mieux pour le gameplay, ceci ou cela. Par conséquent, vous devez implémenter les fonctionnalités nécessaires le plus rapidement possible. Pourquoi introduire un framework sur un petit projet, prescrire toutes les ordures afin d'implémenter trois entités de jeu? Chacun d'eux est en fait une classe de 50 à 100 lignes. L'architecture doit être pensée dans le cadre des tâches du prototype, et dans le cadre d'une éventuelle extension à alpha, mais la seconde est plus nécessaire dans la tête que dans le code pour que le code ne soit pas gravé plus tard


À propos des modificateurs:


Et enfin sur le modificateur lui-même:
Ici et plus tôt, j'appelle les modificateurs les champs de force que la fusée touche et qui affectent sa trajectoire, dans le prototype il y en a deux types - l'accélération et la déviation.

Classe de modificateurs
public 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 }); } } 


Il s'agit d'une classe très simple dont la tâche est de passer deux événements:
  1. Le coup du joueur sur le terrain (qui est en fait un déclencheur d'unité physique), et tout le contexte nécessaire - le type de modificateur, sa position, la taille du collisionneur, la force du modificateur, etc. En plus de l'événement de l'agrégateur, il peut transmettre n'importe quel contexte aux parties intéressées. Dans cette veine, il s'agit d'un modèle de fusée qui traite le modificateur.
  2. Le deuxième événement - le joueur a quitté le terrain. Pour supprimer son influence sur la trajectoire du joueur

À quoi sert le modèle d'événement? Qui d'autre pourrait avoir besoin de cet événement?
Dans le projet actuel, cela n'est pas mis en œuvre, mais:
  1. Jeu de voix (a reçu un événement où quelqu'un est entré sur le terrain - nous jouons le son correspondant, quelqu'un est sorti - de même)
  2. Marqueurs d'interface, disons que pour chaque champ, nous retirerons du carburant de la fusée, des info-bulles devraient apparaître que nous sommes entrés dans le champ et que nous avons perdu du carburant, eh bien, ou gagner des points pour chaque coup sur le terrain, il existe de nombreuses options que l'interface intéresse pour le joueur. le terrain.
  3. Spécial effets - lorsqu'ils sont touchés dans un type de champ différent, différents effets peuvent être superposés, à la fois sur la fusée elle-même et sur l'espace autour de la fusée / champ. Spécial les effets peuvent être gérés par une entité / un contrôleur séparé, qui sera également abonné aux événements des modificateurs.
  4. Eh bien, c'est un minimum de code, les localisateurs de service, l'agrégation, les dépendances, etc. ne sont pas nécessaires.


Base de jeu


Dans ce prototype, l'essence du gameplay est de placer des modificateurs sur le terrain de jeu, d'ajuster la trajectoire de vol de la fusée, de contourner les obstacles et d'atteindre le point / planète de destination. Pour ce faire, nous avons un panneau sur la droite, sur lequel se trouvent les icônes des modificateurs.



Classe de panneau
  [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); } } } 



Anticiper les questions:
 for (var x = 0; x< 3; x++) for (var x = 0; x< 1; x++) 


3 et 1 - les soi-disant nombres magiques qui sont simplement pris de la tête et insérés dans le code, cela devrait être évité, mais pourquoi sont-ils ici? Le principe selon lequel le panneau de droite est formé n'a pas encore été déterminé, et il a été décidé simplement de tester le prototype avec autant de modificateurs sur le prototype.

Comment bien faire? - au moins le mettre dans des champs sérialisables, et définir les quantités requises par l'inspecteur. Pourquoi suis-je trop paresseux et devriez-vous le faire? Ici, nous devons partir de la vue d'ensemble, pour la formation du nombre requis de modificateurs, une entité distincte et la configuration seront toujours responsables, alors j'étais trop paresseux en m'attendant à beaucoup de refactoring à l'avenir. Mais mieux vaut ne pas être paresseux! )

À propos des configurations - lorsque les premières conférences sur ScriptableObject sont apparues, j'ai aimé l'idée de stocker des données comme un atout. Vous obtenez les données nécessaires là où vous en avez besoin, sans être lié à une instance à copie unique. Ensuite, il y a eu une conférence sur une approche du développement de jeux impliquant ScriptableObject, où ils avaient l'habitude de stocker les paramètres d'instance. En fait, les préréglages / paramètres de quelque chose enregistré dans l'actif sont la configuration.

Considérez la classe de configuration:
Classe de configuration
  [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; } 



Quelle est l'essence de son travail? Il stocke une classe de données de modificateur personnalisée.
  public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; } 


Le type de modificateur est nécessaire pour l'identification, une icône pour l'interface, le mode de jeu de l'objet de jeu du modificateur, le matériel ici afin qu'il puisse être modifié pendant la configuration. Les modificateurs peuvent déjà être situés sur le terrain de jeu, et disons que le concepteur de jeu change de type, maintenant il donne l'accélération, le modificateur s'initie dans la configuration et met à jour tous les champs, y compris le matériel, selon ce type de modificateur.

Travailler avec la configuration est très simple - nous nous tournons vers la configuration pour les données d'un type spécifique, nous obtenons ces données, des paramètres visuels intimes et possibles à partir de ces données.

Où est le profit?
L'avantage est une très grande flexibilité, par exemple, vous voulez changer le matériau et l'icône sur le modificateur d'accélération, ou disons remplacer le projet de jeu entier. Au lieu de réécrire et de transférer vers les champs de l'inspecteur, nous modifions simplement ces données dans une seule configuration et le tour est joué - tout sera mis à jour avec nous, sur toutes les scènes / niveaux / panneaux.

Et s'il y a plusieurs données pour le modificateur d'accélérateur dans la configuration?
Dans le prototype, vous pouvez facilement le suivre manuellement afin que les données ne soient pas dupliquées; sur un brouillon de travail, vous avez besoin de tests et de validation des données.

De l'icône au terrain de jeu


Classe d'icône de modificateur
 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; } 



Que se passe-t-il ici?
Nous prenons l'icône du panneau, en dessous, nous créons un blog de jeu du modificateur lui-même. Et en fait, nous définissons les coordonnées du clic / de la brouette vers l'espace de jeu, nous déplaçons donc le modificateur dans l'espace de jeu avec l'icône dans l'interface utilisateur, par la façon dont je vous conseille de lire sur RectTransformUtility, c'est une excellente classe d'aide dans laquelle il y a beaucoup de fonctionnalités pour l'interface.

Disons que nous changeons d'avis sur la mise d'un modificateur et que nous le renvoyons au panneau,
  if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5) 

Ce morceau de code nous permet de comprendre ce qui est sous le clic. Pourquoi la vérification des couches est-elle également indiquée ici? Et pourquoi encore le nombre magique 5? Comme nous nous en souvenons de la deuxième partie, nous utilisons le graphique rakester non seulement pour l'interface utilisateur, mais aussi pour le bouton qui se trouve dans la scène, également si nous ajoutons la fonctionnalité pour supprimer les modificateurs déjà placés sur le champ ou les déplacer, ils tomberont également sous le graphique rake, donc il existe également une vérification supplémentaire de l'appartenance à la couche d'interface utilisateur. Il s'agit du calque par défaut, et son ordre ne change pas, donc le nombre 5 ici n'est généralement pas un nombre magique.

En conséquence, il s'avère que si nous relâchons l'icône au-dessus du panneau, elle reviendra au panneau, si au-dessus du terrain de jeu le modificateur reste sur le terrain, l'icône sera supprimée.

Une journée de travail a été consacrée au code prototype. Plus un peu d'agitation sur les fichiers et les graphiques. En général, le gameplay s'est avéré approprié, malgré la tonne de questions sur les puces d'art et de conception de jeux. Mission terminée.

Conclusions et recommandations


  1. Poser une architecture minimale, mais néanmoins une architecture
  2. Suivez les principes de base, mais sans fanatisme)
  3. Choisissez des solutions simples
  4. Entre polyvalence et vitesse - il vaut mieux choisir une vitesse pour le prototype
  5. Pour les projets de grande / moyenne taille, indiquez que le projet est préférable de réécrire à partir de zéro. Par exemple, maintenant la tendance dans Unity est DOTS, vous devez écrire beaucoup de composants et de systèmes, c'est mauvais pour les petites séries, vous perdez du temps, pour les longues séries - lorsque tous les composants et systèmes sont enregistrés, le gain de temps commence. Je ne pense pas que ce soit cool de passer beaucoup de temps sur les tendances de l'architecture et de découvrir ce qu'est le prototype

Des prototypes réussis à tous.

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


All Articles