区块链游戏开发指南

658 阅读11分钟

区块链开发在过去几年中迅速成长和发展,现在正在软件开发的各个领域中被采用。从去中心化的应用程序(DApps),到去中心化的金融(DeFi)软件,到NFT,再到DAO,区块链技术已经渗透到广泛的行业,并服务于许多用例。

在本教程中,我们将探讨区块链游戏开发这一新兴趋势。基于区块链的游戏也被称为链式游戏。一旦你了解了编写智能合约并将其部署到区块链上所涉及的基本结构,你就可以使用加密货币空间内的工具来构建游戏。

我们将建立一个彩票游戏来展示区块链上的游戏开发是如何进行的。我们还将回顾在区块链游戏中实现交易的基本结构。然后,我们将把它部署到一个测试网网络上。

什么是区块链?

区块链的底层数据结构是一个链接的列表链,或独特的 "块"。每一个被添加到链上的区块都会自动链接到之前添加的区块,而之前的区块也会指向它的前辈。

这个链状的链接列表本身就是一个交易列表。在这些区块被添加到列表数据结构之前,通过这个过程,奠定了区块链给我们带来的关键创新:一个协议。这个协议帮助网络决定如何将区块添加到链上。

这个决策过程催生了区块链的去中心化性质。工作证明(PoW)、取证(PoS)和权力证明(PoA)是去中心化的机制,通过这些机制,在一个区块被添加到链上之前,这些决定被做出并同意。

通过这些区块链出现的加密货币是一种激励人们运行软件的手段,以确保这些区块链周围网络的安全。

NEAR这样的区块链平台为使用智能合约存储、更新和删除区块链上的数据提供了一个加密的安全平台。

Web3游戏开发

Web3,在区块链的背景下,指的是在区块链上运行的去中心化的应用程序。这些应用程序允许任何人参与,而不对其个人数据进行货币化。只要掌握好这些区块链中任何一种支持的编程语言,我们就可以开始编写智能合约,将游戏应用作为区块链上的DApps来构建。

随着区块链生态系统的发展,新的范式出现了。从De-Fi生态系统中汲取灵感,区块链游戏生态系统也已经发展到了被称为GameFi的东西。GameFi,也被称为 "从玩到赚",通过将其普通用户变成游戏行业内重大决策背后的管理力量,引入了一种新的游戏方式。

当涉及到贵重物品的交易,以及用代币和不可替代的代币产生额外收入时,GameFi促进了玩家拥有的经济。这意味着围绕一个特定的游戏建立社区,这些游戏的用户可以赚取加密货币或资产,这些资产在游戏的元宇宙中是有价值的(在游戏之外也一样)。

在NEAR区块链上编写智能合约

在本教程中,我们将通过建立一个示例游戏项目来演示如何在NEAR区块链上构建游戏。

在这个游戏中,我们将探讨如何设置代码库结构和编写在Near区块链上运行的智能合约所需的编程语言。当我们全部完成后,我们将在本地环境中测试我们的应用程序,然后将我们的智能合约部署到测试网中。

我们将克隆一个入门套件的代码库。这个仓库提供了一个基本的模板,当我们建立游戏的各种功能时,可以在此基础上编写更多的智能合约代码。

git clone https://github.com/IkehAkinyemi/lottery-smart-contract.git

一旦上述命令被成功执行,将目录改为lottery-smart-contract 。你可以在任何文本编辑器中打开它;对于本教程,我们将使用Visual Studio Code。

从终端,在文件夹目录内运行code . 命令。

Folder Structure Starter Kit

上图显示了一个使用AssemblyScript的智能合约的NEAR项目的基本文件夹结构。

script 文件夹包含 shell 源文件,用于编译和部署智能合约到区块链上。src 包含lottery 文件夹,我们将在其中为我们的智能合约编写必要的代码。

其余的文件是AssemblyScript需要的配置文件,以理解Near上定义的一些类型。near-sdk-as 库是用于在AssemblyScript中开发NEAR智能合约的包的集合。

如何在NEAR区块链上建立一个彩票游戏

通过这个游戏,我们将探索使用AssemblyScript在Near区块链上编写智能合约的一些基本概念。

运行yarn installnpm install 命令来安装near-sdk-as 库和任何必要的依赖。

接下来,创建一个名为assembly 的文件夹。在这个文件夹中,创建两个文件:index.tsmodel.tsmodel.ts 文件包含了我们将在整个代码中使用的不同对象类型,在index.ts 文件中。model.ts 文件包含以下内容:

import { RNG } from "near-sdk-as";

@nearBindgen
export class Lottery {
  private luckyNum: u32 = 3;

  constructor() {
    const randGen = new RNG<u32>(1, u32.MAX_VALUE);
    this.id = "LO-" + randGen.next().toString();
  }
}

在上面的代码中,我们定义了一个Lottery 类型。这代表了彩票游戏类型的结构。我们将在里面定义我们要提供的不同接口--包括公共接口和私有接口--就像私有的luckyNum 变量是一个无符号整数。

使用RNG (随机数生成器)对象,我们将游戏的this.id 变量初始化为一个随机数。而在randGen 变量中,我们只是初始化了RNG 对象,而通过randGen.next 函数,我们使用传入的种子值<u32>(1, u32.MAX_VALUE) ,生成了一个随机数。

定义函数接口

现在让我们来定义我们游戏的play 功能。这将包含负责在设定的整数范围内生成一个随机数的代码片段。

import { RNG, logging } from "near-sdk-as";

@nearBindgen
export class Lottery {
  ...

  play(): bool {
    const randGen = new RNG<u32>(1, u32.MAX_VALUE);
    const pickNum = randGen.next();

    logging.log("You picked: " + pickedNum.toString());

    return pickedNum === this.luckyNum
  }
}

有了play 功能,任何玩家都可以调用它,使用RNG 对象生成一个随机数。然后,我们导入了logging 对象,它使我们能够访问本地控制台的输出值--也就是我们的本地机器终端。

play 函数返回一个bool 值,这个truefalse 值是将pickedNumthis.luckyNum 比较的结果,以确定猜测的数字是否等于彩票游戏中定义的luckyNum

接下来,我们将定义reset 函数。顾名思义,这将使我们能够将this.luckyNum 重置为一个新的随机数。

...
@nearBindgen
export class Lottery {
  ...

  reset(): string {
    const randGen = new RNG<u32>(1, u32.MAX_VALUE);
    const randNum = randGen.next();
    assert(randNum !== this.luckyNum, "Rerun this function to generate a new random luckyNum");

    this.luckyNum = randNum;
    return "The luckyNum has been reset to another number";
  }
}

在上面的代码中,我们生成了另一个新的随机数。使用assert 函数,我们将其与当前的this.luckyNum 值进行比较。

如果比较的结果是true ,那么函数的其他代码就继续执行。如果不是,该函数就在这一点上停止,并返回断言信息,Rerun this function to generate a new random luckyNum

assert 为真时,我们将变量this.luckyNum 分配给新生成的数字randNum

定义Player 对象

对于彩票游戏的每个玩家,我们将定义一个基本的类型结构。这个结构在我们的游戏中展示了玩家。

用以下代码更新model.ts 文件。

import { RNG, logging, PersistentVector, Context } from "near-sdk-as";

export type AccountID = string;

@nearBindgen
export class Lottery {
  ...
}

@nearBindgen
export class Player {
  id: AccountId;
  guesses: PersistentVector<bool>;

  constructor(isRight: bool) {
    this.id = Context.sender;
    this.guesses = new PersistorVector<bool>("g"); // choose a unique prefix per account

    this.guesses.push(isRight);
  }
}

Player 对象类型包含两个接口:this.id 变量,它是一个AccountID 类型,以及this.guesses ,它是一个布尔值的数组。

PersistentVector 数据结构是一个数组数据类型。在初始化过程中,我们使用Context 对象,通过Context.sender 函数获得这个智能合约的当前调用者。然后,我们把它分配给this.id

对于this.guesses ,我们初始化一个新的 [PersistentVector](https://near.github.io/near-sdk-as/modules/_sdk_core_assembly_collections_persistentvector_.html)对象,并将其分配给this.guesses 。然后,使用PersistorVector 上的push 函数接口,我们将一个新的布尔值,isRight ,追加到this.guesses 变量中。

让我们来定义其他类型和变量,我们将在下一节定义核心函数时使用:

...
exsport const TxFee = u128.from("500000000000000000000000");
export const WinningPrize = u128.from("100000000000000000000000");
export const Gas: u64 = 20_000_000_000_000;

...

export const players = new PersistentMap<AccountID, Player>("p")
...

定义核心游戏功能

assembly 文件夹中创建一个index.ts 文件。这就是我们定义彩票游戏核心功能的地方。

index.ts 文件内,定义一个pickANum 函数,如下所示:

import { TxFee, Lottery, Player, players } from "./model";
import { Context, u128 } from "near-sdk-as";

export function pickANum(): void {
  verifyDeposit(Context.attachedDeposit);
  const game = new Lottery();
  const guess = game.play();
  let player;

  if (players.contains(Context.sender)) {
    player = players.get(Context.sender) as Player;
    player.guesses.push(guess);
    players.set(Context.sender, player);
  } else {
    player = new Player(guess);
  }
}

function verifyDeposit(deposit: u128): void {
  assert(deposit >= TxFee, "You need 0.5 NEAR tokens to pick a number");
}

在上述函数中,我们要验证0.5个NEAR代币的存款,然后彩票游戏的任何玩家才能调用任何调用在智能合约上进行游戏。这样一来,我们的玩家在玩游戏之前就支付了一定的金额。另外,一旦有玩家参与游戏,我们就会在玩家数据结构中更新该玩家的资料。

接下来,让我们定义一个函数,通过随机生成等于luckyNum 的正确数字,来处理支付获胜玩家的问题。

import { TxFee, Lottery, Player, players, Gas, WinningPrize } from "./model";
import { Context, u128, ContractPromiseBatch, logging } from "near-sdk-as";

function on_payout_complete(): string {
  logging.log("This winner has successfully been paid");
}

export function payout(): void {
  const player = players.get(Context.sender) as Player;

  for (let x = 0; x < players.guesses.length; x++) { 
    if (player.guesses[x] === true) {
      const to_winner = ContractPromiseBatch.create(Context.sender);
      const self = Context.contractName;

      to_winner.transfer(WinningPrize);
      to_winner
        .then(self)
        .function_call("on_payout_complete", "{}", u128.Zero, Gas)
    }
  }
}

上述函数帮助我们向彩票游戏的赢家进行转账交易。通过ContractPromiseBatch 对象,我们创建并设置了一个转账交易到我们作为参数传入create 方法的地址。然后,通过transfer 函数,我们用传入的代币WinningPrize 做一个交易的价值。

使用function_call 函数,我们为交易的成功发送安排一个函数调用。对于这个游戏,我们打算在交易成功时调用的函数是on_payout_complete

在本教程中,我们不会专注于建立NEAR TestnetTestnet钱包,但我鼓励你查看链接,了解更多关于NEAR生态系统中存在的各种网络。

在这个演示中,我们将建立我们的彩票游戏来生成二进制格式的.wasm 文件,然后使用near dev-deploy 命令来部署智能合约。

构建和部署智能合约

我们将首先使用asb 命令构建智能合约:

yarn asb

这是对yarn asb --verbose --nologo 命令的一个别名命令,位于根目录下的package.json 文件中定义了该命令。

在我们成功地生成了一个build 文件夹,在build/release/ 文件夹内包含一个lottery.wasm 文件后,我们可以运行以下命令来部署它:

near dev-deploy ./build/release/lottery.wasm 

这将部署智能合约,并为我们提供合约名称或ID,我们可以用它来在前端或通过shell文件与之互动。

$ near dev-deploy ./lottery.wasm                 
Starting deployment. Account id: dev-1635968803538-35727285470528, node: https://rpc.testnet.near.org, helper: https://helper.testnet.near.org, file: ./lottery.wasm
Transaction Id 4TWTTnLEx7hpPsVMfK31DDX3gVmG4dsqoMy7sA7ypHdo
To see the transaction in the transaction explorer, please open this url in your browser

测试我们的区块链游戏

我已经写了两个单元测试,以确认我们的应用程序实际上是功能性的。这两个简单的测试将创建一个彩票游戏,同时将luckyNum 变量重置为一个新的随机数。

/src/lottery/__test__ 文件夹包含测试文件。使用以下命令运行测试套件。

$ yarn test:unit
[Describe]: Checks for creating account

 [Success]: ✔ creates a new game
 [Success]: ✔ create and reset the luckyNum of a new game

    [File]: src/lottery/__tests__/index.unit.spec.ts
  [Groups]: 2 pass, 2 total
  [Result]: ✔ PASS
[Snapshot]: 0 total, 0 added, 0 removed, 0 different
 [Summary]: 2 pass,  0 fail, 2 total
    [Time]: 19.905ms

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  [Result]: ✔ PASS
   [Files]: 1 total
  [Groups]: 2 count, 2 pass
   [Tests]: 2 pass, 0 fail, 2 total
    [Time]: 13907.01ms
Done in 14.90s.

结论

在本教程中,我们演示了如何在区块链平台上创建游戏应用。基于区块链的游戏既可以作为多人游戏,也可以单独玩。

你还可以扩展区块链游戏的概念,在你的游戏周围加入一个元宇宙--一个数字世界。元宇宙是一个玩家可以组队的世界,创造一个治理,甚至创造货币作为价值交换的手段。你可以在一个数字游戏世界中铸造NFT或形成DAO。

查看NEAR的文档,看看如何建立一个前端来消费本教程中创建的游戏智能合约。该智能合约的完整代码库可在GitHub上找到。