Akka.NET lernen: Server eines einfachen Online-Spiels

Hallo Habr! Ich entschied, dass es bedeutet, den Server, den ich mit MS Orleans auf Akka.NET gemacht habe, neu zu schreiben, nur um diese Technologie auch zu testen. Wenn Sie daran interessiert sind, was vorher passiert ist, begrüßen Sie bei cat.

Quellcode


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

Über das Spiel


Aufnehmen im Des Match-Modus. Alles gegen alle. Grüne # sind Gegner. Gelbe # ist dein Charakter. Red $ ist eine Kugel. Die Aufnahme erfolgt in die Richtung, in die Sie sich bewegen.

Die Bewegungsrichtung wird über die WASD-Tasten oder Pfeile gesteuert. Die Leertaste wird für die Aufnahme verwendet. Ich möchte in Zukunft einen grafischen Client auf Three.js erstellen und das Spiel auf eine Art kostenloses Hosting stellen. Bisher gibt es nur einen temporären Konsolenclient.



Persönliche Eindrücke


Im Allgemeinen lösen beide das Problem, wenn Sie Ihre Berechnungen parallelisieren möchten und keine Sperre (Objekt) verwenden. Grob gesagt kann der gesamte Code, den Sie im Schloss haben, normalerweise in einem Schauspieler platziert werden. Darüber hinaus lebt jeder Schauspieler sein eigenes Leben und kann unabhängig von den anderen neu gestartet werden. Gleichzeitig bleibt die Lebensfähigkeit des gesamten Systems erhalten. Fehlertoleranz im Allgemeinen. MS Orleans schien mir bequemer und unter RPC geschärft. Akka.NET ist einfacher und weniger. Es kann einfach als Bibliothek für reaktives asynchrones Computing verwendet werden. MS Orleans muss sofort einen separaten Port auswählen und einen Host für sich konfigurieren, der beim Start der Anwendung gestartet wird. Akka.NET benötigt in der Grundkonfiguration nichts. Ich habe das Nuget-Paket angeschlossen und benutze es. Aber MS Orleans hat stark typisierte Schnittstellen für Schauspieler (Körner).Wenn ich einen Microservice vollständig für die Schauspieler schreiben müsste, würde ich mich im Allgemeinen für MS Orleans entscheiden, wenn wir an einer Stelle nur die Berechnungen parallelisieren und vermeiden, Threads durch Sperre, AutoResetEventSlim oder ähnliches, Akka.NET, zu synchronisieren. Also ja, es gibt ein Missverständnis, angeblich wird der Hallo-Shooting-Server für die Schauspieler erstellt. Oh, immer noch. Dort auf den Akteuren nur jede Infrastruktur wie Zahlungen und andere Dinge. Die Logik der Bewegung des Spielers und des Treffens eines Schusses, das ist die Spiellogik, wird in C ++ - Monolith berechnet. Hier in einem MMO wie WoW, wo Sie das Ziel klar auswählen und eine globale Wiederaufladung von fast 1 Sekunde für alle Zaubersprüche haben, die dort häufig Schauspieler verwenden.

Code und Kommentare


Der Einstiegspunkt unseres Servers. 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);
        }
    }

Registrieren Sie unser Schauspielersystem in 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;
    }
});

Schauspieler


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

Nachrichten, die zwischen Akteuren weitergeleitet werden


//    .
//            .
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(" !");
        }
    }

All Articles