تعلم Akka.NET: خادم لعبة بسيطة على الإنترنت

مرحبا يا هابر! قررت أن هذا يعني محاولة إعادة كتابة الخادم الذي قمت به مع MS Orleans على Akka.NET لمحاولة هذه التقنية أيضًا. إذا كنت مهتمًا بما حدث من قبل ، فمرحباً بك في القط.

مصدر الرمز


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

عن اللعبة


التصوير باستخدام وضع المطابقة. الجميع ضد الجميع. # الخضراء هي خصوم. الأصفر # هو شخصيتك. الرمز الأحمر هو رصاصة. التصوير في الاتجاه الذي تتحرك فيه.

يتم التحكم في اتجاه الحركة بواسطة أزرار WASD أو الأسهم. يتم استخدام شريط المسافة في اللقطة. أريد إنشاء عميل رسومي على Three.js في المستقبل ووضع اللعبة على نوع من الاستضافة المجانية. حتى الآن ، لا يوجد سوى عميل وحدة تحكم مؤقت.



انطباعات شخصية


بشكل عام ، كلاهما يحل المشكلة عندما تريد موازاة حساباتك ولا تستخدم القفل (الكائن). بشكل تقريبي ، يمكن عادة وضع جميع الرموز التي لديك داخل القفل في الممثل. بالإضافة إلى ذلك ، يعيش كل ممثل حياته الخاصة ويمكن إعادة تشغيله بشكل مستقل عن الآخرين. في نفس الوقت ، الحفاظ على صلاحية النظام بأكمله ككل. التسامح مع الخطأ بشكل عام. بدا لي MS Orleans أكثر راحة وشحذًا في ظل RPC. Akka.NET أبسط وأقل. يمكن استخدامه ببساطة كمكتبة للحوسبة التفاعلية غير المتزامنة. تطلب MS Orleans على الفور تحديد منفذ منفصل وتهيئة لنفسها مضيفًا سيتم تشغيله عند بدء التطبيق. لا يحتاج Akka.NET إلى أي شيء في التكوين الأساسي. لقد قمت بتوصيل حزمة nuget واستخدامها. لكن MS Orleans كتبت بقوة واجهات للممثلين (الحبوب).بشكل عام ، إذا كنت بحاجة إلى كتابة خدمة صغيرة تمامًا على الممثلين ، فسأختار MS Orleans إذا قمنا في مكان واحد بموازاة الحسابات وتجنب مزامنة سلاسل الرسائل من خلال القفل أو AutoResetEventSlim أو أي شيء آخر مثل ذلك ، Akka.NET. لذا نعم ، هناك اعتقاد خاطئ يزعم أن خادم الرماية Hallo مصنوع على الممثلين. أوه ، لا يزال. هناك ، على الممثلين ، فقط أي بنية أساسية مثل المدفوعات وأشياء أخرى. منطق حركة اللاعب وضرب اللقطة ، وهذا هو منطق اللعبة ، يتم حسابه في C ++ monolith. هنا في لعبة MMO مثل WoW حيث يمكنك اختيار الهدف بوضوح ولديك إعادة شحن عالمية بحجم ثانية تقريبًا لجميع فترات التعاقد هناك غالبًا ما تستخدم الممثلين.

كود وتعليقات


نقطة دخول خادمنا. مركز 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);
        }
    }

تسجيل نظام الممثل لدينا في 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(" !");
        }
    }

All Articles