Developing real-time multiplayer game using MagicOnion -4. Server Program-

Hosting on .Net Core, MagicOnion, and server game loop using LogicLooper

Table of Contents

Introduction


This is fourth tech post about my game development. In this post, I’ll describe about hosting on .Net Core, MagicOnion, and server game loop using LogicLooper.

I’m a frontend engineer who use Unity, so I haven’t had any confidence developing server program. But I could do some prototype codes.

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

Server Hosting on .Net Core


About .Net Core

Though I’ve never used, I’ve imagined that I would use Windows Server if I would have to launch server on Windows. I’ve also thought that it was limited to use on Windows OS only.

However, recent Microsoft movements are working on opensources or crossplatforms on their products. Especially .Net Core is an implementation of .Net and it is leaded developing by Microsoft for cross platforms. We can develop console-app, GUI-app, and just server-app as using .Net Core. And we can also launch those apps on Mac or Linux not limited to Windows.

MagicOnion recommended using this app, so I have obeyed it. .Net Core enable us to develop server hosting app on small source codes because they support us it can be used easily.

Code Overview

The mainly jobs are aggregated above two source codes and we can boot server hosting application just building them. Though these codes need implement dependent functions such as MagicOnion or LogicLooper, I’ve been impressed that I could implement concise codes than I’ve expected.

I’ll show you the essentials of them below code, for example.

// ==============================================
// Program.cs
// ==============================================
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core; // Kestrel is a cross-platform web server for ASP.NET Core
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System.IO;
using System.Reflection;

namespace App.Server
{
    class Program
    {
        public static void Main(string[] args)
        {
            IHostBuilder builder = createCustomHostBuilder(args);
            IHost host = builder.Build();
            host.Run();
        }

        private static IHostBuilder createCustomHostBuilder(string[] args)
        {
            // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1#default-builder-settings
            IHostBuilder defaultBuilder =
                Host.CreateDefaultBuilder(args)
                    .ConfigureAppConfiguration(
                        (hostingContext, config) =>
                        {
                            var env = hostingContext.HostingEnvironment;
                            var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                            // json(plane) -> json(各環境) -> 環境変数 -> コマンドライン引数 (右に行くほど優先順位上)
                            config.SetBasePath(Directory.GetCurrentDirectory())
                                  .AddJsonFile($"appsettings.json", optional: false)
                                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                                  .AddEnvironmentVariables()
                                  .AddCommandLine(args)
                                  .Build();
                        }
                );

            IHostBuilder webDefaultBuilder = defaultBuilder.ConfigureWebHostDefaults(
                webBuilder =>
                {
                    IWebHostBuilder bld = webBuilder.UseKestrel(
                        options =>
                        {   // WORKAROUND: Accept HTTP/2 only to allow insecure HTTP/2 connections during development.
                            options.ConfigureEndpointDefaults(
                                endpointOptions =>
                                {
                                    endpointOptions.Protocols = HttpProtocols.Http2;
                                }
                            );
                            //options.Listen(System.Net.IPAddress.Parse("xxx.xxx.xxx.xxx"), 12345);
                        });
                    bld.UseStartup<Startup>();
                });
            return webDefaultBuilder;
        }
    }
}

// ==============================================
// Startup.cs
// ==============================================
using App.Server.Looper;
using Cysharp.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using App.Shared.Common;

namespace App.Server
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc(); // MagicOnion depends on ASP.NET Core gRPC service.
            services.AddMagicOnion();

            services.AddSingleton<ILogicLooperPool>(_ => new LogicLooperPool(SharedConstant.FPS * 2, Environment.ProcessorCount, RoundRobinLogicLooperPoolBalancer.Instance));
            services.AddSingleton<MatchingRoomManager>();
            services.AddHostedService<MainGameLoopHostedService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }


            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapMagicOnionService();
            });
        }
    }
}

CreateHostBuilder function create server host which is launched on .Net Core using method chains or lambda function in order to set some parameters. This function has responsibility for infrastructure layers such as IP address or environment files. On the other hand, ConfigureServices function on Startup.cs has responsibility for app settings such as how to handling app data or instances.

Especially, AddSingleton function on ConfigureServices do handle singleton instances correctly when we pass classes what we hope it to be singleton.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MatchingHubServerImpl : StreamingHubBase<IMatchingHub, IMatchingHubReceiver>, IMatchingHub
{
    private readonly ILogger logger;
    private IGroup matchingRoom = null;
    private readonly MatchingRoomManager roomManager;

    /// <summary>
    /// DIコンストラクタ
    /// </summary>
    /// <param name="_mgr"></param>
    /// <param name="_logger"></param>
    public MatchingHubServerImpl(MatchingRoomManager _mgr, ILogger<MatchingHubServerImpl> _logger)
    {
        roomManager = _mgr;
        logger = _logger;
    }

I’ve been impressed that I saw MatchingRoomManager instance would be passed through constructor because this was my first DI framework experience.

MagicOnion(Server)

It is necessary for server MagicOnion are as below.

  • Implement “Send” interface’s methods such as “Client -> Server
  • Execute “Receive” methods such as “Server -> Client” to send response to clients.

Let’s see the sample that we saw previous post. “IMatchingHubReceiver” is an receive interface and we have to implement it on client layer. On the other hand, “IMatchingHub” need be implemented on server layer.

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

Clients call “JoinAsync” or “LeaveAsync” functions as if they are implemented on clients, but their truly implementations are on server layer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class MatchingHubServerImpl : StreamingHubBase<IMatchingHub, IMatchingHubReceiver>, IMatchingHub
{
    private IGroup matchingRoom = null;

    public async Task JoinAsync(MatchingJoinRequest _request)
    {
        matchingRoom = await Group.AddAsync("MatchingRoom");

        // Return server response to client
        BroadcastToSelf(matchingRoom).OnMatchingSuccess();
    }

    public async Task LeaveAsync()
    {
        roomManager.RemoveFromMatchingRoom();
        await matchingRoom.RemoveAsync(Context);
    }

MatchingHubServerImpl which is implemented StreamingHubBase on server would return its responses to client using Broadcast functions and grouping with IGroup interface. Due to these functions, we can return response to clients which are belongs to any Groups not only themselves but also any clients. Though I have not implemented matching program properly in this game, you can implement more interesting matching services if you use evaluation materials such as user scores or player levels.

I’ve also implemented server main game loop networking using above systems.

LogicLooper

I could server hosting and networking program easily thanks to be supported by .Net Core and MagicOnion. But I have been worried to implement “server main game loop” program, because I didn’t know how or where to implement it on server. I have googled day by day, and finally I have found good library to achieve my objectives, “LogicLooper”.

Genereate threads for game loop

LogicLooper would generate thread loops after registering update function using “RegisterActionAsync” function, and execute them every frames.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void CreateNew(ILogicLooperPool _looperPool, ILogger _logger, LoopData _loopData)
{
    var gameLoop = new ServerMainGameLoop(_logger, _loopData, info);
    _looperPool.RegisterActionAsync(gameLoop.UpdateFrame);
}

public bool UpdateFrame(in LogicLooperActionContext _ctx)
{
    // execute every frame
    return true;
}

Please reference below code about detail it.

I could implement server game loop because it was an extension of the client game loop. I’ve expected that my game logic would execute on server so that I’ve implemented game logic with pure C#. Due to this, I could place these logics to shared domain.

Data racing on thread programming

Though I’ve been able to implement game loop on server, I’ve been troubled that index out of range exceptions which were caused irregularly. You may notice due to be caused this problem irregularly. You are right. These problems were caused because LogicLooper is a library that it is executed on threads.

In this game, we have to read and write board information many times, and we also need reference opponent’s board information in real-time. If you would reference opponent’s information while they are in construction, the program would be caused “Data race” phenomena.

To avoid this phenomena, we can set some scope called “Critical sections”, or thread lock areas. To do this, just one thread can access these areas and we could avoid data racing. In case of my program, I’ve set critical section on two areas, one is the area that need reference opponent’s information, the other is changed each information.

 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
// for thread lock
private static Object threadLockObj = new object();

// mine
updateMemberBoardInfo(loopData.MineId, PlayingState.Board, PlayingState.CurrentScore, PlayingState.NextFirst, PlayingState.NextSecond, PlayingState.IsActive);

// other
if (loopData.HubImpl.WithOther)
{
    var enem = TryTakeGame(loopData.EnemyId);
    if (enem != null)
    {
        BlockStatus first = new BlockStatus();
        BlockStatus second = new BlockStatus();
        int enemScore = 0;
        bool isActive = true;

        lock (threadLockObj)
        {   // lock for data racing
            for (int i = 0; i < SharedConstant.BOARD_ARRAY_SIZE; ++i)
            {
                enemyBoard[i] = enem.PlayingState.Board[i];
            }
            enemScore = enem.PlayingState.CurrentScore;
            first = enem.PlayingState.NextFirst;
            second = enem.PlayingState.NextSecond;
            isActive = enem.PlayingState.IsActive;
        }

        updateMemberBoardInfo(loopData.EnemyId, enemyBoard, enemScore, first, second, isActive);
    }
}

“Data Racing” bug is one of the most annoying errors because it would be caused irregularly and we couldn’t reproduce it. The worst thing is that you wouldn’t find any problem in your development phase, but it would be found on production phase. It can happen enough. So you must be careful when you implement thread program.

Conclusion


In this post, I’ve described the detail of server program on my game.

About my server program, I have some things to be considered.

  • How the possession of instance is bad a little.
  • Matching process is very bad, because it has handled very simple queue.
  • I couldn’t implements backend techniques such as Databases.

If I have some chances, I’ll try to resolve these problems.

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.

Previous

Related