Permalänk
Medlem

Klurighet. Teewars nätverk

Tjo,

Här kommer en liten rolig uppgift med anknytning till Teewars.

När jag designade nätverket i Teewars så var det flera problem att slåss med. Bland annat hur man skall packa data över nätverket. Att använda structs är trevligt men har vissa problem. Här kommer ett exempel.

struct player_info { float x, y; float angle; unsigned char current_weapon; unsigned char emote; bool hook_active; float hook_x, hook_y; unsigned char health; unsigned char armor; };

Problemet med att skicka en sådan strukt över nätverket är att dessa.
* Olika platformar / kompilatorer kommer göra structen olika stor vilket pajar allt.
* Olika processorer har olika endianness

Dessa går att lösa genom att skriva två funktioner, en som packar och en som packar upp structen och ser till att swappa den korrekt. Dessa blir bara en pain att underhålla. Allt i dessa structar får bara innehålla datatypen int!. Då ser det ut såhär.

struct player_info { int x, y; int angle; int current_weapon; int emote; int health; int armor; int hook_active; int hook_x, hook_y; };

Detta gör att structen alltid blir lika stor. Du kan skriva en generell funktion för att packa ner den så att endianness inte blir ett problem. Den har ju vissa drawbacks och det är att den generellt blir större och det är här lilla klurigheten kommer in. Många av dessa members är värden från säg, -30 till 30 oftast vilket gör att det är ganska oeffektivt att skicka den över nätverket i sin nuvarande form. Jag har lagt på en kompression som heter LZW på detta men det går ju att gåra något mycket bättre.

Själva klurigheten. Vad som behövs är en funktion som packar ner en int till ett variablet antal bytes. Så att säg, -30 till 30 bara blir 1 byte stor, utöver det behöver den 2 bytes och sedan 3, 4 osv. tills den når sitt tak. Denna funktion skall fungera på alla 32bit int tal, negativa och positiva. Den skall vara effektiv just runt 0 +- 30 typ där den tar en byte för att lagra det. Denna funktion måste vara snabb eftersom den kommer köras på servern väldigt ofta.

Här kommer lite kod ni kan använda för att testa. Det är alltså vint_pack och vint_unpack som skall göras mycket smartare. Språket är C.

#include <stdio.h> // pack i into dst and return where to pack the next int unsigned char *vint_pack(unsigned char *dst, int i) { *(int*)dst = i; return dst+sizeof(int); } // unpack an int from src into out and return where to find the next const unsigned char *vint_unpack(const unsigned char *src, int *out) { *out = *(int*)src; return src+sizeof(int); } // the test application int main() { unsigned char buffer[32]; unsigned char *p1; const unsigned char *p2; int j; unsigned i; //for(i = 0x70000000; i < 0x8fffffff; i++) // full test for(i = 0x7ff00000; i < 0x8ff0000; i++) // fast test { j = 0; p1 = vint_pack(buffer, *((int *)&i)); p2 = vint_unpack(buffer, &j); if(p1 != p2) { printf("%d (%x) failed. pointer problem %p != %p\n", i, i, p1, p2); return -1; } if(i != j) { printf("%d (%x) failed. %d != %d %x != %x %02x%02x%02x%02x%02x%02x%02x%02x\n", i, i, i, j, i, j, buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7]); return -1; } if((i&0x7fffff) == 0) // remove this when profiling printf(".\n"); } printf("all done!\n"); return 0; }

Skall bli intressant att se diskussionerna + lösningar som folk kommer på.

Visa signatur

Teeworlds - För dig som gillar gulliga saker med stora vapen.

Permalänk
Medlem

Blir det verkligen så mycket mindre data jämfört med den LZW komprimerade varianten du kör med nu? Och är det värt besväret att krångla till det? Jag menar, hur slött skulle det bli om du skapade en XML-fil som skickades över LZW komprimerad istället? Xml är i alla fall standardiserat så alla plattformar borde kunna läsa det.

Annars får man speca upp ett fast binärt protokoll som måste följas och så har man en översättningsfunktion för varje plattform som återskapar datat på rätt sätt. Eller så kan man ha en "haeder" alltså information i början som talar om hur efterföljande data ska tolkas, t.ex. antal bytes man kommer att överföra och vilken platform som skapat informationen och så har man olika översättningsfunktioner beroende på informationen i "headern".

Det där med olika antal bytes för respektive integer så kan man speca upp en integer i "headern" som har 2 bytes för respektive efterföljande integer för att deklarera storleken i antal byte (1-4 bytes). Det krävs alltså 20 bytes för 10 stycken integer.

Själva programmeringen överlåter jag åt andra, detta var mer allmänna funderingar på själva problemet och hur man kan lösa det.

Permalänk
Medlem
Citat:

Ursprungligen inskrivet av ronnylov
Blir det verkligen så mycket mindre data jämfört med den LZW komprimerade varianten du kör med nu? Och är det värt besväret att krångla till det? Jag menar, hur slött skulle det bli om du skapade en XML-fil som skickades över LZW komprimerad istället? Xml är i alla fall standardiserat så alla plattformar borde kunna läsa det.

Annars får man speca upp ett fast binärt protokoll som måste följas och så har man en översättningsfunktion för varje plattform som återskapar datat på rätt sätt. Eller så kan man ha en "haeder" alltså information i början som talar om hur efterföljande data ska tolkas, t.ex. antal bytes man kommer att överföra och vilken platform som skapat informationen och så har man olika översättningsfunktioner beroende på informationen i "headern".

Det där med olika antal bytes för respektive integer så kan man
speca upp en integer i "headern" som har 2 bytes för respektive efterföljande integer för att deklarera storleken i antal byte (1-4 bytes). Det krävs alltså 20 bytes för 10 stycken integer.

Själva programmeringen överlåter jag åt andra, detta var mer allmänna funderingar på själva problemet och hur man kan lösa det.

Jag tänker vara snäll mot dig och inte ens kommentera XML iden.

Angående ett fast binärt protokoll så har jag det, det hela handlar om att göra det effektivare så att det tar mindre bandbredd.

Det är bra att du funderar, men dina ideer är, utan att vilja vara elak, inget vidare.

Minimera datamängden snabbt och effektivt är själva temat. Teewars skickar snapshots med hur världen har ändrat sig 50ggr/sekund vilket leder till att varje byte som kan sparas i dessa snapshot deltas minskar bandbredden med 50 byte/s. Kanske inte låter så mycket, men det är en heldel.

Jag estimerar att genom att packa dessa med denna variable integer grejan så bör trafiken sjunka med ungefär 50-60% kanske, beroende på hur effektiv den blir.

EDIT: snacket i #c++.se har vart ganska roligt.

Det fina sättet verkar vara att ha en bit i varje byte som säger ifall det är den sista byten för int:en. Problemet är att implementera det effektivt. Sedan kanske man skall köra med en sign-bit i första byten för att kunna representera -1 (0xffffffff) etc effektivt.

EDIT2:

För den som är intresserad. detta är vad vi kom fram till:

// Format: ESDDDDDD EDDDDDDD EDD... Extended, Data, Sign unsigned char *vint_pack(unsigned char *dst, int i) { *dst = (i>>25)&0x40; // set sign bit if i<0 i = i^(i>>31); // if(i<0) i = ~i *dst |= i&0x3F; // pack 6bit into dst i >>= 6; // discard 6 bits if(i) { *dst |= 0x80; // set extend bit while(1) { dst++; *dst = i&(0x7F); // pack 7bit i >>= 7; // discard 7 bits *dst |= (i!=0)<<7; // set extend bit (may branch) if(!i) break; } } dst++; return dst; } const unsigned char *vint_unpack(const unsigned char *src, int *i) { int sign = (*src>>6)&1; *i = *src&0x3F; while(1) { if(!(*src&0x80)) break; src++; *i |= (*src&(0x7F))<<(6); if(!(*src&0x80)) break; src++; *i |= (*src&(0x7F))<<(6+7); if(!(*src&0x80)) break; src++; *i |= (*src&(0x7F))<<(6+7+7); if(!(*src&0x80)) break; src++; *i |= (*src&(0x7F))<<(6+7+7+7); } src++; *i ^= -sign; // if(sign) *i = ~(*i) return src; }

Visa signatur

Teeworlds - För dig som gillar gulliga saker med stora vapen.

Permalänk

Satt och försökte lösa den, samma packningsprincip, lite annorlunda (antagligen sämre, och klart sämre dokumenterad) kod:

int LenLUT[32]; char MaskLUT[32][6]; void CreateLUTs(){ for (int i = 0; i<32; i++){ if((i&0xF) == 0xF){ //1111 LenLUT[i] = 5; strcpy( MaskLUT[i], "\x3f\x7f\x7f\x7f\x1f"); } else if((i&0x7) == 7){ //0111 LenLUT[i] = 4; strcpy( MaskLUT[i], "\x3f\x7f\x7f\x7f\x00"); } else if((i&0x3) == 3){ //0011 LenLUT[i] = 3; strcpy( MaskLUT[i], "\x3f\x7f\x7f\x00\x00"); } else if((i&1) == 1){ //0001 LenLUT[i] = 2; strcpy( MaskLUT[i], "\x3f\x7f\x00\x00\x00"); } else { //0000 LenLUT[i] = 1; strcpy( MaskLUT[i], "\x3f\x00\x00\x00\x00"); } } } // unpack an int from src into out and return where to find the next const unsigned char *vint_unpack(const unsigned char *src, int *out) { int sign = (*src & 0x40)>>6; int lenident = (0x80 & *src)>>7 | (0x80 & *(src+1))>>6 | (0x80 & *(src+2))>>5 | (0x80 & *(src+3))>>4; char *m = MaskLUT[lenident]; int value = m[0] & *src | (m[1] & *(src+1))<<6 | (m[2] & *(src+2))<<13 | (m[3] & *(src+3))<<20 | (m[4] & *(src+4))<<27; int a[2]= {value, ~(value)}; *out = a[sign]; return src+LenLUT[lenident]; } // pack i into dst and return where to pack the next int unsigned char *vint_pack(unsigned char *dst, int i) { const unsigned int signmask = 1<<31; unsigned int sign = (i&signmask)>>31; unsigned int b = i; if(sign) b = ~i; const unsigned int m5 = 0xf8000000; // 11111 0000000B 0000000B 0000000B 000000BS const unsigned int m4 = 0x07f00000; // 00000 1111111B 0000000B 0000000B 000000BS const unsigned int m3 = 0x000fe000; // 00000 0000000B 1111111B 0000000B 000000BS const unsigned int m2 = 0x00001fc0; // 00000 0000000B 0000000B 1111111B 000000BS const unsigned int m1 = 0x0000003f; // 00000 0000000B 0000000B 0000000B 111111BS if ( !(b & ~m1) ){ *(dst) = (b&m1)|(sign<<6); return dst+1; } else if ( !(b & ~(m1|m2) ) ){ *(dst+1) = (b&m2)>>6; *(dst) = (b&m1)|0x80|(sign<<6); return dst+2; } else if ( !(b & ~(m1|m2|m3) ) ){ *(dst+2) = (b&m3)>>13; *(dst+1) = ((b&m2)>>6)|0x80; *(dst) = (b&m1)|0x80|(sign<<6); return dst+3; } else if ( !(b & m5) ){ *(dst+3) = (b&m4)>>20; *(dst+2) = ((b&m3)>>13)|0x80; *(dst+1) = ((b&m2)>>6)|0x80; *(dst) = (b&m1)|0x80|(sign<<6); return dst+4; } else { *(dst+4) = (b&m5)>>27; *(dst+3) = ((b&m4)>>20)|0x80; *(dst+2) = ((b&m3)>>13)|0x80; *(dst+1) = ((b&m2)>>6)|0x80; *(dst) = (b&m1)|0x80|(sign<<6); return dst+5; } }

Man måste köra CreateLUTs(); i manin innan man kör resten.

EDIT: Fixade en liten bugg

Visa signatur

Python-IRC på svenska: #python.se

Permalänk
Medlem

Har ni kollat in hur t.ex. RakNET gör? Och dessutom, hur stora paket skickar ni i detta spelet egentligen om inte totala headern på paketet är "stor" i förhållande? (Och på liknande sätt görs ju unicode/UTF-8, skriv en 0:a som första biten om det är 1 byte (eller sista för den delen om du vill ha signtecket och sen bitshifta).

Visa signatur

g++

Permalänk
Medlem

Kan man inte tänka sig nån metadatastruct/klass av nåt slag? Servern och klienten måste ju vara matchade i vilket fall (versionsmässigt), så man borde lika gärna kunna ha en matchande datastruktur (som då inte behöver skickas över nätverket) som säger hur den första är uppbyggd... eller?

struct player_info { int x, y; int angle; int current_weapon; int emote; int health; int armor; int hook_active; int hook_x, hook_y; }; //! en medlemsvariabel per medlem i player_info, värdet = hur många bytes den tar upp struct player_info_sizeinfo { static const int x = 4; static const int angle = 1; static const int current_weapon = 1; //osv static const int emote ; static const int health; static const int armor; static const int hook_active; static const int hook_x, hook_y; };

Sen borde det vara en barnlek att packa upp och ner med kod motsvarande den här:

const unsigned char *vint_unpack(const unsigned char *src, player_info& out) { out.x = *src; src += player_info_sizeinfo.x; //... out.angle = *src; out.angle &= //maska ut player_info_sizeinfo.angle många bytes på nåt bra sätt src += player_info_sizeinfo.angle; // osv }

Då slipper man lagra storleken i player_info, vilket innebär att man tom. kan lagra saker i chunks som är < 1 byte (vilket är vettigt för dom som var -30 till 30 stora). Alltså att istället för att "player_info_sizeinfo.x" är hur många bytes det är, så kan det vara hur många bitar det är. Då blir det ju 100% optimalt om man ser till nätverkstrafiken iaf (bortsett från att det är okomprimerat förstås).

Vet inte hur optimalt det är prestandamässigt dock. Borde inte vara någon jätteskillnad kan man tycka.

Tycker nästan det är för enkelt så om jag missat nåt så... EDIT: Eh, just det. Det blir ju jobb att underhålla den där funktionen, vilket var dumt. Men det borde väl gå att komma runt. Ska tänka lite.

Såhär kanske?

struct player_info_sizeinfo { const int x = 4; const int angle = 1; const int current_weapon = 1; //osv const int emote ; const int health; const int armor; const int hook_active; const int hook_x, hook_y; }; const unsigned char *vint_unpack(const unsigned char *src, int* out) { const int num_members = sizeof( player_info ) / 4; // int måste vara 32bitar här... player_info_sizeinfo sizedata; player_info_sizeinfo* p_sizedata = &sizedata; for ( int x = 0; x < num_members; ++x ) { const int curr_size = *p_sizedata; int curr_member = *src; curr_member &= //maska ut curr_size många bytes på nåt bra sätt *out = curr_member; // increment pointers src += curr_size; ++p_sizedata; ++out; } }

Nåt sånt?

Visa signatur

Min hemsida: http://www.srekel.net
Pocket Task Force: http://ptf.srekel.net
Kaka e gott! http://kaka.srekel.net

Permalänk
Medlem

Matricks:
Finns det någon anledning till att skicka all den där datan 50ggr/sek?
Varför inte bara skicka den data som ändrats sedan senaste gången?
Skulle lätt kunna lösas med en liten header-del på 10bit* som maskar ut de värden som skickas.. Om ex. bara positionen (dvs. x och y) ändrats skulle man ju bara behöva skicka ut en bråkdel av all data (8bytes+header) jämfört med om allt skulle skickas hela tiden.

* Eller snarare 9bit, då hook_active inte tjänar något på att maskas.. Och om man dessutom använder endast en bit för att maska båda hook-positionerna, så klarar man sig ju med en header på endast 8bit (Jag antar här alltså att hook:en oftast rör sig i båda riktningarna samtidigt, varpå de bör dela mask).

Undrar också vilka max/min-värden de olika variablerna i din struct har, alltså ex. x kräver den verkligen 4bytes, eller klarar den sig med mindre upplösning osv.?

Visa signatur

The difference between stupidity and genius - the latter has limits

Permalänk
Medlem

Jag gjorde nått liknande för länge sen. Jag har för mig att jag löste det genom att skicka med en liten header som innehöll 2 bitar/medlem i structen som talade om antal bytes. Sen lät jag all data vara signed oxå för att slippa jobb. Vet inte om det är en lösning som skulle fungera här, men det var enkelt att koda iaf

Visa signatur

www.filipsprogram.tk - lite freeware
"Delight, herregud. Talang är bara förnamnet."

Permalänk

För att leva upp till min "macroexpand" signatur:

struct INetworkStream { virtual void serialize(void* pData, int numBits) = 0; virtual void serialize_if_modified(void* pData, void* pPrevData, int numBits) = 0; virtual void serialize(void* pData, int rangeMin, int rangeMax) = 0; virtual void serialize_if_modified(void* pData, void* pPrevData, int rangeMin, int rangeMax) = 0; }; enum EGameWeapons { ePistol, eShotgun, eBFG, eNumGameWeapons } /* @generate filter:/usr/bin/python output:playerGameInfo.h defpacket(playerGameInfo) add_field("x", type="int", rangeMin=0, rangeMax=100, alwaysSend = true) add_field("y", type="int", rangeMin=50, rangeMax=80, alwaysSend = true) add_field("weapon", enumType="EGameWeapon", enumMaxValue="eNumGameWeapons") add_field("score", type="int") endpacket() @end */ #include "playerGameInfo.h"

Den genererade headern "playerGameInfo.h" kan t.ex. se ut så här:

struct playerGameInfo { int x; int y; enum EGameWeapons weapon; int score; void serialize(INetworkStream* stream) { stream->serialize(&x, 0, 100); stream->serialize(&y, 50, 80); stream->serialize_if_modified(&weapon, &_prev_weapon, log2(eNumGameWeapons)); _prev_weapon = weapon; stream->serialize_if_modified(&score, &_prev_score, sizeof(int) * 8); _prev_score = score; } enum EGameWeapons _prev_weapon; int _prev_score; };

Permalänk
Medlem

//edit, såg nu att zevon hade tipsat om samma sak, oh well.

//edit igen, har hört en del om att man ska hålla sig undan högerskift, som jag sett att många har använt, eftersom man inte riktigt vet vad som kommer längst till vänster efter skiftningen.

Om man nu vill minska datan som skickas, det borde då gå snabbast att jämföra den structen som ska skickas med den förra som skickades, ta ut en diff, och skicka diffen.

Det kräver givetvis att både server och klient vet hur structen ser ut.

Funderade lite till, kom fram med lite helt otestad kod, vet bara att den kompilerar än så länge.

//edit, uppdaterat med testad kod nu

Om den fungerar så sparar man bandbredd i alla fall, förutom när alla poster uppdateras samtidigt, då är man tvungen att skicka en int extra.

#include <stdio.h> #include <stdlib.h> #include <string.h> struct player_info{ int x:9, y:9; int angle:6; int current_weapon:4; int emote:3; int health:8; int armor:8; int hook_active:1; int hook_x:9, hook_y:9; }; int player_info_to_array(int* array_player_info, struct player_info* struct_player_info){ int array_size=0; array_player_info[array_size++] = struct_player_info->x; array_player_info[array_size++] = struct_player_info->y; array_player_info[array_size++] = struct_player_info->angle; array_player_info[array_size++] = struct_player_info->current_weapon; array_player_info[array_size++] = struct_player_info->emote; array_player_info[array_size++] = struct_player_info->health; array_player_info[array_size++] = struct_player_info->armor; array_player_info[array_size++] = struct_player_info->hook_active; array_player_info[array_size++] = struct_player_info->hook_x; array_player_info[array_size++] = struct_player_info->hook_y; return array_size; } int diff_to_player_info(struct player_info* struct_player_info, int* diff){ const int header_size=1; const int struct_size=10; int array_size=header_size; int i; for(i=0;i<struct_size;i++){ //only patch the changed values if( diff[0] & (1<<i) ){ switch(i + header_size){ case 1: struct_player_info->x += diff[array_size++]; break; case 2: struct_player_info->y += diff[array_size++]; break; case 3: struct_player_info->angle += diff[array_size++]; break; case 4: struct_player_info->current_weapon += diff[array_size++]; break; case 5: struct_player_info->emote += diff[array_size++]; break; case 6: struct_player_info->health += diff[array_size++]; break; case 7: struct_player_info->armor += diff[array_size++]; break; case 8: struct_player_info->hook_active += diff[array_size++]; break; case 9: struct_player_info->hook_x += diff[array_size++]; break; case 10: struct_player_info->hook_y += diff[array_size++]; break; } } } return array_size; } //only works if the same datatype is used throughout the struct int array_int_diff(int* diff_array, int* old_array, int* new_array, const int header_size){ int i; int header=0; int diff_array_size=1;//a header is counted for //create a header //make the diff_array for(i=0;i<header_size;i++){ if(old_array[i] != new_array[i] ){ header |= 1<<(i); diff_array[diff_array_size++] = new_array[i] - old_array[i]; } } //put the header first diff_array[0] = header; //be nice and free up unused memory diff_array = (int*) realloc(diff_array, (diff_array_size-1)*sizeof(int)); // it returns an array with a header at the first two indexes // with a bitmask telling which setting that was changed // for example: 0000 0010 0110 0010 // means 2,6,7,10 entry in the struct was changed, // the diff array is then: // diff_array // [0] header // [1] entry2; // [2] entry6; // [3] entry7; // [4] entry10; // return diff_array_size; } // the test application int main(){ int * array_in; int * array_out; int * diff_array; int diff_array_size; int struct_size; struct player_info* struct_old = (struct player_info*) malloc(sizeof(struct player_info)); struct player_info* struct_new = (struct player_info*) malloc(sizeof(struct player_info)); //some test-values memcpy(struct_old, &(struct player_info){40, 1,1,1,1,1,1,1,6,1},sizeof(struct player_info)); memcpy(struct_new, &(struct player_info){39,1,1,1,1,2,1,1,5,1},sizeof(struct player_info)); //containers array_in = (int*)calloc(12, sizeof(int)); array_out = (int*)calloc(12, sizeof(int)); diff_array = (int*)calloc(12, sizeof(int)); //generate arrays from the structs struct_size = player_info_to_array(array_in, struct_old); struct_size = player_info_to_array(array_out, struct_new); //get a diff diff_array_size = array_int_diff(diff_array, array_in, array_out, struct_size); //test if the diff was successful diff_to_player_info(struct_old, diff_array); if(memcmp((void*)struct_old, (void*)struct_new, sizeof(struct player_info*))!=0){ printf("Diff failed!\n"); exit(-1); } printf("No errors. \n //Diff size: %i\n //Compressed size: %i\n",0,0); //clean free( array_in ); free( array_out ); free( struct_old ); free( struct_new ); free( diff_array ); return 0; }

Det är mycket möjligt att jag tänkt fel någonstans, eller rentav gjort fel, det var ett tag sedan jag skrev kod senast.

Men som man kan se behöver man wrappers på structen, en som gör om till en array på klient-sidan, och en annan som lägger på diffen på serversidan.

Visa signatur
Permalänk
Medlem

hook_x och hook_y borde kunna reduseras till hook_distance med tanke på att du har angle.
Är både hook_x & hook_y 0.0 (delta 0.01) kan man anta att hook_active är false?

Permalänk
Medlem

Angle är nog vilken vinkel man har vapnet i, den är (typ) oberoende av vinkeln på hooken...

Visa signatur

Militant VIM-användare.

Permalänk
Medlem

Att bara skicka updates när man gör något är en bra idé.
Att skicka updates när man ändrar angle, hastighet och riktning kan också spara in en del.
T.ex. om spelare 1 börjar springa åt höger, då skickar man att hastigheten åt höger är ett visst värde, sen skickas inte det igen då alla klienter själva förstår att spelaren springer.
Sen om hastigheten ändras (dvs om spelaren stannar, byter riktning eller hoppar) så skickas ett paket. Istället för att uppdatera positioner.
Problem uppstår dock om klienterna inte har samma timing, men så länge det fungerar ordentligt så borde det inte vara några problem.
Vad man kan göra då är att man skickar rörelse-paketet lite oftare än vid förändring enbart och rapporterar koordinater i samma paket så att de andra klienterna uppdateras med rätt positioner.
Ett annat tips är att bygga protokollet på sådant sätt att man kan enkelt separera olika data så att man kanske inte skickar t.ex. emote varje update, etc etc.

Lycka till

Visa signatur

i7-4770K @ 3.5GHz - 32GB RAM - 2x 500GB SSD - 40TB HDD - GTX770

Permalänk
Medlem

Oj, här har det hänt saker. Min semester har börjat och jag var iväg på en liten tur och är nu tillbaka. Dessa problem som diskuteras löste jag innan jag drog.

Många kommenterar på att man bara borde behöva skicka det som ändrats och det gör jag idag (delta compression). Nätverket i teewars är mycket inspererat av hur just Quake III Arena sköter det hela. För den som är intresserad finns det att läsa http://trac.bookofhook.com/bookofhook/trac.cgi/wiki/Quake3Net...

Många har löst det med att skriva dessa serializering funktioner etc och det är dom jag vill försöka bli av med vilket jag bivigt till stordel.

Visa signatur

Teeworlds - För dig som gillar gulliga saker med stora vapen.