Comment créer un jeu Play To Earn sur NEAR Protocol

19 min read
To Share and +4 nLEARNs

Examen approfondi du prototype actuel — BerryClub

Ce guide est destiné à vous permettre de comprendre la dynamique de base qui vous permettra de créer un jeu pour gagner un jeu sur Near Protocol.

Nous ne voulons pas vous rendre riches, vous ou vos joueurs, mais les mécanismes de Near fonctionnent de manière à ce que les petites transactions soient très utiles sur une simple interface utilisateur HTML5 et créent un jeu agricole simple et amusant.

Nous analyserons un contrat intelligent et les fichiers js/rust derrière un jeu agricole existant, berryclub. Si vous appliquez la même logique à vos idées de jeu, vous obtiendrez peut-être des résultats encore plus amusants !

Pour suivre le côté technique de ce didacticiel, nous vous recommandons fortement de vérifier la figure Near Pathway pour créer une transaction et créer votre premier contrat intelligent sur Near, les concepts présents dans ces guides ne sont pas inclus dans ce didacticiel. Il vous sera beaucoup plus facile de suivre ce tutoriel si vous gardez un onglet ouvert sur le fichier source lié pendant la lecture de l’explication, car une grande partie du code mentionné est référencée avec des numéros de ligne mais n’est pas reproduite ici. Le dépôt original de BerryClub a été fork pour ce tutoriel afin de garder cet effort valide dans la mesure où le code fonctionne toujours sur la blockchain; comme vous le verrez dans la section suivante, le jeu lui-même a évolué au fil du temps et évoluera à nouveau, nous voulions faire un point sur son état maintenant.

L’interface du Berryclub

Berryclub est construit avec React, donc la toute première chose que nous allons aborder dans le fichier app.js situé dans le dossier src du référentiel github, cela nous fera gagner du temps dans l’analyse du contrat nous permettant de nous concentrer sur ce que nous devons extrapoler la logique en dehors du jeu réel (qui est amusant et joué par une grande communauté d’ailleurs).

Après l’import de React la première librairie nécessaire est bn.js un simple utilitaire pour gérer des entiers ou des nombres non décimaux, de nombreuses fonctionnalités peuvent venir sur votre interface avec cette librairie, tout d’abord ici elle est utilisée pour gérer les transactions :

const PixelPrice = new BN(“10000000000000000000000”);

Le jeu berryclub est basé sur l’économie immobilière, il y a un plateau géré par une partie du contrat appelé board.rs, il est subdivisé en pixels, et chaque pixel a un prix qu’il faut payer pour pouvoir puiser dessus. Les mécanismes de l’action « tirer » sont au cœur des capacités d’agriculture et d’auto-entretien du jeu et seront analysés en profondeur lorsque nous arriverons au contrat intelligent.

Comme vous pouvez le voir ici, le prix du pixel unique est déclaré comme une constante au tout début de notre application, et peut être modifié à l’aide des outils frontend et de la bibliothèque bn.j. La deuxième importation est le near sdk qui nous permet d’interagir avec la blockchain Near comme expliqué dans le Near pathway de Figment. La première utilisation de l’API NEAR est de déclarer les variables de contrat utilisées et de s’assurer que le réseau principal est utilisé lorsque le code est exécuté à partir de l’url du berryclub :

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;

Ensuite, nous importons les utilitaires d’interface utilisateur de réaction pour créer notre interface et permettre aux utilisateurs de dessiner, de réagir en couleur, de réagir en commutateur et de réagir en composé-timer. Le premier de ces utilitaires utilisé est le timer, il est utilisé pour définir un délai d’attente pour rafraîchir la carte sur la ligne 62.

Le « rafraîchissement » de la carte est effectué par le frontend pour afficher l’état mis à jour de la carte à l’aide d’un appel RPC au contrat intelligent.

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

Ce que nous voyons ici, ce sont deux const de plus que celles nécessaires au rafraîchissement, les deux derniers sont en fait utilisés pour Farmer les pixels après les avoir dessinés, et définissent un créneau horaire d’une journée pour calculer les récompenses. D’autres const sont également déclarés pour gérer le Conseil en conséquence avec le contrat Smart, et ici, pour la première fois, nous croisons le concept de lignes, qui sera très important pour comprendre la gestion du Conseil et est l’élément le plus réutilisable de toute l’interface :

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

Comme vous pouvez le voir, après avoir subdivisé le tableau 50×50, nous disons à notre interface de récupérer uniquement les lignes suivant l’instruction RefreshBoardTimeout et de considérer la longueur de chaque ligne comme la largeur du tableau multipliée par 12, la taille d’une seule cellule.

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

Les pixels sont considérés par lots, et non indépendamment, à la fois lorsque l’action de dessin est appelée et lorsque l’interface est actualisée.

Enfin, nos importations incluent un composant d’interface utilisateur personnalisé, appelé Weapons,js : ce composant a été développé plus tard dans l’histoire de la communauté berryclub, afin que chaque utilisateur puisse télécharger et dessiner une image entière sur le tableau, et lancez-le sur le marché berrycards NFT.

Mécanique DeFi

Les lignes entre 27 et 51 sont une référence utile sur la façon dont cette Dapp construit ses capacités d’agriculture sur certains mécanismes DeFi de base qui seront analysés dans les dernières parties de ce didacticiel. À présent, nous mentionnons brièvement que pour dessiner/acheter un pixel, berryclub vous fait passer par quelques opérations DeFi sur ref.finance en utilisant ses propres jetons spécifiques, des avocats pour acheter des pixels et des bananes gagnées avec les pixels que vous avez achetés.

Il existe un simple clone uniswap créé pour échanger à proximité des bananes/avocats qui fonctionnaient sur le même contrat intelligent construit pour les autres jetons de ce jeu/prototype. Il existe également un jeton agricole créé pour le jeu, appelé concombre, il permet aux gens de gagner une partie des jetons que toute la communauté qui joue au jeu paie pour l’essence à puiser sur le plateau.

Le compte ou comment les utilisateurs gagnent de l’argent

C’est la toute première étape que nous faisons dans le code de rouille du contrat intelligent, mais j’ai ressenti le besoin de vous rappeler que la mécanique DeFi n’est pas la seule façon dont Berryclub vous permet de gagner des jetons. Le compte a un fichier particulier dans le contrat intelligent berryclub, nous n’avons pas besoin d’y entrer immédiatement, ce que nous devons savoir, c’est que certaines informations sont collectées dans l’objet de compte qui sont cruciales pour la mécanique agricole et de revenus :

  • identifiant de compte
  • accountIndex pour la liste (vecteur) des comptes qui ont touché le tableau de pixels pour la dernière fois
  • solde (vecteur pour plusieurs jetons possédés)
  • nombre de pixels
  • horodatage réclamé (nanosecondes, lorsque le compte donné a réclamé des récompenses pour la dernière fois)
  • préférences agricoles (bananes ou concombres)

Les deux dernières infos sont de calculer les récompenses à un moment donné, par exemple si vous possédez 5 pixels pour un jour vous acquérez 5 bananes. Si vous achetez auprès de quelqu’un d’autre, leurs revenus diminuent car le nombre de pixels qu’ils possèdent diminue, de sorte que le montant des revenus est calculé et l’horodatage renouvelé concernant le nouveau nombre de pixels possédés. Comme nous allons le voir, les récompenses sont calculées en fonction de ces deux variables. L’opération appliquée au compte du propriétaire précédent lorsqu’un pixel est dessiné s’appelle « toucher » et vous pouvez la trouver dans le fichier rouille account.rs. La propriété de l’unité de pixel unique est la base pour gagner sur berryclub, et de cette façon, cette mécanique est à peu près la même qu’une interface de jalonnement NFT pourrait utiliser, récompensant la propriété NFT.

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)
    }

Pour dissiper tout doute, le propriétaire initial de la planche est 0, le contrat lui-même, et s’il n’est pas possible de trouver un propriétaire précédent, le contrat est utilisé comme propriétaire précédent. Enfin, pour démarrer le jeu, des jetons ont été stockés dans le compte du contrat et ils sont toujours augmentés en utilisant le prix du gaz établi pour que les gens achètent des avocats et des bananes, de sorte que le “coffre” du jeu soit toujours rempli de quelques jetons pour le utilisateurs à gagner. Revenons maintenant à notre interface.

Numéros aux couleurs et retour

Les lignes entre 67 et 82 sur app.js sont utilisées pour décoder les nombres en couleurs et inversement, afin que les éléments de l’interface utilisateur interagissent avec le tableau, deux variables constantes sont définies, intToColor et rgbaToInt. Ce que nous pouvons remarquer ici, c’est que pour transformer un nombre entier en une chaîne de couleurs, des méthodes sont utilisées pour diviser les 3 nombres pour le rouge vert et le bleu :

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

Pour inverser la chaîne de couleur en entier, nous appliquons simplement une fonction math.round() et utilisons l’entier résultant.

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;
};

Les lignes ci-dessous concernent le téléchargement et l’impression d’images sur le tableau à l’aide du composant d’arme et nous ne nous en occuperons pas en profondeur : imgColorToInt et int2hsv transforment les nombres en deux types d’échelles de couleurs, puis transparentColor est défini et un gamma pour le image à imprimer avec generateGamma. Dans decodeLine, nous transformons le tampon en un tableau de pixels à imprimer sur le tableau en utilisant les couleurs ci-dessus, en les parcourant avec for.

Premier constructeur React

Dans les lignes suivantes de app.js, nous définissons un constructeur qui définira les états que nous utiliserons plus tard dans notre interface utilisateur pour interagir avec la blockchain.

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

L’utilisation de constructeur et de super nous permettra de l’utiliser. dans le constructeur. Les états définis ici sont la couleur et la palette de couleurs sélectionnées par défaut :

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;

Et pour le timer qui rafraichit le tableau :

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

Ensuite, les états du compte d’utilisateur en cours d’utilisation sont définis, surtout si l’utilisateur s’est connecté, s’il y a des transactions en attente (définies comme en attente de Pixels), l’état boardLoaded chargera le canevas pour dessiner les états alpha et pickerColor sélectionnés. états des composants interactifs pour ajouter des couleurs au tableau, ainsi que pickingColor pour choisir la couleur du tableau et gammaColors est utile pour l’impression d’images sur le tableau avec les états WeaponsOn et WeaponsCodePosition.

Ces autres états sont utiles pour que le compte gagne sur le jeu, basé sur les pixels et basé sur DeFi :

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

Alors que les trois derniers états configurent la minuterie pour une utilisation ultérieure :

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

La liste suivante (lignes 203-215) définit les objets et les actions qui vont interagir avec les états, faisant référence pour la première fois à un élément du DOM, le canvas.

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 = {};

Enfin, nous définissons certains des états une fois la connexion terminée :

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);
          }
        }
      );
    });

Interactions de base

Nous commençons maintenant à décrire les interactions sur le tableau/la toile les reliant aux états précédemment définis. Pour ces interactions, nous utilisons des fonctions. Le premier utilisera notre référence précédente à l’élément canvas pour le créer et l’instruira avec des détails sur le type de mouvement de souris que nous autorisons à nos utilisateurs. Au premier clic, nous activons le mode veille pour que notre minuteur démarre :

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

Et le mode rendu d’image si l’utilisateur veut imprimer une image sur le plateau :

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);
      }

Le suivant est la partie importante, nous définissons comment l’interface lit le mouvement de la souris et du toucher sur le tableau :

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;
      }

Le code utilisé prend soigneusement en considération les utilisateurs mobiles à la fois, en créant une fonction ad hoc pour calculer la position et en ajoutant un écouteur au canevas/au tableau pour les événements tactiles : canvas.addEventListener(“touchmove”, mouseMove); Ensuite, ces interactions sont utilisées pour définir l’état de la cellule sélectionnée et suivre à la fois le début et la fin de l’action de la souris/du toucher sur le canevas ainsi que son mouvement sur chaque cellule :

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;
        }
      }
    });

L’interaction fonctionne ici sur les états précédemment définis, comme par exemple le sélecteur de couleurs nous permet de choisir des couleurs sur le tableau et de les utiliser ensuite pour dessiner. La clé utilisée par le sélecteur de couleurs est la touche alt et nous ne pouvons télécharger et imprimer des images sur le tableau que si le sélecteur de couleurs est désactivé, car nous déclencherons alors la fonction generategamma. De cette façon, la fonction pickcolor(), référencée à la cellule sera utilisable pour définir un seul pixel ou à la place l’ensemble du tableau pour restituer une image :

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

Maintenant, les gars, nous arrivons à l’essentiel, alors soyez prêt à commencer à plonger sur le contrat Smart. Nous savons comment dessiner le pixel dans l’interface, mais nous devons leur attacher les transactions pour que notre interface soit un véritable jeu à gagner. Alors s’il vous plaît faites très attention à ce que je m’apprête à dire, car même si votre jeu est complètement différent de celui-ci en termes d’interface utilisateur, les mécanismes de gain peuvent très bien convenir à tout autre type de jeu et seront expliqués ici de la manière la plus simple façon que je peux.

Smart Contract du Berryclub
Lignes :

Nous avons rencontré des lignes pour la première fois au début de cet article, tout en considérant les définitions des États de l’interface utilisateur. Les lignes sont un concept important de l’interface berryclub, ce sont les lignes par lesquelles le tableau/la toile est subdivisée et chaque pixel est un morceau de métadonnées. Ils font partie de l’interface utilisateur qui interagit avec le contrat intelligent et ils sont l’objet le plus réutilisable du jeu (par exemple pour créer des niveaux dans un jeu plus articulé), nous allons donc passer un peu de temps à analyser comment ils sont utilisé pour stocker les données du tableau et évalué pendant que les utilisateurs jouent au jeu.

Tout d’abord dans le fichier board.rs, nous trouvons une définition de PixelLine juste après la définition de Pixel :

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

Un vecteur (array) de données de chaîne subdivisé par la largeur du tableau.
Et puis nous définissons dans le PixelBoard comme un vecteur des PixelLines de cette façon :

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

Ainsi, chaque ligne est stockée dans le tableau en tant qu’enregistrement unique avec un champ de métadonnées appelé line_versions incrémenté à chaque fois que vous modifiez une ligne. Ainsi, chaque fois que notre interface récupère le tableau, vous obtenez 50 lignes mais également une métadonnée pour chaque ligne qui représente le nombre de fois que la ligne a été mise à jour, et en récupérant ces métadonnées, l’interface sait quel est le nombre de fois que la ligne a été modifiée, si la ligne a été modifié par rapport à la récupération précédente, vous récupérez les données pour chaque pixel, sinon vous ne le faites pas.

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()
    }
}

Il s’agit d’un moyen intelligent de stocker et de récupérer des données de l’interface qui peuvent être utiles à utiliser lors de votre prochaine partie pour gagner une partie near.

Les Transactions

Revenons un instant dans notre interface utilisateur dans app.js pour être sûr de comprendre comment les transactions sont gérées depuis le frontend. Nous avons d’abord besoin d’une fonction pour vérifier le compte si quelque chose ne va pas et c’est tout :

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

Alors vous souvenez-vous des tableaux _queue et _pendingPixels que nous avons définis dans notre constructeur ? Il est définitivement temps de les utiliser car les transactions sont gérées en fonction des pixels que vous avez dessinés sur le tableau :

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 = [];
  }

Attendez, je n’étais pas prêt pour ce tas de code… Oui, vous l’êtes ! Mais regardons-le attentivement, nous créons un objet pixels (vecteur), nous modifions notre objet _queue pour ajuster les pixels et nous affectons sa valeur à l’objet _pendingPixel dans une fonction asynchrone.

Et maintenant quoi? wNous dessinons simplement sur un objet contrat qui est appelé depuis le sdk proche, et l’action pour dessiner (une partie des actions que nous avons définies pour l’utilisateur) est définie dans le fichier lib.rs rust.

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

Pour le contrat intelligent, les pixels sont une couleur et un identifiant de compte (le propriétaire mystique), et c’est un jeu basé sur l’immobilier : nous avons donc un ancien propriétaire qui a dessiné le pixel avant et un nouveau propriétaire qui veut le dessiner maintenant. Avec l’action de tirage, nous obtenons le old_owner et le remplaçons par le nouveau compte propriétaire en changeant la valeur de couleur de tous les pixels à l’intérieur du vecteur PixelRequest, puis nous envoyons des récompenses à l’ancien propriétaire tout en chargeant le nouveau. Les horodatages des récompenses sont réinitialisés et le décompte recommence à zéro avec un pixel de moins pour l’ancien propriétaire et un de plus pour le nouveau. L’action setPixelRequest est définie dans le fichier board.rs de notre contrat, mais revenons à notre libs.rs.

À quoi ressemble la fonction may_send_rewards() ? Le voici dans toute sa splendeur :

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

S’il vous plaît, ne soyez pas paresseux, si vous ne pouvez pas vous en empêcher, vous pouvez y revenir plus tard avec cette vidéo de l’auteur du jeu. Les explications que je vais utiliser sont également tirées de cette vidéo !

La fonction vérifie l’heure sur la blockchain (nous n’utilisons pas la minuterie sur l’interface ici, parce que nous voulons être sûrs !) puis appelle enfin get_expected_reward() pour calculer les récompenses dues au compte.

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()
    }

Nous prenons donc le solde actuel du compte berryclub (rappelez-vous que nous avons un champ de solde sur le compte?), l’utilisation et les coûts de stockage actuels et un seuil de sécurité de 50 avocats. Si le solde est sûr pour une utilisation en dehors des coûts de stockage, nous le divisons en 24 (heures) * 60 (minutes) portion de récompense, ce qui signifie que vous obtenez fondamentalement exactement le même solde que vous avez une fois si vous l’appelez toutes les minutes, vous pouvez le trouver défini au début du fichier lib.rs :

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

Je parie que vous pensez que le processus de récompense est terminé. Tort.

Nous devons en fait revenir à notre fonction may_send_reward() pour voir qu’elle appelle le nouveau contrat de ferme berryclub pour distribuer les récompenses de jalonnement, qui sont… des concombres, le jeton de jalonnement sur berryclub 🙂

const FARM_CONTRACT_ID_PREFIX: &str = "farm";

Ce n’est en fait pas la seule source de revenus distribués avec cette fonction, elle tire également parti des coûts de gaz payés par les gens pour acheter des avocats et échanger des bananes afin de récompenser toute la communauté !

Comment est-ce possible? Tout d’abord, le GAS_BASE_COMPUTE est défini dans le fichier token.rs où la quantité de gaz pour le contrat intelligent est définie. Oui tu as raison! Le prix de l’essence est bas à proximité et il peut être utilisé pour récompenser les utilisateurs qui interagissent avec votre jeu vidéo !!!

Pour mieux comprendre le fonctionnement des frais GAS sur Near, veuillez vous référer à cette documentation détaillée !

Ce tutoriel vous est présenté par jilt.near et son projet NFT Gaming, soutenez-la dans l’achat de NFT !

Generate comment with AI 2 nL
395

Laisser un commentaire


To leave a comment you should to:


Retour haut de page
Report a bug👀