共计 3878 个字符,预计需要花费 10 分钟才能阅读完成。
盲盒是什么?
就是你永远猜不到盒子外面是什么,这就是盲盒的魅力所在。投合了公众的好奇心理,谋求未知的刺激。而现在盲盒游戏也开始转移到线上,再次掀起新的浪潮。
就拿当初的 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 服务获取随机数:在 vrf.chain.link/ 上创立订阅会失去一个订阅 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 通道: https://docs.chain.link/docs/vrf-contracts/#configurations
bytes32 keyHash;
// 回调的 gas 限度,其值取决于要获取的随机数的数量
// 获取一个随机数须要 20000 wei
uint32 gasLimit;
// 申请确认的次数 - 设置为 3 即可
uint16 requestConfirms;
// 每次申请取得的随机数数量
uint32 numWords;
}
ChainlinkParams public chainlinkParams;
// 存储返回的随机数的数组
uint256[] public randomNums;
// _vrfCoordinator 是协调器地址,不同网络地址查看 https://docs.chain.link/docs/vrf-contracts/#configurations
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;
}
}