Programmera spel i C++ för nybörjare/Nätverksprogrammering 3
OBS!!!
Exemplet i botten fungerar inte, structen skickas aldrig eftersom kontakt inte uppnås mellan sändande och mottagande socket. Om någon som är bättre än jag på nätverksprogrammering hittar felet och ändrar koden vore ingen gladare än jag :)
Mycket av informationen grundar sig på:
http://www.sfml-dev.org/tutorials/1.6/network-packets.php
För att spelexemplen skall fungera föresätts det att du använder SFML 1.6 samt Visual C++ 2010 express.
Problemen
[redigera]De två föregående kapitlen visade hur man kan skapa enkel kontakt mellan datorer i ett nätverk. Redan med de enkla programmen kan det uppstå problem och det finns främst tre olika felkällor:
1: Värsta problemet kallas "endianism" på engelska. Enkelt beskrivet har det att göra med hur olika processorer tolkar heltal. Skall man säga att 48 = fyrtio och åtta, eller åtta och fyrtio (som t.ex. danskar och tyskar säger)? En enkel processor som driver Windows XP i en 32 bitars miljö hanterar t.ex. 16 bitars talet 48 som: 00000000 00110000 medan en Apple PowerPC som tar emot talet mycket väl kan se samma 16 bitars heltal som 00000011 00000000 (talet 768) eftersom de olika processorfamiljerna hanterar heltal helt olika. Flyttal är normalt sett mycket svårare att skicka och då kan man t.ex. skicka flyttalet i två olika heltalsvärden, heltalet som ett värde och decimalerna som ett annat "heltalsvärde" och programeringsmässigt slå ihop dem vid framkomsten. Läs mer om: flyttal och endianism. Observera att problem med endianism inte uppstår om programmet körs på en enda dator utan bara när samma kod körs på olika datorer med olika processorfamiljer som risken t.ex. är i ett nätverksspel.
2: Olika processorer ger primitiverna olika mycket bits. En long int variabel kan vara 16 bitar på din dator medan en annan dator kan tycka att samma variabel skall vara en 32 bitars long int och då blir siffrorna vid överföringen över nätverket fel.
Lösningen på bägge dessa problemen är att alltid använda SFML:s egna primitiver (se nedan) när du hanterar heltal.
3: Slutligen blir det lätt fel när man skickar- och tar emot större bitar information än enstaka bokstäver och siffror. I standard C++ programmering användes ofta länkade listor (så nu vet du varför du bör lära dig mer om det) där man skickar struct-instanser med information mellan datorerna. SFML använder en egen class som heter sf::Packet som du bör använda när du skapar spel i SFML.
Primitiva typer
[redigera]Som du vet (eller borde veta) som C++ programmerare kallas alla grundtyperna för primitiva typer. Det inkluderar int, unsigned int, short int, long int osv. Problemet är att dessa typer är generella och kan vara av olika storlek i minnet beroende på vilket system programmet körs på. Eftersom vi måste veta med 100% säkerhet exakt hur mycket minne som en primitiv använder, så kan vi inte skicka meddelanden baserade på generella primitiver mellan datorer i ett nätverk.
Lösningen är att använda SFML:s inbyggda primitiver:
sf::Int8 = 8 bitar heltal sf::Uint8 = 8 bitar positivt heltal sf::Int16 = 16 bitar heltal sf::Uint16 = 16 bitar positivt heltal sf::Int32 = 32 bitar heltal sf::Uint32 = 32 bitar positivt heltal (Uint, förkortning för unsigned int = heltal utan signatur som talar om ifall det är positivt eller negativt så det är altid ett värde från 0 och uppåt).
Om du t.ex. definierar
int iEttHeltal = 48;
kan det alltså bli fel, då är det bättre att skriva
sf::Int8 iEttHeltal = 48;
Enkelt exempel
[redigera]Hur skickar man då information mellan datorer? Här följer ett superenkelt exempel. Vi använder oss av << och >> symbolerna, precis som när man skall få ut och in information på skärmen, men istället är den överlagrad för att skicka- eller ta emot information över nätverket. Det finns ett "float" värde med så att du skall kunna testa om det förändras mellan datorerna.
// Information som skall skickas sf::Int8 x = 24; std::string s = "hej"; float f = 59864.265f; sf::Packet ToSend; ToSend << x << s << f;
När man sedan skall läsa av informationen skriver man:
// Information som tas emot sf::Packet Received; sf::Int8 x; std::string s; float f; Received >> x >> s >> f;
UDP och TCP
[redigera]Man skickar, och tar emot, paketet litet olika beroende på om man använder UDP eller TCP protokollet.
// Med TCP sockets Socket.Send(Packet); Socket.Receive(Packet);
// Med UDP sockets Socket.Send(Packet, Address, Port); Socket.Receive(Packet, Address);
Exempel med en struct
[redigera]Rent praktiskt nu då, hur gör man för att skicka en information om en struct (eller klass) från en dator till en annan? Vi börjar med att skapa en struct som heter Character:
struct Character { sf::Uint8 Age; std::string Name; float Height; };
Därefter skall vi skapa en ny instans av Character som vi döper till C. Med hjälp av den instansen skall vi kunna skicka och lämna information över nätverket.
sf::Packet& operator <<(sf::Packet& Packet, const Character& C) { return Packet << C.Age << C.Name << C.Height; } sf::Packet& operator >>(sf::Packet& Packet, Character& C) { return Packet >> C.Age >> C.Name >> C.Height; }
I praktiken
[redigera]Nu skall vi kunna läsa och skicka information, då skapar vi karaktären Bob. Till Bob har vi paketet Packet. Bob innehåller namn, ålder och storlek. När vi skickar information om Bob kopplar vi bara ihop Bob Med paketet:
Character Bob; //Bob skapas sf::Packet Packet; //Paketet skapas //Redigera informationen i paketet Packet << Bob; //Fyll paketet med information från Bob Packet >> Bob; //Fyll Bob med information från paketet
Struct eller class?
[redigera]Du får hela tiden uppmaningen om att skicka information över nätverket i form av en struct, samtidigt som en van C++ programmerare helst jobbar med klasser/class för en ren Objekt Orienterad Programmering. Så varför struct?
Förklaringen är enkel, du kan inte skicka värden som är private eller protected över nätverket, bara värden som är public. Eftersom en struct har samtliga värden satta som public när den skapas minskar risken att göra fel. Du kan naturligtvis använda klasser istället så länge som du kommer ihåg att det bara är public värdena som kommer att kunna användas.
Pojke och ko
[redigera]Kommer du ihåg övningen där du skulle få en ko att jaga en pojke? Det står i kapitlet: Sprites och spelpjäser 2. I det exemplet styrdes pojken och kon med samma tangentbord, nu är det dags att göra om det till ett nätverksspel där den ena styr kon och den andra styr pojken. För enkelhetens skull låter vi datorn som har pojken agera server. Det vi behöver veta är: Vilken spelare är det (ID), hastigheten i X- och Y-led samt vilken bild som skall visas upp.
class spelare { public: sf::Uint8 ID; //0=pojke, 1=ko sf::int32 hastighetX; //Hastighet i X-led, kan vara negativ sf::int32 hastighetY; //Hastighet i Y-led, kan vara negativ sf::Sprite sprite; //Bilden som representerar pojken eller kon };
Komplett programkod till enkelt nätverksspel
[redigera]//Originalkod från SFML hemsida. //Bearbetad för att driva ett enkelt spel och inte bara skicka ett ord fram och tillbaka #include <iostream> #include <SFML/System.hpp> #include <SFML/Graphics.hpp> #include <SFML/Window.hpp> #include <SFML/Network.hpp> #include <vector> //Måste vara med, vet ej varför // using namespace std; #define SFML_STATIC //Se till så att det inte behövs extra DLL-filer //Skapa en struct som är "budbäraren" mellan datorerna //Det är den som skickas mellan så att spelarna fr tillgång till varandras värden /* struct Position { sf::Uint8 spelarID; sf::Int32 xfart; sf::Int32 yfart; }; */ //Skapa den klass som spelarna pojken och kon kommer från class spelare { public: sf::Uint8 ID; //0=pojke, 1=ko sf::Int32 hastighetX; //Hastighet i X-led, kan vara negativ sf::Int32 hastighetY; //Hastighet i Y-led, kan vara negativ sf::Sprite sprite; //Bilden som representerar pojken eller kon }; // Server = styr pojken // Client = styr ko //-------------------------------------------------------------- //Skapa paketen som skall skickas mellan datorerna //-------------------------------------------------------------- sf::Packet &operator <<(sf::Packet &Packet, const spelare &C) { return Packet << C.ID << C.hastighetX << C.hastighetY; } sf::Packet &operator >>(sf::Packet &Packet, spelare &C) { return Packet >> C.ID >> C.hastighetX >> C.hastighetY; } //Funktioner för att köra programmet som en server eller som en klient void RunClient(unsigned short Port); void RunServer(unsigned short Port); //--------------------------------------------------------- // Programstart //--------------------------------------------------------- int main (int argc, char **argv) { //Början av programkörningen char Who = 'Z'; //Avgör om man skall vara klient eller server float ElapsedTime = 0.0f; //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer //-------------------------------------------------------- // Visa upp lokal adress, troligen 192.168.x.x eller 10.100.x.x eller 169.254.x.x //------------------------------------------- sf::IPAddress Address1 = sf::IPAddress::GetLocalAddress(); std::string IP1 = Address1.ToString(); std::cout<<"Din egen lokala IP-adress = " << IP1 << std::endl << std::endl; //Skapa paket sf::Packet ToSend;//Paketet skapas för att skicak uppgifter sf::Packet Received; //Paket att ta emot uppgifter skapas //Skapa en port som vi skall använda till vår socket //(portarna < 1024 är reserverade) const unsigned short Port = 2435; // Client eller server ?------------------------------------------------ std::cout << "Vill du vara server ('s') eller en klient ('c') ? "; std::cin >> Who; if (Who == 's') RunServer(Port); else RunClient(Port); //Skapa inte spelfönstret innan anslutning skett sf::RenderWindow App(sf::VideoMode(800, 600, 32), "Test - nätverksspel"); //Skapa en pojke spelare pojke; //pojke blir en kopia av klassen spelare pojke.hastighetX = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.hastighetY = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.ID=0; //Skapa en ko spelare ko; //ko blir en kopia av klassen spelare ko.hastighetX = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. ko.hastighetY = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. ko.ID=1; //Skapa bildhållarna sf::Image pojkbild; sf::Image kobild; //Ladda in bildfilerna pojkbild.LoadFromFile("boy.png"); kobild.LoadFromFile("cow.png"); //Dela ut bild till ko ko.sprite.SetImage(kobild); ko.sprite.SetPosition(500,500); //Dela ut bild till pojke pojke.sprite.SetImage(pojkbild); pojke.sprite.SetPosition(100,100); while(App.IsOpened()) { //Gameloop ElapsedTime=App.GetFrameTime(); //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer sf::Event Event; while (App.GetEvent(Event)) // Ta hand om händelser { //while 2 if (Event.Type == sf::Event::Closed) //kryssat på [x] symbolen? stäng programmet App.Close(); if (Event.Type == sf::Event::KeyPressed) // En tangent har tryckts ner { //if 1 if (Event.Key.Code == sf::Key::Escape) // ESC tangenten = stäng programmet App.Close(); } //slut if 1 } //slut, while 2 if (Who == 's') { //Du är en server //X-led värden if (App.GetInput().IsKeyDown(sf::Key::Left)) pojke.hastighetX= -100; if (App.GetInput().IsKeyDown(sf::Key::Right)) pojke.hastighetX= 100; //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Left) && !App.GetInput().IsKeyDown(sf::Key::Right)) pojke.hastighetX= 0; if (App.GetInput().IsKeyDown(sf::Key::Up)) pojke.hastighetY= -100; if (App.GetInput().IsKeyDown(sf::Key::Down)) pojke.hastighetY= 100; //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Up) && !App.GetInput().IsKeyDown(sf::Key::Down)) pojke.hastighetY= 0; } //Du är en server if ( Who != 's') { //Du är en klient //X-led värden if (App.GetInput().IsKeyDown(sf::Key::Left)) ko.hastighetX= -100; if (App.GetInput().IsKeyDown(sf::Key::Right)) ko.hastighetX= 100; //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Left) && !App.GetInput().IsKeyDown(sf::Key::Right)) ko.hastighetX= 0; if (App.GetInput().IsKeyDown(sf::Key::Up)) ko.hastighetY= -100; if (App.GetInput().IsKeyDown(sf::Key::Down)) ko.hastighetY= 100; //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Up) && !App.GetInput().IsKeyDown(sf::Key::Down)) ko.hastighetY= 0; } //Du är en klient //Flytta spelpjäserna pojke.sprite.Move(pojke.hastighetX * ElapsedTime,pojke.hastighetY * ElapsedTime); ko.sprite.Move(ko.hastighetX * ElapsedTime,ko.hastighetY* ElapsedTime); //Rita ut ändringarna på skärmen App.Clear(); App.Draw(pojke.sprite); App.Draw(ko.sprite); App.Display(); } //Gameloop return 0; } //slut på programkörningen void RunClient(unsigned short Port) { // Fråga efter serveradressen sf::IPAddress ServerAddress; do { std::cout << "Skriv in IP adress eller namn att ansluta till : "; std::cin >> ServerAddress; } while (!ServerAddress.IsValid()); // Skapa en TCP socket för att samarbeta med servern sf::SocketTCP Client; // Anslut till servern if (Client.Connect(Port, ServerAddress) != sf::Socket::Done) return; std::cout << "Ansluten till server " << ServerAddress << std::endl; sf::Packet RegularPacket; if (Client.Receive(RegularPacket) != sf::Socket::Done) return; spelare C1; if (RegularPacket >> C1) { std::cout << "Character received from the server (regular packet) : " << std::endl; std::cout << C1.ID << "ID, " << C1.hastighetX << " hastighet x, " << C1.hastighetY << " hastighet y" << std::endl; } } //Serverfunktionerna void RunServer(unsigned short Port) { // Skapa en TCP socket för att kommunicera med klienterna sf::SocketTCP Server; // Lyssna på en port för inkommande anrop if (!Server.Listen(Port)) return; std::cout << "Servern lyssnar på port " << Port << ", väntar på anslutning... " << std::endl; // Väntar på kontakt sf::IPAddress ClientAddress; sf::SocketTCP Client; Server.Accept(Client, &ClientAddress); std::cout << "Klient ansluten : " << ClientAddress << std::endl; spelare C1 = {1, 100, 100}; sf::Packet RegularPacket; RegularPacket << C1; if (Client.Send(RegularPacket) != sf::Socket::Done) return; std::cout << "Spelare skickad till klienten: " << std::endl; std::cout << C1.ID << "ID, " << C1.hastighetX << " hastighet x, " << C1.hastighetY << " hastighet y," << std::endl; }
Skrotkod här nedanför!
[redigera]//Originalkod från SFML hemsida. //Bearbetad för att driva ett enkelt spel och inte bara skicka ett ord fram och tillbaka #include <iostream> #include <SFML/System.hpp> #include <SFML/Graphics.hpp> #include <SFML/Window.hpp> #include <SFML/Network.hpp> #include <vector> // using namespace std; #define SFML_STATIC //Se till så att det inte behövs extra DLL-filer //Skapa den klass som spelarna pojken och kon kommer från class spelare { public: sf::Uint8 ID; //0=pojke, 1=ko sf::Int32 hastighetX; //Hastighet i X-led, kan vara negativ sf::Int32 hastighetY; //Hastighet i Y-led, kan vara negativ sf::Sprite sprite; //Bilden som representerar pojken eller kon }; // Server = styr pojken // Client = styr ko //-------------------------------------------------------------- //Skapa paketen som skall skickas mellan datorerna //-------------------------------------------------------------- sf::Packet &operator <<(sf::Packet &Packet, const spelare &C) { return Packet << C.ID << C.hastighetX << C.hastighetY; } sf::Packet &operator >>(sf::Packet &Packet, spelare &C) { return Packet >> C.ID >> C.hastighetX >> C.hastighetY; } // Skapa en global TCP socket sf::SocketTCP spelsocket; //Funktioner för att köra programmet som en server eller som en klient void RunClient(unsigned short Port); void RunServer(unsigned short Port); void PlayerSend(class spelare &s); void PlayerRecieve( class spelare &s); //--------------------------------------------------------- // Programstart //--------------------------------------------------------- int main (int argc, char **argv) { //Början av programkörningen char Who = 'Z'; //Avgör om man skall vara klient eller server float ElapsedTime = 0.0f; //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer spelsocket.SetBlocking(false); //Tar emot utan avbrott //-------------------------------------------------------- // Visa upp lokal adress, troligen 192.168.x.x eller 10.100.x.x eller 169.254.x.x //------------------------------------------- sf::IPAddress Address1 = sf::IPAddress::GetLocalAddress(); std::string IP1 = Address1.ToString(); std::cout<<"Din egen lokala IP-adress = " << IP1 << std::endl << std::endl; //Skapa en port som vi skall använda till vår socket //(portarna < 1024 är reserverade) const unsigned short Port = 2435; // Client eller server ?------------------------------------------------ std::cout << "Vill du vara server ('s') eller en klient ('c') ? "; std::cin >> Who; if (Who == 's') RunServer(Port); //Vänta else RunClient(Port); //Anslut till server //Skapa inte spelfönstret innan anslutning skett sf::RenderWindow App(sf::VideoMode(800, 600, 32), "Test - nätverksspel"); //Skapa en pojke spelare pojke; //pojke blir en kopia av klassen spelare pojke.hastighetX = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.hastighetY = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.ID=0; //Skapa en ko spelare ko; //ko blir en kopia av klassen spelare ko.hastighetX = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. ko.hastighetY = 0; //grundhastighet, skall multipliceras med ”ElapsedTime ”. ko.ID=1; //Skapa bildhållarna sf::Image pojkbild; sf::Image kobild; //Ladda in bildfilerna pojkbild.LoadFromFile("boy.png"); kobild.LoadFromFile("cow.png"); //Dela ut bild till ko ko.sprite.SetImage(kobild); ko.sprite.SetPosition(500,500); //Dela ut bild till pojke pojke.sprite.SetImage(pojkbild); pojke.sprite.SetPosition(100,100); while(App.IsOpened()) { //Gameloop ElapsedTime=App.GetFrameTime(); //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer sf::Event Event; while (App.GetEvent(Event)) // Ta hand om händelser { //while 2 if (Event.Type == sf::Event::Closed) //kryssat på [x] symbolen? stäng programmet App.Close(); if (Event.Type == sf::Event::KeyPressed) // En tangent har tryckts ner { //if 1 if (Event.Key.Code == sf::Key::Escape) // ESC tangenten = stäng programmet App.Close(); } //slut if 1 } //slut, while 2 if (Who == 's') { //Du är en server //X-led värden if (App.GetInput().IsKeyDown(sf::Key::Left)) { pojke.hastighetX= -100; PlayerSend(pojke); //Skicka ut var pojken är } if (App.GetInput().IsKeyDown(sf::Key::Right)) { pojke.hastighetX= 100; PlayerSend(pojke); //Skicka ut var pojken är } //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Left) && !App.GetInput().IsKeyDown(sf::Key::Right)) { pojke.hastighetX= 0; PlayerSend(pojke); //Skicka ut var pojken är } if (App.GetInput().IsKeyDown(sf::Key::Up)) { pojke.hastighetY= -100; PlayerSend(pojke); //Skicka ut var pojken är } if (App.GetInput().IsKeyDown(sf::Key::Down)) { pojke.hastighetY= 100; PlayerSend(pojke); //Skicka ut var pojken är } //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Up) && !App.GetInput().IsKeyDown(sf::Key::Down)) { pojke.hastighetY= 0; PlayerSend(pojke); //Skicka ut var pojken är } //Ta emot motspelarens värde PlayerRecieve(ko); } //Du är en server if ( Who != 's') { //Du är en klient //X-led värden if (App.GetInput().IsKeyDown(sf::Key::Left)) { ko.hastighetX= -100; PlayerSend(ko); //Skicka ut var kon är } if (App.GetInput().IsKeyDown(sf::Key::Right)) { ko.hastighetX= 100; PlayerSend(ko); //Skicka ut var kon är } //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Left) && !App.GetInput().IsKeyDown(sf::Key::Right)) { ko.hastighetX= 0; PlayerSend(ko); //Skicka ut var kon är } if (App.GetInput().IsKeyDown(sf::Key::Up)) { ko.hastighetY= -100; PlayerSend(ko); //Skicka ut var kon är } if (App.GetInput().IsKeyDown(sf::Key::Down)) { ko.hastighetY= 100; PlayerSend(ko); //Skicka ut var kon är } //Spring ingenstans om inte knapparna är nedtryckta if (!App.GetInput().IsKeyDown(sf::Key::Up) && !App.GetInput().IsKeyDown(sf::Key::Down)) { ko.hastighetY= 0; PlayerSend(ko); //Skicka ut var kon är } PlayerRecieve(pojke); } //Du är en klient //Flytta spelpjäserna pojke.sprite.Move(pojke.hastighetX * ElapsedTime,pojke.hastighetY * ElapsedTime); ko.sprite.Move(ko.hastighetX * ElapsedTime,ko.hastighetY* ElapsedTime); //Rita ut ändringarna på skärmen App.Clear(); App.Draw(pojke.sprite); App.Draw(ko.sprite); App.Display(); } //Gameloop return 0; } //slut på programkörningen void RunClient(unsigned short Port) { // Fråga efter serveradressen sf::IPAddress ServerAddress; do { std::cout << "Skriv in IP adress eller namn att ansluta till : "; std::cin >> ServerAddress; } while (!ServerAddress.IsValid()); //Socket spelsocket skapas som global variabel // Anslut till servern if (spelsocket.Connect(Port, ServerAddress) != sf::Socket::Done) return; std::cout << "Ansluten till server " << ServerAddress << std::endl; } //Serverfunktionerna void RunServer(unsigned short Port) { //Socket spelsocket skapas som global variabel // Lyssna på en port för inkommande anrop if (!spelsocket.Listen(Port)) return; std::cout << "Servern lyssnar på port " << Port << ", väntar på anslutning... " << std::endl; // Väntar på kontakt sf::IPAddress ClientAddress; sf::SocketTCP Client; spelsocket.Accept(Client, &ClientAddress); std::cout << "Klient ansluten : " << ClientAddress << std::endl; } //Skicka iväg informationen om spelaren till mottagaren void PlayerSend(class spelare &s) { sf::Packet ToSend; //Skapa paket ToSend << s.ID << s.hastighetX << s.hastighetY; //Fyll paketet if (spelsocket.Send(ToSend) != sf::Socket::Done) //Paketet skickas return; } void PlayerRecieve(class spelare &s) { sf::Packet Received; spelare R; if (spelsocket.Receive(Received) != sf::Socket::Done) return; if (Received >> R) { //Kopiera över värdena till angiven spelare s.hastighetX = R.hastighetX; s.hastighetY = R.hastighetY; } else {std::cout << "Nothing recieved: " << std::endl;} }