Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?

[C++, SFML] Architektura serwera w grze sieciowej

Ostatnio zmodyfikowano 2015-02-24 12:36
Autor Wiadomość
Rughailon
Temat założony przez niniejszego użytkownika
[C++, SFML] Architektura serwera w grze sieciowej
» 2015-02-23 18:12:32
Witam. Mam pewien problem, który od jakiegoś czasu trzyma mnie w tym samym miejscu.

Tworzę sobie gierkę z wykorzystaniem protokołu TCP*, platformówkę inspirowaną grą "King Artur's Gold". Oczywiście gra nie będzie tak bardzo dynamiczna i rozbudowana, jak KAG, głównie chodzi o temat rozgrywki: 'dwie drużyny, które walczą między sobą'. W założeniach miałyby to być niewielkie bitwy 3vs3 + kilku obserwatorów, którzy jedynie by patrzeli, jak inni grają.

Nadszedł czas na dodanie ruchu. Postanowiłem zastosować enum z listą czynności i przypisywać do obiektu wartość przy kliknięciu w odpowiedni klawisz, który następnie wysyłam serwerowi. Czyli np. kliknięcie przycisku 'A' ustawi czynność na "MOVE" i kierunek na "LEFT"(enum z kierunkiem), a puszczenie klawisza ustawiałoby czynność na "STAND". Następnie serwer ustawia u siebie tą czynność i w jednej metodzie aktualizuje dane(pozycje) wszystkich klientów w sposób, jaki każe mu czynność(czyli np. ruch w lewo = (client[i].player).position=-(client[i].player).vX)), client również aktualizuje pozycję, ale co 1/3 sekundy wysyła zapytanie i sprawdza, czy pozycja jest taka sama, jak w serwerze. Przy okazji również odbiera pozycje innych klientów i je ustawia.

I tutaj rodzi się problem, ponieważ client posiada pętlę stałokrokową, która działa, jakby było 60 fps, a serwer posiada jedynie selector czekający 0.2 sekundy. W takim wypadku w serwerze pozycje będą się szybciej aktualizować, a nie mogę ograniczyć prędkości pętli, tak jak w kliencie, ponieważ selector zatrzymuje pętlę o 0.2 sekundy i szybkość aktualizowania również będzie się nie zgadzać z szybkością w klientach. Rozwiązaniem byłoby pewnie usunięcie selectora, ale w takim razie zmuszony byłbym na ustawienie socketów na tryb nieblokujący. Myślałem też o wrzuceniu metody odbierającej dane w osobny wątek, ale coś wtedy nie działa.

Co waszym zdaniem powinienem zrobić?

*Tak, wiem, że dynamiczne gierki powinny używać UDP. Dlatego postanowiłem swój projekt ograniczyć w dynamice, aby TCP wytrzymało.

KOD:
server.hpp - klasa serwera, trzymająca vector clientów i parę rzeczy.
C/C++
namespace rha {
    class cServer {
    private:
        //sf::TcpListener listener;
        //sf::SocketSelector selector;
       
        cNetworkManager manager;
        std::vector < cClient > vClients;
    public:
        //sf::TcpListener listener; //todo - test
        sf::SocketSelector selector;
       
        bool listening( sf::TcpListener * listener, unsigned int port );
        bool findNewConnection( sf::TcpListener * listener, tgui::Gui & gui );
        void serveClients( tgui::Gui & gui );
    };
}
server.cpp - Odbieranie danych w serwerze.
C/C++
void rha::cServer::serveClients( tgui::Gui & gui ) {
    tgui::ChatBox::Ptr cBoxCopy = gui.get( "ChatBox1" );
    tgui::ListBox::Ptr lBoxCopy = gui.get( "ListBox1" );
   
    if( !vClients.empty() ) {
        for( short i = 0; i < vClients.size(); ++i ) {
            if( selector.isReady( * vClients[ i ].socket ) ) {
                if( manager.receivePacket( vClients[ i ].socket ) ) {
                    switch( manager.getLastTypePacket() ) {
                        //...
                       
                    case rha::typePacketsInClient::QUESTION_CLIENT_DATA: { //ten pakiet wysyłany
                            sf::Int32 x, y; //jest co 1/3 sekundy, czyli 20 obiegów pętli.
                            if( manager.getLastPacket() >> x >> y ) {
                                if(( vClients[ i ].player ).action == cPlayer::MOVE ) {
                                    if(( vClients[ i ].player ).direction == cPlayer::RIGHT ) x =( vClients[ i ].player ).x +( vClients[ i ].player ).vX;
                                    else if(( vClients[ i ].player ).direction == cPlayer::LEFT ) x =( vClients[ i ].player ).x -( vClients[ i ].player ).vX;
                                   
                                    std::cout << x << " SEND " << y << std::endl;
                                   
                                    ( vClients[ i ].player ).x = x;
                                    ( vClients[ i ].player ).y = y;
                                }
                               
                                sf::Packet packet;
                                packet << rha::typePacketsInServer::INFO_CLIENTS_DATA <<( vClients[ i ].player ).x <<( vClients[ i ].player ).y;
                                //...
                               
                                manager.sendPacket( vClients[ i ].socket, packet ); break;
                            }
                        }
                    case rha::typePacketsInClient::QUESTION_PLAYER_ACTION: { //pakiet z czynnoscia
                            int converterAction, converterDirection;
                           
                            if( manager.getLastPacket() >> converterAction >> converterDirection ) {
                                ( vClients[ i ].player ).action = static_cast < cPlayer::eAction >( converterAction );
                                ( vClients[ i ].player ).direction = static_cast < cPlayer::eDirection >( converterDirection );
                            } break;
                        }
                    case rha::typePacketsInClient::STOP_PLAYER_ACTION: {( vClients[ i ].player ).action = cPlayer::STAND; break; } //pakiet z czynnoscia "STAND"
                        //case rha::typePacketsInServer::TEST:
                        //break;
                    }
                }
            }
        }
    }
}
client.hpp //Budowa klasy klienta, która trzyma też obiekt managera(do wysyłania i odbierania pakietów) i
C/C++
namespace rha { //obiekt postaci(pozycje, ilość hp, czynności itp).
    class cClient {
    public:
        enum eStatus { NOTCONNECT, WATCHING, PLAYING };
        std::vector < cOtherPlayer > vOtherPlayers;
        cPlayer player;
    private:
        cServerInfo server;
        //cPlayer player;
       
        std::string nick;
        eStatus status;
    public:
        cNetworkManager manager;
        sf::TcpSocket socket;
       
        cClient();
        void connect( sf::IpAddress ip, unsigned short port, std::string nick );
        bool login( std::string nick );
        bool join();
        void managePlayer( sf::Event event ); int test = 0;
        void updateAll( sf::RenderWindow & window );
        void drawAll( sf::RenderWindow & window );
        void disconnect();
       
        inline bool getStatus() { return status; }
    };
}
client.cpp - Ustawianie czynności i jej wysyłanie.
C/C++
void rha::cClient::managePlayer( sf::Event event ) {
    switch( event.type ) {
    case sf::Event::KeyPressed: {
            if(( event.key ).code == sf::Keyboard::A ||( event.key ).code == sf::Keyboard::D ) {
                player.setAction( cPlayer::eAction::MOVE );
               
                if(( event.key ).code == sf::Keyboard::A )
                     player.setDirection( cPlayer::eDirection::LEFT );
                else player.setDirection( cPlayer::eDirection::RIGHT );
               
            } else if(( event.key ).code == sf::Keyboard::W )
                 player.setAction( cPlayer::eAction::JUMP );
           
            if( status == PLAYING ) {
                sf::Packet packet;
                packet << rha::typePacketsInClient::QUESTION_PLAYER_ACTION << int( player.getAction() ) << int( player.getDirection() );
                if( !manager.sendPacket( & socket, packet ) ) disconnect(); //todo - test
               
            }
            break;
        }
    case sf::Event::KeyReleased: {
            if(( event.key ).code == sf::Keyboard::A ||( event.key ).code == sf::Keyboard::D )
                 player.setAction( cPlayer::eAction::STAND );
           
            if( status == PLAYING ) {
                if( !manager.sendRawPacket( & socket, rha::typePacketsInClient::STOP_PLAYER_ACTION ) ) disconnect(); //todo - test
               
            }
            break;
        }
    case sf::Event::MouseWheelMoved: {
            float x = player.view.getSize().x +( -( event.mouseWheel ).delta * 100 );
            float y = player.view.getSize().y +( -( event.mouseWheel ).delta * 100 );
            if( x < 800 && x > 100 && y < 600 && y > 75 )( player.view ).setSize( x, y ); break;
        }
        default: break;
    }
}
client.cpp - Aktualizacja gracza i odbieranie pakietów.
C/C++
void rha::cClient::updateAll( sf::RenderWindow & window ) {
    if( manager.receivePacket( & socket ) ) {
        switch( manager.getLastTypePacket() ) {
            //...
           
        case INFO_CLIENTS_DATA: {
                sf::Int32 x, y;
               
                if( manager.getLastPacket() >> x >> y ) {
                    player.registeredX = x; //registered to pozycja, która jest potwierdzona przez serwer. Jeśli klient
                    player.registeredY = y; //nie otrzyma tego pakietu, to pozycja gracza = pozycja registered.
                    player.x = x; player.y = y;
                    player.gX = x, player.gY = y; //A te pozycje są na razie tylko do testów.
                    break;
                }
            }
            //...
        }
    }
    ( player.textureAnimation ).setAnimation( 2 ); //todo - only test
    ( player.textureAnimation ).serveAnimation();
   
    /*if(player.action==cPlayer::MOVE){ //ten kawałek kodu odpowiada za to, aby
            if(player.direction==cPlayer::LEFT) player.gX+=player.vX/20; //pozycja gracza była powoli aktualizowana.
            else if(player.direction==cPlayer::RIGHT) player.gX-=player.vX/20; //Czyli wysyłam prośbę o pozycję co 1/3 sekundy.
        }*/ //Bez tego kodu wyświetlałbym gracza, który poruszałby się skokowo
    ( player.view ).setCenter( player.gX, player.gY ); //z pozycją aktualizowaną co 1/3 sekundy.
   
    test += 1;
    if( test >= 20 ) { //only test.
        player.performAction();
       
        sf::Packet packet;
        packet << rha::QUESTION_CLIENT_DATA << player.x << player.y;
        if( manager.sendPacket( & socket, packet ) );
       
        std::cout << "send: " << player.x << " " << player.y << std::endl;
       
        test = 0;
    }
    if( !vOtherPlayers.empty() ) {
        for( int i = 0; i < vOtherPlayers.size(); ++i ) {
            vOtherPlayers[ i ].performAction();
            ( vOtherPlayers[ i ].textureAnimation ).serveAnimation();
        }
    }
}
[/i]
P-127258
Pokropow
» 2015-02-23 18:18:03
Uważam, że najlepszym wyjściem byłoby właśnie wrzucenie metody odbierającej w oddzielny wątek. Nie wiem co ci wtedy nie działa, ale jeżeli zrobisz to poprawnie to powinno śmigać.
P-127259
Rughailon
Temat założony przez niniejszego użytkownika
» 2015-02-23 18:56:50
W serwerze kiedy wrzucam cały kod odpowiedzialny za odbieranie danych, to praca się zatrzymuje przy:
selector.wait(...);

Może w nowym wątku zastosować wątki nieblokujące. Nie wiem, czy to dobry pomysł. Ale spróbuję.
P-127260
Pokropow
» 2015-02-23 19:19:38
Nie radzę stosować wątków nieblokujących. Ja u siebie w serwerze robiłem odbieranie i tworzenie nowych połączeń w pętli głównej, ale robiłem tam tylko to, a wszystko inne było w oddzielnych wątkach (wszystko inne czyli wysyłanie danych do klientów). Wtedy oczekiwanie selektora mi nie przeszkadzało a serwer odzbierał dane kiedy otrzymywał je od klienta,a wysyłał wtedy kiedy klient chciał odebrać ( czyli wtedy kiedy wywołał metodę recv() ).
P-127262
Rughailon
Temat założony przez niniejszego użytkownika
» 2015-02-23 20:28:52
A nie lepiej byłoby odwrotnie zrobić: odbieranie w podrzędnym wątku, a wysyłanie i inne w głównym? W ogóle serwer działa w oknie SFML, a nie konsoli, dlatego też w pętli mam np. renderowanie elementów GUI, co przy jednoczesnym odbieraniu danych niepotrzebnie zwolni cały jej obieg.

Przed tym sposobem tylko ten selektor mnie "broni". Może lepiej naprawdę go usunąć?

P-127279
Pokropow
» 2015-02-23 21:27:56
Skoro twój serwer nie jest w konsoli to nie wiem, ale nie radzę usónąć selektora . Jest on przydatny, bo jak ustawisz sockety nieblokujące to trudniej będzie sprawdzić czy klient się wylogował.
P-127287
DejaVu
» 2015-02-24 01:19:30
Polecam przeczytać Ci tutorial SFML-a do obsługi komunikacji sieciowej TCP. Tam są gotowe, proste i jednocześnie wydajne narzędzia do poprawnej obsługi wielu połączeń. Ty obecnie próbujesz tworzyć pokraczną figurę tylko po to, aby uzyskać 'jakąś' komunikację, a za chwilę będziesz zadawał pytania 'jak zrobić aby działało lepiej'.
1. TCP to fatalne rozwiązanie dla dynamicznej komunikacji. Szybko się odbijesz od tej ściany.
2. Nie używaj protokołu TCP grach, które wysyłają więcej jak 2 pakiety na sekundę na gracza, bo po prostu gra będzie niegrywalna.
3. Jak już walczysz z komunikacją sieciową to poczytaj o niej. Oszczędzisz sobie czasu i zrobisz projekt lepiej. Kurs sieciowy SFML-a jest napisany prostym językiem i omawia jak łatwo używać narzędzi sieciowych.
P-127300
Rughailon
Temat założony przez niniejszego użytkownika
» 2015-02-24 12:36:15
@DejaVu
Pamiętam, że już kiedyś pisałeś o tym, że TCP też jest szybkie. Tutorial czytałem wiele razy.
Ale gdybym posiadał pakiety z małą zawartością danych, które byłyby wysyłane trochę wolniej, a klient po prostu po odebraniu pakietu sprawdzałby, czy pozycja, którą on aktualizuje jest równa pozycji odebranej. Obsługa kolizji byłaby również obsługiwana u gracza i na serwerze, ale z tą różnicą, że klient pytałby się co jakiś czas serwera o ich status i porównywałby ze swoim. To by było takie zabezpieczenie przed cheaterami. Wtedy mogłoby się to udać?

---

Stworzyłem osobne wątki z obsługą połączeń i odbieraniem danych. Selektor usunąłem, ponieważ w wątku coś nie działał i dodałem pętlę stałokrokową. Wszystko działa dobrze, a protokół TCP chyba da radę to wszystko obsłużyć.

serverManager.cpp
C/C++
void rha::cServerManager::running() {
    sf::TcpListener listener;
    tgui::Gui gui( window );
   
    sf::Clock clock; //for fixed step loop
    sf::Time timeOfUpdate = sf::Time::Zero;
    const sf::Time timeStep = sf::seconds( 1 / 60.f );
   
    gui.setGlobalFont( font );
    server.listening( & listener, 7415 );
    gui.loadWidgetsFromFile( "media/runForm.RhAf" );
    server.setGUIPtr( & gui );
   
    std::thread connectThread( & rha::cServer::findNewConnection, & server );
    std::thread receiveThread( & rha::cServer::receiveData, & server );
   
    while( state == RUN ) {
        sf::Time time = clock.restart();
       
        timeOfUpdate += time;
        while( timeOfUpdate > timeStep ) {
            timeOfUpdate -= timeStep;
            while( window.pollEvent( event ) ) {
                switch( event.type ) {
                case sf::Event::Closed:
                    state = END; break;
                default: break;
                }
                gui.handleEvent( event );
            } while( gui.pollCallback( callback ) ) {
                if( callback.id == 1 ) { state = WAIT; break; }
            }
           
            server.serveClients(); //zmiana pozycji graczy itp.
        }
       
        window.clear();
        gui.draw();
       
        window.display();
    }
}
P-127309
« 1 »
  Strona 1 z 1