Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Two way script communication

Discussion in 'Scripting' started by kailin89, Jun 5, 2020.

  1. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    This is probably an opinion based question, but how would you handle two way script communication?

    For example - I have a UI manager that handles button clicks, showing panels, and also a Network Manager, that handles network requests (joining rooms, starting games).

    Say the user clicks on a button to join a room, the UI calls that method from the Network Manager, which then lets the UI Manager know when it has joined.

    At the moment, both scripts hold references to one another, so obviously they're tightly coupled.

    What other options do I have for this?

    Thought about using Singleton for the Network Manager, and for the UI to register for Actions for each given Network Manager state but it seems kinda convoluted.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,724
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Some one is going to have to know of something in the end. Who ever injects that information is up to you. I don't know what a "UIManager" is in this case... is this what defines how the UI is layed out? I'd think that'd be the job of the Canvas and what not... so my take away is the UIManager is the thing that decides "what the buttons do".

    If it is true that UIManager defines what the UI does... then it would likely know about the NetworkManager since it's where the code that calls the NetworkManager resides.

    But there's no reason NetworkManager would need to know about UIManager since it just does a thing and responds back on completion.

    There's many ways to do this...

    events:
    Code (csharp):
    1.  
    2. public class UIManager
    3. {
    4.  
    5.     public NetworkManager Network;
    6.     public Button ConnectButton;
    7.  
    8.     void Start()
    9.     {
    10.         ConnectButton.onClick.AddListener(OnConnectButtonClick);
    11.     }
    12.  
    13.     private void OnConnectButtonClick()
    14.     {
    15.         var cmd = new ConnectCommand(this, Network);
    16.         cmd.Execute();
    17.     }
    18.  
    19.     private class ConnectCommand
    20.     {
    21.         UIManager ui;
    22.         NetworkManager manager;
    23.      
    24.         public ConnectCommand(UIManager ui, NetworkManager manager)
    25.         {
    26.             this.ui = ui;
    27.             this.manager = manager;
    28.         }
    29.      
    30.         public void Execute()
    31.         {
    32.             ui.ConnectButton.interactable = false;
    33.             manager.OnConnectComplete += OnComplete;
    34.             manager.DoConnect();
    35.         }
    36.      
    37.         private void OnComplete(object sender, NetworkManager.ConnectEventArgs e)
    38.         {
    39.             ui.ConnectButton.interactable = true;
    40.             manager.OnConnectComplete -= OnComplete;
    41.             //do whatever else is necessary based on e.Success
    42.         }
    43.      
    44.     }
    45.  
    46. }
    47.  
    48. public class NetworkManager
    49. {
    50.  
    51.     public event System.EventHandler<ConnectEventArgs> OnConnectComplete;
    52.  
    53.     public void DoConnect()
    54.     {
    55.         //do stuff, this is likely don async since it's going to call up to a server
    56.         //so likely a thread is spun out:
    57.         Task.Run(() => {
    58.             //do connection
    59.             OnConnectComplete?.Invoke(this, new ConnectEventArgs() { Success = success });
    60.         });
    61.     }
    62.  
    63.     public class ConnectEventArgs : System.EventArgs
    64.     {
    65.         public bool Success;
    66.     }
    67.  
    68. }
    69.  
    Or async:
    Code (csharp):
    1.  
    2. public class UIManager
    3. {
    4.  
    5.     public NetworkManager Network;
    6.     public Button ConnectButton;
    7.  
    8.     void Start()
    9.     {
    10.         ConnectButton.onClick.AddListener(OnConnectButtonClick);
    11.     }
    12.  
    13.     private async void OnConnectButtonClick()
    14.     {
    15.         var cmd = new ConnectCommand(this, Network);
    16.         await cmd.Execute();
    17.     }
    18.  
    19.     private class ConnectCommand
    20.     {
    21.         UIManager ui;
    22.         NetworkManager manager;
    23.      
    24.         public ConnectCommand(UIManager ui, NetworkManager manager)
    25.         {
    26.             this.ui = ui;
    27.             this.manager = manager;
    28.         }
    29.      
    30.         public async Task Execute()
    31.         {
    32.             ui.ConnectButton.interactable = false;
    33.             bool success = await manager.DoConnect();
    34.             ui.ConnectButton.interactable = true;
    35.             //do whatever else is necessary based on success
    36.         }
    37.      
    38.     }
    39.  
    40. }
    41.  
    42. public class NetworkManager
    43. {
    44.  
    45.     public Task<bool> DoConnect()
    46.     {
    47.         //do stuff, this is likely don async since it's going to call up to a server
    48.         //so likely a thread is spun out:
    49.         return Task.Run(this.DoWork);
    50.     }
    51.  
    52.     private async Task<bool> DoWork()
    53.     {
    54.         //do connection await style
    55.         return success;
    56.     }
    57. }
    58.  
    Or a callback:
    Code (csharp):
    1.  
    2. public class UIManager
    3. {
    4.  
    5.     public NetworkManager Network;
    6.     public Button ConnectButton;
    7.  
    8.     void Start()
    9.     {
    10.         ConnectButton.onClick.AddListener(OnConnectButtonClick);
    11.     }
    12.  
    13.     private void OnConnectButtonClick()
    14.     {
    15.         var cmd = new ConnectCommand(this, Network);
    16.         cmd.Execute();
    17.     }
    18.  
    19.     private class ConnectCommand
    20.     {
    21.         UIManager ui;
    22.         NetworkManager manager;
    23.      
    24.         public ConnectCommand(UIManager ui, NetworkManager manager)
    25.         {
    26.             this.ui = ui;
    27.             this.manager = manager;
    28.         }
    29.      
    30.         public void Execute()
    31.         {
    32.             ui.ConnectButton.interactable = false;
    33.             manager.DoConnect((success) => {
    34.                 ui.ConnectButton.interactable = true;
    35.                 //do whatever else is necessary based on 'success'
    36.             });
    37.         }
    38.      
    39.     }
    40.  
    41. }
    42.  
    43. public class NetworkManager
    44. {
    45.  
    46.     public void DoConnect(Action<bool> callback)
    47.     {
    48.         //do stuff, this is likely don async since it's going to call up to a server
    49.         //so likely a thread is spun out:
    50.         Task.Run(() => {
    51.             //do connection
    52.             callback(success);
    53.         });
    54.     }
    55.  
    56. }
    57.  
    Or a Coroutine as a token:
    Code (csharp):
    1.  
    2. public class UIManager : MonoBehaviour
    3. {
    4.  
    5.     public NetworkManager Network;
    6.     public Button ConnectButton;
    7.  
    8.     void Start()
    9.     {
    10.         ConnectButton.onClick.AddListener(OnConnectButtonClick);
    11.     }
    12.  
    13.     private void OnConnectButtonClick()
    14.     {
    15.          this.StartCoroutine(this.ConnectCommand());
    16.     }
    17.  
    18.     private IEnumerator ConnectCommand()
    19.     {
    20.         this.ConnectButton.interactable = false;
    21.         yield return Network.DoConnect();
    22.         this.ConnectButton.interactable = true;
    23.         //do whatever is necessary based on Network.Succeeded
    24.     }
    25.  
    26. }
    27.  
    28. public class NetworkManager : MonoBehaviour
    29. {
    30.  
    31.     public bool Succeeded;
    32.  
    33.     public Coroutine DoConnect()
    34.     {
    35.         return this.StartCoroutine(this.DoConnectWork());
    36.     }
    37.  
    38.     private IEnumerator DoConnectWork()
    39.     {
    40.         this.Succeeded = false;
    41.         //do connection
    42.         this.Succeeded = success;
    43.     }
    44.  
    45. }
    46.  
    NOTE - I use a Task.Run in all the examples because honestly I don't know what your connection code looks like. I just assume it's probably asynchronous in some way. It could be a coroutine for all I know.

    The point here is that NetworkManager doesn't have to know about who called it for any reason. It just needs a way to signal that it completed stuff.

    May that be:
    event
    async Task
    callback
    coroutine
    some other token (like you could use a CustomYieldInstruction in the coroutine example)

    WARNING - I wrote all these examples slap dash here in the browser... I probably typoed and maybe left remnants of previous examples in later... keep that in mind. These are NOT cut/paste examples. They serve a demonstrative purpose as pseudocode.
     
    kailin89 likes this.
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    I made a package called datasacks. It's a general purpose Unity Editor-friendly data storage and interprocess communication / signaling library. As @lordofduct pointed out, "Some one is going to have to know of something in the end" and with Datasacks you would simply agree how to communicate with each other: pick a Datasack and start talking through it.

    Right now it is intended for primitive signaling and IPC (payloads of void, string, int, bool, float, and a few common structures) but via partial classes you can trivially append any payload you want onto an existing Datasack and have any number of consumers subscribe to it.

    A rough schematic of how it works:

    20180724_datasacks_overview.png

    Datasacks is presently hosted at these locations:

    https://bitbucket.org/kurtdekker/datasacks

    https://github.com/kurtdekker/datasacks

    https://gitlab.com/kurtdekker/datasacks

    https://sourceforge.net/projects/datasacks/
     
    kailin89 and lordofduct like this.
  5. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    Thanks lordofduct for the detailed response, lots to think about. Haven't thought about spinning off the actual command into its own class. I would assume then, that for each command, you'd have to individually write how it then handles the success/failure of the DoConnect, a simple example being changing UI panels or similar? It does seem like a lot of writing though. Could you not just have a ExecuteConnect() function within the UI Manager instead of a private class? Something like this within the UIManager class:


    Code (CSharp):
    1.  
    2. public void ExecuteConnect()
    3.         {
    4.             ConnectButton.interactable = false;
    5.             manager.DoConnect((success) => {
    6.                 ConnectButton.interactable = true;
    7.                 //do whatever else is necessary based on 'success'
    8.             });
    9.         }
    10.  
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Yeah... you could just have it a method. The coroutine example I put was just a method rather than a class (though I'll gloss over the syntax sugar going on with an iterator method).

    Though honestly the amount of code difference is minimal. The fact I used a class was to demonstrate that all of these things are the "command pattern".

    There are benefits to encapsulating away into a command class... it relates the members together that are related. It organizes your code "Oh these variables,event listeners, etc pertain ONLY to this command".

    But at the end of the day, how you organize your code is up to you. That's not the important aspect of the examples... like I said it's pseudo-code.

    The importance is that NetworkManager doesn't need to know jack about UIManager.
     
    kailin89 likes this.
  7. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    fair dos, all very helpful, thanks!