Изучаю Akka.NET: Сервер простой онлайн игры

от автора

Привет, Хабр! Решил я значит попробовать переписать тот сервер что делал с MS Orleans на Akka.NET просто чтобы попробовать и эту технологию тоже. Если вам интересно что получилось до добро пожаловать под кат.

Исходники

gitlab.com/VictorWinbringer/msorleansonlinegame/-/tree/master/Server/AkkaActors

Об игре

Стрелялка с режимом дес матч. Все против всех. Зеленые # это противники. Желтый # это ваш персонаж. Красный $ это пуля. Стрельба ведется в том направлении куда вы движетесь.

Направление движения регулируется кнопками W A S D или стрелочками. Для выстрела предназначена клавиша пробела. Хочу сделать в будущем графический клиент на Three.js и выложить игру на какой нибудь бесплатный хостинг. Пока что есть только временный консольный клиент.

Личные впечатления

В общем то они оба решают проблему когда вы хотите распаралелить свои вычисления и при этом не использовать lock(object). Грубо говоря весь код который у вас находиться внутри lock обычно можно поместить в актор. Кроме этого каждый актор живет своей жизнью и его независимо от других можно перезапустить. При этом сохраняя жизнеспособность всей системы в целом. Отказоустойчивость в общем. MS Orleans мне показался более удобный и заточенным под RPC. Akka.NET проще и меньше. Его можно просто как библиотеку для реактивный асинхронных вычислений использовать. MS Orleans сразу требует себе выделить отдельный порт и настроить для себя хост который будет запускаться при старте приложения. Akka.NET же в базовой комплектации ничего не надо. Подключил nuget пакет и пользуешься. Зато у MS Orleans строго типизированные интерфейсы для акторов(грейнов). В целом если бы мне нужно было написать микросервис целиком на акторах то я выбрал бы MS Orleans если же просто в одном месте распаралелить вычисления и избежать синхронизации потоков через lock, AutoResetEventSlim или еще что-то в этом роде то Akka.NET. Таки да, бытует заблуждение якобы сервер стрелялки Hallo сделан на акторах. Ой таки вей. Там на акторах только всякая инфраструктура вроде платежей и прочего. Сама логика движения игрока и попадания выстрела, тобишь игровая логика, она в C++ монолите вычисляется. Вот в MMO вроде WoW где где вы выбираете явно цель и у вас есть глобальная перезарядка размером в почти 1 секунду на все заклинания там часто используют акторы.

Код и коментарии

Входная точка нашего сервера. SignalR Hub

      public class GameHub : Hub     {         private readonly ActorSystem _system;         private readonly IServiceProvider _provider;         private readonly IGrainFactory _client;          public GameHub(             IGrainFactory client,             ActorSystem system,             IServiceProvider provider             )         {             _client = client;             _system = system;             _provider = provider;         }          public async Task JoinGame(long gameId, Guid playerId)         { //IActorRef это прокси к нашему актору. // Он передает полученные сообщения актору на который ссылается             var gameFactory = _provider.GetRequiredServiceByName<Func<long, IActorRef>>("game");             var game = gameFactory(gameId);             var random = new Random();             var player = new Player()             {                 IsAlive = true,                 GameId = gameId,                 Id = playerId,                 Point = new Point()                 {                     X = (byte)random.Next(1, GameActor.WIDTH - 1),                     Y = (byte)random.Next(1, GameActor.HEIGHT - 1)                 }             };             game.Tell(player);         }          public async Task GameInput(Input input, long gameId)         { // Путь к актору состоит из имени его родителя и его собственного имения.  // user это все пользовательские акторы. // целиком это будет что-то типо akka://game/user/1/2             _system.ActorSelection($"user/{gameId}/{input.PlayerId}").Tell(input);         }     } 

Регистрация нашей актор системы в DI:

services.AddSingleton(ActorSystem.Create("game")); var games = new Dictionary<long, IActorRef>(); services.AddSingletonNamedService<Func<long, IActorRef>>(     "game", (sp, name) => gameId => {     lock (games)     {         if (!games.TryGetValue(gameId, out IActorRef gameActor))         {             var frame = new Frame(GameActor.WIDTH, GameActor.HEIGHT) { GameId = gameId };             var gameEntity = new Game()             {                 Id = gameId,                 Frame = frame             }; //Фабрика для создания инстансов актора. // Передаем туда IServiceProvide чтобы актор мог резолвить нужные ему зависимости             var props = Props.Create(() => new GameActor(gameEntity, sp));             var actorSystem = sp.GetRequiredService<ActorSystem>(); //Создаем новый актор если такой мы еще не запускали.             gameActor = actorSystem.ActorOf(props, gameId.ToString());             games[gameId] = gameActor;         }         return gameActor;     } }); 

Акторы

GameActor

    public sealed class GameActor : UntypedActor     {         public const byte WIDTH = 100;         public const byte HEIGHT = 50;         private DateTime _updateTime;         private double _totalTime;         private readonly Game _game;         private readonly IHubContext<GameHub> _hub;          public GameActor(Game game, IServiceProvider provider)         {             _updateTime = DateTime.Now;             _game = game;             _hub = (IHubContext<GameHub>)provider.GetService(typeof(IHubContext<GameHub>)); //Запускаем шедулер который буде постоянно отправлять нашему актору сообщение //RunMessage говорящее ему пробежать один игровой цикл. //Обновление и оправка нового состояния на клиент.             Context                 .System                 .Scheduler                 .ScheduleTellRepeatedly( //Сколько надо подождать прежде чем начать отсылать сообщения.                     TimeSpan.FromMilliseconds(100),  //Раз в сколько миллисекунд отправлять сообщения                     TimeSpan.FromMilliseconds(1), //Получатель сообщения                     Context.Self,  //Что за сообщение отправлять получателю                     new RunMessage(), //Кто является отправителем сообщения. Nobody значит что никто. null тобишь.                     ActorRefs.Nobody                     );         }  //Основная точка входа нашего актора. //Срабатывает когда актор получает какое-то сообщение снаружи.         protected override void OnReceive(object message)         {             if (message is RunMessage run)                 Handle(run);             if (message is Player player)                 Handle(player);             if (message is Bullet bullet)                 Handle(bullet);         }  //Работает по принципу Create or Update //Если игровая сущность мертва то останавливает ее актор и удаляет ее из списка         private void Update<T>(             List<T> entities,             T entity,             Func<object> createInitMessage,             Func<Props> createProps             )             where T : IGameEntity         {             if (!entity.IsAlive)             {                 var actor = Context.Child(entity.Id.ToString());                 if (!actor.IsNobody())                     Context.Stop(actor);                 entities.RemoveAll(b => b.Id == entity.Id);             } //Create             else if (!entities.Any(b => b.Id == entity.Id))             {                 Context.ActorOf(createProps(), entity.Id.ToString());                 entities.Add(entity);                 Context.Child(entity.Id.ToString()).Tell(createInitMessage());             } //Update             else             {                 entities.RemoveAll(b => b.Id == entity.Id);                 entities.Add(entity);             }         }          private void Handle(Bullet bullet)         {             Update(                 _game.Bullets,                 bullet,                 () => new InitBulletMessage(bullet.Clone(), _game.Frame.Clone()),                 () => Props.Create(() => new BulletActor())                 );         }          private void Handle(Player player)         {             Update(                 _game.Players,                 player,                 () => new InitPlayerMessage(player.Clone(), _game.Frame.Clone()),                 () => Props.Create(() => new PlayerActor())             );         }          private void Handle(RunMessage run)         {             var deltaTime = DateTime.Now - _updateTime;             _updateTime = DateTime.Now;             var delta = deltaTime.TotalMilliseconds;             Update(delta);             Draw(delta);         }          private void Update(double deltaTime)         {             var players = _game.Players.Select(p => p.Clone()).ToList();             foreach (var child in Context.GetChildren())             {                 child.Tell(new UpdateMessage(deltaTime, players));             }         }          private void Draw(double deltaTime)         {             _totalTime += deltaTime;             if (_totalTime < 50)                 return;             _totalTime = 0; //PipeTo отправляем результат работы Task этому актору в виде сообщения. //Для ReciveActor можно просто стандартный async await использовать             _hub.Clients.All.SendAsync("gameUpdated", _game.Clone()).PipeTo(Self);         }     } 

BulletActor

    public class BulletActor : UntypedActor     {         private Bullet _bullet;         private Frame _frame;          protected override void OnReceive(object message)         {             if (message is InitBulletMessage bullet)                 Handle(bullet);             if (message is UpdateMessage update)                 Handle(update);         }          private void Handle(InitBulletMessage message)         {             _bullet = message.Bullet;             _frame = message.Frame;         }          private void Handle(UpdateMessage message)         {             if (_bullet == null)                 return;             if (!_bullet.IsAlive)             {                 Context.Parent.Tell(_bullet.Clone());                 return;             }             _bullet.Move(message.DeltaTime);             if (_frame.Collide(_bullet))                 _bullet.IsAlive = false;             if (!_bullet.IsInFrame(_frame))                 _bullet.IsAlive = false;             foreach (var player in message.Players)             {                 if (player.Id == _bullet.PlayerId)                     continue;  //Если пуля сталкивается с игроком то она говорит ему умереть //и умирает сама                 if (player.Collide(_bullet))                 {                     _bullet.IsAlive = false;                     Context                         .ActorSelection(Context.Parent.Path.ToString() + "/" + player.Id.ToString())                         .Tell(new DieMessage());                 }             }             Context.Parent.Tell(_bullet.Clone());         }     } 

PlayerActor

    public class PlayerActor : UntypedActor     {         private Player _player;         private Queue<Direction> _directions;         private Queue<Command> _commands;         private Frame _frame;          public PlayerActor()         {             _directions = new Queue<Direction>();             _commands = new Queue<Command>();         }          protected override void OnReceive(object message)         {             if (message is Input input)                 Handle(input);             if (message is UpdateMessage update)                 Handle(update);             if (message is InitPlayerMessage init)                 Handle(init);             if (message is DieMessage)             {                 _player.IsAlive = false;                 Context.Parent.Tell(_player.Clone());             }         }          private void Handle(InitPlayerMessage message)         {             _player = message.Player;             _frame = message.Frame;         }          private void Handle(Input message)         {             if (_player == null)                 return;             if (_player.IsAlive)             {                 foreach (var command in message.Commands)                 {                     _commands.Enqueue(command);                 }                  foreach (var direction in message.Directions)                 {                     _directions.Enqueue(direction);                 }             }         }          private void Handle(UpdateMessage update)         {             if (_player == null)                 return;             if (_player.IsAlive)             {                 HandleCommands(update.DeltaTime);                 HandleDirections();                 Move(update.DeltaTime);             }             Context.Parent.Tell(_player.Clone());         }          private void HandleDirections()         {             while (_directions.Count > 0)             {                 _player.Direction = _directions.Dequeue();             }         }          private void HandleCommands(double delta)         {             _player.TimeAfterLastShot += delta;             if (!_player.HasColldown && _commands.Any(command => command == Command.Shoot))             { //Shot просто фабричный метод который создает пулю // которая движется в том направлении куда смотри персонаж игрока                 var bullet = _player.Shot();                 Context.Parent.Tell(bullet.Clone());                 _commands.Clear();             }         }          private void Move(double delta)         {             _player.Move(delta);             if (_frame.Collide(_player))                 _player.MoveBack();         }     } 

Сообщения пересылаемые между акторами

//Просто приказывает персонажу игрока умереть. //Если он вдруг в этот момент неуязвим то он может проигнорировать это сообщение. public sealed class DieMessage { } 

//Инициализирует актор управляющей состоянием пули начальным значениями //Точнее говорит актору чтобы он себя инициализировал этими значениями     public sealed class InitBulletMessage     {         public Bullet Bullet { get; }         public Frame Frame { get; }          public InitBulletMessage(Bullet bullet, Frame frame)         {             Bullet = bullet ?? throw new ApplicationException("Укажите пулю");             Frame = frame ?? throw new ApplicationException("Укажите фрейм");         }     } 

//Говорит актору управляющему состоянием персонажа игрока  //воспользоваться этими значениями для своей инициализации     public class InitPlayerMessage     {         public Player Player { get; }         public Frame Frame { get; }          public InitPlayerMessage(Player player, Frame frame)         {             Player = player ?? throw new ApplicationException("Укажите игрока!");             Frame = frame ?? throw new ApplicationException("Укажите фрейм");         }     } 

//Просто говорит актору управляющему всем состоянием игры пробежать один игровой цикл public sealed class RunMessage { } 

//Говорит актору пули обновится свое состояние // с учетом прошедшего с последнего обновления времени.     public sealed class UpdateMessage     {         public double DeltaTime { get; } //Нужны чтобы пуля могла проверить свое попадание в одного из них         public List<Player> Players { get; }          public UpdateMessage(double deltaTime, List<Player> players)         {             DeltaTime = deltaTime;             Players = players ?? throw new ApplicationException("Укажите игроков!");         }     } 

ссылка на оригинал статьи https://habr.com/ru/post/500232/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *