Wie man ein Play To Earn-Spiel auf der NEAR Blockchain erstellt

18 min read
To Share and +4 nLEARNs

Ausführliche Beschreibung des aktuellen Prototyps – BerryClub

Dieser Leitfaden soll Dir helfen, die grundlegende Dynamik zu verstehen, mit der Du ein Play-to-Earn-Spiel auf der NEAR Blockchain erstellen kannst.

Absicht dieses Tutorials ist es nicht jemanden reich zu machen, sondern mithilfe eines einfachen HTML5-Frontends zu verdeutlichen, wie man ein einfaches Farming-Spiel erstellen kann.

Wir werden einen Smart Contract und die JS/Rust-Dateien hinter dem bestehenden Farming-Spiel BerryClub analysieren. Wenn Du die gleiche Logik auf Ihre Spielideen anwendest, werden vielleicht noch witzigere Ergebnisse entstehen.

Um der technischen Seite dieses Tutorials zu folgen, empfehlen ich Dir, die Abbildung NEAR Pathway, um eine Transaktion zu erstellen und deinen ersten Smart Contract auf NEAR zu deployen. Es wird für Dich viel einfacher sein, diesem Tutorial zu folgen, wenn Du während des Lesens der Erklärung einen Tab auf der verlinkten Quelldatei offen hältst. Ein Großteil des erwähnten Codes wird mit Zeilennummern referenziert, aber hier nicht wiedergegeben. Das ursprüngliche BerryClub-Repository wurde für dieses Tutorial geforked, um die Gültigkeit des Codes auf der Blockchain aufrechtzuerhalten. Wie Du im nächsten Abschnitt sehen wirst, hat sich das Spiel selbst im Laufe der Zeit weiterentwickelt und wird sich auch in Zukunft weiterentwickeln. An dieser Stelle werden wir uns aber nur mit dem Stand zum Zeitpunkt des Erstellens von diesem Artikel beschäftigen.

Das BerryClub Frontend

BerryClub wurde mit React entwickelt, also werden wir als allererstes in die app.js-Datei im src-Ordner des Github-Repos einsteigen. Das spart uns Zeit bei der Analyse des Contracts, so dass wir uns auf das konzentrieren können, was wir brauchen, um die Logik außerhalb des eigentlichen Spiels zu erschließen (das im Übrigen Spaß macht und von einer großen Community gespielt wird).

Nach dem Importieren von React ist die erste Library, die benötigt wird, bn.js, ein einfaches Dienstprogramm zum Verwalten von dezimalen Zahlen oder nicht dezimalen Zahlen. Viele Funktionen werden diese Library verwenden, um mit dem UI zu interagieren, wie zum Beispiel die folgende Konstante:

const PixelPrice = new BN(“10000000000000000000000”);

Das Spiel BerryClub basiert auf der Immobilienökonomie. Es gibt ein Brett, das von einem Teil des Contracts namens board.rs verwaltet wird. Es ist in Pixel unterteilt. Jedes Pixel hat einen Preis, der bezahlt werden muss, um darauf zu zeichnen. Die Finanz Mechanik der „Draw“-Aktion ist der Kern der Farming- und Selbsterhaltungsfähigkeiten des Spiels und wird im Detail analysiert, wenn wir zum Smart Contract kommen.

Wie Du hier sehen kannst, wird der Preis für ein Pixel gleich zu Beginn unserer Anwendung als Konstante deklariert und kann mit den Frontend-Tools und der bn.j-Bibliothek geändert werden. Der zweite Import ist das NEAR SDK, welches uns die Interaktion mit der Near-Blockchain ermöglicht, wie in der Abbildung NEAR Pathway erläutert. Die erste Verwendung der Near-Api besteht darin, die verwendeten Contract-Variablen zu benennen und sicherzustellen, dass das Mainnet verwendet wird, wenn der Code über die BerryClub-Url ausgeführt wird:

const IsMainnet = window.location.hostname === "berryclub.io";
const TestNearConfig = {
  networkId: "testnet",
  nodeUrl: "https://rpc.testnet.near.org",
  contractName: "berryclub.testnet",
  walletUrl: "https://wallet.testnet.near.org",
};
const MainNearConfig = {
  networkId: "mainnet",
  nodeUrl: "https://rpc.mainnet.near.org",
  contractName: "berryclub.ek.near",
  walletUrl: "https://wallet.near.org",
};
const NearConfig = IsMainnet ? MainNearConfig : TestNearConfig;

Zunächst importieren wir die React-UI-Utilities, um unsere Oberfläche zu erstellen und den Benutzern das Zeichnen zu ermöglichen: react-color, react-switch und react-compound-timer. Die erste davon ist der Timer. Dieser wird genutzt um eine Zeitüberschreitung für die Auffrischung des Boards in Zeile 62 zu setzen.

Die “ Aktualisierung “ des Boards wird vom Frontend durchgeführt, um den aktualisierten Board-Status über einen RPC-Aufruf an den Smart Contract anzuzeigen.

const BatchTimeout = 500;
const RefreshBoardTimeout = 1000;
const MaxWorkTime = 10 * 60 * 1000;
const OneDayMs = 24 * 60 * 60 * 1000;

Was wir hier sehen, sind zwei Konstanten mehr als die, die für die Auffrischung benötigt werden. Die letzten beiden werden tatsächlich für das Farmen der Pixel nach dem Zeichnen verwendet und setzen ein Zeitfenster von einem Tag, um die Belohnungen zu berechnen. Andere Konstanten werden ebenfalls angegeben, um das Board entsprechend mit dem Smart Contract zu verwalten. Hier kreuzen wir zum ersten Mal das Konzept der Linien, das sehr wichtig sein wird, um das Board-Management zu verstehen und welches das am meisten wiederverwendbare Element der gesamten Schnittstelle ist:

const BoardHeight = 50;
const BoardWidth = 50;
const NumLinesPerFetch = 50;
const ExpectedLineLength = 4 + 8 * BoardWidth;

Wie Du sehen, wird das Board in 50×50 Pixel unterteilt und das Interface wird angewiesen, nur die Zeilen nach der Anweisung „RefreshBoardTimeout“ abzurufen. Die Länge jeder Zeile wird als BoardWidth multipliziert mit 12, also der Größe einer einzelnen Zelle betrachtet.

const CellWidth = 12;
const CellHeight = 12;
const MaxNumColors = 31;
const BatchOfPixels = 100;

Die Pixel werden im Stapeln und nicht unabhängig voneinander betrachtet. Sowohl beim Aufruf der Zeichen Aktion als auch beim Aktualisieren des Interface.

Zu guter Letzt enthalten unsere Importe eine benutzerdefinierte UI-Komponente namens Weapons,js: Diese Komponente wurde später in der Entwicklungsgeschichte der BerryClub-Community eingeführt, damit jeder Benutzer ein komplettes Bild auf das Board hochladen, zeichnen und auf dem NFT-Marktplatz für Berry Cards verkaufen kann.

DeFi-Mechanik

Die Zeilen zwischen 27 und 51 sind ein nützlicher Hinweis darauf, wie diese dezentrale App ihre Farming-Fähigkeiten über einige grundlegende DeFi-Mechanismen aufbaut. Diese werden in den letzten Teilen dieses Tutorials analysiert. Bis jetzt haben wir nur kurz erwähnt, dass, um ein Pixel zu zeichnen/kaufen BerryClub Dich durch eine Reihe von DeFi Operationen auf ref.finance leitet.  Dabei werden spezifische Token, die Avocados, für den Kauf von Pixeln und Bananen verwendet. Diese Token werden von den Pixeln verdient, die Du gekauft hast.

Es wurde ein einfacher Uniswap-Klon erstellt, um Bananen/Avocados gegen NEAR zu tauschen, der auf demselben Smart Contract basiert, der für die anderen Token dieses Spiels/Prototyps erstellt wurde. Es wurde auch einen Farming Token für das Spiel erstellt, genannt Gurke. Er ermöglicht es den Spielern, einen Teil der Token zu verdienen, den die ganze Spielgemeinschaft zahlt, um auf dem Brett zu zeichnen.

Wie Nutzer Geld verdienen

Dies ist der allererste Schritt, den wir im Rust-Code des Smart Contracts machen.  Ich erinnere daran, dass diese DeFi-Mechanik nicht der einzige Weg ist, wie BerryClub einen Spieler Token verdienen lässt. Der Account hat eine spezielle Datei im BerryClub Smart Contract, die wir uns nicht jetzt ansehen müssen. Was wir aber wissen müssen ist, dass einige Informationen, die für die Farming- und Earning-Mechanismen entscheidend sind, werden im Konto-Objekt gesammelt werden:

  • AccountID
  • AccountIndex für die Liste der Konten, welche die Pixeltafel zum letzten Mal berührt haben
  • Balance(Vector für den Besitz mehrerer Token)
  • Anzahl der Pixel
  • Claimed Timestamp (Nanosekunden, wann der betreffende Account das letzte Mal Belohnungen beansprucht hat)
  • Farming-Präferenzen (entweder Bananen oder Gurken)

Die letzten beiden Informationen dienen dazu, die Belohnungen zu einem bestimmten Zeitpunkt zu berechnen. Wenn beispielsweise 5 Pixel für einen Tag kontrolliert werden, ergibt das 5 Bananen. Wenn man einen Pixel der Kontrolle eines Anderen entnimmt, verdient dieser weniger, weil die Anzahl der Pixel, die er besitzt, abnimmt. Dabei wird der Zeitstempel im Bezug auf die besessenen Pixel erneuert. Wie wir gleich sehen werden, werden die Belohnungen auf der Grundlage dieser beiden Variablen berechnet. Die Operation, die auf dem Account des vorherigen Besitzers angewandt wird, wenn ein Pixel gezeichnet wird, heißt das „touch“ und ist in der Datei account.rs rust zu finden. Der Besitz eines einzelnen Pixels ist die Grundlage für das Verdienen auf BerryClub. Auf diese Weise ist diese Mechanik so ziemlich die gleiche, die ein NFT Staking Interface verwenden könnte, das NFT Besitz belohnt.

pub fn touch(&mut self) -> (Berry, Balance) {
        let block_timestamp = env::block_timestamp();
        let time_diff = block_timestamp - self.claim_timestamp;
        let farm_bonus = if self.farming_preference == Berry::Avocado {
            1
        } else {
            0
        };
        let farmed = Balance::from(self.num_pixels + farm_bonus)
            * Balance::from(time_diff)
            * REWARD_PER_PIXEL_PER_NANOSEC;
        self.claim_timestamp = block_timestamp;
        self.balances[self.farming_preference as usize] += farmed;
        (self.farming_preference, farmed)
    }

Um alle Zweifel auszuräumen, ist der anfängliche Besitzer des Boards 0, der Contract selbst. Wenn es nicht möglich ist, einen früheren Besitzer zu finden, wird der Contract als der frühere Besitzer verwendet. Um das Spiel zu starten, wurden einige Token auf dem Contract Account gespeichert. Der „Tresor“ des Spiels ist immer mit einigen Token gefüllt, die die Benutzer gewinnen können. Kommen wir nun zu unserem Interface zurück.

Zahlen zu Farben und zurück

Die Zeilen zwischen 67 und 82 in app.js werden zur Dekodierung von Zahlen in Farben und zurück verwendet. Damit die UI-Elemente mit dem Board interagieren können werden zwei konstante Variablen definiert, intToColor und rgbaToInt. Hierbei können wir feststellen, dass zur Umwandlung einer Integer Zahl in einem “colour string” die drei Farben Rot, Grün und Blau verwendet werden.

const intToColor = (c) => `#${c.toString(16).padStart(6, "0")}`;
const intToColorWithAlpha = (c, a) =>
  `#${c.toString(16).padStart(6, "0")}${Math.round(255 * a)
    .toString(16)
    .padStart(2, "0")}`;

Um die „colour string“ in eine ganze Zahl umzuwandeln, wenden wir einfach eine math.round()-Funktion an und verwenden die resultierende ganze Zahl.

const rgbaToInt = (cr, cg, cb, ca, bgColor) => {
  const bb = bgColor & 255;
  const bg = (bgColor >> 8) & 255;
  const br = (bgColor >> 16) & 255;  const r = Math.round(cr * ca + br * (1 - ca));
  const g = Math.round(cg * ca + bg * (1 - ca));
  const b = Math.round(cb * ca + bb * (1 - ca));
  return (r << 16) + (g << 8) + b;
};

In den Zeilen darunter geht es um das Hochladen und Rendern von Bildern auf dem Board mit der „weapon“-Komponente, auf die wir nicht näher eingehen werden. imgColorToInt und int2hsv wandeln Zahlen in zwei verschiedene Arten von Farbskalen um, dann wird transparentColor definiert und ein Gamma für das zu rendernde Bild mit generateGamma. In decodeLine wandeln wir den Buffer in ein Array von Pixeln um, die mit den oben genannten Farben auf das Board gedruckt werden sollen, und iterieren sie mit einer for-Schleife.

Erster React-Constructor

In den nächsten Zeilen der app.js definieren wir einen Constructor, der die Zustände festlegt, die wir später in unserer UI zur Interaktion mit der Blockchain verwenden werden.

class App extends React.Component {
  constructor(props) {
    super(props);

Die hier definierten Eigenschaften sind die standardmäßig ausgewählte Farbe und die Farbpalette:

const colors = [
      "#000000",
      "#666666",
      "#aaaaaa",
      "#FFFFFF",
      "#F44E3B",
      "#D33115",
      "#9F0500",
      "#FE9200",
      "#E27300",
      "#C45100",
      "#FCDC00",
      "#FCC400",
      "#FB9E00",
      "#DBDF00",
      "#B0BC00",
      "#808900",
      "#A4DD00",
      "#68BC00",
      "#194D33",
      "#68CCCA",
      "#16A5A5",
      "#0C797D",
      "#73D8FF",
      "#009CE0",
      "#0062B1",
      "#AEA1FF",
      "#7B64FF",
      "#653294",
      "#FDA1FF",
      "#FA28FF",
      "#AB149E",
    ].map((c) => c.toLowerCase());
    // const currentColor = parseInt(colors[Math.floor(Math.random() * colors.length)].substring(1), 16);
    const currentColor = parseInt(colors[0].substring(1), 16);
    const defaultAlpha = 0.25;

Und für den Timer, der das Board aktualisiert:

const timeMs = new Date().getTime();
    const freeDrawingStartMsEstimated =
      timeMs -
      ((timeMs - new Date("2021-05-09")) % (7 * OneDayMs)) +
      OneDayMs * 6;

Dann wird der Zustand des Benutzerkontos definiert, vor allem, ob der Benutzer sich angemeldet hat oder ob es irgendwelche ausstehenden Transaktionen gibt (definiert als pendingPixels). Der boardLoaded-Zustand lädt das Canvas-Board zum Zeichnen der selectedCell Alpha- und PickerColor-Zustände. Diese definieren die Zustände der interaktiven Komponenten, um dem Board Farben hinzuzufügen. Zusammen mit pickingColor zum Auswählen der Farbe vom Board und gammaColors wird der Bilddruck auf dem Board gerendert, sowie den weaponsOn- und weaponsCodePosition-Zuständen.

Die hier folgenden Variablen definieren, welche dem Account des Spielers zugeordnet sind. Diese Informationen sind nützlich um die Spiel Token Ausschüttung zu berechnen:

owners: [],
      accounts: {},
      highlightedAccountIndex: -1,
      selectedOwnerIndex: false,
      farmingBanana: false,

Bei den letzten drei Zuständen wird der Timer für die spätere Verwendung eingerichtet:

freeDrawingStart: new Date(freeDrawingStartMsEstimated),
      freeDrawingEnd: new Date(freeDrawingStartMsEstimated + OneDayMs),
      watchMode: false,

Die folgende Liste (Zeilen 203-215) definiert Objekte und Aktionen, die mit den Zuständen interagieren und zum ersten Mal ein DOM-Element referenzieren, das “Canvas board”.

this._buttonDown = false;
    this._oldCounts = {};
    this._numFailedTxs = 0;
    this._balanceRefreshTimer = null;
    this.canvasRef = React.createRef();
    this._context = false;
    this._lines = false;
    this._queue = [];
    this._pendingPixels = [];
    this._refreshBoardTimer = null;
    this._sendQueueTimer = null;
    this._stopRefreshTime = new Date().getTime() + MaxWorkTime;
    this._accounts = {};

Schließlich definieren wir einige der Zustände, nachdem die Anmeldung erfolgt ist:

this._initNear().then(() => {
      this.setState(
        {
          connected: true,
          signedIn: !!this._accountId,
          accountId: this._accountId,
          ircAccountId: this._accountId.replace(".", "_"),
          freeDrawingStart: this._freeDrawingStart,
          freeDrawingEnd: this._freeDrawingEnd,
        },
        () => {
          if (window.location.hash.indexOf("watch") >= 0) {
            setTimeout(() => this.enableWatchMode(), 500);
          }
        }
      );
    });

Grundlegende Interaktionen

Nun beginnen wir mit der Beschreibung von Interaktionen auf dem Board/Canvas und verbinden diese mit den zuvor definierten Zuständen. Für diese Interaktionen verwenden wir Funktionen. Die erste Funktion verwendet unseren vorherigen Verweis auf das Canvas-Element, um es zu erstellen und ihm Details über die Art der Mausbewegung mitzuteilen, die wir unseren Benutzern erlauben. Beim ersten Klick aktivieren wir den Watchmode, um den Timer zu starten:

const click = async () => {
      if (this.state.watchMode) {
        return;
      }

Und den „image rendering mode“, wenn der Benutzer ein Bild auf dem Board rendern möchte:

if (this.state.rendering) {
        await this.drawImg(this.state.selectedCell);
      } else if (this.state.pickingColor) {
        this.pickColor(this.state.selectedCell);
      } else {
        this.saveColor();
        await this.drawPixel(this.state.selectedCell);
      }

Der nächste wichtige Teil ist, dass wir definieren, wie die Schnittstelle die Maus- und Touch-Bewegung über das Board liest:

if ("touches" in e) {
        if (e.touches.length > 1) {
          return true;
        } else {
          const rect = e.target.getBoundingClientRect();
          x = e.targetTouches[0].clientX - rect.left;
          y = e.targetTouches[0].clientY - rect.top;
        }
      } else {
        x = e.offsetX;
        y = e.offsetY;
      }

Der verwendete Code berücksichtigt die Bedürfnisse der mobilen Benutzer, indem er eine Ad-hoc-Funktion zur Berechnung der Position und einen Listener für Berührungsereignisse zum Canvas/Board hinzufügt: canvas.addEventListener(„touchmove“, mouseMove); Diese Interaktionen werden dann verwendet, um den selectedCell-Status zu setzen und sowohl den Beginn und das Ende der Maus-/Berührungsaktion auf dem Canvas als auch die Bewegung auf jeder Zelle zu verfolgen:

const mouseDown = async (e) => {
      this._buttonDown = true;
      if (this.state.selectedCell !== null) {
        await click();
      }
    };    canvas.addEventListener("mousedown", mouseDown);
    canvas.addEventListener("touchstart", mouseDown);    const unselectCell = () => {
      this.setState(
        {
          selectedCell: null,
        },
        () => this.renderCanvas()
      );
    };    const mouseUp = async (e) => {
      this._buttonDown = false;
      if ("touches" in e) {
        unselectCell();
      }
    };    canvas.addEventListener("mouseup", mouseUp);
    canvas.addEventListener("touchend", mouseUp);    canvas.addEventListener("mouseleave", unselectCell);    canvas.addEventListener("mouseenter", (e) => {
      if (this._buttonDown) {
        if (!("touches" in e) && !(e.buttons & 1)) {
          this._buttonDown = false;
        }
      }
    });

Die Interaktion hier funktioniert über die zuvor definierten Zustände. Beispielsweise der „color picker“ ermöglicht uns, Farben vom Board zu wählen und dann zu zeichnen.Die Taste, welche vom color picker verwendet wird, ist die Alt-Taste. Wir können Bilder auf dem Bord nur dann rendern, wenn diese Taste nicht gedrückt ist, da wir andererseits die generategamma Funktion triggern würden. Auf diese Art und Weise ist die pickcolor() Funktion, wenn sie von einem Pixel referenziert wird, nützlich um einen einzelnen Pixel oder das gesamte Board zu färben.

pickColor(cell) {
    if (!this.state.signedIn || !this._lines || !this._lines[cell.y]) {
      return;
    }
    const color = this._lines[cell.y][cell.x].color;    this.setState(
      {
        currentColor: color,
        alpha: 1,
        pickerColor: intToColorWithAlpha(color, 1),
        gammaColors: generateGamma(int2hsv(color)[0]),
        pickingColor: false,
      },
      () => {
        this.renderCanvas();
      }
    );
  }

An dieser Stelle kommen wir zum Kern unserer DApp, dem Smart Contract. Wir wissen, wie wir die Pixel im Interface zeichnen, aber wir müssen die Transaktionen mit der Blockchain verbinden, damit unser Interface ein echtes Spiel zum Verdienen von Token ist. Achte also bitte genau auf das, was ich jetzt sage, denn auch wenn dein Spiel in Bezug auf das UI völlig anders aussieht als dieses, kann die Earn-Mechanik sehr wohl für jede andere Art von Spiel geeignet sein und wird hier auf die einfachste Weise erklärt, die ich kann.

Der BerryClub Smart Contract

Linien

Wir sind den Linien zum ersten Mal zu Beginn dieses Artikels begegnet, als wir uns mit den Definitionen des UI für die Zustände beschäftigt haben. Linien sind ein wichtiges Konzept der BerryClub-Benutzeroberfläche. Sie sind die Zeilen, durch die das Board/Canvas unterteilt ist. Jeder Pixel in dem Board ist ein Teil der Metadaten. Sie sind ein Teil des UI, welches mit dem Smart Contract interagiert und sie sind das am meisten wiederverwendbare Objekt des Spiels (zum Beispiel, um Level in einem zusammenhängenden Spiel zu erstellen). Also werden wir ein wenig Zeit damit verbringen, zu analysieren, wie sie verwendet werden, um Daten vom Board zu speichern und auszuwerten, während die Benutzer das Spiel spielen.

Zunächst einmal finden wir in der Datei board.rs eine Definition von PixelLine direkt nach der Definition der Pixel:

pub struct PixelLine(pub Vec<Pixel>);impl Default for PixelLine {
    fn default() -> Self {
        Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

Ein Vektor (Array) von String-Daten, unterteilt durch die Breite des Boards.

Und dann definieren wir das PixelBoard als einen Vektor der PixelLines auf diese Weise:

pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

Jede Linie wird also im Board als einzelner Datensatz mit einem Metadatenfeld namens line_versions gespeichert, das bei jeder Änderung einer Linie erweitert wird. Jedes Mal, wenn unser Interface das Board abruft, erhält man also 50 Zeilen, aber auch Metadaten für jede Linie wie zum Beispiel wie oft diese Linie aktualisiert wurde. Wenn man diese Metadaten abruft, weiß das Interface, wie oft die Linie geändert wurde. Wenn die Linie seit dem letzten Abruf geändert wurde, dann holt man die Daten für jedes Pixel, wenn nicht, dann eben nicht.

impl Place {
    pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> {
        lines
            .into_iter()
            .map(|i| {
                let line = self.board.get_line(i);
                line.try_to_vec().unwrap().into()
            })
            .collect()
    }    pub fn get_line_versions(&self) -> Vec<u32> {
        self.board.line_versions.clone()
    }
}

Dies ist ein effizienter Weg, um Daten zu speichern und über das Interface abzurufen. Merke dir dies für dein nächstes play-to-earn NEAR Spiel.

Transaktionen

Gehen wir kurz zurück zu unserem UI in app.js, um sicherzustellen, dass wir verstehen, wie die Transaktionen vom Frontend aus verwaltet werden. Zuerst brauchen wir eine Funktion, um den Account zu überprüfen, wenn etwas schief läuft:

async refreshAllowance() {
    alert(
      "You're out of access key allowance. Need sign in again to refresh it"
    );
    await this.logOut();
    await this.requestSignIn();
  }

Erinnerst Du dich dann an die Arrays _queue und _pendingPixels, die wir in unserem Constructor definiert haben? Es ist definitiv an der Zeit sie zu verwenden, da die Transaktionen in Abhängigkeit davon verwaltet werden, welche Pixel auf dem Board gezeichnet wurden:

async _sendQueue() {
    const pixels = this._queue.slice(0, BatchOfPixels);
    this._queue = this._queue.slice(BatchOfPixels);
    this._pendingPixels = pixels;    try {
      await this._contract.draw(
        {
          pixels,
        },
        new BN("75000000000000")
      );
      this._numFailedTxs = 0;
    } catch (error) {
      const msg = error.toString();
      if (msg.indexOf("does not have enough balance") !== -1) {
        await this.refreshAllowance();
        return;
 }
      console.log("Failed to send a transaction", error);
      this._numFailedTxs += 1;
      if (this._numFailedTxs < 3) {
        this._queue = this._queue.concat(this._pendingPixels);
        this._pendingPixels = [];
      } else {
        this._pendingPixels = [];
        this._queue = [];
      }
    }
    try {
      await Promise.all([this.refreshBoard(true), this.refreshAccountStats()]);
    } catch (e) {
      // ignore
    }
    this._pendingPixels.forEach((p) => {
      if (this._pending[p.y][p.x] === p.color) {
        this._pending[p.y][p.x] = -1;
      }
    });
    this._pendingPixels = [];
  }

Warten Sie, ich war nicht bereit für diese Menge an Code… Doch, das bist Du! Aber schauen wir es uns genau an, wir erstellen ein Pixel-Objekt (vector), wir ändern unser _queue-Objekt, damit es in die Pixel passt und wir weisen deren Wert dem _pendingPixel-Objekt in einer async Funktion zu.

Und was dann? Wir zeichnen einfach auf ein Contract Objekt, das aus dem NEAR SDK aufgerufen wird. Somit ist die Aktion für das Zeichnen (ein Teil der Aktionen, die wir für den Benutzer definiert haben) in der Rust Datei lib.rs definiert.

pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) {
        if pixels.is_empty() {
            return;
        }
        let mut account = self.get_mut_account(env::predecessor_account_id());
        let new_pixels = pixels.len() as u32;
        if ms_time() < self.get_free_drawing_timestamp() {
            let cost = account.charge(Berry::Avocado, new_pixels);
            self.burned_balances[Berry::Avocado as usize] += cost;
        }
    let mut old_owners = self.board.set_pixels(account.account_index, &pixels);
        let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0);
        account.num_pixels += new_pixels - replaced_pixels;
        self.save_account(account);        for (account_index, num_pixels) in old_owners {
            let mut account = self.get_internal_account_by_index(account_index).unwrap();
            self.touch(&mut account);
            account.num_pixels -= num_pixels;
            self.save_account(account);
        }        self.maybe_send_reward();
    }

Für den Smart Contract sind Pixel eine Farbe und eine Account-ID (der Mystical Owner), und es ist ein immobilien basiertes Spiel: Wir haben also einen alten Besitzer, der das Pixel zuvor gezeichnet hat, und einen neuen Besitzer, der es jetzt zeichnen möchte. Mit der Draw-Aktion holen wir uns den alten Besitzer und ersetzen ihn durch den neuen Besitzer, indem wir den Farbwert aller Pixel innerhalb des PixelRequest-Vektors ändern, dann senden wir Belohnungen an den alten Besitzer und belasten den neuen. Die Zeitstempel für die Belohnungen werden zurückgesetzt und die Zählung beginnt wieder bei Null mit einem Pixel weniger für den alten Besitzer und einem mehr für den neuen. Die Aktion setPixelRequest ist in der Datei board.rs unseres Contract definiert, aber kommen wir zurück zu unserer libs.rs.

Wie sieht die Funktion maybe_send_rewards() aus? Hier ist sie in ihrer ganzen Pracht:

impl Place {
    fn maybe_send_reward(&mut self) {
        let current_time = env::block_timestamp();
        let next_reward_timestamp: u64 = self.get_next_reward_timestamp().into();
        if next_reward_timestamp > current_time {
            return;
        }
        self.last_reward_timestamp = current_time;
        let reward: Balance = self.get_expected_reward().into();
        env::log(format!("Distributed reward of {}", reward).as_bytes());
        Promise::new(format!(
            "{}.{}",
            FARM_CONTRACT_ID_PREFIX,
            env::current_account_id()
        ))
        .function_call(
            b"take_my_near".to_vec(),
            b"{}".to_vec(),
            reward,
            GAS_BASE_COMPUTE,
        );
    }
}

Bitte werde nicht faul, wenn es nicht anders geht, kann später das Video des Spieleautors angesehen werden. Die Erklärungen, die ich verwenden werde, stammen ebenfalls aus diesem Video.

Die Funktion verifiziert die Zeit auf der Blockchain (wir verwenden hier nicht den Timer auf dem Interface, weil wir sicher gehen wollen, dass nicht geschummelt wird) und nutzt die Farming-Capabilities des Contracts für einen Global Timestamp mit der Funktion get_next_reward_timestamp() und last_reward_timestamp() 

. Anschließend wird get_expected_reward() aufgerufen, um dem Account die verdienten Rewards zuzuteilen.

pub fn get_expected_reward(&self) -> U128 {
        let account_balance = env::account_balance();
        let storage_usage = env::storage_usage();
        let locked_for_storage = Balance::from(storage_usage) * STORAGE_PRICE_PER_BYTE + SAFETY_BAR;
        if account_balance <= locked_for_storage {
            return 0.into();
        }
        let liquid_balance = account_balance - locked_for_storage;
        let reward = liquid_balance / PORTION_OF_REWARDS;
        reward.into()
    }

Wir nehmen also den aktuellen Kontostand des BerryClub-Accounts (erinnerst Du dich, dass wir ein Guthaben Feld auf dem Account haben?), die aktuelle Speichernutzung und -kosten und eine Sicherheitsreserve von 50 Avocados. Wenn das Guthaben sicher für die Nutzung außerhalb der Speicherkosten ist, teilen wir das Guthaben in 24 (Stunden) * 60 (Minuten) Teile der Belohnung. Das bedeutet, dass Du im Grunde genau das gleiche Guthaben erhältst, wie wenn Du es jede Minute aufrufen würdest. Dies ist  am Anfang der lib.rs Datei definiert:

const PORTION_OF_REWARDS: Balance = 24 * 60;
const SAFETY_BAR: Balance = 50_000000_000000_000000_000000;

Ich wette, ihr Leute denkt, der Belohnung Prozess ist vorbei. Falsch gedacht.

Wir müssen tatsächlich zurück in unsere Funktion maybe_send_reward() gehen, um zu sehen, dass sie den neuen BerryClub-Farm-Contract aufruft, wo wiederum die Staking-Rewards verteilt werden, die… Gurken. Die Gurken sind ja der Staking-Token auf BerryClub 🙂

const FARM_CONTRACT_ID_PREFIX: &str = "farm";

Das ist aber nicht die einzige Quelle um Einnahmen mit dieser Funktion zu verteilen, denn sie gleicht auch die Netzwerk Gebühren in Gas aus, die die Leute bezahlen, um Avocados zu kaufen und Bananen zu tauschen, um die ganze Spiel Gemeinschaft zu belohnen!

Wie ist das möglich? Das Wichtigste zuerst: GAS_BASE_COMPUTE wird in der Datei token.rs definiert, wo die Gasmenge für den Smart Contract festgelegt wird. Ja, Du hast Recht! Der Gaspreis ist auf der NEAR Blockchain niedrig und kann verwendet werden, um Benutzer zu belohnen, die mit Ihrem Videospiel interagieren!!!

Um besser zu verstehen, wie GAS-Gebühren auf NEAR funktionieren, lies diese detailierte Beschreibung.

Dieses Tutorial wurde von jilt.near und ihrem NFT Gaming Projekt zur Verfügung gestellt. Unterstützte diesen Account durch den Kauf von NFTs!

15
Scroll to Top