📜 ⬆️ ⬇️

UniRx - Rx for Unity3d

Hello! I have long wanted to write an article about UniRx on Unity3d. Let's start with a little philosophy of RX programming. For example, when developing a game, we create a button, observe a click event of this button and react to it with some kind of code.

Reactive programming is all the same, only on steroids, that is, we can create data streams of everything. And also watch them and respond. Update, OnCollisionEnter, Coroutine, Event, Mouse input, Keyboard input, Joystick input are all streams.
All that surrounds us is flows.

image

In addition, we are provided with an amazing set of functions for combining, creating and filtering any of these streams. This is where “functional” magic happens. A stream can be used as data for another. Even several streams can be used as data in another stream. You can combine two streams. You can filter the stream to get another one that has only those events that interest you. You can map data values ​​from one stream to another new.
')

Streams


In this example, we track the keystroke and turn it into an event.

void Start () { Observable.EveryUpdate() //  update .Where(_ => Input.anyKeyDown) //      .Select(_ => Input.inputString) //    .Subscribe (x => { //  OnKeyDown (x); //   OnKeyDown c    }).AddTo (this); //    gameobject- } private void OnKeyDown (string keyCode) { switch (keyCode) { case "w": Debug.Log ("keyCode: W"); break; default: Debug.Log ("keyCode: "+keyCode); break; } } 

MultiThreading


In this example, we execute a heavy method in thread-e and use the result already obtained in main thread-e.

 Observable.Start (() => { //  Observable  thread int n = 100000000; int res = Fibonacci(n); //    return res; //   }).ObserveOnMainThread () //     main thread- .Subscribe (xs => { //  Debug.Log ("res: "+xs); //     main thread- }).AddTo (this); 

Thus it is very convenient to perform long calculations and work with the network.

Httprequest


Since we are talking about working with the network, here is a small example of working with http requests.

 var request = ObservableWWW.Get("http://api.duckduckgo.com/?q=habrahabr&format=json") .Subscribe(x => { //  Debug.Log ("res: "+x); //  }, ex => { //    Debug.Log ("error: "+ex); }); // request.Dispose ();    

Coroutines
Here we simultaneously launch 3 cortices, turn them into streams and merge them into one stream. Next, subscribe to this thread.

 void Start () { Observable.WhenAll ( //  WhenAll    Observable  Observable.FromCoroutine (AsyncA), //      Observable Observable.FromCoroutine (AsyncB), Observable.FromCoroutine (AsyncC) ).Subscribe (_ => { //        WhenAll Debug.Log ("end"); }).AddTo (this); } IEnumerator AsyncA () { Debug.Log("a start"); yield return new WaitForSeconds (1); Debug.Log("a end"); } IEnumerator AsyncB () { Debug.Log("b start"); yield return new WaitForFixedUpdate (); Debug.Log("b end"); } IEnumerator AsyncC () { Debug.Log("c start"); yield return new WaitForEndOfFrame (); Debug.Log("c end"); } 

We can also run cortinas alternately.

 Observable.FromCoroutine (AsyncA) //   AsyncA .SelectMany (AsyncB) // AsyncB     AsyncA .SelectMany (AsyncC) //    AsynC .Subscribe(_ => { Debug.Log ("end"); }).AddTo (this); 

Asynchronous scene loading


 SceneManager.LoadSceneAsync ("HeavyScene") //    .AsAsyncOperationObservable () //    Observable  .Do (x => { //     Debug.Log ("progress: " + x.progress); //   }).Subscribe (_ => { //  Debug.Log ("loaded"); }).AddTo (this); 

It is very convenient to use if you first load a light scene with loading screen and already
then asynchronously load a heavy scene, the animations in the loading screen will not hang.

Asynchronous resource loading


 void Start () { SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer> (); Resources.LoadAsync<Sprite> ("sprite") //    .AsAsyncOperationObservable () //    Observable  .Subscribe (xs => { //  if (xs.asset != null) { //   null Sprite sprite = xs.asset as Sprite; //  asset  sprite spriteRenderer.sprite = sprite; //   sprite- } }).AddTo (this); } 

This way you can load prefabs, text resources, and so on.

It is also a very handy thing to use when you need to load heavy prefabs or sprites, the game becomes truly responsive, because while something is loading the game does not slow down.

Timers


 void Start () { Observable.Timer (System.TimeSpan.FromSeconds (3)) //  timer Observable .Subscribe (_ => { //  Debug.Log (" 3 "); }).AddTo (disposables); //    disposable Observable.Timer (System.TimeSpan.FromSeconds (1)) //  timer Observable .Repeat () //    .Subscribe (_ => { //  Debug.Log (" 1 "); }).AddTo (disposables); //    disposable } void OnEnable () { //  disposable disposables = new CompositeDisposable(); } void OnDisable () { //   if (disposables != null) { disposables.Dispose (); } } 

MessageBroker


UniRx MessageBroker is an RX publisher-subscriber system, filtered by type.

Publisher Subscriber (publisher-subscriber or pub / sub English) is a behavioral design pattern of messaging in which message senders, called publishers, are not directly tied to the subscribers' program code. . Instead, messages are divided into classes and do not contain information about their subscribers, if any. Similarly, subscribers deal with one or more classes of messages, abstracting from specific publishers.

Sometimes when developing a game, we need to call a method on the component to which we do not have direct access. Of course, we can use DI or Singleton, but it all depends on the specific case. And this is when we need to call the method on many objects, or when we just want to use MessageBroker.

As it was written above, filtering is done by type and in order not to create a bunch of classes for each subscriber, I created a MessageBase class in which there are fields: sender (MonoBehaviour) for simplified debugging and functionality, id (int) through it, and we will do filtering, data ( System.Object) to transfer some data that needs to be cast. Also in this class there is a static method (Create) which creates and returns us a MessageBase.

Messagebase

 public class MessageBase { public MonoBehaviour sender {get; private set;} // MonoBehaviour  public int id {get; private set;} // id  public System.Object data {get; private set;} //  public MessageBase (MonoBehaviour sender, int id, System.Object data) { this.sender = sender; this.id = id; this.data = data; } public static MessageBase Create (MonoBehaviour sender, int id, System.Object data) { return new MessageBase (sender, id, data); } } 

I also created the ServiceShareData class for simplified debugging where all the messages are stored for the moment. This is to ensure that in the development process there is no leakage of the message and confusion in the code.

ServiceShareData

 public class ServiceShareData { public const int MSG_ATTACK = 1001; } 

Example of sending a message

 MessageBroker.Default .Publish (MessageBase.Create ( this, // sender MonoBehaviour ServiceShareData.MSG_ATTACK, // message id "attack!" // data System.Ojbect )); 

The Publish method sends a class that is filtered by type. We here send MessageBase with sender-ohm this, id by which will be filtering, and at the end of data which in theory can be anything.

Message Acceptance Example

 public CompositeDisposable disposables; void OnEnable () { disposables = new CompositeDisposable(); MessageBroker.Default .Receive<MessageBase>() //   MessageBase .Where(msg => msg.id == ServiceShareData.MSG_ATTACK)// message  id .Subscribe(msg => { //  string data = (string)msg.data; //      //      sender-     Debug.Log ("sender:"+msg.sender.name+" receiver:"+name+" data:"+data); }).AddTo (disposables); } void OnDisable () { //  if (disposables != null) { disposables.Dispose (); } } 

The Receive method has a generic type by which it is filtered. In Where we already do filtering by message id. It is important to understand that the subscriber (message recipient) will cast data depending on the message id.

MVP


An example of a classic MVP pattern. Where Model serves for data storage and also serialization of data deserialization and so on. View to display this same data. Well, Presenter is responsible for business logic.

Model

  public class SomeModel { public ReactiveProperty<int> count { get; private set; } public SomeModel () { count = new ReactiveProperty<int> (0); //  ReactiveProperty c 0 } } 

As you can see here there is a ReactiveProperty in this case, it is an int on steroids for changes of which we can subscribe and respond.

View

 public class SomeView : MonoBehaviour { public Text someText; public Button someButton; public void RenderCount (int count) { //   count someText.text = count.ToString (); } public void AnimateButton () { //   someButton.transform .DOShakeScale (0.5F, 0.3F) //   .OnComplete (() => { //       someButton.transform.localScale = Vector3.one; }); } } 

View has a text and a button. As well as methods for animating buttons and displaying data. For animation, DoTween asset is used.

Presenter

 public class SomePresenter : MonoBehaviour { public SomeView someView; // view      public SomeModel someModel = new SomeModel (); // model void Start () { someModel.count // ReactiveProperty count .ObserveEveryValueChanged (x => x.Value) //     .Subscribe (xs => { //  someView.RenderCount (xs); //     }).AddTo (this); someView.someButton //  .OnClickAsObservable () //    Observable  .Subscribe (_ => OnClick (someView.someButton.GetInstanceID ())) .AddTo (this); } private void OnClick (int buttonId) { if (buttonId == someView.someButton.GetInstanceID ()) { someModel.count.Value++; someView.AnimateButton (); } } } 

As you can see, we first subscribe to the changes in the reactiveProperty in order to draw only the changes.

Next, we subscribe to the button where, when clicked, the OnClick method will be called in which we push the instanceId (Unity3d guarantees its uniqueness) of this button.

And in OnClick there is a check for this very intanceId, then with a click we increase the count (reactiveProperty) and animate the button.

I will be glad to questions and comments.

I recommend reading:
github.com/neuecc/UniRx
gist.github.com/staltz/868e7e9bc2a7b8c1f754
www.reactivemanifesto.org

Source

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


All Articles