Jogo de pato: Guia Netcode para modders

Entendendo isso, como o netcode funciona tornará suas armas / itens compatíveis com multijogador sem ter que fazer um monte de testes, e se você não tem amigos ou dois computadores, pode economizar horas de depuração desnecessária e situações desesperadoras.
P.s este guia é uma tradução. Original: https://steamcommunity.com/sharedfiles/filedetails/?id=1394086869

 

Conexões e outros

Primeiro, quero explicar brevemente, como funciona o netcode (código de rede), e nas versões mais recentes é bastante complicado e usa "fantasmas" (fantasmas) para sincronização de dados, não requer uso intensivo do processador. Mas isso realmente não importa para nós, uma vez que faz parte do backend ("Código de rede interno"), o que não afeta seu código.

Partes mais relevantes do código de rede.

Netcode consiste em conexões com outros patos, e você pode enviar NetMessage (mensagens de rede) ou conexões específicas, ou todas as conexões (normalmente apenas passando null como a conexão).

Cada coisa no nível pertence a alguma conexão, e o proprietário desta conexão enviará propriedades sobre a coisa específica para todos os outros; então o host / servidor não está no controle de tudo, que eu acho que é um equívoco bastante comum. No início do nível, o host controla tudo, além de outros patos.

Vou explicar isso com mais detalhes nessas partes, onde você realmente os usa.

Statebinding

StateBinding (algo como um link de estado) informar backend, quais variáveis ​​sincronizar com outras conexões.

public int myNumber = 25;
public StateBinding _number = new StaeBinding("myNumber",-1,false,false);
//это гарантирует автоматическую синхронизацию int myNumber.

Existem várias sobrecargas para StateBinding, a maioria dos quais nunca será usado.

Principal, qual você, provavelmente, use para quase tudo (e que foi usado no primeiro exemplo):

public StateBinding(string field, int bits = -1, bool rot = false, bool vel = false)

 

Valor de bits – este é o tamanho do campo, ou seja, Int32 consiste em 32 página. Se o valor for -1, o tamanho será recebido automaticamente. E economizar alguns nanossegundos em cada correspondência não vale o risco de cometer um erro no valor. Então deixe ligado -1. Você pode ver mais claramente, como funciona na classe BitBuffer, usando algum descompilador (por exemplo, DnsPy).

Os valores booleanos rot e vel são usados ​​apenas se, se você está lidando com rotações e velocidades, e, Como eu entendo, eles são usados ​​apenas para cálculos de física. Não mude seus valores para verdadeiros, se você não sabe, o que você está fazendo.

Aqui estão outras sobrecargas, que você pode usar (E o que eles fazem):

//изменяет значение постепенно вместо мгновенных скачков, работает только с числами с плавающей запятой.
public StateBinding(bool doLerp, string field, int bits = -1, bool rot = false, bool vel = false);
//наиболее специфичный конструктор statebind, работает как обычный + вы можете сделать его "плавным" и установить его важность (низкий, средний или высокий уровень). Использование данного конструктора может замедлить работу других вещей, поэтому вам не следует его использовать.
public StateBinding(GhostPriority p, string field, int bits = -1, bool rot = false, bool vel = false, bool doLerp = false)

Tipos, ao qual o statebind pode ser aplicado:]
  • fragmento
  • flutuador
  • Duplo
  • byte
  • sbyte
  • bool
  • baixo
  • ushort
  • int
  • uint
  • longo
  • Ulong
  • Caracteres
  • Vec2
  • BitBuffer
  • NetIndex16
  • NetIndex2
  • NetIndex4dex4
  • NetIndex8
  • Tudo, o que herda ou é coisa

StateBind também trabalhará com propriedades. Um exemplo é que, que é usado no MegaLaser:

public StateBinding _frameBinding = new StateBinding("spriteFrame", -1, false, false);
public byte spriteFrame {
      get {
        if (this._chargeAnim == null)
          return (byte) 0;
        return (byte) this._chargeAnim._frame;
      }

      set {
        if (this._chargeAnim == null)
          return;
        this._chargeAnim._frame = (int) value;
      }
    }
NetSoundBinding

NetSoundEffects são como sons regulares, e também podem acionar funções ao reproduzir áudio. Além disso, sua altura pode depender de um valor diferente.. Mas o mais importante, quando usados ​​com o NetSoundBinding, eles serão sincronizados online.

NetSoundEffects e NetSoundBinding são usados, por exemplo, quando o pato grasna ou quando você bate no tambor.

Você pode usar o NetSoundEffect de duas maneiras:

public StateBinding _netBounceSoundBinding = (StateBinding) new NetSoundBinding("_bounceSound "); //Statebinding для _bounceSound
public StateBinding _netYellSoundBinding = (StateBinding) new NetSoundBinding("_netYellSound "); //StateBinding для _netYellSound

public NetSoundEffect _bounceSound = new NetSoundEffect(); //Пустой SoundEffect, который будет привязан к функции.

public NetSoundEffect _netYellSound = new NetSoundEffect(new string[3]
    {
      "quackYell01",
      "quackYell02",
      "quackYell03"
    }); //Создание нового NetSoundEffect, который, будучи единожды воспроизведён, запустит три новых звука, и его синхронизация, чтобы остальные игроки услышали новый звук.
public override void Initialize()
{
    _bounceSound.function = new NetSoundEffect.Function(Bounce);
}

void Bounce()
{
    SFX.Play(GetPath("myBounceSound.wav"));
    shake = 5f;
}

Certificar-se de que, qual é o tipo da variável – StateBinding, à не NetSoundBinding

StateBinding (continuação)

public StateFlagBinding(string params[] Campos)

StateFlagBinding é muito útil, quando você está lidando com vários booleanos, por exemplo, se algo está aberto, fechado ou bloqueado.

É apenas uma maneira mais rápida de fazer várias ligações de estado lógico, que está muito desatualizado.

Os parâmetros devem ser bool para usar, e então seus valores serão compactados em um ushort. Usar ushort significa, que você não pode ter mais 16 campos lógicos.

public StateBinding _laserStateBinding = (StateBinding) new StateFlagBinding(new string[3]
{
"_charging",
"_fired",
"doBlast"
});
public bool doBlast;
public bool _fired;
public bool _charging;

Exemplo retirado de HugeLaser. Na realidade, campos _charging e_fired não são usados ​​nele, e depende do quadro do sprite e do índice de animação, que, por sua vez, sincronizado. E esta é a melhor forma de ligar, caso contrário, haveria um atraso entre todos os clientes. além disso, na escrita de código adequado, isso funciona com StateBindigs (partes).

Tipos de StateBindigs, que você não precisa saber, uma vez que são usados ​​apenas para otimização:

Ligação de dados(campo de string) – Funciona com mais eficiência com BitBuffers, e só com eles.
CompressedVec2Binding – Reduz o tamanho Vec2. Todos os construtores vêm com valores máximos.
CompressedFloatBinding – Diminui o tamanho do flutuador. Todos os construtores vêm com valores máximos.
InterpolatedVec2Binding[/b] – Extensão CompressedFloatBinding de maior prioridade.

[h]Como escrever o código correto, que funciona com StateBinds.[/h]
Isso é especialmente importante se você estiver lidando com coisas como temporizadores ou valores que mudam rapidamente. Basicamente, o que você precisa fazer é escrever suas coisas como se elas pudessem pular um monte de números aleatoriamente e ainda funcionar no final.

Isso é especialmente importante, se você está lidando com temporizadores ou valores, que mudam rapidamente. Essencialmente, você precisa escrever o código assim, como se pudesse acidentalmente pular alguns números e ainda funcionar

Aqui está um exemplo: Você tem um temporizador de flutuação, o que diminui seu valor a cada quadro, e assim que chegar a zero, Algo está acontecendo.
Aqui, como não vale a pena:

cronômetro–;
E se(temporizador == 0) Fazer coisas();

Isso funciona muito bem em jogos locais; mas online, cada quadro só pode enviar uma quantidade limitada de dados, e muito provavelmente o valor do cronômetro diminuirá a cada três ou mais quadros. Nesse caso, o cronômetro diminuirá assim(assumindo, com o que isso começa 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
Como você vê, 0 foi pulado e doStuff() nunca começou.

Aqui, Como corrigi-lo:

cronômetro–;
E se(cronômetro <= 0) Fazer coisas();

Facilmente, não é? E vai realmente funcionar!
Foi por isso que houve um bug com um carregamento infinito do mapa da Internet. Londres (desenvolvedor) usado ==, e quando o número necessário foi pulado devido às curvas dos mapas, foi mais longe no infinito. Felizmente, foi corrigido em beta.

Apesar, esta é apenas metade do quebra-cabeça, pois é igualmente importante escolher os valores corretos para a sincronização. Além dos campos _charging e_fired, Laser enorme – este é um ótimo exemplo de, como os cronômetros devem ser.

Brevemente, então aqui, o que está acontecendo lá: 2 StateBinding para animações, usando índice de animação e quadro de sprite. Isso pode ser usado para sincronizar animações.

public StateFlagBinding(string params[] Campos)

StateFlagBinding é muito útil, quando você está lidando com vários booleanos, por exemplo, se algo está aberto, fechado ou bloqueado.

É apenas uma maneira mais rápida de fazer várias ligações de estado lógico, que está muito desatualizado.

Os parâmetros devem ser bool para usar, e então seus valores serão compactados em um ushort. Usar ushort significa, que você não pode ter mais 16 campos lógicos.

public StateBinding _laserStateBinding = (StateBinding) new StateFlagBinding(new string[3]
{
"_charging",
"_fired",
"doBlast"
});
public bool doBlast;
public bool _fired;
public bool _charging;

Exemplo retirado de HugeLaser. Na realidade, campos _charging e_fired não são usados ​​nele, e depende do quadro do sprite e do índice de animação, que, por sua vez, sincronizado. E esta é a melhor forma de ligar, caso contrário, haveria um atraso entre todos os clientes. além disso, na escrita de código adequado, isso funciona com StateBindigs (partes).

Tipos de StateBindigs, que você não precisa saber, uma vez que são usados ​​apenas para otimização:

Ligação de dados(campo de string) – Funciona com mais eficiência com BitBuffers, e só com eles.
CompressedVec2Binding – Reduz o tamanho Vec2. Todos os construtores vêm com valores máximos.
CompressedFloatBinding – Diminui o tamanho do flutuador. Todos os construtores vêm com valores máximos.
InterpolatedVec2Binding[/b] – Extensão CompressedFloatBinding de maior prioridade.

[h]Como escrever o código correto, que funciona com StateBinds.[/h]
Isso é especialmente importante se você estiver lidando com coisas como temporizadores ou valores que mudam rapidamente. Basicamente, o que você precisa fazer é escrever suas coisas como se elas pudessem pular um monte de números aleatoriamente e ainda funcionar no final.

Isso é especialmente importante, se você está lidando com temporizadores ou valores, que mudam rapidamente. Essencialmente, você precisa escrever o código assim, como se pudesse acidentalmente pular alguns números e ainda funcionar

Aqui está um exemplo: Você tem um temporizador de flutuação, o que diminui seu valor a cada quadro, e assim que chegar a zero, Algo está acontecendo.
Aqui, como não vale a pena:

cronômetro–;
E se(temporizador == 0) Fazer coisas();

Isso funciona muito bem em jogos locais; mas online, cada quadro só pode enviar uma quantidade limitada de dados, e muito provavelmente o valor do cronômetro diminuirá a cada três ou mais quadros. Nesse caso, o cronômetro diminuirá assim(assumindo, com o que isso começa 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
Como você vê, 0 foi pulado e doStuff() nunca começou.

Aqui, Como corrigi-lo:

cronômetro–;
E se(cronômetro <= 0) Fazer coisas();

Facilmente, não é? E vai realmente funcionar!
Foi por isso que houve um bug com um carregamento infinito do mapa da Internet. Londres (desenvolvedor) usado ==, e quando o número necessário foi pulado devido às curvas dos mapas, foi mais longe no infinito. Felizmente, isso foi corrigido na versão beta, embora, esta é apenas metade do quebra-cabeça, pois é igualmente importante escolher os valores corretos para a sincronização. Além dos campos _charging e_fired, Laser enorme – este é um ótimo exemplo de, como os temporizadores devem ser., então aqui, o que está acontecendo lá: 2 StateBinding para animações, usando índice de animação e quadro de sprite. Isso pode ser usado para sincronizar animações e também há um campo bool doBlast., o que se torna verdade, quando o dono disparou a arma. E, se doBlast for verdadeiro (porque é um StateBinding) e você não é o dono, há um tiro.
Seguem as partes mais importantes., que não permitem um disparo duplo e desligamento instantâneo doBlast, para que outros jogadores sempre tenham jogos atualizados

Propriedades da classe NetWork e muito mais

Network.isServer

– o computador atual é um host.

Network.isClient

– o computador atual é um host.

Network.isActive

– a rede está ativa?.

Ao criar AmmoType, adequado para multijogador, deve ter um construtor vazio (sem parâmetros).
É ele quem é chamado, tão, se você tem outro construtor, não vai funcionar de jeito nenhum e vai travar ao descompactar a mensagem.

this.Fire() sincronizado online.
Na realidade, geração de bala sincronizada, mas se sua arma atira com objetos comuns, você não precisa tomar medidas adicionais, Para ter a certeza, que tudo está em sincronia.

OnHoldAction está sincronizando, e OnReleaseAction não é.
Isso significa, que você pode usar OnHoldAction para cobrar coisas (como isso é feito com o Phaser). Mas você vai precisar de alguma maneira de ter certeza, que OnReleaseAction está sincronizado (por exemplo, usando this.Fire, como em Phaser, ou usando StateBinding).

Conexões e como gerar coisas

Como mencionado anteriormente: Tudo (Coisa) há um campo de conexão, e todos os valores de StateBidning serão sincronizados com os valores dessas conexões. Não com host / servidor. Ainda não é uma opinião, no início de cada nível, o host controlará tudo, além de outros patos.
Para coisas assim, como Holdables, existe otimização – assim que forem apanhados, eles mudam a conexão com os patos. Mas se você, por exemplo, crie alguma coisa nova, que faz alguma coisa, você precisará realizar algumas etapas adicionais., O que você precisa fazer, para gerar a caixa, se o seu mod é para jogos locais:
Level.Add(nova caixa(500,500));
Se o seu mod é para multijogador:
if(isServerForObject)
  Level.Add(new Crate(500,500));

sim, você precisa de uma condição adicional, E é por causa disso:

isServerForObject

Uma vez que cada cliente tem seu próprio código de arma rodando, заспавнится 2 caixas; um localmente, e a segunda do dono do objeto.
verificações de isServerForObject, se o computador atual possui o objeto, de modo que o objeto desova apenas uma vez.

Na realidade, você pode checar, é o pato local, usando isServerForObject.

Parabéns! Você fez isso.

Na realidade, tem mais uma coisa, sobre o qual você deve saber, e este é o Fondleing. Na verdade, permite que você assuma o controle da coisa e estabeleça uma conexão.

public void Fondle(Thing t)
    {
      if (t == null || !Network.isActive || (!this.isServerForObject || !t.CanBeControlled()) || (this.connection != DuckNetwork.localConnection || t.connection == this.connection))
        return;
      t.connection = this.connection;
      ++t.authority;
}

Isso é usado para manter tudo em sincronia, que você não pega, como, por exemplo, RCCar, para controlar qual você seleciona apenas o controlador. Lembrar, aquele Fondle existe. Ele vai salvar sua vida, quando você tenta fazer algo complicado e não funciona online.

Se você quiser ver um exemplo, olhe para o código da classe RCController e sua atualização. Isso também é usado em caixas eletrônicos..

Bitbuffer’ы

BitBuffers contêm dados, que você pode enviar para outros. Se você for fazer um NetMessage mais complexo, é importante saber, como eles funcionam.

Na verdade, BitBuffers contêm dados e permitem que você os leia / grave como tipos diferentes. BitBuffer tem uma posição, significa que, de onde você está lendo dados atualmente. E conforme os valores são lidos, a posição aumentará. Desta maneira, você não precisa lidar com variáveis, mas apenas com a ordem daquele, como eles foram colocados.

Escrevendo valores para BitBuffer.

BitBuffer myData = novo BitBuffer();
myData.Write((byte)25);
myData.Write((int)50);
myData.Write(verdadeiro);
myData.Write(novo Vec2(25,25));

Lendo valores do mesmo BitBuffer.

BitBuffer sameData = novo BitBuffer(myData.buffer);
byte mybyte = sameData.ReadByte();
int myInt = sameData.ReadInt();
bool myBool = sameData.ReadBool();
Vec2 myVec = sameData.ReadVec2();

Muito importante, para que a ordem de leitura e escrita seja a mesma, caso contrário não vai funcionar.

Aqui está um exemplo, como posso escrever um array para BitBuffer, Em ocasião, если это поможет вам понять его Pouco Melhor.

int[] myArray = new int[] { 5,125,123123,67,2,324 };
BitBuffer myData = novo BitBuffer();myData.Write(myArray.Length); //tamanho da matriz
para cada (var num in myArray)
myData.Write(num); //escrever cada numberBitBuffer sameData = new BitBuffer(myData.buffer);int arraySize = sameData.ReadInt(); //lendo o primeiro int (Tamanho)
int[] theArray = new int[arraySize];para (int i = 0; eu < arraySize; i ++)
theArray = sameData.ReadInt(); //lendo cada número e colocando-o em uma matriz[/citar]

Limite de BitBuffer.

Cada BitBuffer armazena seu tamanho em ushost, que é um número positivo de 0 para 2?? ou 65535. E uma vez que cada byte consiste em 8 página, BitBuffer pode conter até 8kb de dados.

 

Mensagens personalizadas da rede

Sincronize coisas com StateBinding e certifique-se, que eles têm os proprietários certos nem sempre é suficiente para funcionar direito.

Como fazer suas mensagens de rede funcionarem

DuckGame carrega todos os NetMessages antes de carregar os mods, então seus tipos de NetMessage não serão adicionados à lista de netmessage.

Para combater isso, devemos adicionar nosso MessageType's.

public static void UpdateNetmessageTypes()
public static void UpdateNetmessageTypes() {
IEnumerable subclasses = Editor.GetSubclasses(typeof(NetMessage));
    Network.typeToMessageID.Clear();
    ushort key = 1;
    foreach (System.Type type in subclasses)
    {
        if (type.GetCustomAttributes(typeof(FixedNetworkID), false).Length != 0) {
            FixedNetworkID customAttribute = (FixedNetworkID)type.GetCustomAttributes(typeof(FixedNetworkID), false)[0];
            if (customAttribute != null)
                Network.typeToMessageID.Add(type, customAttribute.FixedID);
        }
    }
    foreach (System.Type type in subclasses) {
        if (!Network.typeToMessageID.ContainsValue(type)) {
            while (Network.typeToMessageID.ContainsKey(key))
                ++key;
            Network.typeToMessageID.Add(type, key);
            ++key;
        }
    }
}

public override void OnPostInitialize() {
    UpdateNetmessageTypes();
}

public override void OnPostInitialize() {
    UpdateNetmessageTypes();
}

Voila, agora você pode usar o NetMessage em seu mod.

Na verdade, todo o NetMessage, qual você vai usar, estes são eventos (eventos).
Você também pode usar o NetMessage normal(NMDuckNetwork), mas apenas se você enviar dados para outras pessoas, como chapéus personalizados.

Seu hipotético NetMessage funciona mais ou menos assim:
A substituição OnSerilize preenche o bitbuffer serializedData com dados de cada campo em sua classe (em oposição a StateBidning, propriedades não funcionam), então eles são encaminhados por interwebs(interwebs), onde, após o recebimento, eles passam pela substituição OnDeserilize, onde ele coloca todos os valores do bitbuffer nos campos corretos.

Para todas as mensagens do Net você precisa de um construtor vazio, Porque, quando você receber a mensagem, um novo objeto deste tipo será criado, usando um construtor vazio. Se você não tem, você será esmagado (não é um jogador de anime).

Criação de NetMessage.

Aqui, como é um NetMessage simples:

class NMTimeLimit : NMDuckNetworkEvent
    {
        public string message;
        public NMTimeLimit()
        {

        }

        public NMTimeLimit(string msg)
        {
            message = msg;
        }

        public override void Activate()
        {          
            base.Activate();
            HUD.AddInputChangeDisplay(message);
        }

    }

Aqui está uma lista de todos os tipos de dados, que será serializado e desserializado automaticamente(muito importante, para tornar os campos públicos! caso contrário, eles não serão serializados):

  • fragmento
  • flutuador
  • Duplo
  • byte
  • sbyte
  • bool
  • baixo
  • ushort
  • int
  • uint
  • longo
  • Ulong
  • Caracteres
  • Vec2
  • Tudo, o que herda ou é coisa

Ao aplicar isso a patos: usar byte para netIndex.

duck.profile.networkIndex

e então encontrar um pato, usando

DuckNetwork.profiles[(int) índice].Pato;

Desta maneira, todas as suas mensagens, relacionado ao pato, será enviado muito mais rápido, e as chances de fora de sincronia serão bastante reduzidas.

Serializando dados personalizados.

E o que, se você quer algum tipo de array? Uma vez que não é automaticamente compatível, teremos que mudar os overrides OnSerilize e OnDeserilize.

int[] myArray = new int[] { 25,30,29};
protected override void OnSerialize()
{
      base.OnSerialize(); //это то, что автоматически сериализирует поля, так что не убирайте это.

      serializedData.Write(myArray.Length);
      foreach (var num in myArray)
             serializedData.Write(num); //то же самое, что и в примере с BitBuffer'ом

public override void OnDeserialize(BitBuffer msg)
{
      base.OnDeserialize(msg); //автоматически десериализирует поля, не убирайте это.
      //так как при сериализации base.OnSerilize выполнялся первым, base.OnDeserilize тоже должен выполняться первым.

      myArray = new int[msg.ReadInt()];
      for (int i = 0; i < myArray.Length; i++)
          myArray[] = msg.ReadInt();
}
//по сути, вам нужно выяснить, как превратить что-либо в BitBuffer
Enviando NetMessages em tamanho > 8kb

Uma vez que tudo no duckgame é enviado com BitBuffers, o tamanho máximo de uma mensagem do Netmessage será 8 KB menos o tamanho do cabeçalho (Cabeçalho (cabeçalho) contém informações sobre NetMessage, para que possa ser transformado em um NetMessage de heap de bytes novamente).

Caminho, pelo qual DuckGame resolve este problema, é usar NMLevelDataHeader, vários NMLevelDataChunks, que são fragmentos do nível, que pode ser colocado junto, e depois de recebê-los, ele irá enviar uma mensagem NMLevelDataReady, confirmar, que a transferência está completa.

Mesmo que funcione, eu não recomendaria copiar o código, pois só funciona ao fazer uma transferência ao mesmo tempo com um cliente ou todos os clientes ao mesmo tempo. Eu usei um post, contendo sessão e bool encerrado (sessão e um bool acabado), e então eu tive um gerenciador de dados, quem descobriu, quando uma nova sessão foi aberta, e quando acabou, usando o último bool. Antes, como DuckGame enviará suas NetMessages, classifica-os por tamanho, e, portanto, todas as mensagens, exceto o último, deve ser do mesmo tamanho, e o último deve ser menor, se você decidir apenas adicionar os dados recebidos em uma matriz ao receber.

Em vez de classificar a matriz, quando eu enviei, eu decidi classificá-lo mais cedo, quando todos os dados foram recebidos. Agora, quando eu penso sobre isso, Não tenho certeza, qual caminho era melhor, mas isso realmente não importa. Qualquer maneira, aqui está o código, mostrando, Como eu posso fazer isso (do mod do autor, Reskins), usando a abordagem transfermanager: DataTransferSession[github.com], DataTransferSession[github.com] e NMDataSlice[github.com]

Como sou um bom programador e escrevi um código flexível, você pode copiar arquivos e usar a função DataTransferManager.SendLotsOfData e conectar algo ao evento DataTransferManager.onMessageCompleted, e tudo deve estar bem.

NetDebug

Se você ainda precisa protestar contra algo, mas peça constantemente aos seus amigos para baixarem o seu mod e irem para o seu lobby – não é uma opção, especialmente para você, há um parâmetro de inicialização -netdebug.

Para ativá-lo, clique com o botão direito no jogo na biblioteca:

No painel que é aberto, insira o parâmetro de inicialização no campo apropriado:

Para ativá-lo, clique com o botão direito no jogo na biblioteca:

A partir de Polanas

Seja o primeiro a comentar

Deixe uma resposta

O seu endereço de email não será publicado.


*