Add mobile support
All checks were successful
Build & deploy web version / Build WebAssembly & deploy (push) Successful in 1m1s
All checks were successful
Build & deploy web version / Build WebAssembly & deploy (push) Successful in 1m1s
This commit is contained in:
parent
6e4f0428dc
commit
c4f7565d18
1 changed files with 567 additions and 69 deletions
375
Forráskód/modules/touch.c
Normal file
375
Forráskód/modules/touch.c
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue