Internetcomputer bieten gegenüber herkömmlichen Cloud-Computing-Plattformen mehrere Vorteile, die eine effizientere Anwendungsentwicklung ermöglichen.
Ich bin Ingenieur bei DFINITY, aber auch Softwareentwickler. Deshalb wollte ich diese Prämisse testen und die Erfahrungen beim Bauen auf einem Internetcomputer aus der Sicht eines Webentwicklers bewerten.
Ich habe mich entschieden, eine umkehrbare Version zu bauen, ein Strategie-Brettspiel für zwei Spieler, nicht als Beispielanwendung, sondern als echte Anwendung mit allen Möglichkeiten und Details, die ich mir von einem umkehrbaren Mehrspielerspiel vorgestellt habe.
Bevor ich auf die technischen Details hinter den Kulissen eingehe, möchte ich mich auf das übergeordnete Konzept konzentrieren: eine virtuelle Umgebung, in der Internetanwendungen nahtlos miteinander verbunden werden können.
Ich persönlich glaube, dass die Infrastruktur mit der Weiterentwicklung des Cloud Computing zur Ware werden wird. Mit anderen Worten: Es spielt keine Rolle mehr, wer die Infrastruktur bereitstellt.
Wichtig ist: Man schreibt eine Anwendung und sie läuft im Internet.
Programmiermodell
Die Erfahrung bei der Entwicklung von Webanwendungen auf einem Internetcomputer ähnelte in etwa der Erfahrung neuerer Plattformen wie (inzwischen nicht mehr existierenden) Parse oder ähnlichen Plattformen.
Die Grundvoraussetzung einer solchen Plattform besteht darin, die Komplexität des Aufbaus und der Wartung von Backend-Diensten (wie HTTP-Servern, Datenbanken, Benutzeranmeldungen usw.) zu verbergen.
Stattdessen stellen sie eine abstrakte virtuelle Umgebung bereit, in der nur Benutzeranwendungen ausgeführt werden, ohne dass Benutzer wissen oder darauf achten müssen, wo und wie ihre Anwendungen ausgeführt werden.
Aus dieser Perspektive sind Internetcomputer sowohl vertraut als auch anders.
Der Grundbaustein von Internet-Computeranwendungen ist ein Container, bei dem es sich konzeptionell um einen in Echtzeit laufenden Prozess handelt, der:
ist 100 % deterministisch (wenn alle Eingaben und Zustände gleich sind, muss die Ausgabe gleich sein)
Transparente Persistenz (auch orthogonale Persistenz genannt)
Kommunizieren Sie mit Benutzern oder anderen Containern über asynchrone Nachrichten (Remote-Funktionsaufrufe).
Verarbeiten Sie jeweils eine Nachricht (gemäß dem Akteurmodell).
Wenn wir uns Docker-Container als Virtualisierung eines gesamten Betriebssystems (OS) vorstellen, virtualisiert ein Container ein einzelnes Programm und verbirgt dabei fast alle Betriebssystemdetails.
Scheint zu restriktiv, da Ihr Lieblingsbetriebssystem oder Ihre Lieblingsdatenbank nicht ausgeführt wird. wofür ist das?
Ich persönlich denke lieber in Disziplinen als in Einschränkungen, um nur zwei Eigenschaften (unter vielen) hervorzuheben, die das Containermodell von regulären Webdiensten unterscheiden:
Atomarität: Die Statusaktualisierungen jedes Nachrichten-JARs sind atomar (Remote-Funktionsaufrufe), der Aufruf ist erfolgreich und der Status wird aktualisiert, oder es wird ein Fehler ausgelöst und der Status wird nicht berührt (als ob der Aufruf nie stattgefunden hätte).
Bidirektionale Nachrichtenübermittlung: Nachrichten werden höchstens einmal zugestellt, und dem Anrufer der Nachricht wird immer eine erfolgreiche oder fehlgeschlagene Antwort garantiert.
Es ist schwierig, eine solche Garantie zu erhalten, ohne die Funktionalität des Benutzerprogramms einzuschränken.
Hoffentlich werden Sie am Ende dieses Artikels zustimmen, dass das Constrained-Container-Modell tatsächlich viel erreichen kann, indem es die optimale Kombination aus Effizienz, Robustheit und Einfachheit findet.
Client: Serverarchitektur
Multiplayer-Spiele erfordern den Datenaustausch zwischen Spielern und ihre Implementierung folgt normalerweise einer Client-Server-Architektur:
Der Server hostet das eigentliche Spiel und verwaltet die Kommunikation mit den Spielclients
Zwei oder mehr Clients (jeder repräsentiert einen Spieler) rufen den Status vom Server ab, rendern die Benutzeroberfläche des Spiels und akzeptieren auch Spielereingaben zur Weiterleitung an den Server
Um ein Multiplayer-Spiel als Webanwendung zu erstellen, muss der Client in einem Browser ausgeführt werden, das HTTP-Protokoll für die Datenkommunikation verwenden und Javascript (JS) verwenden, um die Benutzeroberfläche des Spiels als Webseite darzustellen.
Für dieses Multiplayer-Umkehrspiel möchte ich die folgende Funktionalität implementieren:
Es können zwei beliebige Spieler gegeneinander spielen
Spieler erhalten Punkte, indem sie Spiele gewinnen, die ebenfalls auf ihre Gesamtpunktzahl angerechnet werden
Anzeigetafel mit den besten Spielern
Und natürlich gibt es den üblichen Spielablauf: Eingaben von jedem Spieler nacheinander entgegennehmen, nur legale Züge erzwingen und das Ende des Spiels erkennen, um die Punkte zu berechnen
Ein Großteil dieser Spiellogik dreht sich um Zustandsmanipulation, und die serverseitige Implementierung trägt dazu bei, dass die Spieler eine konsistente Sicht haben.
Backend-Server
In einem herkömmlichen Backend-Setup müsste ich eine Suite serverseitiger Software auswählen, einschließlich einer Datenbank zur Speicherung von Spieler- und Spieldaten, eines Webservers zur Verarbeitung von HTTP-Anfragen und dann meine eigene Anwendungssoftware schreiben, um die beiden zu verbinden Implementieren Sie einen vollständigen Satz serverseitiger Logik.
In einem „serverlosen“ Setup stellt die Plattform normalerweise bereits Webserver- und Datenbankdienste bereit, und ich muss nur Anwendungssoftware schreiben, die die Plattform aufruft, um diese Dienste zu nutzen.
Trotz des irreführenden Begriffs „serverlos“ übernimmt die Anwendung immer noch die Rolle eines „Servers“, wie es die Client-Server-Architektur vorschreibt.
Unabhängig vom Backend-Setup ist das Herzstück meines Anwendungsdesigns eine Reihe von APIs, die die Kommunikation zwischen dem Spieleserver und seinen Clients steuern.
Die Entwicklung dieser Anwendung auf einem Internetcomputer ist nicht anders, daher habe ich mit dem folgenden übergeordneten Design des Spielablaufs begonnen:

Nachdem sich die Spieler registriert haben und zwei von ihnen den Wunsch äußern, miteinander zu spielen, wird ein neues Spiel durch den Aufruf von start(opponent_name) gestartet.
Anschließend platzieren die Spieler abwechselnd die nächste Aktion, und der andere Spieler muss regelmäßig view() aufrufen, um seine Ansicht auf den neuesten Spielstand zu aktualisieren, dann die nächste Aktion ausführen und so weiter, bis das Spiel vorbei ist.
Als einfache Faustregel gilt, dass Spieler jeweils nur ein Spiel spielen können.
Der Server muss die folgenden Datensätze speichern:
Liste der registrierten Spieler, deren Namen und Punktestände usw.
Liste der laufenden Spiele, jedes Spiel enthält den neuesten Spielplan, wer das Schwarz-Weiß-Spiel spielt, wer als nächstes ziehen darf, und das Endergebnis nach Abschluss des Spiels usw.
Ich habe mich für die Implementierung des Servers in Motoko entschieden, aber theoretisch sollte jede Sprache, die zu Web Assembly (Wasm) kompiliert werden kann, einwandfrei funktionieren, solange sie dieselbe System-API für die Kommunikation mit Internetkomponenten verwendet. (Zum jetzigen Zeitpunkt steht das Rust SDK kurz vor der tatsächlichen Veröffentlichung.)
Als neue Sprache hat Motoko einige große Vorteile (z. B. ist die Basisbibliothek etwas mangelhaft und noch nicht stabil), verfügt jedoch bereits über Paketmanager und LSP-Unterstützung (Language Server Protocol) in VSCode, was die Entwicklung erleichtert Der Prozess wurde recht angenehm (das liegt daran, dass ich ein Vim-Benutzer bin).
In diesem Artikel werde ich nicht auf die Motoko-Sprache selbst eingehen.
Stattdessen werde ich einige der bemerkenswerten Funktionen von Motoko und Internet Computers besprechen, die die Containerentwicklung spannend machen.
stabile Variable
Orthogonale Persistenz (OP) ist keine neue Idee.
Neue Generationen von Computerhardware wie NVRam haben die Barriere zur dauerhaften Speicherung des gesamten Programmspeichers weitgehend beseitigt, und der Zugriff auf externe Speicher wie Dateisysteme ist für Programme optional geworden.
Eine in der OP-Literatur häufig erwähnte Herausforderung betrifft jedoch Upgrades, d. h. was passiert, wenn bei einem Update die Datenstrukturen oder das Speicherlayout des Programms geändert werden müssen?
Motoko beantwortete diese Frage mit stabilen Variablen. Sie können Upgrades überstehen, was meiner Meinung nach ideal zum Speichern von Spielerdaten ist, da ich nicht möchte, dass Spieler ihre Konten verlieren, wenn sie die Container-Software aktualisieren.
Bei der regulären serverseitigen Entwicklung muss ich Spielerkonten in einer Datei oder Datenbank speichern, was ein grundlegender Systemdienst für „serverlose“ Plattformen ist.
Nur bestimmte Arten von Variablen sind stabil, ansonsten sind sie wie jede andere Variable, die Daten auf dem Heap speichert und als solche verwendet werden kann.
Allerdings gibt es derzeit eine Einschränkung bei der Verwendung von HashMaps als stabile Variablen, sodass ich auf Arrays zurückgreifen muss. Hier ist ein Beispiel:

Ich hoffe, dass eine zukünftige Version des DFINITY SDK diese Einschränkung aufhebt, sodass ich einfach einen stabilen Var-Player ohne Konvertierungen verwenden kann.
Benutzerauthentifizierung
Jeder Container und jeder Client (z. B. DFX-Befehlszeile oder Browser) erhalten eine Prinzipal-ID, die sie eindeutig identifiziert (für Clients werden solche IDs automatisch aus öffentlichen/privaten Schlüsselpaaren generiert, und die DFINITY JS-Bibliothek verwaltet sie und befindet sich derzeit im Browser lokaler Speicher).
Motoko ermöglicht es dem Container, den Aufrufer der „gemeinsamen“ Funktion zu identifizieren, was wir für Authentifizierungszwecke verwenden können.
Die Registrierungs- und Ansichtsfunktionen definiere ich beispielsweise wie folgt:

Der Ausdruck msg.caller gibt die Haupt-ID des Aufrufers der Nachricht an. Beachten Sie, dass sich diese vom Aufrufer der Funktion unterscheidet.
In Motoko müssen Nachrichten an Akteure an eine öffentlich zugängliche Funktion gesendet werden, die einen asynchronen Rückgabetyp haben muss.
Der obige Code zeigt zwei öffentliche Funktionen: Register und View, wobei letzteres ein Abfrageaufruf ist, der durch das Schlüsselwort query gekennzeichnet ist.
Wie wir gesehen haben, muss für den Zugriff auf das Nachrichtenaufruferfeld eine spezielle Syntax verwendet werden: shared(msg) oder shared query(msg), wobei msg ein formaler Parameter ist, der sich auf die eingehende Nachricht als Ganzes bezieht.
Derzeit ist das einzige Attribut, das es hat, „Anrufer“.
Die Möglichkeit, auf die eindeutige ID des Anrufers (Absender der Nachricht) zuzugreifen, fühlt sich vertraut an, wie bei einem HTTP-Cookie.
Aber im Gegensatz zu HTTP stellt das Internet Computer Protocol tatsächlich sicher, dass die Subjekt-ID kryptografisch sicher ist und dass Benutzerprogramme, die auf Internetcomputern ausgeführt werden, vollständig auf ihre Authentizität vertrauen können.
Persönlich denke ich, dass es wahrscheinlich zu mächtig und zu starr ist, wenn das Programm seinen Aufrufer kennt (was passiert beispielsweise, wenn eine solche ID geändert werden muss?).
Aber im Moment führt es zu einem sehr einfachen Authentifizierungsschema, das Anwendungsentwickler nutzen können, und ich hoffe, dass in diesem Bereich weitere Entwicklungen zu sehen sind.
Parallelität und Atomizität
Spielclients können jederzeit Nachrichten an den Spielserver senden, daher liegt es in der Verantwortung des Servers, gleichzeitige Anfragen korrekt zu verarbeiten.
In einer regulären Architektur müsste ich eine Logik erstellen, um die Reihenfolge zu bestimmen, in der sich die Spieler der Reihe nach bewegen (normalerweise über eine Nachrichtenübermittlungswarteschlange oder einen Mutex).
Durch das vom Container verwendete Akteurprogrammiermodell wird dieses Problem automatisch gelöst, ohne dass ich dafür Code schreiben muss.
Nachrichten sind lediglich Remote-Funktionsaufrufe und der Container verarbeitet garantiert jeweils nur eine Nachricht. Dies führt zu einer vereinfachten Programmierlogik und ich muss mir überhaupt keine Sorgen darüber machen, dass Funktionen gleichzeitig aufgerufen werden.
Da der Containerstatus erst dann erhalten bleibt, wenn eine Nachricht vollständig verarbeitet wurde (d. h. wenn der öffentliche Funktionsaufruf zurückkehrt), muss ich mir keine Gedanken darüber machen, ob Speicher auf die Festplatte geleert wird, unabhängig davon, ob Ausnahmen zu einer Beschädigung des Festplattenstatus führen oder mit der Zuverlässigkeit zusammenhängen.
Beachten Sie außerdem, dass dauerhafte Zustandsänderungen pro Nachricht atomar sind.
Es steht öffentlichen Funktionen frei, jede andere nicht asynchrone Funktion aufzurufen, und der geänderte Status bleibt erhalten, solange die gesamte Ausführung ohne Fehler abgeschlossen wird (weitere Einzelheiten zu Aktualisierungsaufrufen finden Sie weiter unten).
Eine feinere Granularität kann erreicht werden, indem asynchrone statt synchrone Aufrufe ausgegeben werden, die zu neuen Nachrichten werden, die vom System geplant und nicht sofort ausgeführt werden können.
Wenn ich dieses Spiel mit einer herkömmlichen Architektur erstellen würde, würde ich wahrscheinlich auch ein Akteur-Framework wählen, wie zum Beispiel Javas Akka, Rusts Actix usw.
Motoko bietet Unterstützung für native Schauspieler und reiht sich damit in die Familie der schauspielerbasierten Programmiersprachen wie Erlang und Pony ein.
Anrufe aktualisieren und Anrufe abfragen
Ich denke, diese Funktion könnte das Benutzererlebnis für Internet-Computeranwendungen wirklich verbessern und sie auf Augenhöhe mit dem bringen, was herkömmliche Cloud-Plattformen hosten (und um Größenordnungen schneller im Vergleich zu anderen Blockchains).
Es ist auch ein einfaches Konzept: Jede öffentliche Funktion, die keine Änderung des Programmstatus erfordert, kann als „Query“-Aufruf markiert werden, andernfalls wird sie standardmäßig als „Update“-Aufruf behandelt.
Der Unterschied zwischen Abfragen und Aktualisierungen besteht in Latenz und Parallelität:
Ein Abfrageaufruf dauert möglicherweise nur wenige Millisekunden, während ein Aktualisierungsaufruf etwa zwei Sekunden dauert.
Abfrageaufrufe können gleichzeitig ausgeführt und gut skaliert werden, Aktualisierungsaufrufe werden sequentiell durchgeführt (basierend auf dem Akteurmodell) und sie bieten Atomizitätsgarantien.
Genau wie im obigen Codebeispiel konnte ich die Ansichtsfunktion als Abfrageaufruf markieren, da sie einfach nach dem Spielstatus sucht, den der Spieler spielt, und ihn zurückgibt.
Tatsächlich führen wir die meiste Zeit, in der wir im Internet surfen, Abfrageaufrufe durch: Daten werden vom Server abgerufen, aber nicht geändert.
Andererseits wird die obige Registrierungsfunktion als Update-Aufruf beibehalten, da der neue Spieler nach erfolgreicher Registrierung zur Spielerliste hinzugefügt werden muss.
Aktualisierungsaufrufe dauern aus vielen Gründen länger, z. B. Datenkonsistenz, Atomizität und Zuverlässigkeit.
Dies ist jedoch kein inhärentes Problem bei Internetcomputern.
Viele Aktionen im Internet dauern heutzutage tatsächlich mehr als zwei Sekunden, etwa das Bezahlen mit Kreditkarte, das Aufgeben einer Bestellung oder das Einloggen in ein Bankkonto, um nur einige zu nennen.
Ich denke, zwei Sekunden sind der Wendepunkt für eine gute Benutzererfahrung.
Zurück zum umgekehrten Spiel: Wenn der Spieler den nächsten Zug macht, muss es sich auch um einen Aktualisierungsaufruf handeln:

Wenn der Bildschirm eines Spiels erst zwei Sekunden nach dem Klicken mit der Maus (oder der Berührung des Bildschirms) aktualisiert wird, fühlt es sich an, als würde es nicht reagieren, und niemand wird ein Spiel mit solch schlechtem Timing spielen wollen.
Daher musste ich diesen Teil optimieren, indem ich direkt auf der Clientseite auf Benutzereingaben reagierte, ohne auf die Antwort des Servers warten zu müssen.
Das bedeutet, dass die Front-End-Benutzeroberfläche die Züge des Spielers validieren, berechnen muss, welche Teile umgedreht werden, und sie sofort auf dem Bildschirm anzeigen muss.
Das bedeutet auch, dass alles, was das Frontend dem Spieler anzeigt, bei der Rückkehr mit der Reaktion des Servers auf dieselbe Aktion übereinstimmen muss, sonst besteht die Gefahr, dass es zu Inkonsistenzen kommt.
Aber auch hier glaube ich, dass jede vernünftige Implementierung eines Mehrspieler-Zwei-Wege-Spiels oder eines Schachspiels dies leisten kann, unabhängig davon, ob das Backend 200 ms oder 2 Sekunden für die Reaktion benötigt.
Frontend-Client
Das DFINITY SDK bietet ein Frontend, das Anwendungen direkt in den Browser lädt.
Es unterscheidet sich jedoch von gewöhnlichen HTML-Seiten, die von Webservern bereitgestellt werden.
Die Kommunikation mit dem Backend-Container erfolgt über Remote-Funktionsaufrufe, die im Fall von Browsern HTTP überlagern.
Dies wird von der JS-Benutzerbibliothek transparent gehandhabt, sodass ein JS-Programm den Container einfach als JS-Objekt importiert und seine öffentlichen Funktionen genau wie die regulären asynchronen JS-Funktionen des Objekts aufrufen kann.
Das DFINITY SDK verfügt über eine Reihe von Tutorials zum Einrichten eines JS-Frontends, daher werde ich hier nicht näher darauf eingehen.
Hinter den Kulissen verwendet der dfx-Befehl im SDK Webpack, um Ressourcen wie JS, CSS, Bilder und andere Dateien, die Sie möglicherweise haben, zu bündeln.
Sie können auch Ihre bevorzugten JS-Frameworks (wie React, AngularJS, Vue.js usw.) mit der DFINITY-Benutzerbibliothek kombinieren, um ein JS-Frontend für die Verwendung in Browsern oder mobilen Apps zu entwickeln.
Hauptkomponenten der Benutzeroberfläche
Ich bin relativ neu in der Frontend-Entwicklung und habe nur kurze Erfahrung mit React.
Dieses Mal habe ich mir erlaubt, Mithril zu lernen, weil ich viel Gutes über Mithril gehört hatte, insbesondere über seine Einfachheit.
Der Einfachheit halber habe ich mir auch ein Design mit nur zwei Bildschirmen ausgedacht:
Ein „Spiel“-Bildschirm, der es den Spielern ermöglicht, ihren eigenen Namen und den Namen ihres Gegners einzugeben, bevor sie den „Spiel“-Bildschirm aufrufen. Außerdem werden einige Tipps und Anweisungen, eine Grafik mit Top-Spielern, aktuellen Spielern und mehr angezeigt.
Ein „Spiel“-Bildschirm, der Spielereingaben akzeptiert und mit dem Backend-Container kommuniziert, um ein inverses Spielfeld darzustellen. Außerdem wird am Ende des Spiels der Punktestand des Spielers angezeigt und der Spieler gelangt dann zurück zum Spielbildschirm.
Der folgende Codeausschnitt zeigt das Framework des JS-Spiel-Frontends:

Es gibt ein paar Dinge zu beachten:
Wie bei jeder anderen JS-Bibliothek wird auch hier der Haupt-Backend-Container importiert. Stellen Sie es sich als einen Proxy vor, der Funktionsaufrufe an einen Remote-Server weiterleitet, Antworten empfängt und die erforderliche Authentifizierung, Nachrichtensignatur, Datenserialisierung/Deserialisierung, Fehlerweitergabe usw. transparent abwickelt.
Ein weiterer Container „reversi_assets“ wird ebenfalls importiert. Dies ist eine Möglichkeit, bei der Installation des Backend-Containers die erforderlichen Assets mit Webpack zu bündeln. In diesem Fall habe ich eine Sounddatei, die abgespielt wird, wenn der Spieler ein neues Stück platziert.
Ein Logobild, das direkt hineinpasst. Dies muss in Webpack mit dem URL-Loader konfiguriert werden, einem Tool, das den Inhalt des Bildes tatsächlich als Base64-String einbettet, der für das Bildelement verwendet wird. Funktioniert für kleine Bilder, aber nicht für große Bilder.
Die endgültige Anwendung wird mit Mithril über die beiden Pfade /play und /game eingerichtet. Letzterer übernimmt die Namen des Spielers und des Gegners als zwei Parameter, was ein erneutes Laden des Spielbildschirms in den Browser ermöglicht, ohne das Spiel zu unterbrechen.
Laden Sie Ressourcen aus dem Asset-Container
Da ich neu im asynchronen Laden von DOM-Elementen in JS bin, habe ich mir einige Mühe gegeben.
Wenn DFX das JAR erstellt, erstellt es auch ein Reversi_assets-JAR, das im Grunde einfach alles in src/reversi_assets/assets/ darin verpackt.
Ich verwende dies, um eine Sounddatei abzurufen, aber das korrekte Laden ist nicht so einfach wie das Einfügen der URL zur MP3-Datei in das src-Feld des HTML-Elements.
So lade ich es (wenn Sie ein Front-End-Entwickler sind, wissen Sie das wahrscheinlich bereits):

Wenn die Startfunktion aufgerufen wird (aus dem asynchronen Kontext), versucht sie, die Datei „put.mp3“ aus dem Remote-Container abzurufen.
Nach erfolgreichem Abruf werden die Audiodaten mit dem JS-Tool AudioContext dekodiert und die globale Variable putsound initialisiert.
Wenn putsound korrekt initialisiert ist, wird durch einen Aufruf von playAudio(putsound) der eigentliche Sound abgespielt:

Andere Ressourcen können auf ähnliche Weise geladen werden. Ich verwende keine anderen Bilder als das Logo, das klein ist und dessen Quellcode in Webpack eingebettet werden kann, indem die folgende Konfiguration zu webpack.config.js hinzugefügt wird:

Datenaustauschformat
Motokos Konzept sind „shareable“ Daten, also Daten, die über Container- oder Sprachgrenzen hinweg versendet werden können.
Natürlich würde ich mir einen Heap-Zeiger in C nicht als „gemeinsam nutzbar“ vorstellen, aber für mich ist alles, was JSON zugeordnet werden kann, „gemeinsam nutzbar“.
Zu diesem Zweck hat DFINITY eine IDL (Interface Description Language) namens Candid für Internet-Computeranwendungen entwickelt.
Candid vereinfacht die Art und Weise, wie das Frontend mit dem Backend oder zwischen Containern kommuniziert, erheblich.
Hier ist zum Beispiel ein (unvollständiger) Ausschnitt des von Candid beschriebenen Backend-Reversible-Containers:

Nehmen Sie als Beispiel die Move-Methode:
Dies ist eine der Methoden, die unter der Serviceschnittstelle des Containers exportiert werden.
Als Eingabe werden zwei Ganzzahlen (die eine Koordinate darstellen) verwendet und ein Ergebnis vom Typ MoveResult zurückgegeben.
MoveResult ist eine Variante (auch als Aufzählung bezeichnet), die die Ergebnisse und Fehler darstellt, die auftreten können, wenn sich der Spieler bewegt.
In den verschiedenen Zweigen von MoveResult zeigt GameOver an, dass das Spiel abgeschlossen ist, und verwendet einen ColorCount-Parameter, der die Anzahl der schwarzen und weißen Spielsteine auf dem Spielbrett darstellt.
Der Motoko-Quellcode generiert automatisch eine Candid-Datei für jeden Container und wird automatisch von der JS-Benutzerbibliothek ohne Beteiligung des Entwicklers verwendet:
Auf der Motoko-Seite entspricht jeder Candid-Typ einem Motoko-Typ und jede Methode entspricht einer öffentlichen Funktion.
Auf der JS-Seite entspricht jeder Candid-Typ einem JSON-Objekt und jede Methode entspricht einer Mitgliedsfunktion des importierten Containerobjekts.
Die meisten Candid-Typen verfügen über eine direkte JS-Darstellung, einige erfordern eine gewisse Konvertierung.
Beispielsweise ist nat sowohl in Motoko als auch in Candid eine beliebige Genauigkeit, in JS wird es der Ganzzahl von bignumber.js zugeordnet und muss daher mit n.toNumber() in den nativen JS-Zahlentyp konvertiert werden.
Ein Problem, das ich habe, sind Nullwerte in Candid (und Motokos Option-Typ).
Es wird in JSON als leeres Array[] und nicht als nativer Nullwert dargestellt. Dies dient dazu, den Fall zu unterscheiden, in dem wir verschachtelte Optionen haben, wie zum Beispiel Option >:

Candid ist sehr mächtig, obwohl es oberflächlich betrachtet sehr nach Protocolbuf oder JSON klingt.
Warum ist es also notwendig?
Es gibt viele gute Gründe, die über das hier Dargelegte hinausgehen, und ich empfehle jedem, der sich für dieses Thema interessiert, Candid Spec zu lesen.
Spielstatus mit Backend synchronisieren
Wie bereits erwähnt, habe ich einen Trick verwendet, um sofort auf gültige Benutzereingaben zu reagieren, ohne auf die Antwort des Backend-Spieleservers warten zu müssen.
Das bedeutet, dass das Frontend nur eine Bestätigung vom Spielserver (oder, falls vorhanden, eine Fehlerbehandlung) benötigt, nachdem der Spieler umgezogen ist.
Zusätzlich zum Senden seiner eigenen Züge muss der Client auch etwas über die Züge des anderen Spielers erfahren.
Dies wird durch den regelmäßigen Aufruf der view()-Funktion des serverseitig gehosteten Spielecontainers erreicht.
Dieses Design hat zur Folge, dass ich einen Teil der gleichen Spiellogik im Backend (Motoko) und im Frontend (JS) wiederholen muss, was nicht ideal ist.
Da Motoko zu Wasm kompiliert wird und Wasm im Browser ausgeführt werden kann, wäre es nicht großartig, wenn Frontend und Backend dasselbe Wasm-Modul verwenden könnten, das die Kernlogik des Spiels implementiert? Diese Art der Freigabe teilt nur den Code, nicht den Status.
Es erfordert möglicherweise einige Einstellungen, aber ich denke, es ist durchaus möglich und ich werde es möglicherweise in einem zukünftigen Update versuchen.
Insbesondere beim Rückwärtsspiel kann es in einigen Fällen vorkommen, dass ein Spieler keine Aktion ausführen kann, sodass der andere Spieler zwei oder sogar mehr Aktionen hintereinander ausführen kann.
Um jede vom Spieler ausgeführte Bewegung anzuzeigen, habe ich mich dafür entschieden, den Spielstatus als eine Abfolge von Aktionen zu implementieren und nicht nur den neuesten Status des Spielbretts.
Dies bedeutet auch, dass wir durch den Vergleich der Aktionsliste im lokalen Status des Frontends mit dem, was durch den Aufruf der Funktion view() zurückgegeben wird, leicht erkennen können, was sich seit der letzten Aktion des Spielers (der Spieler ist an der Reihe, den nächsten Schritt zu machen) geändert hat , usw.
SVG-Animation
Das Thema Animation mit Scalable Vector Graphics (SVG) gehört vielleicht nicht in diesen Artikel, aber einmal blieb ich wirklich dabei hängen.
Deshalb möchte ich die Lektionen, die ich gelernt habe, teilen.
Das Problem, das ich habe, ist, dass die Animation nicht startet, wenn ich mit „repeatCount“ festlege, dass die Animation nur einmal angezeigt wird.
Die meisten Online-Ressourcen zu SVG bieten nur Beispiele, die unendlich oft wiederholt werden können oder eine „repeatCount“-Einstellung verwenden.
Sie gehen implizit davon aus, dass die Animation, wenn sie nur einmal angezeigt wird, nach dem Laden der Seite startet (oder eine Verzögerung eingestellt ist).
Bei den meisten One-Page-Anwendungsframeworks wie React oder Mithril wird die Seite jedoch normalerweise nicht neu geladen, sondern lediglich neu gerendert.
Wenn ich also zeigen möchte, dass ein Spielfragment von Weiß zu Schwarz oder von Schwarz zu Weiß wechselt, muss dies beim erneuten Rendern der Seite geschehen, nicht beim Neuladen der Seite.
Ich habe diesen entscheidenden Unterschied übersehen und ihn erst nach vielen Versuchen entdeckt.
So verwende ich Mithril, um ein animiertes Element (als untergeordnetes Element einer SVG-Datei) zu rendern, bei dem sich der RX der Ellipse vom ursprünglichen Radius auf 0 und zurück ändert.

Die Erklärung lautet wie folgt:
begin ist auf unbestimmt gesetzt, sodass die Animation manuell gesteuert/gestartet werden kann
„Füllung“ ist auf „Einfrieren“ eingestellt, was bedeutet, dass der Endzustand nach dem Ende der Animation unverändert bleibt.
Die Werte werden auf 4 Werte festgelegt, wobei die ersten beiden als Trick wiederholt werden, um die Animation nach einer Verzögerung von 0,1 s (1/4 der Dauer) zu starten. Dies liegt daran, dass begin auf unbestimmte Zeit eingestellt ist
Der Hauptpunkt ist, dass die Animation manuell gestartet werden sollte. Ich löse es mit einer Verzögerung von 0 Sekunden mit setTimeout aus, einem Trick, der wartet, bis das neue von Mithril vorbereitete UI-Element im Browser-DOM gerendert wird:

Wie oben erwähnt, wird jedes animierte Element, dessen ID nicht mit „Punkt“ beginnt, sofort gestartet.
Entwicklungsprozess
Ich habe das Spiel unter Linux entwickelt und die Ersteinrichtung bestand aus der Installation des DFINITY SDK und der Befolgung seiner Anweisungen zum Erstellen des Projekts.
Es ist umständlich, sich alle dfx-Befehlszeilen zu merken, daher habe ich als Hilfe ein Makefile erstellt.

Das Debuggen und Testen erfolgt größtenteils im Browser, daher ist viel console.log() erforderlich.
Es gibt tatsächlich eine Möglichkeit, Unit-Tests in Motoko zu schreiben, aber ich habe davon erst erfahren, nachdem ich ein Spiel geschrieben hatte.
Zunächst habe ich auch ein Terminal-basiertes Frontend mit Shell-Skripten und DFX entwickelt.
Ich denke, dass dies dazu beiträgt, das Debuggen zu beschleunigen, ohne den Browser aufrufen zu müssen.
Aber natürlich sind Unit-Tests eine bessere Möglichkeit, die Korrektheit sicherzustellen.
Spiele spielen!
Um dieses Spiel tatsächlich auf einem Internetcomputer auszuführen, gibt es jetzt ein Tungsten-Netzwerk, das Drittentwicklern offen steht.
Ich empfehle Ihnen, sich anzumelden, dieses Projekt zu klonen und das Spiel selbst bereitzustellen, um Entwicklererfahrungen aus erster Hand zu sammeln.
Aber Nicht-Entwickler können auf Tungsten nicht auf die App zugreifen, da sie noch nicht öffentlich ist.
Deshalb habe ich es auch selbst gehostet und dabei dfx und nginx als Reverse-Proxy verwendet, damit ich Freunde zum Spielen einladen konnte.
Ich würde die Leute nicht dazu ermutigen, dies selbst zu tun, da sich die Software noch im Alpha-Stadium befindet.
Dies ist ein Link zum eigentlichen Spiel, der nur zu Demonstrationszwecken dient. Mein Plan ist es, es in einem öffentlichen Internet-Computernetzwerk bereitzustellen, sobald es später in diesem Jahr auf den Markt kommt.
Wenn Sie Fragen haben, besuchen Sie gerne das Projekt-Repository und reichen Sie ein Problem ein. Pull-Requests sind ebenfalls willkommen!
Treten Sie unserer Entwickler-Community bei und beginnen Sie mit der Entwicklung unter forum.dfinity.org.

IC-Inhalte, die Ihnen wichtig sind
Technologiefortschritt |. Projektinformationen |
Sammeln und folgen Sie dem IC Binance Channel
Bleiben Sie mit den neuesten Informationen auf dem Laufenden
