Each developer finds his own advantages and disadvantages of using Corutin in Unity. And he decides in which scenarios to use them, and in which to give preference to alternatives.
In everyday practice, I often use Korutin for various types of tasks. One day, I realized that it was annoying and rejecting many novices in them.
The engine provides only a couple of methods and several overloads for working with corortes:
Run ( docs )
StartCoroutine(string name, object value = null)
StartCoroutine(IEnumerator routine)
Stop ( docs )
StopCoroutine(string methodName)
StopCoroutine(IEnumerator routine)
StopCoroutine(Coroutine routine)
Overloads with string parameters (despite their deceptive convenience) can be immediately send in the trash forget for at least three reasons.
StopCoroutine
did not occur at all. Korutina continued to be executed.On the one hand, the methods provided are quite enough to cover basic needs. But over time, I began to notice that with active use you have to write a large number of sample code - this is tiring and impairs its readability.
In this article I want to describe a small wrapper that I have been using for a long time. Thanks to her, with thoughts of corutines in my head, fragments of the template code with which I had to dance around no longer arise. In addition, the whole team has made it easier to read and understand the components that use coroutines.
Suppose we have the next task - to write a component that allows you to move an object to a given point.
At the moment, it does not matter what method will be performed and in which coordinates. I will select only one of the many options - this is interpolation and global coordinates.
Please note that it is not recommended to move the object by changing its coordinates “to the forehead”, that is, with the transform.position = newPosition
, if the RigidBody
component is used with it (especially in the Update
method ( docs )).
I propose the following implementation variant of the necessary component:
using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; public void Move() { if (moveRoutine == null) StartCoroutine(MoveRoutine()); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } }
In the Move
method, it is very important to start a coruntine only when it is not yet running. Otherwise, they can be run as many as you want and each of them will move the object.
threshold
- tolerance. In other words, the distance to the point, approaching which we will assume that we have reached the goal.
What is it for
Given that all components ( x
, y
, z
) of the Vector3
structure are of type float
, using the result of checking for equality of the distance to the target and the tolerance as a loop condition is a bad idea .
We check the distance to the target for more / less, which allows us to avoid this problem.
Also, if you wish, you can use the Mathf.Approximately
( docs ) method for an approximate test for equality. It is worth noting that with some methods of moving, the speed may be large enough so that the object “jumped” the target in one frame. Then the cycle will never end. For example, if you use the Vector3.MoveTowards
method.
As Vector3
as I know, in the Unity engine for the Vector3
structure, the Vector3
operator is already redefined in such a way that Mathf.Approximately
is called for component-wise equality Mathf.Approximately
.
So far this is all, our component is quite simple. And at the moment there are no problems. But what is this component that allows you to move an object to a point, but does not provide an opportunity to stop it. Let's fix this injustice.
Since you and I decided not to go over to the side of evil, and not to use overloads with string parameters, now we need to save somewhere a link to the running coruntine. Otherwise, how then to stop it?
Add a field:
private Coroutine moveRoutine;
Let's correct the Move
:
public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); }
Add a stop motion method:
public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); }
using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } }
It is quite another matter! Though attached to the wound.
So. We have a small component that performs the task. What is my indignation?
Over time, the project grows, and with it the number of components, including those using coroutines. And each time I have more and more peace here:
StartCoroutine(MoveRoutine());
StopCoroutine(moveRoutine);
One of them makes my eye twitch, and reading this code is a dubious pleasure (I agree, it can be worse). But it would be much nicer and clearer to have something like this:
moveRoutine.Start();
moveRoutine.Stop();
StartCoroutine
you need to remember to save the return value: moveRoutine = StartCoroutine(MoveRoutine());
Otherwise, due to the lack of reference to corutin, you simply can not stop it.
if (moveRoutine == null)
if (moveRoutine != null)
yield break
) it is necessary to reset the field value. moveRoutine = null;
If you forget, you will receive a one-time coruntine. After the first launch in moveRoutine
, the link to corutin will remain, and the new one will not be launched anymore.
The same should be done in the case of a forced stop:
public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } }
public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null; } }
At one fine moment, I really want to endure this whole masquerade once, and for myself leave only the necessary methods: Start
, Stop
and a couple of events and properties.
Let's finally do it!
using System.Collections; using System; using UnityEngine; public sealed class CoroutineObject { public MonoBehaviour Owner { get; private set; } public Coroutine Coroutine { get; private set; } public Func<IEnumerator> Routine { get; private set; } public bool IsProcessing => Coroutine != null; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if (IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } }
Owner
- a link to the MonoBehaviour
instance to which the quorum will be attached. As you know, it should be performed in the context of a specific component, since it is to him that the StartCoroutine
and StopCoroutine
methods StopCoroutine
. Accordingly, we need a reference to the component that will own the cororne.
Coroutine
- the analogue of the moveRoutine
field in the MoveToPoint
component, contains a link to the current corute.
Routine
is the delegate with whom the method that plays the role of coroutine will be communicated.
Process()
is a small wrapper over the main Routine
method. It is necessary in order to be able to trace when the execution of the coroutine will end, reset the link to it and execute another code at this point (if necessary).
IsProcessing
- allows you to find out if the quorutine is currently being executed.
Thus, we get rid of a large number of headaches, and our component takes on a completely different look:
using IEnumerator = System.Collections; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private CoroutineObject moveRoutine; private void Awake() { moveRoutine = new CoroutineObject(this, MoveRoutine); } public void Move() => moveRoutine.Start(); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } }
All that remained was Korutina herself and a few lines of code to work with her. Much better.
Let's say a new task has arrived - you need to add the ability to execute any code after the object has reached the goal.
In the original version, we would have to add an additional delegate parameter to each coroutine, which can be pulled at the end.
private IEnumerator MoveRoutine(System.Action callback) { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null callback?.Invoke(); }
And call as follows:
moveRoutine = StartCoroutine(moveRoutine(CallbackHandler)); private void CallbackHandler() { // do something }
And if there is any lambda as a handler, it looks even worse.
With our own wrapper, you only need to add this event to it once.
public Action Finish;
private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finish?.Invoke(); }
And then, if necessary, subscribe.
moveRoutine.Finished += OnFinish; private void OnFinish() { // do something }
I suppose you have already noticed that the current version of the wrapper provides the opportunity to work only with Korutins without parameters. Therefore, we can write a generalized wrapper for Corutin with one parameter. The rest are done by analogy.
But, for good, it would be nice to first take out the code that will be the same for all wrappers, in some kind of base class, so as not to write the same thing. After all, we are struggling with this.
In it we will remove:
Owner
, Coroutine
, IsProcessing
Finished
using Action = System.Action; using UnityEngine; public abstract class CoroutineObjectBase { public MonoBehaviour Owner { get; protected set; } public Coroutine Coroutine { get; protected set; } public bool IsProcessing => Coroutine != null; public abstract event Action Finished; }
using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject : CoroutineObjectBase { public Func<IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finished?.Invoke(); } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } }
And now, strictly speaking, a wrapper for Corutin with one parameter:
using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject<T> : CoroutineObjectBase { public Func<T, IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<T, IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process(T arg) { yield return Routine.Invoke(arg); Coroutine = null; Finished?.Invoke(); } public void Start(T arg) { Stop(); Coroutine = Owner.StartCoroutine(Process(arg)); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } }
As you can see, the code is almost the same. Only in some places fragments were added, depending on the number of arguments.
Suppose we were asked to update the MoveToPoint component so that the point could be set not through the Inspector
window in the editor, but by code when calling the Move
method.
using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public float speed; public float threshold; private CoroutineObject<Vector3> moveRoutine; public bool IsMoving => moveRoutine.IsProcessing; private void Awake() { moveRoutine = new CoroutineObject<Vector3>(this, MoveRoutine); } public void Move(Vector3 target) => moveRoutine.Start(target); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine(Vector3 target) { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed); yield return null; } } }
There are many options to extend the functionality of this wrapper as much as possible: add a launch with a delay, events with parameters, possible tracking of the progress of the coroutine, and so on. But I suggest to stop at this stage.
The purpose of this article is to share the pressing problems that I encountered and suggest a solution for them, and not to cover the possible needs of all developers.
I hope that both newcomers and experienced comrades will benefit from my experience. Perhaps they will share their comments or point out errors that I could make.
Source: https://habr.com/ru/post/442622/
All Articles