خادم اللعبة على MS Orleans - الجزء 3: ملخص



مرحبا يا هابر! أواصل دراسة MS Orleans وعمل لعبة بسيطة عبر الإنترنت مع عميل وحدة تحكم وخادم يعمل مع حبيبات Orleans. هذه المرة سأخبرك كيف انتهى كل شيء وما الاستنتاجات التي توصلت إليها لنفسي. لمزيد من التفاصيل ، مرحبا بك في القط.

لذا نعم ، إذا كنت مهتمًا على الإطلاق بكيفية عمل خوادم الألعاب للألعاب الديناميكية ، وليس تجربتي مع MS Orleans ، أوصيك بالنظر إلى هذا المستودع (UDP) وقراءة هذه المقالات:

  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

المحتوى



مصدر الرمز


MsOrleansOnlineGame

عن اللعبة


كانت النتيجة مطلق نار بسيط. # الخضراء هي خصوم. الأصفر # هو شخصيتك. الرمز الأحمر هو رصاصة. الرماية في الاتجاه حيث أنت ذاهب. يتم التحكم في اتجاه الحركة بواسطة أزرار WASD أو الأسهم. يتم استخدام شريط المسافة في اللقطة. لا أرى فائدة من وصف رمز العميل بالتفصيل لأنه يجب استبداله برمز عادي. الرسم.

حول الفاعلين (الحبوب)


وباختصار: بلدي IMHO أن أورليانز هو gRPC على المنشطات شحذ تحت Azure ، التحجيم والعمل مع نصب تذكاري. مع ذاكرة التخزين المؤقت على سبيل المثال. على الرغم من أنه بدون دولة ، مثل RPC العادي ، فإنه يعرف كيف يعمل من خلال حبوب العامل عديم الجنسية. يمكن للحبوب (الفاعل) في أورليانز أن تعمل كنقطة دخول كمراقب في Asp.Net. ولكن على عكس وحدة التحكم ، فإن الحبوب لها مثيل واحد له معرفه الخاص. الحبوب جيدة عندما تحتاج إلى العمل مع حالة ما في نفس الوقت من عدة خيوط أو من عدة مستخدمين. أنها توفر عملية ترابط آمنة معها.

على سبيل المثال ، هنا ممثل لسلة من السلع. في المكالمة الأولى ، سيتم إنشاؤه وسيتم تعليقه في الذاكرة وهو يلعب دور ذاكرة التخزين المؤقت. في الوقت نفسه ، يمكن لآلاف المستخدمين من آلاف مجموعات البث المختلفة تقديم طلبات إليها في نفس الوقت لإضافة عناصر وإزالتها. سيكون كل العمل مع حالته داخله آمنًا تمامًا. في هذه الحالة ، بالطبع ، سيكون من المفيد إنشاء متجر فاعل مع طريقة List GetBaskets () للحصول على قائمة بجميع السلال المتاحة في النظام. في الوقت نفسه ، سيتم تعليق المتجر أيضًا في الذاكرة كذاكرة تخزين مؤقت وسيكون كل العمل معه آمنًا.

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

مثال للاستخدام في بعض تطبيقات وحدة التحكم:

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

كود اللعبة



الخريطة التي يقاتل اللاعبون عليها:

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

حبة اللعبة التي تخزن الحالة العامة للعبة الحالية وترسلها إلى العميل 20 مرة في الثانية عبر 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 هو المحور الذي نتواصل من خلاله مع العميل. وهي تعمل كوكيل بين عميل WebGl و Orleans. حتى الآن ، العميل هو وحدة التحكم وهو غبي بشكل كبير. أريد إنشاء عميل ويب في المستقبل في مستعرض على Three.js وبالتالي أحتاج إلى اتصال على مقبس الويب SignalR. يتوفر عميل Orleans في حد ذاته فقط في C # ، بخلاف gRPC ، المتاح بالعديد من اللغات ، لذلك بالنسبة إلى عميل الويب ، تحتاج إلى تثبيت وكيل بين خادم وعملاء Orleans (Gateway asp.net core).

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

لاعب الحبوب. يتحرك تلقائيًا وفقًا للمؤقت ويستجيب لأوامر المستخدم. إذا وصل أمر إطلاق النار ، فإنه يخلق شريان رصاصة ويحدد اتجاه الحركة له.

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


رصاصات الحبوب. يتحرك تلقائيًا على جهاز توقيت ، وإذا واجه لاعبًا ، يأمره بالموت. إذا واجهت عقبة على الخريطة ، فإنها تموت نفسها.
  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