Conectando Unity3D a um servidor de jogo Erlang

Neste protocolo, vou mostrar como você pode facilmente fazer uma conexão TCP entre Erlang e Unity3D (C #).

Este protocolo pressupõe que o leitor esteja familiarizado com os conceitos básicos do Erlang, como supervisor ou gen_server, bem como com os conceitos básicos de script do Unity em C #. Além disso, alguns códigos serão deixados de fora. Mostro apenas as partes necessárias.

Fundamentos

A coisa mais importante a entender aqui é como o protocolo TCP é projetado e como queremos (ab) usá-lo. O TCP é um protocolo de streaming, o que significa que, se enviarmos N bytes de dados, dependendo de muitas coisas, podemos recebê-los em um pacote ou em vários pacotes. Isso é lamentável para nós, já que normalmente queremos enviar uma mensagem (como mover o player para (X, Y)) e não um monte de bytes. O método que queremos usar é chamado de enquadramento. Isto é realmente simples: primeiro enviamos qual é o tamanho da mensagem (normalmente 1, 2 ou 4 bytes inteiros), e vem a mensagem real.

Como a mensagem média é bastante curta, vamos escolher 2 bytes para indicar o comprimento da mensagem.

Servidor

Supervisor

Vamos começar com a parte de Erlang. A chave aqui é usar o sistema de enquadramento integrado de Erlang.
Primeiro crie um supervisor. O método deve ser semelhante a este:init/1

init([]) ->
case gen_tcp:listen(Port, [{active, false}, binary, {packet, 2}]) of
{ok, Socket} -> {ok, SupervisorFlags, [ChildDefinition]};
{error, Error} -> {error, Error}
end.

Eu recomendo usar um simple_one_for_onetipo de criança aqui. Além disso, certifique-se de que algo gere alguns filhos após o supervisor iniciar, para que haja processos realmente ouvindo a porta de rede.
A parte importante aqui é a opção:{packet, 2}

Os pacotes consistem em um cabeçalho que especifica o número de bytes no pacote, seguido por esse número de bytes. O comprimento do cabeçalho pode ser um, dois ou quatro bytes; contendo um inteiro sem sinal na ordem de bytes big-endian. Cada operação de envio gerará o cabeçalho, e o cabeçalho será retirado em cada operação de recebimento.

(veja: inet: setopts / 2 )
Isso é exatamente o que queremos. Observe a ordem de bytes big-endian, teremos que lidar com isso no código C # mais tarde.

Servidor TCP

Em segundo lugar, vem o processo do servidor TCP. Isso deve usar o gen_servercomportamento.

-record(state, {socket=nil}).

init
(Socket) ->
gen_server
:cast(self(), accept),
{ok, #state{socket=Socket}}.

handle_cast
(accept, S = #state{socket=ListenSocket}) ->
{ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
ok
= inet:setopts(AcceptSocket, [{active, once}]),
{noreply, S#state{socket=AcceptSocket};
handle_cast
({tcp, Socket, Data}, S = #state{socket=Socket}) ->
Reply = some_module:calculate_reply(Data),
gen_tcp
:send(Socket, Reply),
inet
:setopts(Socket, {active, once}),
{noreply, S}.

handle_info
({tcp, Socket, Data}, State) ->
gen_server
:cast(self(), {tcp, Socket, Data}),
{noreply, State};
handle_info
({tcp_closed, _Socket}, State) ->
{stop, tcp_closed, State}.

É isso. Vamos ver o que está acontecendo aqui. Em e , basicamente redirecionamos as mensagens para , para fazer as coisas da maneira gen_server. Com ativamos o socket para receber uma mensagem.init/1handle_info/2handle_cast/2
inet:setopts/2

Cliente

O cliente C # tem duas classes. O NetworkController (que é um MonoBehaviour) e a Message, que é uma classe simples.
Dê uma olhada no NetworkController.

public class NetworkController : MonoBehaviour {

void Awake() {
DontDestroyOnLoad(this);
}

// Use this for initialization
void Start() {
startServer
();
}

// Update is called once per frame
void Update() {
processMessage
();
}

static TcpClient client = null;
static BinaryReader reader = null;
static BinaryWriter writer = null;
static Thread networkThread = null;
private static Queue<Message> messageQueue = new Queue<Message>();

static void addItemToQueue(Message item) {
lock(messageQueue) {
messageQueue
.Enqueue(item);
}
}

static Message getItemFromQueue() {
lock(messageQueue) {
if (messageQueue.Count > 0) {
return messageQueue.Dequeue();
} else {
return null;
}
}
}

static void processMessage() {
Message msg = getItemFromQueue();
if (msg != null) {
// do some processing here, like update the player state
}
}

static void startServer() {
if (networkThread == null) {
connect
();
networkThread
= new Thread(() => {
while (reader != null) {
Message msg = Message.ReadFromStream(reader);
addItemToQueue
(msg);
}
lock(networkThread) {
networkThread
= null;
}
});
networkThread
.Start();
}
}

static void connect() {
if (client == null) {
string server = "localhost";
int port = 12345;
client
= new TcpClient(server, port);
Stream stream = client.GetStream();
reader
= new BinaryReader(stream);
writer
= new BinaryWriter(stream);
}
}

public static void send(Message msg) {
msg
.WriteToStream(writer);
writer
.Flush();
}
}

Esta classe faz várias coisas.

  • O método garante que essa classe não seja destruída.Awake()
  • O método inicia a conexão TCP e define o fluxo de leitura e gravação. Como o protocolo usa dados binários (comprimento do quadro), é importante usar e aqui.connect()BinaryReaderBinaryWriter
  • O método inicia um thread em segundo plano, que lê as mensagens e as coloca em uma fila de maneira segura para thread ( ).startServer()addItemToQueue()
  • Em cada renderização de quadro, o método chama , o que retira da fila uma mensagem por vez (isso também é seguro para thread). Com esta técnica é possível usar threads de fundo reais no Unity.Update()processMessage()
  • O método público envia uma mensagem. Isso provavelmente será chamado por outro controlador.send()

E para colocar todas as peças juntas, aqui vem a classe Message:

public class Message {
public ushort length { get; set; }
public byte[] content { get; set; }

public static Message ReadFromStream(BinaryReader reader) {
ushort len;
byte[] len_buf;
byte[] buffer;

len_buf
= reader.ReadBytes(2);
if (BitConverter.IsLittleEndian) {
Array.Reverse(len_buf);
}
len
= BitConverter.ToUInt16(len_buf, 0);

buffer
= reader.ReadBytes(len);

return new Message(buffer);
}

public void WriteToStream(BinaryWriter writer) {
byte[] len_bytes = BitConverter.GetBytes(length);

if (BitConverter.IsLittleEndian) {
Array.Reverse(len_bytes);
}
writer
.Write(len_bytes);

writer
.Write(content);
}

public Message(byte[] data) {
content
= data;
}
}

Nada complicado está acontecendo aqui. Há um método estático que pode criar uma instância lendo a partir do fio e há um método que grava o conteúdo da mensagem no fluxo de saída. A única coisa a notar aqui é a conversão endian. Pode haver uma maneira melhor de fazer isso em C #, mas esta foi a primeira que encontrei e faz o trabalho.WriteToStream()

Espero que você goste desse post. Sinta-se a vontade para deixar comentários.