Сервер Игры на MS Orleans — часть 1: Что такое Акторы



    Привет Хабр! И так, на четырнадцатый день копья решил я значит начать делать простенький игровой сервер для простой онлайн стрелялки. За одно тему распределенных вычислений затронуть. В этой вводной статье цикла хочу рассказать что такое акторы (в Орлеанс их зернами называют) и принцип их работы. Для этого я пока пример простенького приложения с самодельными акторами без Orleans. Как говориться прежде чем строить корабль посмотрим как плавает и почему плавает обычный бумажный кораблик. За подробностями добро пожаловать под кат.

    Содержание


    • Сервер Игры на MS Orleans — часть 1: Что такое Акторы


    Акторы


    Актор является вычислительной сущностью, которая в ответ на полученное сообщение может одновременно:
    отправить конечное число сообщений другим акторам;
    создать конечное число новых акторов;
    выбрать поведение, которое будет использоваться при обработке следующего полученного сообщения.
    Не предполагается существования определённой последовательности вышеописанных действий и все они могут выполняться параллельно.
    Мы посылаем актору какие-то сообщения (Команды или События) и он сам может посылать другим такие же сообщения. По похожему принципу работаю корутины с каналами. Они принимает какие-то данные из канала и отправляет какие-то данные в канал работая при этом асинхронно.

    Пример


    Собственно код для двух очень простых акторов:
        class Program
        {
            interface IMessage { }
    
            class IncrementCommand : IMessage { }
    
            class TellCountCommand : IMessage { }
    
            class SaidCountEvent : IMessage
            {
                public int Count { get; }
                public int ActorId { get; }
    
                public SaidCountEvent(int count, int actorId)
                {
                    Count = count;
                    ActorId = actorId;
                }
            }
    
            class WriteMessageCommand : IMessage
            {
                public string Message { get; }
    
                public WriteMessageCommand(string message)
                {
                    Message = message;
                }
            }
    
            static Task CreateCounterActor(
                BlockingCollection<IMessage> output,
                BlockingCollection<IMessage> input,
                int id
                )
            {
                return Task.Run(() =>
                 {
                     var count = 0;
    
                     while (true)
                     {
                         var m = input.Take();
                         if (m is IncrementCommand)
                             count++;
                         if (m is TellCountCommand)
                             output.Add(new SaidCountEvent(count, id));
                     }
                 });
            }
    
            static Task CreateWriterActor(BlockingCollection<IMessage> input)
            {
                return Task.Run(() =>
                 {
                     while (true)
                     {
                         var m = input.Take();
                         if (m is WriteMessageCommand write)
                             Console.WriteLine(write.Message);
                         if (m is SaidCountEvent sc)
                             Console.WriteLine(
                                 $"Counter сейчас равен {sc.Count} для актора {sc.ActorId}"
                                 );
                     }
                 });
            }
    
            static void Main(string[] args)
            {
                var writerInput = new BlockingCollection<IMessage>();
                var firstInput = new BlockingCollection<IMessage>();
                var secondInput = new BlockingCollection<IMessage>();
                var writer = CreateWriterActor(writerInput);
                var firstCounter = CreateCounterActor(writerInput, firstInput, 1);
                var secondCounter = CreateCounterActor(writerInput, secondInput, 2);
                for (int i = 0; i < 5; i++)
                {
                    firstInput.Add(new IncrementCommand());
                }
                for (int i = 0; i < 9; i++)
                {
                    secondInput.Add(new IncrementCommand());
                }
                firstInput.Add(new TellCountCommand());
                secondInput.Add(new TellCountCommand());
                writerInput.Add(new WriteMessageCommand("Конец метода Main"));
                Console.ReadLine();
            }
        }
    


    1. Интерфейс которым мы помечаем сообщения которые получает или отправляет актор:
      interface IMessage { }
      

    2. Команда с помощью которой мы говорим актору увеличить его счетчик (внутренне состояние):
      class IncrementCommand : IMessage { }
      

    3. Команда с помощью которой мы говорим актору сказать его текущее состояние (счетчик) другим:
      class TellCountCommand : IMessage { }
      

    4. Событие которое говорит другим акторам о том что актор сказал всем свое текущее состояние (счетчик). Генерируется при обработке команды TellCountCommand:
      class SaidCountEvent : IMessage
      {
          public int Count { get; }
          public int ActorId { get; }
      
          public SaidCountEvent(int count, int actorId)
          {
              Count = count;
              ActorId = actorId;
          }
      }
      

      Count это сколько сейчас набралось на счетчике у актора с идентификатором равным ActorId
    5. Эта команда говорит актору вывести данное сообщение на консоль:
      class WriteMessageCommand : IMessage
      {
          public string Message { get; }
      
          public WriteMessageCommand(string message)
          {
              Message = message;
          }
      }
      

    6. Запускает инстанс актора который управляет текущим состоянием счетчика:
              static Task CreateCounterActor(
               //Исходящие сообщения из актора
                  BlockingCollection<IMessage> output,
                //Входящие сообщения
                  BlockingCollection<IMessage> input,
               // Идентификатор по которому мы будем различать разные инстансы актора одного и того же типа
                  int id
                  )
              {
                  return Task.Run(() =>
                   { 
                       //Внутренне состояние актора. То чем он управляет
                       var count = 0;
      
                       while (true)
                       {
                         //Достаем следующее сообщение из потокобезопасной коллекции
                        //Выполнение блокируется на этом месте до тех пор пока в коллекции не появится значение
                           var m = input.Take();
                           if (m is IncrementCommand)
                               count++;
                           if (m is TellCountCommand)
                               output.Add(new SaidCountEvent(count, id));
                       }
                   });
              }
      

    7. Создает актор который просто пишет сообщения в консоль:
              static Task CreateWriterActor(BlockingCollection<IMessage> input)
              {
                  return Task.Run(() =>
                   {
                       while (true)
                       {
                           var m = input.Take();
                           if (m is WriteMessageCommand write)
                               Console.WriteLine(write.Message);
                           if (m is SaidCountEvent sc)
                               Console.WriteLine(
                                   $"Counter сейчас равен {sc.Count} для актора {sc.ActorId}"
                                   );
                       }
                   });
              }
      

    8.         static void Main(string[] args)
              {
                  var writerInput = new BlockingCollection<IMessage>();
                  var firstInput = new BlockingCollection<IMessage>();
                  var secondInput = new BlockingCollection<IMessage>();
                  var writer = CreateWriterActor(writerInput);
                //Создаем два актора одного типа с разными идентификаторами
                  var firstCounter = CreateCounterActor(writerInput, firstInput, 1);
                  var secondCounter = CreateCounterActor(writerInput, secondInput, 2);
                  //Пять раз говорим первому актору увеличить его счетчик
                  for (int i = 0; i < 5; i++)
                  {
                      firstInput.Add(new IncrementCommand());
                  }
                  //Девять раз говорим второму актору увеличить его счетчик.
                  for (int i = 0; i < 9; i++)
                  {
                      secondInput.Add(new IncrementCommand());
                  }
                  //Говорим акторам сказать всем сколько у них набралось на счетчике.
                  //Так как они отправляют свои события writer актору он выводит их на экран
                  firstInput.Add(new TellCountCommand());
                  secondInput.Add(new TellCountCommand());
                 //говорим writer актору вывести сообщение в консоль.
                  writerInput.Add(new WriteMessageCommand("Конец метода Main"));
                  Console.ReadLine();
              }
      



    Ну и чтобы совсем было совсем красиво и похоже на грейн (актор) из Orleans давай сделаем обертку на нашим каунтером:
        public interface ICounterActor
        {
            void Increment();
            void TellCount();
        }
    
        public class CounterActor : ICounterActor
        {
            private readonly Task _taks;
            private readonly BlockingCollection<IMessage> _input;
            private readonly BlockingCollection<IMessage> _output;
    
            public CounterActor(BlockingCollection<IMessage> output)
            {
                _input = new BlockingCollection<IMessage>(); ;
                _output = output;
                _taks = Task.Run(() =>
                {
                    //State
                    var count = 0;
    
                    while (true)
                    {
                        var m = _input.Take();
                        if (m is IncrementCommand)
                            count++;
                        if (m is TellCountCommand)
                            _output.Add(new SaidCountEvent(count, _taks.Id));
                    }
                });
            }
    
            public void Increment()
            {
                _input.Add(new IncrementCommand());
            }
    
            public void TellCount()
            {
                _input.Add(new TellCountCommand());
            }
        }
    

    Тут использовалась реактивная модель. Наши акторы не возвращают значения а только генерируют новые сообщения с нужными значениями. Тут у нас один актор просто в своем отдельном потоке, потокобезопасно от всех остальных, увеличивает свой счетчик. Другой актор просто пишет в консоль. Однако представим что у нас каждый актор например вычисляет текущие координаты, запас здоровья и манны одного из десятка игроков которые сейчас играют на нашем сервере. Каждый со своим собственным состоянием в своем собственном потоке. Отправляем другим сообщения или получает от других сообщения о полученном уроне, использованном навыке или смене своих координат.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      –1
      Что-то мне подсказывает, что Orleans недостаточно производительное решение для шутеров.
        0
        Я это просто по фану делаю. Настоящие игровые сервера для игр вроде шутеров имеют высокие ЦПУ баунд/Мемори баунд/Лоу латенси нагрузки. Их имеет смысл вообще на C++ или на Rust делать. Например сервер игры LOL сделан на C++. Да и вообще, иногда вообще без серверных вычислений используют решение. Каждый инстанс игры просто отправляет другим инстансам команды которые он получил. Например играют люди в какую нибудь сесионку 3 на 3. Персонаж игрока 1 сушествует на всех 6 компах одновременно. Когда он жмет кнопку огонь команда отправляется и его персонажу на его компе и остальным 5 персонажам по сети на других 5 компах. Сервер тут только проверяет на читы да собирает людей в команды. Ну да, для зашиты от читов обычно на сервере еще седьмой инстанс игры запускают помимо того что крутятся на 6 компах игроков. Ну и если человек вдруг вылетит и переподключится то он из этого инстанса текущее состояние игры получит. Тобишь вы нажимаете кнопку стрелять и информация о том что вы нажали эту кнопку отправляется 5 инстансам на компах 5 других игроков. Инстансу который запушен на вашем компе. Анти чит и резерв инстансу который крутиться на серверном компе. Вы нажали одну кнопку 7 копий вашего персонажа выстрелило. В общем в онлайн игре вообще может не быть именно игровых вычислений на сервере.
          0
          Послежу за продолжением. Пока смотриться интересно для кейса с тяжёлыми тасками связанными в цепочки, для шутера пока плохо представляю архитектуру.
          0
          Тем не менее вот здесь есть информация что его используют для Halo 4
            0
            В Quake Champignons тоже использовали, но для матчмейкинга.
            0

            Его не для игровой механики используют, а для всей обвязки вокруг. Хотя для MMO RPG игр подобная архитектура используется и для самого игрового мира. Для сессионных же всё, что не касается самого боя: авторизация, биллинг, матчмейкинг, состояние игрока и куча другой обвязки (ачивки, квесты, чат, соц связи, ивенты, нотификации, магазин и т.п.)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое