For those who missed the first part -
Part 1
Next part -
Part 3
If anyone is interested in reading about the used event aggregator, then you are
here , but this is not necessary.
So, we start to collect everything in a bunch
Rocket:
')
Base missile classusing DG.Tweening; using GlobalEventAggregator; using UnityEngine; namespace PlayerRocket { public class Rocket : PlayerRocketBase { [SerializeField] private float pathorrectionTime = 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, pathorrectionTime); } 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 a rocket to take off? In the game space, we need a conditional planet with which we start, a start button and a rocket. What should be able to rocket?
- Wait for the start
- Fly
- Be affected by modifiers
- Stop
That is, we have a different behavior / state of the rocket, depending on the current state, the rocket must provide different behavior. In programming, we are constantly faced with a situation where an object can have many radically different behaviors.
For complex object behaviors, it is better to use behavioral patterns, such as the state pattern. For simple - novice programmers often use many many if else. I recommend using switch and enum. Firstly, this is a clearer division of logic into concrete stages, thanks to which we will know exactly what state we are in and what is happening, there are fewer opportunities to turn code into noodles from dozens of exceptions.
How it works:
First we start the 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, it is assigned the first value. Enum defaults itself to setting values, but for data that can be changed from above or tuned by game designers - I prescribe values ​​manually, for what? In order to be able to add another value ina to any place and not violate the stored data. I also advise you to study the flag enum.
Further:
We define 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; } }
Decipher what's going on:
- When we wait, we simply rotate the rocket in the direction of the mouse cursor, thus setting the initial trajectory
- The second state is that we fly, accelerate the rocket in the right direction, and update the model of modifiers for the appearance of objects affecting the trajectory.
- The third state is when the team arrives to stop us, then we work on everything so that the rocket stops and is transferred to the state - we completely stopped.
- The last state - we are doing nothing.
The convenience of the current pattern is all very easily expanded and regulated, but there is one thing, but the weak link is when we can have a state that combines a number of other states. Here either flag inam, with complication of processing, or already to pass to more "heavy" patterns.
With a rocket figured out. The next step is a simple but fun object - the start button.
Start button
The following functionality is required from it - they clicked, she notified that they clicked on it.
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 { }
Game design 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 you google then we will find a bunch of OnMouse methods, among which there will be a click. It would seem an easy choice, but it’s just very bad, starting with the fact that it often works crookedly (there are a lot of nuances in tracking pressure), “expensive”, ending with the fact that it doesn’t give the ton of buns that UnityEngine.EventSystems has.
As a result, I recommend using UnityEngine.EventSystems and interfaces - IPointerDownHandler, IPointerClickHandler. In their methods, we implement the response to pressing, but there are several nuances.
- The scene must have an EventSystem, this is an object / class / component of the unit, usually created when we create a canvas for the interface, but you can also create it yourself.
- Physics RayCaster must be present on the camera (this is for 3D, for 2D graphics there is a separate racaster)
- The facility must have a collider.
In the project it looks like this:
The object now tracks clicks 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's going on here:
We have a boolean field in which we monitor whether the button is pressed or not (this is protection against repeated presses so that the launch script does not start every time).
Then we call the event - the button is pressed, on which the rocket class is signed, and we transfer the rocket to the state of motion.
Looking ahead a bit - why are there pretty often events here? This is event-oriented programming. Firstly, the event model is cheaper than continuous data processing in order to ascertain 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 act. 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 launch of the rocket. This event can also be an interesting input to the controller - and user input may not be processed or handled 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’s not at all clear where to start and whether it ends somewhere, not to mention the fact that you also need to follow the unsubscribe / subscription and that all objects are alive.
And that is why for the implementation of events I use my event aggregator, which in fact transfers not data events, but data containers through events, and classes subscribe to the data they are interested in. Also, the aggregator itself monitors living objects and throws out dead objects from subscribers. Thanks to the transfer of the container, it is also possible to implement the injection, you can send a link to the class of interest. By container, you can easily track who is processing and sending this data. Prototyping is a great thing.
Rotation of the rocket to determine the starting trajectory
According to game design, a rocket should be able to rotate around the planet to determine the initial trajectory, but not more than some angle. The rotation is carried out by a tachim - the rocket just keeps track of the finger and is always directed at the place where we poked at the screen. By the way, just the prototype allowed us to determine that this is a weak point and there are many borderline episodes related to this functionality.
But in order:
- We need the rocket to rotate relative to the planet towards the wheelbarrow
- We need to clamp the rotation angle.
As for turning relative to the planet, you can slyly rotate around the axis and calculate the axis of rotation, or you can simply create an object with a center inside the planet, move the rocket there, and calmly rotate the pacifier around the Z axis, the pacifier will have a class that will determine the object's behavior. The rocket will rotate with it. The object I called RocketHolder. With this sorted out.
Now, about turning restrictions and turning towards the wheelbarrow:
lass RocketHolder using UnityEngine; public class RocketHolder : MonoBehaviour { [SerializeField] private float clampAngle = 45; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> 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 3d in theory, but all the logic and gameplay is actually 2d. And we just need to rotate the rocket around the Z axis in the direction of the click. 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 the injection class through the aggregator.
InputController
One of the most important classes, it is he who collects and processes user behavior. Press hotkeys, gamepad buttons, keyboards, and so on. I have a fairly simple input in the prototype, in fact you only need to know 3 things:
- Is there a click and its coordinates
- Is there a vertical swipe and how much is it connected
- 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<float> ReactiveVerticalScroll { get; private set; } public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition); public bool OnTouch { get; private set; } public bool OnDrag { get; private set; }
It's all in the forehead and without problems, from the interesting can be a primitive implementation of reactive perpet - when I was just starting to program, it was always interesting how to find out that the data had changed without constant ventilation of the data. Well, that's it.
It looks like this:
class ReactiveValue public class ReactiveValue<T> where T: struct { private T currentState; public Action<T> OnChange; public T CurrentValue { get => currentState; set { if (value.Equals(currentState)) return; else { currentState = value; OnChange?.Invoke(currentState); } } } }
Subscribe to OnChange, and jerk if the value has changed.
Regarding prototyping and architecture - the tips are the same, public only properties and methods, all data should be changed only locally. Any processing and calculations - add on separate methods. As a result, you can always change the implementation / calculations, 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 drops). And I plan to put the project on git so that you can see / touch. If you have questions about prototyping, ask, I will try to clearly answer.