Developing real-time multiplayer game using MagicOnion -3. Client Program-

MagicOnion, UniTask, and other programming paterns

Table of Contents

Introduction


This is third tech post about my game development. In this post, you’ll see the points that how I’ve implemented client program using libraries such as “MagicOnion” or programming patterns.

You can see other posts or source code are as below.

  1. Developing real-time multiplayer game using MagicOnion -1. Overview-
  2. Developing real-time multiplayer game using MagicOnion -2. Architectures-
  3. Developing real-time multiplayer game using MagicOnion -3. Client Program-
  4. Developing real-time multiplayer game using MagicOnion -4. Server Program-
  5. Github Source Code / LineDeleteGame

MagicOnion(Client)


I’ve referenced in the previous posts, I’ve used MagicOnion to implement real-time networking with server. Let’s see detail of them.

Introduction of MagicOnion

If you investigate about MagicOnion, you’ll see sample image as below that client call server functions as if they are defined on client.

To achieve this, we have to define interfaces between client and server. For example in my matching code, I’ve defined interfaces in shared domain like “IMatchingHub”, which is used for matching in server, and “IMatchingHubReceiver” which is to receive response in client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface IMatchingHub : IStreamingHub<IMatchingHub, IMatchingHubReceiver>
{
    Task JoinAsync(MatchingJoinRequest _request);
    Task LeaveAsync();
}

public interface IMatchingHubReceiver
{
    void OnMatchingSuccess(MatchingRequestSuccessResponse _response);
}

And we have to implement “IMatchingHubReceiver” derived class interfaces in client,

  • Execute “JoinAsync” or “LeaveAsync” function to send parameters to server
  • Receive responses from server to derive “OnMatchingSuccess” function

Server will execute based on parameters which are sending from clients, and send responses to client what they calculate parameters.

Espcecially there are two types of communictaions on MagicOnion or gRPC. Cite as gRPC's Core concepts, architecture and lifecycle are as below,

Unary RPCs where the client sends a single request to the server and gets a single response back, just like a normal function call.

Bidirectional streaming RPCs where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like.

I’ve used Streaming communication methods in this game because I have to implement real-time network game and it has to communicate each other on real-time independently.

Setup MagicOnion Environment

To set developing environment, I’ve read these documents (Sorry, but these documents are Japanese only.)

In my program, I’ve mainly used streaming connection and I could developed my game easily to use it. On the other hand, it was very hard for me to implement game server program because I haven’t developed it. (I’ll show you the detail of them in the next post!)

These are ones of interfaces and utilities that I’ve implemented using MagicOnion in my program. On Github, you can transit the codes which are referenced them if you’ll click function names.

Handling of MagicOnion or gRPC libraries

Thanks to above those documents and slides, I’ve been able to implement network program on client side except Unity freezing. MagicOnion is based on gRPC library which uses unsafe codes such as pointers or other low layer programs. So you’ll face unstable situations if you won’t manage gRPC connections correctly.

Especially, I’ve had a hard time to solve the problem that my game would have frozen when I’ve done connect and disconnect repeatedly while server was not launched. While I was seeing this phenomena, I noticed that I have to confirm whether server is launched or not previous connect to it. So I’ve inserted confirmation code which is established connection or not, and I’ve been able to solve it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>gRPC channel</summary>
private Grpc.Core.Channel channel = null;

/// <summary>cancel token</summary>
private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

public async UniTask ConnectStartAsync()
{
    while (!cancellationTokenSource.IsCancellationRequested)
    {
        try
        {   // Confirm that server connect is established
            await channel.ConnectAsync(DateTime.UtcNow.AddSeconds(5));
            Debug.Log("connecting to the server ... ");
            ServerImpl = await StreamingHubClient.ConnectAsync<TServer, TClient>(channel, ClientImpl, cancellationToken: cancellationTokenSource.Token);
            executeDisconnectEventWaiterAsync(ServerImpl).Forget();
            Debug.Log("established connect");
            break;
        }
        catch (TaskCanceledException)
        {
        }
        catch (OperationCanceledException e)
        {
            Debug.LogWarning(e);
            throw new OperationCanceledException();
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }

        Debug.Log("Retry after 2 seconds");
        await UniTask.Delay(2 * 1000, cancellationToken: cancellationTokenSource.Token);
    }
}

UniTask


I’ve never used UniTask in my hobby or work, so I’ve adopted it in order to learn asynchronous programming. In the beginning phase, I believed that it works well how I thought. But once I’ve examined whether UniTask objects leak or not, they were leaked more than I thought. Unity’s coroutine will be working with Unity’s GameObject life scope, so it won’t leak very much. But UniTask will be working not to be related Unity’s lifecycle, so it is easy for UniTask to leak its objects if you won’t manage it correctly.

How to find leaks of UniTask object

You can trace UniTask’s objects using “UniTaskTracker” in your game playing. This window exists “Window” -> “UniTaskTracker” on above menu in UnityEditor. If you want to trace in real-time, you have to activate these elements.

  • Enable AutoReload
  • Enable Tracking
  • Enable StackTrace

I have believed myself that I could manage it well, but the truth, I couldn’t do that. I was producing lots of “zombie objects”.

Why I’ve leaked UniTask object

Finished Task using “discard”

When we call asynchronous function in synchronous function, we can leave it alone using discard expression as below.

1
2
3
4
5
6
async Task AsyncAnything()
{
    await foo();
}

_ = AsyncAnything();

I did do that in my program with UniTask, too. But it caused object leak when I examined with UniTaskTracker. In UniTask, “Forget” function is prepared for this situation, so we should use it.

1
2
3
4
5
6
async UniTask AsyncAnything()
{
    await foo();
}

AsyncAnything().Forget();

CancellationToken is not operated correctly

We have lots of situations that we have to interrupt asynchronous executions while scene transition or networking with server. Though UniTask can do that using “CancellationToken”, I have not known it until my UniTask program did unstable behavior.

If your asynchronous execution will be related with Unity GameObject’s life span, you only have to pass “GetCancellationTokenOnDestroy” as CancellationToken.

1
await UniTask.Delay(500, cancellationToken: this.GetCancellationTokenOnDestroy());

You can also manage your cancelation operations yourself using “CancellationTokenSource” if you will have to operate connection disposing in the user timings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

private async UniTask onDisposing()
{
    // fire cancel trigger
    cancellationTokenSource.Cancel();

    if (ServerImpl != null)
    {
        await ServerImpl.DisposeAsync();
        ServerImpl = default;
    }
    if (channel != null)
    {
        await channel.ShutdownAsync();
        channel = null;
    }

    ClientImpl = default;
}

Transition scenes using State pattern


I’ve used “State” programming pattern in my code. This pattern is used how we transit scene or game state such as “Start”, “Update”, and “End”. I’ve created “IState” interfaces and derived it to implement to transition code.

IState Interface

For example, you can imagine that

  • implement “Line Delete” effect when the blocks are lined up in a row
  • implement “Pause” state

The easiest ways to implement it is that using switch distributions with “enum” as below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
enum eState
{
    Start,
    Update,
    Pause,
    LineDeleteEffect,
}

void Update()
{
   switch(currentState)
   {
      case eState.Start:
        ...
      case eState.Update:
        ...
   }
}

Though we can handle this method in a simple game as my game, it becomes very hard to implement various states. If you want to implement “Open Menu, and use items” or “Occur converstaion events with story”, the costs of implementation become very large because we have to investigate their scope of influence.

A common way to solve this problem, we often define “IState” interface and derive it to implement detail each state behavior.

A sample code is as below.

public abstract class IState
{
    public StateController Ctrl { get; private set; }

    public virtual void Enter() { }

    public virtual bool Update(float _dt) { return false; }

    public virtual IState Exit() { return null; }

    public virtual void CalledOnResume(IState _prevState) { }

    public virtual void CalledOnPause(IState _nextState) { }

    internal void SetController(StateController _ctrl)
    {
        Ctrl = _ctrl;
    }
}

public class StateController
{
    private Stack<IState> stateStack = new Stack<IState>();

    private Queue<IState> reservedStateQueue = new Queue<IState>(RESERVED_MAX);

    public IState Run(float _dt)
    {
        while (reservedStateQueue.Count > 0)
        {
            addState(reservedStateQueue.Dequeue());
        }

        if (stateStack.Count <= 0)
        {
            return null;
        }

        IState st = stateStack.Peek();
        if (st.Update(_dt))
        {
            st.Draw(_dt);
            return st;
        }

        IState prevState = st;
        IState nextState = st.Exit();
        stateStack.Pop();

        if (nextState != null)
        {
            ReserveAddState(nextState);
        }
        else if (stateStack.Count > 0)
        {
            st = stateStack.Peek();
            st.CalledOnResume(prevState);
        }

        return nextState;
    }

    public void ReserveAddState(IState _state)
    {
        reservedStateQueue.Enqueue(_state);
    }

    private void addState(IState _state)
    {
        _state.SetController(this);

        if (stateStack.Count > 0)
        {
            var prev = stateStack.Peek();
            prev.CalledOnPause(_state);
        }

        stateStack.Push(_state);
        _state.Enter();
    }
}

The game scenes are transited linked each other on StateController. Though it looks very hard to manage, it has lots of merits. We only have to focus on “add state” or previous state and we can manage it easier than enum’s state handling. I’ve placed it on shared domain in order to use it on client and server domains, so that I’ve written it pure C#.

I’ve referenced below Unity post.(Sorry, this post is Japanese only.)

Unity x RPG x Multi Platform | Made with Unity

Conclusion and about next post


In this third post, I’ve shown you the details of client-programming about MagicOnion, UniTask, and State-Patterns. I’ll show you more Unity techniques such as Tilemap in future posts somedays.

Next post, you will see how I’ve coded server-side programming.

y-tomita
y-tomita
Game Programmer
Build Engineer

I’m a Game Programmer. In my work, I often use Unity3D Engine with C#/C++ to develop game app.

Next
Previous

Related