рд▓рд░реНрдирд┐рдВрдЧ рдЕрдХреНрдХрд╛.рдиреЗрдЯ: рдПрдХ рд╕рд░рд▓ рдСрдирд▓рд╛рдЗрди рдЧреЗрдо рдХрд╛ рд╕рд░реНрд╡рд░

рдирдорд╕реНрдХрд╛рд░, рд╣реЗрдмреНрд░! рдореИрдВрдиреЗ рддрдп рдХрд┐рдпрд╛ рдХрд┐ рдЗрд╕рдХрд╛ рдорддрд▓рдм рд╣реИ рдХрд┐ рдореИрдВ рд╕рд░реНрд╡рд░ рдХреЛ рдлрд┐рд░ рд╕реЗ рд▓рд┐рдЦрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣рд╛ рд╣реВрдВ рдЬреЛ рдореИрдВрдиреЗ рдЗрд╕ рддрдХрдиреАрдХ рдХреЗ рд▓рд┐рдП рднреА рдкреНрд░рдпрд╛рд╕ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд┐рд░реНрдл рдЕрдХреНрдХрд╛.рдиреЗрдЯ рдкрд░ рдПрдордПрд╕ рдСрд░рд▓рд┐рдпрдиреНрд╕ рдХреЗ рд╕рд╛рде рдХрд┐рдпрд╛ рдерд╛ред рдпрджрд┐ рдЖрдк рдкрд╣рд▓реЗ рдХреНрдпрд╛ рд╣реБрдЖ рдореЗрдВ рд░реБрдЪрд┐ рд░рдЦрддреЗ рд╣реИрдВ, рддреЛ рдмрд┐рд▓реНрд▓реА рдХрд╛ рд╕реНрд╡рд╛рдЧрдд рдХрд░реЗрдВред

рд╕реЛрд░реНрд╕ рдХреЛрдб


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

рдЦреЗрд▓ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ


рдбреЗрд╕ рдореИрдЪ рдореЛрдб рдХреЗ рд╕рд╛рде рд╢реВрдЯрд┐рдВрдЧред рд╕рдмрдХреЗ рдЦрд┐рд▓рд╛рдлред рд╣рд░рд╛ # рд╡рд┐рд░реЛрдзреА рд╣реИрдВред рдкреАрд▓рд╛ # рдЖрдкрдХрд╛ рдЪрд░рд┐рддреНрд░ рд╣реИред рд▓рд╛рд▓ $ рдПрдХ рдЧреЛрд▓реА рд╣реИред рд╢реВрдЯрд┐рдВрдЧ рдЙрд╕ рджрд┐рд╢рд╛ рдореЗрдВ рд╣реИ рдЬреЛ рдЖрдк рдЖрдЧреЗ рдмрдврд╝ рд░рд╣реЗ рд╣реИрдВред

рдЖрдВрджреЛрд▓рди рдХреА рджрд┐рд╢рд╛ WASD рдмрдЯрди рдпрд╛ рддреАрд░ рджреНрд╡рд╛рд░рд╛ рдирд┐рдпрдВрддреНрд░рд┐рдд рдХреА рдЬрд╛рддреА рд╣реИред рд╕реНрдкреЗрд╕ рдмрд╛рд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рд╢реЙрдЯ рдХреЗ рд▓рд┐рдП рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдореИрдВ рднрд╡рд┐рд╖реНрдп рдореЗрдВ рдереНрд░реАред рдЬреЗрдПрд╕ рдкрд░ рдПрдХ рдЧреНрд░рд╛рдлрд┐рдХрд▓ рдХреНрд▓рд╛рдЗрдВрдЯ рдмрдирд╛рдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВ рдФрд░ рдЧреЗрдо рдХреЛ рдХрд┐рд╕реА рддрд░рд╣ рдХреА рдореБрдлреНрдд рд╣реЛрд╕реНрдЯрд┐рдВрдЧ рдкрд░ рд░рдЦрдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВред рдЕрдм рддрдХ, рдХреЗрд╡рд▓ рдПрдХ рдЕрд╕реНрдерд╛рдпреА рдХрдВрд╕реЛрд▓ рдХреНрд▓рд╛рдЗрдВрдЯ рд╣реИред



рд╡реНрдпрдХреНрддрд┐рдЧрдд рдЫрд╛рдкреЗрдВ


рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рд╡реЗ рджреЛрдиреЛрдВ рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рддреЗ рд╣реИрдВ рдЬрдм рдЖрдк рдЕрдкрдиреА рдЧрдгрдирд╛рдУрдВ рдХреЛ рд╕рдорд╛рдирд╛рдВрддрд░ рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ рдФрд░ рд▓реЙрдХ (рдСрдмреНрдЬреЗрдХреНрдЯ) рдХрд╛ рдЙрдкрдпреЛрдЧ рдирд╣реАрдВ рдХрд░рддреЗ рд╣реИрдВред рдореЛрдЯреЗ рддреМрд░ рдкрд░, рд▓реЙрдХ рдХреЗ рдЕрдВрджрд░ рдЖрдкрдХреЗ рдкрд╛рд╕ рдореМрдЬреВрдж рд╕рднреА рдХреЛрдб рдЖрдорддреМрд░ рдкрд░ рдПрдХ рдЕрднрд┐рдиреЗрддрд╛ рдореЗрдВ рд░рдЦреЗ рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВред рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, рдкреНрд░рддреНрдпреЗрдХ рдЕрднрд┐рдиреЗрддрд╛ рдЕрдкрдирд╛ рдЬреАрд╡рди рдЬреАрддрд╛ рд╣реИ рдФрд░ рджреВрд╕рд░реЛрдВ рд╕реЗ рд╕реНрд╡рддрдВрддреНрд░ рд░реВрдк рд╕реЗ рдкреБрдирдГ рдЖрд░рдВрдн рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рдЗрд╕реА рд╕рдордп, рдкреВрд░реЗ рд╕рд┐рд╕реНрдЯрдо рдХреА рд╡реНрдпрд╡рд╣рд╛рд░реНрдпрддрд╛ рдХреЛ рдПрдХ рдкреВрд░реЗ рдХреЗ рд░реВрдк рдореЗрдВ рдмрдирд╛рдП рд░рдЦрдирд╛ред рд╕рд╛рдорд╛рдиреНрдп рд░реВрдк рд╕реЗ рджреЛрд╖ рд╕рд╣рд┐рд╖реНрдгреБрддрд╛ред рдПрдордПрд╕ рдСрд░рд▓рд┐рдпрдиреНрд╕ рдореБрдЭреЗ рдФрд░ рдЕрдзрд┐рдХ рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рдФрд░ рдЖрд░рдкреАрд╕реА рдХреЗ рддрд╣рдд рддреЗрдЬ рд▓рдЧ рд░рд╣рд╛ рдерд╛ред Akka.NET рд╕рд░рд▓ рдФрд░ рдХрдо рд╣реИред рдпрд╣ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рд╢реАрд▓ рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рдХрдВрдкреНрдпреВрдЯрд┐рдВрдЧ рдХреЗ рд▓рд┐рдП рдПрдХ рдкреБрд╕реНрддрдХрд╛рд▓рдп рдХреЗ рд░реВрдк рдореЗрдВ рдмрд╕ рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рдПрдордПрд╕ рдСрд░рд▓рд┐рдпрдиреНрд╕ рдХреЛ рддреБрд░рдВрдд рдЦреБрдж рдХреЛ рдПрдХ рдЕрд▓рдЧ рдкреЛрд░реНрдЯ рдХрд╛ рдЪрдпрди рдХрд░рдиреЗ рдФрд░ рдЕрдкрдиреЗ рд▓рд┐рдП рдПрдХ рд╣реЛрд╕реНрдЯ рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ рдЬрд┐рд╕реЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╢реБрд░реВ рд╣реЛрдиреЗ рдкрд░ рд▓реЙрдиреНрдЪ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред рдмреБрдирд┐рдпрд╛рджреА рд╡рд┐рдиреНрдпрд╛рд╕ рдореЗрдВ рдЕрдХреНрдХрд╛.рдиреЗрдЯ рдХреЛ рдХрд┐рд╕реА рдЪреАрдЬ рдХреА рдЬрд░реВрд░рдд рдирд╣реАрдВ рд╣реИред рдореИрдВрдиреЗ рдирдЧреЗрдЯ рдкреИрдХреЗрдЬ рдХреЛ рдЬреЛрдбрд╝рд╛ рдФрд░ рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ред рд▓реЗрдХрд┐рди рдПрдордПрд╕ рдСрд░рд▓рд┐рдпрдиреНрд╕ рдиреЗ рдЕрднрд┐рдиреЗрддрд╛рдУрдВ (рдЕрдирд╛рдЬ) рдХреЗ рд▓рд┐рдП рдЬреЛрд░рджрд╛рд░ рдЗрдВрдЯрд░рдлреЗрд╕ рдЯрд╛рдЗрдк рдХрд┐рдпрд╛ рд╣реИредрд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рдЕрдЧрд░ рдореБрдЭреЗ рдЕрднрд┐рдиреЗрддрд╛рдУрдВ рдкрд░ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдПрдХ рдорд╛рдЗрдХреНрд░реЛ-рд╕рд░реНрд╡рд┐рд╕ рд▓рд┐рдЦрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ, рддреЛ рдореИрдВ рдПрдордПрд╕ рдСрд░рд▓рд┐рдпрдиреНрд╕ рдХреЛ рдЪреБрдиреВрдВрдЧрд╛, рдЕрдЧрд░, рдПрдХ рд╕реНрдерд╛рди рдкрд░, рд╣рдо рд╕рд┐рд░реНрдл рдЧрдгрдирд╛рдУрдВ рдХреЛ рд╕рдорд╛рдирд╛рдВрддрд░ рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рд▓реЙрдХ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдереНрд░реЗрдб рдХреЛ рд╕рд┐рдВрдХреНрд░рдирд╛рдЗрдЬрд╝ рдХрд░рдиреЗ рд╕реЗ рдмрдЪрддреЗ рд╣реИрдВ, AutoResetEventSlim рдпрд╛ рдРрд╕рд╛ рд╣реА рдХреБрдЫ, Akka.NETред рддреЛ рд╣рд╛рдВ, рдПрдХ рдЧрд▓рдд рдзрд╛рд░рдгрд╛ рд╣реИ рдХрд┐ рдХрдерд┐рдд рддреМрд░ рдкрд░ рд╣реЗрд▓реЛ рд╢реВрдЯрд┐рдВрдЧ рд╕рд░реНрд╡рд░ рдЕрднрд┐рдиреЗрддрд╛рдУрдВ рдкрд░ рдмрдирд╛рдпрд╛ рдЧрдпрд╛ рд╣реИред рдУрд╣, рдЕрднреА рднреАред рд╡рд╣рд╛рдВ, рдЕрднрд┐рдиреЗрддрд╛рдУрдВ рдкрд░, рднреБрдЧрддрд╛рди рдФрд░ рдЕрдиреНрдп рдЪреАрдЬреЛрдВ рдХреА рддрд░рд╣ рдХреЗрд╡рд▓ рдХреЛрдИ рдмреБрдирд┐рдпрд╛рджреА рдврд╛рдВрдЪрд╛ред рдЦрд┐рд▓рд╛рдбрд╝реА рдХреЗ рдЖрдВрджреЛрд▓рди рдФрд░ рд╢реЙрдЯ рдорд╛рд░рдиреЗ рдХрд╛ рдмрд╣реБрдд рддрд░реНрдХ, рдЦреЗрд▓ рддрд░реНрдХ, рдЗрд╕рдХреА рдЧрдгрдирд╛ C ++ рдореЛрдиреЛрд▓рд┐рде рдореЗрдВ рдХреА рдЬрд╛рддреА рд╣реИред рдпрд╣рд╛рдБ рд╡рд╛рд╣ рдХреА рддрд░рд╣ рдПрдХ MMO рдореЗрдВ рдЬрд╣рд╛рдБ рдЖрдк рд╕реНрдкрд╖реНрдЯ рд░реВрдк рд╕реЗ рдПрдХ рд▓рдХреНрд╖реНрдп рдХрд╛ рдЪрдпрди рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдЖрдкрдХреЗ рдкрд╛рд╕ рд▓рдЧрднрдЧ 1 рд╕реЗрдХрдВрдб рдХрд╛ рд╡реИрд╢реНрд╡рд┐рдХ рд░рд┐рдЪрд╛рд░реНрдЬ рд╣реЛрддрд╛ рд╣реИ, рд╡рд╣рд╛рдБ рд╕рднреА рдордВрддреНрд░реЛрдВ рдХреЗ рд▓рд┐рдП рдЕрдХреНрд╕рд░ рдЕрднрд┐рдиреЗрддрд╛рдУрдВ рдХрд╛ рдЙрдкрдпреЛрдЧ рд╣реЛрддрд╛ рд╣реИред

рдХреЛрдб рдФрд░ рдЯрд┐рдкреНрдкрдгрд┐рдпрд╛рдБ


рд╣рдорд╛рд░реЗ рд╕рд░реНрд╡рд░ рдХрд╛ рдкреНрд░рд╡реЗрд╢ рдмрд┐рдВрджреБред рд╕рд┐рдЧреНрдирд▓рдЖрд░ рд╣рдм


      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