Prototyping a mobile game, where to start, and how to do it. Part 2

  • Tutorial
For those who missed the first part - Part 1
Next part - Part 3

If anyone is interested in reading about the aggregator used by the event, then here you are , but this is not necessary.

So, we begin to collect everything in a heap




Rocket:

Base rocket class
using DG.Tweening;
using GlobalEventAggregator;
using UnityEngine;
namespace PlayerRocket
{
    public class Rocket : PlayerRocketBase
    {
        [SerializeField] private float pathСorrectionTime = 10;
        private Vector3 movingUp = new Vector3(0, 1, 0);
        protected override void StartEventReact(ButtonStartPressed buttonStartPressed)
        {
            transform.SetParent(null);
            rocketState = RocketState.MOVE;
            transform.DORotate(Vector3.zero, pathСorrectionTime);
        }
        protected override void Start()
        {
            base.Start();
            EventAggregator.Invoke(new RegisterUser { playerHelper = this });
            if (rocketState == RocketState.WAITFORSTART)
                return;
            RocketBehaviour();
        }
        private void FixedUpdate()
        {
            RocketBehaviour();
        }
        private void RocketBehaviour()
        {
            switch (rocketState)
            {
                case RocketState.WAITFORSTART:
                    if (inputController.OnTouch && !inputController.OnDrag)
                        rocketHolder.RotateHolder(inputController.worldMousePos);
                    break;
                case RocketState.MOVE:
                    rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime));
                    forceModel.AddModificator();
                    break;
                case RocketState.STOP:
                    Debug.Log("мы стопаемся");
                    rigidbody.velocity = Vector3.zero;
                    rigidbody.drag = 50;
                    rocketState = RocketState.COMPLETESTOP;
                    break;
                case RocketState.COMPLETESTOP:
                    break;
                default:
                    rocketState = RocketState.COMPLETESTOP;
                    break;
            }
        }
    }
}


What do we need for a rocket to take off? In the playing space, we need a conditional planet with which we start, a start button and a rocket. What should a rocket be able to do?

  1. Wait for the start
  2. Fly
  3. Be affected by modifiers
  4. Stop

That is, we have different behavior / state of the rocket, depending on the current state, the rocket should provide different behavior. In programming, we are constantly faced with a situation where an object can have many radically different behaviors.

For complex behavior of objects - it is better to use behavioral patterns, for example, a state pattern. For the simple ones, novice programmers often use a lot a lot if if else. I recommend using switch and enum. Firstly, this is a clearer division of logic into specific stages, thanks to this we will know exactly what state we are in now, and what is happening, there are fewer opportunities to turn the code into a noodle of dozens of exceptions.

How it works:

First we start enum with the states we need:

 public enum RocketState
    {
        WAITFORSTART = 0,
        MOVE = 1,
        STOP = 2,
        COMPLETESTOP = 3,
    }

In the parent class we have a field -
protected RocketState rocketState;

By default, the first value is assigned to it. Enum itself sets the default values, but for data that can be changed from above or configured by game designers - I manually set the values, for what? In order to be able to add another value to the inam anywhere and not violate the stored data. I also advise you to study flag enum.

Next:

We determine the behavior itself in the update, depending on the value of the rocketState field

 private void FixedUpdate()
        {
            RocketBehaviour();
        }
        private void RocketBehaviour()
        {
            switch (rocketState)
            {
                case RocketState.WAITFORSTART:
                    if (inputController.OnTouch && !inputController.OnDrag)
                        rocketHolder.RotateHolder(inputController.worldMousePos);
                    break;
                case RocketState.MOVE:
                    rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime));
                    forceModel.AddModificator();
                    break;
                case RocketState.STOP:
                    Debug.Log("мы стопаемся");
                    rigidbody.velocity = Vector3.zero;
                    rigidbody.drag = 50;
                    rocketState = RocketState.COMPLETESTOP;
                    break;
                case RocketState.COMPLETESTOP:
                    break;
                default:
                    rocketState = RocketState.COMPLETESTOP;
                    break;
            }
        }

I will decipher what is happening:

  1. When we wait, we simply rotate the rocket towards the mouse cursor, thus setting the initial trajectory
  2. The second state - we fly, accelerate the rocket in the right direction, and update the modifier model for the appearance of objects affecting the trajectory
  3. The third state is when the team arrives at us to stop, here we work out everything so that the rocket stops and translates into the state - we completely stopped.
  4. The last state is that we are doing nothing.

The convenience of the current pattern - it is all very easily expandable and adjustable, but there is one thing but a weak link - this is when we can have a state that combines a number of other states. Here either a flag inam, with a complication of processing, or already switch to more "heavy" patterns.

We figured out the rocket. The next step is a simple but funny object - the start button.

Start button


The following functionality is required of her - clicked, she notified that they clicked on her.

Start Button Class
using UnityEngine;
using UnityEngine.EventSystems;
public class StartButton : MonoBehaviour, IPointerDownHandler
{
    private bool isTriggered;
    private void ButtonStartPressed()
    {
        if (isTriggered)
            return;
        isTriggered = true;
        GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());
        Debug.Log("поехали");
    }
    public void OnPointerDown(PointerEventData eventData)
    {
        ButtonStartPressed();
    }
}
public struct ButtonStartPressed { }


According to game design, this is a 3D object on the stage, the button is supposed to be integrated into the design of the starting planet. Well, ok, there is a nuance - how to track a click on an object in a scene?

If we google, we will find a bunch of OnMouse methods, among which there will be a click. It would seem like an easy choice, but it is just very bad, starting with the fact that it often works crookedly (there are many nuances for tracking clicks), “dear”, ending with the fact that it does not give that tons of buns that are in UnityEngine.EventSystems.

In the end, I recommend using UnityEngine.EventSystems and the interfaces IPointerDownHandler, IPointerClickHandler. In their methods, we realize the reaction to pressing, but there are several nuances.

  1. An EventSystem must be present in the scene, this is an object / class / component of the unit, usually created when we create the canvas for the interface, but you can also create it yourself.
  2. Physics RayCaster must be present on the camera (this is for 3D, for 2D graphics there is a separate racaster)
  3. There must be a collider at the facility

In the project, it looks like this:



Now the object tracks the click and this method is called:


public void OnPointerDown(PointerEventData eventData)
    {
        ButtonStartPressed();
    }
    private void ButtonStartPressed()
    {
        if (isTriggered)
            return;
        isTriggered = true;
        GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed());
        Debug.Log("поехали");
    }

What happens here:

We have a Boolean field in which we track whether the button is pressed or not (this is protection against repeated pressing so that we do not have a start script every time).

Next, we call the event - the button is pressed, which the rocket class is subscribed to, and put the rocket in a state of motion.

Jumping ahead a bit - why is it here and there for events? This is an event-oriented programming. Firstly, an event model is cheaper than continuous data processing in order to find out their changes. Secondly, this is the weakest connection, we don’t need to know on the rocket that there is a button, that someone pressed it, and so on, we just know that there is an event to start, we received it and are acting. Further, this event is interesting not only for the rocket, for example, a panel with modifiers is signed for the same event, it is hidden at the start of the rocket. Also, this event may be of interest to the input controller - and user input may not be processed or processed differently after the launch of the rocket.

Why don't many programmers like the event paradigm? Because a ton of events and subscriptions to these events easily turn the code into noodles, in which it is not at all clear where to start and whether it will end somewhere, not to mention the fact that you also need to monitor your unsubscribe / subscription and keep all objects alive.

And that is why for the implementation of events I use my event aggregator, which in fact does not transmit events, but data containers through events, and classes subscribe to the data that interests them. Also, the aggregator itself monitors live objects and throws dead objects out of subscribers. Thanks to the transfer of the container, the injection is also possible; you can pass a link to the class of interest to us. By the container, you can easily track who processes and sends this data. For prototyping is a great thing.

Rocket rotation to determine the starting path



According to the game design, the rocket should be able to rotate around the planet to determine the initial trajectory, but not more than a certain angle. The rotation is carried out by the touch - the rocket simply follows the finger and is always directed to the place where we poked at the screen. By the way, it’s just the prototype that made it possible to determine that this is a weak point and there are many episodes associated with management that are bordering on this functionality.

But in order:

  1. We need the rocket to turn relative to the planet in the direction of the wheelbarrow
  2. We need to clamp the rotation angle

As for rotation relative to the planet - you can slyly rotate around the axis and calculate the rotation axis, or you can simply create an object with a dummy centered inside the planet, move the rocket there, and quietly rotate the dummy around the Z axis, the dummy will have a class that will determine the behavior of the object. The rocket will spin with it. The object I called RocketHolder. We figured it out.

Now about the restrictions on turning and turning in the direction of the wheelbarrow:

class RocketHolder
using UnityEngine;
public class RocketHolder : MonoBehaviour
{
    [SerializeField] private float clampAngle = 45;
    private void Awake()
    {
        GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent obj) => obj.inject(this));
    }
    private float ClampAngle(float angle, float from, float to)
    {
        if (angle < 0f) angle = 360 + angle;
        if (angle > 180f) return Mathf.Max(angle, 360 + from);
        return Mathf.Min(angle, to);
    }
    private Vector3 ClampRotationVectorZ (Vector3 rotation )
    {
        return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle));
    }
    public void RotateHolder(Vector3 targetPosition)
    {
        var diff = targetPosition - transform.position;
        diff.Normalize();
        float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90);
        transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles);
    }
}


Despite the fact that the game is in theory 3D, but the whole logic and gameplay is actually 2D. And we just need to tighten the rocket around the Z axis in the direction of the place of pressing. At the end of the method, we clamp the degree of rotation by the value specified in the inspector. In the Awake method, you can see the most correct implementation of a class injection through an aggregator.

Inputcontroller


One of the most important classes, it is he who collects and processes user behavior. Pressing hotkeys, gamepad buttons, keyboards, etc. I have a fairly simple input in the prototype, in fact you need to know only 3 things:

  1. Is there a click and its coordinates
  2. Is there a vertical swipe and how much to swipe
  3. Do I operate with interface / modifiers

class InputController
using System;
using UnityEngine;
using UnityEngine.EventSystems;
public class InputController : MonoBehaviour
{
    public const float DirectionRange = 10;
    private Vector3 clickedPosition;
    [Header("расстояние после которого мы считаем свайп")]
    [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f;
    [Header("скорость вертикального скролла")]
    [SerializeField] private float speedOfVerticalScroll = 2;
    public ReactiveValue ReactiveVerticalScroll { get; private set; }
    public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition);
    public bool OnTouch { get; private set; }
    public bool OnDrag { get; private set; }
    // Start is called before the first frame update
    private void Awake()
    {
        ReactiveVerticalScroll = new ReactiveValue();
        GlobalEventAggregator.EventAggregator.AddListener(this, (ImOnDragEvent obj) => OnDrag = obj.IsDragging);
        GlobalEventAggregator.EventAggregator.AddListener>(this, InjectReact);
    }
    private void InjectReact(InjectEvent obj)
    {
        obj.inject(this);
    }
    private void OnEnable()
    {
        GlobalEventAggregator.EventAggregator.Invoke(this);
    }
    void Start()
    {
        GlobalEventAggregator.EventAggregator.Invoke(this);
    }
    private void MouseInput()
    {
        if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.gameObject.layer == 5)
            return;
        if (Input.GetKeyDown(KeyCode.Mouse0))
            clickedPosition = Input.mousePosition;
        if (Input.GetKey(KeyCode.Mouse0))
        {
            if (OnDrag)
                return;
            VerticalMove();
            OnTouch = true;
            return;
        }
        OnTouch = false;
        ReactiveVerticalScroll.CurrentValue = 0;
    }
    private void VerticalMove()
    {
        if ( Math.Abs(Input.mousePosition.y-clickedPosition.y) < afterThisDistanceWeGonnaDoSwipe)
            return;
        var distance = clickedPosition.y + Input.mousePosition.y * speedOfVerticalScroll;
        if (Input.mousePosition.y > clickedPosition.y)
            ReactiveVerticalScroll.CurrentValue = distance;
        else if (Input.mousePosition.y < clickedPosition.y)
            ReactiveVerticalScroll.CurrentValue = -distance;
        else
            ReactiveVerticalScroll.CurrentValue = 0;
    }
    // Update is called once per frame
    void Update()
    {
        MouseInput();
    }
}
}


It's all in the forehead and without troubles, interesting may be the primitive implementation of reactive proprietary - when I was just starting to program, it was always interesting how to find out that the data has changed, without constant ventilation of the data. Well, that's it.

It looks like this:

class ReactiveValue
public class ReactiveValue where T: struct
{
    private T currentState;
    public Action OnChange;
    public T CurrentValue
    {
        get => currentState;
        set
        {
            if (value.Equals(currentState))
                return;
            else
            {
                currentState = value;
                OnChange?.Invoke(currentState);
            }
        }
    }
}


We subscribe to OnChange, and we twitch if only the value has changed.

Regarding prototyping and architecture - the tips are the same, public only properties and methods, all data should only be changed locally. Any processing and calculations - add up according to separate methods. As a result, you can always change the implementation / calculation, and this will not affect the external users of the class. That's all for now, in the third final part - about modifiers and interface (drag drop). And I plan to put the project on git so that I can see / feel. If you have questions about prototyping - ask, I'll try to clearly answer.

Also popular now: