Hola Habr! Decidí que significa tratar de reescribir el servidor que hice con MS Orleans en Akka.NET solo para probar esta tecnología también. Si está interesado en lo que sucedió antes, bienvenido a cat.Código fuente
gitlab.com/VictorWinbringer/msorleansonlinegame/-/tree/master/Server/AkkaActorsSobre el juego
Disparo con el modo des match. Todo contra todos. Green # son oponentes. # Amarillo es tu personaje. Red $ es una bala. Disparar es en la dirección en que te mueves.La dirección del movimiento está controlada por los botones o flechas WASD. La barra espaciadora se usa para el disparo. Quiero hacer un cliente gráfico en Three.js en el futuro y poner el juego en algún tipo de alojamiento gratuito. Hasta ahora, solo hay un cliente de consola temporal.
Impresiones personales
En general, ambos resuelven el problema cuando quieres paralelizar tus cálculos y no usas lock (objeto). En términos generales, todo el código que tiene dentro de la cerradura generalmente se puede colocar en un actor. Además, cada actor vive su propia vida y puede reiniciarse independientemente de los demás. Al mismo tiempo, mantiene la viabilidad de todo el sistema en su conjunto. Tolerancia a fallos en general. MS Orleans me pareció más conveniente y afilado bajo RPC. Akka.NET es más simple y menos. Se puede usar simplemente como una biblioteca para la computación asíncrona reactiva. MS Orleans se requiere inmediatamente para seleccionar un puerto separado y configurar por sí mismo un host que se iniciará cuando se inicie la aplicación. Akka.NET no necesita nada en la configuración básica. Conecté el paquete nuget y lo uso. Pero MS Orleans tiene interfaces fuertemente tipadas para actores (granos).En general, si tuviera que escribir un microservicio completamente en los actores, elegiría MS Orleans si, en un lugar, paralelizamos los cálculos y evitamos sincronizar los hilos a través del bloqueo, AutoResetEventSlim o algo así, Akka.NET. Entonces, sí, hay una idea errónea que supuestamente el servidor de tiro de Hallo está hecho para los actores. Oh aun. Allí, en los actores, solo cualquier infraestructura como pagos y otras cosas. La lógica del movimiento del jugador y golpear un tiro, esa es la lógica del juego, se calcula en monolito C ++. Aquí en un MMO como WoW, donde eliges claramente un objetivo y tienes una recarga global de casi 1 segundo de tamaño para todos los hechizos que a menudo usan actores.Código y comentarios
El punto de entrada de nuestro servidor. 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)
{
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)
{
_system.ActorSelection($"user/{gameId}/{input.PlayerId}").Tell(input);
}
}
Registre nuestro sistema de actores en 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
};
var props = Props.Create(() => new GameActor(gameEntity, sp));
var actorSystem = sp.GetRequiredService<ActorSystem>();
gameActor = actorSystem.ActorOf(props, gameId.ToString());
games[gameId] = gameActor;
}
return gameActor;
}
});
Actores
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>));
Context
.System
.Scheduler
.ScheduleTellRepeatedly(
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(1),
Context.Self,
new RunMessage(),
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);
}
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);
}
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());
}
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;
_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))
{
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();
}
}
Mensajes reenviados entre actores
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(" !");
}
}