Tastenprellen?



  • Hallo liebe Forengemeinschaft,

    Ich heiße Oliver bin ein leidenschaftlicher, aber nicht ausgebildeter Programmierer.
    Ich habe meine Anfänge mit Assembler gemacht und arbeite nun im hobbybereich seit mehreren Jahren mit AVR-GCC und C++.
    Das Wissen habe ich mir hauptsächlich aus Büchern, Online-Tutorials und Try&Error angeeignet.

    Genug von mir. Nun zu meinem Problem.

    Ich habe ein kleines C++ Rollenspiel auf Konsole-Basis geschrieben. Den Spaghetti-Code möchte ich euch wegen Augenkrebs-Gefahr nicht unbedingt präsentieren...
    Eventuell ist es auch so lösbar.
    Das Rollenspiel bedient sich der Steuerung über Tastatur und nun habe ich das Problem, wenn ich zu oft eine Richtungs-Taste drücke, dann hängt es und der Befehl wird ununterbrochen ausgelöst.

    Aus dem embedded C Bereich würde man von Tastenprellen sprechen, aber in C++ ist mir das so noch nicht untergekommen. Pullups wird es hier wohl nicht geben. Ein Delay zum Entprellen (?) hat nicht funktioniert.

    Kann jemand vielleicht mit dieser Aussage etwas anfangen?



  • Die Frage ist so allgemein leider nicht zu beantworten, da nicht klar ist, wie du den Tastendruck erkennst.
    Auf welchem OS läuft das denn?



  • Windows 10, das Programm ist mit Visual Studio Community geschrieben.

    Der Code ist sehr umfangreich, weswegen ich ihn gerne auf die wichtigsten Elemente kürzen möchte. Weiß aber nicht welche Teile wichtig für eine Analyse wären.

    Das wäre der Navigations-Teil, eventuell erkennt man ja bereits meine "Logik" x.X

                        // Opening map
                        system("cls");
                        std::cout << "* World map *\n\nMove with W-A-S-D.\n";
                        std::cout << "You are here: ";
                        maptile();
    
                        // Game Loop
                        while (choiceMap) {
                            std::cout << "\nWhich direction do you want to go?\n";
                            char ctrl = ' ';
                            std::cin >> ctrl;
    
                            // W-A-S-D control
                            if (ctrl == 'w') {
                                system("cls");
                                //   std::cout << "north\n";
                            }
                            else if (ctrl == 'a') {
                                system("cls");
                                //    std::cout << "west\n";
                            }
                            else if (ctrl == 's') {
                                system("cls");
                                //    std::cout << "south\n";
                            }
                            else if (ctrl == 'd') {
                                system("cls");
                                //    std::cout << "east\n";
                            }
                            else if (ctrl == 'b') {
                                system("cls");
                                std::cout << "* Backpack *\n\n";
                                std::cout << "Gold: " << gold << "\n";
                                if (key == true) {
                                    std::cout << "Key:  " << key << "\n";
                                }
                                else {
                                    std::cout << "You have no items.\n";
                                }
    
                            }
                            else if (ctrl == 'p') {
                                system("cls");
                                std::cout << "* Profile *\n\n";
                                std::cout << "Name:  " << name << "\n";
                                std::cout << "Level: " << lvl << "\n";
                                std::cout << "Exp.:  " << xp << "\n\n";
                                std::cout << "Str:   " << str << "\n";
                                std::cout << "Dex:   " << dex << "\n";
                                std::cout << "Vit:   " << vit << "\n";
                                std::cout << "Life:  " << lifeCurrent << "\n";
                            }
                            else if (ctrl == 'x') {
                                system("cls");
                                choiceMap = false;
                                location = 101;
                                //    std::cout << "Exit Map.\n";
                            }
                            else {
                                system("cls");
                                std::cerr << "Wrong input, please try again.\n\n";
                            }
    
                            // Navigation on the map
                            if ((location == 101) && (ctrl == 'w')) {
                                std::cout << "You can't move there.\n";
                                ctrl = ' ';
                            }
                            if ((location == 101) && (ctrl == 'a')) {
                                std::cout << "You can't move there.\n";
                                ctrl = ' ';
                            }
                            if ((location == 101) && (ctrl == 's')) {
                                location = 201;
                                ctrl = ' ';
                                maptile();
                            }
                            if ((location == 101) && (ctrl == 'd')) {
                                location = 102;
                                ctrl = ' ';
                                maptile();
                            }
    


  • Zustandsautomat:

    2 Zustände: KeyIsDown, KeyIsUp
    Initialzustand ist KeyIsUp
    2 Ereignisse: Keydown, Keyup

    Transitionen:
    Kommt Keydown in Zustand KeyIsUp, mach, was passieren soll und nächster Zustand ist KeyIsDown
    Kommt Keyup in Zustand KeyIsDown, mach, was passieren soll und nächster Zustand ist KeyIsUp

    Keyup wird ignoriert in Zustand KeyIsUp
    Keydown wird ignoriert in Zustand KeyIsDown



  • Die Frage ist auch, wie sich dein Programm verhalten soll. Sollen bei gedrückter Taste die Eingabe wiederholt werden, oder soll jede Taste erst "losgelassen" werden und muss erneut gedrückt werden, um eine Aktion auszulösen?

    Das von dir beschriebene Verhalten kommt vermutlich daher, dass das OS schneller in den Eingabepuffer schreibt, als dein Programm sie auslesen und behandeln kann. Als Quick & Dirty Lösung kannst mal versuchen, alle Zeichen im Puffer zu löschen, bevor du das nächste Zeichen liest.
    Füge zwischen Zeile 9 und 10 mal folgende Zeile ein:

    std::cin.ignore( std::numeric_limits<std::streamsize>::max() );
    

    Eventuell musst du zusätzlich die beiden Header <limits> und <ios>inkludieren.



  • @cpp-n00b sagte in Tastenprellen?:

    std::cin >> ctrl;

    Das Problem ist ja, dass cin erst liest, wenn du Enter drückst - und dann alle Zeichen direkt nacheinander kommen. Das ist für ein Spiel also wohl keine gute Wahl. Auch das system("cls") ist nicht wirklich toll.

    Was tun stattdessen?

    Da es ja um eine Consolenapp unter Windows geht, da mal nachschauen:

    1. Tasten lesen: https://learn.microsoft.com/en-us/windows/console/readconsoleinput?redirectedfrom=MSDN
    2. Bildschirm löschen: https://learn.microsoft.com/en-us/windows/console/clearing-the-screen

    Vielleicht kannst du mal in die Richtung weitergucken.



  • @DocShoe

    Mit copy & paste in die besagte Stelle eingefügt und limits und ios nach einem Test inkludiert.
    Der Befehl wird nicht mehr ausgeführt. Das Programm bleibt "stehen". Dasselbe Verhalten wenn ich die Includes einfüge. Noch eine Idee? Ich mag es Quick & Dirty...

    @wob

    Danke für den Tipp, das werde ich mir anschließend mal durchlesen und schauen ob das besagtes Problem löst.



  • ich hab hier n paar uralte Schnipsel bei mir gefunden:

    void ClearScreen(void)
    {
        CONSOLE_SCREEN_BUFFER_INFO csbi;
        COORD target = {0, 0};
        DWORD written;
    
        GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
        FillConsoleOutputCharacter(GetStdHandle(STD_OUTPUT_HANDLE), ' ',
                                                csbi.dwSize.X * csbi.dwSize.Y,
                                                target, &written);
        FillConsoleOutputAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7,
                                                csbi.dwSize.X * csbi.dwSize.Y,
                                                target, &written);
    }
    
    struct taste
    {
       taste(int c, int k) : AsciiChar(c), VirtualKey(k){};
       int AsciiChar;
       int VirtualKey;
    };
    
    taste getInput()
    {
        INPUT_RECORD ir;
       
        DWORD dummy;
        do
        {
            ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &ir, 1, &dummy);
        }while(ir.EventType != KEY_EVENT || !ir.Event.KeyEvent.bKeyDown);
       
        return taste(ir.Event.KeyEvent.uChar.AsciiChar, ir.Event.KeyEvent.wVirtualKeyCode);
    }
    
    void consumeInput()
    {
        INPUT_RECORD ir;
       
        DWORD dummy = 1;
        
        while(dummy)
        {
            PeekConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &ir, 1, &dummy);
            if(dummy)
               ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &ir, 1, &dummy);
        }
    }
    

    ClearScreen leert den Bildschirm ohne das hässliche system(...)
    GetInput liest eine gedrückte Taste, ohne dass 'Enter' gedrückt werden muss
    consumeInput leert den Tastaturpuffer

    Vielleicht kannst Du was damit anfangen ...

    windows.h muss dafür eingebunden werden.



  • @cpp-n00b sagte in Tastenprellen?:

    @DocShoe

    Mit copy & paste in die besagte Stelle eingefügt und limits und ios nach einem Test inkludiert.
    Der Befehl wird nicht mehr ausgeführt. Das Programm bleibt "stehen". Dasselbe Verhalten wenn ich die Includes einfüge. Noch eine Idee? Ich mag es Quick & Dirty...

    @wob

    Danke für den Tipp, das werde ich mir anschließend mal durchlesen und schauen ob das besagtes Problem löst.

    Ja, war auch ein total bescheuerter Vorschlag von mir. Aus irgendwelchen Gründen bin ich davon ausgegangen, dass

    1. std::cin.ignore( std::numeric_limits<std::streamsize>::max() );den Eingabepuffer leert. Tut`s aber nicht.
    2. std::cin >> ctrl; ein einzelnes Zeichen liest und sofort zurückkehrt (also nicht auf Enter wartet)

    MIt deinem std::cinAnsatz wird das eher schwierig, ist es wirklich beabsichtigt, dass jede Eingabe mit Enter bestätigt werden soll?
    Nächster Quick&Dirty Vorschlag (dieses mal getestet ;)):

    char ch;
    std::cin >> ch; // Liest das erste Zeichen aus dem Eingabepuffer
    std::cin.clear(); // löscht die übrigen Zeichen aus dem Eingabepuffer
    

    Damit wird nur das erste Zeichen der Eingabe behandelt. Der Benutzer muss weiterhin die Eingabe mit Enter bestätigen, was dazu führen kann, dass sowas wie "wabc77uz0ä?" im Eingabepuffer steht, aber nur das erste Zeichen behandelt wird.
    Leider gibt es für dein Problem keine Standardlösung in C++, möglicherweise unterstützt dein Compiler die Funktionen getch() und kbdhit(), unter Umständen mit einem vorangestellten Underscore. Das sind aber keine Funktionen, die im C oder C++ Standard definiert sind.

    char ch = getch();
    

    Oder du benutzt, wie @wob schon vorgeschlagen hat, plattformspezifische Funktionen.

    Noch ein genereller Verbesserungshinweis:
    Trenne Funktionalitäten. Das Einlesen und Behandeln von Eingaben in einer Funktion ist unschön, das könnte man besser so umsetzen:

    void run_game_loop()
    {
       while( !terminated() )
       {
          char const ch = read_console_input();
          process_console_input( ch );
       }
    }
    

    PS:
    Üblicherweise möchte man nicht auf eine Benutzereingabe warten, sondern arbeitet mit einer ständig laufenden Schleife, die auch ohne Benutzereingabe weiterläuft, um div. andere Dinge erledigen zu können (NPCs tun irgendwas, die Welt verändert sich, Zeit läuft weiter, etc.) Das Ganze läuft dann vermutlich auch nicht einfach mehr unkontrolliert in einer Schleife, sondern ein Schleifendurchlauf wird in einem bestimmten, festen Intervall ausgeführt, z.b. alle 100ms. Für alle Ereignisse gibt es dann einen Countdown, bei dessen Ablauf dieses Ereignis behandelt wird und der Countdown zurückgesetzt wird.


Anmelden zum Antworten