0.3.1-0-pre.4
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/apps/game-snake-poc-tauri/README.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# game-snake-poc-tauri
|
||||
|
||||
@@ -31,3 +31,13 @@ Les hooks `beforeDevCommand` et `beforeBuildCommand` de Tauri possèdent le buil
|
||||
Le host ne modifie pas le projet Android généré par Tauri. Pour `0.3.1`, la chaîne Tauri 2.11.x est conservée telle que générée : Gradle `8.14.3` et Android Gradle Plugin `8.11.0`. Le JDK Android de référence du projet est JDK 17, qui correspond au minimum et à la valeur par défaut documentés par AGP 8.11. Le Java 25 global et le JBR 25 de l'Android Studio actuellement installé ne doivent pas exécuter ce wrapper.
|
||||
|
||||
Voir [`USAGE.md`](USAGE.md) pour l'initialisation locale et les smokes Android.
|
||||
|
||||
## Lifecycle et Back Android
|
||||
|
||||
Le host suspend explicitement sa boucle `requestAnimationFrame` lorsque la WebView devient cachée et la reprend en réinitialisant son horloge locale. Une reprise après background ne doit donc pas simuler le temps passé hors écran. Resize et rotation passent par un `ResizeObserver` sur la zone de jeu et réévaluent aussi la provenance device.
|
||||
|
||||
Le POC reste mono-page et n’enregistre volontairement aucun handler Tauri `onBackButtonPress`. La politique `EngineGame::quit_requested` de Snake est actuellement la politique moteur par défaut `Exit`, cohérente avec le Back Android/Tauri naturel. Un futur jeu retournant `Continue` devra introduire une intégration Back explicite au lieu de modifier ce POC silencieusement.
|
||||
|
||||
## Limite ergonomique connue
|
||||
|
||||
Le D-pad tactile reste sous le Canvas dans ce POC et peut nécessiter un scroll sur petit écran. Des contrôles superposés ou des gestes swipe seraient techniquement possibles, mais ne sont pas introduits dans `0.3.1` : ils ne changent pas la conclusion d’architecture et appartiendraient à une UX produit réelle.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/apps/game-snake-poc-tauri/USAGE.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# Utilisation — Snake Tauri Android
|
||||
|
||||
@@ -64,3 +64,16 @@ Ne pas utiliser `npm run dev` ou `npm run build` comme gate manuelle : ces scrip
|
||||
## Positionnement
|
||||
|
||||
`game-snake-poc-tauri` est un POC de référence. Il ne doit pas être utilisé comme base automatique des futurs jeux Android ; la cible Android productive reste SDL3 natif tant qu’un besoin concret ne justifie pas la WebView Tauri.
|
||||
|
||||
## Smoke lifecycle `0-pre.4`
|
||||
|
||||
Pendant `cargo tauri android dev` :
|
||||
|
||||
1. lancer une partie et effectuer au moins une entrée tactile ;
|
||||
2. mettre l’application en arrière-plan puis la ramener au premier plan ;
|
||||
3. vérifier que le serpent reprend sans accélération ni rattrapage de la durée passée en background ;
|
||||
4. faire pivoter/redimensionner l’émulateur si disponible et vérifier que le Canvas reste rendu ;
|
||||
5. utiliser Back Android : pour Snake, l’application doit suivre le comportement système de sortie, sans navigation Web interne ni interception spécifique ;
|
||||
6. vérifier dans logcat les traces `runtime paused`, `runtime resumed` et les événements `visibility hidden/visible`.
|
||||
|
||||
Le D-pad placé sous le Canvas est une limite ergonomique connue du POC de référence et n’est pas un critère bloquant de cette gate.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/apps/game-snake-poc-tauri/frontend/main.html -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
@@ -23,7 +23,7 @@
|
||||
<output id="score">Score : 0</output>
|
||||
<output id="length">Longueur : 0</output>
|
||||
</div>
|
||||
<div class="game-stage">
|
||||
<div id="game-stage" class="game-stage">
|
||||
<canvas id="game" width="360" height="600" tabindex="0" aria-label="Zone de jeu Snake"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/game.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import init, { SnakeWasmGame } from "@snake-wasm";
|
||||
import type { RuntimeDescriptor } from "./bridge";
|
||||
@@ -13,6 +13,8 @@ export type SnakeDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
export interface SnakeTauriSession {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
renderNow(): void;
|
||||
refreshDeviceClass(): void;
|
||||
dispose(): void;
|
||||
@@ -92,6 +94,7 @@ export async function startGame(
|
||||
let previous = performance.now();
|
||||
let accumulator = 0;
|
||||
let animationFrame: number | null = null;
|
||||
let running = false;
|
||||
let disposed = false;
|
||||
|
||||
function renderNow(): void {
|
||||
@@ -101,8 +104,16 @@ export async function startGame(
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
}
|
||||
|
||||
function schedule(): void {
|
||||
if (!running || disposed || animationFrame !== null) {
|
||||
return;
|
||||
}
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
function frame(now: number): void {
|
||||
if (disposed) {
|
||||
animationFrame = null;
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS);
|
||||
@@ -113,11 +124,35 @@ export async function startGame(
|
||||
accumulator -= FRAME_MILLIS;
|
||||
}
|
||||
renderNow();
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
schedule();
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
running = false;
|
||||
accumulator = 0;
|
||||
if (animationFrame !== null) {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
frontendInfo("Snake Tauri runtime paused");
|
||||
}
|
||||
|
||||
function resume(): void {
|
||||
if (running || disposed) {
|
||||
return;
|
||||
}
|
||||
previous = performance.now();
|
||||
accumulator = 0;
|
||||
running = true;
|
||||
schedule();
|
||||
frontendInfo("Snake Tauri runtime resumed");
|
||||
}
|
||||
|
||||
renderNow();
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
resume();
|
||||
return {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void {
|
||||
if (disposed) {
|
||||
@@ -126,6 +161,8 @@ export async function startGame(
|
||||
provenance.recordInput(source);
|
||||
queueDirection(game, direction);
|
||||
},
|
||||
pause,
|
||||
resume,
|
||||
renderNow,
|
||||
refreshDeviceClass(): void {
|
||||
if (!disposed) {
|
||||
@@ -136,12 +173,10 @@ export async function startGame(
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
pause();
|
||||
disposed = true;
|
||||
if (animationFrame !== null) {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
game.free();
|
||||
frontendInfo("Snake Tauri runtime disposed");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
60
crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
Normal file
60
crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
|
||||
// version: 1
|
||||
|
||||
import type { SnakeTauriSession } from "./game";
|
||||
import { frontendInfo } from "./logging";
|
||||
|
||||
export interface SnakeTauriLifecycleBinding {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function bindTauriLifecycle(session: SnakeTauriSession, stage: HTMLElement): SnakeTauriLifecycleBinding {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
});
|
||||
|
||||
function synchronizeVisibility(): void {
|
||||
if (document.visibilityState === "hidden") {
|
||||
session.pause();
|
||||
frontendInfo("Snake Tauri lifecycle: visibility hidden");
|
||||
return;
|
||||
}
|
||||
session.resume();
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
frontendInfo("Snake Tauri lifecycle: visibility visible");
|
||||
}
|
||||
|
||||
function pageHide(event: PageTransitionEvent): void {
|
||||
if (event.persisted) {
|
||||
session.pause();
|
||||
} else {
|
||||
session.dispose();
|
||||
}
|
||||
frontendInfo(`Snake Tauri lifecycle: page hide persisted=${event.persisted}`);
|
||||
}
|
||||
|
||||
function pageShow(event: PageTransitionEvent): void {
|
||||
session.resume();
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
frontendInfo(`Snake Tauri lifecycle: page show persisted=${event.persisted}`);
|
||||
}
|
||||
|
||||
resizeObserver.observe(stage);
|
||||
document.addEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.addEventListener("pagehide", pageHide);
|
||||
window.addEventListener("pageshow", pageShow);
|
||||
synchronizeVisibility();
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
resizeObserver.disconnect();
|
||||
document.removeEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.removeEventListener("pagehide", pageHide);
|
||||
window.removeEventListener("pageshow", pageShow);
|
||||
session.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import "../css/main.css";
|
||||
import { loadSnakeTauriAssets } from "./assets";
|
||||
import { getRuntimeDescriptor, reportFrontendReady } from "./bridge";
|
||||
import { startGame } from "./game";
|
||||
import { bindInputs } from "./input";
|
||||
import { bindTauriLifecycle } from "./lifecycle";
|
||||
import { frontendError, frontendInfo, initializeFrontendTracing } from "./logging";
|
||||
|
||||
function requiredElement<T extends HTMLElement>(identifier: string): T {
|
||||
@@ -21,6 +22,7 @@ async function bootstrap(): Promise<void> {
|
||||
frontendInfo("Snake Tauri frontend bootstrap started");
|
||||
const descriptor = await getRuntimeDescriptor();
|
||||
const canvas = requiredElement<HTMLCanvasElement>("game");
|
||||
const stage = requiredElement<HTMLElement>("game-stage");
|
||||
const score = requiredElement<HTMLOutputElement>("score");
|
||||
const length = requiredElement<HTMLOutputElement>("length");
|
||||
const provenance = requiredElement<HTMLOutputElement>("runtime-provenance");
|
||||
@@ -37,11 +39,7 @@ async function bootstrap(): Promise<void> {
|
||||
status.textContent = "Chargement WASM…";
|
||||
const session = await startGame(canvas, score, length, provenance, descriptor);
|
||||
bindInputs(session, buttons);
|
||||
window.addEventListener("resize", () => {
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
});
|
||||
window.addEventListener("pagehide", () => session.dispose(), { once: true });
|
||||
bindTauriLifecycle(session, stage);
|
||||
|
||||
document.body.dataset.runtimeState = "ready";
|
||||
status.textContent = "Prêt";
|
||||
|
||||
Reference in New Issue
Block a user