From c4f7565d18c5965d9fb9f8020102b8cca60a457c Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 9 Aug 2026 20:39:30 +0100 Subject: [PATCH] Add mobile support --- CMakeLists.txt | 1 + Forráskód/main.c | 20 +- Forráskód/modules/auxiliary/auxSDL.c | 8 +- Forráskód/modules/auxiliary/gfxPrimitives.c | 35 +- Forráskód/modules/auxiliary/gfxPrimitives.h | 4 +- Forráskód/modules/auxiliary/unicodePrint.c | 14 +- Forráskód/modules/auxiliary/unicodePrint.h | 1 + Forráskód/modules/camera.c | 34 +- Forráskód/modules/camera.h | 2 +- Forráskód/modules/floaters.c | 6 +- Forráskód/modules/menu/menuDraw.c | 20 +- Forráskód/modules/menu/menuFSM.c | 43 ++- Forráskód/modules/menu/menuFSM.h | 2 + Forráskód/modules/particles.c | 13 +- Forráskód/modules/player.c | 12 +- Forráskód/modules/touch.c | 375 ++++++++++++++++++++ Forráskód/modules/touch.h | 17 + web/shell.html | 29 +- 18 files changed, 567 insertions(+), 69 deletions(-) create mode 100644 Forráskód/modules/touch.c create mode 100644 Forráskód/modules/touch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ed19c82..5a8b4d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ if(EMSCRIPTEN) "SHELL:-sALLOW_MEMORY_GROWTH=1" "SHELL:-sEXIT_RUNTIME=0" "SHELL:-sFORCE_FILESYSTEM=1" + "SHELL:-sEXPORTED_RUNTIME_METHODS=ccall" "SHELL:-lidbfs.js" "SHELL:--preload-file ${GAME_SRC}/resources@/resources" "SHELL:--shell-file ${GAME_WEB_SHELL}" diff --git a/Forráskód/main.c b/Forráskód/main.c index 4dd4c6e..9aa66dd 100644 --- a/Forráskód/main.c +++ b/Forráskód/main.c @@ -14,6 +14,7 @@ #include "modules/map.h" #include "modules/menu/menuDraw.h" #include "modules/menu/menuFSM.h" +#include "modules/touch.h" /**\mainpage Leírás @@ -93,6 +94,7 @@ static void handleEvents() { case SDL_MOUSEBUTTONDOWN: + noteMouseInput(); if(event.button.button == SDL_BUTTON_LEFT) handleClicks(&prematureClick); if(event.button.button == SDL_BUTTON_RIGHT) isRMBDown = true; if(event.button.button == SDL_BUTTON_LEFT) isLMBDown = true; @@ -106,6 +108,7 @@ static void handleEvents() { case SDL_MOUSEMOTION: + noteMouseInput(); setMousePosition(event.motion.x, event.motion.y); break; @@ -120,6 +123,13 @@ static void handleEvents() { processTextInput(event.text.text); break; + case SDL_FINGERDOWN: + case SDL_FINGERMOTION: + case SDL_FINGERUP: + + handleFingerEvent(&event, &isLMBDown, &prematureClick); + break; + } } } @@ -144,11 +154,14 @@ static void frame() { setRenderingEnabled(isLastStep); if(isLastStep) SDL_FillRect(screen, NULL, backgroundColor); - doStuff(isLMBDown, isRMBDown, prematureClick); + applyTouchKeys(); + doStuff(isLMBDown, isRMBDown || isTouchSlowMo(), prematureClick); hasDrawn = hasDrawn || isLastStep; } setRenderingEnabled(true); + syncSoftKeyboard(getGameState()); + if(hasDrawn) presentFrame(); #ifdef __EMSCRIPTEN__ if(getGameState() == quit || quitRequested) emscripten_cancel_main_loop(); @@ -160,7 +173,12 @@ static void frame() { int main(int argc, char *argv[]) { srand(time(NULL)); + + //az érintéseket magunk dolgozzuk fel, az sdl egérré alakítása egyszerre csak egy ujjat tudna + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); + openWindow(); + initTouch(); keys = (unsigned char*) SDL_GetKeyboardState(NULL); SDL_StartTextInput(); diff --git a/Forráskód/modules/auxiliary/auxSDL.c b/Forráskód/modules/auxiliary/auxSDL.c index b54434b..f3a5eec 100644 --- a/Forráskód/modules/auxiliary/auxSDL.c +++ b/Forráskód/modules/auxiliary/auxSDL.c @@ -99,10 +99,12 @@ void handleResize() { if(screen != NULL && screen -> w == outputWidth && screen -> h == outputHeight) return; - bool heightChanged = screen == NULL || screen -> h != outputHeight; + //a betűméret a kisebbik képernyőoldalhoz igazodik, csak annak változásakor kell újratölteni + bool scaleChanged = screen == NULL || + smaller(screen -> w, screen -> h) != smaller(outputWidth, outputHeight); createDrawingSurface(outputWidth, outputHeight); - if(heightChanged) { + if(scaleChanged) { freeFonts(); openFonts(); } @@ -218,7 +220,7 @@ 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 size = larger(4, mouseSize * screen -> h / designHeight); + Sint16 size = larger(4, (int)(mouseSize * uiScale())); Sint16 x1 = mouse.x - size / 2; Sint16 x2 = mouse.x + size / 2; Sint16 y1 = mouse.y - size / 2; diff --git a/Forráskód/modules/auxiliary/gfxPrimitives.c b/Forráskód/modules/auxiliary/gfxPrimitives.c index 81019d3..16e1df8 100644 --- a/Forráskód/modules/auxiliary/gfxPrimitives.c +++ b/Forráskód/modules/auxiliary/gfxPrimitives.c @@ -197,7 +197,7 @@ int aacircleColor(SDL_Surface* dst, Sint16 x, Sint16 y, Sint16 rad, Uint32 color return 0; } -int filledPolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { +int filledPolygonRGBA(SDL_Surface* dst, const float* vx, const float* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { if(n < 3) return -1; target t; @@ -212,26 +212,32 @@ int filledPolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int if(crossings == NULL) { endTarget(&t); return -1; } } - int minY = vy[0], maxY = vy[0]; + double 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; + //A vágás még a törtszámokon megtörténik, így a kerekítés biztosan nem lóg ki az int-ből + if(minY < 0) minY = 0; + if(maxY > t.h - 1) maxY = t.h - 1; + if(minY > maxY) { if(crossings != inlineCrossings) free(crossings); endTarget(&t); return 0; } + + int yStart = (int) ceil(minY); + int yEnd = (int) floor(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; } + double y1 = vy[i], y2 = vy[j], x1 = vx[i], x2 = vx[j]; + if(y1 > y2) { double 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); + //Félig nyílt intervallum, hogy a két él közös csúcsát pontosan egyszer számoljuk + if(y >= y1 && y < y2) + crossings[count++] = x1 + ((y - y1) * (x2 - x1)) / (y2 - y1); } if(count < 2) continue; @@ -244,8 +250,15 @@ int filledPolygonRGBA(SDL_Surface* dst, const Sint16* vx, const Sint16* vy, int 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); + for(i = 0; i + 1 < count; i += 2) { + double left = crossings[i], right = crossings[i + 1]; + if(left < 0) left = 0; + if(right > t.w - 1) right = t.w - 1; + if(left > right) continue; + + int xa = (int) ceil(left), xb = (int) floor(right); + if(xa <= xb) blendHLine(&t, xa, xb, y, r, g, b, a); + } } if(crossings != inlineCrossings) free(crossings); @@ -283,7 +296,7 @@ static void aaLine(const target* t, double x0, double y0, double x1, double y1, } } -int aapolygonRGBA(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 float* vx, const float* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { if(n < 2) return -1; target t; diff --git a/Forráskód/modules/auxiliary/gfxPrimitives.h b/Forráskód/modules/auxiliary/gfxPrimitives.h index e4628cb..94fbfb4 100644 --- a/Forráskód/modules/auxiliary/gfxPrimitives.h +++ b/Forráskód/modules/auxiliary/gfxPrimitives.h @@ -12,7 +12,7 @@ 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); + int filledPolygonRGBA(SDL_Surface* dst, const float* vx, const float* vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a); + int aapolygonRGBA(SDL_Surface* dst, const float* vx, const float* 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 3e4e54e..988d19e 100644 --- a/Forráskód/modules/auxiliary/unicodePrint.c +++ b/Forráskód/modules/auxiliary/unicodePrint.c @@ -1,11 +1,23 @@ #include "unicodePrint.h" +/** \brief A feliratok és a felület méretezéséhez használt arányszám. + * A program az 1920x1080-as képernyőre készült, minden mérete ehhez képest értendő. + * Csak a magassághoz kötni viszont nem szabad: álló tájolású telefonon a képernyő magas, + * de keskeny, így a magasságból számolt betűk kilógnának a szélességből. A kisebbik + * képernyőoldalhoz igazítva a szöveg akkora lesz, amekkorára a fekvő képernyőn is, tehát + * a készülék elforgatása nem változtatja meg a méretét. + */ +float uiScale() { + if(screen == NULL) return 1; + return (float) smaller(screen -> w, screen -> h) / designHeight; +} + ///Indításnál betölti a szükséges betűtípusokat. void openFonts() { if(!TTF_WasInit()) TTF_Init(); - float scale = (float) screen -> h / designHeight; + float scale = uiScale(); 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))); diff --git a/Forráskód/modules/auxiliary/unicodePrint.h b/Forráskód/modules/auxiliary/unicodePrint.h index d2051f8..b8fb261 100644 --- a/Forráskód/modules/auxiliary/unicodePrint.h +++ b/Forráskód/modules/auxiliary/unicodePrint.h @@ -7,6 +7,7 @@ tiny, small, smallBold, large } fontType; + float uiScale(); void openFonts(); void freeFonts(); bool isValidChar(wchar_t chr); diff --git a/Forráskód/modules/camera.c b/Forráskód/modules/camera.c index 7fe4f9e..659004d 100644 --- a/Forráskód/modules/camera.c +++ b/Forráskód/modules/camera.c @@ -155,26 +155,34 @@ bool cameraDollyZ(int deltaTime, bool restart) { * 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. + * A visszaadott pont koordinátái törtszámok: egy pixelen belüli helyet is meg tudnak adni. + * Erre azért van szükség, mert a kamera folyamatosan mozog, és ha itt pixelre kerekítenénk, + * akkor a testek élei egymástól függetlenül, más-más képkockán ugranának át a szomszéd pixelre. * Ez a függvény igazából egy camera obscura modellje. */ -intVector2D get2DPoint(vector3D point) { +vector2D 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 + /* A metszéspont képlete kibontva összevonható, hiszen zDistFromHole = zDist - focalLength: + * (kamera * (zDist - f) + f * pont) / zDist - kamera == f * (pont - kamera) / zDist + * A két alak matematikailag azonos, de a bal oldali a pálya közepén (kamera ~1000 m-nél) + * egy ~20000 nagyságrendű számból vonna ki majdnem ugyanakkorát, és a maradék néhány + * tizedből épp a lényeg veszne el: a float pontossága ott már csak ezred méter, amit a + * magnification több tized pixelnyi hibává nagyít. A jobb oldali alak előbb végzi el a + * kivonást (két közeli szám különbsége hibamentes), így a leképezés a pálya szélén is + * pixeltört pontos marad. */ 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; + float dx = focalLength * (point.x - cameraPosition.x) / zDist; + float dy = focalLength * (point.y - cameraPosition.y) / zDist; //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) + vector2D resultPoint = { + screen -> w / 2.0f - magnification * dx, + screen -> h / 2.0f + magnification * dy }; return resultPoint; @@ -190,9 +198,9 @@ intVector2D get2DPoint(vector3D point) { * 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}; +static void drawTetragon(vector2D a, vector2D b, vector2D c, vector2D d, color color) { + float xCoords[4] = {a.x, b.x, c.x, d.x}; + float yCoords[4] = {a.y, b.y, c.y, 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); @@ -206,7 +214,7 @@ static void drawTetragon(intVector2D a, intVector2D b, intVector2D c, intVector2 * azokat se rajzolja le, amiket a helyzetük miatt biztosan nem láthat a kamera. */ void drawCuboid(renderObject object, bool sides[]) { - intVector2D vertexes[8]; + vector2D vertexes[8]; //A téglatestet bejárva megkeressük a csúcsait int i; for(i = 0; i < 8; i++) { diff --git a/Forráskód/modules/camera.h b/Forráskód/modules/camera.h index cbc78c0..0b02ec1 100644 --- a/Forráskód/modules/camera.h +++ b/Forráskód/modules/camera.h @@ -18,7 +18,7 @@ void menuCamera(int* getColumns, int* getRows); int getColumns(); bool cameraDollyZ(int deltaTime, bool restart); - intVector2D get2DPoint(vector3D point); + vector2D get2DPoint(vector3D point); void drawCuboid(renderObject object, bool sides[]); void drawMap(int deltaTime); diff --git a/Forráskód/modules/floaters.c b/Forráskód/modules/floaters.c index 72cdf15..a0a98eb 100644 --- a/Forráskód/modules/floaters.c +++ b/Forráskód/modules/floaters.c @@ -73,9 +73,9 @@ void createProjectile(vector3D playerPosition, vector3D playerSize, vector2D* pl newProjectile -> position.y = playerPosition.y + playerSize.y / 2; newProjectile -> size = projectileSize; - intVector2D playerCoord = get2DPoint((vector3D) {newProjectile -> position.x, newProjectile -> position.y, 0}); - int deltaX = mouse.x - playerCoord.x; - int deltaY = playerCoord.y - mouse.y; + vector2D playerCoord = get2DPoint((vector3D) {newProjectile -> position.x, newProjectile -> position.y, 0}); + float deltaX = mouse.x - playerCoord.x; + float deltaY = playerCoord.y - mouse.y; float distance = sqrt(deltaX * deltaX + deltaY * deltaY); float compensation = 0; if(distance > 0) compensation = projectileVelocity / distance; diff --git a/Forráskód/modules/menu/menuDraw.c b/Forráskód/modules/menu/menuDraw.c index aaddfaa..7f651dc 100644 --- a/Forráskód/modules/menu/menuDraw.c +++ b/Forráskód/modules/menu/menuDraw.c @@ -59,25 +59,27 @@ 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), larger(4, 20 * screen -> h / designHeight), 0xFFFFFFFF + (float) screen -> h / 100 * (optionsStart + i * optionsTav), larger(4, (int)(20 * uiScale())), 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), larger(4, 20 * screen -> h / designHeight), 0xFFFFFFFF + (float) screen -> h / 100 * (optionsStart + i * optionsTav), larger(4, (int)(20 * uiScale())), 0xFFFFFFFF ); i++; } - if(click) - for(i = 0; i < 5; i++) + if(click) { + for(i = 0; i < 5; i++) { if(isMouseOverRect(50, optionsStart + optionsTav * i, 50, optionsTav / 2)) { - //Ezek nem magic számok, csak egy kicsit. - currentOptions[i] = minOptions[i] + (int)((maxOptions[i] - minOptions[i]) * (getMousePosition().x - screen -> w * 0.3) / (screen -> w * 0.4) ); - putInsideInterval(minOptions[i], ¤tOptions[i], maxOptions[i]); + //Ezek nem magic számok, csak egy kicsit. + currentOptions[i] = minOptions[i] + (int)((maxOptions[i] - minOptions[i]) * (getMousePosition().x - screen -> w * 0.3) / (screen -> w * 0.4) ); + putInsideInterval(minOptions[i], ¤tOptions[i], maxOptions[i]); + } } + } - printBack(); - drawMenuParticles(deltaTime); + printBack(); + drawMenuParticles(deltaTime); } ///A játék leírását rajzolja meg. diff --git a/Forráskód/modules/menu/menuFSM.c b/Forráskód/modules/menu/menuFSM.c index 5471fb6..d7ee9fa 100644 --- a/Forráskód/modules/menu/menuFSM.c +++ b/Forráskód/modules/menu/menuFSM.c @@ -1,4 +1,5 @@ #include "menuFSM.h" +#include "../touch.h" int currentOptions[] = {2000, 2000, 3, 450, 6000}; @@ -86,6 +87,14 @@ void handleClicks(bool* prematureClick) { if(isMouseOverRect(50, menuStart + menuTav*5, 15, menuTav/2)) gameState = mainMenu; break; + + case nameInput: + case fading: + case initGame: + case initLoadedGame: + case quit: + //ezekben az állapotokban a kattintásnak nincs hatása + break; } } @@ -183,8 +192,16 @@ void doStuff(bool isLMBDown, bool isRMBDown, bool prematureClick) { snprintf(loseText, sizeof(loseText), "%s%s vesztett, a pontszáma %d.", typedName, cursorBlinking > 500 ? "|" : "", getPoints()); utf8PrintCentered(loseText, 50, 50, small); break; + + case quit: + //a kilépésről a main gondoskodik, itt már nincs mit rajzolni + break; + } + //érintéssel nincs kurzor, helyette a képernyőre rajzolt gombok látszanak + if(isAlive()) { + drawTouchControls(); + if(shouldDrawCursor()) drawMouse(isInSlowMo); } - if(isAlive()) drawMouse(isInSlowMo); } ///A nameInputból returnnel és ESC-vel is ki lehet lépni, ezért kell ez a függvény rá, amit mindkét esetben meg lehet hívni. @@ -238,6 +255,30 @@ void processTyping(SDL_Event event) { } } +/** \brief A név teljes tartalmát cseréli le, a böngésző beviteli mezőjéhez. + * A mobilos billentyűzet nem karakterenként érkezik: a html mező mindig a teljes szövegét + * küldi el, mert így a törlés, a javítás és a szövegbeillesztés is magától jó lesz. + */ +void setNameInput(const char* utf8) { + if(gameState != nameInput) return; + + wchar_t typed[sizeof(name) / sizeof(wchar_t)]; + int count = utf8ToWide(utf8, typed, sizeof(typed) / sizeof(wchar_t)); + + nameIndex = 0; + 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; +} + +///A böngésző beviteli mezőjének lezárása ugyanaz, mint az enter megnyomása. +void submitNameInput() { + escapeFromNameInput(); +} + ///A beírt karaktereket utf-8-ból átalakítva fűzi a névhez. void processTextInput(const char* utf8) { if(gameState == nameInput) { diff --git a/Forráskód/modules/menu/menuFSM.h b/Forráskód/modules/menu/menuFSM.h index f37844c..df22751 100644 --- a/Forráskód/modules/menu/menuFSM.h +++ b/Forráskód/modules/menu/menuFSM.h @@ -18,5 +18,7 @@ void whatToDoOnEscape(); void processTyping(SDL_Event event); void processTextInput(const char* utf8); + void setNameInput(const char* utf8); + void submitNameInput(); #endif // MENUFSM_H_INCLUDED diff --git a/Forráskód/modules/particles.c b/Forráskód/modules/particles.c index 39687fa..db225ef 100644 --- a/Forráskód/modules/particles.c +++ b/Forráskód/modules/particles.c @@ -4,6 +4,9 @@ #define particleSizeVariance 0.05 /**< Az eső részecskék (~hó) méretének átlagostól való eltérése [m]. */ #define particleBaseVelocity 0.0025 /**< Az eső részecskék (~hó) átlagos sebessége [m/ms]. */ #define particleVelocityVariance 0.0010 /**< Az eső részecskék (~hó) sebességének átlagostól való eltérése [m/ms]. */ +#define particleMinDistance 5 /**< Ennél közelebb a kamerához egy részecske sem kerülhet [m]. */ +#define particleMinZ 1.0 /**< A pályához legközelebbi részecskesík, a kockák előlapja z = 1-nél van [m]. */ +#define particleMaxZ (cameraDeltaZDistanceEnd - particleMinDistance) /**< A kamerához legközelebbi részecskesík [m]. */ #define particleColor (colors) { (color) {220, 220, 220, 255}, (color){220, 220, 220, 255}, (color) {220, 220, 220, 255} } /**< A részecskék színe. */ int particleMultiplier; /**< Egy oszlopban hány részecske van? */ @@ -15,10 +18,16 @@ void setParticleMultiplier(int a, bool isMenu) { particleMultiplier = isMenu ? a : getMapSize().y * a / 100; } -///A makrókban beállított paraméterek alapján hoz létre egy részecskét, a pálya és a kamera közti z koorddal. +/** \brief A makrókban beállított paraméterek alapján hoz létre egy részecskét, a pálya és a kamera közti z koorddal. + * A z koordot nem a teljes [pálya, kamera] szakaszról sorsoljuk: a kamera modellje szerint egy test látszólagos + * mérete a lencsétől mért távolságával fordítottan arányos, így a lencse elé kerülő részecske nem hópehelynek, + * hanem a képernyőn átsuhanó fehér csíknak látszana. A kamera legközelebbi állása (a dolly vége) a + * cameraDeltaZDistanceEnd, ezért ehhez képest hagyunk particleMinDistance ráhagyást. + */ particle createRandomParticle(int baseY, bool isYRand) { return (particle) { - (vector2D) {isYRand ? rand() % (baseY-1) : baseY, (float) (rand() % (cameraDeltaZDistanceEnd - 1) + 1)}, + (vector2D) {isYRand ? rand() % (baseY-1) : baseY, + randomWithinBorders((particleMaxZ + particleMinZ) / 2, (particleMaxZ - particleMinZ) / 2)}, randomWithinBorders(particleBaseSize, particleSizeVariance), randomWithinBorders(particleBaseVelocity, particleVelocityVariance) }; diff --git a/Forráskód/modules/player.c b/Forráskód/modules/player.c index 9c0661f..26cf654 100644 --- a/Forráskód/modules/player.c +++ b/Forráskód/modules/player.c @@ -63,13 +63,11 @@ void movePlayer(unsigned char* keys, int deltaTime) { //<\input feldolgozása> // if(directionOfMovement != 0) { - //billentyű lenyomás hatására elkezd gyorsulni - if(abs(playerVelocity.x) < playerMaxVelocity.x) { - //hogy ne lehesen gyorsabb a max sebességénél - if(playerVelocity.x > playerMaxVelocity.x) playerVelocity.x = playerMaxVelocity.x; - else if(playerVelocity.x < -playerMaxVelocity.x) playerVelocity.x = - playerMaxVelocity.x; - playerVelocity.x += directionOfMovement * playerXAcceleration * deltaTime; - } + //billentyű lenyomás hatására elkezd gyorsulni, irányváltáskor pedig lefékez, majd megfordul + playerVelocity.x += directionOfMovement * playerXAcceleration * deltaTime; + //hogy ne lehesen gyorsabb a max sebességénél + if(playerVelocity.x > playerMaxVelocity.x) playerVelocity.x = playerMaxVelocity.x; + else if(playerVelocity.x < -playerMaxVelocity.x) playerVelocity.x = - playerMaxVelocity.x; } else { //lassulva megáll diff --git a/Forráskód/modules/touch.c b/Forráskód/modules/touch.c new file mode 100644 index 0000000..7b5d757 --- /dev/null +++ b/Forráskód/modules/touch.c @@ -0,0 +1,375 @@ +#include "touch.h" +#include "player.h" +#include "menu/menuFSM.h" + +#ifdef __EMSCRIPTEN__ + #include +#endif + +/** \file touch.c + * Érintőképernyős vezérlés. A játék eredetileg billentyűzetre és egérre készült, itt + * fordítjuk le az ujjak mozgását ugyanerre: a mozgatógombok a billentyűzet állapotát + * írják át, a szabad felületre koppintás pedig egérkattintásként viselkedik. Így a + * játék többi része semmit nem tud arról, hogy épp érintéssel játszanak-e. + */ + +#define maxFingers 10 /**< Ennél több ujjat úgyse tesz senki a képernyőre. */ +#define noButton (-1) /**< Ez az ujj nem gombon van, tehát egérként viselkedik. */ + +typedef enum touchButton { + touchLeft, touchRight, touchJump, touchSlow, touchPause, touchButtonCount +} touchButton; + +typedef struct touchArea { + int x, y, r; +} touchArea; + +typedef struct fingerState { + SDL_FingerID id; + bool isActive; + int button; +} fingerState; + +static bool touchMode = false; +static bool isLastInputTouch = false; /**< Érintőképernyős laptopon az egér és az ujj is jó, mindig az utóbb használt számít. */ +static touchArea areas[touchButtonCount]; +static bool isHeld[touchButtonCount]; +static bool isForced[touchButtonCount]; +static fingerState fingers[maxFingers]; + +static const SDL_Scancode buttonKeys[touchButtonCount] = { + SDL_SCANCODE_A, SDL_SCANCODE_D, SDL_SCANCODE_SPACE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN +}; + +///Érintőképernyős eszközön futunk-e? Az első ujjlenyomás ezt amúgy is bekapcsolja. +void initTouch() { +#ifdef __EMSCRIPTEN__ + touchMode = EM_ASM_INT({ + return (navigator.maxTouchPoints > 0 || 'ontouchstart' in window) ? 1 : 0; + }); + //érintésre képes eszközön alapból ne villanjon fel a bal felső sarokban a kurzor + isLastInputTouch = touchMode; +#endif +} + +bool isTouchMode() { + return touchMode; +} + +///Az egérrel célzáshoz kell a rajzolt kurzor, ujjal viszont csak útban lenne. +bool shouldDrawCursor() { + return !isLastInputTouch; +} + +///Egérhasználatkor visszakapcsol az egeres megjelenésre. +void noteMouseInput() { + isLastInputTouch = false; +} + +///Csak a játék közben, életben lévő játékosnál van értelme a gomboknak. +static bool areControlsVisible() { + return touchMode && isLastInputTouch && screen != NULL && getGameState() == game && isAlive(); +} + +/** \brief Kiszámolja a gombok helyét a képernyő aktuális méretéhez. + * A gombok mérete a kisebbik képernyőoldalhoz igazodik, így fekvő és álló tájolásban is + * hüvelykujjal elérhető marad. Minden képkockában újraszámoljuk, mert az ablak bármikor + * változhat, és így nem kell külön értesítést kötni az átméretezéshez. + */ +static void layoutButtons() { + if(screen == NULL) return; + + int unit = smaller(screen -> w, screen -> h); + int r = larger(28, (int)(unit * 0.1)); + int margin = (int)(unit * 0.055) + r; + int gap = (int)(r * 0.55); + + areas[touchLeft] = (touchArea) {margin, screen -> h - margin, r}; + areas[touchRight] = (touchArea) {margin + 2 * r + gap, screen -> h - margin, r}; + areas[touchJump] = (touchArea) {screen -> w - margin, screen -> h - margin, r}; + areas[touchSlow] = (touchArea) {screen -> w - margin, screen -> h - margin - 2 * r - gap, r}; + + //a szünet gomb szorosan a sarokba kerül, különben álló tájolásban a pontszámra lógna + int pauseRadius = (int)(r * 0.6); + int pauseInset = (int)(unit * 0.035) + pauseRadius; + areas[touchPause] = (touchArea) {screen -> w - pauseInset, pauseInset, pauseRadius}; +} + +///Melyik gombra esik a megadott pont? Kicsit nagyobb sugárral számolunk, mert az ujj pontatlan. +static int buttonAt(int x, int y) { + int i; + for(i = 0; i < touchButtonCount; i++) { + float reach = areas[i].r * 1.2f; + if(square(x - areas[i].x) + square(y - areas[i].y) < square(reach)) return i; + } + return noButton; +} + +static fingerState* findFinger(SDL_FingerID id) { + int i; + for(i = 0; i < maxFingers; i++) + if(fingers[i].isActive && fingers[i].id == id) return &fingers[i]; + return NULL; +} + +static fingerState* addFinger(SDL_FingerID id) { + fingerState* existing = findFinger(id); + if(existing != NULL) return existing; + + int i; + for(i = 0; i < maxFingers; i++) + if(!fingers[i].isActive) { + fingers[i] = (fingerState) {id, true, noButton}; + return &fingers[i]; + } + return NULL; +} + +///Az ujjak alapján újraszámolja, melyik gomb van épp lenyomva. +static void refreshHeldButtons() { + int i; + for(i = 0; i < touchButtonCount; i++) isHeld[i] = false; + + for(i = 0; i < maxFingers; i++) + if(fingers[i].isActive && fingers[i].button != noButton) + isHeld[fingers[i].button] = true; +} + +/** \brief Az ujjakhoz tartozó eventeket kezeli. + * A gombokra eső ujjak a gombjukhoz tapadnak, a többi ujj egérként viselkedik: mozgatja a + * kurzort és kattint is vele. Emiatt lehet egyszerre menni, ugrani és lőni, amit egyetlen + * emulált egérrel nem lehetne. + * \param a feldolgozandó event + * \param a bal egérgomb állapota, ezt a hívó adja át a játéknak + * \param prematureClick a beállítás menü miatt kell + */ +void handleFingerEvent(SDL_Event* event, bool* isLMBDown, bool* prematureClick) { + if(screen == NULL) return; + + touchMode = isLastInputTouch = true; + layoutButtons(); + + int x = (int)(event -> tfinger.x * screen -> w); + int y = (int)(event -> tfinger.y * screen -> h); + putInsideInterval(0, &x, screen -> w - 1); + putInsideInterval(0, &y, screen -> h - 1); + + fingerState* finger; + + switch(event -> type) { + case SDL_FINGERDOWN: + finger = addFinger(event -> tfinger.fingerId); + if(finger == NULL) break; + + finger -> button = areControlsVisible() ? buttonAt(x, y) : noButton; + + if(finger -> button == noButton) { + setMousePosition(x, y); + *isLMBDown = true; + handleClicks(prematureClick); + } + //a szünet gomb kattintásként viselkedik, nem tartásként + else if(finger -> button == touchPause) whatToDoOnEscape(); + break; + + case SDL_FINGERMOTION: + finger = findFinger(event -> tfinger.fingerId); + if(finger == NULL) break; + + if(finger -> button == noButton) setMousePosition(x, y); + //a két iránygomb közt át lehet csúsztatni az ujjat, a többi gomb megtartja az ujját + else if(finger -> button == touchLeft || finger -> button == touchRight) { + int moved = buttonAt(x, y); + if(moved == touchLeft || moved == touchRight) finger -> button = moved; + } + break; + + case SDL_FINGERUP: + finger = findFinger(event -> tfinger.fingerId); + if(finger == NULL) break; + + if(finger -> button == noButton) *isLMBDown = *prematureClick = false; + finger -> isActive = false; + break; + } + + refreshHeldButtons(); +} + +bool isTouchSlowMo() { + return touchMode && isHeld[touchSlow]; +} + +/** \brief A megnyomott iránygombokat billentyűlenyomásnak állítja be. + * Csak azokat a billentyűket írja át, amiket ő maga nyomott le, így egy érintőképernyős + * laptopon a valódi billentyűzet is használható marad. + */ +void applyTouchKeys() { + if(!touchMode || keys == NULL) return; + + int i; + for(i = 0; i < touchButtonCount; i++) { + if(buttonKeys[i] == SDL_SCANCODE_UNKNOWN) continue; + + if(isHeld[i]) { + keys[buttonKeys[i]] = 1; + isForced[i] = true; + } + else if(isForced[i]) { + keys[buttonKeys[i]] = 0; + isForced[i] = false; + } + } +} + +///Egy gomb korongja, lenyomva világosabb. +static void drawButtonBase(touchArea area, bool pressed) { + filledCircleColor(screen, area.x, area.y, area.r, pressed ? 0xFFFFFF44 : 0xFFFFFF1E); + aacircleColor(screen, area.x, area.y, area.r, pressed ? 0xFFFFFFCC : 0xFFFFFF66); +} + +///Háromszög alakú nyíl, az irány -1 (balra), +1 (jobbra) vagy 0 (felfelé). +static void drawArrow(touchArea area, int direction) { + float size = area.r * 0.45f; + float vx[3], vy[3]; + + if(direction == 0) { + vx[0] = area.x; vy[0] = area.y - size; + vx[1] = area.x - size; vy[1] = area.y + size * 0.8; + vx[2] = area.x + size; vy[2] = area.y + size * 0.8; + } + else { + vx[0] = area.x + direction * size; vy[0] = area.y; + vx[1] = area.x - direction * size * 0.8; vy[1] = area.y - size; + vx[2] = area.x - direction * size * 0.8; vy[2] = area.y + size; + } + + filledPolygonRGBA(screen, vx, vy, 3, 255, 255, 255, 220); + aapolygonRGBA(screen, vx, vy, 3, 255, 255, 255, 220); +} + +///Az időlassítás ikonja egy óra, a szünet gombé két álló csík. +static void drawSlowMoIcon(touchArea area) { + Sint16 size = (Sint16)(area.r * 0.45); + aacircleColor(screen, area.x, area.y, size, 0xFFFFFFDC); + lineColor(screen, area.x, area.y, area.x, area.y - (Sint16)(size * 0.7), 0xFFFFFFDC); + lineColor(screen, area.x, area.y, area.x + (Sint16)(size * 0.5), area.y, 0xFFFFFFDC); +} + +static void drawPauseIcon(touchArea area) { + int width = larger(2, (int)(area.r * 0.16)); + int height = (int)(area.r * 0.45); + int gap = (int)(area.r * 0.2); + + boxRGBA(screen, area.x - gap - width, area.y - height, area.x - gap, area.y + height, 255, 255, 255, 220); + boxRGBA(screen, area.x + gap, area.y - height, area.x + gap + width, area.y + height, 255, 255, 255, 220); +} + +///Kirajzolja a képernyőre az érintéses gombokat, egérrel játszva semmit nem csinál. +void drawTouchControls() { + if(!areControlsVisible()) return; + + layoutButtons(); + + int i; + for(i = 0; i < touchButtonCount; i++) drawButtonBase(areas[i], isHeld[i]); + + drawArrow(areas[touchLeft], -1); + drawArrow(areas[touchRight], 1); + drawArrow(areas[touchJump], 0); + drawSlowMoIcon(areas[touchSlow]); + drawPauseIcon(areas[touchPause]); +} + + +#ifdef __EMSCRIPTEN__ + +/** \brief Megjeleníti a névbeíráshoz tartozó html mezőt. + * Az emscripten SDL2-je nem hozza elő a mobilos billentyűzetet, ráadásul iOS-en csak a + * felhasználó koppintására lehet fókuszt adni egy mezőnek. Ezért egy látható beviteli + * mezőt teszünk a vászon fölé, amit a játékos maga koppint meg. + */ +EM_JS(void, showNameOverlay, (), { + var box = document.getElementById('nameOverlay'); + + if (!box) { + box = document.createElement('form'); + box.id = 'nameOverlay'; + box.style.cssText = + 'position:fixed;left:50%;top:62%;transform:translate(-50%,-50%);z-index:10;' + + 'display:flex;gap:8px;padding:10px;border-radius:12px;' + + 'background:rgba(26,5,5,0.92);border:1px solid rgba(232,213,213,0.35);' + + 'font:16px system-ui,sans-serif;touch-action:manipulation;'; + + var field = document.createElement('input'); + field.type = 'text'; + field.maxLength = 40; + field.placeholder = 'Neved'; + field.autocomplete = 'off'; + field.autocapitalize = 'off'; + field.style.cssText = + 'width:9em;min-width:0;padding:10px 12px;font:inherit;color:#e8d5d5;' + + 'background:rgba(255,255,255,0.08);border:0;border-radius:8px;outline:none;'; + + var done = document.createElement('button'); + done.type = 'submit'; + done.textContent = 'Kész'; + done.style.cssText = + 'padding:10px 16px;font:inherit;color:#1a0505;background:#e8d5d5;' + + 'border:0;border-radius:8px;'; + + box.appendChild(field); + box.appendChild(done); + document.body.appendChild(box); + + field.addEventListener('input', function () { + Module.ccall('mobileNameChanged', null, ['string'], [field.value]); + }); + + box.addEventListener('submit', function (event) { + event.preventDefault(); + field.blur(); + Module.ccall('mobileNameSubmitted', null, [], []); + }); + } + + var current = box.querySelector('input'); + current.value = ""; + box.style.display = 'flex'; +}); + +EM_JS(void, hideNameOverlay, (), { + var box = document.getElementById('nameOverlay'); + if (box) { + box.querySelector('input').blur(); + box.style.display = 'none'; + } +}); + +///A html mezőbe írt teljes nevet veszi át. +EMSCRIPTEN_KEEPALIVE void mobileNameChanged(const char* utf8) { + setNameInput(utf8); +} + +///A "Kész" gomb megnyomását továbbítja. +EMSCRIPTEN_KEEPALIVE void mobileNameSubmitted() { + submitNameInput(); +} + +#endif + +///A névbeíró állapotba lépve előveszi, kilépve elrejti a böngésző beviteli mezőjét. +void syncSoftKeyboard(states state) { +#ifdef __EMSCRIPTEN__ + static bool isShown = false; + bool isWanted = touchMode && state == nameInput; + + if(isWanted == isShown) return; + isShown = isWanted; + + if(isWanted) showNameOverlay(); + else hideNameOverlay(); +#else + (void) state; +#endif +} diff --git a/Forráskód/modules/touch.h b/Forráskód/modules/touch.h new file mode 100644 index 0000000..175b713 --- /dev/null +++ b/Forráskód/modules/touch.h @@ -0,0 +1,17 @@ +#ifndef TOUCH_H_INCLUDED +#define TOUCH_H_INCLUDED + + #include "global.h" + #include "menu/menuDraw.h" + + void initTouch(); + bool isTouchMode(); + bool shouldDrawCursor(); + void noteMouseInput(); + void handleFingerEvent(SDL_Event* event, bool* isLMBDown, bool* prematureClick); + bool isTouchSlowMo(); + void applyTouchKeys(); + void drawTouchControls(); + void syncSoftKeyboard(states state); + +#endif // TOUCH_H_INCLUDED diff --git a/web/shell.html b/web/shell.html index 55216e5..f600e67 100644 --- a/web/shell.html +++ b/web/shell.html @@ -2,7 +2,7 @@ - + Platform játék
-
Betöltés…