Исходники
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/
Добавить комментарий