C++ lernen, Beispiel: RPG (Console)



  • Hi,
    danke für die Tips! 🙂

    cooky451 schrieb:

    <stdlib.h> ist zwar (noch) gültig, aber üblicherweise nutzt man in C++ <cstdlib>, falls Funktionen daraus gebraucht werden. Was du ja auch einbindest, hast du vermutlich übersehen. Ansonsten sollte man mehr oder weniger nur die Header einbinden die man auch braucht, ich sehe dich z.B. nichts aus <cmath> benutzen. Das macht's einfach übersichtlicher. (Und es kompiliert uU schneller).

    <stdlib.h> <cstdlib> 👍

    Ich habe erstmal alle Header rein getan, die ich voraussichtlich brauchen werde/könnte, erspart mir später Errors.

    cooky451 schrieb:

    Du hast eine Funktion clear_screen() in der du zwischen den Systemen unterscheidest, was so eigentlich gut ist, aber dann benutzt du doch noch system("cls") in Zeile 35. Die << endl sind unnötig, benutze einfach \n. Technisch gesehen kannst du aus dem ganzen sogar einfach einen langen string machen.

    👍

    cooky451 schrieb:

    Es bietet sich zudem an eine Klasse für dein Spiel zu erstellen. (Von der du dann eine Instanz in der main erstellst.) Denn die Funkionen (dann Methoden) werden sich ja mit Sicherheit einige Variablen teilen wollen.

    Hilfe, Klassen 😕
    Wie mach ich das z.B.?

    class main_class{
        public:
    
        protected:
    
        private:
    };
    

    So?

    Edit: Was tu ich dann da wo rein?
    Und wofür verwende ich die Klasse dann im Verlauf des Codes?

    Edit2: Erstelle ich z.B. main_class main; in main() und tue alle Deklarationen meines Menüs bzw der direkten childs von main() dann in public: von main_class?

    cooky451 schrieb:

    Das exit(0) ist in C++ eine sehr, sehr schlechte Idee. Eines der wichtigsten Prinzipien von C++ (quasi das wichtigste) ist, dass Ressourcen in Destruktoren wieder freigegeben werden. Destruktoren werden aber nur aufgerufen, wenn du aus einem scope läufst. exit(0) beendet einfach das Programm. Bitte wenn möglich immer mit return beenden, also einfach aus dem main-scope laufen. (PS: main gibt in C++ automatisch 0 zurück, wenn man kein return statement angibt.)

    Der Code sieht jetzt so aus:

    #include <iostream>
    #include <string>
    #include <cmath>
    #include <algorithm>
    #include <iomanip>
    #include <vector>
    #include <fstream>
    #include <cstdlib>
    using namespace std;
    
        // Menü
        void menu();
        void new_char();
        void load_char();
        void settings();
        void help();
        void credits();
        bool exit_game();
    
        void clear_screen();
    
    class main_class{
        public:
    
        protected:
    
        private:
    };
    
    int main(){
    
        menu();
        // go directly to the menu
    
        return 0;
    }
    
    void menu(){
    // Menu screen
        bool exit = false;
        while (exit==false){
    
            clear_screen();
    
            cout <<
                "#########################\n\n"
                "\t[N]ew Character\n\n"
                "\t[L]oad Character\n\n"
                "\t[S]ettings\n\n"
                "\t[H]elp\n\n"
                "\t[C]redits\n\n"
                "\t[E]xit\n\n"
                "#########################\n\n";
    
                char choice;
                cin >> choice;
    
            switch(choice){
                case 'N':
                case 'n':
                    new_char();
                    break;
                case 'L':
                case 'l':
                    load_char();
                    break;
                case 'S':
                case 's':
                    settings();
                    break;
                case 'H':
                case 'h':
                    help();
                    break;
                case 'C':
                case 'c':
                    credits();
                    break;
                case 'E':
                case 'e':
                    exit = exit_game();
            }
        }
    }
    
    void clear_screen(){
    // clear the screen, depending on os
    
        #ifdef WINDOWS
        system("CLS");
    
        #else
        // assume linux
        system("clear");
    
        #endif
    }
    
    // ...
    
    bool exit_game(){
    // set exit true
        return(true);
    }
    

    Allerdings habe ich jetzt das Problem, dass die OS Unterscheidung für Windows "inaktiv" ist, aund nicht mehr ausgeführt wird. Stattdessen bekomme ich jetzt eine Fehlermeldung wegen "clear". 😕
    ➡ gelöst durch Editorwechsel CodeBlocks CodeLite



  • Ja, endl gibt den Puffer frei.
    Braucht man das denn? IdR nicht.
    Er wird sowieso alle x Zeichen, vor jedem cin/clog/cerr und beim Programmende geflusht.
    Auf den meisten Sytem bewirkt flusg zwar nur, dass die Zeichen vom streambuf Puffer in einen OS Puffer verschoben wird, aber...



  • ... aber? Du wolltest noch was sagen?



  • Da sollte eine Begründung hin, wieso man sich nicht auf Implementierungen verlassen soll.
    Aber ich ging davon aus, dass dir das klar ist, deshalb hab ich das weggelassen.



  • Nathan schrieb:

    Braucht man das denn? IdR nicht.

    Doch, wenn man sicherstellen will, dass der Benutzer das zuletzt Geschriebene auf der Konsole sieht.

    Du solltest nicht undifferenziert das Dogma von den anderen nachplappern, '\n' sei immer besser als std::endl . Ursprünglich war die Situation nämlich, dass Leute nur std::endl beigebracht bekamen und dann das auch in Datei-Streams, Logs und überall brauchten, wo es tatsächlich einen negativen Einfluss auf die Performance hat. Das ist das Problem, nicht ein endl nach einem Block von Konsolenausgaben.



  • Nexus schrieb:

    Nathan schrieb:

    Braucht man das denn? IdR nicht.

    Doch, wenn man sicherstellen will, dass der Benutzer das zuletzt Geschriebene auf der Konsole sieht.

    Wann will man denn, dass der User etwas sowieso sieht?
    -Vor Eingaben -> passiert sowieso
    -Vor Programmende -> passiert sowieso

    Ich benutze std::cout lediglich als Gegenstück für std::cin.
    Sprich, mit std::cout werden z.B. die Beschreibung für die nächste Eingabe o.ä. ausgeben oder Ergebnisse eines Programmes. Und in beiden Situationen wird kurz danach geflusht.
    Für Log/Fehlerausgaben nutz ich std::cerr/std::clog. Und die hab ich nicht gebuffert.
    std::endl brauch ich persönlich nicht.

    Du solltest nicht undifferenziert das Dogma von den anderen nachplappern, '\n' sei immer besser als std::endl .

    Das hab ich nie behauptet.



  • Nexus schrieb:

    Nathan schrieb:

    Braucht man das denn? IdR nicht.

    Doch, wenn man sicherstellen will, dass der Benutzer das zuletzt Geschriebene auf der Konsole sieht.

    Du solltest nicht undifferenziert das Dogma von den anderen nachplappern, '\n' sei immer besser als std::endl . Ursprünglich war die Situation nämlich, dass Leute nur std::endl beigebracht bekamen und dann das auch in Datei-Streams, Logs und überall brauchten, wo es tatsächlich einen negativen Einfluss auf die Performance hat. Das ist das Problem, nicht ein endl nach einem Block von Konsolenausgaben.

    Also wo braucht man endl?
    Braucht dieses Programm endl?
    Was kümmert es mich, daß die Leute damals endl beigebracht bekamen und es noch in allen schlechteren Fachbüchern steht?
    Du solltest nicht undifferenziert guten Ratschlägen widersprechen.



  • Also soweit ich das verstanden habe, ist \n einfacher zu benutzen in einem längeren Fließtext ohne Variablen etc., wohingegen endl überall da angewendet wird, wo \n mangels Textausgabe zu umständlich wäre. Kann man das so sagen? 😕

    Ich habe jetzt mein Programm unterteilt in mehrere .cpp + .h Files.
    Funktioniert soweit.

    Das Problem mit der OS Erkennung hat sich nur zum Teil gelöst. Windows wird immernoch nicht erkannt, stattdessen kommt eine Fehlermeldung wegen des Linux Befehls.
    Deshalb habe ich angefangen, unter dem Menüpunkt Settings die Möglichkeit zur Einstellung des OS zu geben.

    Ich habe versucht, eine settings.txt Datei zu schreiben/erstellen, die die Zeile

    OS=Windows
    

    enthält.
    Das klappt auch, allerdings scheitert es noch am Finden von "OS=" und Auslesen des nachfolgenden "Windows", und anschließender Ersetzung durch "Linux".
    Alle Tutorials zu dem Thema zeigen entweder nur die "Grundlagen" vom File-handling, oder sind so theoretisch dass ich nicht durchblicke.

    Kann mir jemand zeigen, wie ich das umsetzen kann?
    Mein Test-Code sieht bisher so aus:

    int main(){
    string a;
    	ofstream writefile;
    	writefile.open("test.txt");
    	writefile << "dies ist ein test\n"
    			"und noch ein test."
    			"os=windows"
    			"test"
    			"";
    	writefile.close();
    
    	ifstream readfile;
    	readfile.open("test.txt");
    	while(getline(readfile,a)){
    		size_t pos = str.find("OS=");
    
    	}
    	readfile.close();
    
    	system("pause");
    }
    

    Kennt jemand ein Tutorial, das anschaulich zeigt, wie man
    - string in einer datei suchen
    - string löschen
    - string einfügen
    kann?



  • Komplett falscher Ansatz.
    Du kannst nicht dynamisch das OS wechseln.
    Das Windows nicht erkannt wird, liegt wohl daran, dass dein Compiler kein entsprechendes Makro setzt.
    Siehe: http://sourceforge.net/p/predef/wiki/OperatingSystems/
    Probiers mal mit _WIN32.



  • Nathan schrieb:

    Komplett falscher Ansatz.
    Du kannst nicht dynamisch das OS wechseln.

    Inwiefern?

    Nathan schrieb:

    Probiers mal mit _WIN32.

    👍



  • origami schrieb:

    Nathan schrieb:

    Komplett falscher Ansatz.
    Du kannst nicht dynamisch das OS wechseln.

    Inwiefern?

    Du willst ja irgendwo aus einer Datei die Einstellung laden.
    Und dein clear-Code sieht dann so aus, richtig?

    void clear_screen(){
      if (os == windows)
        system("CLS");
      else
        system("clear");
    }
    

    In diesem Fall mag das ja noch funktionieren, aber nehmen wir mal an du machst das Löschen "richtig", nämlich über die API des OS:

    void clear_screen()
    {
      if (os == windows)
      {
        // Code grad von StackOverflow kopiert, keine Garantie und so
        COORD topLeft  = { 0, 0 };
        HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_SCREEN_BUFFER_INFO screen;
        DWORD written;
    
        GetConsoleScreenBufferInfo(console, &screen);
        FillConsoleOutputCharacterA(
            console, ' ', screen.dwSize.X * screen.dwSize.Y, topLeft, &written
        );
        FillConsoleOutputAttribute(
            console, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE,
            screen.dwSize.X * screen.dwSize.Y, topLeft, &written
        );
        SetConsoleCursorPosition(console, topLeft);
      }
      else
      {
        // Irgendwelche Linux-API Systemaufrufe, die ich nicht kenne
      }
    }
    

    Wenn du nun versuchst, dein Programm unter Linux zu kompilieren, wird sich der Compiler beschweren, da die ganzen Funktionen etc. nicht bekannt sind. Und umgekehrt unter Windows. Die OS Entscheidung muss zur Compiletime ablaufen.



  • Nathan schrieb:

    Wenn du nun versuchst, dein Programm unter Linux zu kompilieren, wird sich der Compiler beschweren, da die ganzen Funktionen etc. nicht bekannt sind. Und umgekehrt unter Windows. Die OS Entscheidung muss zur Compiletime ablaufen.

    Achso, ja ab einem bestimmten Entwicklungsstand geht das nicht mehr, klar. Aber grafisch will ich erstmal garnichts machen, nichtmal WindowsAPI.
    Farben wären zwar auch jetzt schon nett, aber farbiger Text bringt mir nichts, solange ich noch kein Spiel habe das Charaktere/Dateien erstellen/laden/verändern kann.

    Die "Beta" soll erstmal rein Konsolen-basiert sein. Auch die erste Version einer Karte soll rein ASCII-basiert sein, und auch von der bin ich noch weit entfernt.

    Ich will erstmal nur die Basics von C++ verstehen. Es steht im Moment weniger ein "echtes Spiel" im Vordergrund, als das C++ lernen.



  • Wenn du erstmal nur C++ lernen möchtest, dann programmiere ruhig erstmal platformabhängig. Mach dir keine Sorgen, ob das Programm auch unter Linux oder auf deinem Toaster läuft, lern erstmal die Sprache vernünftig. Wenn du ein bisschen weiter bist wirst du von alleine sehen, dass alles was du bisher geschrieben hast sich noch viel besser schreiben lässt.
    Und es ist eine sehr gute Idee dich erstmal auf die Konsole zu beschränken!



  • Biolunar schrieb:

    Wenn du ein bisschen weiter bist wirst du von alleine sehen, dass alles was du bisher geschrieben hast sich noch viel besser schreiben lässt.

    Vorschläge willkommen 🙂



  • Nun ja, Nathan hat insofern recht als dass es einfach üblich ist das OS zur Compilezeit zu unterscheiden. Was generell auch Sinn macht, weil du kannst eine für Windows kompilierte .exe eh nicht auf Linux ausführen. Was eine "Settings-Datei" angeht, da würde ich gar nicht erst versuchen irgendetwas in der Datei zu suchen, einfach komplett auslesen und am Ende wieder komplett überschreiben. Am besten so in der Art dass du eine Datei im Stil von

    setting1=value1
    setting2=value2

    etc. hast, dann liest du diese Key-Wert Paare einfach in eine std::map, und am Ende des Programms überschreibst du die Datei komplett mit neuen Key-Wert Paaren. (Damit alle Änderungen übernommen werden.) Ich gebe dir hier mal eine Klasse die ich irgendwann mal dafür benutzt habe, aber ich bin mir leider nicht sicher wie viel dir das hilft, denn du scheinst von Klassen und Templates noch nicht so viel Ahnung zu haben. Vielleicht lohnt es doch sich die Sprache noch ein bisschen weiter anzugucken. 😉

    #ifndef CONFIGURATION_CLASS_HPP
    #define CONFIGURATION_CLASS_HPP
    
    #include <cctype>
    #include <string>
    #include <map>
    #include <fstream>
    #include <sstream>
    
    class configuration
    {
    	bool use_file_;
    	std::string filename_;
    	std::map<std::string, std::string> config_;
    
    public:
    	configuration(std::string filename, bool use_file = true)
    		: use_file_(use_file)
    		, filename_(std::move(filename))
    	{
    		if (use_file_)
    		{
    			std::ifstream infile(filename_);
    			std::string key, value;
    			while (std::getline(infile, key, '=') && std::getline(infile, value))
    				set(to_lower(key), to_lower(value));
    		}
    	}
    
    	configuration(const configuration&) = delete;
    
    	configuration(configuration&& other)
    		: use_file_(other.use_file_)
    		, filename_(std::move(other.filename_))
    		, config_(std::move(config_))
    	{}
    
    	~configuration()
    	{
    		flush_to_file();
    	}
    
    	void flush_to_file() const
    	{
    		if (use_file_)
    		{
    			std::ofstream outfile(filename_);
    			for (auto& pr : config_)
    				outfile << pr.first << "=" << pr.second << '\n';
    		}
    	}
    
    	bool contains(const std::string& value) const
    	{
    		return config_.find(value) != config_.end();
    	}
    
    	bool get_string(std::string& value, const std::string& key) const
    	{
    		auto found = config_.find(to_lower(key));
    
    		if (found != config_.end())
    		{
    			value = found->second;
    			return true;
    		}
    
    		return false;
    	}
    
    	void set_string(const std::string& key, const std::string& value)
    	{
    		config_[to_lower(key)] = to_lower(value);
    	}
    
    	template <typename T>
    	bool get(T& value, const std::string& key) const
    	{
    		std::string str_value;
    		if (get_string(str_value, key))
    		{
    			std::stringstream ss;
    			ss << str_value;
    			ss >> value;
    			return ss.good();
    		}
    		return false;
    	}
    
    	template <typename T>
    	void set(const std::string& key, const T& value)
    	{
    		std::stringstream ss;
    		ss << value;
    		set_string(key, ss.str());
    	}
    
    	template <typename T>
    	bool get_or_set(T& value, const std::string& key)
    	{
    		if (!get(value, key))
    		{
    			std::stringstream ss;
    			ss << value;
    			set(key, ss.str());
    			return false;
    		}
    		return true;
    	}	
    
    private:
    	static std::string to_lower(std::string s)
    	{
    		for (auto& c : s)
    			c = static_cast<char>(std::tolower(c));
    
    		return s;
    	}
    };
    
    #endif
    
    #include <iostream>
    
    int main()
    {
    	configuration cfg("config.cfg");
    
    	int difficulty = 2;
    	double map_size = 1337.42;
    	std::string player_name = "Bob";
    
    	cfg.get_or_set(difficulty, "difficulty");
    	cfg.get_or_set(map_size, "map_size");
    	cfg.get_or_set(player_name, "player_name");
    
    	std::cout << "difficulty: " << difficulty << '\n';
    	std::cout << "map_size: " << map_size << '\n';
    	std::cout << "player_name: " << player_name << '\n';
    }
    

    Ist natürlich noch endless erweiterbar mit Kategorien etc., aber wie gesagt ist relativ kurz gehalten. Hab ich damals nur aus der Not heraus gebaut schnell irgendwas zu haben was funktioniert. Hoffe ich habe bei ein paar Änderungen keine Bugs eingebaut.



  • cooky451 schrieb:

    Was eine "Settings-Datei" angeht, da würde ich gar nicht erst versuchen irgendetwas in der Datei zu suchen, einfach komplett auslesen und am Ende wieder komplett überschreiben. Am besten so in der Art dass du eine Datei im Stil von

    setting1=value1
    setting2=value2

    etc. hast, dann liest du diese Key-Wert Paare einfach in eine std::map, und am Ende des Programms überschreibst du die Datei komplett mit neuen Key-Wert Paaren.

    Beim Charakter, ein paar Monstern und NPCs, und den Settings kommt nicht viel zusammen, aber mein vorrangiges Ziel ist ja nicht, möglichst schnell auf schlechte Programmier-Art ein Spiel auf die Beine zu stellen, sondern es gleich von Anfang an richtig zu machen.
    Mindestens bei der Map würde ich deshalb nur das Feld (+ vllt ein paar umliegende) in den Speicher laden, und die Tiles nur aufrufen, wenn der Charakter in die Nähe kommt.

    Das Programm müsste die Daten in einer bestimmten Zeile von/bis zu einem bestimmten Offset lesen.
    Ich bin mir sicher, dass so etwas nicht schwer zu programmieren ist, ich kenne nur einfach die Befehle dafür nicht. Man braucht eigentlich nur:
    - Datei öffnen (krieg ich hin)
    - Zeile lesen (krieg ich auch hin)
    - String auf eine bestimmte Zeichenfolge untersuchen (Settings)
    - Zeile von/bis zu einem festgelegten Offset einlesen (Map)
    - Daten innerhalb eines bestimmten Offset-Bereichs überschreiben
    - Daten an einem bestimmten Offset zwischen vorhandene Zeichen einfügen, ohne zu überschreiben
    - Datei schließen (krieg ich wieder hin)

    Bisher weiss ich zwar, wie ich in einer Zeile Zeichen einlesen kann, allerdings immer nur bis zum nächsten Whitespace.

    cooky451 schrieb:

    Ich gebe dir hier mal eine Klasse die ich irgendwann mal dafür benutzt habe, aber ich bin mir leider nicht sicher wie viel dir das hilft, denn du scheinst von Klassen und Templates noch nicht so viel Ahnung zu haben. Vielleicht lohnt es doch sich die Sprache noch ein bisschen weiter anzugucken. 😉

    Danke für den Code, ich werd mal versuchen, da durch zu steigen. Sieht auf den ersten Blick noch recht komplex aus.



  • Ich rede nur von einem config-file. Ein map-file ist etwas komplett anderes. 😉 Da muss man sich dann erst mal überlegen wie man das aufbauen will, welche Daten man woher bekommt, welche Daten man überhaupt braucht etc. da gibt es unendlich viele Möglichkeiten das aufzuziehen. Mit seekg kannst du übrigens bestimmte Teile einer Datei lesen. http://en.cppreference.com/w/cpp/io/basic_ifstream



  • Jau, habs hinbekommen, mit seek/tellg/p, setw(), substr(), find().
    Danke nochmal!

    Im Prinzip ist es also sinnvoll zu wissen, wie lang die Zeilen in einer Datei einmal sein werden, wenn man später an einzelnen Zeilen basteln will.

    Mal eine generelle Frage: Gibt es eine Länge, nach der bei C++ automatisch ein '\n' erfolgt, oder könnte ich eine einzige Zeile mit 10.000 Zeichen schreiben?



  • Nein, Zeilenumbrüche sind oft nur eine Konzession an die Lesbarkeit. Du kannst beliebig große Textdateien erzeugen, die nur vom verfügbaren Speicherplatz begrenzt sind.



  • DocShoe schrieb:

    _________________
    Die fünf häufigsten Anzeichen für Faulheit:
    1.

    😃

    Ich würde gerne eine "Sleep" Funktion aufrufen, aber keine der folgenden geht:
    Sleep()
    usleep()
    sleep()
    sleep_for()

    <chrono> <thread> <unisth.h> <ctime> und <windows.h> hab ich drin.
    Weiss jemand Rat?


Anmelden zum Antworten