Add mobile support
All checks were successful
Build & deploy web version / Build WebAssembly & deploy (push) Successful in 1m1s

This commit is contained in:
Andras Schmelczer 2026-08-09 20:39:30 +01:00
commit c4f7565d18
18 changed files with 567 additions and 69 deletions

View file

@ -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}"

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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)));

View file

@ -7,6 +7,7 @@
tiny, small, smallBold, large
} fontType;
float uiScale();
void openFonts();
void freeFonts();
bool isValidChar(wchar_t chr);

View file

@ -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++) {

View file

@ -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);

View file

@ -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;

View file

@ -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], &currentOptions[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], &currentOptions[i], maxOptions[i]);
}
}
}
printBack();
drawMenuParticles(deltaTime);
printBack();
drawMenuParticles(deltaTime);
}
///A játék leírását rajzolja meg.

View file

@ -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) {

View file

@ -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

View file

@ -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)
};

View file

@ -63,13 +63,11 @@ void movePlayer(unsigned char* keys, int deltaTime) {
//<\input feldolgozása>
//<sebesség kiszámítá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

375
Forráskód/modules/touch.c Normal file
View file

@ -0,0 +1,375 @@
#include "touch.h"
#include "player.h"
#include "menu/menuFSM.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#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
}

View file

@ -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

View file

@ -2,7 +2,7 @@
<html lang="hu">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>Platform játék</title>
<style>
html, body {
@ -12,6 +12,15 @@
color: #e8d5d5;
font: 16px/1.5 system-ui, sans-serif;
overflow: hidden;
overscroll-behavior: none;
}
#shell, canvas {
touch-action: none;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
#shell {
@ -27,33 +36,23 @@
background: #280000;
outline: none;
}
#status {
position: fixed;
bottom: 12px;
font-size: 14px;
opacity: 0.65;
}
</style>
</head>
<body>
<div id="shell">
<canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
</div>
<div id="status">Betöltés…</div>
<script>
var statusElement = document.getElementById('status');
var canvasElement = document.getElementById('canvas');
var Module = {
canvas: canvasElement,
preRun: [],
postRun: [],
postRun: [function () {
canvasElement.focus();
}],
print: function (text) { console.log(text); },
printErr: function (text) { console.error(text); },
setStatus: function (text) {
if (text) statusElement.textContent = text;
}
printErr: function (text) { console.error(text); }
};
canvasElement.addEventListener('click', function () { canvasElement.focus(); });