diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bd143b..3d413ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(SOURCES src/render.c src/snake.c src/map.c + src/common.c ) diff --git a/function-flowcharts.drawio.pdf b/function-flowcharts.drawio.pdf index 2d6b865..5fa8a1e 100644 Binary files a/function-flowcharts.drawio.pdf and b/function-flowcharts.drawio.pdf differ diff --git a/include/common.h b/include/common.h index 93a5590..3b88384 100644 --- a/include/common.h +++ b/include/common.h @@ -2,6 +2,7 @@ #include #include +#include #include "config.h" //=========================== @@ -10,15 +11,43 @@ //conditional logging when DEBUG_OUTPUT_ENABLED is defined in config.h //example: LOGD("game: %d", count) #ifdef DEBUG_OUTPUT_ENABLED -#define LOGD(format, ...) printf(format, ##__VA_ARGS__) +#define LOGD(format, ...) printf("[D] " format, ##__VA_ARGS__) #else #define LOGD(format, ...) do {} while (0) #endif //conditional logging when INFO_OUTPUT_ENABLED is defined in config.h -//example: LOGD("game: %d", count) +//example: LOGI("game: %d", count) #ifdef INFO_OUTPUT_ENABLED -#define LOGI(format, ...) printf(format, ##__VA_ARGS__) +#define LOGI(format, ...) printf("[I] " format, ##__VA_ARGS__) #else #define LOGI(format, ...) do {} while (0) #endif + + +//=========================== +//========== DELAY ========== +//=========================== +//macro for DELAY(ms) function that works on Windows and Linux +#ifdef _WIN32 +#include +#else +#include +#endif + +#ifdef _WIN32 +#include +#define DELAY(ms) Sleep(ms) +#else +#include +#define DELAY(ms) usleep((ms) * 1000) +#endif + + +//=========================== +//======= GET_TIME_MS ======= +//=========================== +// macro to get time in milliseconds +#define GET_TIME_MS() get_current_time() +// defined in common.c due to differences with windows and other systems +int64_t get_current_time(); \ No newline at end of file diff --git a/include/config.h b/include/config.h index 2fd58b0..23fe7be 100644 --- a/include/config.h +++ b/include/config.h @@ -1,12 +1,15 @@ #pragma once // global configuration macros -#define MAX_MAP_SIZE 10 +#define MAX_MAP_SIZE 20 #define MAX_MAP_FIELDS (MAX_MAP_SIZE*MAX_MAP_SIZE) + // logging settings #define DEBUG_OUTPUT_ENABLED #define INFO_OUTPUT_ENABLED + + // struct for storing game configuration typedef struct config_t { @@ -18,6 +21,7 @@ typedef struct config_t int difficulty; // 0-3 //Schwierigkeitsgrad int snakeDefaultLength; // = 2 //Länge der Schlange const char *leaderboardFilename; // Dateiname des Leaderboards + const char *defaultMapName; // Name der map die initial geladen wird } config_t; // global config struct defined in config.c diff --git a/include/food.h b/include/food.h index e3aed75..f8a9e40 100644 --- a/include/food.h +++ b/include/food.h @@ -1,10 +1,19 @@ #pragma once + #include +//function that spawns food respecting the following rules: +// - not at Collision, Snake-tail, Snake-head, portal-in, portal-out position (objects) +// - not closer to an object than minDist (if possible) +// - not further from an object than maxDist (if possible) +// maxDist and minDist are currently defined in food.c void placeFood(); -// platziert zufällig (mit bestimmtem Algorithmus) Fressen auf dem Spielfeld -// darf nicht auf der Schlange oder auf Wänden sein + +//function that returns true when snake head is at current food position bool checkEaten(); -// Überprüft, ob Snake gefressen hat -> true wenn gefressen -// Vergleich mit gameData_t foodX, foodY \ No newline at end of file + + +// indefinitely spawn food and print the map to console until the program is killed +// for testing and adjusting the food placement algorithm +void startFoodPlacementTest(); \ No newline at end of file diff --git a/include/game.h b/include/game.h index 304c4b0..e34e021 100644 --- a/include/game.h +++ b/include/game.h @@ -4,6 +4,8 @@ #include "snake.h" #include "config.h" #include "map.h" +#include "SDL.h" + // Enum that defines the current game state typedef enum gameState_t @@ -18,32 +20,34 @@ typedef enum gameState_t // Struct that stores all data of the running game (all game-related functions access it globally) typedef struct gameData_t { - snake_t snake; - map_t map; // definition der geladenen karte - bool mapIsLoaded; // true when config.map is valid - int foodX, foodY; // Positon des Futters (es gibt immer nur 1 Futter) - int lifesRemaining; // implementieren wir nicht!! - int timestampLastCycle; - gameState_t gameState; + snake_t snake; // data describing snake + map_t map; // loaded map + bool mapIsLoaded; // true when game.map is valid + int foodX, foodY; // current position of food + int lifesRemaining; // not implemented + int timestampLastCycle; // time last game cycle started + SDL_Renderer* renderer; // used by render.c and menu.c + SDL_Window *window; // used by render.c and menu.c + 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 +// - place initial food void gameInit(); -// berechnet BlockSizePx: windowSize/mapWidth -// ruft snakeInit auf -// ruft placeFood auf -// platziert Wände -void handlePortals(); //(local) -// Prüft, ob Snake sich auf einem Portal befindet -//if true: snakeSetHeadPos auf -void runGameCycle(); -// checkCollision() auf -// ruft placeFood() auf -// ruft checkEaten() auf -// if checkEaten then snakeGrow() -// Snakemove(), TickTimerReset -//ruft am Ende vom gameCycle renderGame() auf +// when snake head is on a portal-input, sets snake head to portal-target +void handlePortals(); //(ran in gameCycle) + + +// function that is repeatedly run at every game tick +// - moves snake to next position +// - handles collision, portals, food +// - triggers frame update (render.c) +void runGameCycle(); \ No newline at end of file diff --git a/include/input.h b/include/input.h index 0cb0eba..ff64731 100644 --- a/include/input.h +++ b/include/input.h @@ -1,8 +1,6 @@ #pragma once -void processInputEvent(); -//wird von SDL aufgerufen, wenn Taste gedrückt wurde -//bekommt Info darüber, welche Taste gedrückt wurde -//ruft zugehörige Aktion über switch caseauf - // z.B. bei Pfeiltaste -> rufe snakeSetDir auf - // im Menü bei Settings -> rufe menuNavigate auf \ No newline at end of file +// checks for and processes queued SDL input events and passes them +// to menu.c or controls snake depending on current game.gameState +// => has to be run repeatedly from main() +void processInputEvent(); \ No newline at end of file diff --git a/include/map.h b/include/map.h index b66719c..8c6dc0f 100644 --- a/include/map.h +++ b/include/map.h @@ -1,6 +1,8 @@ #pragma once + #include #include "config.h" +#include "snake.h" // Struct that stores all information needed for one Portal on the map @@ -11,32 +13,65 @@ typedef struct portal_t char *color; } portal_t; + // Struct that stores all information needed for one Collision box on the map typedef struct collisionBox_t { int posX, posY; } collisionBox_t; + // Struct that describes an entire map typedef struct map_t { int width; int height; - const char *name[128]; + char name[128]; collisionBox_t collisions[MAX_MAP_FIELDS]; int collisionCount; portal_t portals[MAX_MAP_FIELDS]; int portalCount; } map_t; -//return true when provided coordinate matches a collision box -bool checkCollides(int x, int y); -//generate random map based on difficulty level -map_t generateMap(int difficulty); +// calculates width in pixels of one block in the SDL window according to currently loaded map and configured window size and updates the config. +void updateBlockSizePx(); -//search and load map by name (if not found loads default map) + +// search and load map by name in storedMaps[] (map.c) +// stops program when map not found! void loadMapByName(char *name); + //load map by passed definition void loadMap(map_t map); + +//load next map in stored maps (rotate through stored maps) +void rotateMapNext(); + + +//return true when provided coordinate matches the position of a collision box +bool checkCollides(map_t map, int x, int y); + + +// generate random map based on difficulty level +// NOT IMPLEMENTED +map_t generateMap(int difficulty, int sizeX, int sizeY); + + +void printMap(map_t map); +// function that prints a map to console (stdout) +// note: currently also prints snake and food which may be bugged/unintended + + +// function that renders all current game objects to one 2d int array +// NOTE: passed Array has to be zero-initialized! (int arr[][] = {{0}}) +// useful for rendering game to console or sdl +// 1=collision, 2=portalIn, 3=portalOut, 4=snakeHead, 5=snakeTail +void renderGameToArray(int mapFrame[MAX_MAP_SIZE][MAX_MAP_SIZE], map_t map, snake_t snake); + + +// stored map presets can be globally accessed (maybe needed by menu.c) +// not: maps defined in map.c end of file + extern const map_t * storedMaps[16]; + extern const int storedMapsCount; \ No newline at end of file diff --git a/include/menu.h b/include/menu.h index 5514834..0f8e4c5 100644 --- a/include/menu.h +++ b/include/menu.h @@ -1,15 +1,19 @@ #pragma once +#include "SDL.h" + void showStartScreen(); //zum Starten Enter drücken //optional: "E" eingeben für Settings +void showPauseScreen(); + void showLeaderboard(); //zeigt die besten Spieldurchläufe inkl. Punktestand an void showSettings(); //optional //startet Settungs-Menü -void menuHandleInput(int event); //als Übergabeparameter: int(?) event -> welcher Datentyp hängt von SDL ab +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 dfd0ba3..fd09479 100644 --- a/include/render.h +++ b/include/render.h @@ -2,6 +2,11 @@ #include "game.h" #include "snake.h" +#include "SDL.h" void renderGame(); -//erstellt aus Spielfeldstruktur die graphische Anzeige mit SDL-Framework \ No newline at end of file +//erstellt aus Spielfeldstruktur die graphische Anzeige mit SDL-Framework + +int CreateSDLWindow(); + +void DestroySDLWindow(); \ No newline at end of file diff --git a/include/snake.h b/include/snake.h index 6f62e86..ddd784a 100644 --- a/include/snake.h +++ b/include/snake.h @@ -21,6 +21,7 @@ typedef struct snake_t bool isAlive; // lebt die Schlange noch oder ist sie mit sich selbst kollidiert? } snake_t; + void snakeInit(); // Snake mit bestimmter Startlänge an Startposition erstellen @@ -38,6 +39,9 @@ bool snakeIsAlive(); // Überprüfen, ob Schlange noch lebt // Prüft Kollision mit sich selbst -void snakeSetHeadPos(); // optional +void snakeSetHeadPos(int xPos, int yPos); // optional // für handlePortals -// generiert zufällige Zielsposition, wohin sich die Schlange nach Betreten eines Portals bewegt \ No newline at end of file +// generiert zufällige Zielposition(Übergabeparameter), wohin sich die Schlange nach Betreten eines Portals bewegt + +void snakeUpdateHeadPos(); +// berechnet neue Position des Kopfs anhand der aktuellen Bewegungsrichtung \ No newline at end of file diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..e4a3413 --- /dev/null +++ b/src/common.c @@ -0,0 +1,41 @@ +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "common.h" + + +//============================ +//==== get_current_time() ==== +//============================ +// Function that returns current time in milliseconds (can be used on windows and other systems) +int64_t get_current_time() +{ +#ifdef WIN32 + //=== WINDOWS === + FILETIME ft; + LARGE_INTEGER li; + // Get the amount of 100 nano seconds intervals elapsed since January 1, 1601 (UTC) and copy it to a LARGE_INTEGER structure. + GetSystemTimeAsFileTime(&ft); + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + uint64_t ret = li.QuadPart; + ret -= 116444736000000000LL; // Convert from file time to UNIX epoch time. + ret /= 10000; // From 100 nano seconds (10^-7) to 1 millisecond (10^-3) intervals + return ret; + +#else + + //=== LINUX === + struct timeval tv; + gettimeofday(&tv, NULL); + uint64_t ret = tv.tv_usec; + // Convert from micro seconds (10^-6) to milliseconds (10^-3) + ret /= 1000; + // Adds the seconds (10^0) after converting them to milliseconds (10^-3) + ret += (tv.tv_sec * 1000); + return ret; +#endif +} \ No newline at end of file diff --git a/src/config.c b/src/config.c index 658f531..e482e95 100644 --- a/src/config.c +++ b/src/config.c @@ -1,4 +1,15 @@ #include "config.h" -//global config struct -config_t config; \ No newline at end of file +//define global struct for configuration values +//default values defined here: (note: gets modified by map.c or menu.c) +config_t config = { + .windowSize = 800, + .blockSizePx = 800/10, //default map is 10x10 blocks + .cycleDurationMs = 400, + .difficulty = 1, + .snakeDefaultLength = 2, + .leaderboardFilename = "", + //.defaultMapName = "default" //10x10 + .defaultMapName = "intermediate" //15x15 + //.defaultMapName = "empty" //20x10 +}; \ No newline at end of file diff --git a/src/food.c b/src/food.c index 3804d90..92c3060 100644 --- a/src/food.c +++ b/src/food.c @@ -1,14 +1,150 @@ -#include "food.h" #include +#include +#include +#include "food.h" +#include "common.h" +#include "map.h" +#include "game.h" -// platziert zufällig (mit bestimmtem Algorithmus) Fressen auf dem Spielfeld -void placeFood(int count) + + +//-------------------------------------------- +//----- getRandomPositionWithMinDistance ----- +//-------------------------------------------- +// local function used in placeFood that returns random coordinates that have +// at least in_minDist blocks distance to every Collision, portal and snake block on the current map. +void getRandomPositionWithMinDistance(int *outX, int *outY, float *out_minDist, int *out_triesNeeded, float in_minDist) { + //--- config --- + static const int maxTries = 50; // each maxTries the limits above get loosened + + //--- get game frame --- + // get 2d array containing position of all objects of current game state + int gameFrame[MAX_MAP_SIZE][MAX_MAP_SIZE] = {{0}}; + renderGameToArray(gameFrame, game.map, game.snake); + int foodX, foodY; + + //--- search random location --- + // search random location for food that is within the defined min distance + int tries = 0; + //stores distance to closest block for return value + float minActualDistance = MAX_MAP_SIZE; +newValues: + while (1) + { + // generate random coodinates within map range + foodX = rand() % game.map.width; + foodY = rand() % game.map.height; + tries++; + // loop through all coordinates of current game/map + for (int y = 0; y < game.map.height; y++) + { + for (int x = 0; x < game.map.width; x++) + { + if (gameFrame[y][x] > 0) // any object is present + { + // calculate from random-coordinate to current block + float dist = sqrt(pow(foodX - x, 2) + pow(foodY - y, 2)); + // save minimum distance + if (dist < minActualDistance) minActualDistance = dist; + // verify minimum distance is kept + if (dist < in_minDist) //too close + { + LOGD("food: distance: min=%.1f now=%.1f => placed too close => reroll...\n", in_minDist, dist); + // prevent deadlock if no suitable position exists - loosen limits every k*maxTries + if (tries % maxTries == 0) + { + //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); + } + //reset stored distance and reroll coordinates + minActualDistance = MAX_MAP_SIZE; + goto newValues; + } + } + } + } + //success: no block closer than minDist to randomly generated coordinates -> break loop + break; + } + // return variables + *out_minDist = minActualDistance; + *outX = foodX; + *outY = foodY; + *out_triesNeeded = tries; +} + + + +//========================= +//======= placeFood ======= +//========================= +//function that spawns food respecting the following rules: +// - not at Collision, Snake-tail, Snake-head, portal-in, portal-out position (objects) +// - not closer to an object than minDist (if possible) +// - not further from an object than maxDist (if possible) +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 + 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 + + //--- variables --- + int foodX, foodY, triesMax = 0, triesMin; + float currentMinDist; + + //--- generate random food position within min/max range --- + LOGD("food: generating random position + verifying min/max distance...\n"); + do + { + // prevent deadlock when position respecting maxDist not found + triesMax++; + if (triesMax % maxTries == 0) + { + maxDist += 0.1; + LOGI("[WARN] food: too many tries for MAX_DIST -> loosen limits to max=%.1f\n", maxDist); + } + // generate random coordinates respecting minimum distance to objects + getRandomPositionWithMinDistance(&foodX, &foodY, ¤tMinDist, &triesMin, minDist); + //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); + game.foodX = foodX; + game.foodY = foodY; return; } -// Überprüft, ob Snake gefressen hat + + +//============================= +//===== foodPlacementTest ===== +//============================= +// indefinitely spawn food and print the map to console until the program is killed +// for testing and adjusting the food placement algorithm +void startFoodPlacementTest() +{ + while (1) + { + loadMapByName("default"); + placeFood(); + printMap(game.map); + DELAY(100); + } +} + + + +//========================== +//======= checkEaten ======= +//========================== +//returns true when snake head is at current food position bool checkEaten() { - return 0; + return (game.snake.headX == game.foodX && game.snake.headY == game.foodY); } diff --git a/src/game.c b/src/game.c index 179343b..48c9ca2 100644 --- a/src/game.c +++ b/src/game.c @@ -1,49 +1,117 @@ #include "game.h" #include "map.h" +#include "common.h" +#include "menu.h" +#include "food.h" +#include "render.h" + // global struct for storing all game data -gameData_t game; +// default values where needed: +gameData_t game = { + .snake.length = 2, + .foodX = 0, + .foodY = 0, + .mapIsLoaded = false, + .lifesRemaining = 1, + .timestampLastCycle = 0, + .gameState = RUNNING, +}; + + //======================== //======= gameInit ======= //======================== +// run once at game start and does the following: +// - init snake +// - load map +// - place initial food void gameInit() { - //----- snake ----- - // defines initial values of game.snake - // snakeInit(); FIXME: uncomment when implemented - + LOGI("game: initializing game...\n"); //----- load map ----- //load default map if no map loaded yet if (!game.mapIsLoaded){ - char * defaultName = "default"; - loadMapByName("default"); + //loadMapByName("default"); + loadMapByName(config.defaultMapName); } - // place initial food - //placeFood(); FIXME uncomment when implemented + + //----- snake ----- + // defines initial values of game.snake + snakeInit(); //TODO assign return value to game.snake? - //----- initialize variables ----- - game.lifesRemaining = 1; - // game.lifesRemaining = config.maxLifes; TODO: add maxLifes to config - // game.gameState = RUNNING; ?? - - game.timestampLastCycle = -config.cycleDurationMs; // next cycle starts immediately + //--- place initial food --- + placeFood(); + LOGI("game: placed initial food at x=%d, y=%d\n", game.foodX, game.foodY); } + + //========================= //===== handlePortals ===== //========================= +// when snake head is on a portal-input, sets snake head to portal-target void handlePortals() { + LOGD("game: handling portals...\n"); + // loop through all existin portals in current map (game.map) + for (int i=0; i < game.map.portalCount; i++){ + portal_t p = game.map.portals[i]; //copy curren portal (code more readable) + // is at portal + if (game.snake.headX == p.posX && game.snake.headY == p.posY){ + 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); + return; + } + } + // snake not on any portal return; } + + //========================== //====== runGameCycle ====== //========================== +// function that is repeatedly run at every game tick +// - moves snake to next position +// - handles collision, portals, food +// - triggers frame update (render.c) void runGameCycle() { - if (checkCollides(game.snake.headX, game.snake.headY)) + LOGD("game: starting GameCycle %d\n", game.timestampLastCycle); + + //--- move snake --- + // move snake to next position + snakeMove(); + + //--- 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"); + LOGI("DEBUG: collision currently disabled, game will continue in 1s...\n"); + DELAY(800); + //game.gameState = MENU; //TODO add config.collisionEnabled option? + showLeaderboard(); return; + } + + //--- handle portals --- + handlePortals(); + + //--- handle food --- + if (checkEaten()) { + LOGI("game: picked up food at x=%d y=%d -> growing, placing food\n", game.foodX, game.foodY); + // NOTE: order of place and grow is relevant, otherwise function in food.c will access invalid memory + placeFood(); + snakeGrow(); +} + + //--- update frame --- + renderGame(); + printMap(game.map); //render game to console return; } \ No newline at end of file diff --git a/src/input.c b/src/input.c index 77d4773..c1adfff 100644 --- a/src/input.c +++ b/src/input.c @@ -1,5 +1,114 @@ #include "input.h" +#include "common.h" +#include "SDL.h" +#include "game.h" +#include "menu.h" +#include "snake.h" +#include "map.h" + + + +//-------------------------------- +//--- handleInput_runningState --- +//-------------------------------- +// local function that handles keyboard input when in RUNNING gameState +// - control snake via WASD or arrow-keys +// - quit with q +// - pause with p +void handleInput_runningState(SDL_Event event) +{ + switch (event.key.keysym.sym) + { + case SDLK_q: // q: quit + game.gameState = EXIT; + break; + + case SDLK_p: // p: pause + case SDLK_ESCAPE: + game.gameState = PAUSED; + showPauseScreen(); + break; + + //--- control snake direction --- + case SDLK_UP: + case SDLK_w: + snakeSetDir(UP); + break; + + case SDLK_DOWN: + case SDLK_s: + snakeSetDir(DOWN); + break; + + case SDLK_LEFT: + case SDLK_a: + snakeSetDir(LEFT); + break; + + case SDLK_RIGHT: + case SDLK_d: + snakeSetDir(RIGHT); + 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_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); + } + return; +} + +//============================= +//===== processInputEvent ===== +//============================= +// checks for and processes queued SDL input events and passes them +// to menu.c or controls snake depending on current game.gameState +// => has to be run repeatedly from main() +void processInputEvent() +{ + SDL_Event event; + // loop through all queued input events that occoured since last run + while (SDL_PollEvent(&event)) + { + // LOGD("event: %d detected", event.type); + //--- quit event --- + if (event.type == SDL_QUIT) + { + game.gameState = EXIT; + return; + } + //--- key pressed --- + // TODO also send mouse-events to menu? + else if (event.type == SDL_KEYDOWN) + { + //LOGD("keydown event key=%d\n", event.key.keysym.sym); + // run functions that handle the input depending on game.state + switch (game.gameState) + { + case RUNNING: + // control snake with keys (function above) + handleInput_runningState(event); + break; + case PAUSED: + case MENU: + // pass key event to menu handle function which updates menu + menuHandleInput(event); + break; + } + } + } -void processInputEvent(){ return; } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f0a7d5c..079f4d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,12 @@ -#include "SDL.h" +#include +#include -extern "C" { -#include "food.h" +extern "C" +{ +#include "game.h" +#include "common.h" +#include "input.h" +#include "render.h" } //initialize SDL window @@ -11,28 +16,80 @@ extern "C" { //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 + + + + int main(int argc, char *argv[]) { - SDL_Init(SDL_INIT_VIDEO); + gameInit(); - SDL_Window *window = SDL_CreateWindow( - "SDL2Test", - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, - 640, - 480, - 0 - ); + // Initialisiere SDL + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("SDL konnte nicht initialisiert werden! SDL_Error: %s\n", SDL_GetError()); + return 1; + } + CreateSDLWindow(); - SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); - SDL_RenderClear(renderer); - SDL_RenderPresent(renderer); - SDL_Delay(1000); - SDL_DestroyWindow(window); + + + time_t now; + now = GET_TIME_MS(); // Timer startet + game.timestampLastCycle = now; + int diff; + + while(game.gameState != EXIT) { + if (game.gameState == RUNNING) { + now = GET_TIME_MS(); // Timer startet + diff = now-game.timestampLastCycle; + + if (diff > config.cycleDurationMs){ + game.timestampLastCycle = now; + runGameCycle(); + } + } + DELAY(5); //verhindert maximale Durchlaufgeschwindigkeit der Schleife + 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(); SDL_Quit(); - return 0; } diff --git a/src/map.c b/src/map.c index 52c5cce..879c3c0 100644 --- a/src/map.c +++ b/src/map.c @@ -1,43 +1,261 @@ +#include #include "map.h" #include "game.h" #include "common.h" +int indexLoadedMap = 0; + +//=========================== +//==== renderGameToArray ==== +//=========================== +// function that renders all current game objects to one 2d int array +// NOTE: passed Array has to be zero-initialized! (int arr[][] = {{0}}) +// useful for rendering game to console or sdl +// 1=collision, 2=portalIn, 3=portalOut, 4=snakeHead, 5=snakeTail +void renderGameToArray(int mapFrame[MAX_MAP_SIZE][MAX_MAP_SIZE], map_t map, snake_t snake) +{ + // copy collisions + for (int i = 0; i < map.collisionCount; i++) + { + mapFrame[map.collisions[i].posY][map.collisions[i].posX] = 1; + } + // copy portals + for (int i = 0; i < map.portalCount; i++) + { + mapFrame[map.portals[i].posY][map.portals[i].posX] = 2; + mapFrame[map.portals[i].targetY][map.portals[i].targetX] = 3; + } + // copy snake tail + for (int i = 0; i < snake.length; i++) + { + mapFrame[snake.tail[i][1]][snake.tail[i][0]] = 5; + } + // copy food + mapFrame[game.foodY][game.foodX] = 6; + // copy snake head (last element -> head overwrites previous elements) + mapFrame[snake.headY][snake.headX] = 4; + return; +} + + + +//======================== +//======= printMap ======= +//======================== +// function that prints a map to console (stdout) +// note: currently also prints snake and food which may be bugged/unintended +#define PRINT_SNAKE_ENABLED +void printMap(map_t map) +{ + LOGI("map: Preview of map '%s' (%dx%d):\n", map.name, map.width, map.height); + int mapFrame[MAX_MAP_SIZE][MAX_MAP_SIZE] = {{0}}; + renderGameToArray(mapFrame, map, game.snake); + // --- print top line --- + printf("+"); + for (int i = 0; i < map.width; i++) printf("-"); + printf("+\n"); + // --- print field --- + // loop through rows (y) + for (int row = 0; row < map.height; row++) + { + printf("|"); // vert line left + // loop through line (x) + for (int column = 0; column < map.width; column++) + { + switch (mapFrame[row][column]) + { + case 1: printf("X"); // collistion + break; + case 2: printf("O"); // portal-in + break; + case 3: printf("T"); // portal-out + break; +#ifdef PRINT_SNAKE_ENABLED + case 4: printf("H"); // snake-head + break; + case 5: printf("S"); // snake-tail + break; +#endif + case 6: printf("F"); // food + break; + default: printf(" "); // empty + break; + } + } + printf("|\n"); // vert line right + } + // --- print bot line --- + printf("+"); + for (int i = 0; i < map.width; i++) + printf("-"); + printf("+\n"); +} + + + +//=========================== +//======= generateMap ======= +//=========================== // generate random map based on difficulty level -map_t generateMap(int difficulty) +// NOT IMPLEMENTED +map_t generateMap(int difficulty, int sizeX, int sizeY) { map_t newMap; return newMap; // TODO add map generator } -// search and load map by name (if not found loads default map) -void loadMapByName(char *name) -{ - LOGI("map: loading map %s", name); - return; - // TODO add map presets + + +//=========================== +//==== updateBlockSizePx ==== +//=========================== +// calculates width in pixels of one block in the SDL window according to currently loaded map and configured window size and updates the config. +void updateBlockSizePx(){ + // due to square pixel requirement + // larger dimension of the map has to be used if map is not square + if (game.map.height >= game.map.width) { + config.blockSizePx = config.windowSize / game.map.height; + } + else + { + config.blockSizePx = config.windowSize / game.map.width; + } } + + +//=========================== +//====== loadMapByName ====== +//=========================== +// search and load map by name in storedMaps[] (map.c) +// stops program when map not found! +void loadMapByName(char *name) +{ + // loop through all stored maps + for (int i = 0; i < storedMapsCount; i++) + { + // compare name + if (strcmp(name, storedMaps[i]->name) == 0) + { + // load matched map + LOGI("map: found map '%s'\n", name); + loadMap(*storedMaps[i]); + return; + } + } + // map not found + printf("[FATAL ERROR] map: could not find '%s' in storedMaps!\n", name); + game.gameState = EXIT; + return; +} + + +//=========================== +//========= loadMap ========= +//=========================== // load map by passed definition void loadMap(map_t map) { + LOGI("map: loading map '%s':\n", map.name); +#ifdef DEBUG_OUTPUT_ENABLED + printMap(map); +#endif game.map = map; + // update rendered pixel size (due to new map size) + updateBlockSizePx(); game.mapIsLoaded = true; return; } -// check if there is collision at certain coordinate -bool checkCollides(int x, int y) + +//=========================== +//====== rotateMapNext ====== +//=========================== +//load next map in stored maps (rotate through stored maps) +void rotateMapNext(){ + if (indexLoadedMap >= storedMapsCount -1 ){ + indexLoadedMap = 0; + } else { + indexLoadedMap ++; + } + loadMap(*storedMaps[indexLoadedMap]); +} + + + +//=========================== +//====== checkCollides ====== +//=========================== +// check if there is collision box at a certain coordinate +bool checkCollides(map_t map, int x, int y) { // loop through all collision boxes on the map - for (int i = 0; i < game.map.collisionCount; i++) + for (int i = 0; i < map.collisionCount; i++) { - // return true if match found - if (game.map.collisions[i].posX == x && game.map.collisions[i].posY == y) + // LOGD("map: checking collision i=%d at x=%d y=%d\n", i, map.collisions[i].posX, map.collisions[i].posY); + if (map.collisions[i].posX == x && map.collisions[i].posY == y) return true; } return false; } -//TODO add map presets here: \ No newline at end of file + +//=========================== +//======= MAP PRESETS ======= +//=========================== +// stored map presets +// TODO add more maps or map generator +static const map_t map_default = { + .width = 10, + .height = 10, + .name = "default", + .collisions = {{8, 9}, {8, 8}, {4, 5}, {0, 1}}, + .collisionCount = 4, + .portals = { + {.posX = 5, + .posY = 8, + .targetX = 7, + .targetY = 1, + .color = "blue"}}, + .portalCount = 1}; + + +static const map_t map_empty = { + .width = 20, + .height = 15, + .name = "empty", + .collisions = {}, + .collisionCount = 0, + .portals = { + {.posX = 5, + .posY = 8, + .targetX = 7, + .targetY = 1, + .color = "blue"}}, + .portalCount = 1}; + + +static const map_t map_intermediate = { + .width = 15, + .height = 15, + .name = "intermediate", + .collisions = {{8, 9}, {8, 8}, {4, 6}, {0, 1}, {9, 9}, {7, 6}, {4, 0}, {3, 0}, {12, 11}, {14, 13}}, + .collisionCount = 10, + .portals = { + {.posX = 5, + .posY = 8, + .targetX = 13, + .targetY = 2, + .color = "blue"}, + {.posX = 2, + .posY = 2, + .targetX = 3, + .targetY = 12, + .color = "red"}}, + .portalCount = 2}; + +// global variables for accessing the stored maps +const map_t *storedMaps[16] = {&map_default, &map_empty, &map_intermediate}; +const int storedMapsCount = 3; \ No newline at end of file diff --git a/src/menu.c b/src/menu.c index e24988b..a80ce9e 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1,11 +1,19 @@ #include "menu.h" +#include "game.h" void showStartScreen(){ + game.gameState = RUNNING; return; } void showLeaderboard(){ + game.gameState = EXIT; + return; +} + +void showPauseScreen(){ + game.gameState = PAUSED; return; } @@ -13,6 +21,7 @@ void showSettings(){ return; } -void menuHandleInput(int event){ +void menuHandleInput(SDL_Event event){ + //compare 'handleInput_runningState(SDL_Event event)' in input.c return; } \ No newline at end of file diff --git a/src/render.c b/src/render.c index cc2b428..86a0f48 100644 --- a/src/render.c +++ b/src/render.c @@ -1,5 +1,137 @@ #include "render.h" +#include "SDL.h" +#include "game.h" +#include "snake.h" +#include "food.h" +#include "config.h" +#include + + void renderGame(){ - return; + SDL_SetRenderDrawColor(game.renderer, 0, 0, 0, 255); + SDL_RenderClear(game.renderer); + + SDL_Rect rect; //Rechteck anlegen + rect.w = config.blockSizePx; //Breite festlegen + rect.h = config.blockSizePx; //Höhe festlegen + +//_______Head kreieren__________________________________________________ + SDL_SetRenderDrawColor(game.renderer, 0, 255, 0, 255); //RGB-Farbe Kopf + rect.x = (game.snake.headX * config.blockSizePx); //Abstand links + rect.y = (game.snake.headY * config.blockSizePx); //Abstand rechts + SDL_RenderFillRect(game.renderer, &rect); //Rechteck rendern + + +//______Tail kreieren_________________________________________________________________ + for(int i = 1; i 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]; return; } -void snakeMove(){ +void snakeMove() +{ + int i = game.snake.length - 1; // counter for snake moving + + // update head position automatically + snakeUpdateHeadPos(); + + // tail part of[x,y][0,1] get coordinates of tail part before + while(i) + { + game.snake.tail[i][0] = game.snake.tail[i-1][0]; + game.snake.tail[i][1] = game.snake.tail[i-1][1]; + i--; + } + game.snake.tail[0][0] = game.snake.headX; + game.snake.tail[0][1] = game.snake.headY; + return; } -void snakeSetDir(snakeDirection_t dir){ +void snakeSetDir(snakeDirection_t dir) +{ + game.snake.direction = dir; return; } -bool snakeIsAlive(){ +bool snakeIsAlive() +{ + for(int i = 1; i < game.snake.length; i++) + { + if(game.snake.tail[i][0] == game.snake.headX && game.snake.tail[i][1] == game.snake.headY) + return false; + } return true; } -void snakeSetHeadPos(){ +void snakeSetHeadPos(int xPos, int yPos) +{ + game.snake.headX = xPos; + game.snake.headY = yPos; + game.snake.tail[0][0] = xPos; + game.snake.tail[0][1] = yPos; + return; +} + + +void snakeUpdateHeadPos() +{ + switch(game.snake.direction) + { + // DOWN + case DOWN: + game.snake.headX = game.snake.tail[0][0]; + game.snake.headY = game.snake.tail[0][1] + 1; + if(game.snake.headY >= game.map.height) + game.snake.headY = 0; + break; + + // UP + case UP: + game.snake.headX = game.snake.tail[0][0]; + game.snake.headY = game.snake.tail[0][1] - 1; + if(game.snake.headY < 0) + game.snake.headY = game.map.height - 1; + break; + + // LEFT + case LEFT: + game.snake.headX = game.snake.tail[0][0] - 1; + game.snake.headY = game.snake.tail[0][1]; + if(game.snake.headX < 0) + game.snake.headX = game.map.width - 1; + break; + + // RIGHT + case RIGHT: + game.snake.headX = game.snake.tail[0][0] + 1; + game.snake.headY = game.snake.tail[0][1]; + if(game.snake.headX >= game.map.width) + game.snake.headX = 0; + break; + } return; } \ No newline at end of file