La boucle de jeu et les tâches
Choisir la forme du programme, finir chaque image avec Sync(), attendre en temps de jeu, et mener plusieurs travaux de front avec des tâches coopératives.
En un mot#
C'est l'Engine qui dessine les images, pas votre programme. Un programme BASIC
fait un peu de travail, puis rend la main pour que l'image soit dessinée.
Sync() est ce passage de main. Les tâches permettent à plusieurs travaux de
se relayer à ces moments-là.
Trois formes de programme#
| Forme | À quoi elle ressemble | Quand elle s'exécute | Pour quoi faire |
|---|---|---|---|
| Linéaire | des instructions, de haut en bas | une fois, au démarrage | l'initialisation, des outils, des tests |
| Boucle principale | un seul DO … LOOP avec Sync() | le corps de la boucle tourne une fois par image | un jeu classique façon AGK ou DarkBASIC |
| Fonctions de cycle de vie | FUNCTION start(), FUNCTION update(dt)… | l'Engine les appelle | le même modèle que les scripts JavaScript |
Linéaire#
lives = 3
Console("Vies : " + Str(lives)) ' Vies : 3
Le programme s'exécute une fois, de haut en bas, puis se termine.
La boucle principale#
[instructions] ' une fois, avant la première image
DO
[instructions] ' à chaque image
Sync()
LOOP
[instructions] ' une fois, quand EXIT quitte la boucle
frames = 0
Console("début") ' début
DO
frames = frames + 1
IF frames = 3 THEN EXIT
Sync()
LOOP
Console("sorti après " + Str(frames)) ' sorti après 3
- Un seul
DO … LOOPpar programme, au niveau supérieur. Pas dans unIF, unWHILEni uneFUNCTION. - La boucle doit contenir
Sync()(ouSwap(), ouYieldTask()). Sans lui, le compilateur s'arrête sur « DO/LOOP requires SYNC() so the loop can yield a frame ». EXITquitte la boucle.ENDarrête tout le programme sur-le-champ.
DO
Sync()
LOOP
DO ' refusé : une seconde boucle principale
Sync()
LOOP
Dans AGK et DarkBASIC,
DO … LOOPn'est qu'une boucle sans fin qu'on place où l'on veut. Dans aukimi, c'est la boucle d'image : une par programme. Pour toute autre boucle, utilisezWHILE … ENDWHILEouREPEAT … UNTIL.
⚠️ N'écrivez pas de FUNCTION main() dans un programme qui a une boucle
DO. La boucle est compilée dans une fonction de ce nom, et les deux se
télescopent. Le compilateur ne dit rien, et la boucle ne tourne jamais.
Les fonctions de cycle de vie#
GLOBAL elapsed# = 0
FUNCTION start()
Console("prêt") ' prêt
ENDFUNCTION
FUNCTION update(dt#)
elapsed# = elapsed# + dt#
Print("Temps : " + Str(elapsed#, 1))
ENDFUNCTION
L'Engine appelle start une fois, puis update à chaque image, avec la durée
de l'image en secondes. Il n'y a pas de boucle à écrire ni de Sync() à
appeler : l'Engine dessine l'image quand update rend la main. La liste
complète des points d'entrée (fixedUpdate, onCollisionEnter, onMessage…)
est dans Les fonctions.
⚠️ Un programme en cycle de vie ne peut pas attendre. Wait(), Sleep(),
AWAITTASK, AWAITALL et AWAITANY sont refusés dans une FUNCTION. Pire :
un seul d'entre eux n'importe où dans un programme sans DO, même dans
l'étiquette d'une tâche, enferme tout le programme dans un unique main(), et
l'Engine ne trouve plus start ni update. Rien ne le signale. Pour mesurer
une durée dans un programme en cycle de vie, comptez les images dans une tâche
avec YieldTask() (voir Les tâches, plus bas), ou additionnez les dt.
Finir une image : Sync(), Swap(), YieldTask()#
| Appel | Ce qu'il fait |
|---|---|
Sync() | termine l'image : l'Engine dessine, et le programme reprend à l'image suivante. Il attend aussi les modèles 3D encore en chargement : l'image qui suit LoadObject(...) et Sync() contient le modèle. |
Swap() | la même chose. Conservé pour la séquence classique Render2DBack(), Render3D(), Render2DFront(), Swap() : les trois Render ne font rien, Swap() dessine toute l'image, et l'on obtient la bonne image. |
YieldTask() | encore la même chose, sous un nom qui se lit bien dans une tâche |
Les trois exigent leurs parenthèses : SYNC seul est refusé.
Quand ils rendent vraiment la main#
Sync()ne cède une image que là où le programme peut attendre : le programme a unDO … LOOPde premier niveau, ou il contient une instructionEND, ou l'appel se trouve dans une étiquette (un sous-programmeGOSUBou une tâche).
Partout ailleurs, Sync() dessine une fois et revient aussitôt. Le programme
ne fait pas de pause.
⚠️ Le piège du WHILE linéaire. C'est la boucle classique « attendre un
clic ». Dans un programme sans DO ni END, son Sync() ne cède jamais :
la boucle tourne sans répit et fige l'onglet du navigateur. Aucune erreur.
' Ni DO ni END dans ce programme : ne le lancez pas.
ready = 0
WHILE ready = 0
IF PointerPressed() THEN ready = 1
Sync() ' dessine, mais n'attend pas l'image suivante
ENDWHILE
La correction tient en une ligne. Avec un END, la même boucle cède à chaque
image :
ready = 0
WHILE ready = 0
IF PointerPressed() THEN ready = 1
Sync() ' cède : le programme contient un END
ENDWHILE
Console("clic") ' clic
END
- Un
Wait()ou unSleep()ailleurs dans le programme ne corrige pas le problème. SeulsDO … LOOP,ENDou une étiquette le font. - Dans AGK,
Sync()termine l'image où qu'il soit écrit. C'est la principale différence à retenir.
⚠️ N'appelez jamais Sync() dans une FUNCTION. Sans boucle DO, il ne
cède pas. Avec une, le programme ne démarre pas. Les fonctions de cycle de vie
n'en ont jamais besoin : l'Engine dessine quand elles rendent la main.
Avant DO et après LOOP#
| Où | S'exécute | Autorisé |
|---|---|---|
avant DO | une fois, avant la première image | tout : chargement, Wait(), un écran de chargement en WHILE … Sync() |
entre DO et LOOP | une fois par image | tout |
après LOOP | une fois, quand EXIT quitte la boucle | tout sauf Wait() et Sleep() |
Console("chargement") ' chargement
Wait(0.5) ' une demi-seconde de temps de jeu
frames = 0
DO
frames = frames + 1
IF frames = 120 THEN EXIT
Sync()
LOOP
Console("fini après " + Str(frames) + " images") ' fini après 120 images
- Un
ENDdans la boucle saute le code qui suitLOOP. - Les blocs
FUNCTIONet les étiquettes peuvent se placer n'importe où, avant ou après la boucle. Ce n'est pas du « code aprèsLOOP» : ils ne s'exécutent que quand on les appelle. Wait()ouSleep()aprèsLOOPest refusé :must appear before or inside the top-level DO/LOOP.
Attendre : Wait() et Sleep()#
Wait(<secondes>)
Sleep(<millisecondes>)
Console("3") ' 3
Wait(1) ' une seconde
Console("2") ' 2
Sleep(500) ' une demi-seconde
Console("partez") ' partez
- Les deux attendent en temps de jeu. Ils s'arrêtent quand le jeu est en pause, et suivent l'échelle de temps. L'Engine continue de dessiner pendant l'attente.
Waitcompte en secondes,Sleepen millisecondes.Waitest un ajout d'aukimi ;Sleepest la commande d'AGK.- ⚠️ Dans DarkBASIC,
WAITcompte en millisecondes. Ici,Wait(1000)attend seize minutes.
| Endroit | Wait() / Sleep() | Sync() / Swap() / YieldTask() |
|---|---|---|
| programme linéaire | oui | ne cède que si le programme contient un END |
avant DO ou dans la boucle | oui | oui |
après LOOP | refusé | oui |
une étiquette (sous-programme GOSUB ou tâche) | oui | oui |
dans une FUNCTION, cycle de vie compris | refusé | ne cède pas : à éviter |
FUNCTION Pause()
Wait(1) ' refusé : une FUNCTION ne peut pas attendre
ENDFUNCTION
Le message est Wait(...) is supported in the main BASIC program, not inside FUNCTION. Pour attendre dans un bout de code réutilisable, écrivez une
étiquette et rejoignez-la par GOSUB, ou lancez-la comme tâche. Un GOSUB
vers une étiquette qui attend exige une boucle DO ou un END dans le
programme, sinon le compilateur le refuse.
Print dessine, Console journalise#
| Appel | Où ça va | À quelle fréquence l'appeler |
|---|---|---|
Print(...) | dessiné à l'écran, en haut à gauche | à chaque image, dans la boucle |
PrintC(...) | pareil, sans retour à la ligne | à chaque image |
Console(...) | la console de l'éditeur | quand quelque chose se produit |
Debug(...) | comme Console, sous le nom de PureBasic | quand quelque chose se produit |
ConsoleWarning, ConsoleError, ClearConsole | la console | quand quelque chose se produit |
Print écrit dans l'image en cours de construction. Chaque nouvelle image qui
dessine quoi que ce soit repart d'une couche de texte vide : un texte imprimé
une seule fois disparaît. Imprimez-le à chaque image.
Console écrit une ligne, une fois. Ne l'appelez pas à chaque image : les
vraies erreurs s'y noient. Les lignes identiques d'une même rafale sont
regroupées et comptées.
score = 0
DO
score = score + 1
Print("Score : " + Str(score)) ' redessiné à chaque image
IF score = 100 THEN Console("100 atteint") ' journalisé une fois
IF score = 200 THEN EXIT
Sync()
LOOPLes tâches#
Une tâche exécute une étiquette à côté du programme principal. Les tâches sont coopératives : un seul morceau de code tourne à la fois, et ils ne se passent le relais que lorsque celui qui tourne rend la main.
<poignée> AS TASKHANDLE ' ou AS JOB ; facultatif
STARTTASK <poignée>, <Étiquette>
AWAITTASK <poignée>
AWAITALL <poignée>, <poignée> [, <poignée>…]
AWAITANY <poignée>, <poignée> [, <poignée>…]
CANCELTASK <poignée>
YieldTask()
<Étiquette>:
[instructions]
RETURN <valeur>
Lancer une tâche et attendre son résultat#
job AS TASKHANDLE
STARTTASK job, CountDown
Console("lancée") ' lancée
AWAITTASK job
Console("résultat " + Str(TaskResult(job))) ' résultat 42
END
CountDown:
FOR i = 3 TO 1 STEP -1
Console("tic " + Str(i)) ' tic 3, tic 2, tic 1
Wait(0.5)
NEXT
RETURN 42
STARTTASKne lance pas la tâche sur-le-champ. La tâche démarre au prochain moment où le programme principal rend la main :Sync(),Wait(),AWAITTASK… C'est pourquoilancées'affiche en premier.- Une tâche est une étiquette qui se termine par
RETURN valeur. Cette valeur est ce queTaskResultrend. UnRETURNseul ne laisse aucun résultat utilisable. - Les tâches partagent les variables du programme. Une tâche peut mettre le score à jour ; la boucle principale voit la nouvelle valeur dès sa ligne suivante.
- Une tâche peut lancer d'autres tâches et les attendre.
AWAITTASK,AWAITALLetAWAITANYsont refusés dans uneFUNCTION, commeWait().STARTTASKetCANCELTASKy sont permis.
⚠️ Dans une étiquette, donnez à chaque attente sa propre ligne. Le
compilateur décide si une étiquette attend en lisant ses lignes. Il ne voit
pas IF … THEN Sync() ni IF … THEN Wait(1) écrits sur une ligne, ni
Sync(), Swap() ou YieldTask() suivis d'un commentaire. Le programme ne
démarre alors pas, sans aucune erreur à la compilation. Écrivez un IF en
bloc, et mettez le commentaire sur la ligne du dessus, comme dans l'exemple
plus bas.
La poignée#
STARTTASK crée la variable si elle n'existe pas, même sous
OPTION EXPLICIT. La déclarer AS TASKHANDLE (ou AS JOB, le même type) lui
donne un type :
- elle vaut d'abord
NULL; - elle refuse tout ce qui n'est pas une tâche :
job = 5arrête le programme avecBASIC value is not a task handle.
⚠️ La poignée est une variable simple. STARTTASK jobs[1], Worker n'est
pas pris en charge : lancez la tâche dans une variable, puis copiez-la dans le
tableau.
Lire l'état d'une tâche#
| Appel | Rend |
|---|---|
TaskDone(h) | vrai une fois que la tâche a rendu son résultat |
TaskFailed(h) | vrai si la tâche s'est arrêtée sur une erreur |
TaskCancelled(h) | vrai dès que CANCELTASK a été appelé sur elle |
TaskResult(h) | la valeur du RETURN. Une erreur si la tâche n'a pas fini, ou a échoué. |
⚠️ Ces quatre-là répondent vrai ou faux, pas 1 ou 0. Testez-les seuls ou
avec NOT : IF TaskDone(h) THEN …. Écrit IF TaskDone(h) = 1, le test est
toujours faux, et Str(TaskDone(h)) affiche true.
Interroger la tâche depuis la boucle principale laisse le jeu dessiner pendant que le travail avance :
loader AS TASKHANDLE
STARTTASK loader, LoadLevel
DO
IF TaskDone(loader)
Console("salles : " + Str(TaskResult(loader))) ' salles : 3
EXIT
ENDIF
Print("Chargement...")
Sync()
LOOP
Console("jouer") ' jouer
LoadLevel:
FOR room = 1 TO 3
' une salle par image
Sync()
NEXT
RETURN 3
Les erreurs dans une tâche#
Une erreur dans une tâche n'arrête pas le programme. La tâche est marquée en échec, et l'erreur ressort là où vous l'attendez :
t AS TASKHANDLE
STARTTASK t, Reload
TRY
AWAITTASK t
CATCH e$
Console("attrapé : " + e$) ' attrapé : plus de munitions
ENDTRY
IF TaskFailed(t) THEN Console("échec") ' échec
END
Reload:
Wait(0.2)
THROW "plus de munitions"
RETURN 0
| Attente | Quand une tâche a échoué |
|---|---|
AWAITTASK h | lève l'erreur de la tâche |
AWAITALL a, b | lève la première erreur rencontrée |
AWAITANY a, b | ne lève rien : vérifiez TaskFailed ensuite |
TaskResult(h) | lève l'erreur de la tâche |
Les erreurs sont traitées dans Les erreurs.
Annuler une tâche#
CANCELTASK h demande à la tâche de s'arrêter. Elle s'arrête à sa prochaine
attente : Wait(), Sleep(), Sync(), Swap() ou YieldTask(). En général le
code qui suit cette attente ne s'exécute jamais. L'exception est une attente
placée dans un TRY : voir plus bas.
slow AS TASKHANDLE
fast AS TASKHANDLE
STARTTASK slow, Slow
STARTTASK fast, Fast
AWAITANY slow, fast
IF TaskDone(fast) THEN Console("fast a fini en premier") ' fast a fini en premier
CANCELTASK slow
AWAITALL slow, fast
IF TaskCancelled(slow) THEN Console("slow annulée") ' slow annulée
END
Slow:
Wait(10)
Console("jamais affiché")
RETURN 1
Fast:
Sync()
RETURN 2
TaskCancelledest vrai aussitôt, etAWAITTASKsur une tâche annulée revient aussitôt, sans erreur.TaskResultsur une tâche annulée est une erreur : elle n'a pas de résultat.CANCELTASKsur une tâche déjà terminée ne fait rien.- ⚠️ Une tâche annulée juste après
STARTTASKs'exécute quand même jusqu'à sa première attente, puisqu'elle n'avait pas encore démarré. - ⚠️ Une tâche qui n'attend jamais ne peut pas être arrêtée : elle va jusqu'à
son
RETURNd'une traite.AWAITTASKdans une tâche n'est pas non plus un point d'annulation. - ⚠️ Un
TRYautour d'une attente attrape l'annulation. SonCATCHreçoit le messageBASIC task cancelled, et le code aprèsENDTRYcontinue jusqu'à la prochaine attente. Pour laisser passer l'annulation, relancez-la :CATCH e$, puisIF e$ = "BASIC task cancelled", puisRETHROW.
Pourquoi il n'y a pas de fils d'exécution#
Les fils d'exécution à mémoire partagée sont volontairement exclus. Pas « pas encore » : exclus.
Avec les tâches, un seul morceau de code tourne à la fois, et le relais ne se fait qu'à une attente. Entre deux attentes, rien d'autre ne peut toucher à vos variables. Pas de verrou à poser, et pas de course de données à traquer.
PureBasic propose CreateThread avec des mutex ; AGK et DarkBASIC n'ont pas
de tâches du tout. Dans aukimi, le vrai travail en parallèle passe ailleurs :
- le gros calcul part sur le GPU, voir Le calcul GPU ;
CreateWorkerdémarre un Web Worker écrit en JavaScript. Il échange des messages JSON et ne partage aucune mémoire.
Limites et pannes courantes#
Sync()fige une boucleWHILElinéaire. Ajoutez unEND, ou passez à unDO … LOOP.- Un seul
DO … LOOP, au niveau supérieur. Les autres boucles sont desWHILEou desREPEAT. - Pas d'attente dans une
FUNCTION. Utilisez une étiquette avecGOSUB, ou une tâche. Waiten secondes,Sleepen millisecondes. Les deux en temps de jeu.Printà chaque image,Consoleune fois.- Testez l'état d'une tâche seul :
IF TaskDone(h), jamais= 1. - Dans une étiquette, chaque attente sur sa ligne : pas de
IF … THEN Sync(), pas de commentaire aprèsSync().