Belajar Akka.NET: Server game online yang sederhana

Halo, Habr! Saya memutuskan itu berarti mencoba menulis ulang server yang saya lakukan dengan MS Orleans di Akka.NET hanya untuk mencoba teknologi ini juga. Jika Anda tertarik dengan apa yang terjadi sebelumnya, selamat datang di kucing.

Kode sumber


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

Tentang permainan


Memotret dengan mode des match. Semua melawan semua orang. Hijau # adalah lawan. Kuning # adalah karakter Anda. Red $ adalah sebuah peluru. Pemotretan adalah ke arah Anda bergerak.

Arah gerakan dikendalikan oleh tombol WASD atau panah. Bilah spasi digunakan untuk pemotretan. Saya ingin membuat klien grafis di Three.js di masa depan dan menempatkan permainan di beberapa jenis hosting gratis. Sejauh ini, hanya ada klien konsol sementara.



Kesan pribadi


Secara umum, mereka berdua memecahkan masalah ketika Anda ingin memparalelkan perhitungan Anda dan tidak menggunakan kunci (objek). Secara kasar, semua kode yang Anda miliki di dalam kunci biasanya dapat ditempatkan di aktor. Selain itu, setiap aktor menjalani hidupnya sendiri dan dapat memulai kembali secara independen dari yang lain. Pada saat yang sama, menjaga kelangsungan hidup seluruh sistem secara keseluruhan. Toleransi kesalahan secara umum. Bagi saya MS Orleans lebih nyaman dan dipertajam di bawah RPC. Akka.NET lebih sederhana dan lebih sedikit. Ini dapat digunakan hanya sebagai perpustakaan untuk komputasi asinkron yang reaktif. MS Orleans segera mengharuskan dirinya untuk memilih port terpisah dan mengkonfigurasi sendiri host yang akan diluncurkan ketika aplikasi dimulai. Akka.NET tidak memerlukan apa pun dalam konfigurasi dasar. Saya menghubungkan paket nuget dan menggunakannya. Tetapi MS Orleans memiliki antarmuka yang sangat diketik untuk aktor (biji-bijian).Secara umum, jika saya perlu menulis microservice sepenuhnya pada aktor, saya akan memilih MS Orleans jika, di satu tempat, kami hanya menyejajarkan perhitungan dan menghindari menyinkronkan utas melalui kunci, AutoResetEventSlim atau yang lainnya, Akka.NET. Jadi ya, ada kesalahpahaman yang diduga server shooting Hallo dibuat pada aktor. Oh, tetap saja. Di sana, pada aktor, hanya infrastruktur seperti pembayaran dan hal-hal lain. Logika gerakan pemain dan pukulan, itulah logika permainan, dihitung dalam C ++ monolith. Di sini, di MMO seperti WoW di mana Anda memilih target dengan jelas dan Anda memiliki pengisian global hampir 1 detik untuk semua mantra yang sering menggunakan aktor.

Kode dan komentar


Titik masuk server kami. Hub SignalR


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

Daftarkan sistem aktor kami di 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;
    }
});

Aktor


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

Pesan diteruskan antar aktor


//    .
//            .
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