Gioco dell'anatra: Guida Netcode per modder

Capire che, come funziona il netcode renderà le tue armi/oggetti compatibili con il multiplayer senza dover fare un sacco di test, e se non hai amici o due computer, può risparmiare ore di debugging non necessario e situazioni senza speranza.
P.s questa guida è una traduzione. Originale: https://steamcommunity.com/sharedfiles/filedetails/?id=1394086869

 

Connessioni e altro

Per prima cosa, voglio spiegare brevemente briefly, come funziona il codice di rete (codice di rete), e nelle versioni più recenti è piuttosto complicato e usa "fantasmi" (fantasmi) per la sincronizzazione dei dati, non richiede un uso intensivo del processore. Ma per noi non ha molta importanza, poiché fa parte del backend ("Codice di rete interno"), che non influisce sul tuo codice.

Parti più rilevanti del netcode.

Netcode consiste in connessioni con altre anatre, e puoi inviare NetMessage (messaggistica di rete) o connessioni specifiche, o tutte le connessioni (di solito passa solo null come connessione).

Ogni cosa a livello appartiene a qualche connessione, e il proprietario di questa connessione invierà proprietà sulla cosa specifica a tutti gli altri; quindi l'host/server non ha il controllo di tutto, che penso sia un malinteso abbastanza comune. All'inizio del livello, l'host controlla tutto, a parte altre anatre.

Lo spiegherò più dettagliatamente in quelle parti, dove li usi effettivamente?.

vincolante per lo stato

Stato vincolante (qualcosa come un collegamento di stato) informare il backend, quali variabili sincronizzare con altre connessioni.

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

Esistono diversi overload per StateBinding, la maggior parte dei quali non verrà mai utilizzata.

Principale, che tu, più probabilmente, usare per quasi tutto (e che è stato usato nel primo esempio):

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

 

Valore in bit – questa è la dimensione del campo, cioè, Int32 consiste di 32 pagina. Se il valore è -1, la taglia verrà ricevuta automaticamente. E risparmiare un paio di nanosecondi per ogni match non vale il possibile rischio di sbagliare il valore. Quindi lascialo acceso -1. Puoi vedere più chiaramente, come funziona nella classe BitBuffer, usando un decompilatore (per esempio, DnsPy).

I valori booleani rot e vel vengono utilizzati solo se, se hai a che fare con rotazioni e velocità, e, come capisco, sono usati solo quando si calcola la fisica. Non modificare i loro valori in true, se non lo sai, cosa stai facendo.

Ecco altri sovraccarichi, che puoi usare (E che cosa fanno):

//изменяет значение постепенно вместо мгновенных скачков, работает только с числами с плавающей запятой.
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)

tipi, a quale statebind può essere applicato:]
  • corda
  • galleggiante
  • Doppio
  • byte
  • sbyte
  • bool
  • corto
  • ucorto
  • int
  • uint
  • lungo
  • ulong
  • char
  • Vec2
  • BitBuffer
  • NetIndex16
  • NetIndex2
  • NetIndex4dex4
  • NetIndex8
  • Qualunque cosa, ciò che eredita o è Cosa

StateBind funzionerà anche con le proprietà. Un esempio è quello, che viene utilizzato in 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 sono come suoni normali, e possono anche attivare funzioni durante la riproduzione dell'audio. Inoltre, la loro altezza può dipendere da un valore diverso.. Ma la cosa più importante, se usati con NetSoundBinding si sincronizzeranno online.

Vengono utilizzati NetSoundEffects e NetSoundBinding, per esempio, quando l'anatra starnazza o quando batti il ​​tamburo.

Puoi usare NetSoundEffect in due modi:

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;
}

Assicurarsi, qual è il tipo di variabile? – Stato vincolante, un NetSoundBinding

Stato vincolante (continuazione)

rilegatura della bandiera dello stato pubblico(stringa parametri[] campi)

StateFlagBinding è molto utile, quando hai a che fare con più booleani, per esempio, se qualcosa è aperto, chiuso o bloccato.

È solo un modo più veloce per eseguire più associazioni di stati logici, che in realtà è abbastanza obsoleto.

I parametri devono essere bool da usare, e quindi i loro valori verranno compressi in uno ushort. Usare ushort significa, che non puoi avere di più 16 campi logici.

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

Esempio tratto da HugeLaser. In realtà, i campi _charge e_fired non vengono utilizzati in esso, e dipende dallo sprite frame e dall'indice di animazione, quale, a sua volta, sincronizzato. E questo è il modo migliore per chiamare, altrimenti ci sarebbe un ritardo tra tutti i clienti. inoltre, nella scrittura del codice corretto funziona con StateBindigs (parti).

Tipi di StateBindigs, che non hai bisogno di sapere, poiché vengono utilizzati solo per l'ottimizzazione:

Associazione dati(campo stringa) – Funziona in modo più efficiente con BitBuffers, e solo con loro.
CompressedVec2Binding – Riduce le dimensioni di Vec2. Tutti i costruttori hanno valori massimi.
CompressoFloatBinding – Diminuisce la dimensione del galleggiante. Tutti i costruttori hanno valori massimi.
InterpolatedVec2Binding[/b] – Estensione CompressedFloatBinding con priorità più alta.

[h]Come scrivere il codice corretto, che funziona con StateBindinds.[/h]
Questo è particolarmente importante se hai a che fare con cose come timer o valori che cambiano rapidamente. Fondamentalmente quello che devi fare è scrivere le tue cose come se potessero saltare casualmente un mucchio di numeri e funzionare ancora alla fine.

Questo è particolarmente importante, se hai a che fare con timer o valori, che cambiano rapidamente. In sostanza devi scrivere il codice in questo modo, come se potesse saltare accidentalmente alcuni numeri e continuare a funzionare

Ecco un esempio: Hai un timer galleggiante, che decrementa il suo valore ad ogni frame, e appena arriva a zero, sta succedendo qualcosa.
Qui, come non valerne la pena:

Timer–;
Se(timer == 0) fare cose();

Funziona alla grande nel gioco locale; ma online, ogni frame può inviare solo una quantità limitata di dati, e molto probabilmente il valore del timer diminuirà ogni tre o più fotogrammi. In questo caso, il timer diminuirà così(supponendo, con cosa inizia? 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
Come vedi, 0 è stato saltato e doStuff() mai iniziato.

Qui, Come sistemarlo:

Timer–;
Se(Timer <= 0) fare cose();

Facilmente, non è questo? E funzionerà davvero!
Per questo motivo c'era un bug con un caricamento infinito della mappa Internet. Londra (sviluppatore) usato ==, e quando il numero richiesto è stato saltato a causa delle curve delle mappe, è andato oltre nell'infinito. per fortuna, è stato corretto in beta.

Sebbene, questa è solo metà del puzzle, in quanto è altrettanto importante scegliere i valori corretti per la sincronizzazione. Oltre ai campi _carica e_cotto, Enorme laser – questo è un ottimo esempio di, come dovrebbero essere i timer.

Brevemente, allora qui, cosa sta succedendo lì?: 2 StateBinding per le animazioni, usando l'indice di animazione e lo sprite frame. Questo può essere usato per sincronizzare le animazioni.

rilegatura della bandiera dello stato pubblico(stringa parametri[] campi)

StateFlagBinding è molto utile, quando hai a che fare con più booleani, per esempio, se qualcosa è aperto, chiuso o bloccato.

È solo un modo più veloce per eseguire più associazioni di stati logici, che in realtà è abbastanza obsoleto.

I parametri devono essere bool da usare, e quindi i loro valori verranno compressi in uno ushort. Usare ushort significa, che non puoi avere di più 16 campi logici.

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

Esempio tratto da HugeLaser. In realtà, i campi _charge e_fired non vengono utilizzati in esso, e dipende dallo sprite frame e dall'indice di animazione, quale, a sua volta, sincronizzato. E questo è il modo migliore per chiamare, altrimenti ci sarebbe un ritardo tra tutti i clienti. inoltre, nella scrittura del codice corretto funziona con StateBindigs (parti).

Tipi di StateBindigs, che non hai bisogno di sapere, poiché vengono utilizzati solo per l'ottimizzazione:

Associazione dati(campo stringa) – Funziona in modo più efficiente con BitBuffers, e solo con loro.
CompressedVec2Binding – Riduce le dimensioni di Vec2. Tutti i costruttori hanno valori massimi.
CompressoFloatBinding – Diminuisce la dimensione del galleggiante. Tutti i costruttori hanno valori massimi.
InterpolatedVec2Binding[/b] – Estensione CompressedFloatBinding con priorità più alta.

[h]Come scrivere il codice corretto, che funziona con StateBindinds.[/h]
Questo è particolarmente importante se hai a che fare con cose come timer o valori che cambiano rapidamente. Fondamentalmente quello che devi fare è scrivere le tue cose come se potessero saltare casualmente un mucchio di numeri e funzionare ancora alla fine.

Questo è particolarmente importante, se hai a che fare con timer o valori, che cambiano rapidamente. In sostanza devi scrivere il codice in questo modo, come se potesse saltare accidentalmente alcuni numeri e continuare a funzionare

Ecco un esempio: Hai un timer galleggiante, che decrementa il suo valore ad ogni frame, e appena arriva a zero, sta succedendo qualcosa.
Qui, come non valerne la pena:

Timer–;
Se(timer == 0) fare cose();

Funziona alla grande nel gioco locale; ma online, ogni frame può inviare solo una quantità limitata di dati, e molto probabilmente il valore del timer diminuirà ogni tre o più fotogrammi. In questo caso, il timer diminuirà così(supponendo, con cosa inizia? 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
Come vedi, 0 è stato saltato e doStuff() mai iniziato.

Qui, Come sistemarlo:

Timer–;
Se(Timer <= 0) fare cose();

Facilmente, non è questo? E funzionerà davvero!
Per questo motivo c'era un bug con un caricamento infinito della mappa Internet. Londra (sviluppatore) usato ==, e quando il numero richiesto è stato saltato a causa delle curve delle mappe, è andato oltre nell'infinito. per fortuna, questo è stato risolto in beta, anche se, questa è solo metà del puzzle, in quanto è altrettanto importante scegliere i valori corretti per la sincronizzazione. Oltre ai campi _carica e_cotto, Enorme laser – questo è un ottimo esempio di, come dovrebbero essere i timer., allora qui, cosa sta succedendo lì?: 2 StateBinding per le animazioni, usando l'indice di animazione e lo sprite frame. Questo può essere usato per sincronizzare le animazioni e c'è anche un campo bool doBlast., che diventa vero, quando il proprietario ha sparato con l'arma. E, se doBlast è vero (perché è un vincolo di stato) e tu non sei il proprietario, c'è un colpo.
Seguono le parti più importanti., che non consentono un doppio colpo e lo spegnimento istantaneo di doBlast, in modo che gli altri giocatori ricevano sempre giochi aggiornati

Proprietà della classe NetWork e altro

Rete.isServer

– il computer corrente è un host?.

Rete.isClient

– il computer corrente è un host?.

Rete.èAttivo

– la rete è attiva?.

Quando si crea AmmoType, adatto per multiplayer, deve avere un costruttore vuoto (nessun parametro).
È lui che si chiama, così, se hai un altro costruttore, non funzionerà affatto e si bloccherà durante l'estrazione del messaggio.

questo.Fuoco() sincronizzato online.
In realtà, generazione di proiettili sincronizzati, ma se la tua arma spara con oggetti ordinari, non è necessario prendere ulteriori misure, Assicurarsi, che tutto sia sincronizzato.

OnHoldAction sta sincronizzando, e OnReleaseAction non lo è.
Significa, che puoi usare OnHoldAction per caricare le cose (come si fa con Phaser?). Ma avrai bisogno di un modo per esserne sicuro, che OnReleaseAction sia sincronizzato (per esempio, usando questo. Fuoco, come in Phaser, o usando StateBinding).

Connessioni e come generare le cose

Come menzionato prima: Qualunque cosa (Cosa) c'è un campo di connessione, e tutti i valori di StateBidning saranno sincronizzati con i valori di queste connessioni. Non con host/server. Ancora non è un'opinione, all'inizio di ogni livello, l'host controllerà tutto, a parte altre anatre.
Per cose del genere, come Holdable, c'è l'ottimizzazione – non appena vengono raccolti, cambiano connessione con le anatre. Ma se tu, per esempio, creare qualcosa di nuovo, che fa qualcosa, dovrai fare un paio di passaggi aggiuntivi., cosa devi fare, per far nascere la scatola, se la tua mod è per il gioco locale:
Livello.Aggiungi(nuova cassa(500,500));
Se la tua mod è per il multiplayer:
if(isServerForObject)
  Level.Add(new Crate(500,500));

sì, hai bisogno di una condizione aggiuntiva, ed ecco perché:

isServerForObject

Poiché ogni cliente ha il proprio codice arma in esecuzione, аспавнится 2 scatole; uno a livello locale, e il secondo dal proprietario dell'oggetto.
isServerForObject controlla, se il computer corrente possiede l'oggetto, in modo che l'oggetto venga generato solo una volta.

In realtà, Puoi controllare, è l'anatra locale?, utilizzando isServerForObject.

Congratulazioni! Ce l'hai fatta.

In realtà, c'è un'altra cosa, che dovresti conoscere, e questo è Accarezzare. Infatti, ti permette di prendere il controllo della cosa e stabilire una connessione.

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;
}

Questo è usato per mantenere le cose sincronizzate, che non raccogli, come, per esempio, RCCar, per controllare quale si seleziona solo il controller. Ricorda, che Fondle esiste. Lui ti salverà la vita, quando provi a fare qualcosa di complicato e non funziona online.

Se vuoi vedere un esempio, guarda il codice della classe RCController e il suo aggiornamento. Questo è anche usato in aiitembox..

Bitbuffer'ы

I BitBuffer contengono dati, che puoi inviare ad altri. Se hai intenzione di fare NetMessage più complesso, è importante saperlo, Come funzionano.

Infatti, I BitBuffer contengono dati e ti consentono di leggerli / scriverli come tipi diversi. BitBuffer ha una posizione, intendendo che, da dove stai leggendo i dati?. E man mano che i valori vengono letti, la posizione aumenterà. In questo modo, non hai bisogno di gestire le variabili, ma solo con l'ordine di quello, come sono stati posizionati.

Scrivere valori su BitBuffer.

BitBuffer myData = nuovo BitBuffer();
myData.Write((byte)25);
myData.Write((int)50);
myData.Write(vero);
myData.Write(nuovo Vec2(25,25));

Lettura dei valori dallo stesso BitBuffer.

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

Molto importante, in modo che l'ordine di lettura e scrittura sia lo stesso, altrimenti non funzionerà.

Ecco un esempio, come posso scrivere un array su BitBuffer?, all'occasione, se ti aiuta a capirlo Poco meglio.

int[] myArray = nuovo int[] { 5,125,123123,67,2,324 };
BitBuffer myData = nuovo BitBuffer();myData.Write(myArray.Length); //dimensione dell'array
per ciascuno (num var in myArray)
myData.Write(nessuno); //scrivi ogni numeroBitBuffer sameData = nuovo BitBuffer(myData.buffer);int arraySize = sameData.ReadInt(); //leggendo il primo int (dimensione)
int[] theArray = nuovo int[dimensione array];per (int io = 0; io < dimensione array; io++)
theArray = stessiDati.ReadInt(); //leggendo ogni numero e inserendolo in un array[/citazione]

Limite BitBuffer.

Ogni BitBuffer memorizza la sua dimensione in ushost, che è un numero positivo da 0 per 2?? o 65535. E poiché ogni byte è composto da 8 pagina, BitBuffer può contenere fino a 8kb di dati.

 

Messaggi di rete personalizzati

Sincronizza le cose con StateBinding e assicurati, che abbiano i proprietari giusti non è sempre sufficiente per lavorare bene.

Come far funzionare i tuoi messaggi di rete

DuckGame carica tutti i NetMessage prima di caricare le mod, quindi i tuoi tipi di NetMessage non verranno aggiunti all'elenco dei messaggi di rete.

Per combattere questo, dobbiamo aggiungere i nostri MessageType.

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();
}

Ecco, ora puoi usare NetMessage nella tua mod.

Infatti, tutto NetMessage, che userai, questi sono eventi (eventi).
Puoi anche usare il normale NetMessage(NMDuckNetwork), ma solo se invii dati ad altri, come cappelli personalizzati.

Il tuo ipotetico NetMessage funziona in questo modo:
L'override di OnSerilize riempie il buffer di bit serializedData con i dati di ogni campo della classe (al contrario di StateBidning, le proprietà non funzionano), poi vengono inoltrati da interwebs(interweb), dove al ricevimento passano attraverso l'override di OnDeserilize, dove mette tutti i valori dal bitbuffer nei campi corretti.

Per tutti i Netmessages è necessario un costruttore vuoto, perché, quando ricevi il messaggio, verrà creato un nuovo oggetto di questo tipo, usando un costruttore vuoto. Se non ce l'hai, sarai schiacciato (non un giocatore di anime).

Creazione di NetMessage.

Qui, che aspetto ha un semplice NetMessage:

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

        }

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

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

    }

Ecco un elenco di tutti i tipi di dati, che verrà automaticamente serializzato e deserializzato(molto importante, per rendere pubblici i campi! altrimenti non verranno serializzati):

  • corda
  • galleggiante
  • Doppio
  • byte
  • sbyte
  • bool
  • corto
  • ucorto
  • int
  • uint
  • lungo
  • ulong
  • char
  • Vec2
  • Qualunque cosa, ciò che eredita o è Cosa

Quando si applica questo alle anatre: usa byte per netIndex.

duck.profile.networkIndex

e poi trova un'anatra, usando

DuckNetwork.profili[(int) indice].anatra;

In questo modo, tutti i tuoi messaggi, legati all'anatra, spedirà molto più velocemente, e le possibilità di fuori sincrono saranno notevolmente ridotte.

Serializzazione dei dati personalizzati.

E cosa, se vuoi un qualche tipo di array? Poiché non è supportato automaticamente, dovremo cambiare le sostituzioni 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
Invio di NetMessage in dimensioni > 8kb

Dal momento che tutto in duckgame viene inviato con BitBuffers, la dimensione massima di un messaggio Netmessage sarà 8 KB meno la dimensione dell'intestazione (Intestazione (intestazione) contiene informazioni su NetMessage, in modo che possa essere nuovamente trasformato in un heap di byte NetMessage).

Modo, con cui DuckGame risolve questo problema, è usare NMLevelDataHeader, più NMLevelDataChunks, che sono frammenti del livello, che si può mettere insieme, e dopo averli ricevuti, invierà un messaggio NMLevelDataReady, per confermare, che il trasferimento è completo.

Anche se funziona, non consiglierei di copiare il codice, poiché funziona solo quando si esegue un trasferimento contemporaneamente con un cliente o tutti i clienti contemporaneamente. ho usato un post, contenente sessione e bool terminato (sessione e un bool finito), e poi ho avuto un gestore di dati, chi ha capito, quando si apre una nuova sessione session, e quando è finita, usando l'ultimo bool. Prima, come DuckGame invierà i suoi NetMessage, li ordina per dimensione, e quindi tutti i messaggi, tranne l'ultimo, deve essere della stessa misura, e quest'ultimo dovrebbe essere inferiore, se decidi di aggiungere semplicemente i dati ricevuti in un array durante la ricezione.

Invece di ordinare l'array, quando l'ho inviato, ho deciso di ordinarlo prima, quando tutti i dati sono stati ricevuti. Adesso, quando ci penso, Non sono sicuro, in che modo era meglio?, ma non ha molta importanza. Comunque, ecco il codice, mostrando, Come posso fare ciò (dalla modalità dell'autore, Reskin), utilizzando l'approccio del transfer manager: DataTransferSession[github.com], DataTransferSession[github.com] e NMDataSlice[github.com]

Dato che sono un buon programmatore e ho scritto codice flessibile, puoi copiare file e utilizzare la funzione DataTransferManager.SendLotsOfData e agganciare qualcosa all'evento DataTransferManager.onMessageCompleted, e dovrebbe essere tutto ok.

NetDebug

Se hai ancora bisogno di protestare per qualcosa, ma chiedi costantemente ai tuoi amici di scaricare la tua mod e andare nella tua lobby – non è un'opzione, specialmente per te c'è un parametro di lancio -netdebug.

Per attivarlo, fare clic con il tasto destro sul gioco nella libreria:

Nel pannello che si apre, inserisci il parametro di lancio nell'apposito campo:

Per attivarlo, fare clic con il tasto destro sul gioco nella libreria:

A partire dal Polanas

Diventa il primo a commentare

lascia un commento

L'indirizzo email non sarà pubblicato.


*