Les ordinateurs Internet offrent plusieurs avantages par rapport aux plates-formes de cloud computing traditionnelles, qui offrent une expérience de développement d'applications plus rationalisée.
Je suis ingénieur chez DFINITY, mais je suis également développeur de logiciels, je voulais donc tester cette prémisse et évaluer l'expérience de construction sur un ordinateur Internet du point de vue d'un développeur Web.
J'ai choisi de construire une version réversible, un jeu de société de stratégie pour deux joueurs, non pas comme un exemple d'application mais comme une véritable application avec toutes les possibilités et les détails que j'imaginais avoir dans un jeu réversible multijoueur.
Avant de plonger dans les détails techniques en coulisses, je souhaite me concentrer sur le concept de haut niveau : un environnement virtuel dans lequel les applications Internet peuvent se connecter de manière transparente les unes aux autres.
Je crois personnellement qu’à mesure que le cloud computing se développe, l’infrastructure deviendra une marchandise. En d’autres termes, peu importe qui fournit l’infrastructure.
L'important est le suivant : vous écrivez une application et elle s'exécute sur Internet.
modèle de programmation
L'expérience du développement d'applications Web sur un ordinateur Internet était proche de celle de plates-formes plus récentes telles que Parse (aujourd'hui disparue) ou des plates-formes similaires.
Le principe de base d'une telle plate-forme est de masquer la complexité de la création et de la maintenance des services backend (tels que les serveurs HTTP, les bases de données, les connexions des utilisateurs, etc.).
Au lieu de cela, ils fournissent un environnement virtuel abstrait qui exécute uniquement les applications utilisateur, sans que les utilisateurs sachent ou aient à prêter attention à l'endroit et à la manière dont leurs applications s'exécutent.
De ce point de vue, les ordinateurs Internet sont à la fois familiers et différents.
L'élément de base des applications informatiques Internet est un conteneur, qui est conceptuellement un processus exécuté en temps réel qui :
est 100 % déterministe (si toutes les entrées et tous les états sont identiques, la sortie doit être la même)
Persistance transparente (également appelée persistance orthogonale)
Communiquer avec des utilisateurs ou d'autres conteneurs via des messages asynchrones (appels de fonction à distance)
Traiter un message à la fois (selon le modèle d'acteur)
Si nous considérons les conteneurs Docker comme virtualisant l'intégralité d'un système d'exploitation (OS), un conteneur virtualise un seul programme, cachant presque tous les détails du système d'exploitation.
Cela semble trop restrictif car il n'exécutera pas votre système d'exploitation ou votre base de données préférée. A quoi sert-il ?
Personnellement, je préfère penser en termes de disciplines plutôt qu'en termes de limites, juste pour souligner deux propriétés (parmi tant d'autres) qui différencient le modèle de conteneur des services Web classiques :
Atomicité : les mises à jour de l'état de chaque fichier de message sont atomiques (appels de fonction à distance), l'appel réussit et l'état est mis à jour, ou une erreur est générée et l'état n'est pas touché (comme si l'appel n'avait jamais eu lieu).
Messagerie bidirectionnelle : les messages sont délivrés au maximum une fois et l'appelant du message a toujours la garantie d'une réponse de succès ou d'échec.
Il est difficile d'obtenir une telle garantie sans limiter les fonctionnalités du programme utilisateur.
Espérons qu'à la fin de cet article, vous conviendrez que le modèle de conteneur contraint peut réellement accomplir beaucoup en trouvant la combinaison optimale d'efficacité, de robustesse et de simplicité.
Client : Architecture du serveur
Les jeux multijoueurs nécessitent l'échange de données entre joueurs, et leur mise en œuvre suit généralement une architecture client-serveur :
Le serveur héberge le jeu lui-même et gère la communication avec les clients du jeu
Deux clients ou plus (chacun représentant un joueur) obtiennent l'état du serveur, restituent l'interface utilisateur du jeu et acceptent également les entrées du joueur à transmettre au serveur.
Créer un jeu multijoueur en tant qu'application Web signifie que le client doit s'exécuter dans un navigateur, en utilisant le protocole HTTP pour la communication des données et en utilisant Javascript (JS) pour afficher l'interface utilisateur du jeu sous forme de page Web.
Pour ce jeu d'inversion multijoueur, je souhaite implémenter la fonctionnalité suivante :
Deux joueurs peuvent choisir de jouer l'un contre l'autre
Les joueurs gagnent des points en remportant des parties, qui comptent également dans leur score cumulé
Tableau de bord montrant les meilleurs joueurs
Et bien sûr, il y a le déroulement habituel du jeu : prendre en compte les commentaires de chaque joueur à tour de rôle, appliquer uniquement les mouvements légaux et détecter la fin de la partie pour calculer les points.
Une grande partie de cette logique de jeu concerne la manipulation d'état, et l'implémentation côté serveur permet de garantir que les joueurs ont une vue cohérente.
serveur principal
Dans une configuration backend traditionnelle, je devrais choisir une suite de logiciels côté serveur, comprenant une base de données pour contenir les données des joueurs et des jeux, un serveur Web pour gérer les requêtes HTTP, puis écrire mon propre logiciel d'application pour combiner les deux. implémenter un ensemble complet de logique côté serveur.
Dans une configuration « sans serveur », la plate-forme fournit généralement déjà des services de serveur Web et de base de données, et je n'ai besoin que d'écrire un logiciel d'application qui appelle la plate-forme pour utiliser ces services.
Malgré le terme trompeur « sans serveur », l'application jouera toujours le rôle de « serveur » tel que dicté par l'architecture client-serveur.
Quelle que soit la configuration du backend, la pièce maîtresse de la conception de mon application est un ensemble d'API qui contrôlent la communication entre le serveur de jeu et ses clients.
Développer cette application sur un ordinateur Internet n'est pas différent, j'ai donc commencé par la conception de haut niveau suivante du flux de jeu :

Une fois les joueurs inscrits, si deux d'entre eux expriment le souhait de jouer ensemble, une nouvelle partie sera lancée en appelant start(opponent_name).
Les joueurs effectuent ensuite à tour de rôle l'action suivante, et l'autre joueur devra périodiquement appeler view() pour actualiser sa vue au dernier état du jeu, puis effectuer l'action suivante, et ainsi de suite jusqu'à la fin du jeu.
En règle générale, les joueurs ne peuvent jouer qu’à une seule partie à la fois.
Le serveur doit conserver les ensembles de données suivants :
Liste des joueurs inscrits, leurs noms et scores, etc.
Liste des jeux en cours, chaque jeu comprend le dernier plateau de jeu, qui joue au jeu en noir et blanc, qui est autorisé à bouger ensuite, et le résultat final après avoir terminé le jeu, etc.
J'ai choisi d'implémenter le serveur dans Motoko, mais en théorie, tout langage capable de compiler en Web Assembly (Wasm) devrait fonctionner correctement tant qu'il utilise la même API système pour communiquer avec les composants Internet. (Au moment d’écrire ces lignes, le SDK Rust est sur le point d’être lancé.)
En tant que nouveau langage, Motoko présente quelques avantages grossiers (par exemple, sa bibliothèque de base est un peu manquante et pas encore stable), mais il dispose déjà d'un gestionnaire de packages et d'un support Language Server Protocol (LSP) dans VSCode, ce qui rend le développement Le processus est devenu assez agréable (c'est parce que je suis un utilisateur de Vim).
Dans cet article, je ne discuterai pas du langage Motoko lui-même.
Au lieu de cela, je discuterai de certaines des fonctionnalités remarquables de Motoko et des ordinateurs Internet qui rendent le développement de conteneurs passionnant.
variable stable
La persistance orthogonale (OP) n'est pas une idée nouvelle.
Les nouvelles générations de matériel informatique tel que NVRam ont largement supprimé l'obstacle au stockage persistant de toute la mémoire programme, et l'accès au stockage externe tel que les systèmes de fichiers est devenu facultatif pour les programmes.
Cependant, un défi souvent mentionné dans la littérature OP concerne les mises à niveau, c'est-à-dire que se passe-t-il lorsqu'une mise à jour doit modifier les structures de données ou la disposition de la mémoire du programme ?
Motoko a répondu à cette question avec des variables stables. Ils peuvent survivre aux mises à niveau, ce qui, à mon avis, est idéal pour sauvegarder les données des joueurs, car je ne veux pas que les joueurs perdent leur compte lors de la mise à jour du logiciel du conteneur.
Dans le développement côté serveur régulier, je dois stocker les comptes de joueurs dans un fichier ou une base de données, ce qui est un service système de base pour les plates-formes « sans serveur ».
Seuls certains types de variables sont stables, mais sinon, elles ressemblent à toute autre variable stockant des données sur le tas et pouvant être utilisées comme telles.
Cela dit, il existe actuellement une limitation dans l'utilisation de HashMaps comme variables stables, je dois donc recourir à des tableaux. Voici un exemple :

J'espère qu'une future version du SDK DFINITY supprimera cette limitation afin que je puisse simplement utiliser un lecteur var stable sans aucune conversion.
Authentification de l'utilisateur
Chaque conteneur et chaque client (par exemple, ligne de commande dfx ou navigateur) obtiendra un identifiant principal qui les identifie de manière unique (pour les clients, ces identifiants sont automatiquement générés à partir de paires de clés publiques/privées, et la bibliothèque DFINITY JS le gère et réside actuellement dans le navigateur. stockage local).
Motoko permet au conteneur d'identifier l'appelant de la fonction "partagée", que nous pouvons utiliser à des fins d'authentification.
Par exemple, je définis les fonctions d'enregistrement et de visualisation comme suit :

L'expression msg.caller donne l'ID principal de l'appelant du message. Notez qu'il est différent de l'appelant de la fonction.
Dans Motoko, les messages aux acteurs doivent être envoyés à une fonction accessible au public, qui doit avoir un type de retour asynchrone.
Le code ci-dessus montre deux fonctions publiques : register et view, où cette dernière est un appel de requête, marqué par le mot-clé query.
Comme nous l'avons vu, l'accès au champ de l'appelant du message doit utiliser une syntaxe particulière : shared(msg) ou shared query(msg), où msg est un paramètre formel qui fait référence au message entrant dans son ensemble.
Actuellement, le seul attribut dont il dispose est l’appelant.
Pouvoir accéder à l'identifiant unique de l'appelant (expéditeur du message) semble familier, comme un cookie HTTP.
Mais contrairement à HTTP, le protocole informatique Internet garantit en réalité que l'ID du sujet est cryptographiquement sécurisé et que les programmes utilisateur exécutés sur des ordinateurs Internet peuvent avoir une confiance totale dans son authenticité.
Personnellement, je pense que faire connaître au programme son appelant est probablement trop puissant et trop rigide (par exemple, que se passe-t-il lorsqu'un tel identifiant doit être modifié ?).
Mais pour l’instant, cela conduit à un système d’authentification très simple dont les développeurs d’applications peuvent profiter, et j’espère voir davantage de développements dans ce domaine.
Concurrence et atomicité
Les clients du jeu peuvent envoyer des messages au serveur de jeu à tout moment, il est donc de la responsabilité du serveur de gérer correctement les demandes simultanées.
Dans une architecture normale, je devrais construire une logique pour déterminer l'ordre dans lequel les joueurs se déplacent (généralement via une file d'attente de transmission de messages ou un mutex).
Grâce au modèle de programmation d'acteur utilisé par le conteneur, ce problème est résolu automatiquement sans que j'aie à écrire de code pour cela.
Les messages ne sont que des appels de fonction à distance et le conteneur est assuré de ne traiter qu'un seul message à la fois. Cela se traduit par une logique de programmation simplifiée et je n'ai pas du tout à me soucier des fonctions appelées simultanément.
Étant donné que l'état du conteneur n'est conservé qu'après le traitement complet d'un message (c'est-à-dire le retour de l'appel de fonction publique), je n'ai pas à me soucier du vidage de la mémoire sur le disque, que les exceptions provoquent une corruption de l'état du disque ou soient liées à la fiabilité.
Notez également que les changements d'état persistants sont atomiques par message.
Les fonctions publiques sont libres d'appeler n'importe quelle autre fonction non asynchrone, et l'état modifié est conservé tant que l'exécution entière se termine sans erreur (pour les appels de mise à jour, plus de détails ci-dessous).
Une granularité plus fine peut être obtenue en émettant des appels asynchrones au lieu d'appels synchrones, qui deviennent de nouveaux messages que le système doit planifier plutôt que d'exécuter immédiatement.
Si je devais créer ce jeu en utilisant une architecture conventionnelle, je choisirais probablement également un framework d'acteur, tel qu'Akka de Java, Actix de Rust, etc.
Motoko offre un support natif aux acteurs, rejoignant la famille des langages de programmation basés sur les acteurs tels qu'Erlang et Pony.
Mettre à jour les appels et interroger les appels
Je pense que cette fonctionnalité pourrait vraiment améliorer l’expérience utilisateur pour les applications informatiques Internet, et les mettrait à égalité avec ce que les plates-formes cloud traditionnelles hébergent (et des ordres de grandeur plus rapides que les autres blockchains).
C'est aussi un concept simple : toute fonction publique qui ne nécessite pas de changer l'état du programme peut être marquée comme un appel « requête », sinon elle est traitée comme un appel « mise à jour » par défaut.
La différence entre les requêtes et les mises à jour réside dans la latence et la concurrence :
Un appel de requête ne peut prendre que quelques millisecondes, tandis qu'un appel de mise à jour prend environ deux secondes.
Les appels de requête peuvent être exécutés simultanément et bien évolutifs, les appels de mise à jour sont effectués séquentiellement (sur la base du modèle d'acteur) et offrent des garanties d'atomicité.
Tout comme l'exemple de code ci-dessus, j'ai pu marquer la fonction d'affichage comme un appel de requête car elle recherche et renvoie simplement l'état du jeu auquel le joueur joue.
En fait, la plupart du temps que nous naviguons sur le Web, nous effectuons des appels de requêtes : les données sont récupérées sur le serveur mais ne sont pas modifiées.
D'autre part, la fonction d'enregistrement ci-dessus est conservée comme un appel de mise à jour puisqu'elle doit ajouter le nouveau joueur à la liste des joueurs après une inscription réussie.
Les appels de mise à jour prendront plus de temps pour de nombreuses raisons telles que la cohérence des données, l'atomicité et la fiabilité.
Mais ce n’est pas un problème inhérent aux ordinateurs Internet.
De nos jours, de nombreuses actions sur le Web prennent en réalité plus de deux secondes, comme payer avec une carte de crédit, passer une commande ou se connecter à un compte bancaire, pour n'en nommer que quelques-unes.
Je pense que deux secondes constituent le point de bascule pour une bonne expérience utilisateur.
Pour en revenir au jeu inversé, lorsque le joueur effectue son prochain mouvement, il doit également s'agir d'un appel de mise à jour :

Si un jeu n'actualise son écran que deux secondes après qu'un joueur clique sur la souris (ou touche l'écran), il ne répondra pas et personne ne voudra jouer à un jeu avec un timing aussi médiocre.
J'ai donc dû optimiser cette partie en réagissant aux entrées des utilisateurs directement côté client sans avoir à attendre la réponse du serveur.
Cela signifie que l'interface utilisateur frontale devra valider les mouvements du joueur, calculer quelles pièces seront retournées et les afficher immédiatement à l'écran.
Cela signifie également que quoi que le frontend montre au joueur, à son retour, il doit correspondre à la réponse du serveur à la même action, sinon nous risquons de tomber sur des incohérences.
Mais encore une fois, je crois que toute implémentation raisonnable d'un jeu multijoueur bidirectionnel ou d'échecs peut faire cela, que son backend prenne 200 ms pour répondre ou 2 secondes.
client frontal
Le SDK DFINITY fournit un frontal qui charge les applications directement dans le navigateur.
Cependant, cela diffère des pages HTML ordinaires servies par des serveurs Web.
La communication avec le conteneur backend s'effectue via des appels de fonctions à distance, qui, dans le cas des navigateurs, sont superposés au HTTP.
Ceci est géré de manière transparente par la bibliothèque utilisateur JS, de sorte qu'un programme JS importe simplement le conteneur en tant qu'objet JS et peut appeler ses fonctions publiques tout comme les fonctions JS asynchrones habituelles de l'objet.
Le SDK DFINITY propose un ensemble de didacticiels sur la façon de configurer un frontal JS, je n'entrerai donc pas dans les détails ici.
En coulisses, la commande dfx du SDK utilise Webpack pour regrouper des ressources, notamment JS, CSS, des images et d'autres fichiers que vous pourriez avoir.
Vous pouvez également combiner vos frameworks JS préférés (tels que React, AngularJS, Vue.js, etc.) avec la bibliothèque utilisateur DFINITY pour développer un front-end JS à utiliser dans les navigateurs ou les applications mobiles.
Principaux composants de l'interface utilisateur
Je suis relativement nouveau dans le développement front-end et je n'ai qu'une brève expérience avec React.
J'ai pris la liberté d'apprendre le Mithril cette fois-ci car j'avais entendu beaucoup de bonnes choses sur le Mithril, notamment sa simplicité.
Par souci de simplicité, j'ai également imaginé un design avec seulement deux écrans :
Un écran « Jouer » qui permet aux joueurs de saisir leur propre nom et celui de leur adversaire avant d'accéder à l'écran « Jeu ». Il affichera également des conseils et des instructions, un graphique des meilleurs joueurs, des joueurs récents, et bien plus encore.
Un écran de « jeu » qui accepte les entrées du joueur et communique avec le conteneur backend pour afficher un tableau inversé. Il affichera également le score du joueur à la fin de la partie, puis ramènera le joueur à l'écran de jeu.
L'extrait de code suivant montre le cadre du front-end du jeu JS :

Il y a quelques points à noter :
Comme toute autre bibliothèque JS, le conteneur backend principal reversi est importé. Considérez-le comme un proxy qui transmet les appels de fonction à un serveur distant, reçoit les réponses et gère de manière transparente l'authentification nécessaire, la signature des messages, la sérialisation/désérialisation des données, la propagation des erreurs, etc.
Un autre conteneur reversi_assets sera également importé. Il s'agit d'un moyen d'obtenir les actifs nécessaires regroupés avec Webpack lors de l'installation du conteneur backend. Dans ce cas, j'ai un fichier son qui sera joué lorsque le joueur placera une nouvelle pièce.
Une image de logo qui entre directement dedans. Cela doit être configuré dans Webpack à l'aide de url-loader, qui est un outil qui intègre réellement le contenu de l'image sous forme de chaîne Base64 à utiliser pour l'élément d'image. Fonctionne pour les petites images mais pas pour les grandes images.
L'application finale est configurée à l'aide de Mithril via les deux chemins /play et /game. Ce dernier prend comme deux paramètres les noms du joueur et de l'adversaire, ce qui permet de recharger l'écran de jeu dans le navigateur sans interrompre la partie.
Charger des ressources à partir du conteneur d'actifs
Comme je suis nouveau dans le chargement d'éléments DOM de manière asynchrone dans JS, j'ai déployé des efforts dans ce domaine.
Lorsque DFX crée le pot, il crée également un pot reversi_assets, qui contient simplement tout ce qui se trouve dans src/reversi_assets/assets/.
J'utilise ceci pour récupérer un fichier son, mais le charger correctement n'est pas aussi simple que de placer l'URL du fichier mp3 dans le champ src de l'élément HTML.
Voici comment je le charge (si vous êtes un développeur front-end, vous le savez probablement déjà) :

Lorsque la fonction start est appelée (depuis le contexte asynchrone), elle tentera de récupérer le fichier "put.mp3" depuis le conteneur distant.
Après une récupération réussie, il utilise l'outil JS AudioContext pour décoder les données audio et initialiser la variable globale putsound.
Si putsound est correctement initialisé, un appel à playAudio(putsound) jouera le son réel :

D'autres ressources peuvent être chargées de la même manière. Je n'utilise aucune image autre que le logo, qui est petit et dont le code source peut être intégré dans Webpack en ajoutant la configuration suivante à webpack.config.js :

format d'échange de données
Le concept de Motoko est celui des données « partageables », c'est-à-dire des données qui peuvent être envoyées au-delà des frontières des conteneurs ou des langues.
Évidemment, je n'imaginerais pas qu'un pointeur de tas en C soit "partageable", mais pour moi, tout ce qui peut être mappé sur JSON est "partageable".
À cette fin, DFINITY a développé un IDL (Interface Description Language) appelé Candid pour les applications informatiques Internet.
Candid simplifie grandement la façon dont le front-end communique avec le back-end ou entre les conteneurs.
Par exemple, voici un extrait (incomplet) du conteneur réversible backend décrit par Candid :

Prenons l'exemple de la méthode move :
C'est l'une des méthodes exportées sous l'interface de service du conteneur.
Il prend en entrée deux entiers (représentant une coordonnée) et renvoie un résultat de type MoveResult.
MoveResult est une variante (également appelée énumération) qui représente les résultats et les erreurs pouvant survenir lorsque le joueur se déplace.
Dans les différentes branches de MoveResult, GameOver indique que le jeu est terminé, et prend un paramètre ColorCount, qui représente le nombre de pièces noires et blanches sur le plateau de jeu.
Le code source Motoko génère automatiquement un fichier Candid pour chaque conteneur et est automatiquement utilisé par la bibliothèque utilisateur JS sans implication du développeur :
Côté Motoko, chaque type Candid correspond à un type Motoko, et chaque méthode correspond à une fonction publique.
Côté JS, chaque type Candid correspond à un objet JSON, et chaque méthode correspond à une fonction membre de l'objet conteneur importé.
La plupart des types Candid ont une représentation JS directe, certains nécessitent une conversion.
Par exemple, nat est une précision arbitraire à la fois dans Motoko et Candid, dans JS, il est mappé sur l'entier bignumber.js, il doit donc être converti en type de nombre natif JS à l'aide de n.toNumber().
Un problème que j'ai concerne les valeurs nulles dans Candid (et le type Option de Motoko).
Il est représenté en JSON sous la forme d'un tableau vide[], plutôt que sous sa valeur nulle native. Ceci permet de distinguer le cas où nous avons des options imbriquées, telles que Option > :

Candid est très puissant, même si, à première vue, cela ressemble beaucoup à Protocolbuf ou JSON.
Alors pourquoi est-ce nécessaire ?
Il existe de nombreuses bonnes raisons au-delà de ce qui est présenté ici, et j'encourage toute personne intéressée par ce sujet à lire Candid Spec.
Synchroniser l'état du jeu avec le backend
Comme mentionné précédemment, j'ai utilisé une astuce pour réagir immédiatement aux entrées valides de l'utilisateur sans avoir à attendre la réponse du serveur de jeu principal.
Cela signifie que le frontend n'a besoin que d'un accusé de réception du serveur de jeu (ou, le cas échéant, d'une gestion des erreurs) après le déplacement du joueur.
En plus d'envoyer ses propres mouvements, le client doit également se renseigner sur les mouvements des autres joueurs.
Ceci est accompli en appelant périodiquement la fonction view() du conteneur de jeu hébergé côté serveur.
L'implication de cette conception est que je dois répéter une partie de la même logique de jeu dans le backend (Motoko) et le frontend (JS), ce qui n'est pas idéal.
Puisque Motoko compile vers Wasm et que Wasm s'exécute dans le navigateur, ne serait-il pas formidable si le frontend et le backend pouvaient partager le même module Wasm qui implémente la logique de base du jeu ? Ce type de partage ne partage que le code, pas l'état.
Cela peut nécessiter une certaine configuration, mais je pense que c'est tout à fait possible et je pourrai l'essayer dans une prochaine mise à jour.
Surtout avec le jeu inversé, dans certains cas, un joueur peut être empêché d'effectuer une action, de sorte que l'autre joueur peut effectuer deux actions consécutives, voire plus.
Afin d'afficher chaque mouvement effectué par le joueur, j'ai choisi d'implémenter l'état du jeu comme une séquence d'actions plutôt que simplement le dernier état du plateau de jeu.
Cela signifie également qu'en comparant la liste d'actions dans l'état local du frontend avec ce qui est renvoyé en appelant la fonction view(), nous pouvons facilement savoir ce qui s'est passé depuis la dernière action du joueur (c'est au tour du joueur de passer à l'étape suivante). , etc.
Animations SVG
Le sujet de l'animation utilisant Scalable Vector Graphics (SVG) n'a peut-être pas sa place dans cet article, mais une fois, je suis vraiment resté coincé là-dessus.
Je souhaite donc partager les leçons que j’ai apprises.
Le problème que j'ai est que lorsque j'utilise repeatCount pour définir l'animation pour qu'elle ne s'affiche qu'une seule fois, l'animation ne démarre pas.
La plupart des ressources en ligne sur SVG ne fournissent que des exemples qui peuvent être répétés à l'infini ou en utilisant un paramètre repeatCount.
Ils supposent implicitement que si l'animation n'est affichée qu'une seule fois, elle démarre après le chargement de la page (ou un certain délai est défini).
Cependant, avec la plupart des frameworks d'application d'une page comme React ou Mithril, la page n'est généralement pas rechargée, mais simplement restituée.
Ainsi, lorsque je veux montrer un fragment de jeu passant du blanc au noir ou du noir au blanc, cela doit se produire lors du nouveau rendu de la page, et non lors du rechargement de la page.
Cette différence clé m'a manqué et je ne l'ai découverte qu'après l'avoir essayée plusieurs fois.
C'est ainsi que j'utilise Mithril pour restituer un élément animé (en tant qu'enfant d'un SVG) où le rx de l'ellipse passe du rayon initial à 0 et inversement.

L'explication est la suivante :
start est défini sur indéterminé afin que l'animation puisse être contrôlée/démarrée manuellement
Fill est configuré pour geler, ce qui signifie qu'une fois l'animation terminée, son état final restera inchangé.
Les valeurs sont définies sur 4 valeurs où les deux premières sont répétées comme une astuce pour démarrer l'animation après un délai de 0,1 s (1/4 de dur), car le début est défini sur indéfini.
Le point principal est que l'animation doit être démarrée manuellement. Je le déclenche avec un délai de 0 s en utilisant setTimeout, une astuce qui attend que le nouvel élément d'interface utilisateur préparé par Mithril soit rendu dans le DOM du navigateur :

Comme mentionné ci-dessus, tout élément animé dont l'ID ne commence pas par « point » démarrera immédiatement.
processus de développement
J'ai développé le jeu sous Linux et la configuration initiale consistait à installer le SDK DFINITY et à suivre ses instructions pour créer le projet.
Se souvenir de toutes les lignes de commande dfx est fastidieux, j'ai donc créé un Makefile pour vous aider.

Le débogage et les tests sont principalement effectués dans le navigateur, donc beaucoup de console.log() sont nécessaires.
Il existe en fait un moyen d'écrire des tests unitaires dans Motoko, mais je ne l'ai appris qu'après avoir écrit un jeu.
Initialement, j'ai également développé une interface basée sur un terminal utilisant des scripts shell et dfx.
Je pense que cela permet d'accélérer le débogage sans avoir à passer par le navigateur.
Mais bien sûr, les tests unitaires constituent un meilleur moyen de garantir l’exactitude.
Jouez à des jeux !
Afin de réellement exécuter ce jeu sur un ordinateur Internet, il existe désormais un réseau Tungsten ouvert aux développeurs tiers.
Je vous encourage à vous inscrire, à cloner ce projet et à déployer le jeu vous-même pour acquérir une expérience de développeur directe.
Mais les non-développeurs ne peuvent pas accéder à l'application sur Tungsten car elle n'est pas encore publique.
Je l'ai donc également hébergé moi-même en utilisant dfx et nginx comme proxy inverse afin de pouvoir inviter des amis à jouer.
Je n'encouragerais pas les gens à le faire eux-mêmes car le logiciel est encore au stade Alpha.
Ceci est un lien vers le jeu lui-même, à des fins de démonstration uniquement. Mon plan est de le déployer sur un réseau informatique Internet public une fois lancé plus tard cette année.
Si vous avez des questions, n'hésitez pas à visiter le référentiel du projet et à soumettre un problème, les pull request sont également les bienvenues !
Rejoignez notre communauté de développeurs et commencez à créer sur forum.dfinity.org.

Contenu IC qui vous intéresse
Progrès technologique | Informations sur le projet |
Collectez et suivez IC Binance Channel
Restez à jour avec les dernières informations
