Привет Хабр! И так, на четырнадцатый день копья решил я значит начать делать простенький игровой сервер для простой онлайн стрелялки. За одно тему распределенных вычислений затронуть. В этой вводной статье цикла хочу рассказать что такое акторы (в Орлеанс их зернами называют) и принцип их работы. Для этого я пока пример простенького приложения с самодельными акторами без 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();
}
}
- Интерфейс которым мы помечаем сообщения которые получает или отправляет актор:
interface IMessage { }
- Команда с помощью которой мы говорим актору увеличить его счетчик (внутренне состояние):
class IncrementCommand : IMessage { }
- Команда с помощью которой мы говорим актору сказать его текущее состояние (счетчик) другим:
class TellCountCommand : IMessage { }
- Событие которое говорит другим акторам о том что актор сказал всем свое текущее состояние (счетчик). Генерируется при обработке команды TellCountCommand:
class SaidCountEvent : IMessage { public int Count { get; } public int ActorId { get; } public SaidCountEvent(int count, int actorId) { Count = count; ActorId = actorId; } }
Count это сколько сейчас набралось на счетчике у актора с идентификатором равным 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()); } //Говорим акторам сказать всем сколько у них набралось на счетчике. //Так как они отправляют свои события writer актору он выводит их на экран firstInput.Add(new TellCountCommand()); secondInput.Add(new TellCountCommand()); //говорим writer актору вывести сообщение в консоль. writerInput.Add(new WriteMessageCommand("Конец метода Main")); Console.ReadLine(); }
Тут использовалась реактивная модель. Наши акторы не возвращают значения а только генерируют новые сообщения с нужными значениями. Тут у нас один актор просто в своем отдельном потоке, потокобезопасно от всех остальных, увеличивает свой счетчик. Другой актор просто пишет в консоль. Однако представим что у нас каждый актор например вычисляет текущие координаты, запас здоровья и манны одного из десятка игроков которые сейчас играют на нашем сервере. Каждый со своим собственным состоянием в своем собственном потоке. Отправляем другим сообщения или получает от других сообщения о полученном уроне, использованном навыке или смене своих координат.
FreeBa
Что-то мне подсказывает, что Orleans недостаточно производительное решение для шутеров.
VanquisherWinbringer Автор
Я это просто по фану делаю. Настоящие игровые сервера для игр вроде шутеров имеют высокие ЦПУ баунд/Мемори баунд/Лоу латенси нагрузки. Их имеет смысл вообще на C++ или на Rust делать. Например сервер игры LOL сделан на C++. Да и вообще, иногда вообще без серверных вычислений используют решение. Каждый инстанс игры просто отправляет другим инстансам команды которые он получил. Например играют люди в какую нибудь сесионку 3 на 3. Персонаж игрока 1 сушествует на всех 6 компах одновременно. Когда он жмет кнопку огонь команда отправляется и его персонажу на его компе и остальным 5 персонажам по сети на других 5 компах. Сервер тут только проверяет на читы да собирает людей в команды. Ну да, для зашиты от читов обычно на сервере еще седьмой инстанс игры запускают помимо того что крутятся на 6 компах игроков. Ну и если человек вдруг вылетит и переподключится то он из этого инстанса текущее состояние игры получит. Тобишь вы нажимаете кнопку стрелять и информация о том что вы нажали эту кнопку отправляется 5 инстансам на компах 5 других игроков. Инстансу который запушен на вашем компе. Анти чит и резерв инстансу который крутиться на серверном компе. Вы нажали одну кнопку 7 копий вашего персонажа выстрелило. В общем в онлайн игре вообще может не быть именно игровых вычислений на сервере.
eternum
Послежу за продолжением. Пока смотриться интересно для кейса с тяжёлыми тасками связанными в цепочки, для шутера пока плохо представляю архитектуру.
AndreyNikolin
Тем не менее вот здесь есть информация что его используют для Halo 4
eternum
В Quake Champignons тоже использовали, но для матчмейкинга.
DrVirtual
Его не для игровой механики используют, а для всей обвязки вокруг. Хотя для MMO RPG игр подобная архитектура используется и для самого игрового мира. Для сессионных же всё, что не касается самого боя: авторизация, биллинг, матчмейкинг, состояние игрока и куча другой обвязки (ачивки, квесты, чат, соц связи, ивенты, нотификации, магазин и т.п.)