Изучаю 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("Укажите игроков!");
            }
        }
    
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

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

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