diff --git a/.gitignore b/.gitignore index a868b20..b12b28d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,14 @@ compile_commands.json *.exe *.out +# Binary files +*.bin + # Libraries *.a *.lib SDL2 +SDL2_ttf # Ignore any backup files *~ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f374bf..1240192 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,17 +12,29 @@ if(WIN32) set(SDL2_LIBS "${SDL2_FOLDER}/lib/x64/SDL2.lib") set(SDL2_DLLS "${SDL2_FOLDER}/lib/x64/SDL2.dll") set(SDL2_DIR "${SDL2_FOLDER}/cmake/") - # On Linux, the library is found automatically if installed + + # Specify downloaded SDL2_ttf library folder location + set(SDL2_TTF_FOLDER "${CMAKE_SOURCE_DIR}/SDL2_ttf/") + + set(SDL2_TTF_INCLUDE_DIRS "${SDL2_TTF_FOLDER}/include") + set(SDL_TTF_LIBRARIES "${SDL2_TTF_FOLDER}/lib/x64/SDL2_ttf.lib") + set(SDL2_TTF_LIBS "${SDL2_TTF_FOLDER}/lib/x64/SDL2_ttf.lib") + set(SDL2_TTF_DLLS "${SDL2_TTF_FOLDER}/lib/x64/SDL2_ttf.dll") + set(SDL2_TTF_DIR "${SDL2_TTF_FOLDER}/cmake/") + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "${SDL2_TTF_FOLDER}") endif() +# Note: On Linux, the libraries are found automatically if installed # --- Locate SDL2 --- # Uses SDL2_DIR on Windows, on Linux it's found automatically -find_package(SDL2 REQUIRED) +find_package(SDL2 REQUIRED) +# --- Locate SDL2_ttf --- +find_package(SDL2_ttf REQUIRED) #--- Include directories --- -include_directories(${SDL2_INCLUDE_DIRS} ./include ./src) +include_directories(${SDL2_INCLUDE_DIRS} ${SDL2_TTF_INCLUDE_DIRS} ./include ./src) # --- Source files --- @@ -39,17 +51,72 @@ set(SOURCES src/map.c src/common.c src/sound.c + src/difficulty.c + src/files.c ) +#--- executable --- add_executable(Snake ${SOURCES}) -target_link_libraries(Snake ${SDL2_LIBRARIES}) + + +#--- link libraries --- +if(WIN32) + # Link libraries statically on Windows to prevent missing basic libraries on other systems. + target_link_options(Snake PRIVATE -static) + target_link_libraries(Snake ${SDL2_LIBRARIES} ${SDL_TTF_LIBRARIES}) +else() + target_link_libraries(Snake SDL2::SDL2 SDL2_ttf::SDL2_ttf) +endif() + + +# --- Copy assets to output folder --- +file(COPY ${CMAKE_SOURCE_DIR}/assets DESTINATION ${CMAKE_BINARY_DIR}) # --- Copy SDL2 DLLs to the output folder on Windows --- if(WIN32) - foreach(DLL ${SDL2_DLLS}) + foreach(DLL ${SDL2_DLLS} ${SDL2_TTF_DLLS}) add_custom_command(TARGET Snake POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${DLL} $) endforeach() +endif() + + + + +############################################ +######## generate release .zip file ######## +############################################ +# Generate a distributable ZIP archive for sharing the game. +# Note: currently only intended for windows systems +# Usage: +# 1. Build the project: cd build && cmake .. && make +# 2. Generate ZIP file: cpack +# 3. Share the ZIP file for others to run the game effortlessly. +if(WIN32) +# generator for zip archive TODO add other generator e.g. installer +set(CPACK_GENERATOR "ZIP") + +# Specify to exclude the top-level directory from the archive. +set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON) + +# Specify the components to be included in the package +install(TARGETS Snake + RUNTIME DESTINATION . + COMPONENT Runtime +) + +# copy DLL files +install(FILES ${SDL2_DLLS} ${SDL2_TTF_DLLS} + DESTINATION . + COMPONENT Runtime +) +# copy assets folder +install(DIRECTORY ${CMAKE_SOURCE_DIR}/assets + DESTINATION . + COMPONENT Assets +) + +include(CPack) endif() \ No newline at end of file diff --git a/README.md b/README.md index 7d540d4..de21bfc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@ -Development of the game Snake++ using C, C++ and SDK2 for the Software Engineering course as part of our studies. +# Snake++ +Development of the game "Snake" featuring portals, custom maps and sound effects. +Cross-platform compatibility using CMake, C, C++ and SDL2. +A project for the Software Engineering course as part of our studies. + +## Preview + + + +# Installation +For Windows 64-Bit a pre-compiled release, that also includes the required .dll files and assets is available: +- Download the zip file from [Releases](https://github.com/Jonny999999/snake-pp/releases/latest) (e.g. "Snake-1.0-win64.zip") +- Extract zip file +- Navigate to the folder and run `Snake.exe` +On other Systems you currently have to compile from source (see next sections) + # Compilation ## Linux **Install tools and SDL2** ```bash -pacman -S sdl2 sdl2_rrf +pacman -S sdl2 sdl2_ttf pacman -S cmake gcc ``` **Build** @@ -18,10 +33,16 @@ make ## Windows **Download SDL** -- Download `SDL2-devel-2.28.5-VC.zip` from https://github.com/libsdl-org/SDL/releases/tag/release-2.28.5 +- Download `SDL2-devel-2.28.5-VC.zip` from [github/libsdl-org](https://github.com/libsdl-org/SDL/releases/download/release-2.28.5/SDL2-devel-2.28.5-VC.zip) - Unzip the file and rename the folder to `SDL2` (avoid unnecessary subfolder) - Place it in the root folder of this repository. +**Download SDL_ttf** +- Download `SDL2_ttf-devel-2.20.2-VC.zip` from [github/libsdl-org](https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.20.2/SDL2_ttf-devel-2.20.2-VC.zip) +- Unzip the file and rename the folder to `SDL2_ttf` (avoid unnecessary subfolder) +- Place it in the root folder of this repository. + + **Install compiler** (if not available already) - download mingw: - visit https://altushost-swe.dl.sourceforge.net/project/mingw-w64/ @@ -40,17 +61,17 @@ See VS Code section # VS Code instructions ## Required extensions -- Cmake -- CmakeTools -- C/C++ +- [Cmake](https://open-vsx.org/extension/twxs/cmake) +- [CmakeTools](https://open-vsx.org/extension/ms-vscode/cmake-tools) +- [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) -## setup +## Setup - With CmakeTools installed open the project folder in VS Code - open cmd-prompt with `CTRL + SHIFT + P` run `cmake.build` - select kit (gcc or installed minGW compiler) Alternatively use cmake related buttons in bottom toolbar (CMake: Kit, Build, [all]...) -## compile +## Compile - Use buttons in bottom toolbar (CMake: ... Build [BUG-BUTTON] [PLAY-BUTTON] ... ) You can also use: - `ctrl-F5` run without debugger diff --git a/assets/fonts/CenturyGothic.ttf b/assets/fonts/CenturyGothic.ttf new file mode 100644 index 0000000..879704e Binary files /dev/null and b/assets/fonts/CenturyGothic.ttf differ diff --git a/assets/fonts/Prototype.ttf b/assets/fonts/Prototype.ttf new file mode 100644 index 0000000..c70bf00 Binary files /dev/null and b/assets/fonts/Prototype.ttf differ diff --git a/assets/fonts/Quirkus.ttf b/assets/fonts/Quirkus.ttf new file mode 100644 index 0000000..ec994fd Binary files /dev/null and b/assets/fonts/Quirkus.ttf differ diff --git a/assets/fonts/ZIPERHEA.ttf b/assets/fonts/ZIPERHEA.ttf new file mode 100644 index 0000000..96ce6dd Binary files /dev/null and b/assets/fonts/ZIPERHEA.ttf differ diff --git a/assets/sounds/crash_rock-cinematic.wav b/assets/sounds/crash_rock-cinematic.wav new file mode 100644 index 0000000..fb595c5 Binary files /dev/null and b/assets/sounds/crash_rock-cinematic.wav differ diff --git a/sounds/eat-bite1.wav b/assets/sounds/eat-bite1.wav similarity index 100% rename from sounds/eat-bite1.wav rename to assets/sounds/eat-bite1.wav diff --git a/sounds/eat-bite2.wav b/assets/sounds/eat-bite2.wav similarity index 100% rename from sounds/eat-bite2.wav rename to assets/sounds/eat-bite2.wav diff --git a/sounds/eat-crunch1.wav b/assets/sounds/eat-crunch1.wav similarity index 100% rename from sounds/eat-crunch1.wav rename to assets/sounds/eat-crunch1.wav diff --git a/sounds/eat-crunch2.wav b/assets/sounds/eat-crunch2.wav similarity index 100% rename from sounds/eat-crunch2.wav rename to assets/sounds/eat-crunch2.wav diff --git a/sounds/portal1_short.wav b/assets/sounds/portal1_short.wav similarity index 100% rename from sounds/portal1_short.wav rename to assets/sounds/portal1_short.wav diff --git a/sounds/portal2_oscillate.wav b/assets/sounds/portal2_oscillate.wav similarity index 100% rename from sounds/portal2_oscillate.wav rename to assets/sounds/portal2_oscillate.wav diff --git a/assets/sounds/portal3_in-out.wav b/assets/sounds/portal3_in-out.wav new file mode 100644 index 0000000..bf9bf19 Binary files /dev/null and b/assets/sounds/portal3_in-out.wav differ diff --git a/sounds/portal4_ramp.wav b/assets/sounds/portal4_ramp.wav similarity index 100% rename from sounds/portal4_ramp.wav rename to assets/sounds/portal4_ramp.wav diff --git a/sounds/space-gun.wav b/assets/sounds/space-gun.wav similarity index 100% rename from sounds/space-gun.wav rename to assets/sounds/space-gun.wav diff --git a/demo.jpg b/demo.jpg new file mode 100644 index 0000000..9ef9ad1 Binary files /dev/null and b/demo.jpg differ diff --git a/include/common.h b/include/common.h index 3b88384..d15f2b0 100644 --- a/include/common.h +++ b/include/common.h @@ -24,6 +24,21 @@ #define LOGI(format, ...) do {} while (0) #endif +//conditional logging when ERROR_OUTPUT_ENABLED is defined in config.h +//also prints in text in red color (windows not supported) +//example: LOGE("game: %d", count) +#ifdef ERROR_OUTPUT_ENABLED + #ifdef _WIN32 //print error output in default color (currently no color support for windows) + #define LOGE(format, ...) printf("[E] " format, ##__VA_ARGS__) + #else //print error output in red color + #define RED_TEXT "\033[1;31m" + #define RESET_TEXT "\033[0m" + #define LOGE(format, ...) printf(RED_TEXT "[E] " format RESET_TEXT, ##__VA_ARGS__) + #endif +#else + #define LOGE(format, ...) do {} while (1) +#endif + //=========================== //========== DELAY ========== diff --git a/include/config.h b/include/config.h index 58095a5..3e43dae 100644 --- a/include/config.h +++ b/include/config.h @@ -7,6 +7,7 @@ // logging settings //#define DEBUG_OUTPUT_ENABLED #define INFO_OUTPUT_ENABLED +#define ERROR_OUTPUT_ENABLED //#define RENDER_GAME_TO_CONSOLE diff --git a/include/difficulty.h b/include/difficulty.h new file mode 100644 index 0000000..2bbab35 --- /dev/null +++ b/include/difficulty.h @@ -0,0 +1,12 @@ +#pragma once +#include "config.h" + +//---------------------------------------------------------------- +// All functions/options regarding/affecting the difficulty level +// -> collected in one file for easy adjustment +//---------------------------------------------------------------- + + + +// set parameters for food placement by current difficulty level +void difficulty_getFoodPlacementParam(float * minDist, float * maxDist); \ No newline at end of file diff --git a/include/files.h b/include/files.h new file mode 100644 index 0000000..987a834 --- /dev/null +++ b/include/files.h @@ -0,0 +1,27 @@ +#pragma once +#include "config.h" + +#define MAX_PRINTED_SCORES 10 + +extern int recordsInFile; + +// struct that store player score at the end of the game +typedef struct playerScore_t +{ + int score; + char playerName[30]; + int difficulty; + char map[30]; +} playerScore_t; + +// global struct for storing the 10 best players after reading it from the file (defined in files.c) +extern playerScore_t topScores[MAX_PRINTED_SCORES]; + +// function which saves score in a .bin file +void savePlayerScore(const char *filename); + +// function which reads the 10 best finisher from the .bin file +void readTopScores(const char *filename); + +// counts records in file +int countRecordsInFile(const char *filename); \ No newline at end of file diff --git a/include/game.h b/include/game.h index e34e021..9a971d8 100644 --- a/include/game.h +++ b/include/game.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "snake.h" #include "config.h" @@ -31,10 +32,15 @@ typedef struct gameData_t gameState_t gameState; // state the game is in } gameData_t; + + + + // global struct for storing all game data (defined in game.c) extern gameData_t game; + // run once at game start and does the following: // - init snake // - load map @@ -50,4 +56,5 @@ void handlePortals(); //(ran in gameCycle) // - moves snake to next position // - handles collision, portals, food // - triggers frame update (render.c) -void runGameCycle(); \ No newline at end of file +void runGameCycle(); + diff --git a/include/map.h b/include/map.h index 8c6dc0f..2468ca1 100644 --- a/include/map.h +++ b/include/map.h @@ -39,7 +39,7 @@ void updateBlockSizePx(); // search and load map by name in storedMaps[] (map.c) // stops program when map not found! -void loadMapByName(char *name); +void loadMapByName(const char *name); //load map by passed definition diff --git a/include/menu.h b/include/menu.h index 0f8e4c5..086960d 100644 --- a/include/menu.h +++ b/include/menu.h @@ -1,6 +1,67 @@ #pragma once +#include #include "SDL.h" +#include "SDL_ttf.h" +#include "common.h" + +#define MAX_COLOURS 10 +#define MAX_LINES_TFF 20 //max lines (of ttf) for the whole project +#define MAX_LINES_STARTSCREEN 6 +#define MAX_LINES_SETTINGS 14 +#define MAX_LINES_INFOSCREEN 17 + +#define TEXT_INPUT_SIZE 30 +#define TIME_BETWEEN_UPDATE_MENU 1000 // [ms] + +// Enum that defines the active menu +typedef enum menus_t +{ + NONE = 0, + START, + SETTINGS, + INFOSCREEN, + LEADERBOARD, + PAUSE +} menus_t; + +// Struct that include pointer for ttl-functions in menu.c and render.c +typedef struct tllData_t +{ + TTF_Font *ptrFont_20; + TTF_Font *ptrFont_30; // used by menu.c and render.c + TTF_Font *ptrFont_200; // used by menu.c and render.c + SDL_Surface *textSurface; // used by menu.c and render.c + SDL_Texture *textTexture; // used by menu.c and render.c + SDL_Texture *textTextures[MAX_LINES_TFF]; + SDL_Color textColour[MAX_COLOURS]; // colour in which the text is printed + const int fontSize_20; + const int fontSize_30; + const int fontSize_200; + int64_t lastTimeStep; + int lineHeight; // to print text in middle + int totalHeight; // to print text in middle + int textPrintPosition; // where first line is printed + const time_t cycleDuration; // time between blinking ENTER + bool showEnter; // ENTER should be printed only every second cycle + int inputStatus; // 1 if player name was entered; 2 if difficulty level was entered, 3 map was entered + char textInput[TEXT_INPUT_SIZE]; // auxiliary variable for user input + char numbers[2][TEXT_INPUT_SIZE]; // auxiliary variable to store entered textInput-number into local pointer 'textLinesInMenu' in render.c + char userName[TEXT_INPUT_SIZE]; // user name + int userDifficultyLevel; // difficulty level which was entered by user + int userSelectedMap; // map which was entered by user +} ttlData_t; + +extern ttlData_t ttlStorage; + +extern menus_t activeMenu; + + + + +// edit various menus +// is called up in main.cpp +void manageMenu(); void showStartScreen(); //zum Starten Enter drücken @@ -14,6 +75,12 @@ void showLeaderboard(); void showSettings(); //optional //startet Settungs-Menü + +// show info screen +void showInfoScreen(); + void menuHandleInput(SDL_Event event); //als Übergabeparameter: int(?) event -> welcher Datentyp hängt von SDL ab //switch case für welcher Modus //switch case für welche Taste gedrückt wurde + + diff --git a/include/render.h b/include/render.h index fd09479..1cbed2b 100644 --- a/include/render.h +++ b/include/render.h @@ -1,7 +1,9 @@ #pragma once +#include #include "game.h" #include "snake.h" +#include "menu.h" #include "SDL.h" void renderGame(); @@ -9,4 +11,12 @@ void renderGame(); int CreateSDLWindow(); -void DestroySDLWindow(); \ No newline at end of file +void DestroySDLWindow(); + +void renderStartMenu(); + +void renderSettings(); + +void renderInfoScreen(); + +void renderLeaderboard(); \ No newline at end of file diff --git a/sounds/crash_rock-cinematic.wav b/sounds/crash_rock-cinematic.wav deleted file mode 100644 index e5871b7..0000000 Binary files a/sounds/crash_rock-cinematic.wav and /dev/null differ diff --git a/sounds/portal3_in-out.wav b/sounds/portal3_in-out.wav deleted file mode 100644 index 95821ed..0000000 Binary files a/sounds/portal3_in-out.wav and /dev/null differ diff --git a/src/config.c b/src/config.c index e482e95..4da2d06 100644 --- a/src/config.c +++ b/src/config.c @@ -8,7 +8,7 @@ config_t config = { .cycleDurationMs = 400, .difficulty = 1, .snakeDefaultLength = 2, - .leaderboardFilename = "", + .leaderboardFilename = "player_scores.bin", //.defaultMapName = "default" //10x10 .defaultMapName = "intermediate" //15x15 //.defaultMapName = "empty" //20x10 diff --git a/src/difficulty.c b/src/difficulty.c new file mode 100644 index 0000000..2b6979a --- /dev/null +++ b/src/difficulty.c @@ -0,0 +1,33 @@ +#include "difficulty.h" + +//---------------------------------------------------------------- +// All functions/options regarding/affecting the difficulty level +// -> collected in one file for easy adjustment +//---------------------------------------------------------------- + + +//====================================== +//== difficulty_getFoodPlacementParam == +//====================================== +// set parameters for food placement by current difficulty level +void difficulty_getFoodPlacementParam(float *minDist, float *maxDist) +{ + switch (config.difficulty) + { + default: + case 0: + case 1: //EASY/default: always place far away + *minDist = 3.0; // new food has to be at least minDist blocks away from any object + *maxDist = 5.0; // new food has to be closer than maxDist to an object + break; + case 2: //MEDIUM: place next to OR further away + *minDist = 1.0; + *maxDist = 4.0; + break; + case 3: //HARD: always place next to block + *minDist = 1.0; + *maxDist = 1.0; + break; + } + return; +} diff --git a/src/files.c b/src/files.c new file mode 100644 index 0000000..74bd736 --- /dev/null +++ b/src/files.c @@ -0,0 +1,146 @@ +#include "game.h" +#include "menu.h" +#include "files.h" + + + +//global struct for storing all data of the 10 best players +playerScore_t topScores[]; + +int recordsInFile; + +//========================== +//==== savePlayerScores ==== +//========================== + void savePlayerScore(const char *filename/*int score, int difficulty, const char *playerName, const char *map*/) + { + playerScore_t playerScore; + + // copy data into struct + playerScore.score = game.snake.length - config.snakeDefaultLength; + playerScore.difficulty = config.difficulty; + strcpy(playerScore.playerName, ttlStorage.userName); + //strcpy(playerScore.map, "testmap"); + + strcpy(playerScore.map, storedMaps[ttlStorage.userSelectedMap - 1]->name); + + + FILE *file; + // open file + file = fopen(filename, "ab"); + + // write data in file + if (file != NULL) + { + fwrite(&playerScore, sizeof(playerScore_t), 1, file); + fclose(file); + + LOGI("Datei: Spielergebnis erfolgreich in %s gespeichert.\n", config.leaderboardFilename); + } + else + { + LOGE("file: Fehler beim Öffnen der Datei!\n"); + } +} + + +//========================== +//==== readTopScores ====== +//========================== +// number of reads depends on 'MAX_PRINTED_SCORES' +void readTopScores(const char *filename) +{ + FILE *filePtr; + playerScore_t tempPlayerScore; + int highestPlayerScore = 0; + int count = 0; // increase up to 'MAX_PRINTED_SCORES' + + // determine the number of contents in the file + recordsInFile = countRecordsInFile(filename); + // if failure + if(recordsInFile == -1) + { + game.gameState = EXIT; + return; + } + + filePtr = fopen(filename, "rb"); + + // fail with file opening + if (filePtr == NULL) + { + LOGE("Datei: Fehler beim Öffnen der Datei für die besten 10 Ergebnisse!\n"); + game.gameState = EXIT; + return; + } + + + LOGI("Datei: Datensaetze in Datei: %d\n", recordsInFile); + + + //---- search for the highest score------ + for (int i = 0; i < recordsInFile; i++) + { + fread(&tempPlayerScore, sizeof(playerScore_t), 1, filePtr); + if(tempPlayerScore.score > highestPlayerScore) + { + highestPlayerScore = tempPlayerScore.score; + } + } + + //--- decrease highest score ----- + while((count < MAX_PRINTED_SCORES) && (count < recordsInFile)) + { + // set file pointer to start of the file + rewind(filePtr); + // search for the highest score and then save it in topScores + for (int i = 0; i < recordsInFile; i++) + { + // read record from the file + fread(&tempPlayerScore, sizeof(playerScore_t), 1, filePtr); + + // current highscore found + if(tempPlayerScore.score == highestPlayerScore) + { + topScores[count] = tempPlayerScore; + LOGI("Datei: score: %d name: %s schwierigkeit: %d map: %s\n", topScores[count].score, topScores[count].playerName, topScores[count].difficulty, topScores[count].map); + count++; + } + // leave if limit is reached + if(count >= recordsInFile || count >= MAX_PRINTED_SCORES) + { + break; + } + } + highestPlayerScore--; + } + fclose(filePtr); +} + + + +//========================== +//==== readTop10Scores ===== +//========================== +// return the number of various scores of the file +int countRecordsInFile(const char *filename) +{ + FILE *file; + file = fopen(filename, "rb"); + + // failure + if (file == NULL) + { + LOGI("Datei: Fehler beim Öffnen der Datei!\n"); + return -1; + } + + fseek(file, 0, SEEK_END); // Gehe zum Dateiende + long fileSize = ftell(file); // Hole die Größe der Datei in Bytes + fclose(file); + + int recordSize = sizeof(playerScore_t); + int numberOfRecords = fileSize / recordSize; // Berechne die Anzahl der Datensätze + + return numberOfRecords; +} diff --git a/src/food.c b/src/food.c index 92c3060..42652ea 100644 --- a/src/food.c +++ b/src/food.c @@ -5,6 +5,7 @@ #include "common.h" #include "map.h" #include "game.h" +#include "difficulty.h" @@ -56,7 +57,7 @@ newValues: { //decrease min distance but not below 1 if ((in_minDist -= 0.1) < 1) in_minDist = 1; - LOGI("[WARN] food: too much tries achieving min dist -> loosen limit to %.1f\n", in_minDist); + LOGI("[WARN] food: too many tries achieving min dist -> loosen limit to %.1f\n", in_minDist); } //reset stored distance and reroll coordinates minActualDistance = MAX_MAP_SIZE; @@ -87,14 +88,14 @@ newValues: void placeFood() { //--- config --- - static const float minDist = 3; // new food has to be at least minDist blocks away from any object - float maxDist = 5; // new food has to be closer than maxDist to an object + float minDist; // new food has to be at least minDist blocks away from any object + float maxDist; // new food has to be closer than maxDist to an object static const int maxTries = 25; // each maxTries the limit maxDist get loosened (prevents deadlock) - // TODO calculate this range using configured difficulty level - // e.g. in hard difficulty the maxDist could be <2 so it is always placed close to a collision + // get food placement parameters according to difficulty (difficulty.c) + difficulty_getFoodPlacementParam(&minDist, &maxDist); //--- variables --- - int foodX, foodY, triesMax = 0, triesMin; + int foodX, foodY, triesMax = 0, triesMin, triesTotal = 0; float currentMinDist; //--- generate random food position within min/max range --- @@ -106,15 +107,17 @@ void placeFood() if (triesMax % maxTries == 0) { maxDist += 0.1; - LOGI("[WARN] food: too many tries for MAX_DIST -> loosen limits to max=%.1f\n", maxDist); + LOGI("[WARN] food: too many tries achieving max dist -> loosen limits to max=%.1f\n", maxDist); } // generate random coordinates respecting minimum distance to objects getRandomPositionWithMinDistance(&foodX, &foodY, ¤tMinDist, &triesMin, minDist); + triesTotal += triesMin; //restart when max distance limit exceeded } while (currentMinDist > maxDist); //--- update position --- - LOGI("food: placed food at x=%d, y=%d (took %d = %d*%d tries)\n", foodX, foodY, triesMax * triesMin, triesMax, triesMin); + LOGI("food: placed food at x=%d, y=%d (took %d tries)\n", foodX, foodY, triesTotal); + LOGD("food: tries for constraints: Max=%d Min(last)=%d Total=%d)\n", triesMax, triesMin, triesTotal); game.foodX = foodX; game.foodY = foodY; return; diff --git a/src/game.c b/src/game.c index 562f9ff..d4617a7 100644 --- a/src/game.c +++ b/src/game.c @@ -5,6 +5,7 @@ #include "food.h" #include "render.h" #include "sound.h" +#include "files.h" // global struct for storing all game data @@ -16,16 +17,16 @@ gameData_t game = { .mapIsLoaded = false, .lifesRemaining = 1, .timestampLastCycle = 0, - .gameState = RUNNING, + .gameState = MENU, }; // list of audio files randomly played when food eaten const char *eatSounds[] = { - "../sounds/eat-bite1.wav", - "../sounds/eat-bite2.wav", - "../sounds/eat-crunch1.wav", - "../sounds/eat-crunch2.wav"}; + "assets/sounds/eat-bite1.wav", + "assets/sounds/eat-bite2.wav", + "assets/sounds/eat-crunch1.wav", + "assets/sounds/eat-crunch2.wav"}; #define EAT_SOUNDS_COUNT 4 @@ -43,7 +44,9 @@ void gameInit() //load default map if no map loaded yet if (!game.mapIsLoaded){ //loadMapByName("default"); - loadMapByName(config.defaultMapName); + + //loadMapByName(config.defaultMapName); + loadMap(*storedMaps[ttlStorage.userSelectedMap - 1]); } //----- snake ----- @@ -51,8 +54,8 @@ void gameInit() snakeInit(); //TODO assign return value to game.snake? //--- place initial food --- + LOGI("game: placing initial food\n"); placeFood(); - LOGI("game: placed initial food at x=%d, y=%d\n", game.foodX, game.foodY); } @@ -73,11 +76,11 @@ void handlePortals() snakeSetHeadPos(p.targetX, p.targetY); LOGI("game: entered portal i=%d at x=%d, y=%d -> set head to x=%d y=%d\n", i, p.posX, p.posY, p.targetX, p.targetY); //--- play sound --- - //playSoundAsync("../sounds/portal1_short.wav"); //too short - //playSoundAsync("../sounds/portal2_oscillate.wav"); //too much bass - //playSoundAsync("../sounds/space-gun.wav"); //too loud - playSoundAsync("../sounds/portal3_in-out.wav"); - //playSoundAsync("../sounds/portal4_ramp.wav"); + //playSoundAsync("assets/sounds/portal1_short.wav"); //too short + //playSoundAsync("assets/sounds/portal2_oscillate.wav"); //too much bass + //playSoundAsync("assets/sounds/space-gun.wav"); //too loud + playSoundAsync("assets/sounds/portal3_in-out.wav"); + //playSoundAsync("assets/sounds/portal4_ramp.wav"); return; } } @@ -105,11 +108,19 @@ void runGameCycle() //--- handle collision --- //collision with map object or snake tail if (checkCollides(game.map, game.snake.headX, game.snake.headY) || !snakeIsAlive()){ - // show leaderboard when collided // TODO consider game.lifesRemaining and reset if still good? - LOGI("game: collided with wall or self! => show leaderboard\n"); + //--- play crash sound --- + LOGI("game: collided with wall or self\n"); + playSound("assets/sounds/crash_rock-cinematic.wav", false); + DELAY(200); + //--- leaderboard --- //game.gameState = MENU; //TODO add config.collisionEnabled option? - showLeaderboard(); + LOGI("game: saving player score\n"); + savePlayerScore(config.leaderboardFilename/*(game.snake.length - config.snakeDefaultLength), ttlStorage.userName, config.difficulty, *storedMaps[ttlStorage.userSelectedMap - 1]*/); + readTopScores(config.leaderboardFilename); + LOGI("game: showing leaderboard\n"); + game.gameState = MENU; + activeMenu = LEADERBOARD; return; } @@ -124,7 +135,7 @@ void runGameCycle() // NOTE: order of place and grow is relevant, otherwise function in food.c will access invalid memory placeFood(); snakeGrow(); -} + } //--- update frame --- renderGame(); @@ -132,4 +143,4 @@ void runGameCycle() printMap(game.map); //render game to console #endif return; -} \ No newline at end of file +} diff --git a/src/input.c b/src/input.c index c1adfff..9479969 100644 --- a/src/input.c +++ b/src/input.c @@ -1,3 +1,5 @@ +#include +#include "render.h" #include "input.h" #include "common.h" #include "SDL.h" @@ -25,6 +27,8 @@ void handleInput_runningState(SDL_Event event) case SDLK_p: // p: pause case SDLK_ESCAPE: + case SDLK_SPACE: + LOGI("input: pausing game\n"); game.gameState = PAUSED; showPauseScreen(); break; @@ -50,19 +54,19 @@ void handleInput_runningState(SDL_Event event) snakeSetDir(RIGHT); break; - case SDLK_m: // m: cycle through maps - rotateMapNext(); - break; + // case SDLK_m: // m: cycle through maps + // rotateMapNext(); + // break; - case SDLK_2: // 2: speed up game by increment - config.cycleDurationMs -= sqrt(config.cycleDurationMs) + 1; - if (config.cycleDurationMs < 20) - config.cycleDurationMs = 20; - break; + // case SDLK_2: // 2: speed up game by increment + // config.cycleDurationMs -= sqrt(config.cycleDurationMs) + 1; + // if (config.cycleDurationMs < 20) + // config.cycleDurationMs = 20; + // break; - case SDLK_1: // 1: slow down game by increment - config.cycleDurationMs += 50; - break; + // case SDLK_1: // 1: slow down game by increment + // config.cycleDurationMs += 50; + // break; default: LOGD("input: key %d is not handled in RUNNING mode\n", event.key.keysym.sym); @@ -79,6 +83,7 @@ void handleInput_runningState(SDL_Event event) void processInputEvent() { SDL_Event event; + // loop through all queued input events that occoured since last run while (SDL_PollEvent(&event)) { @@ -102,12 +107,20 @@ void processInputEvent() handleInput_runningState(event); break; case PAUSED: + LOGI("input: resume game from paused state\n"); + game.gameState = RUNNING; case MENU: // pass key event to menu handle function which updates menu menuHandleInput(event); break; } } + //--- key text input --- + // not possible to make this check in the else if query before + else if(event.type == SDL_TEXTINPUT && game.gameState == MENU) + { + menuHandleInput(event); + } } return; diff --git a/src/main.cpp b/src/main.cpp index 079f4d0..d5ca543 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,48 +7,44 @@ extern "C" #include "common.h" #include "input.h" #include "render.h" +#include "menu.h" } -//initialize SDL window -//ruft showStartScreen -//initialize game -//main loop: processInputEvents, runGameCycle -//uninitialize SDL - - - -//========================== -//====== enabled test ====== -//========================== -//uncomment one test at a time to run the corresponding code in main() -//#define TEST__FOOD_PLACEMENT -//#define TEST__SDL_INPUT -#define TEST__GAME_WITH_CONSOLE_OUTPUT - - - +//1. initialize SDL window +//2. call showStartScreen +//3. initialize game +//4. main loop: processInputEvents, runGameCycle +//5. uninitialize SDL int main(int argc, char *argv[]) -{ - gameInit(); +{ + // gameInit(); moved to menu.c // Initialisiere SDL if (SDL_Init(SDL_INIT_VIDEO) < 0) { - printf("SDL konnte nicht initialisiert werden! SDL_Error: %s\n", SDL_GetError()); + LOGE("SDL: SDL konnte nicht initialisiert werden! SDL_Error: %s\n", SDL_GetError()); return 1; } + + // Initialisiere SDL_ttl, um Text ausgeben zu können + if (TTF_Init() == -1) { + LOGE("SDL: SDL_ttf konnte nicht initialisiert werden! SDL_Error: %s\n"); + return 1; + } + CreateSDLWindow(); - - - time_t now; now = GET_TIME_MS(); // Timer startet game.timestampLastCycle = now; int diff; while(game.gameState != EXIT) { + if(game.gameState == MENU) + { + manageMenu(); + } if (game.gameState == RUNNING) { now = GET_TIME_MS(); // Timer startet diff = now-game.timestampLastCycle; @@ -62,31 +58,6 @@ int main(int argc, char *argv[]) processInputEvent(); } -/* time_t t; - - long long ms = time(NULL) *1000; - game.timestampLastCycle = ms; - - printf("timestamp: %lld",game.timestampLastCycle); - printf("ms: %lld",ms); - - while(game.gameState != EXIT) { - if (game.gameState == RUNNING) { - ms = time(NULL) *1000; // Timer startet - - printf("ms2: %lld", ms); - - if (ms - game.timestampLastCycle > config.cycleDurationMs){ - - game.timestampLastCycle = ms; - runGameCycle(); - } - } - DELAY(5); //verhindert maximale Durchlaufgeschwindigkeit der Schleife - processInputEvent(); - }*/ - - DestroySDLWindow(); diff --git a/src/map.c b/src/map.c index 879c3c0..4eb5d8e 100644 --- a/src/map.c +++ b/src/map.c @@ -130,7 +130,7 @@ void updateBlockSizePx(){ //=========================== // search and load map by name in storedMaps[] (map.c) // stops program when map not found! -void loadMapByName(char *name) +void loadMapByName(const char *name) { // loop through all stored maps for (int i = 0; i < storedMapsCount; i++) @@ -145,7 +145,7 @@ void loadMapByName(char *name) } } // map not found - printf("[FATAL ERROR] map: could not find '%s' in storedMaps!\n", name); + LOGE("[FATAL] map: could not find '%s' in storedMaps!\n", name); game.gameState = EXIT; return; } @@ -224,7 +224,7 @@ static const map_t map_default = { static const map_t map_empty = { .width = 20, - .height = 15, + .height = 20, .name = "empty", .collisions = {}, .collisionCount = 0, diff --git a/src/menu.c b/src/menu.c index 04c0227..70287eb 100644 --- a/src/menu.c +++ b/src/menu.c @@ -3,41 +3,292 @@ #include "sound.h" #include "common.h" #include -//#include +#include +#include "render.h" -void showStartScreen(){ - LOGI("menu: showing start-screen\n"); - game.gameState = RUNNING; + +//define global struct for tllStorage values +//default values defined here: (note: gets modified by render.c or menu.c) +ttlData_t ttlStorage = +{ + .fontSize_20 = 20, // size of font + .fontSize_30 = 30, // size of font + .fontSize_200 = 200, // size of font + .cycleDuration = 500, // time between show and not showing ENTER in start screen + .showEnter = false, + .inputStatus = 0, + .textInput[0] = '\0' + + +}; + +// Default +menus_t activeMenu = START; + +// is called by main function if game.gameState == MENU +// choose between the selected menu functions +void manageMenu() +{ + switch(activeMenu) + { + case START: + showStartScreen(); + break; + + case SETTINGS: + showSettings(); + break; + + case INFOSCREEN: + showInfoScreen(); + break; + + case LEADERBOARD: + showLeaderboard(); + break; + + case PAUSE: + showPauseScreen(); + break; + } +} + + +// shows start screen with blinking ENTER +void showStartScreen() + +{ + LOGD("menu: showing start-screen\n"); + + time_t now = GET_TIME_MS(); + + // is used to make ENTER blink + if(now > (ttlStorage.lastTimeStep + ttlStorage.cycleDuration)) + { + ttlStorage.showEnter = !ttlStorage.showEnter; + renderStartMenu(); + } return; } -void showLeaderboard(){ - LOGI("menu: showing leaderboard\n"); + +void showLeaderboard() +{ + LOGD("menu: showing leaderboard\n"); + //--- play crash sound --- //play audio file, wait until playback is finished //note: when displaying actual leaderboard, the second parameter should be 'false' to not block the program - playSound("../sounds/crash_rock-cinematic.wav", true); - DELAY(100); - //--- quit game --- - game.gameState = EXIT; + renderLeaderboard(); + return; + + } -void showPauseScreen(){ - LOGI("menu: showing leaderboard\n"); +void showPauseScreen() +{ + LOGD("menu: showing leaderboard\n"); game.gameState = PAUSED; return; } -void showSettings(){ - LOGI("menu: showing settings\n"); + +void showSettings() +{ + LOGD("menu: showing settings\n"); + + + time_t now = GET_TIME_MS(); + + // is used to make ENTER blink + if(now > (ttlStorage.lastTimeStep + ttlStorage.cycleDuration)) + { + ttlStorage.showEnter = !ttlStorage.showEnter; + renderSettings(); + } + return; } + +void showInfoScreen() +{ + LOGD("menu: showing info-screen\n"); + + time_t now = GET_TIME_MS(); + + // is used to make ENTER blink + if(now > (ttlStorage.lastTimeStep + ttlStorage.cycleDuration)) + { + ttlStorage.showEnter = !ttlStorage.showEnter; + renderInfoScreen(); + } + + return; +} + +// handle input over keyboard +// delete text at the end of one menu section void menuHandleInput(SDL_Event event){ //compare 'handleInput_runningState(SDL_Event event)' in input.c + + int numberOfContents = 0; // count number of entered numbers by the user + + switch(activeMenu) + { + // start + case START: + switch (event.key.keysym.sym) + { + case SDLK_q: // q: quit + game.gameState = EXIT; + break; + + case SDLK_RETURN: // Enter key + activeMenu = SETTINGS; + ttlStorage.lastTimeStep = 0; + // delete text + for (int i = 0; i < MAX_LINES_TFF; ++i) + { + SDL_DestroyTexture(ttlStorage.textTextures[i]); + } + } + break; + + // settings + case SETTINGS: + switch(event.key.keysym.sym) + { + // case SDLK_q: // q: quit + // game.gameState = EXIT; + // break; + + case SDLK_F1: // go to info screen + activeMenu = INFOSCREEN; + ttlStorage.lastTimeStep = 0; + // delete text + for (int i = 0; i < MAX_LINES_TFF; ++i) + { + SDL_DestroyTexture(ttlStorage.textTextures[i]); + } + break; + + case SDLK_RETURN: // confirm user input or start game + + switch(ttlStorage.inputStatus) + { + case 0: // confirm user name + // user must enter at least one letter + if(ttlStorage.textInput[0] != '\0') + { + ttlStorage.inputStatus++; + strcpy(ttlStorage.userName, ttlStorage.textInput); // copy textInput to userName + memset(ttlStorage.textInput, 0, sizeof(ttlStorage.textInput)); // clear textInput[] + } + break; + + case 1: // confirm difficulty level + // count number of entered numbers + while (ttlStorage.textInput[numberOfContents] != '\0' && numberOfContents < sizeof(ttlStorage.textInput)) + { + numberOfContents++; + } + + // user input must be between 1 and 3 + if((ttlStorage.textInput[0] > '0') && (ttlStorage.textInput[0] <= '3') && numberOfContents == 1) + { + ttlStorage.inputStatus++; + strcpy(ttlStorage.numbers[0], ttlStorage.textInput); // copy textInput to userDifficultyLevel + ttlStorage.userDifficultyLevel = ttlStorage.textInput[0] - '0'; // copy textInput to userDifficultyLevel + } + memset(ttlStorage.textInput, 0, sizeof(ttlStorage.textInput)); // clear textInput[] + break; + + case 2: // confirm map + // count number of entered numbers + while (ttlStorage.textInput[numberOfContents] != '\0' && numberOfContents < sizeof(ttlStorage.textInput)) + { + numberOfContents++; + } + + // user input must be between 1 and 3 + if((ttlStorage.textInput[0] > '0') && (ttlStorage.textInput[0] <= '3') && numberOfContents == 1) + { + ttlStorage.inputStatus++; + strcpy(ttlStorage.numbers[1], ttlStorage.textInput); // copy textInput to userSelectedMap + ttlStorage.userSelectedMap = ttlStorage.textInput[0] - '0'; // copy textInput to userSelectedMap + } + memset(ttlStorage.textInput, 0, sizeof(ttlStorage.textInput)); // clear textInput[] + break; + + case 3: // start game + activeMenu = NONE; + game.gameState = RUNNING; + // delete text + for (int i = 0; i < MAX_LINES_TFF; ++i) + { + SDL_DestroyTexture(ttlStorage.textTextures[i]); + } + + // initialize game + config.difficulty = ttlStorage.userDifficultyLevel; + config.cycleDurationMs = config.cycleDurationMs / sqrt(config.difficulty); + gameInit(); + + break; + } + break; + + + default: // for user inputs + // keyboard input + if (event.type == SDL_TEXTINPUT) + { + strcat(ttlStorage.textInput, event.text.text); + } + // delete single inputs via backspace + else if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_BACKSPACE && strlen(ttlStorage.textInput) > 0) + { + ttlStorage.textInput[strlen(ttlStorage.textInput) - 1] = '\0'; + } + break; + } + + break; + + // info screen + case INFOSCREEN: + switch(event.key.keysym.sym) + { + case SDLK_q: // q: quit + game.gameState = EXIT; + break; + + case SDLK_RETURN: // go return to settings + activeMenu = SETTINGS; + ttlStorage.lastTimeStep = 0; + // delete text + for (int i = 0; i < MAX_LINES_TFF; ++i) + { + SDL_DestroyTexture(ttlStorage.textTextures[i]); + } + } + break; + + case LEADERBOARD: + switch(event.key.keysym.sym) + { + case SDLK_q: // q: quit + case SDLK_RETURN: + game.gameState = EXIT; + } + break; + } + return; -} \ No newline at end of file +} + diff --git a/src/render.c b/src/render.c index 86a0f48..b782e73 100644 --- a/src/render.c +++ b/src/render.c @@ -4,9 +4,14 @@ #include "snake.h" #include "food.h" #include "config.h" +#include "menu.h" +#include "common.h" +#include "files.h" #include +#define NUM_COLUMNS 4 + void renderGame(){ SDL_SetRenderDrawColor(game.renderer, 0, 0, 0, 255); @@ -113,18 +118,470 @@ void renderGame(){ +//-------------------------------------------------------------- +//-----------------START MENU----------------------------------- +//-------------------------------------------------------------- +void renderStartMenu() +{ + //=========== only first loop ================ + if(ttlStorage.lastTimeStep == 0) + { + ttlStorage.ptrFont_200 = TTF_OpenFont("assets/fonts/Quirkus.ttf", ttlStorage.fontSize_200); + ttlStorage.ptrFont_30 = TTF_OpenFont("assets/fonts/Quirkus.ttf", ttlStorage.fontSize_30); + + + SDL_Color textColor1 = {255, 0, 255}; // rosa Text + SDL_Color textColor2 = {255, 200, 0}; // oranger Text + SDL_Color textColor3 = {0, 255, 0}; // grüner Text + SDL_Color textColor4 = {255, 0, 0}; // rot Text + SDL_Color textColor5 = {0, 255, 255}; // türkiser Text + SDL_Color textColor6 = {255, 255, 255}; // weißer Text + + ttlStorage.textColour[0] = textColor1; // rosa + ttlStorage.textColour[1] = textColor2; // orange + ttlStorage.textColour[2] = textColor3; // grün + ttlStorage.textColour[3] = textColor4; // rot + ttlStorage.textColour[4] = textColor5; // türkis + ttlStorage.textColour[5] = textColor6; // weiß + + // text in start screen + const char* textLines[] = { + "Snake++", + "In Pixeln bunt, sie schlaengelt flink,", + "Durch Mauern, Portale, ohne Blink.", + "Starte das Spiel, sei klug und schnell,", + "Die Schlange wartet, auf dein Pixel-Ziel!", + "-- ENTER --" + }; + + + // render game name (SNAKE++) bigger than poem + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_200, textLines[0], ttlStorage.textColour[0]); + ttlStorage.textTextures[0] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + + // render poem + for (int i = 1; i < (MAX_LINES_STARTSCREEN - 1); ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, textLines[i], ttlStorage.textColour[i]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + } + + // render ENTER + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, textLines[MAX_LINES_STARTSCREEN-1], ttlStorage.textColour[5]); + ttlStorage.textTextures[MAX_LINES_STARTSCREEN-1] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + } + + + //=========== is always performerd ================ + SDL_RenderClear(game.renderer); + + int textWidth, textHeight; + + // print game name + ttlStorage.textPrintPosition = (config.windowSize) / 9; // print position for game name (SNAKE++) + SDL_QueryTexture(ttlStorage.textTextures[0], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect = { (config.windowSize - textWidth) / 2, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[0], NULL, &dstRect); + + // print poem + ttlStorage.textPrintPosition = (config.windowSize) / 2.7; // change print position for poem + for (int i = 1; i < (MAX_LINES_STARTSCREEN-1); ++i) { + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + + SDL_Rect dstRect = { (config.windowSize - textWidth) / 2, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + //print ENTER every second cycle + if(ttlStorage.showEnter) + { + ttlStorage.textPrintPosition = (config.windowSize / 1.5); // print position for ENTER + SDL_QueryTexture(ttlStorage.textTextures[MAX_LINES_STARTSCREEN-1], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect1 = { (config.windowSize - textWidth) / 2, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[MAX_LINES_STARTSCREEN-1], NULL, &dstRect1); + } + + // update screen + SDL_RenderPresent(game.renderer); + ttlStorage.lastTimeStep = GET_TIME_MS(); + + return; +} + + +//-------------------------------------------------------------- +//-------------------SETTINGS----------------------------------- +//-------------------------------------------------------------- +void renderSettings() +{ + int textWidth, textHeight; // auxiliary variables for printing + + // text in menu settings + static char* textLinesInMenu[MAX_LINES_SETTINGS] = { + "Fuer Infos zum Gameplay, sowie einigen Shortcuts druecken Sie bitte F1", + " ", + " ", + "Bitte geben Sie ihren Nickname ein: ", + " ", + " ", + "Bitte geben Sie ein Schwierigkeitslevel ein:", + "1 fuer Beginner - 2 fuer Fortgeschrittener - 3 fuer Profi", + " ", + " ", + "Bitte waehlen Sie eine Map:", + "1 fuer Standard - 2 fuer Leer - 3 fuer Intermediate", + " ", + "-- ENTER --" + }; + + + //=========== only first loop ================ + if(ttlStorage.lastTimeStep == 0) + { + ttlStorage.ptrFont_20 = TTF_OpenFont("assets/fonts/Prototype.ttf", ttlStorage.fontSize_20); + SDL_StartTextInput(); // start text input + } + + + + //=========dependent of which text is currently entered(userName, difficultyLevel, userSelectedMap) ========= + switch(ttlStorage.inputStatus) + { + //=== no user inputs === + case 0: + //--- rendering --- + for (int i = 0; i < 4; ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLinesInMenu[i], ttlStorage.textColour[5]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + + } + + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, ttlStorage.textInput, ttlStorage.textColour[5]); + ttlStorage.textTextures[4] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + SDL_RenderClear(game.renderer); + + //--- printing --- + ttlStorage.textPrintPosition = 0; // first print position + for (int i = 0; i < 5; ++i) + { + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect = { 1, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + break; + + + // === one user input === + case 1: + textLinesInMenu[4] = ttlStorage.userName; + //--- rendering --- + for (int i = 0; i < 8; ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLinesInMenu[i], ttlStorage.textColour[5]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + + } + + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, ttlStorage.textInput, ttlStorage.textColour[5]); + ttlStorage.textTextures[8] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + SDL_RenderClear(game.renderer); + + //---printing --- + ttlStorage.textPrintPosition = 0; // first print position + for (int i = 0; i < 9; ++i) + { + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect = { 1, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + break; + + //=== two user inputs === + case 2: + textLinesInMenu[8] = ttlStorage.numbers[0]; + //--- rendering --- + for (int i = 0; i < 12; ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLinesInMenu[i], ttlStorage.textColour[5]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + + } + + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, ttlStorage.textInput, ttlStorage.textColour[5]); + ttlStorage.textTextures[12] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + SDL_RenderClear(game.renderer); + + //---printing --- + ttlStorage.textPrintPosition = 0; // first print position + for (int i = 0; i < 13; ++i) + { + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect = { 1, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + break; + + //=== user inputs completely + case 3: + textLinesInMenu[12] = ttlStorage.numbers[1]; + //--- rendering --- + for (int i = 0; i < (MAX_LINES_SETTINGS - 1); ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLinesInMenu[i], ttlStorage.textColour[5]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + + } + + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLinesInMenu[MAX_LINES_SETTINGS - 1], ttlStorage.textColour[5]); + ttlStorage.textTextures[MAX_LINES_SETTINGS - 1] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + SDL_RenderClear(game.renderer); + + //---printing --- + ttlStorage.textPrintPosition = 0; // first print position + for (int i = 0; i < (MAX_LINES_SETTINGS - 1); ++i) + { + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect = { 1, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + + //print ENTER every second cycle + if(ttlStorage.showEnter) + { + ttlStorage.textPrintPosition = (config.windowSize / 1.5); // print position for ENTER + SDL_QueryTexture(ttlStorage.textTextures[MAX_LINES_SETTINGS-1], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect1 = { (config.windowSize - textWidth) / 2, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[MAX_LINES_SETTINGS-1], NULL, &dstRect1); + } + break; + } + + // update screen + SDL_RenderPresent(game.renderer); + ttlStorage.lastTimeStep = GET_TIME_MS(); + + return; +} + +//-------------------------------------------------------------- +//-------------------INFO SCREEN-------------------------------- +//-------------------------------------------------------------- +void renderInfoScreen() +{ + //=========== only first loop ================ + if(ttlStorage.lastTimeStep == 0) + { + // text in start screen + const char* textLines[] = { + " STEUERUNG: W (nach oben)", + " A (nach links)", + " S (nach unten)", + " D (nach rechts) ", + " oder: Pfeiltasten ", + " ", + " ", + " PAUSE: p", + " oder: Leertaste", + " ", + " ", + "SPIEL VERLASSEN: q ", + " ", + " ", + " ", + "By Jonas Schoenberger, Johannes Graf und Julia Steinberger", + "-- ENTER --" + }; + + + // render setting screen + for (int i = 0; i < MAX_LINES_INFOSCREEN; ++i) + { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_20, textLines[i], ttlStorage.textColour[5]); + ttlStorage.textTextures[i] = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + SDL_FreeSurface(ttlStorage.textSurface); + } + } + + + //=========== is always performerd ================ + SDL_RenderClear(game.renderer); + + int textWidth, textHeight; + + // print settings + ttlStorage.textPrintPosition = 0; // first print position + for (int i = 0; i < (MAX_LINES_INFOSCREEN - 1); ++i) { + + SDL_QueryTexture(ttlStorage.textTextures[i], NULL, NULL, &textWidth, &textHeight); + + SDL_Rect dstRect = { 1, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[i], NULL, &dstRect); + ttlStorage.textPrintPosition += textHeight; // increase print position + } + + //print ENTER every second cycle + if(ttlStorage.showEnter) + { + ttlStorage.textPrintPosition = (config.windowSize / 1.5); // print position for ENTER + SDL_QueryTexture(ttlStorage.textTextures[MAX_LINES_INFOSCREEN-1], NULL, NULL, &textWidth, &textHeight); + SDL_Rect dstRect1 = { (config.windowSize - textWidth) / 2, ttlStorage.textPrintPosition, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTextures[MAX_LINES_INFOSCREEN-1], NULL, &dstRect1); + } + + + // update screen + SDL_RenderPresent(game.renderer); + ttlStorage.lastTimeStep = GET_TIME_MS(); + + return; +} + + +//-------------------------------------------------------------- +//-------------------RENDER LEADERBOARD------------------------- +//-------------------------------------------------------------- +void renderLeaderboard() +{ + + char* menuDescription[] ={"LEADERBOARD"}; + char* columnDescriptions[NUM_COLUMNS] = + { + "Score", + "Spieler", + "Schwierigkeitslevel", + "Map" + }; + + SDL_SetRenderDrawColor(game.renderer, 0, 0, 0, 255); + SDL_RenderClear(game.renderer); + + int rowHeight = config.windowSize / (MAX_PRINTED_SCORES * 10); // Höhe einer Zeile + int columnWidth = config.windowSize / NUM_COLUMNS; // Breite einer Spalte + int textWidth, textHeight; + + + // rendering 'LEADERBOARD' + + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, menuDescription[0], ttlStorage.textColour[5]); + ttlStorage.textTexture = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + + SDL_QueryTexture(ttlStorage.textTexture, NULL, NULL, &textWidth, &textHeight); + + SDL_Rect dstRect = {1 , 0, textWidth, textHeight }; + SDL_RenderCopy(game.renderer, ttlStorage.textTexture, NULL, &dstRect); + + SDL_FreeSurface(ttlStorage.textSurface); + SDL_DestroyTexture(ttlStorage.textTexture); + + + // rendering columns description + for (int i = 0; i < NUM_COLUMNS; ++i) { + ttlStorage.textSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, columnDescriptions[i], ttlStorage.textColour[5]); + ttlStorage.textTexture = SDL_CreateTextureFromSurface(game.renderer, ttlStorage.textSurface); + + SDL_Rect textRect; + textRect.x = i * columnWidth + (columnWidth - ttlStorage.textSurface->w) / 2 - 30; // Zentrieren + textRect.y = 50; // Y-Koordinate für die Spaltenbeschreibung + textRect.w = ttlStorage.textSurface->w; + textRect.h = ttlStorage.textSurface->h; + + SDL_RenderCopy(game.renderer, ttlStorage.textTexture, NULL, &textRect); + + SDL_FreeSurface(ttlStorage.textSurface); + SDL_DestroyTexture(ttlStorage.textTexture); + } + + // rendering score data + int maxCycles = (recordsInFile < MAX_PRINTED_SCORES) ? recordsInFile : MAX_PRINTED_SCORES; + for (int i = 0; i < maxCycles; ++i) { + char playerName[50]; // temporary buffer for text + char map[50]; // temporary buffer for text + strcpy(playerName, topScores[i].playerName); + strcpy(map, topScores[i].map); + + // player name + SDL_Surface* playerNameSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, playerName, ttlStorage.textColour[5]); + SDL_Texture* playerNameTexture = SDL_CreateTextureFromSurface(game.renderer, playerNameSurface); + + // map name + SDL_Surface* mapSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, map, ttlStorage.textColour[5]); + SDL_Texture* mapTexture = SDL_CreateTextureFromSurface(game.renderer, mapSurface); + + + // score + SDL_Surface *scoreSurface = NULL; + char numberScore[30]; // buffer for number 'score' + snprintf(numberScore, sizeof(numberScore), "%d", topScores[i].score); + scoreSurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, numberScore, ttlStorage.textColour[5]); + SDL_Texture *numberTexture1 = SDL_CreateTextureFromSurface(game.renderer, scoreSurface); + + // difficulty + SDL_Surface *difficultySurface = NULL; + char numberDifficulty[30]; // buffer for number 'difficulty' + snprintf(numberDifficulty, sizeof(numberDifficulty), "%d", topScores[i].difficulty); + difficultySurface = TTF_RenderText_Solid(ttlStorage.ptrFont_30, numberDifficulty, ttlStorage.textColour[5]); + SDL_Texture *numberTexture2 = SDL_CreateTextureFromSurface(game.renderer, difficultySurface); + + int numberS = 70; //x distance + int textPlayer = 220; //x distance + int numberD = 470; //x distance + int textMap = 620; //x distance + int y = 90 + i*30; //y distance + + SDL_Rect scoreRect = {numberS, y, scoreSurface->w, scoreSurface->h}; + SDL_Rect playerNameRect = {textPlayer, y, playerNameSurface->w, playerNameSurface->h }; + SDL_Rect difficultyRect = {numberD, y, difficultySurface->w, difficultySurface->h}; + SDL_Rect mapNameRect = {textMap, y, mapSurface->w, mapSurface->h }; + + SDL_RenderCopy(game.renderer, numberTexture1, NULL, &scoreRect); + SDL_RenderCopy(game.renderer, playerNameTexture, NULL, &playerNameRect); + SDL_RenderCopy(game.renderer, numberTexture2, NULL, &difficultyRect); + SDL_RenderCopy(game.renderer, mapTexture, NULL, &mapNameRect); + + SDL_FreeSurface(playerNameSurface); + SDL_FreeSurface(mapSurface); + SDL_FreeSurface(scoreSurface); + SDL_FreeSurface(difficultySurface); + + SDL_DestroyTexture(playerNameTexture); + SDL_DestroyTexture(mapTexture); + SDL_DestroyTexture(numberTexture1); + SDL_DestroyTexture(numberTexture2); + } + SDL_RenderPresent(game.renderer); + +} + + + int CreateSDLWindow(){ // Erstelle ein SDL-Fenster game.window = SDL_CreateWindow("Snake", 350, 50, config.windowSize, config.windowSize, SDL_WINDOW_OPENGL); if (game.window == NULL) { - printf("Fenster konnte nicht erstellt werden! SDL_Error: %s\n", SDL_GetError()); + LOGE("SDL: Fenster konnte nicht erstellt werden! SDL_Error: %s\n", SDL_GetError()); return 1; } game.renderer = SDL_CreateRenderer(game.window, -1, SDL_RENDERER_ACCELERATED); if (game.renderer == NULL) { - printf("Renderer konnte nicht erstellt werden! SDL_Error: %s\n", SDL_GetError()); + LOGE("SDL: Renderer konnte nicht erstellt werden! SDL_Error: %s\n", SDL_GetError()); return 1; } return 0; @@ -132,6 +589,15 @@ int CreateSDLWindow(){ void DestroySDLWindow(){ // Zerstöre das Fenster und beende SDL + + TTF_CloseFont(ttlStorage.ptrFont_20); + TTF_CloseFont(ttlStorage.ptrFont_30); + TTF_CloseFont(ttlStorage.ptrFont_200); + SDL_DestroyRenderer(game.renderer); SDL_DestroyWindow(game.window); -} \ No newline at end of file + + TTF_Quit(); + SDL_Quit(); +} + diff --git a/src/snake.c b/src/snake.c index 9520aa7..2a22497 100644 --- a/src/snake.c +++ b/src/snake.c @@ -1,6 +1,7 @@ #include "snake.h" #include "game.h" //for access to global 'game' struct +bool snakeSetDirectionIsAllowed = true; void snakeInit() { @@ -36,14 +37,16 @@ void snakeGrow() game.snake.length++; // tail part is attached left of last tail part // maybe problem while rendering --> MUST BE SOLVED THEN - game.snake.tail[game.snake.length][0] = game.snake.tail[game.snake.length - 1][0] - 1; - game.snake.tail[game.snake.length][1] = game.snake.tail[game.snake.length - 1][1]; + game.snake.tail[game.snake.length - 1][0] = game.snake.tail[game.snake.length - 2][0]; + game.snake.tail[game.snake.length - 1][1] = game.snake.tail[game.snake.length - 2][1]; return; } void snakeMove() { int i = game.snake.length - 1; // counter for snake moving + + snakeSetDirectionIsAllowed = true; // direction can be changed now // update head position automatically snakeUpdateHeadPos(); @@ -63,6 +66,12 @@ void snakeMove() void snakeSetDir(snakeDirection_t dir) { + // if direction mustn changed + if(!snakeSetDirectionIsAllowed) + { + return; + } + // check, if snake should be move in opposite direction -> new direction stays old direction switch(dir) { @@ -92,6 +101,7 @@ void snakeSetDir(snakeDirection_t dir) } game.snake.direction = dir; + snakeSetDirectionIsAllowed = false; // direction cannot updated until next snakeMove return; } diff --git a/src/sound.c b/src/sound.c index 7c724a9..b08e86e 100644 --- a/src/sound.c +++ b/src/sound.c @@ -27,7 +27,7 @@ int initAudio() //--- initialize SDL audio --- if (SDL_Init(SDL_INIT_AUDIO) < 0) { - printf("SDL: could not init audio!\n"); + LOGE("sound: SDL - could not init audio!\n"); return 1; } audioIsInitialized = true; @@ -66,7 +66,8 @@ int playSound(const char *filePath, bool wait) //--- load file --- if (SDL_LoadWAV(filePath, &wavSpec, &wavBuffer, &wavLength) == NULL) { - printf("sound: file '%s' could not be loaded E:'%s'\n", filePath, SDL_GetError()); + LOGE("sound: file '%s' could not be loaded E:'%s'\n", filePath, SDL_GetError()); + pthread_mutex_unlock(&mutexSoundPlaying); return 1; } @@ -75,7 +76,7 @@ int playSound(const char *filePath, bool wait) deviceExists = true; SDL_QueueAudio(deviceId, wavBuffer, wavLength); SDL_PauseAudioDevice(deviceId, 0); - LOGI("sound: playing file '%s'\n", filePath); + LOGD("sound: playing file '%s'\n", filePath); //--- wait until playback is finished --- while (SDL_GetQueuedAudioSize(deviceId) > 0) @@ -123,14 +124,14 @@ int playSoundAsync(const char *filePath) { //--- allocate memory for filePath --- char *filePathCopy = strdup(filePath); if (filePathCopy == NULL) { - fprintf(stderr, "Memory allocation failed\n"); + LOGE("sound: Memory allocation failed\n"); return 1; } //--- create new thread --- pthread_t thread; if (pthread_create(&thread, NULL, playSoundThread, filePathCopy) != 0) { - fprintf(stderr, "Failed to create thread\n"); + LOGE("sound: Failed to create thread\n"); free(filePathCopy); return 1; } else {