📜 ⬆️ ⬇️

Prototyping a mobile game, where to start, and how to do it. Part 3 (final)

In the first part, I reasoned for what prototyping and in general where to start
- Part 1

In the second part, we ran a little bit by key classes.
and architecture - Part 2

And here is the third part - there will actually be some reasoning in it, let's look at how modifiers work, and drops on the playing field (this is not difficult, but there are nuances). And a little about architecture, I will try without tediousness.
')
Screenshots about the rocket as a whole are dull, so I propose to watch a video of another prototype, which was assembled in 2 weeks together with graphics, and was abandoned due to the fact that in the genre of platformers and so not overflowed. By the way, this is one of the key ideas around the prototype - to collect, to see if it is shit, is it necessary? And honestly throw in the basket if you find the answers do not seem convincing enough. But! This does not apply to creative projects - sometimes creativity is for the sake of creativity).

So vidosik, you need to look at the texts placed on the level, and not on the gameplay):



Small retreat


In the last article I received a review code and am grateful for it, criticism helps to develop even if you disagree with it.

But I want to deploy for the architecture and syntax regarding prototypes:
  1. No matter how steep you are, it’s impossible to foresee that it will not be pledged, and there may be a lot of pledging - which is not needed. Therefore, in any case, refactoring or expansion will be needed. If you can not describe the specific benefits of the code / approach, it is better not to spend a lot of time on this code.
  2. Why, in my opinion, OOP / Event model / composition is simpler for prototypes than ECS, Unity COOP, DI FrameWorks, Reactive Frameworks, etc. Less scribbling, all links are visible in the code, because the main task of the prototype is to answer the main question - is it possible to play this, and on a number of minor ones - what is better for the gameplay, this or that. Therefore, you need as quickly as possible to implement the necessary functionality. Why on a small project to introduce the framework, prescribe all the junk to implement three game entities? Each of which is actually a class of 50-100 lines. The architecture should be thought through as part of the tasks for the prototype, and as part of a possible expansion to the alpha, but the second is needed in the head rather than in the code, in order not to burn while writing the code


About modifiers:


And finally, about the modifier itself:
Here and earlier I call the modifiers the force fields that the rocket touches, and which affect its trajectory, in the prototype there are two types - acceleration and deflection.

Modifier class
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 }); } } 


This is a very simple class whose task is to pass two events:
  1. A player is hit in a field (which is actually a physical unit trigger), and all the necessary context is the type of modifier, its position, the size of the collider, the impact force of the modifier, and so on. In this plus the event aggregator, it can transfer any context to interested parties. In this vein, this is a rocket model that processes the modifier.
  2. The second event - the player left the field. To remove its effect on the player's trajectory

What is the event model for? Who else might need this event?
This is not implemented in the current project, but:
  1. Voice acting (got an event that someone entered the field - we play the corresponding sound, someone came out - similarly)
  2. UI markers, let's say for each field we’ll remove some fuel from the rocket, pop-up prompts should appear that we entered the field and lost fuel, well, or score points for each hit in the field, there are many options for the fact that the interface is interesting to the player field.
  3. Specialist. effects - when getting into a different type of field, different effects can be superimposed, both on the rocket itself and on the space around the rocket / field. Specialist. effects can be handled by a separate entity / controller, which will also be subscribed to events from modifiers.
  4. Well, this is a minimum of code, no service locators, aggregation, dependencies, etc. are needed.


The basis of the gameplay


In this prototype, the essence of the gameplay is to place modifiers on the playing field, adjusting the flight path of the rocket, for flying over obstacles and hitting the destination point / planet. To do this, we have a panel on the right, on which icons of modifiers are located.



Panel class
  [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); } } } 



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


3 and 1 - the so-called magic numbers, which are simply taken from the head and inserted into the code, this should be avoided, but why are they here? The principle by which the right panel is formed has not been defined, and it was decided to just test the prototype for the prototype with just such a number of modifiers.

How to do it right? - at least put in the serializable fields, and set the required quantities through the inspector. Why am I too lazy and should you do it? Here it is necessary to proceed from the general picture; a separate entity and a config will still be responsible for forming the required number of modifiers, so here I was too lazy to rely on greater refactoring in the future. But better not lazy! )

About configs - when the first lectures about ScriptableObject appeared, I liked the idea of ​​storing data as an asset. You get the necessary data where you need, without reference to a single-copy instance. Then there was a lecture on the approach to developing games with ScriptableObject, where storage of instance settings was used. Actually the presets / settings of something stored in an asset is the config.

Consider the config class:
Config class
  [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; } 



What is the essence of his work? It stores a customized data class about the modifier.
  public class DropModifier { public TypeOfForce TypeOfModifier; public Sprite Icon; public GameObject Modifier; public Material Material; } 


The type of the modifier is needed for identification, the icon for the interface, the gamemob itself the game object of the modifier, the material is here so that it can be changed during configuration. The modifiers may already be located on the playing field, and let's say the game designer changes its type, now it gives acceleration, the modifier is inserted from the config and updates all fields, including the material, according to this type of modifier.

Working with the config is very simple - we refer to the configuration for the data for a particular type, we obtain this data, initial visual and possible settings from this data.

Where is the profit?
The benefit is a lot of flexibility, for example, you want to change the material and the icon on the acceleration modifier, or let's assume you can replace the game object as a whole. Instead of rewriting and forwarding into the fields of the inspector, we simply change this data in one config and voila - everything will be updated on all scenes / levels / panels.

And if in the config there are several data for the acceleration modifier?
In the prototype, you can easily track it manually so that the data are not duplicated; on the working draft, you need tests and data validation.

From the icon on the playing field


Class modifier icon
 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; } 



What's going on here?
We grab the icon from the panel, under it we create the gamemoj of the modifier itself. And we actually coordinate the click / click coordinate in the game space, thus moving the modifier in the game space along with the icon in the UI, by the way I advise you to read about RectTransformUtility, this is a great helper class in which there are a lot of features for the interface.

Suppose we change our mind to put a modifier and return it back to the panel,
  if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.layer == 5) 

This piece of code makes us understand what is under the click. Why is there also a check for the layer? And why again the magic number 5? As we remember from the second part, we use the racaster graph not only for the UI, but also for the button that is in the scene, and if we add the functionality to remove already placed modifiers across the field or move them, they will also fall under the raikast graph, therefore there is also an additional check for belonging to the UI layer. This is the default layer, and its order does not change, so the figure 5 here is generally not a magic number.

The result is that if we release the icon above the panel - it will return to the panel, if above the playing field - the modifier remains on the field, the icon will be removed.

The prototype code was spent 1 working day. Plus a little mess on the filing and graphics. In general, the gameplay was found to be suitable, despite a ton of questions on art and game design tweaks. Mission Complete.

Conclusions and recommendations


  1. Lay the minimum architecture, but nevertheless the architecture
  2. Follow the basic principles, but without fanaticism)
  3. Choose simple solutions
  4. Between versatility and speed - for the prototype it is better to choose the speed
  5. For large / medium-sized projects, mean that the project is better rewritten from scratch. For example, now the trend in Unity - DOTS, will have to write a lot of components and systems, on short runs it is bad, you lose on time, on long runs - when all components and systems are registered, the gain in time begins. I do not think it's cool to spend a lot of time on a trend architecture, and find out that the prototype is shitty

All successful prototypes.

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


All Articles