📜 ⬆️ ⬇️

How to make Korutin in Unity a little more convenient

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.


Nightmarish interface


The engine provides only a couple of methods and several overloads for working with corortes:


Run ( docs )



Stop ( docs )



Overloads with string parameters (despite their deceptive convenience) can be immediately send in the trash forget for at least three reasons.



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.


Closer to the point


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 )).


Standard implementation


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; } } } 

A bit about the code

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

All code in its entirety
 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?


Problems and solutions


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(); 


 moveRoutine = StartCoroutine(MoveRoutine()); 

Otherwise, due to the lack of reference to corutin, you simply can not stop it.



 if (moveRoutine == null) 

 if (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; } } 

Code with all edits
 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; } } } 

Debriefing

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:



 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; } 

Wrapper without parameters after refactoring
 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