區塊鏈技術|NFT盲盒遊戲開發設計思路(成品原始碼示例)

v_ch3nguang發表於2023-04-06

盲盒是什麼?

就是 你永遠猜不到盒子裡面是什麼 這就是盲盒的魅力所在。迎合了大眾的 好奇 心理,追求未知的刺激 。而如今 盲盒 遊戲 也開始轉移到線上 再次掀起 新的 浪潮

 

 

就拿現在的NFT 專案與盲盒結合舉例來說,盲盒與挖礦的結合保證了專案的穩定發展和社群的持續活力。專案透過 DeFi 生態系統上流動性、產量礦池和 NFT 的獨特性將其與盲盒遊戲模式結合。

 

 

NFT 透過區塊鏈技術將養成類遊戲與盲盒玩法結合,透過線下盲盒購買實物獲得相關人物道具,每一個人物和道具都擁有的身份識別碼,透過線上遊戲兌換獲得相關道具人物,每一個盲盒可以開出一個人物和兩張道具,同一人物在遊戲中可進行升級,升級分三種形態,當到達一定形態後可兌換更高形態人物模型,郵寄到家。

 

人物池:

 

這個池子設定了人物生長週期,日常可進行人物打怪,進食等獲取使用者成長經驗,而人物的催化成長速度是根據獲取人物的數量決定,線下盲盒獲取該人物越多成長越快

 

 

道具池:

 

這個池子設定了人物的相關道具,武器及裝扮道具,透過武器及裝扮道具提高人物打怪速度和戰鬥屬性。

NFT   主要是因為每一枚代幣在智慧合約中都有一個 tokenID   來表示,以此便可以透過 tokenID   來對代幣進行屬性設定,這些屬性描述為 metadata json 。如下程式碼所示:

 

mapping(uint256 => string) private _tokenURIs;  

_tokenURIs   key   就是 tokenID , value   則是 metadata json   檔案的地址。

metadata json   檔案中儲存著該 NFT   的圖片資源地址等資訊,若要實現盲盒本質上就是在未開盲盒之前給所有的盲盒設定一個預設的 metadata json   檔案地址或不設定,開盲盒的時候再返回具體的檔案地址。

function tokenURI(uint256 tokenId) public view override returns (string memory) {

  // 預設的檔案地址

  if (!canOpen) return unrevealURI;

  // 具體的檔案地址

  return _tokenURIs[tokenId];

}

方案一:直接設定

專案方在開盲盒之前首先準備好每個 tokenID   所對應的 metadata json   檔案。檔名為 ${tokenID}.json , 之後將這些檔案放到一個資料夾中並儲存到 ipfs   中,透過 ipfs://{ 資料夾的 CID}/${tokenID}.json   即可訪問檔案。

同時將檔案的 baseURL ( ipfs://{ 資料夾的 CID}/ ) 儲存到智慧合約中。開盲盒的時候直接對地址進行拼接就能達到開盲盒的目的。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

 

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

import "@openzeppelin/contracts/utils/Counters.sol";

import "@openzeppelin/contracts/utils/Strings.sol";

 

contract GameItem is ERC721URIStorage {

  using Counters for Counters.Counter;

  

  // 自增的 tokenId

  Counters.Counter private _tokenIds;

  

  // 是否可以開盲盒

  bool public canOpen = false;

 

  constructor() ERC721("GameItem", "ITM") {}

 

  function tokenURI(uint256 tokenId) public view override returns (string memory) {

    // 判斷是否可以開盲盒

    require(canOpen, 'can not open now');

    // 確保已被 mint

    require(_exists(tokenId), 'token not minted');

 

    string memory baseURI = _baseURI();

    if (bytes(baseURI).length > 0) {

        // 拼接 json 檔案地址

        string memory path = string.concat(baseURI, Strings.toString(tokenId));

        return string.concat(path, '.json');

    } else {

        return ''

    }

  }

}  

這種方式雖然需要的 gas   低,但存在一個很明顯的問題:如何證明專案方開盒後的圖片在出售前就是確定的。例如,專案方出售了10 個盲盒,在未開盲盒的情況下,這 10 個盲盒的圖片應該都是確定的。但當前這種方案就沒法確定,在盲盒出售後未開盲盒的情況下,專案方大可以隨意調換或更改檔案的內容。例如將 3 號盲盒和 5 號盲盒的內容調換,而此時各自對應的 tokenURI   卻沒有變化。

另一個問題是 nft   的屬性對應關係是人為編造的,並不是真正的隨機。

方案二:隨機數 + 洗牌演演算法

當專案方已經準備好了 nft   的配置檔案並已上傳到了 ipfs 。開盲盒的時候,只需要隨機的從檔案池中取出不重複的檔案就能解決隨機性的問題。例如,當使用者開 1 號盲盒的時候,隨機對應的配置檔案地址是 ipfs://{ 資料夾的 CID}/5.json   。因此可以一定程度上解決方案一隨機性的問題。

其次就是 baseURI   的設定,由於 ipfs   的特殊性,資料夾的 CID   是由其內部檔案決定的,一旦內部檔案修改了則資料夾的 CID 必然會變化, 所以為了防止修改檔案內容,部署智慧合約的時候的就需要去設定 baseURI ,並且其是不可修改的。

針對方案二有兩個問題需要解決:

如何獲取隨機數 - chainlink

如何不重複的抽取檔案 - 洗牌演演算法

隨機數

使用 chainlink   服務獲取隨機數:在   上建立訂閱會得到一個 訂閱ID , 在此訂閱中充值 link   代幣( 每次獲取隨機數都需要消耗 LINK 代幣 ) , 最後並繫結合約地址。

如果你的專案是使用 hardhat   框架,需要安裝 chainlink   的合約庫

$ yarn add @chainlink/contracts  

獲取隨機數示例:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;

 

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

 

contract RoboNFT is VRFConsumerBaseV2 {

  // 協調器

  VRFCoordinatorV2Interface COORDINATOR;

 

  struct ChainlinkParams {

    // 訂閱 ID

    uint64 subId;

    // 要使用的 gas 通道

    // 不同網路的 gas 通道 :

    bytes32 keyHash;

    // 回撥的 gas 限制,其值取決於要獲取的隨機數的數量

    // 獲取一個隨機數需要 20000 wei

    uint32 gasLimit;

    // 請求確認的次數 - 設定為 3 即可

    uint16 requestConfirms;

    // 每次請求獲得的隨機數數量

    uint32 numWords;

  }

  ChainlinkParams public chainlinkParams;

 

  // 儲存返回的隨機數的陣列

  uint256[] public randomNums;

 

  // _vrfCoordinator 是協調器地址,不同網路地址檢視

  constructor(

    ChainlinkParams memory _chainlinkParams,

    address _vrfCoordinator

  ) VRFConsumerBaseV2(_vrfCoordinator) {

    // 建立協調器合約例項

    COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);

    // 初始化 chainlink 引數

    chainlinkParams = _chainlinkParams;

  }

 

  // 請求隨機數 ( 需要錢包有充足 Link 代幣 )

  function requestRandomWords() external {

    // 透過協調器請求隨機數,並返回請求 ID

    uint requestId  = COORDINATOR.requestRandomWords(

      chainlinkParams.keyHash,

      chainlinkParams.subId,

      chainlinkParams.requestConfirms,

      chainlinkParams.gasLimit,

      chainlinkParams.numWords

    );

  }

 

  // chainlink 回撥,並傳入請求 ID 和 隨機數

  function fulfillRandomWords(

    uint256 requestId,

    uint256[] memory randomWords

  ) internal override {

   // 獲取的隨機數

    randomNums = randomWords;

  }

}

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70028290/viewspace-2944148/,如需轉載,請註明出處,否則將追究法律責任。

相關文章