Servidor de juegos en MS Orleans - Parte 3: Resumen



Hola Habr! Sigo estudiando MS Orleans y hago un juego simple en línea con un cliente de consola y un servidor que trabajan con granos de Orleans. Esta vez te contaré cómo terminó todo y qué conclusiones saqué por mí mismo. Para más detalles, bienvenido a cat.

Entonces, sí, si está interesado en cómo se hacen los servidores de juegos para juegos dinámicos, y no en mi experimento con MS Orleans, le recomiendo que mire este repositorio (UDP) y lea estos artículos:

  1. habr.com/en/post/303006
  2. habr.com/en/post/328118
  3. habr.com/en/company/pixonic/blog/499642
  4. habr.com/en/company/pixonic/blog/420019

Contenido



Código fuente


MsOrleansOnlineGame

Sobre el juego


El resultado fue un simple tirador. Green # son oponentes. # Amarillo es tu personaje. Red $ es una bala. Disparar es en la dirección hacia donde vas. La dirección del movimiento está controlada por los botones o flechas WASD. La barra espaciadora se usa para el disparo. No veo el punto de describir el código del cliente en detalle porque necesita ser reemplazado por uno normal. Gráfico.

Sobre actores (granos)


En resumen: en mi humilde opinión, Orleans es un gRPC con esteroides perfeccionados bajo Azure, escalando y trabajando con un monumento. Con un caché por ejemplo. Aunque sin un estado, como un RPC normal, sabe cómo trabajar con Stateless Worker Grains. Grain (Actor) en Orleans puede actuar como un punto de entrada como controlador en Asp.Net. Pero a diferencia del Controlador, el grano tiene una única instancia que tiene su propio identificador. Los granos son buenos cuando necesita trabajar con algún estado al mismo tiempo desde varios hilos o desde varios usuarios. Proporcionan un funcionamiento seguro con el hilo.

Por ejemplo, aquí hay un actor para una canasta de bienes. En la primera llamada, se creará y se bloqueará en la memoria desempeñando el papel de caché. Al mismo tiempo, miles de usuarios de miles de transmisiones diferentes pueden realizar solicitudes simultáneas para agregar y eliminar elementos. Todo el trabajo con su condición dentro de él será absolutamente seguro. En este caso, por supuesto, sería útil hacer que un actor compre con el método List GetBaskets () para obtener una lista de todas las cestas disponibles en el sistema. Al mismo tiempo, la Tienda también se colgará en la memoria como caché y todo el trabajo con ella será seguro para subprocesos.

    public interface IBasket : IGrainWithGuidKey
    {
        Task Add(string item);
        Task Remove(string item);
        Task<List<string>> GetItems();
    }

    public class BasketGrain : Grain, IBasket
    {
        private readonly ILogger<BasketGrain> _logger;
        private readonly IPersistentState<List<string>> _store;

        public BasketGrain(
            ILogger<BasketGrain> logger,
            [PersistentState("basket", "shopState")] IPersistentState<List<string>> store
        )
        {
            _logger = logger;
            _store = store;
        }

         public override Task OnActivateAsync()
        {
             var shop = GrainFactory.GetGrain<IShop>();
           //          .
            await shop.AddBasketIfNotContains(this.GetPrimaryKey())
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
          //       
         //    Asp.Net  . 
        //           - .
        //    -      .
        //         .
     //        
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }


        public Task Add(string item)
        {
            _store.State.Add(item);
            return Task.CompletedTask;
        }

        public Task Remove(string item)
        {
            _store.State.Remove(item);
            return Task.CompletedTask;
        }

        public Task<List<string>> GetItems()
        {
           //      .
          //        
         //           
            return Task.FromResult(new List<string>(_store.State));
        }
    }

Un ejemplo de uso en alguna aplicación de consola:

         private static async Task DoClientWork(IClusterClient client, Guid baskeId)
        {
            var basket = client.GetGrain<IBasket>(baskeId);
           //   gRPC -                
            await basket.Add("Apple");
        }

Código del juego



El mapa en el que luchan los jugadores:

   public interface IFrame : IGrainWithIntegerKey
    {
        Task Update(Frame frame);
        Task<Frame> GetState();
    }

    public class FrameGrain : Grain, IFrame
    {
        private readonly ILogger<FrameGrain> _logger;
        private readonly IPersistentState<Frame> _store;

        public FrameGrain(
            ILogger<FrameGrain> logger,
            [PersistentState("frame", "gameState")] IPersistentState<Frame> store
        )
        {
            _logger = logger;
            _store = store;
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
           //    1  1      .
            _store.State.GameId = this.GetPrimaryKeyLong();
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("DEACTIVATED");
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public Task Update(Frame frame)
        {
            _store.State = frame;
            return Task.CompletedTask;
        }

        public Task<Frame> GetState() => Task.FromResult(_store.State.Clone());
    }

Grano del juego que almacena el estado general del juego actual y lo envía al cliente 20 veces por segundo a través de SignalR.

    public interface IGame : IGrainWithIntegerKey
    {
        Task Update(Player player);
        Task Update(Bullet bullet);
        Task<List<Player>> GetAlivePlayers();
    }

    public class GameGrain : Grain, IGame
    {
        private const byte WIDTH = 100;
        private const byte HEIGHT = 50;
        private readonly ILogger<GameGrain> _logger;
        private readonly IPersistentState<Game> _store;
        private readonly IHubContext<GameHub> _hub;
        private IDisposable _timer;
        public GameGrain(
            ILogger<GameGrain> logger,
            [PersistentState("game", "gameState")] IPersistentState<Game> store,
            IHubContext<GameHub> hub
            )
        {
            _logger = logger;
            _store = store;
            _hub = hub;
        }

        public override async Task OnActivateAsync()
        {
            _store.State.Id = this.GetPrimaryKeyLong();
            _store.State.Frame = new Frame(WIDTH, HEIGHT) { GameId = _store.State.Id };
            var frame = GrainFactory.GetGrain<IFrame>(_store.State.Id);
            await frame.Update(_store.State.Frame.Clone());
            _logger.LogWarning("ACTIVATED");
            //      50      .       .
            _timer = RegisterTimer(Draw, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(50));
            await base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogWarning("DEACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Draw(object obj)
        {
            var state = _store.State;
            state.Bullets.RemoveAll(b => !b.IsAlive);
            state.Players.RemoveAll(p => !p.IsAlive);
            try
            {
                await _hub.Clients.All.SendAsync("gameUpdated", state.Clone());
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Error on send s");
            }
        }

        public Task Update(Player player)
        {
            _store.State.Players.RemoveAll(x => x.Id == player.Id);
            _store.State.Players.Add(player);
            return Task.CompletedTask;
        }
        public Task Update(Bullet bullet)
        {
            _store.State.Bullets.RemoveAll(x => x.Id == bullet.Id);
            _store.State.Bullets.Add(bullet);
            return Task.CompletedTask;
        }

        public Task<List<Player>> GetAlivePlayers() =>
            Task.FromResult(_store.State.Players.Where(p => p.IsAlive).Select(p => p.Clone()).ToList());
    }

SignalR es el centro a través del cual nos comunicamos con el cliente. Actúa como un proxy entre el cliente WebGl y Orleans. Hasta ahora, el cliente es la consola y es muy tonto. Quiero hacer un cliente web en el futuro en un navegador en Three.js y, por lo tanto, necesito una conexión en el zócalo web SignalR. El cliente Orleans en sí solo está en C #, a diferencia de gRPC, que está disponible en muchos idiomas, por lo tanto, para el cliente web, entre el servidor Orleans y los clientes necesita instalar un proxy (Gateway asp.net core).

    public class GameHub : Hub
    {
        private readonly IGrainFactory _client;

        public GameHub(IGrainFactory client)
        {
            _client = client;
        }

        public async Task GameInput(Input input)
        {
            var player = _client.GetGrain<IPlayer>(input.PlayerId);
            await player.Handle(input);
        }
    }

Jugador de grano. Se mueve automáticamente según el temporizador y responde a los comandos del usuario. Si llega un comando de disparo, crea una bala graine y establece la dirección del movimiento para él.

    public class PlayerGrain : Grain, IPlayer
    {
        private readonly ILogger<PlayerGrain> _logger;
        private readonly IPersistentState<Player> _store;
        private IDisposable _timer;
        private readonly Queue<Input> _inputs;
        public PlayerGrain(
            ILogger<PlayerGrain> logger,
            [PersistentState("player", "gameState")] IPersistentState<Player> store
        )
        {
            _logger = logger;
            _store = store;
            _inputs = new Queue<Input>();
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            // State   POCO     . Entity Player   
            _store.State.Id = this.GetPrimaryKey();
            _timer = RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(200));
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Handle(Input input)
        {
            _store.State.GameId = input.GameId;
            _inputs.Enqueue(input);
        }

        public async Task Update(object obj)
        {
            if (!_store.State.IsAlive)
            {
                await _store.ClearStateAsync();
               //          .
             //       .       .
                DeactivateOnIdle();
                return;
            }

            while (_inputs.Count > 0)
            {
                var input = _inputs.Dequeue();
                foreach (var direction in input.Directions.Where(d => d != Direction.None))
                {
                    _store.State.Direction = direction;
                }

                foreach (var command in input.Commands.Where(c => c != Command.None))
                {
                    if (command == Command.Shoot)
                    {
                        var bulletId = Guid.NewGuid();
                        var bullet = GrainFactory.GetGrain<IBullet>(bulletId);
                        //  Shot()           .
                        bullet.Update(_store.State.Shot()).Ignore(); //Ignore()        
                    }
                }
            }
            _store.State.Move();
            if (_store.State.GameId.HasValue)
            {
                var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);
                var fs = await frame.GetState();
                if (fs.Collide(_store.State))
                    _store.State.MoveBack();
                GrainFactory.GetGrain<IGame>(_store.State.GameId.Value)
                    .Update(_store.State.Clone())
                    .Ignore();
            }
        }

        public async Task Die()
        {
            _store.State.IsAlive = false;
            if (_store.State.GameId.HasValue)
                await GrainFactory.GetGrain<IGame>(_store.State.GameId.Value).Update(_store.State.Clone());
            await _store.ClearStateAsync();
            DeactivateOnIdle();
        }
    }


Balas de grano. Se mueve automáticamente en un temporizador y, si se encuentra con un jugador, le ordena morir. Si encuentra un obstáculo en el mapa, ella misma muere.
  public interface IBullet : IGrainWithGuidKey
    {
        Task Update(Bullet dto);
    }

    public class BulletGrain : Grain, IBullet
    {
        private readonly ILogger<BulletGrain> _logger;
        private readonly IPersistentState<Bullet> _store;
        private IDisposable _timer;
        public BulletGrain(
            ILogger<BulletGrain> logger,
            [PersistentState("bullet", "gameState")] IPersistentState<Bullet> store
        )
        {
            _logger = logger;
            _store = store;
        }

        public Task Update(Bullet dto)
        {
            _store.State = dto;
            _store.State.Id = this.GetPrimaryKey();
            return Task.CompletedTask;
        }

        public override Task OnActivateAsync()
        {
            _logger.LogInformation("ACTIVATED");
            _timer = this.RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(50));
            return base.OnActivateAsync();
        }

        public override async Task OnDeactivateAsync()
        {
            _logger.LogInformation("DEACTIVATED");
            _timer?.Dispose();
            _timer = null;
            await _store.WriteStateAsync();
            await base.OnDeactivateAsync();
        }

        public async Task Update(object obj)
        {
            if (!_store.State.IsAlive)
            {
                await _store.ClearStateAsync();
                DeactivateOnIdle();
                return;
            }
            _store.State.Move();
            if (_store.State.GameId.HasValue)
            {
                var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);
                var fs = await frame.GetState();
                if (fs.Collide(_store.State))
                    _store.State.IsAlive = false;
                if (_store.State.Point.X > fs.Width || _store.State.Point.Y > fs.Height)
                    _store.State.IsAlive = false;
                var game = GrainFactory.GetGrain<IGame>(_store.State.GameId.Value);
                var players = await game.GetAlivePlayers();
                foreach (var player in players)
                {
                    if (player.Collide(_store.State))
                    {
                        _store.State.IsAlive = false;
                        GrainFactory.GetGrain<IPlayer>(player.Id).Die().Ignore();
                        break;
                    }
                }
                game.Update(_store.State.Clone()).Ignore();
            }
        }
    }

All Articles