0.3.1-0-pre.4

This commit is contained in:
2026-09-21 01:21:33 +02:00
parent 4f677dbc77
commit a36b666d22
14 changed files with 270 additions and 29 deletions

View File

@@ -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 nenregistre 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 darchitecture et appartiendraient à une UX produit réelle.

View File

@@ -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 quun 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 lapplication 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, lapplication 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 nest pas un critère bloquant de cette gate.

View File

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

View File

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

View 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();
},
};
}

View File

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