Game Server auf MS Orleans - Teil 3: Zusammenfassung



Hallo Habr! Ich studiere weiterhin MS Orleans und mache ein einfaches Online-Spiel mit einem Konsolen-Client und -Server, die mit Orleans-Körnern arbeiten. Dieses Mal werde ich Ihnen erzählen, wie alles endete und welche Schlussfolgerungen ich für mich selbst gezogen habe. Für Details willkommen bei Katze.

Also ja, wenn Sie überhaupt daran interessiert sind, wie Spieleserver für dynamische Spiele hergestellt werden, und nicht an meinem Experiment mit MS Orleans, empfehle ich Ihnen, sich dieses Repository (UDP) anzusehen und die folgenden Artikel zu lesen:

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

Inhalt



Quellcode


MsOrleansOnlineGame

Über das Spiel


Das Ergebnis war ein einfacher Schütze. Grüne # sind Gegner. Gelbe # ist dein Charakter. Red $ ist eine Kugel. Das Schießen erfolgt in die Richtung, in die Sie gehen. Die Bewegungsrichtung wird über die WASD-Tasten oder Pfeile gesteuert. Die Leertaste wird für die Aufnahme verwendet. Ich sehe keinen Sinn darin, den Client-Code im Detail zu beschreiben, da er durch einen normalen ersetzt werden muss. Grafik.

Über Schauspieler (Körner)


Kurz gesagt: Meiner Meinung nach ist Orleans ein gRPC für Steroide, die unter Azure geschliffen wurden, skaliert und mit einem Denkmal arbeitet. Zum Beispiel mit einem Cache. Obwohl er keinen Staat hat, wie ein normaler RPC, weiß er, wie man mit Stateless Worker Grains arbeitet. Grain (Schauspieler) in Orleans kann als Einstiegspunkt als Controller in Asp.Net fungieren. Im Gegensatz zum Controller verfügt das Korn jedoch über eine einzelne Instanz mit einer eigenen Kennung. Körner sind gut, wenn Sie mit einem bestimmten Status gleichzeitig von mehreren Threads oder von mehreren Benutzern aus arbeiten müssen. Sie bieten damit einen thread-sicheren Betrieb.

Hier ist zum Beispiel ein Schauspieler für einen Warenkorb. Beim ersten Aufruf wird es erstellt und bleibt im Speicher hängen und spielt die Rolle eines Caches. Gleichzeitig können Tausende von Benutzern aus Tausenden verschiedener Streams gleichzeitig Anfragen zum Hinzufügen und Entfernen von Elementen stellen. Alle Arbeiten mit seinem Zustand in ihm sind absolut fadensicher. In diesem Fall wäre es natürlich nützlich, einen Schauspieler-Shop mit der List GetBaskets () -Methode zu erstellen, um eine Liste aller im System verfügbaren Körbe abzurufen. Gleichzeitig bleibt der Shop als Cache im Speicher und alle Arbeiten damit sind threadsicher.

    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));
        }
    }

Ein Beispiel für die Verwendung in einigen Konsolenanwendungen:

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

Spielcode



Die Karte, auf der die Spieler kämpfen:

   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());
    }

Körnung des Spiels, in der der allgemeine Status des aktuellen Spiels gespeichert und 20 Mal pro Sekunde über SignalR an den Client gesendet wird.

    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 ist der Hub, über den wir mit dem Kunden kommunizieren. Es fungiert als Proxy zwischen dem WebGl-Client und Orleans. Bisher ist der Client Konsole und er ist wild dumm. Ich möchte in Zukunft einen Webclient in einem Browser auf Three.js erstellen und benötige daher eine Verbindung zum SignalR-Web-Socket. Der Orleans-Client selbst ist im Gegensatz zu gRPC, das in vielen Sprachen verfügbar ist, nur in C # verfügbar. Für den Web-Client müssen Sie daher einen Proxy zwischen dem Orleans-Server und den Clients installieren (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);
        }
    }

Getreidespieler. Es bewegt sich automatisch entsprechend dem Timer und reagiert auf Benutzerbefehle. Wenn ein Schießbefehl eintrifft, erstellt er eine Kugel und legt die Bewegungsrichtung für ihn fest.

    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();
        }
    }


Getreidekugeln. Es bewegt sich automatisch auf einem Timer und befiehlt ihm zu sterben, wenn es auf einen Spieler trifft. Wenn sie auf ein Hindernis auf der Karte stößt, stirbt sie selbst.
  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