Hoje, quando o Node.js está em pleno andamento, já podemos usá -lo para fazer todos os tipos de coisas. Algum tempo atrás, o apresentador da UP participou do evento Geek Song. Durante este evento, pretendemos criar um jogo que permita que as "pessoas de cabeça para baixo" se comuniquem mais. A função principal é a interação em várias pessoas em tempo real do conceito de partido LAN. A corrida dos geeks foi apenas um pouco de 36 horas, exigindo que tudo seja rápido e rápido. Sob essa premissa, a preparação inicial parece um pouco "natural". Solução de aplicativos de plataforma cruzada, escolhemos o Node-Webkit, que é simples o suficiente e atende aos nossos requisitos.
De acordo com os requisitos, nosso desenvolvimento pode ser realizado separadamente de acordo com os módulos. Este artigo descreve especificamente o processo de desenvolvimento da espaçadora (nossa estrutura de jogos multiplayer em tempo real), incluindo uma série de explorações e tentativas, além de soluções para algumas restrições no próprio Node.js e na plataforma webkit e na proposta de soluções.
Começando
Olhar para a espaçadora
No começo, o design da espaçadora era definitivamente orientado para a demanda. Esperamos que essa estrutura possa fornecer as seguintes funções básicas:
Pode distinguir um grupo de usuários com base em salas (ou canais)
Capaz de receber instruções de usuários no grupo de coleta
Ao combinar entre cada cliente, os dados do jogo podem ser transmitidos com precisão de acordo com o intervalo especificado
Pode eliminar o impacto da latência da rede o máximo possível
Obviamente, nos estágios posteriores da codificação, fornecemos à espaçoseoom mais funções, incluindo a pausa do jogo, gerando números aleatórios consistentes entre cada cliente, etc. (é claro, estes podem ser implementados por eles mesmos de acordo com os requisitos e não é necessário usar a espaçonave, uma estrutura que funciona no nível de comunicação, que é mais do nível de comunicação).
APIs
A espaçadora é dividida em duas partes: front e traseira. O trabalho exigido pelo servidor inclui a manutenção de uma lista de quartos e o fornecimento das funções de criação e ingresso em uma sala. Nossas APIs de clientes se parecem com a seguinte:
Spaceroom.connect (endereço, retorno de chamada) Conecte -se ao servidor
Spaceroom.Createrom (retorno de chamada) Crie uma sala
Spaceroom.Joinroom (Roomid) Participe de uma sala
Spaceroom.on (Evento, retorno de chamada) ouve eventos
...
Depois que o cliente se conecta ao servidor, ele recebe vários eventos. Por exemplo, um usuário em uma sala pode receber um evento em que um novo jogador se junta ou um evento em que o jogo começa. Demos ao cliente um "ciclo de vida", que estará em um dos seguintes estados a qualquer momento:
Você pode obter o status atual do cliente através da espaçadora.State.
O uso de uma estrutura do lado do servidor é relativamente simples. Se você usar o arquivo de configuração padrão, poderá executar a estrutura do lado do servidor diretamente. Temos um requisito básico: o código do servidor pode ser executado diretamente no cliente sem a necessidade de um servidor separado. Jogadores que jogaram PS ou PSP devem saber do que estou falando. Obviamente, também é excelente executar em um servidor especial.
A implementação do código lógico é breve aqui. A primeira geração de espaçadoras completou a função de um servidor de soquete, que mantém uma lista de salas, incluindo o status da sala e as comunicações do tempo de jogo correspondentes a cada sala (coleta de instruções, transmissão de balde etc.). Para implementação específica, consulte o código -fonte.
Algoritmo síncrono
Então, como podemos fazer as coisas exibidas entre cada cliente consistente em tempo real?
Essa coisa parece interessante. Pense nisso com cuidado, o que precisamos do servidor para nos ajudar a entregar? Naturalmente, você pensará no que pode causar inconsistências lógicas entre vários clientes: instruções do usuário. Como os códigos que lidam com a lógica do jogo são os mesmos, dadas as mesmas condições, o código executa o mesmo resultado. A única diferença são os vários comandos de jogadores recebidos durante o jogo. Certo, precisamos de uma maneira de sincronizar essas instruções. Se todos os clientes puderem obter a mesma instrução, todos os clientes poderão teoricamente o mesmo resultado de operação.
Os algoritmos de sincronização dos jogos on -line são todos os tipos de cenários estranhos e aplicáveis. O espaçador usa algoritmos de sincronização semelhantes ao conceito de bloqueio de quadros. Dividimos a linha do tempo em intervalos um por um, e cada intervalo é chamado de balde. O balde é usado para carregar instruções e é mantido pelo lado do servidor. No final de cada período de bucket, o servidor transmite o balde para todos os clientes. Depois que o cliente recebe o balde, as instruções são recuperadas e executadas após a verificação.
Para reduzir o impacto do atraso da rede, cada instrução recebida pelo servidor do cliente será entregue no balde correspondente de acordo com um determinado algoritmo, e as etapas a seguir são seguidas especificamente:
Seja Order_start a hora do comando transportado pelo comando, e T é o tempo de início do balde em que o Order_start está localizado.
Se t + telto_time <= hora de início do balde que atualmente está coletando as instruções, entregue o comando ao balde que está atualmente coletando as instruções; caso contrário, continue a Etapa 3
Entregue a instrução para o balde correspondente de T + Atraso_time
Onde o atraso_time é o tempo de atraso do servidor acordado, que pode ser considerado como o atraso médio entre os clientes. O valor padrão na espaçadora é 80 e o valor padrão do comprimento do balde é 48. No final de cada período de tempo, o servidor transmite esse bucket para todos os clientes e começa a receber instruções para o próximo balde. O cliente controla o erro de tempo dentro de um intervalo aceitável ao executar automaticamente a correspondência na lógica com base no intervalo de balde recebido.
Isso significa que, em circunstâncias normais, o cliente receberá um balde enviado do servidor a cada 48ms. Quando o tempo em que o balde precisa ser processado, o cliente lidará com ele de acordo. Supondo que o cliente FPS = 60, ele receberá um balde a cada 3 quadros ou mais, e a lógica será atualizada de acordo com este balde. Se o balde não tiver sido recebido após o tempo expirado devido a flutuações de rede, o cliente faz uma pausa na lógica do jogo e espera. Em um momento dentro de um balde, a lógica pode ser atualizada usando o método Lerp.
No caso de atraso_time = 80, bucket_size = 48, qualquer uma das instruções será atrasada por pelo menos 96ms de execução. Altere esses dois parâmetros, por exemplo, no caso de atraso_time = 60, bucket_size = 32, qualquer uma das instruções será atrasada em pelo menos 64ms.
Um incidente sangrento causado por um cronômetro
Olhando para a coisa toda, nossa estrutura precisa ter um cronômetro preciso quando estiver em execução. Execute a transmissão do balde em um intervalo fixo. Obviamente, primeiro pensamos em usar o setInterval (), mas no segundo segundo percebemos o quão confiável é essa idéia: o travesso de setInterval () parece ter erros muito graves. E o que é tão ruim é que todos os erros se acumulam, causando consequências cada vez mais graves.
Por isso, pensamos imediatamente em usar o setTimeout () para tornar nossa lógica aproximadamente estável em torno do intervalo especificado corrigindo dinamicamente o tempo da próxima chegada. Por exemplo, desta vez setTimeout () é 5ms a menos do que o esperado, por isso deixaremos 5ms antes do tempo na próxima vez. No entanto, os resultados do teste não são satisfatórios, e isso não é elegante o suficiente, não importa como você o olha.
Então, precisamos mudar nosso pensamento. É possível que o setTimeout () expire o mais rápido possível e, em seguida, verificamos se o horário atual atingiu o tempo de destino. Por exemplo, em nosso loop, usando o setTimeout (retorno de chamada, 1) para continuar a verificação do tempo, o que parece uma boa ideia.
Timer decepcionante
Escrevemos imediatamente um pedaço de código para testar nossas idéias, e os resultados foram decepcionantes. Na versão estável Node.js mais recente atual (v0.10.32) e na plataforma Windows, execute este pedaço de código:
A cópia do código é a seguinte:
var sum = 0, count = 0;
function test () {
var agora = date.now ();
setTimeout (function () {
var diff = date.now () - agora;
soma += diff;
contagem ++;
teste();
});
}
teste();
Após um período de tempo, insira a soma/contagem no console e você poderá ver um resultado, semelhante a:
A cópia do código é a seguinte:
> soma / contagem
15.624555160142348
O que?! Eu quero intervalos de 1ms, mas você me diz que o intervalo médio real é de 15.625ms! Esta foto é simplesmente bonita demais. Fizemos o mesmo teste no Mac e o resultado foi de 1,4ms. Então fomos confusos: o que diabos é isso? Se eu fosse um fã da Apple, poderia ter concluído que o Windows era muito lixo e desistiu do Windows. Felizmente, eu era um engenheiro de front-end rigoroso, então comecei a continuar pensando nesse número.
Espere, por que esse número é tão familiar? Esse número será muito semelhante ao intervalo máximo do timer no Windows? Eu imediatamente baixei um relógio para testes e, depois de executar o console, obtive os seguintes resultados:
A cópia do código é a seguinte:
Intervalo máximo do timer: 15.625 ms
Intervalo de timer mínimo: 0,500 ms
Intervalo do timer atual: 1,001 ms
Com certeza! Olhando para o manual Node.js, podemos ver uma descrição do setTimeout como este:
O atraso real depende de fatores externos como granularidade do temporizador do sistema operacional e carga do sistema.
No entanto, os resultados do teste mostram que esse atraso real é o intervalo máximo do timer (observe que o intervalo atual do timer do sistema é de apenas 1,001ms), o que é inaceitável em qualquer caso. A forte curiosidade nos leva a examinar o código -fonte do Node.js para ter um vislumbre da verdade.
Bug em node.js
Eu acredito que a maioria de vocês e eu temos um certo entendimento do mecanismo de loop uniforme do Node.JS. Observando o código -fonte da implementação do timer, podemos entender aproximadamente o princípio da implementação do timer. Vamos começar com o loop principal do ciclo de eventos:
A cópia do código é a seguinte:
while (r! = 0 && loop-> stop_flag == 0) {
/* Atualize o tempo global*/
uv_update_time (loop);
/* Verifique se o timer expira e execute o retorno de chamada do temporizador correspondente*/
uv_process_timers (loop);
/* Ligue para os retornos de chamada ociosos, se nada para fazer. */
if (loop-> Pending_reqs_tail == null &&
loop-> endgame_handles == null) {
/* Impedir o loop de eventos de sair*/
uv_idle_invoke (loop);
}
uv_process_reqs (loop);
uv_process_endgames (loop);
uv_prepare_invoke (loop);
/* Colete eventos de IO*/
(*Poll) (loop, loop-> idle_handles == null &&
loop-> Pending_reqs_tail == null &&
loop-> endgame_handles == null &&
! loop-> stop_flag &&
(Loop-> Active_handles> 0 ||
! ngx_queue_empty (& loop-> Active_reqs)) &&
! (Modo & uv_run_nowait));
/* setImImediate () etc*/
uv_check_invoke (loop);
r = uv__loop_alive (loop);
if (mode & (uv_run_once | uv_run_nowait))
quebrar;
}
O código -fonte da função uv_update_time é o seguinte: (https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))
A cópia do código é a seguinte:
void uv_update_time (uv_loop_t* loop) {
/* Obtenha o tempo atual do sistema*/
DWord ticks = getTickCount ();
/ * A suposição é feita de que Large_Integer.quadPart tem o mesmo tipo */
/* loop-> tempo, o que é. Existe alguma maneira de afirmar isso? */
LIGLE_INTEGER* TIME = (LAMP_INTEGER*) & LOOP-> TIME;
/* Se o timer foi embrulhado, adicione 1 ao seu DWORD de alta ordem. */
/ * uv_poll deve garantir que o temporizador nunca possa transbordar mais do que */
/* Uma vez entre duas chamadas subsequentes uv_update_time. */
if (ticks <Time-> LowPart) {
Tempo-> HighPart += 1;
}
Tempo-> LowPart = ticks;
}
A implementação interna desta função usa a função getTickCount () do Windows para definir a hora atual. Simplificando, depois de chamar a função Settimeout, após uma série de lutas, o timer interno-> será definido para o tempo de loop atual + tempo limite. No loop de eventos, primeiro atualize o horário do loop atual através do UV_UPDATE_TIME e verifique se o timer expira em uv_process_times. Nesse caso, entre no mundo do JavaScript. Depois de ler o artigo inteiro, o loop do evento é aproximadamente como este processo:
Atualize o horário global
Verifique o temporizador. Se o timer expirar, execute o retorno de chamada.
Verifique a fila Reqs e execute o pedido de espera
Digite a função da pesquisa para coletar eventos de IO. Se um evento de IO chegar, adicione a função de processamento correspondente à fila REQS para execução no próximo loop de evento. Dentro da função da pesquisa, um método do sistema é chamado para coletar eventos de IO. Esse método fará com que o processo bloqueie até que um evento de IO chegue ou o tempo de tempo limite definido seja atingido. Quando esse método é chamado, o tempo de tempo limite é definido no momento em que o timer mais recente expira. Isso significa que os eventos de IO são coletados bloqueando e o tempo máximo de bloqueio é o tempo final do próximo temporizador.
Código fonte de uma das funções da pesquisa no Windows:
A cópia do código é a seguinte:
estático void uv_poll (uv_loop_t* loop, int bloqueio) {
DWORD bytes, tempo limite;
Tecla ulong_ptr;
Sobreposto* sobreposto;
uv_req_t* req;
if (block) {
/* Retire o tempo de validade do temporizador mais recente*/
timeout = uv_get_poll_timeout (loop);
} outro {
tempo limite = 0;
}
GetQueedCompletionStatus (loop-> iocp,
& bytes,
&chave,
e se sobrepunha,
/* Na maioria dos bloqueios até o próximo timer expirar*/
tempo esgotado);
se (sobreposto) {
/ * O pacote foi desqueed */
req = uv_overlap_to_req (sobreposto);
/* Insira eventos de IO na fila*/
uv_insert_pending_req (loop, req);
} else if (getLasterRor ()! = wait_timeout) {
/ * Erro grave */
uv_fatal_error (getLasterRor (), "getQueedCompletionStatus");
}
}
Seguindo as etapas acima, assumindo que definimos um timeout = 1ms Timer, a função da pesquisa bloqueará no máximo 1ms e depois nos recuperará após a recuperação (se não houver eventos de IO durante o período). Ao continuar a entrar no loop de loop de eventos, o uv_update_time atualizará a hora e, em seguida, UV_PROCESS_TIMERS descobrirá que nosso timer expira e executará o retorno de chamada. Portanto, a análise preliminar é que o uv_update_time tem um problema (o horário atual não é atualizado corretamente) ou a função de pesquisa aguarda 1ms e depois se recupera. Há um problema com este 1ms esperando.
Olhando para o MSDN, surpreendentemente descobrimos uma descrição da função GetTickCount:
A resolução da função GetTickCount é limitada à resolução do cronômetro do sistema, que normalmente está na faixa de 10 milhões de segundos a 16 milhões de segundos.
A precisão do GetTickCount é tão difícil! Suponha que a função da pesquisa bloqueie corretamente o tempo de 1ms, mas na próxima vez que UV_UPDATE_TIME for executado, o tempo de loop atual não será atualizado corretamente! Portanto, nosso cronômetro não foi considerado expirado, então a pesquisa esperou por mais 1ms e entrou no próximo ciclo de evento. Até o GetTickCount ser finalmente atualizado corretamente (o chamado 15.625ms é atualizado uma vez), o horário atual do loop é atualizado e nosso temporizador é julgado para expirar em UV_Process_timers.
Peça ajuda a Webkit
Esse código -fonte do Node.js é muito desamparado: ele usou uma função de tempo com baixa precisão e não fez nada. Mas pensamos imediatamente que, como usamos o Node-Webkit, além do Settimeout do Node.js, também temos o Settimeout do Chromium. Escreva um código de teste e use nosso navegador ou nó-webkit para executar: http://marks.lrednnight.com/test.html#1 (# seguido pelo número indica o intervalo a ser medido). O resultado é o seguinte:
De acordo com as especificações do HTML5, o resultado teórico deve ser que os 5 primeiros resultados sejam 1ms e os próximos resultados sejam 4ms. Os resultados exibidos no caso de teste começam desde a terceira vez, o que significa que os dados na tabela devem teoricamente 1ms nas três primeiras vezes, e os resultados posteriormente são 4ms. O resultado tem certos erros e, de acordo com os regulamentos, o menor resultado teórico que podemos obter é 4ms. Embora não estejamos satisfeitos, é obviamente muito mais satisfatório do que o resultado de Node.JS. Forte tendência de curiosidade Vamos dar uma olhada no código -fonte do Chromium para ver como ele é implementado. (https://chromium.googlesource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)
Primeiro, o Chromium usa a função timegetTime () na determinação do tempo atual do loop. Observando o MSDN, você pode descobrir que a precisão desta função é afetada pelo intervalo atual do timer do sistema. Em nossa máquina de teste, é teoricamente os 1,001ms mencionados acima. No entanto, por padrão, o intervalo do timer é seu valor máximo (15.625ms na máquina de teste), a menos que o aplicativo modifique o intervalo do timer global.
Se você seguir as notícias no setor de TI, deve ter visto essa notícia. Parece que nosso cromo definiu o intervalo do timer muito pequeno! Parece que não precisamos nos preocupar com o intervalo do timer do sistema? Não fique muito feliz muito cedo, esse reparo nos deu um golpe. De fato, esse problema foi corrigido no Chrome 38. Devemos usar a fixação do Node-Webkit anterior? Obviamente, isso não é elegante o suficiente e nos impede de usar uma versão mais executada do Chromium.
Olhando mais para o código -fonte do Chromium, podemos descobrir que, quando houver um cronômetro e o tempo limite do tempo limite <32ms, o Chromium alterará o intervalo de timer global do sistema para obter um cronômetro com precisão inferior a 15.625ms. (Veja o código -fonte) Ao iniciar o temporizador, algo chamado HighResolutionTimerManager será ativado. Esta classe chamará a função EnableHighResolutionTimer com base no tipo de energia do dispositivo atual. Especificamente, quando o dispositivo atual usa a bateria, ele chamará o EnableHighResolutionTimer (false) e True será passado ao usar energia. A implementação da função EnableHighResolutionTimer é a seguinte:
A cópia do código é a seguinte:
Time Void :: EnableHighResolutionTimer (BOOL Atable) {
base :: bloqueio automático (g_high_res_lock.get ());
if (g_high_res_timer_enabled == Enable)
retornar;
g_high_res_timer_enabled = enable;
if (! g_high_res_timer_count)
retornar;
// desde g_high_res_timer_count! = 0, um ativatehighresolutiontimer (true)
// foi chamado que chamado timebeginperiod com g_high_res_timer_enabled
// com um valor que é o oposto de | ativar |. Com essa informação nós
// Ligue para o timeendPeriod com o mesmo valor usado no timebeginperiod e
// portanto, desfazer o efeito do período.
if (enable) {
timeendPeriod (kmintimerintervallowResms);
timebeginperiod (KmintimerInterValHighResms);
} outro {
timeendPeriod (KmintimerInterValHighResms);
timebeginPeriod (KmintimerInterVallowResms);
}
}
onde, kmintimerintervallowResms = 4 e kmintimerinterValHighResms = 1. timebeginperiod e timeendperiod são funções fornecidas pelo Windows para modificar o intervalo do temporizador do sistema. Ou seja, ao se conectar à fonte de alimentação, o menor intervalo do timer que podemos obter é de 1ms, enquanto, ao usar a bateria, é 4ms. Como nosso loop chama continuamente o setTimeout, de acordo com a especificação W3C, o intervalo mínimo também é de 4ms, então me sinto aliviado, isso tem pouco impacto sobre nós.
Outro problema de precisão
De volta ao início, descobrimos que os resultados dos testes mostram que o intervalo do setTimeout não é estável a 4ms, mas está flutuando continuamente. Os resultados dos testes http://marks.lrednight.com/test.html#48 também mostram que os intervalos estão batendo entre 48ms e 49ms. O motivo é que, no ciclo de Cromo e Node.js, a precisão da chamada da função do Windows aguardando o evento de IO é afetada pelo cronômetro do sistema atual. A implementação da lógica do jogo exige a função RequestanimationFrame (atualizando constantemente a tela), o que pode nos ajudar a definir o intervalo do timer para pelo menos KmintiminterVallowResms (porque precisa de um cronômetro de 16ms, que desencadeia o requisito de um cronômetro de alta precisão). Quando a máquina de teste usa energia, o intervalo do timer do sistema é de 1ms, portanto o resultado do teste tem um erro de ± 1ms. Se o seu computador não alterou o intervalo do timer do sistema e execute o teste nº 48 acima, o máximo poderá atingir 48+16 = 64ms.
Usando a implementação do SetTimeout do Chromium, podemos controlar o erro do setTimeout (FN, 1) a cerca de 4ms, enquanto o erro do setTimeout (FN, 48) pode controlar o erro do setTimeout (FN, 48) a cerca de 1ms. Então, temos um novo plano em nossas mentes, o que faz nosso código se parecer com o seguinte:
A cópia do código é a seguinte:
/ * Obtenha a decoração do intervalo máximo */
var decoration = getMaxIntervalDeviation (BucketSize); // bucketSsize = 48, desvio = 2;
função gameloop () {
var agora = date.now ();
if (anteriorbucket + bucketsize <= agora) {
anteriorbucket = agora;
dologic ();
}
if (anteriorbucket + bucketsize - date.now ()> decoração) {
// Aguarde 46ms. O atraso real é inferior a 48ms.
setTimeout (gameloop, bucketsize - design);
} outro {
// ocupado esperando. Use setImediate em vez de processo.NextTick porque o primeiro não bloqueia os eventos de IO.
SetImediate (gameloop);
}
}
O código acima nos permite aguardar um tempo com um erro menor que o bucket_size (definição bucket_size) em vez de igualar diretamente um bucket_size. Mesmo que o erro máximo ocorra no atraso de 46ms, de acordo com a teoria acima, o intervalo real é inferior a 48ms. O restante do tempo usamos o método de espera ocupado para garantir que nosso gameloop seja executado sob um intervalo com precisão suficiente.
Enquanto resolvemos o problema em certa medida com o cromo, isso obviamente não é elegante o suficiente.
Lembra do nosso pedido inicial? Nosso código do lado do servidor deve poder ser executado diretamente em um computador com um ambiente Node.js sem o cliente Node-Webkit. Se você executar o código acima diretamente, o valor da definição é de pelo menos 16ms, o que significa que em cada 48ms, precisamos esperar 16ms. A taxa de uso da CPU aumentou.
Surpresa inesperada
É tão irritante. Ninguém percebeu um bug tão grande no Node.js? A resposta realmente nos deixa muito felizes. Este bug foi corrigido na versão V.0.11.3. Você também pode ver os resultados modificados visualizando diretamente a filial principal do código Libuv. A abordagem específica é adicionar um tempo limite ao horário atual do loop após a conclusão da função de pesquisa. Dessa forma, mesmo que o GetTickCount não reagisse, ainda adicionamos esse tempo de espera depois que a pesquisa estava esperando. Para que o timer possa expirar sem problemas.
Em outras palavras, o problema que trabalha duro há muito tempo foi resolvido na v.111.3. No entanto, nossos esforços não foram em vão. Porque mesmo que a função GetTickCount seja eliminada, a própria função da pesquisa será afetada pelo cronômetro do sistema. Uma solução é escrever o plug -in Node.js para alterar os intervalos dos temporizadores do sistema.
No entanto, as configurações iniciais para o nosso jogo desta vez não são livres de servidor. Depois que o cliente cria uma sala, ele se torna um servidor. O código do servidor pode ser executado no ambiente Node-Webkit; portanto, a prioridade dos problemas do timer nos sistemas Windows não é a mais alta. Seguindo a solução que demos acima, os resultados são suficientes para nos satisfazer.
final
Depois de resolver o problema do timer, nossa implementação da estrutura basicamente não será mais prejudicada. Fornecemos suporte do WebSocket (em ambientes HTML5 puros) e personalizamos o protocolo de comunicação para obter suporte de soquete de desempenho mais alto (em ambientes de nó-webkit). Obviamente, as funções da espaçadora foram relativamente simples no início, mas à medida que a demanda foi proposta e o tempo aumentou, estamos gradualmente melhorando essa estrutura.
Por exemplo, quando descobrimos que, quando precisamos gerar números aleatórios consistentes em nosso jogo, adicionamos essa função à espaçadora. No início do jogo, a espaçadora distribuirá sementes de números aleatórios. A espaçadora do cliente fornece um método para usar a aleatoriedade do MD5 para gerar números aleatórios com a ajuda de sementes de números aleatórios.
Parece bastante aliviado. Também aprendi muito no processo de escrever essa estrutura. Se você estiver interessado em espaçador, também pode participar. Eu acredito que a espaçoseom usará os punhos e os pés em mais lugares.