Server Game di MS Orleans - Bagian 3: Ringkasan



Halo, Habr! Saya terus belajar MS Orleans dan membuat game online sederhana dengan klien konsol dan server yang bekerja dengan biji-bijian Orleans. Kali ini saya akan memberi tahu Anda bagaimana semuanya berakhir dan kesimpulan apa yang saya buat untuk diri saya sendiri. Untuk detailnya, selamat datang di kucing.

Jadi ya, jika Anda tertarik pada bagaimana server game untuk game dinamis dibuat, dan bukan eksperimen saya dengan MS Orleans, saya sarankan Anda melihat repositori ini (UDP) dan baca artikel ini:

  1. habr.com/en/post/303006
  2. habr.com/en/post/328118
  3. habr.com/en/company/pixonic/blog/499642
  4. habr.com/en/company/pixonic/blog/420019

Kandungan



Kode sumber


MsOrleansOnlineGame

Tentang permainan


Hasilnya adalah penembak sederhana. Hijau # adalah lawan. Kuning # adalah karakter Anda. Red $ adalah sebuah peluru. Pemotretan adalah arah yang Anda tuju. Arah gerakan dikendalikan oleh tombol WASD atau panah. Bilah spasi digunakan untuk pemotretan. Saya tidak melihat titik menggambarkan kode klien secara rinci karena perlu diganti dengan yang normal. Grafis.

Tentang aktor (biji-bijian)


Singkatnya: IMHO saya bahwa Orleans adalah gRPC tentang steroid yang diasah di bawah Azure, scaling dan bekerja dengan peringatan. Dengan cache misalnya. Meskipun tanpa status, seperti RPC biasa, dia tahu cara bekerja melalui Stateless Worker Grains. Grain (Aktor) di Orleans dapat bertindak sebagai titik masuk sebagai Pengontrol di Asp.Net. Tetapi tidak seperti Controller, butir memiliki satu contoh tunggal yang memiliki pengenal sendiri. Butir bagus ketika Anda perlu bekerja dengan beberapa kondisi pada saat yang sama dari beberapa utas atau dari beberapa pengguna. Mereka menyediakan operasi benang aman dengannya.

Misalnya, di sini ada aktor untuk sekeranjang barang. Pada panggilan pertama, itu akan dibuat dan akan tergantung di memori memainkan peran cache. Pada saat yang sama, ribuan pengguna dari ribuan aliran berbeda dapat secara bersamaan membuat permintaan untuk menambahkan dan menghapus item. Semua pekerjaan dengan kondisinya di dalam dirinya akan benar-benar aman. Dalam hal ini, tentu saja, akan berguna untuk membuat aktor Shop dengan metode List GetBaskets () untuk mendapatkan daftar semua keranjang yang tersedia dalam sistem. Pada saat yang sama, Toko juga akan menggantung di memori sebagai cache dan semua bekerja dengannya akan aman.

    public interface IBasket : IGrainWithGuidKey
    {
        Task Add(string item);
        Task Remove(string item);
        Task<List<string>> GetItems();
    }

    public class BasketGrain : Grain, IBasket
    {
        private readonly ILogger<BasketGrain> _logger;
        private readonly IPersistentState<List<string>> _store;

        public BasketGrain(
            ILogger<BasketGrain> logger,
            [PersistentState("basket", "shopState")] IPersistentState<List<string>> store
        )
        {
            _logger = logger;
            _store = store;
        }

         public override Task OnActivateAsync()
        {
             var shop = GrainFactory.GetGrain<IShop>();
           //          .
            await shop.AddBasketIfNotContains(this.GetPrimaryKey())
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
          //       
         //    Asp.Net  . 
        //           - .
        //    -      .
        //         .
     //        
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }


        public Task Add(string item)
        {
            _store.State.Add(item);
            return Task.CompletedTask;
        }

        public Task Remove(string item)
        {
            _store.State.Remove(item);
            return Task.CompletedTask;
        }

        public Task<List<string>> GetItems()
        {
           //      .
          //        
         //           
            return Task.FromResult(new List<string>(_store.State));
        }
    }

Contoh penggunaan di beberapa aplikasi konsol:

         private static async Task DoClientWork(IClusterClient client, Guid baskeId)
        {
            var basket = client.GetGrain<IBasket>(baskeId);
           //   gRPC -                
            await basket.Add("Apple");
        }

Kode permainan



Peta tempat para pemain bertarung:

   public interface IFrame : IGrainWithIntegerKey
    {
        Task Update(Frame frame);
        Task<Frame> GetState();
    }

    public class FrameGrain : Grain, IFrame
    {
        private readonly ILogger<FrameGrain> _logger;
        private readonly IPersistentState<Frame> _store;

        public FrameGrain(
            ILogger<FrameGrain> logger,
            [PersistentState("frame", "gameState")] IPersistentState<Frame> store
        )
        {
            _logger = logger;
            _store = store;
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
           //    1  1      .
            _store.State.GameId = this.GetPrimaryKeyLong();
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("DEACTIVATED");
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public Task Update(Frame frame)
        {
            _store.State = frame;
            return Task.CompletedTask;
        }

        public Task<Frame> GetState() => Task.FromResult(_store.State.Clone());
    }

Butir game yang menyimpan keadaan umum game saat ini dan mengirimkannya ke klien 20 kali per detik melalui SignalR.

    public interface IGame : IGrainWithIntegerKey
    {
        Task Update(Player player);
        Task Update(Bullet bullet);
        Task<List<Player>> GetAlivePlayers();
    }

    public class GameGrain : Grain, IGame
    {
        private const byte WIDTH = 100;
        private const byte HEIGHT = 50;
        private readonly ILogger<GameGrain> _logger;
        private readonly IPersistentState<Game> _store;
        private readonly IHubContext<GameHub> _hub;
        private IDisposable _timer;
        public GameGrain(
            ILogger<GameGrain> logger,
            [PersistentState("game", "gameState")] IPersistentState<Game> store,
            IHubContext<GameHub> hub
            )
        {
            _logger = logger;
            _store = store;
            _hub = hub;
        }

        public override async Task OnActivateAsync()
        {
            _store.State.Id = this.GetPrimaryKeyLong();
            _store.State.Frame = new Frame(WIDTH, HEIGHT) { GameId = _store.State.Id };
            var frame = GrainFactory.GetGrain<IFrame>(_store.State.Id);
            await frame.Update(_store.State.Frame.Clone());
            _logger.LogWarning("ACTIVATED");
            //      50      .       .
            _timer = RegisterTimer(Draw, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(50));
            await base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogWarning("DEACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Draw(object obj)
        {
            var state = _store.State;
            state.Bullets.RemoveAll(b => !b.IsAlive);
            state.Players.RemoveAll(p => !p.IsAlive);
            try
            {
                await _hub.Clients.All.SendAsync("gameUpdated", state.Clone());
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Error on send s");
            }
        }

        public Task Update(Player player)
        {
            _store.State.Players.RemoveAll(x => x.Id == player.Id);
            _store.State.Players.Add(player);
            return Task.CompletedTask;
        }
        public Task Update(Bullet bullet)
        {
            _store.State.Bullets.RemoveAll(x => x.Id == bullet.Id);
            _store.State.Bullets.Add(bullet);
            return Task.CompletedTask;
        }

        public Task<List<Player>> GetAlivePlayers() =>
            Task.FromResult(_store.State.Players.Where(p => p.IsAlive).Select(p => p.Clone()).ToList());
    }

SignalR adalah hub tempat kami berkomunikasi dengan klien. Bertindak sebagai proksi antara klien WebGl dan Orleans. Sejauh ini, kliennya adalah konsol dan dia sangat bodoh. Saya ingin membuat klien web di masa depan di browser di Three.js dan karenanya saya memerlukan koneksi pada soket web SignalR. Klien Orleans itu sendiri hanya dalam C #, tidak seperti gRPC, yang tersedia dalam banyak bahasa, oleh karena itu, untuk klien web, antara server Orleans dan klien Anda perlu menginstal proxy (inti Gateway asp.net).

    public class GameHub : Hub
    {
        private readonly IGrainFactory _client;

        public GameHub(IGrainFactory client)
        {
            _client = client;
        }

        public async Task GameInput(Input input)
        {
            var player = _client.GetGrain<IPlayer>(input.PlayerId);
            await player.Handle(input);
        }
    }

Pemain biji-bijian. Secara otomatis bergerak sesuai dengan pengatur waktu dan merespons perintah pengguna. Jika perintah tembak tiba, ia menciptakan peluru peluru dan menetapkan arah gerakan untuknya.

    public class PlayerGrain : Grain, IPlayer
    {
        private readonly ILogger<PlayerGrain> _logger;
        private readonly IPersistentState<Player> _store;
        private IDisposable _timer;
        private readonly Queue<Input> _inputs;
        public PlayerGrain(
            ILogger<PlayerGrain> logger,
            [PersistentState("player", "gameState")] IPersistentState<Player> store
        )
        {
            _logger = logger;
            _store = store;
            _inputs = new Queue<Input>();
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            // State   POCO     . Entity Player   
            _store.State.Id = this.GetPrimaryKey();
            _timer = RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(200));
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Handle(Input input)
        {
            _store.State.GameId = input.GameId;
            _inputs.Enqueue(input);
        }

        public async Task Update(object obj)
        {
            if (!_store.State.IsAlive)
            {
                await _store.ClearStateAsync();
               //          .
             //       .       .
                DeactivateOnIdle();
                return;
            }

            while (_inputs.Count > 0)
            {
                var input = _inputs.Dequeue();
                foreach (var direction in input.Directions.Where(d => d != Direction.None))
                {
                    _store.State.Direction = direction;
                }

                foreach (var command in input.Commands.Where(c => c != Command.None))
                {
                    if (command == Command.Shoot)
                    {
                        var bulletId = Guid.NewGuid();
                        var bullet = GrainFactory.GetGrain<IBullet>(bulletId);
                        //  Shot()           .
                        bullet.Update(_store.State.Shot()).Ignore(); //Ignore()        
                    }
                }
            }
            _store.State.Move();
            if (_store.State.GameId.HasValue)
            {
                var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);
                var fs = await frame.GetState();
                if (fs.Collide(_store.State))
                    _store.State.MoveBack();
                GrainFactory.GetGrain<IGame>(_store.State.GameId.Value)
                    .Update(_store.State.Clone())
                    .Ignore();
            }
        }

        public async Task Die()
        {
            _store.State.IsAlive = false;
            if (_store.State.GameId.HasValue)
                await GrainFactory.GetGrain<IGame>(_store.State.GameId.Value).Update(_store.State.Clone());
            await _store.ClearStateAsync();
            DeactivateOnIdle();
        }
    }


Butir peluru. Secara otomatis bergerak pada timer dan, jika bertemu pemain, memerintahkannya untuk mati. Jika dia menemui hambatan di peta, dia mati sendiri.
  public interface IBullet : IGrainWithGuidKey
    {
        Task Update(Bullet dto);
    }

    public class BulletGrain : Grain, IBullet
    {
        private readonly ILogger<BulletGrain> _logger;
        private readonly IPersistentState<Bullet> _store;
        private IDisposable _timer;
        public BulletGrain(
            ILogger<BulletGrain> logger,
            [PersistentState("bullet", "gameState")] IPersistentState<Bullet> store
        )
        {
            _logger = logger;
            _store = store;
        }

        public Task Update(Bullet dto)
        {
            _store.State = dto;
            _store.State.Id = this.GetPrimaryKey();
            return Task.CompletedTask;
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            _timer = this.RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(50));
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("DEACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Update(object obj)
        {
            if (!_store.State.IsAlive)
            {
                await _store.ClearStateAsync();
                DeactivateOnIdle();
                return;
            }
            _store.State.Move();
            if (_store.State.GameId.HasValue)
            {
                var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);
                var fs = await frame.GetState();
                if (fs.Collide(_store.State))
                    _store.State.IsAlive = false;
                if (_store.State.Point.X > fs.Width || _store.State.Point.Y > fs.Height)
                    _store.State.IsAlive = false;
                var game = GrainFactory.GetGrain<IGame>(_store.State.GameId.Value);
                var players = await game.GetAlivePlayers();
                foreach (var player in players)
                {
                    if (player.Collide(_store.State))
                    {
                        _store.State.IsAlive = false;
                        GrainFactory.GetGrain<IPlayer>(player.Id).Die().Ignore();
                        break;
                    }
                }
                game.Update(_store.State.Clone()).Ignore();
            }
        }
    }

All Articles