From c9b542eccf4268cbe149593b1b99d5b515844d66 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 9 Aug 2026 20:00:56 +0100 Subject: [PATCH] Migrate to SDL2 and deploy as WASM --- .forgejo/workflows/deploy.yml | 71 +++ .gitignore | 2 + CMakeLists.txt | 79 +++ Forráskód/main.c | 97 ++- Forráskód/modules/auxiliary/auxSDL.c | 229 ++++++- Forráskód/modules/auxiliary/auxSDL.h | 16 +- Forráskód/modules/auxiliary/gfxPrimitives.c | 300 +++++++++ Forráskód/modules/auxiliary/gfxPrimitives.h | 18 + Forráskód/modules/auxiliary/unicodePrint.c | 95 ++- Forráskód/modules/auxiliary/unicodePrint.h | 9 +- Forráskód/modules/camera.c | 657 ++++++++++---------- Forráskód/modules/camera.h | 1 + Forráskód/modules/global.h | 31 +- Forráskód/modules/globals.c | 14 + Forráskód/modules/menu/menuDraw.c | 70 +-- Forráskód/modules/menu/menuFSM.c | 47 +- Forráskód/modules/menu/menuFSM.h | 1 + Forráskód/modules/particles.h | 2 +- Forráskód/modules/player.c | 7 +- Forráskód/modules/saves.c | 22 +- Forráskód/modules/saves.h | 2 +- README.md | 4 + web/pre.js | 60 ++ web/shell.html | 66 ++ 24 files changed, 1414 insertions(+), 486 deletions(-) create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Forráskód/modules/auxiliary/gfxPrimitives.c create mode 100644 Forráskód/modules/auxiliary/gfxPrimitives.h create mode 100644 Forráskód/modules/globals.c create mode 100644 README.md create mode 100644 web/pre.js create mode 100644 web/shell.html diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..c7e3daa --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,71 @@ +name: Build & deploy web version + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + workflow_dispatch: + +concurrency: + group: 'pages' + cancel-in-progress: false + +jobs: + deploy: + name: Build WebAssembly & deploy + runs-on: docker + + env: + # Keep in sync with the version table in README.md. + EMSDK_VERSION: '6.0.6' + + steps: + - uses: actions/checkout@v4 + + - name: Install cmake & friends + run: | + set -eo pipefail + SUDO="" + [ "$(id -u)" -eq 0 ] || SUDO="sudo" + $SUDO apt-get update + $SUDO apt-get install -y --no-install-recommends \ + ca-certificates cmake git ninja-build python3 xz-utils + cmake --version + + - name: Install Emscripten + run: | + set -eo pipefail + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$HOME/emsdk" + cd "$HOME/emsdk" + ./emsdk install "$EMSDK_VERSION" + ./emsdk activate "$EMSDK_VERSION" + . ./emsdk_env.sh + # emsdk_env.sh only touches this step's shell, so hand the activated + # toolchain over to the later steps explicitly. + { + echo "EMSDK=$EMSDK" + echo "EMSDK_NODE=$EMSDK_NODE" + echo "EM_CONFIG=${EM_CONFIG:-$EMSDK/.emscripten}" + echo "PATH=$PATH" + } >> "$GITHUB_ENV" + + - name: Configure + run: emcmake cmake -S . -B build-web -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build-web + + - name: Check the expected files were produced + run: | + set -euo pipefail + for file in index.html index.js index.wasm index.data; do + test -s "build-web/web/$file" || { echo "hiányzik: $file"; exit 1; } + done + + - name: Deploy to nginx pages mount + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main + with: + source: build-web/web + target: platform-game diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bd80f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ed19c82 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.21) +project(platform_game C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + +option(GAME_VENDOR_SDL "Build pinned SDL2/SDL2_ttf from source instead of using system packages" ON) + +set(GAME_SRC "${CMAKE_CURRENT_SOURCE_DIR}/Forráskód") + +file(GLOB_RECURSE GAME_SOURCES CONFIGURE_DEPENDS "${GAME_SRC}/*.c") + +add_executable(game ${GAME_SOURCES}) + +target_include_directories(game PRIVATE "${GAME_SRC}") +target_compile_options(game PRIVATE -Wall) + +if(EMSCRIPTEN) + set(GAME_EM_FLAGS "SHELL:-sUSE_SDL=2" "SHELL:-sUSE_SDL_TTF=2") + set(GAME_WEB_SHELL "${CMAKE_CURRENT_SOURCE_DIR}/web/shell.html") + set(GAME_WEB_PREJS "${CMAKE_CURRENT_SOURCE_DIR}/web/pre.js") + + target_compile_options(game PRIVATE ${GAME_EM_FLAGS}) + target_link_options(game PRIVATE + ${GAME_EM_FLAGS} + "SHELL:-sALLOW_MEMORY_GROWTH=1" + "SHELL:-sEXIT_RUNTIME=0" + "SHELL:-sFORCE_FILESYSTEM=1" + "SHELL:-lidbfs.js" + "SHELL:--preload-file ${GAME_SRC}/resources@/resources" + "SHELL:--shell-file ${GAME_WEB_SHELL}" + "SHELL:--pre-js ${GAME_WEB_PREJS}" + ) + set_target_properties(game PROPERTIES + OUTPUT_NAME index + SUFFIX ".html" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web" + LINK_DEPENDS "${GAME_WEB_SHELL};${GAME_WEB_PREJS}" + ) +elseif(GAME_VENDOR_SDL) + include(FetchContent) + set(FETCHCONTENT_QUIET OFF) + + # As a subproject both libraries already default to no samples and no install + # rules, and BUILD_SHARED_LIBS=OFF is what turns SDL2 static-only, so these + # three are the only knobs that still differ from the defaults. + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(SDL_TEST OFF CACHE BOOL "" FORCE) + set(SDL2TTF_VENDORED ON CACHE BOOL "" FORCE) # build freetype instead of finding it + + FetchContent_Declare(SDL2 + URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.4/SDL2-2.32.4.tar.gz + URL_HASH SHA256=f15b478253e1ff6dac62257ded225ff4e7d0c5230204ac3450f1144ee806f934 + ) + FetchContent_Declare(SDL2_ttf + URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-2.24.0.tar.gz + URL_HASH SHA256=0b2bf1e7b6568adbdbc9bb924643f79d9dedafe061fa1ed687d1d9ac4e453bfd + ) + FetchContent_MakeAvailable(SDL2 SDL2_ttf) + + target_link_libraries(game PRIVATE SDL2::SDL2-static SDL2::SDL2main SDL2_ttf::SDL2_ttf-static) +else() + find_package(SDL2 REQUIRED) + find_package(SDL2_ttf REQUIRED) + target_link_libraries(game PRIVATE SDL2::SDL2 SDL2::SDL2main SDL2_ttf::SDL2_ttf) +endif() + +if(NOT EMSCRIPTEN) + target_link_libraries(game PRIVATE m) + add_custom_command(TARGET game POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${GAME_SRC}/resources" "$/resources" + COMMAND ${CMAKE_COMMAND} -E make_directory "$/saves" + ) +endif() diff --git a/Forráskód/main.c b/Forráskód/main.c index 83a01ed..4dd4c6e 100644 --- a/Forráskód/main.c +++ b/Forráskód/main.c @@ -1,6 +1,10 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include +#endif + #include "modules/global.h" #include "modules/camera.h" #include "modules/color.h" @@ -55,8 +59,8 @@ * - auxSDL.c: a megfelelő sdl-es függvények meghívása, illetve az egér interakciók kezelése * - auxFunctions.c: gyakran használ inline függvények * \section Kompatibilitás - * A program lefordításához szükséges az SDL.h, SDL_ttf.h, illetve SDL_gfxPrimitives.h könyvtárak. Nincs tesztelve, viszont valószínűleg - * az SDL 1.2 által platformokon működőképes az alkalmazás. A játék fix FPS-sel fut, ez jelenleg 50-re van beállítva, így átlagos teljesítményű gépeken is + * A program lefordításához szükséges az SDL2 és az SDL2_ttf könyvtár, a rajzoló primitíveket a program magával hozza. + * Natívan és Emscriptennel webre fordítva is működik. A játék fix FPS-sel fut, ez jelenleg 50-re van beállítva, így átlagos teljesítményű gépeken is * el kell futnia, viszont ehhez szükség van egy átlagos képességű gépre, szóval ez egy lényeges rendszerkövetelmény. * A játék működéséhez szükségesek a megfelelő ttf-fájlok. * \section Készítette @@ -71,31 +75,22 @@ bool isLMBDown = false, isRMBDown = false, prematureClick; -int main(int argc, char *argv[]) { +static Uint32 backgroundColor; +static Uint32 previousTicks; +static double accumulator = 0; +static bool quitRequested = false; - srand(time(NULL)); - SDL_Cursor* cursor = openWindow(); +static void handleEvents() { SDL_Event event; - SDL_TimerID timerID; - timerID = SDL_AddTimer((int)(1000.0 / FPS), timer, NULL); - keys = SDL_GetKeyState(NULL); - //setlocale(0, ""); - - Uint32 backgroundColor = SDL_MapRGB(screen -> format, 40, 0, 0); - - createMenuWorld(); - - while (getGameState() != quit) { - SDL_WaitEvent(&event); + while(SDL_PollEvent(&event)) { switch(event.type) { - case SDL_USEREVENT: + case SDL_QUIT: - SDL_FillRect(screen, NULL, backgroundColor); - doStuff(isLMBDown, isRMBDown, prematureClick); - SDL_UpdateRect(screen, 0, 0, 0, 0); + quitRequested = true; break; + case SDL_MOUSEBUTTONDOWN: if(event.button.button == SDL_BUTTON_LEFT) handleClicks(&prematureClick); @@ -116,12 +111,68 @@ int main(int argc, char *argv[]) { case SDL_KEYDOWN: - if(strcmp(SDL_GetKeyName(event.key.keysym.unicode), "escape") == 0) whatToDoOnEscape(); + if(event.key.keysym.sym == SDLK_ESCAPE) whatToDoOnEscape(); else processTyping(event); break; + case SDL_TEXTINPUT: + + processTextInput(event.text.text); + break; + } } +} + +static void frame() { + const double step = 1000.0 / FPS; + + handleEvents(); + handleResize(); + + Uint32 now = SDL_GetTicks(); + double elapsed = now - previousTicks; + previousTicks = now; + if(elapsed > 5 * step) elapsed = 5 * step; + accumulator += elapsed; + + bool hasDrawn = false; + while(accumulator >= step) { + accumulator -= step; + + bool isLastStep = accumulator < step; + setRenderingEnabled(isLastStep); + + if(isLastStep) SDL_FillRect(screen, NULL, backgroundColor); + doStuff(isLMBDown, isRMBDown, prematureClick); + hasDrawn = hasDrawn || isLastStep; + } + setRenderingEnabled(true); + + if(hasDrawn) presentFrame(); +#ifdef __EMSCRIPTEN__ + if(getGameState() == quit || quitRequested) emscripten_cancel_main_loop(); +#else + if(!hasDrawn) SDL_Delay(1); +#endif +} + +int main(int argc, char *argv[]) { + + srand(time(NULL)); + openWindow(); + keys = (unsigned char*) SDL_GetKeyboardState(NULL); + SDL_StartTextInput(); + + backgroundColor = SDL_MapRGB(screen -> format, 40, 0, 0); + + createMenuWorld(); + previousTicks = SDL_GetTicks(); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(frame, 0, 1); +#else + while(getGameState() != quit && !quitRequested) frame(); //Felszabadítás freeFloaterList(); @@ -131,9 +182,9 @@ int main(int argc, char *argv[]) { freeParticleList(); SDL_FreeSurface(text); freeFonts(); - SDL_RemoveTimer(timerID); - SDL_FreeCursor(cursor); + closeWindow(); SDL_Quit(); +#endif return 0; } diff --git a/Forráskód/modules/auxiliary/auxSDL.c b/Forráskód/modules/auxiliary/auxSDL.c index 749befb..b54434b 100644 --- a/Forráskód/modules/auxiliary/auxSDL.c +++ b/Forráskód/modules/auxiliary/auxSDL.c @@ -1,48 +1,208 @@ #include "auxSDL.h" +#include "../camera.h" + +#ifdef __EMSCRIPTEN__ + #include +#else + #include +#endif mousePos mouse; /**< Az egér koordinátáit tárolja, így egyszerűbb, mint közvetlen az eventek figyelése. */ +static SDL_Window* window; +static SDL_Renderer* renderer; +static SDL_Texture* frameTexture; +static int textureWidth, textureHeight; -/** \brief https://www.libsdl.org/release/SDL-1.2.15/docs/html/sdlcreatecursor.html - * Hogy erre miért van szükség, az egy nagyon jó kérdés. Ha ehelyett a "SDL_ShowCursor(SDL_DISABLE);"-t - * használnám az egér random ugrálna néha néha, így viszont ezt nem teszi. - */ -static SDL_Cursor* createCursor() { - Uint8 data[4*32] = {0}; - Uint8 mask[4*32] = {0}; - return SDL_CreateCursor(data, mask, 32, 32, 0, 0); +#ifdef __EMSCRIPTEN__ +///A böngésző pixelarányát adja vissza, a szoftveres rajzolás miatt megszabott plafonig. +static double drawingPixelRatio() { + double ratio = emscripten_get_device_pixel_ratio(); + return ratio > maxDevicePixelRatio ? maxDevicePixelRatio : ratio; +} +#endif + +static void moveToResourceDirectory() { +#ifndef __EMSCRIPTEN__ + char* base = SDL_GetBasePath(); + if(base != NULL) { + if(chdir(base) != 0) fprintf(stderr, "Nem sikerult a munkakonyvtar valtas!\n"); + SDL_free(base); + } +#endif } -/** \brief Létrehozza a játék futtatásához szükséges környezetet. - * Megnézi az elérhető legnagyobb felbontást, azzal a felbontással létrehoz egy ablakot, illetve - * meghív pár, az inicializáláshoz szükséges függvényt. - * \return kurzor amit fel kell később szabadítanunk +/** \brief Gondoskodik róla, hogy a textúra legalább a kért méretű legyen. + * Böngészőben a textúra eldobása átméretezéskor elveszejti a webgl kontextust, ezért + * a textúrát egyszer, jó bőre foglaljuk, és utána már csak egy részét használjuk. */ -SDL_Cursor* openWindow() { - SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER); - SDL_Rect** screenSizes = SDL_ListModes(NULL, SDL_FULLSCREEN); +static void ensureTexture(int width, int height) { + if(frameTexture != NULL && width <= textureWidth && height <= textureHeight) return; - screen = SDL_SetVideoMode(screenSizes[0] -> w, screenSizes[0] -> h, 0, SDL_FULLSCREEN); + if(width > textureWidth) textureWidth = width; + if(height > textureHeight) textureHeight = height; - if (!screen) { - fprintf(stderr, "Nem sikerult megnyitni az ablakot!\n"); + SDL_DestroyTexture(frameTexture); + frameTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, textureWidth, textureHeight); + + if(frameTexture == NULL) { + fprintf(stderr, "Nem sikerult letrehozni a texturat: %s\n", SDL_GetError()); + exit(1); + } +} + +/** \brief A megadott méretű rajzfelületet hozza létre a régi helyére. + * A rajzfelület mindig pontosan akkora, mint a megjelenítő felülete, így nincs se + * fekete csík, se átméretezés. + */ +static void createDrawingSurface(int width, int height) { + if(width < 1) width = 1; + if(height < 1) height = 1; + + ensureTexture(width, height); + + SDL_FreeSurface(screen); + screen = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888); + + if(screen == NULL) { + fprintf(stderr, "Nem sikerult letrehozni a rajzfeluletet: %s\n", SDL_GetError()); exit(1); } - SDL_WM_SetCaption("Platform játék", "Schmelczer András: Platform játék"); - SDL_Cursor* cursor = createCursor(); - SDL_SetCursor(cursor); - openFonts(); - SDL_WM_GrabInput(SDL_GRAB_FULLSCREEN); - return cursor; + SDL_RenderSetLogicalSize(renderer, width, height); } -///A játék időzítéséhez szükséges. -Uint32 timer(Uint32 ms, void* param) { - SDL_Event event; - event.type = SDL_USEREVENT; - SDL_PushEvent(&event); - return ms; +/** \brief Az ablak méretének változását követi le, képkockánként meghívva. + * Böngészőben a vászon css-méretéhez igazítja az ablakot, majd ha kell, új rajzfelületet + * készít, a betűtípusokat a magassághoz igazítja, és a kamerával újraszámoltatja, + * hogy mennyi látszik a pályából. + */ +void handleResize() { +#ifdef __EMSCRIPTEN__ + double cssWidth, cssHeight; + if(emscripten_get_element_css_size("#canvas", &cssWidth, &cssHeight) == EMSCRIPTEN_RESULT_SUCCESS) { + double ratio = drawingPixelRatio(); + int wantedWidth = (int)(cssWidth * ratio); + int wantedHeight = (int)(cssHeight * ratio); + + int currentWidth, currentHeight; + SDL_GetWindowSize(window, ¤tWidth, ¤tHeight); + + if(wantedWidth > 0 && wantedHeight > 0 && (wantedWidth != currentWidth || wantedHeight != currentHeight)) + SDL_SetWindowSize(window, wantedWidth, wantedHeight); + } +#endif + + int outputWidth, outputHeight; + SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight); + + if(screen != NULL && screen -> w == outputWidth && screen -> h == outputHeight) return; + + bool heightChanged = screen == NULL || screen -> h != outputHeight; + createDrawingSurface(outputWidth, outputHeight); + + if(heightChanged) { + freeFonts(); + openFonts(); + } + + refreshCameraForScreen(); +} + +/** \brief Létrehozza a játék futtatásához szükséges környezetet. + * Létrehoz egy ablakot a hozzá tartozó rajzfelülettel, illetve + * meghív pár, az inicializáláshoz szükséges függvényt. + */ +void openWindow() { + if(SDL_Init(SDL_INIT_VIDEO) != 0) { + fprintf(stderr, "Nem sikerult inicializalni az SDL-t: %s\n", SDL_GetError()); + exit(1); + } + + moveToResourceDirectory(); + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); + + int windowWidth = defaultWindowWidth; + int windowHeight = defaultWindowHeight; + Uint32 windowFlags = 0; + +#ifdef __EMSCRIPTEN__ + double cssWidth, cssHeight; + if(emscripten_get_element_css_size("#canvas", &cssWidth, &cssHeight) == EMSCRIPTEN_RESULT_SUCCESS) { + double ratio = drawingPixelRatio(); + windowWidth = (int) (cssWidth * ratio); + windowHeight = (int) (cssHeight * ratio); + } +#else + windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; + + SDL_Rect usableBounds; + if(SDL_GetDisplayUsableBounds(0, &usableBounds) == 0) { + float fit = smallerF((float) usableBounds.w / defaultWindowWidth, (float) usableBounds.h / defaultWindowHeight); + if(fit < 1.0f) { + windowWidth = (int) (defaultWindowWidth * fit); + windowHeight = (int) (defaultWindowHeight * fit); + } + } +#endif + + const char* forcedSize = SDL_getenv("GAME_WINDOW"); + if(forcedSize != NULL) sscanf(forcedSize, "%dx%d", &windowWidth, &windowHeight); + + window = SDL_CreateWindow("Platform játék", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + windowWidth, windowHeight, + windowFlags); + + if(window == NULL) { + fprintf(stderr, "Nem sikerult megnyitni az ablakot: %s\n", SDL_GetError()); + exit(1); + } + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if(renderer == NULL) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); + if(renderer == NULL) { + fprintf(stderr, "Nem sikerult letrehozni a renderert: %s\n", SDL_GetError()); + exit(1); + } + +#ifdef __EMSCRIPTEN__ + int screenPixelWidth, screenPixelHeight; + emscripten_get_screen_size(&screenPixelWidth, &screenPixelHeight); + double screenRatio = drawingPixelRatio(); + textureWidth = (int)(screenPixelWidth * screenRatio); + textureHeight = (int)(screenPixelHeight * screenRatio); +#else + SDL_DisplayMode desktop; + if(SDL_GetDesktopDisplayMode(0, &desktop) == 0) { + textureWidth = desktop.w; + textureHeight = desktop.h; + } +#endif + + int outputWidth, outputHeight; + SDL_GetRendererOutputSize(renderer, &outputWidth, &outputHeight); + createDrawingSurface(outputWidth, outputHeight); + + SDL_ShowCursor(SDL_DISABLE); + openFonts(); +} + +void presentFrame() { + SDL_Rect used = {0, 0, screen -> w, screen -> h}; + SDL_UpdateTexture(frameTexture, &used, screen -> pixels, screen -> pitch); + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, frameTexture, &used, NULL); + SDL_RenderPresent(renderer); +} + +void closeWindow() { + SDL_FreeSurface(screen); + SDL_DestroyTexture(frameTexture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); } ///Frissití az egér pozicióját. @@ -58,10 +218,11 @@ mousePos getMousePosition() { ///Lerajzolja a képernyőre az egeret, erre azért van szükség, mert az SDL csak fekete-fehér egérmutatót kezel. void drawMouse(bool isBW) { - Sint16 x1 = mouse.x - mouseSize / 2; - Sint16 x2 = mouse.x + mouseSize / 2; - Sint16 y1 = mouse.y - mouseSize / 2; - Sint16 y2 = mouse.y + mouseSize / 2; + Sint16 size = larger(4, mouseSize * screen -> h / designHeight); + Sint16 x1 = mouse.x - size / 2; + Sint16 x2 = mouse.x + size / 2; + Sint16 y1 = mouse.y - size / 2; + Sint16 y2 = mouse.y + size / 2; boxColor(screen, x1, y1, x2, y2, isBW ? 0x7F7F7FFF : 0xFFFF00FF); } diff --git a/Forráskód/modules/auxiliary/auxSDL.h b/Forráskód/modules/auxiliary/auxSDL.h index 9039e5c..92f1d2c 100644 --- a/Forráskód/modules/auxiliary/auxSDL.h +++ b/Forráskód/modules/auxiliary/auxSDL.h @@ -6,12 +6,24 @@ #define mouseSize 20 /**< Az egérmutató négyzetnek az oldalhosszusága. */ + #define defaultWindowWidth 1920 /**< Az ablak alapértelmezett szélessége. */ + #define defaultWindowHeight 1080 /**< Az ablak alapértelmezett magassága. */ + + /** \brief Böngészőben legfeljebb ennyi rajzfelület-pixel jusson egy css-pixelre. + * A teljes képet a processzor rajzolja, így a költsége a pixelek számával nő. Egy retina + * kijelzőn a natív felbontás négyszer annyi pixelt jelentene; helyette css-méretben rajzolunk, + * a vásznat pedig a böngésző nagyítja fel, ami már ingyen van. + */ + #define maxDevicePixelRatio 1.0 + typedef struct mousePos { Uint16 x, y; } mousePos; - SDL_Cursor* openWindow(); - Uint32 timer(Uint32 ms, void* param); + void openWindow(); + void handleResize(); + void presentFrame(); + void closeWindow(); void setMousePosition(Uint16 x, Uint16 y); mousePos getMousePosition(); void drawMouse(bool isBW); diff --git a/Forráskód/modules/auxiliary/gfxPrimitives.c b/Forráskód/modules/auxiliary/gfxPrimitives.c new file mode 100644 index 0000000..81019d3 --- /dev/null +++ b/Forráskód/modules/auxiliary/gfxPrimitives.c @@ -0,0 +1,300 @@ +#include "gfxPrimitives.h" + +#include +#include +#include + +typedef struct target { + SDL_Surface* surface; + Uint32* pixels; + int pitch, w, h; + int rShift, gShift, bShift; + Uint32 aMask; + bool locked; +} target; + +static bool renderingEnabled = true; /**< Kikapcsolva minden rajzoló primitív azonnal visszatér. */ + +/** \brief Ki-be kapcsolja a rajzolást. + * A behozott képkockákat a main.c "utolérő" ciklusa többször is végigszimulálja, de csak + * az utolsót mutatja meg. A közbülső lépések alatt kikapcsolva a rajzolás nem dolgozik feleslegesen. + */ +void setRenderingEnabled(bool enabled) { + renderingEnabled = enabled; +} + +bool isRenderingEnabled() { + return renderingEnabled; +} + +static bool beginTarget(SDL_Surface* dst, target* t) { + if(!renderingEnabled || dst == NULL || dst -> format -> BytesPerPixel != 4) return false; + + t -> surface = dst; + t -> locked = SDL_MUSTLOCK(dst); + if(t -> locked && SDL_LockSurface(dst) != 0) return false; + + t -> pixels = (Uint32*) dst -> pixels; + t -> pitch = dst -> pitch / 4; + t -> w = dst -> w; + t -> h = dst -> h; + t -> rShift = dst -> format -> Rshift; + t -> gShift = dst -> format -> Gshift; + t -> bShift = dst -> format -> Bshift; + t -> aMask = dst -> format -> Amask; + return true; +} + +static void endTarget(target* t) { + if(t -> locked) SDL_UnlockSurface(t -> surface); +} + +static inline Uint32 packColor(const target* t, Uint8 r, Uint8 g, Uint8 b) { + return t -> aMask | ((Uint32) r << t -> rShift) | ((Uint32) g << t -> gShift) | ((Uint32) b << t -> bShift); +} + +static inline void blendPixel(const target* t, int x, int y, Uint8 r, Uint8 g, Uint8 b, int a) { + if(a <= 0 || x < 0 || y < 0 || x >= t -> w || y >= t -> h) return; + + Uint32* p = t -> pixels + y * t -> pitch + x; + + if(a >= 255) { + *p = packColor(t, r, g, b); + return; + } + + Uint32 c = *p; + int ia = 255 - a; + Uint32 dr = (c >> t -> rShift) & 0xFF; + Uint32 dg = (c >> t -> gShift) & 0xFF; + Uint32 db = (c >> t -> bShift) & 0xFF; + + *p = packColor(t, + (Uint8) ((r * a + dr * ia + 127) / 255), + (Uint8) ((g * a + dg * ia + 127) / 255), + (Uint8) ((b * a + db * ia + 127) / 255)); +} + +static void blendHLine(const target* t, int xa, int xb, int y, Uint8 r, Uint8 g, Uint8 b, int a) { + if(y < 0 || y >= t -> h || a <= 0) return; + if(xa > xb) { int tmp = xa; xa = xb; xb = tmp; } + if(xa < 0) xa = 0; + if(xb > t -> w - 1) xb = t -> w - 1; + if(xa > xb) return; + + Uint32* p = t -> pixels + y * t -> pitch + xa; + int i; + + if(a >= 255) { + Uint32 c = packColor(t, r, g, b); + for(i = xa; i <= xb; i++) *p++ = c; + } + else { + int ia = 255 - a; + int ra = r * a, ga = g * a, ba = b * a; + + for(i = xa; i <= xb; i++, p++) { + Uint32 c = *p; + *p = packColor(t, + (Uint8) ((ra + ((c >> t -> rShift) & 0xFF) * ia + 127) / 255), + (Uint8) ((ga + ((c >> t -> gShift) & 0xFF) * ia + 127) / 255), + (Uint8) ((ba + ((c >> t -> bShift) & 0xFF) * ia + 127) / 255)); + } + } +} + +static inline void unpackColor(Uint32 color, Uint8* r, Uint8* g, Uint8* b, Uint8* a) { + *r = (color >> 24) & 0xFF; + *g = (color >> 16) & 0xFF; + *b = (color >> 8) & 0xFF; + *a = color & 0xFF; +} + +int boxRGBA(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + target t; + if(!beginTarget(dst, &t)) return -1; + + if(x1 > x2) { Sint16 tmp = x1; x1 = x2; x2 = tmp; } + if(y1 > y2) { Sint16 tmp = y1; y1 = y2; y2 = tmp; } + + int yStart = y1 < 0 ? 0 : y1; + int yEnd = y2 > t.h - 1 ? t.h - 1 : y2; + + int y; + for(y = yStart; y <= yEnd; y++) blendHLine(&t, x1, x2, y, r, g, b, a); + + endTarget(&t); + return 0; +} + +int boxColor(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint32 color) { + Uint8 r, g, b, a; + unpackColor(color, &r, &g, &b, &a); + return boxRGBA(dst, x1, y1, x2, y2, r, g, b, a); +} + +static int lineRGBA(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + target t; + if(!beginTarget(dst, &t)) return -1; + + int dx = abs(x2 - x1), dy = -abs(y2 - y1); + int sx = x1 < x2 ? 1 : -1, sy = y1 < y2 ? 1 : -1; + int err = dx + dy; + int x = x1, y = y1; + + while(true) { + blendPixel(&t, x, y, r, g, b, a); + if(x == x2 && y == y2) break; + int e2 = 2 * err; + if(e2 >= dy) { err += dy; x += sx; } + if(e2 <= dx) { err += dx; y += sy; } + } + + endTarget(&t); + return 0; +} + +int lineColor(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint32 color) { + Uint8 r, g, b, a; + unpackColor(color, &r, &g, &b, &a); + return lineRGBA(dst, x1, y1, x2, y2, r, g, b, a); +} + +int filledCircleColor(SDL_Surface* dst, Sint16 x, Sint16 y, Sint16 rad, Uint32 color) { + Uint8 r, g, b, a; + unpackColor(color, &r, &g, &b, &a); + + target t; + if(!beginTarget(dst, &t)) return -1; + + int dy; + for(dy = -rad; dy <= rad; dy++) { + int span = (int) floor(sqrt((double) rad * rad - (double) dy * dy)); + blendHLine(&t, x - span, x + span, y + dy, r, g, b, a); + } + + endTarget(&t); + return 0; +} + +int aacircleColor(SDL_Surface* dst, Sint16 x, Sint16 y, Sint16 rad, Uint32 color) { + Uint8 r, g, b, a; + unpackColor(color, &r, &g, &b, &a); + + target t; + if(!beginTarget(dst, &t)) return -1; + + int dx, dy; + for(dy = -rad - 1; dy <= rad + 1; dy++) + for(dx = -rad - 1; dx <= rad + 1; dx++) { + double d = sqrt((double) dx * dx + (double) dy * dy); + double coverage = 1.0 - fabs(d - rad); + if(coverage > 0.0) + blendPixel(&t, x + dx, y + dy, r, g, b, (int) (a * (coverage > 1.0 ? 1.0 : coverage))); + } + + endTarget(&t); + return 0; +} + +int filledPolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + if(n < 3) return -1; + + target t; + if(!beginTarget(dst, &t)) return -1; + + //A játék végig négyszögeket rajzol, azoknak bőven elég a veremben álló tömb; + //képkockánként több tízezer malloc/free párt spórolunk vele. + double inlineCrossings[16]; + double* crossings = inlineCrossings; + if(n > (int)(sizeof(inlineCrossings) / sizeof(*inlineCrossings))) { + crossings = (double*) malloc(n * sizeof(double)); + if(crossings == NULL) { endTarget(&t); return -1; } + } + + int minY = vy[0], maxY = vy[0]; + int i; + for(i = 1; i < n; i++) { + if(vy[i] < minY) minY = vy[i]; + if(vy[i] > maxY) maxY = vy[i]; + } + + int yStart = minY < 0 ? 0 : minY; + int yEnd = maxY > t.h - 1 ? t.h - 1 : maxY; + + int y; + for(y = yStart; y <= yEnd; y++) { + int count = 0; + for(i = 0; i < n; i++) { + int j = (i + 1) % n; + int y1 = vy[i], y2 = vy[j], x1 = vx[i], x2 = vx[j]; + if(y1 > y2) { int tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } + + if((y >= y1 && y < y2) || (y == maxY && y > y1 && y <= y2)) + crossings[count++] = x1 + ((double) (y - y1) * (x2 - x1)) / (y2 - y1); + } + + if(count < 2) continue; + + //Néhány elemre a beszúrásos rendezés gyorsabb, mint a qsort függvénypointeres hívogatása + int k; + for(i = 1; i < count; i++) { + double key = crossings[i]; + for(k = i - 1; k >= 0 && crossings[k] > key; k--) crossings[k + 1] = crossings[k]; + crossings[k + 1] = key; + } + + for(i = 0; i + 1 < count; i += 2) + blendHLine(&t, (int) floor(crossings[i] + 0.5), (int) floor(crossings[i + 1] + 0.5), y, r, g, b, a); + } + + if(crossings != inlineCrossings) free(crossings); + endTarget(&t); + return 0; +} + +static void aaLine(const target* t, double x0, double y0, double x1, double y1, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + bool steep = fabs(y1 - y0) > fabs(x1 - x0); + double tmp; + if(steep) { tmp = x0; x0 = y0; y0 = tmp; tmp = x1; x1 = y1; y1 = tmp; } + if(x0 > x1) { tmp = x0; x0 = x1; x1 = tmp; tmp = y0; y0 = y1; y1 = tmp; } + + double dx = x1 - x0; + double gradient = dx == 0.0 ? 0.0 : (y1 - y0) / dx; + + int xStart = (int) floor(x0 + 0.5), xEnd = (int) floor(x1 + 0.5); + double intersection = y0 + gradient * (xStart - x0); + + int x; + for(x = xStart; x <= xEnd; x++) { + int base = (int) floor(intersection); + double frac = intersection - base; + + if(steep) { + blendPixel(t, base, x, r, g, b, (int) (a * (1.0 - frac))); + blendPixel(t, base + 1, x, r, g, b, (int) (a * frac)); + } + else { + blendPixel(t, x, base, r, g, b, (int) (a * (1.0 - frac))); + blendPixel(t, x, base + 1, r, g, b, (int) (a * frac)); + } + + intersection += gradient; + } +} + +int aapolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + if(n < 2) return -1; + + target t; + if(!beginTarget(dst, &t)) return -1; + + int i; + for(i = 0; i < n; i++) { + int j = (i + 1) % n; + aaLine(&t, vx[i], vy[i], vx[j], vy[j], r, g, b, a); + } + + endTarget(&t); + return 0; +} diff --git a/Forráskód/modules/auxiliary/gfxPrimitives.h b/Forráskód/modules/auxiliary/gfxPrimitives.h new file mode 100644 index 0000000..e4628cb --- /dev/null +++ b/Forráskód/modules/auxiliary/gfxPrimitives.h @@ -0,0 +1,18 @@ +#ifndef GFXPRIMITIVES_H_INCLUDED +#define GFXPRIMITIVES_H_INCLUDED + + #include + #include + + void setRenderingEnabled(bool enabled); + bool isRenderingEnabled(); + + int boxColor(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint32 color); + int boxRGBA(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a); + int lineColor(SDL_Surface* dst, Sint16 x1, Sint16 y1, Sint16 x2, Sint16 y2, Uint32 color); + int filledCircleColor(SDL_Surface* dst, Sint16 x, Sint16 y, Sint16 rad, Uint32 color); + int aacircleColor(SDL_Surface* dst, Sint16 x, Sint16 y, Sint16 rad, Uint32 color); + int filledPolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a); + int aapolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a); + +#endif // GFXPRIMITIVES_H_INCLUDED diff --git a/Forráskód/modules/auxiliary/unicodePrint.c b/Forráskód/modules/auxiliary/unicodePrint.c index 67cb6a6..3e4e54e 100644 --- a/Forráskód/modules/auxiliary/unicodePrint.c +++ b/Forráskód/modules/auxiliary/unicodePrint.c @@ -3,12 +3,13 @@ ///Indításnál betölti a szükséges betűtípusokat. void openFonts() { - SDL_EnableUNICODE(1); - TTF_Init(); - fontSmaller = TTF_OpenFont("resources/Roboto-Thin.ttf", 40); - fontSmall = TTF_OpenFont("resources/Roboto-Thin.ttf", 48); - fontSmallBold = TTF_OpenFont("resources/Roboto-Light.ttf", 53); - fontLarge = TTF_OpenFont("resources/Roboto-Thin.ttf", 120); + if(!TTF_WasInit()) TTF_Init(); + + float scale = (float) screen -> h / designHeight; + fontSmaller = TTF_OpenFont("resources/Roboto-Thin.ttf", larger(1, (int)(40 * scale))); + fontSmall = TTF_OpenFont("resources/Roboto-Thin.ttf", larger(1, (int)(48 * scale))); + fontSmallBold = TTF_OpenFont("resources/Roboto-Light.ttf", larger(1, (int)(53 * scale))); + fontLarge = TTF_OpenFont("resources/Roboto-Thin.ttf", larger(1, (int)(120 * scale))); } ///Ha már nem kellenek, felszabadítja a betűtípusokat. @@ -20,10 +21,10 @@ void freeFonts() { } ///Itt tárolom a használható karaktereket. Eldönti, hogy a bemenet köztük van-e. -bool isValidChar(Uint16 chr) { +bool isValidChar(wchar_t chr) { if(('A' <= chr && chr <= 'Z') || ('a' <= chr && chr <= 'z') || ('0' <= chr && chr <= '9')) return true; - Uint16 validChars[] = L"öüóőúéáűíÖÜÓŐÚÉÁŰÍ|€ß$Łł¤<>#&@{}Đ[]đ+!%=×)(.- "; + wchar_t validChars[] = L"öüóőúéáűíÖÜÓŐÚÉÁŰÍ|€ß$Łł¤<>#&@{}Đ[]đ+!%=×)(.- "; int i; for(i = 0; validChars[i] != 0; i++) if(chr == validChars[i]) return true; @@ -57,14 +58,14 @@ Sint16 percentageToPx(int percentage, bool isWidth) { * \param string vége * \return string konkatenált */ -Uint16* concatUnicode(const Uint16 a[], const Uint16 b[]) { +wchar_t* concatUnicode(const wchar_t a[], const wchar_t b[]) { int aLength, bLength; for(aLength = 0; a[aLength] != 0; aLength++) ; for(bLength = 0; b[bLength] != 0; bLength++) ; - Uint16* newStr = (Uint16*) malloc((aLength + bLength - 1 + 100) * sizeof(Uint16)); + wchar_t* newStr = (wchar_t*) malloc((aLength + bLength - 1 + 100) * sizeof(wchar_t)); int i = 0, ai, bi; for(ai = 0; ai < aLength; ai++) newStr[i++] = a[ai]; @@ -76,9 +77,79 @@ Uint16* concatUnicode(const Uint16 a[], const Uint16 b[]) { } ///A megjelenítendő szöveg közepét kell megadni x,y-ként. A szöveget Uint16*-ban, a betűtipust pedig a betűtipus enumból. Ugyan mindent x = 50-nel hívunk meg, ez később változhat, ezért hagyom meg paraméternek. -void unicodePrintCentered(Uint16 textToWrite[], int x, int y, fontType fontType) { +void unicodePrintCentered(wchar_t textToWrite[], int x, int y, fontType fontType) { + char utf8[2048]; + wideToUtf8(textToWrite, utf8, sizeof(utf8)); + utf8PrintCentered(utf8, x, y, fontType); +} + +///Ugyanaz, mint az unicodePrintCentered, csak utf-8-as bemenettel. +void utf8PrintCentered(const char* textToWrite, int x, int y, fontType fontType) { + if(!isRenderingEnabled()) return; + TTF_Font* currentFont = selectFont(fontType); SDL_FreeSurface(text); - text = TTF_RenderUNICODE_Blended(currentFont, textToWrite, (SDL_Color) {255, 255, 255}); + text = TTF_RenderUTF8_Blended(currentFont, textToWrite, (SDL_Color) {255, 255, 255}); + if(text == NULL) return; SDL_BlitSurface(text, NULL, screen, &(SDL_Rect){percentageToPx(x, true) - text -> w / 2, percentageToPx(y, false) - text -> h / 2, 0, 0}); } + +int wideToUtf8(const wchar_t* source, char* destination, int destinationSize) { + int written = 0; + + for(; *source != 0; source++) { + unsigned int c = (unsigned int) *source; + int needed = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; + if(written + needed >= destinationSize) break; + + switch(needed) { + case 1: + destination[written++] = c; + break; + case 2: + destination[written++] = 0xC0 | (c >> 6); + destination[written++] = 0x80 | (c & 0x3F); + break; + case 3: + destination[written++] = 0xE0 | (c >> 12); + destination[written++] = 0x80 | ((c >> 6) & 0x3F); + destination[written++] = 0x80 | (c & 0x3F); + break; + default: + destination[written++] = 0xF0 | (c >> 18); + destination[written++] = 0x80 | ((c >> 12) & 0x3F); + destination[written++] = 0x80 | ((c >> 6) & 0x3F); + destination[written++] = 0x80 | (c & 0x3F); + } + } + + destination[written] = 0; + return written; +} + +int utf8ToWide(const char* source, wchar_t* destination, int destinationSize) { + int written = 0; + + while(*source != 0 && written < destinationSize - 1) { + unsigned char lead = (unsigned char) *source++; + unsigned int c; + int continuations; + + if(lead < 0x80) { c = lead; continuations = 0; } + else if((lead & 0xE0) == 0xC0) { c = lead & 0x1F; continuations = 1; } + else if((lead & 0xF0) == 0xE0) { c = lead & 0x0F; continuations = 2; } + else if((lead & 0xF8) == 0xF0) { c = lead & 0x07; continuations = 3; } + else continue; + + int i; + for(i = 0; i < continuations; i++) { + if((*source & 0xC0) != 0x80) break; + c = (c << 6) | (*source++ & 0x3F); + } + + destination[written++] = (wchar_t) c; + } + + destination[written] = 0; + return written; +} diff --git a/Forráskód/modules/auxiliary/unicodePrint.h b/Forráskód/modules/auxiliary/unicodePrint.h index 1e0ecb8..d2051f8 100644 --- a/Forráskód/modules/auxiliary/unicodePrint.h +++ b/Forráskód/modules/auxiliary/unicodePrint.h @@ -9,8 +9,11 @@ void openFonts(); void freeFonts(); - bool isValidChar(Uint16 chr); - Uint16* concatUnicode(const Uint16 a[], const Uint16 b[]); - void unicodePrintCentered(Uint16 textToWrite[], int x, int y, fontType fontType); + bool isValidChar(wchar_t chr); + wchar_t* concatUnicode(const wchar_t a[], const wchar_t b[]); + void unicodePrintCentered(wchar_t textToWrite[], int x, int y, fontType fontType); + void utf8PrintCentered(const char* textToWrite, int x, int y, fontType fontType); + int wideToUtf8(const wchar_t* source, char* destination, int destinationSize); + int utf8ToWide(const char* source, wchar_t* destination, int destinationSize); #endif // UNICODEPRINT_H_INCLUDED diff --git a/Forráskód/modules/camera.c b/Forráskód/modules/camera.c index 34abbdf..7fe4f9e 100644 --- a/Forráskód/modules/camera.c +++ b/Forráskód/modules/camera.c @@ -1,326 +1,331 @@ -#include "camera.h" - -#define focalLength 0.05 /**< gyújtótáv [m] */ -#define sensorSize 0.07 /**< szenzor hosszabb oldalának a fele [m] */ -#define renderObjectsPerCube 10 /**< max( (projectile + player + enemy) / m^3), erről később bővebben. */ - -vector3D cameraPosition; /**< A kamera térbeli helyvektora. */ -float magnification, columns, rows; /**< Ennyi látszik a pályából [m]. */ -bool watchBounds = false; /**< Feldarabolós rajzolás van-e? */ -float leftBound, rightBound, upperBound, lowerBound; /**< Darabolás határai. */ -int leftMostBound, rightMostBound, topBound, bottomBound; -renderObject*** renderMap = NULL; /**< Darabolás mátrixa. */ - - -///A feldarabolós rajzoláshoz szükséges mátrixot szabadítja fel. -void freeRenderMap() { - if(renderMap != NULL) { - free(renderMap[0][0]); - free(renderMap[0]); - free(renderMap); - } -} - -///A feldarabolós rajzoláshoz szükséges mátrixot mallocolja. -static void initRenderMap() { - renderMap = (renderObject***) malloc(sizeof(renderObject**)); - renderMap[0] = (renderObject**) malloc(sizeof(renderObject*)); - renderMap[0][0] = (renderObject*) malloc(sizeof(renderObject)); -} - -/** \brief Kiszámolja a kamera poziciója és a képernyő mérete alapján, hogy mennyi látszik a pályából. - * - * Elsősorban ezeket a változókat változtatja: - * columns, rows = képernyőn megjelenő oszlopok és sorok száma, - * magnification = megmondja egy méter a kamera "szenzorán" hány pixelnek felel meg (jó soknak) - * - * Hasonló háromszögek alapján kiszámolja mennyi látszik a pályából: - * sensor size - * | - * V - * ---- - * \ | - * \ | <- focalLength - * \| - * | - * |\ - * | \ - * Z -> | \ - * | \ - * | \ - * ------------- - * columns - * - * Ezek alapján reallocolja a darabolós mátrixot. - * - */ -static void calculateRowsAndColumns() { - magnification = (float) screen -> w / 2 / sensorSize; - columns = 2 * cameraPosition.z * sensorSize / focalLength; - //hogy a négyzet négyzet is maradjon - rows = (float)screen -> h / screen -> w * columns; - - if(renderMap == NULL) initRenderMap(); - - int xSize = (int)columns + 4; //a nagyobb testek átlóghatnak a képernyő határán - int ySize = (int)rows + 4; - int iSize = renderObjectsPerCube; - - renderObject* innerTmp = (renderObject*) realloc(renderMap[0][0], xSize * ySize * iSize * sizeof(renderObject)); - renderObject** outerTmp = (renderObject**) realloc(renderMap[0], xSize * ySize * sizeof(renderObject*)); - renderMap = (renderObject***) realloc(renderMap, xSize * sizeof(renderObject**)); - - renderMap[0] = outerTmp; - - int x; - for(x = 1; x < xSize; x++) - renderMap[x] = renderMap[0] + x * ySize; - - renderMap[0][0] = innerTmp; - - renderObject empty; - empty.doesExist = false; - - int y, i = 0; - int j; - for(x = 0; x < xSize; x++) - for(y = 0; y < ySize; y++) { - renderMap[x][y] = renderMap[0][0] + i++ * iSize; - for(j = 0; j < iSize; j++) renderMap[x][y][j] = empty; - } -} - -///A játékost követi a kamerával, nem hagyja, hogy a pálya alá lásson. -void followPlayerWithCamera() { - renderObject player = getPlayerRenderObject(); - cameraPosition.x = (2 * player.position.x + player.size.x) / 2; - cameraPosition.y = (player.position.y < rows / 2 ? rows / 2 : player.position.y) + 1; -} - -///A menüben használt kamerát állítja be, visszaadja a columns-t és rows-t is. -void menuCamera(int* getColumns, int* getRows) { - cameraPosition.z = cameraDeltaZDistanceStart; - calculateRowsAndColumns(); - *getColumns = columns; - *getRows = rows; - cameraPosition.x = columns / 2; - cameraPosition.y = rows / 2; -} - -///Visszaadja, hogy hány oszlop látszik a pályából. -int getColumns() { - return columns; -} - -/** \brief A kamera dollyzását végzi, a kamerát mozgatja - * A dolly állását staticban tárolja, ezért új dolly eseteén először egy restart = true-val kell meghívni, - * \param ms eltelt idő - * \param kell-e újrakezdés? - * \return elérte-e a végállást? - * Minden frameban újraszámolja a rows&coulmns-t, - */ -bool cameraDollyZ(int deltaTime, bool restart) { - static int timePassed; - if(restart) timePassed = 0; - - bool done = true; - - timePassed += deltaTime; - - float t = (float)(cameraDollyTime - timePassed) / cameraDollyTime; - float cube = t*t*t; - - float currentZ = cameraDeltaZDistanceEnd + (cameraDeltaZDistanceStart - cameraDeltaZDistanceEnd) * cube; - - if(currentZ <= cameraDeltaZDistanceEnd) { - currentZ = cameraDeltaZDistanceEnd; - done = false; - } - - cameraPosition.z = currentZ; - calculateRowsAndColumns(); - return done; -} - -/** \brief Térbeli pontból síkbelit képez. - * A 3D-s pont koordinátái és a kamera alapján kiszámolja, hogy a kamera 2D-s szenzorán hol jelenik meg a pont. - * Geometriailag leírva, a két ponton (point, cameraPosition) átmenő egyenes egyenletének, és a 'szenzor' vízszintesének metszéspontja - * a keresett pontunk. A lent és a linken is látható egyenletet először a tér x-z, majd az y-z vetületére alkalmazzuk, - * és megkapjuk a térbeli pontunk megfelelő síkbeli leképezést. - * Ez interaktívan ábrázolva így néz ki: https://www.desmos.com/calculator/8qjvkptjmn - * \param Térvektor típusú pont - * \return Síkvektor típusú pont - * A visszaadott pontnak int koordinátái vannak, hiszen valamelyik pixel koordinátáit jelöli. - * Ez a függvény igazából egy camera obscura modellje. - */ -intVector2D get2DPoint(vector3D point) { - //Ha watchBounds = true, csak egy megadott kockán belül adja vissza az értéket (rajzol), ez a feldarabolás miatt fontos - if(watchBounds) { - putInsideIntervalF(leftBound, &point.x, rightBound); - putInsideIntervalF(lowerBound, &point.y, upperBound); - } - - //Ezt a két kifejezést újra felhasználjuk - float zDist = point.z - cameraPosition.z; - float zDistFromHole = zDist - focalLength; - float dx = (cameraPosition.x * zDistFromHole + focalLength * point.x) / zDist - cameraPosition.x; - float dy = (cameraPosition.y * zDistFromHole + focalLength * point.y) / zDist - cameraPosition.y; - - //Megfelelően tükrözzük a kapot pontot, és a méterben kapott értéket átváltjuk pixelekre - intVector2D resultPoint = { - (int)(screen -> w / 2 - magnification * dx), - (int)(screen -> h / 2 + magnification * dy) - }; - - return resultPoint; -} - -/** \brief Lerajzol egy négyszöget - * A bemenetként kapott 4 pont alkotta négyszöget rajzolja ki. - * \param síkvektor a csúcshoz - * \param síkvektor b csúcshoz - * \param síkvektor c csúcshoz - * \param síkvektor d csúcshoz - * \param szín négyszög színe - * Ezt egyszer kitöltéssel csinálja, egyszer pedig anti-aliasing poligonnal, - * hogy a rajz széle ne legyen recegős. - */ -static void drawTetragon(intVector2D a, intVector2D b, intVector2D c, intVector2D d, color color) { - Sint16 xCoords[4] = {(Sint16)a.x, (Sint16)b.x, (Sint16)c.x, (Sint16)d.x}; - Sint16 yCoords[4] = {(Sint16)a.y, (Sint16)b.y, (Sint16)c.y, (Sint16)d.y}; - - filledPolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a); - aapolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a); -} - -/** \brief Lerajzol egy téglatestet - * A kamera modul lényege, a rajzolást végzi el az eddigi függvények segítségével. - * \param test téglatest adatai - * \param bool melyik oldalakat kell rajzolni? - * Végez egy plusz ellenőrést is, nem csak a paraméterben megadott tömb alapján nem rajzol oldalakat, - * azokat se rajzolja le, amiket a helyzetük miatt biztosan nem láthat a kamera. - */ -void drawCuboid(renderObject object, bool sides[]) { - intVector2D vertexes[8]; - //A téglatestet bejárva megkeressük a csúcsait - int i; - for(i = 0; i < 8; i++) { - vertexes[i] = get2DPoint(object.position); - switch(i % 4) { - case 0: - object.position.x += object.size.x; - break; - case 1: - object.position.y += object.size.y; - break; - case 2: - object.position.x -= object.size.x; - break; - case 3: - object.position.y -= object.size.y; - } - if(i == 3) object.position.z += object.size.z; - } - - //alja - if(sides[0] && object.position.y > cameraPosition.y) drawTetragon(vertexes[0], vertexes[4], vertexes[5], vertexes[1], object.colors.dark); - //bal oldali lap - if(sides[1] && object.position.x > cameraPosition.x) drawTetragon(vertexes[0], vertexes[3], vertexes[7], vertexes[4], object.colors.dark); - //jobb oldali lap - if(sides[2] && object.position.x + object.size.x < cameraPosition.x) drawTetragon(vertexes[1], vertexes[2], vertexes[6], vertexes[5], object.colors.dark); - //teteje - if(sides[3] && object.position.y + object.size.y < cameraPosition.y) drawTetragon(vertexes[3], vertexes[7], vertexes[6], vertexes[2], object.colors.light); - //eleje - if(sides[4]) drawTetragon(vertexes[4], vertexes[5], vertexes[6], vertexes[7], object.colors.normal); -} - -/// Az egy m^3-ben lévő testeket rajzolja le -static void drawPlace(int x, int* y, int direction, int bound) { - bool sides[5] = {true, true, true, true, true}; - - renderObject toDraw = getBlockRenderObject(x, y, direction, bound, sides); - if(toDraw.doesExist) { - watchBounds = false; - drawCuboid(toDraw, sides); - watchBounds = true; - } - else { - int i = 0; - toDraw = renderMap[x - leftMostBound][*y - bottomBound][i]; - while(toDraw.doesExist && i < renderObjectsPerCube) { - drawCuboid(toDraw, sides); - i++; - toDraw = renderMap[x - leftMostBound][*y - bottomBound][i]; - } - - renderObject player = getPlayerRenderObject(); - - if( - (x == (int)floor(player.position.x) || x == (int)floor(player.position.x + player.size.x)) && - (*y == (int)floor(player.position.y) || *y == (int)floor(player.position.y + player.size.y)) - ) - drawCuboid(player, sides); - } -} - -///Az egy oszlopban lévő testeket rajzolja le, a részecskéket mozgatja közvetlen -static void drawY(int deltaTime, int x) { - int y; - - for(y = topBound; y > cameraPosition.y; y--) { - upperBound = y + 1; - lowerBound = y; - drawPlace(x, &y, -1, (int)cameraPosition.y); - } - for(y = bottomBound; y <= cameraPosition.y; y++) { - upperBound = y + 1; - lowerBound = y; - drawPlace(x, &y, 1, (int)cameraPosition.y + 1); - } - watchBounds = false; - drawParticles(x, bottomBound, topBound, deltaTime); - watchBounds = true; -} - -/** \brief Lerajzolja a teljes látható játékteret. - * A megfelelő rajzoláshoz figyelni kell a testek takarására, nem mindegy ezért, hogy milyen sorrendben rajzoljuk őket. - * A részecskéknél ezzel nincs probléma, azokat bármilyen sorrendben rajzolhatjuk. Ők a játékteret kitakarják, egymásnál viszont nem látszik, - * hogy melyik takrja melyiket, ezért azokat elég a játéktér után bármilyen sorrendben lerajzolni. - * A térképet alkotó kockáknál már nem ilyen egyszeű a helyzet. Azokat 4 irányból kell rajzolni, bal lentről, jobb lentről, bal fentről, illetve jobb fentről középre. - * Ha ebben a négy lépésben rajzoljuk őket (ezek között a sorrend nem számít) akkor mindig a legfesőket rajzoljuk utoljára, és így valósnak néz ki az eredmény. - * A játékos, a lövedékek, illetve az ellenségek viszont sajnos nincsenek egy négyzethálóhoz kötve. Így gyakran megesik, hogy az egyik felüket a pálya - * egyik kockája előtt, a másikat pedig az után, de még a kövektező előtt kéne rajzolni. Ezt nem tudjuk megcsinálni, hatalmas erőforrásba kerülne ez a majdnem pixelről - * pixelre történő szkennelés. A pályát továbbra is m^3-ént szeretnénk lerajzolni. Hogy ez megfelelően nézzen ki, minden köbméterben csak az abban a kockában lévő dolgokat - * szeretnénk megrajzolni. Ehhez fel kell "darabolnunk" a játékost, a lövedékeket, illetve az ellenségeket is. A darabolás azt jelenti, hogy egy mátrix minden cellájába - * beírjuk, hogy az adott kockában melyik, milyen tulajdonsággal rendelkező testek lógnak be. Rajzolásnál pedig a testeknek mindig csak az adott kockabeli részét rajzoljuk le. - * Mivel a kockákat jó sorrendben rajzoljuk, így a bennük levő "testekrészeket" is jó sorrendbe fogjuk rajzolni. A problémát megoldottuk. - * \param ms eltelt idő - * Ez a függvény elvégzi a részecskék, a pálya, illetve minden más test rajzolását is. A részecskék mozgatása innen van meghívva. - */ -void drawMap(int deltaTime) { - leftMostBound = (int)(cameraPosition.x - (float)columns / 2) - 1; - rightMostBound = (int)(cameraPosition.x + (float)columns / 2) + 1; - topBound = (int)(cameraPosition.y + (float)rows / 2 + 1); - bottomBound = (int)(cameraPosition.y - (float)rows / 2 - 1); - - int y, x; - - for(x = leftMostBound; x < rightMostBound; x++) - for(y = bottomBound; y < topBound; y++) - getFloatersRenderObject(x, y, renderMap[x - leftMostBound][y - bottomBound], renderObjectsPerCube); - - float slicer = cameraPosition.x; - watchBounds = true; - for(x = leftMostBound; x <= slicer; x++) { - leftBound = x - 0.005; - rightBound = x + 1 + 0.005; - drawY(deltaTime, x); - } - for(x = rightMostBound; x > slicer - 1; x--) { - leftBound = x - 0.005; - rightBound = x + 1 + 0.005; - drawY(deltaTime, x); - } - - watchBounds = false; -} +#include "camera.h" + +#define focalLength 0.05 /**< gyújtótáv [m] */ +#define sensorSize 0.07 /**< szenzor hosszabb oldalának a fele [m] */ +#define renderObjectsPerCube 10 /**< max( (projectile + player + enemy) / m^3), erről később bővebben. */ + +vector3D cameraPosition; /**< A kamera térbeli helyvektora. */ +float magnification, columns, rows; /**< Ennyi látszik a pályából [m]. */ +bool watchBounds = false; /**< Feldarabolós rajzolás van-e? */ +float leftBound, rightBound, upperBound, lowerBound; /**< Darabolás határai. */ +int leftMostBound, rightMostBound, topBound, bottomBound; +renderObject*** renderMap = NULL; /**< Darabolás mátrixa. */ + + +///A feldarabolós rajzoláshoz szükséges mátrixot szabadítja fel. +void freeRenderMap() { + if(renderMap != NULL) { + free(renderMap[0][0]); + free(renderMap[0]); + free(renderMap); + } +} + +///A feldarabolós rajzoláshoz szükséges mátrixot mallocolja. +static void initRenderMap() { + renderMap = (renderObject***) malloc(sizeof(renderObject**)); + renderMap[0] = (renderObject**) malloc(sizeof(renderObject*)); + renderMap[0][0] = (renderObject*) malloc(sizeof(renderObject)); +} + +/** \brief Kiszámolja a kamera poziciója és a képernyő mérete alapján, hogy mennyi látszik a pályából. + * + * Elsősorban ezeket a változókat változtatja: + * columns, rows = képernyőn megjelenő oszlopok és sorok száma, + * magnification = megmondja egy méter a kamera "szenzorán" hány pixelnek felel meg (jó soknak) + * + * Hasonló háromszögek alapján kiszámolja mennyi látszik a pályából: + * sensor size + * | + * V + * ---- + * \ | + * \ | <- focalLength + * \| + * | + * |\ + * | \ + * Z -> | \ + * | \ + * | \ + * ------------- + * columns + * + * Ezek alapján reallocolja a darabolós mátrixot. + * + */ +static void calculateRowsAndColumns() { + magnification = (float) screen -> w / 2 / sensorSize; + columns = 2 * cameraPosition.z * sensorSize / focalLength; + //hogy a négyzet négyzet is maradjon + rows = (float)screen -> h / screen -> w * columns; + + if(renderMap == NULL) initRenderMap(); + + int xSize = (int)columns + 4; //a nagyobb testek átlóghatnak a képernyő határán + int ySize = (int)rows + 4; + int iSize = renderObjectsPerCube; + + renderObject* innerTmp = (renderObject*) realloc(renderMap[0][0], xSize * ySize * iSize * sizeof(renderObject)); + renderObject** outerTmp = (renderObject**) realloc(renderMap[0], xSize * ySize * sizeof(renderObject*)); + renderMap = (renderObject***) realloc(renderMap, xSize * sizeof(renderObject**)); + + renderMap[0] = outerTmp; + + int x; + for(x = 1; x < xSize; x++) + renderMap[x] = renderMap[0] + x * ySize; + + renderMap[0][0] = innerTmp; + + renderObject empty; + empty.doesExist = false; + + int y, i = 0; + int j; + for(x = 0; x < xSize; x++) + for(y = 0; y < ySize; y++) { + renderMap[x][y] = renderMap[0][0] + i++ * iSize; + for(j = 0; j < iSize; j++) renderMap[x][y][j] = empty; + } +} + +///A képernyő méretének változása után újraszámolja a látható területet és átméretezi a darabolós mátrixot. +void refreshCameraForScreen() { + calculateRowsAndColumns(); +} + +///A játékost követi a kamerával, nem hagyja, hogy a pálya alá lásson. +void followPlayerWithCamera() { + renderObject player = getPlayerRenderObject(); + cameraPosition.x = (2 * player.position.x + player.size.x) / 2; + cameraPosition.y = (player.position.y < rows / 2 ? rows / 2 : player.position.y) + 1; +} + +///A menüben használt kamerát állítja be, visszaadja a columns-t és rows-t is. +void menuCamera(int* getColumns, int* getRows) { + cameraPosition.z = cameraDeltaZDistanceStart; + calculateRowsAndColumns(); + *getColumns = columns; + *getRows = rows; + cameraPosition.x = columns / 2; + cameraPosition.y = rows / 2; +} + +///Visszaadja, hogy hány oszlop látszik a pályából. +int getColumns() { + return columns; +} + +/** \brief A kamera dollyzását végzi, a kamerát mozgatja + * A dolly állását staticban tárolja, ezért új dolly eseteén először egy restart = true-val kell meghívni, + * \param ms eltelt idő + * \param kell-e újrakezdés? + * \return elérte-e a végállást? + * Minden frameban újraszámolja a rows&coulmns-t, + */ +bool cameraDollyZ(int deltaTime, bool restart) { + static int timePassed; + if(restart) timePassed = 0; + + bool done = true; + + timePassed += deltaTime; + + float t = (float)(cameraDollyTime - timePassed) / cameraDollyTime; + float cube = t*t*t; + + float currentZ = cameraDeltaZDistanceEnd + (cameraDeltaZDistanceStart - cameraDeltaZDistanceEnd) * cube; + + if(currentZ <= cameraDeltaZDistanceEnd) { + currentZ = cameraDeltaZDistanceEnd; + done = false; + } + + cameraPosition.z = currentZ; + calculateRowsAndColumns(); + return done; +} + +/** \brief Térbeli pontból síkbelit képez. + * A 3D-s pont koordinátái és a kamera alapján kiszámolja, hogy a kamera 2D-s szenzorán hol jelenik meg a pont. + * Geometriailag leírva, a két ponton (point, cameraPosition) átmenő egyenes egyenletének, és a 'szenzor' vízszintesének metszéspontja + * a keresett pontunk. A lent és a linken is látható egyenletet először a tér x-z, majd az y-z vetületére alkalmazzuk, + * és megkapjuk a térbeli pontunk megfelelő síkbeli leképezést. + * Ez interaktívan ábrázolva így néz ki: https://www.desmos.com/calculator/8qjvkptjmn + * \param Térvektor típusú pont + * \return Síkvektor típusú pont + * A visszaadott pontnak int koordinátái vannak, hiszen valamelyik pixel koordinátáit jelöli. + * Ez a függvény igazából egy camera obscura modellje. + */ +intVector2D get2DPoint(vector3D point) { + //Ha watchBounds = true, csak egy megadott kockán belül adja vissza az értéket (rajzol), ez a feldarabolás miatt fontos + if(watchBounds) { + putInsideIntervalF(leftBound, &point.x, rightBound); + putInsideIntervalF(lowerBound, &point.y, upperBound); + } + + //Ezt a két kifejezést újra felhasználjuk + float zDist = point.z - cameraPosition.z; + float zDistFromHole = zDist - focalLength; + float dx = (cameraPosition.x * zDistFromHole + focalLength * point.x) / zDist - cameraPosition.x; + float dy = (cameraPosition.y * zDistFromHole + focalLength * point.y) / zDist - cameraPosition.y; + + //Megfelelően tükrözzük a kapot pontot, és a méterben kapott értéket átváltjuk pixelekre + intVector2D resultPoint = { + (int)(screen -> w / 2 - magnification * dx), + (int)(screen -> h / 2 + magnification * dy) + }; + + return resultPoint; +} + +/** \brief Lerajzol egy négyszöget + * A bemenetként kapott 4 pont alkotta négyszöget rajzolja ki. + * \param síkvektor a csúcshoz + * \param síkvektor b csúcshoz + * \param síkvektor c csúcshoz + * \param síkvektor d csúcshoz + * \param szín négyszög színe + * Ezt egyszer kitöltéssel csinálja, egyszer pedig anti-aliasing poligonnal, + * hogy a rajz széle ne legyen recegős. + */ +static void drawTetragon(intVector2D a, intVector2D b, intVector2D c, intVector2D d, color color) { + Sint16 xCoords[4] = {(Sint16)a.x, (Sint16)b.x, (Sint16)c.x, (Sint16)d.x}; + Sint16 yCoords[4] = {(Sint16)a.y, (Sint16)b.y, (Sint16)c.y, (Sint16)d.y}; + + filledPolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a); + aapolygonRGBA(screen, xCoords, yCoords, 4, color.r, color.g, color.b, color.a); +} + +/** \brief Lerajzol egy téglatestet + * A kamera modul lényege, a rajzolást végzi el az eddigi függvények segítségével. + * \param test téglatest adatai + * \param bool melyik oldalakat kell rajzolni? + * Végez egy plusz ellenőrést is, nem csak a paraméterben megadott tömb alapján nem rajzol oldalakat, + * azokat se rajzolja le, amiket a helyzetük miatt biztosan nem láthat a kamera. + */ +void drawCuboid(renderObject object, bool sides[]) { + intVector2D vertexes[8]; + //A téglatestet bejárva megkeressük a csúcsait + int i; + for(i = 0; i < 8; i++) { + vertexes[i] = get2DPoint(object.position); + switch(i % 4) { + case 0: + object.position.x += object.size.x; + break; + case 1: + object.position.y += object.size.y; + break; + case 2: + object.position.x -= object.size.x; + break; + case 3: + object.position.y -= object.size.y; + } + if(i == 3) object.position.z += object.size.z; + } + + //alja + if(sides[0] && object.position.y > cameraPosition.y) drawTetragon(vertexes[0], vertexes[4], vertexes[5], vertexes[1], object.colors.dark); + //bal oldali lap + if(sides[1] && object.position.x > cameraPosition.x) drawTetragon(vertexes[0], vertexes[3], vertexes[7], vertexes[4], object.colors.dark); + //jobb oldali lap + if(sides[2] && object.position.x + object.size.x < cameraPosition.x) drawTetragon(vertexes[1], vertexes[2], vertexes[6], vertexes[5], object.colors.dark); + //teteje + if(sides[3] && object.position.y + object.size.y < cameraPosition.y) drawTetragon(vertexes[3], vertexes[7], vertexes[6], vertexes[2], object.colors.light); + //eleje + if(sides[4]) drawTetragon(vertexes[4], vertexes[5], vertexes[6], vertexes[7], object.colors.normal); +} + +/// Az egy m^3-ben lévő testeket rajzolja le +static void drawPlace(int x, int* y, int direction, int bound) { + bool sides[5] = {true, true, true, true, true}; + + renderObject toDraw = getBlockRenderObject(x, y, direction, bound, sides); + if(toDraw.doesExist) { + watchBounds = false; + drawCuboid(toDraw, sides); + watchBounds = true; + } + else { + int i = 0; + toDraw = renderMap[x - leftMostBound][*y - bottomBound][i]; + while(toDraw.doesExist && i < renderObjectsPerCube) { + drawCuboid(toDraw, sides); + i++; + toDraw = renderMap[x - leftMostBound][*y - bottomBound][i]; + } + + renderObject player = getPlayerRenderObject(); + + if( + (x == (int)floor(player.position.x) || x == (int)floor(player.position.x + player.size.x)) && + (*y == (int)floor(player.position.y) || *y == (int)floor(player.position.y + player.size.y)) + ) + drawCuboid(player, sides); + } +} + +///Az egy oszlopban lévő testeket rajzolja le, a részecskéket mozgatja közvetlen +static void drawY(int deltaTime, int x) { + int y; + + for(y = topBound; y > cameraPosition.y; y--) { + upperBound = y + 1; + lowerBound = y; + drawPlace(x, &y, -1, (int)cameraPosition.y); + } + for(y = bottomBound; y <= cameraPosition.y; y++) { + upperBound = y + 1; + lowerBound = y; + drawPlace(x, &y, 1, (int)cameraPosition.y + 1); + } + watchBounds = false; + drawParticles(x, bottomBound, topBound, deltaTime); + watchBounds = true; +} + +/** \brief Lerajzolja a teljes látható játékteret. + * A megfelelő rajzoláshoz figyelni kell a testek takarására, nem mindegy ezért, hogy milyen sorrendben rajzoljuk őket. + * A részecskéknél ezzel nincs probléma, azokat bármilyen sorrendben rajzolhatjuk. Ők a játékteret kitakarják, egymásnál viszont nem látszik, + * hogy melyik takrja melyiket, ezért azokat elég a játéktér után bármilyen sorrendben lerajzolni. + * A térképet alkotó kockáknál már nem ilyen egyszeű a helyzet. Azokat 4 irányból kell rajzolni, bal lentről, jobb lentről, bal fentről, illetve jobb fentről középre. + * Ha ebben a négy lépésben rajzoljuk őket (ezek között a sorrend nem számít) akkor mindig a legfesőket rajzoljuk utoljára, és így valósnak néz ki az eredmény. + * A játékos, a lövedékek, illetve az ellenségek viszont sajnos nincsenek egy négyzethálóhoz kötve. Így gyakran megesik, hogy az egyik felüket a pálya + * egyik kockája előtt, a másikat pedig az után, de még a kövektező előtt kéne rajzolni. Ezt nem tudjuk megcsinálni, hatalmas erőforrásba kerülne ez a majdnem pixelről + * pixelre történő szkennelés. A pályát továbbra is m^3-ént szeretnénk lerajzolni. Hogy ez megfelelően nézzen ki, minden köbméterben csak az abban a kockában lévő dolgokat + * szeretnénk megrajzolni. Ehhez fel kell "darabolnunk" a játékost, a lövedékeket, illetve az ellenségeket is. A darabolás azt jelenti, hogy egy mátrix minden cellájába + * beírjuk, hogy az adott kockában melyik, milyen tulajdonsággal rendelkező testek lógnak be. Rajzolásnál pedig a testeknek mindig csak az adott kockabeli részét rajzoljuk le. + * Mivel a kockákat jó sorrendben rajzoljuk, így a bennük levő "testekrészeket" is jó sorrendbe fogjuk rajzolni. A problémát megoldottuk. + * \param ms eltelt idő + * Ez a függvény elvégzi a részecskék, a pálya, illetve minden más test rajzolását is. A részecskék mozgatása innen van meghívva. + */ +void drawMap(int deltaTime) { + leftMostBound = (int)(cameraPosition.x - (float)columns / 2) - 1; + rightMostBound = (int)(cameraPosition.x + (float)columns / 2) + 1; + topBound = (int)(cameraPosition.y + (float)rows / 2 + 1); + bottomBound = (int)(cameraPosition.y - (float)rows / 2 - 1); + + int y, x; + + for(x = leftMostBound; x < rightMostBound; x++) + for(y = bottomBound; y < topBound; y++) + getFloatersRenderObject(x, y, renderMap[x - leftMostBound][y - bottomBound], renderObjectsPerCube); + + float slicer = cameraPosition.x; + watchBounds = true; + for(x = leftMostBound; x <= slicer; x++) { + leftBound = x - 0.005; + rightBound = x + 1 + 0.005; + drawY(deltaTime, x); + } + for(x = rightMostBound; x > slicer - 1; x--) { + leftBound = x - 0.005; + rightBound = x + 1 + 0.005; + drawY(deltaTime, x); + } + + watchBounds = false; +} diff --git a/Forráskód/modules/camera.h b/Forráskód/modules/camera.h index 4380a3c..cbc78c0 100644 --- a/Forráskód/modules/camera.h +++ b/Forráskód/modules/camera.h @@ -13,6 +13,7 @@ #define cameraDollyTime 3250 /**<[ms]*/ void freeRenderMap(); + void refreshCameraForScreen(); void followPlayerWithCamera(); void menuCamera(int* getColumns, int* getRows); int getColumns(); diff --git a/Forráskód/modules/global.h b/Forráskód/modules/global.h index 7704681..2326bea 100644 --- a/Forráskód/modules/global.h +++ b/Forráskód/modules/global.h @@ -7,7 +7,7 @@ #include #include -#include +#include "auxiliary/gfxPrimitives.h" #include "auxiliary/auxFunctions.h" @@ -15,23 +15,24 @@ #define GLOBAL_H_INCLUDED #define FPS (int) 50 +#define designHeight 1080 /**< Ehhez a magassághoz vannak méretezve a betűtípusok. */ -float g; +extern float g; -/// -SDL_Surface* screen; -TTF_Font* fontSmaller; -TTF_Font* fontSmall; -TTF_Font* fontSmallBold; -TTF_Font* fontLarge; -SDL_Surface* text; -unsigned char* keys; -/// +/// +extern SDL_Surface* screen; +extern TTF_Font* fontSmaller; +extern TTF_Font* fontSmall; +extern TTF_Font* fontSmallBold; +extern TTF_Font* fontLarge; +extern SDL_Surface* text; +extern unsigned char* keys; +/// /// typedef struct vector2D { - //1 egység == 1m + //1 egység == 1m float x, y; } vector2D; @@ -40,13 +41,13 @@ typedef struct intVector2D { } intVector2D; typedef struct vector3D { - //1 egység == 1m + //1 egység == 1m float x, y, z; } vector3D; /// -/// +/// typedef struct color { unsigned char r, g, b, a; } color; @@ -54,7 +55,7 @@ typedef struct color { typedef struct colors { color light, normal, dark; } colors; -/// +/// typedef enum block { diff --git a/Forráskód/modules/globals.c b/Forráskód/modules/globals.c new file mode 100644 index 0000000..364af9f --- /dev/null +++ b/Forráskód/modules/globals.c @@ -0,0 +1,14 @@ +#include "global.h" +#include "saves.h" + +float g; + +SDL_Surface* screen; +TTF_Font* fontSmaller; +TTF_Font* fontSmall; +TTF_Font* fontSmallBold; +TTF_Font* fontLarge; +SDL_Surface* text; +unsigned char* keys; + +bool existingSaves[maxNOSaves]; diff --git a/Forráskód/modules/menu/menuDraw.c b/Forráskód/modules/menu/menuDraw.c index 866eb7b..aaddfaa 100644 --- a/Forráskód/modules/menu/menuDraw.c +++ b/Forráskód/modules/menu/menuDraw.c @@ -11,13 +11,12 @@ static void drawMenuParticles(int deltaTime) { ///A főmenüt rajzolja. void drawMainMenu(int deltaTime) { - Uint16 menuOptions[][50] = { + wchar_t menuOptions[][50] = { L"Indítás", L"Betöltés", L"Beállítások", L"Ranglista", L"Leírás", - L"Kilépés", L"" }; @@ -37,7 +36,7 @@ static inline void printBack() { ///A beállítások menüpont rajzolása, a beállításokat a betöltött mentések is befolyásolják. Figyelni kell, hogy a belépés kattintását ne érzékelje csúszka kattintásnak. void drawOptionsMenu(int deltaTime, int currentOptions[], bool click) { - Uint16 optionsMenuOptions[][50] = { + wchar_t optionsMenuOptions[][50] = { L"Világ szélessége", L"Világ magassága", L"Részecskék száma", @@ -60,10 +59,10 @@ void drawOptionsMenu(int deltaTime, int currentOptions[], bool click) { ); filledCircleColor(screen, (float) screen -> w / 100 * (30 + 0.4 * 100 * (float)(currentOptions[i] - minOptions[i]) / (maxOptions[i] - minOptions[i])), - (float) screen -> h / 100 * (optionsStart + i * optionsTav), 20, 0xFFFFFFFF + (float) screen -> h / 100 * (optionsStart + i * optionsTav), larger(4, 20 * screen -> h / designHeight), 0xFFFFFFFF ); aacircleColor(screen, (float) screen -> w / 100 * (30 + 0.4 * 100 * (float)(currentOptions[i] - minOptions[i]) / (maxOptions[i] - minOptions[i])), - (float) screen -> h / 100 * (optionsStart + i * optionsTav), 20, 0xFFFFFFFF + (float) screen -> h / 100 * (optionsStart + i * optionsTav), larger(4, 20 * screen -> h / designHeight), 0xFFFFFFFF ); i++; @@ -84,7 +83,7 @@ void drawOptionsMenu(int deltaTime, int currentOptions[], bool click) { ///A játék leírását rajzolja meg. void drawInfoMenu(int deltaTime) { - Uint16 info[][150] = { + wchar_t info[][150] = { L"Ebben a játékban pontokat szerezni és költeni", L"is tudsz. Minden megtalált kék kockáért jár egy pont.", L"Mozogni a WAD billentyűk, szóköz, vagy a nyilak", @@ -106,16 +105,16 @@ void drawInfoMenu(int deltaTime) { drawMenuParticles(deltaTime); } -Uint16 names[topListItemsMax][100]; -int points[topListItemsMax]; -int namesIndex; +static wchar_t names[topListItemsMax][100]; +static int points[topListItemsMax]; +static int namesIndex; ///Ha megtelt a toplista, rendezi, és az utolsó elemét lecseréli. A ranglista fájlt se kéne nagyon piszkálni a felhasználónak. static void sortTopList() { int i, j; int tmpInt; - Uint16 tmpStr[100]; + wchar_t tmpStr[100]; for(i = 0; i < namesIndex; i++) { int maxIndex = i; @@ -140,23 +139,19 @@ void loadTopList() { int i; if(topList != NULL) { + char lineName[400]; namesIndex = 0; - while(true) { - i = 0; - do { - if(fwscanf(topList, L"%lc", &names[namesIndex][i]) == 0) goto end; - } - while(names[namesIndex][i++] != L','); - names[namesIndex][i] = 0; + while(fscanf(topList, " %399[^,\n],%d", lineName, &points[namesIndex]) == 2) { + i = strlen(lineName); + lineName[i] = ','; + lineName[i + 1] = 0; + utf8ToWide(lineName, names[namesIndex], 100); - fwscanf(topList, L"%d", &points[namesIndex]); - fwscanf(topList, L"\n"); if(namesIndex < topListItemsMax - 1) namesIndex++; else sortTopList(); } - end: - sortTopList(); - fclose(topList); + sortTopList(); + fclose(topList); } } @@ -164,9 +159,10 @@ void loadTopList() { void drawTopList(int deltaTime) { int i; for(i = 0; i < namesIndex; i++) { - Uint16 nameWScore[600]; - swprintf(nameWScore, L"%ls %d", names[i], points[i]); - unicodePrintCentered(nameWScore, 50, 20 + ((i == 0) ? 0 : (i + 1)*infoTav), (i == 0) ? large : (i == 1) ? small : tiny); + char storedName[400], nameWScore[600]; + wideToUtf8(names[i], storedName, sizeof(storedName)); + snprintf(nameWScore, sizeof(nameWScore), "%s %d", storedName, points[i]); + utf8PrintCentered(nameWScore, 50, 20 + ((i == 0) ? 0 : (i + 1)*infoTav), (i == 0) ? large : (i == 1) ? small : tiny); } printBack(); @@ -175,7 +171,7 @@ void drawTopList(int deltaTime) { ///Ha a játékos szüneteltei a játékot(ESC) ezt fogja látni. Elég drága az átlátszó box, szóval csak óvatosan a méretével. void drawPauseMenu(bool saved) { - Uint16 pauseOptions[][100] = { + wchar_t pauseOptions[][100] = { L"Játék folytatása", L"Pálya elmentése", L"Mentés betöltése", @@ -201,14 +197,14 @@ void drawLoadMenu(int deltaTime) { int i; - Uint16 timeStrings[][50] = { - L"másodperccel", - L"perccel", - L"órával", - L"nappal", - L"héttel", - L"hónappal", - L"évvel" + const char* timeStrings[] = { + "másodperccel", + "perccel", + "órával", + "nappal", + "héttel", + "hónappal", + "évvel" }; int timeConversion[] = { 1, 60, 60*60, 60*60*24, 60*60*24*7, 60*60*24*30, 60*60*24*365 @@ -224,9 +220,9 @@ void drawLoadMenu(int deltaTime) { break; int elapsedSaveTime = deltaRealTime / timeConversion[timeStringsIndex]; - Uint16 save[200]; - swprintf(save, L"%d %ls ezelőtti mentés, %d ponttal", elapsedSaveTime, timeStrings[timeStringsIndex], points[i]); - unicodePrintCentered(save, 50, loadStart + k * loadTav, isMouseOverRect(50, loadStart + loadTav*k, 25, 3) ? smallBold : small); + char save[300]; + snprintf(save, sizeof(save), "%d %s ezelőtti mentés, %d ponttal", elapsedSaveTime, timeStrings[timeStringsIndex], points[i]); + utf8PrintCentered(save, 50, loadStart + k * loadTav, isMouseOverRect(50, loadStart + loadTav*k, 25, 3) ? smallBold : small); unicodePrintCentered(L"Törlés", 50, loadStart + k * loadTav + 5, isMouseOverRect(50, loadStart + loadTav*k + 5, 25, 2) ? smallBold : small); k++; } diff --git a/Forráskód/modules/menu/menuFSM.c b/Forráskód/modules/menu/menuFSM.c index 5a016e3..5471fb6 100644 --- a/Forráskód/modules/menu/menuFSM.c +++ b/Forráskód/modules/menu/menuFSM.c @@ -4,8 +4,8 @@ int currentOptions[] = {2000, 2000, 3, 450, 6000}; int currentSpawnRate, fadeStartTime, elapsedTime = 0, nameIndex = 0, cursorBlinking = 0; -bool prematureClick, isDollying, saved = false; -Uint16 name[100] = {0}; +bool isDollying, saved = false; +wchar_t name[100] = {0}; FILE* topList; states gameState = mainMenu; /**< Kezdőállapot. */ @@ -39,9 +39,6 @@ void handleClicks(bool* prematureClick) { } else if(isMouseOverRect(50, menuStart + menuTav*4, 15, menuTav / 2)) gameState = infoMenu; - else if(isMouseOverRect(50, menuStart + menuTav*5, 15, menuTav / 2)) { - gameState = quit; - } break; case pauseMenu: if(isMouseOverRect(50, menuStart - 5, 15, menuTav/2)) { @@ -181,11 +178,10 @@ void doStuff(bool isLMBDown, bool isRMBDown, bool prematureClick) { boxRGBA(screen, 0, 0, screen -> w, screen -> h, 0, 0, 0, 255); cursorBlinking += deltaTime; if(cursorBlinking > 1000) cursorBlinking = 0; - Uint16 loseText[300]; - swprintf(loseText, L"%ls vesztett, a pontszáma %d.", cursorBlinking > 500 ? L"|" : L"", getPoints()); - Uint16* endText = concatUnicode(name, loseText); - unicodePrintCentered(endText, 50, 50, small); - free(endText); + char typedName[400], loseText[600]; + wideToUtf8(name, typedName, sizeof(typedName)); + snprintf(loseText, sizeof(loseText), "%s%s vesztett, a pontszáma %d.", typedName, cursorBlinking > 500 ? "|" : "", getPoints()); + utf8PrintCentered(loseText, 50, 50, small); break; } if(isAlive()) drawMouse(isInSlowMo); @@ -199,7 +195,9 @@ static void escapeFromNameInput() { createMenuWorld(); topList = fopen("rang.lst", "a"); if(topList != NULL) { - fwprintf(topList, L"%ls,%d\n", name[0] == 0 ? L"Névtelen" : name, getPoints()); + char utf8[400]; + wideToUtf8(name[0] == 0 ? L"Névtelen" : name, utf8, sizeof(utf8)); + fprintf(topList, "%s,%d\n", utf8, getPoints()); fclose(topList); } } @@ -218,9 +216,6 @@ void whatToDoOnEscape() { saved = false; } break; - case mainMenu: - gameState = quit; - break; case fading: gameState = nameInput; break; @@ -233,13 +228,27 @@ void whatToDoOnEscape() { } } -///A szöveges bemenetet továbbítja egy Uint16-os stringbe. +///A szöveges bemenetet továbbítja egy wchar_t-os stringbe. void processTyping(SDL_Event event) { if(gameState == nameInput) { - Uint16 chr = event.key.keysym.unicode; - if(strcmp(SDL_GetKeyName(chr), "backspace") == 0) name[--nameIndex] = 0; - else if(strcmp(SDL_GetKeyName(chr), "return") == 0) escapeFromNameInput(); - else if(isValidChar(chr)) name[nameIndex++] = chr; + SDL_Keycode key = event.key.keysym.sym; + if(key == SDLK_BACKSPACE) { if(nameIndex > 0) name[--nameIndex] = 0; } + else if(key == SDLK_RETURN || key == SDLK_KP_ENTER) escapeFromNameInput(); + name[nameIndex] = 0; + } +} + +///A beírt karaktereket utf-8-ból átalakítva fűzi a névhez. +void processTextInput(const char* utf8) { + if(gameState == nameInput) { + wchar_t typed[16]; + int count = utf8ToWide(utf8, typed, sizeof(typed) / sizeof(wchar_t)); + + int i; + for(i = 0; i < count; i++) + if(isValidChar(typed[i]) && nameIndex < (int) (sizeof(name) / sizeof(wchar_t)) - 1) + name[nameIndex++] = typed[i]; + name[nameIndex] = 0; } } diff --git a/Forráskód/modules/menu/menuFSM.h b/Forráskód/modules/menu/menuFSM.h index 758ccb5..f37844c 100644 --- a/Forráskód/modules/menu/menuFSM.h +++ b/Forráskód/modules/menu/menuFSM.h @@ -17,5 +17,6 @@ void doStuff(bool isLMBDown, bool isRMBDown, bool prematureClick); void whatToDoOnEscape(); void processTyping(SDL_Event event); + void processTextInput(const char* utf8); #endif // MENUFSM_H_INCLUDED diff --git a/Forráskód/modules/particles.h b/Forráskód/modules/particles.h index 2ae9de1..2c525ea 100644 --- a/Forráskód/modules/particles.h +++ b/Forráskód/modules/particles.h @@ -8,7 +8,7 @@ typedef struct particle { //y, z vector2D position; - //mert kockák + //mert kockák float size; float velocity; } particle; diff --git a/Forráskód/modules/player.c b/Forráskód/modules/player.c index 365745a..9c0661f 100644 --- a/Forráskód/modules/player.c +++ b/Forráskód/modules/player.c @@ -10,7 +10,7 @@ vector2D playerVelocity; /**< A játékos sebesség [m/ms], a delta time int directionOfMovement = 0; /**< A mozgás iránya, lehet -1, 0, +1. */ bool isJumping = false, isFalling = true; bool isDead = false; -int points; /**< Mennyi pontot gyűjtött eddig a játékos. Ez nőhet és gyökkenhet is, de mindig >= 0. */ +static int points; /**< Mennyi pontot gyűjtött eddig a játékos. Ez nőhet és gyökkenhet is, de mindig >= 0. */ void setDie(bool newValue) { @@ -29,6 +29,7 @@ void updateText(int newPoints) { } void drawPointsText() { + if(!isRenderingEnabled()) return; SDL_BlitSurface(text, NULL, screen, &(SDL_Rect){50, 30, 0, 0}); } @@ -56,9 +57,9 @@ int getPoints() { void movePlayer(unsigned char* keys, int deltaTime) { //nem a legszebb, de a rövidségtől szép lesz. - directionOfMovement = (keys[SDLK_d] || keys[SDLK_RIGHT]) - (keys[SDLK_a] || keys[SDLK_LEFT]); + directionOfMovement = (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) - (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]); - isJumping = keys[SDLK_w] || keys[SDLK_UP] || keys[SDLK_SPACE]; + isJumping = keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_SPACE]; //<\input feldolgozása> // if(directionOfMovement != 0) { diff --git a/Forráskód/modules/saves.c b/Forráskód/modules/saves.c index e11e681..2631f1a 100644 --- a/Forráskód/modules/saves.c +++ b/Forráskód/modules/saves.c @@ -7,13 +7,15 @@ void getExistingSaveFiles(time_t ages[], int points[]) { char save[50]; FILE* saveFile; while(saveID < maxNOSaves) { - sprintf(save, "saves\\game%d.save", saveID); + sprintf(save, "saves/game%d.save", saveID); saveFile = fopen(save, "r"); if(saveFile != NULL) { existingSaves[saveID] = true; if(saveFile != NULL) { if(ages != NULL) { - fscanf(saveFile, "%ld,", &(ages[saveID])); + long long savedTime = 0; + fscanf(saveFile, "%lld,", &savedTime); + ages[saveID] = (time_t) savedTime; int i; for(i = 0; i < 8; i++) fscanf(saveFile, "%d,", &(points[saveID])); @@ -29,7 +31,7 @@ void getExistingSaveFiles(time_t ages[], int points[]) { ///Mentés törlése (fizikálisan). void deleteSave(int i) { char save[50]; - sprintf(save, "saves\\game%d.save", i); + sprintf(save, "saves/game%d.save", i); remove(save); } @@ -48,12 +50,12 @@ void saveGame(int currentOptions[]) { int i; for(i = 0; i < maxNOSaves - 1 && existingSaves[i]; i++) ; - sprintf(saveName, "saves\\game%d.save", i); + sprintf(saveName, "saves/game%d.save", i); FILE* save = fopen(saveName, "w"); block** map = getMap(); - fprintf(save, "%ld,", time(NULL)); + fprintf(save, "%lld,", (long long) time(NULL)); for(i = 0; i < 5; i++) fprintf(save, "%d,", currentOptions[i]); @@ -82,13 +84,13 @@ void saveGame(int currentOptions[]) { bufferIndex += strlen(temp); } if(bufferIndex > bufferSize - 20) { - fprintf(save, buffer); + fprintf(save, "%s", buffer); bufferIndex = 0; } } } if(bufferIndex != 0) - fprintf(save, buffer); + fprintf(save, "%s", buffer); free(buffer); fclose(save); @@ -97,12 +99,12 @@ void saveGame(int currentOptions[]) { ///Fájlból betölti a játékot, a világot is előkészíti. A mentésnél használt tömörítést dekódolja. A betöltött játék beállítások felülírják az eddigieket. void loadGame(int currentOptions[], int saveID) { char saveName[50]; - sprintf(saveName, "saves\\game%d.save", saveID); + sprintf(saveName, "saves/game%d.save", saveID); FILE* save = fopen(saveName, "r"); - time_t tmpT; - fscanf(save, "%ld,", &tmpT); + long long tmpT; + fscanf(save, "%lld,", &tmpT); int i; for(i = 0; i < 5; i++) diff --git a/Forráskód/modules/saves.h b/Forráskód/modules/saves.h index 617825e..9325849 100644 --- a/Forráskód/modules/saves.h +++ b/Forráskód/modules/saves.h @@ -10,7 +10,7 @@ #define bufferSize 64000 /**< A mentéshez használ buffer mérete [byte]. */ - bool existingSaves[maxNOSaves]; /**< Igaz, ha a save slot üres. */ + extern bool existingSaves[maxNOSaves]; /**< Igaz, ha a save slot üres. */ void getExistingSaveFiles(time_t ages[], int points[]); void deleteSave(int i); diff --git a/README.md b/README.md new file mode 100644 index 0000000..731580c --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Platform játék + +Background: https://schmelczer.dev/articles/platform-game-c-sdl +Try it out at: https://schmelczer.dev/platform-game diff --git a/web/pre.js b/web/pre.js new file mode 100644 index 0000000..2786718 --- /dev/null +++ b/web/pre.js @@ -0,0 +1,60 @@ +Module.preRun = Module.preRun || []; +Module.preRun.push(function () { + var released = false; + + var exists = function (path) { + try { + FS.stat(path); + return true; + } catch (e) { + return false; + } + }; + + var release = function (err) { + if (released) return; + released = true; + + if (err) console.warn('A mentesek betoltese nem sikerult:', err); + + try { + if (!exists('/saves/rang.lst')) FS.writeFile('/saves/rang.lst', ''); + if (!exists('/rang.lst')) FS.symlink('/saves/rang.lst', '/rang.lst'); + } catch (e) { + console.warn('A ranglista nem elerheto:', e); + } + + Module.removeRunDependency('persist'); + }; + + Module.addRunDependency('persist'); + + try { + if (!exists('/saves')) FS.mkdir('/saves'); + FS.mount(IDBFS, {}, '/saves'); + } catch (e) { + console.warn('A tartos tarolas nem erheto el, a mentesek nem maradnak meg:', e); + release(); + return; + } + + setTimeout(release, 3000); + FS.syncfs(true, release); +}); + +Module.postRun = Module.postRun || []; +Module.postRun.push(function () { + var flushing = false; + var flush = function () { + if (flushing) return; + flushing = true; + FS.syncfs(false, function (err) { + flushing = false; + if (err) console.warn('A mentes nem sikerult:', err); + }); + }; + + setInterval(flush, 4000); + addEventListener('pagehide', flush); + addEventListener('visibilitychange', flush); +}); diff --git a/web/shell.html b/web/shell.html new file mode 100644 index 0000000..55216e5 --- /dev/null +++ b/web/shell.html @@ -0,0 +1,66 @@ + + + + + +Platform játék + + + +
+ +
+
Betöltés…
+ +{{{ SCRIPT }}} + +