01_介绍

  • 本课程共四章节,总学习时长约36分钟

目录: - 游戏设计 -城堡序列号 -城堡数据基础 -经济系统 -战斗系统 - 连接到Sui网络 -介绍sui -预备条件和安装 -开发环境 -Sui钱包和水龙头 -Sui_Explorer - Sui项目结构-包和模块 -初始化您的城堡项目 -项目文件 -项目清单 -定义模块 - 构建、测试、发布 -构建包 -单元测试 -发布包

01_游戏设计 - 课程学习时长~约10分钟

城堡序列号

欢迎来到 Move 城堡课程!在这个有趣的课程中,我们将引导您了解如何在 Sui 网络上使用 Move 的基础知识和一些高级概念, 同时您将构建一个令人兴奋的游戏项目。

介绍我们在 Sui 网络上构建的游戏,一个充满多样城堡的生动世界,每个城堡都有独特的外观和属性。 这些城堡是我们游戏经济和战争系统的核心,允许玩家参与战斗并从对手那里夺取经济资源。

我们的游戏核心概念是城堡序列号。可以将其视为一个“非唯一ID”,对于定义您城堡的初始属性至关重要。 这个ID是一个6位十进制数,每位数字对应一个特定属性,这些属性影响您城堡的能力和外观。

Logo

  • 尺寸和种族:这些因素在确定您城堡的强度方面至关重要,并在游戏策略中起着重要作用。
  • 风格、颜色、徽标风格、徽标颜色:这些方面塑造了您城堡 NFT 的视觉识别,使每个城堡都独一无二。
  • 铸造城堡时,玩家可以自由选择其尺寸(小、中、大),而其他属性则是随机生成的,为过程增添了惊喜和独特性。

加入我们,在 Sui 网络上打造您自己的城堡。这次冒险是 Move 基础知识的实用介绍,融合了策略、创意和区块链基础知识。 虽然游戏是作为教育用途的演示设计的,但它提供了坚实的基础,是那些热衷于探索区块链游戏开发的人的绝佳起点。

城堡数据基础

除了我们在上一课中介绍的尺寸和种族属性外,每个城堡还有一些其他的游戏属性。以下是城堡游戏属性的完整表格:

属性描述
尺寸城堡尺寸:小、中、大。
种族五大种族:人类、精灵、兽人、哥布林、僵尸。
等级城堡等级,初始为1,最高可达10。
经验池用于存储升级所需的经验值。
金库用于招募士兵的金币。
基本经济力由尺寸和等级决定的基本经济力,影响金库金币的增长速度。
基本攻击力由尺寸、种族和等级决定的基本攻击力,影响战斗结果。
基本防御力由尺寸、种族和等级决定的基本防御力,影响战斗结果。
士兵士兵数量影响城堡的总体攻击力和防御力。
  • 城堡的种族属性是随机生成的,这将影响城堡属性的增长趋势:是更擅长攻击还是更固若金汤。

  • 不同尺寸的城堡具有不同的经济力。随着时间的推移,金币会自动填充到城堡的金库中。

  • 金币可以用来招募士兵。士兵在战斗中可以提供额外的攻击力和防御力,同时也能增加城堡的经济力,赚取更多的金币。

  • 战斗后,士兵会不可避免地损耗,这取决于战斗双方的战斗力。

  • 战斗结束后,胜利者将获得经验值并在一段时间内获得对方的经济利益。

  • 经验值将用于升级城堡。

经济系统

在上一课中,我们了解到每个城堡都有金库和经济力。在本课中,我们将进一步了解游戏的经济系统设计。

  1. 金库

    • 城堡的金库属性表示城堡拥有的金币数量。城堡的初始金库为0。

    • 金库中的金币可以用来招募士兵。

  2. 经济力 经济力代表城堡每单位时间可以获得的金币数量。

    经济力分为基础经济力和附加经济力两种类型。

    • 基础经济力是城堡自带的,由城堡的尺寸和等级决定。
    • 士兵可以提供附加经济力,每个士兵将为城堡提供1点附加经济力。

    另一种形式的附加经济力是战斗赔款,战斗的胜利者将获得败者基础经济力的附加经济力。另一方面,败者在战斗冷却期间基础经济力将为0。

战斗系统

在上一课中,我们了解了经济系统设计。本课中,我们将学习游戏中的另一个核心概念——战斗系统。

在这个游戏中,城堡可以与其他城堡进行战斗。战斗机制涉及随机性、战斗力确定、胜者奖励、败者惩罚和战斗冷却。

  1. 随机敌人 在进行战斗计算之前,每个开始战斗的城堡都需要从不在战斗冷却期的城堡池中随机挑选一个对手。

  2. 战斗力 每个城堡都有其基础战斗力——基础攻击力和基础防御力,它们由城堡的种族、尺寸和等级决定。

    与经济力类似,士兵也可以提供额外的攻击力和防御力。不同种族的士兵可以提供不同的攻击和防御加成。

  3. 战斗结果确定 要确定战斗中的胜者和败者,必须比较攻击方的总体攻击力和防御方的总体防御力。

    总体攻击力 = 基础攻击力 + 士兵的额外攻击力
    总体防御力 = 基础防御力 + 士兵的额外防御力
    

    除了总体战斗力之外,在战斗结果确定过程中,双方之间还存在种族优势。

    战斗

    在战斗中拥有种族优势的一方,其战斗力将会按比例增加。例如,当人类城堡攻击精灵城堡时,攻击方的总体攻击力将增加50%。另一方面,当精灵城堡攻击人类城堡时,防御方的总体防御力将增加50%。

  4. 战斗赔款 当胜者确定后,胜者将在一段时间内获得败者的经济利益。在此期间,败者的基础经济力将为0,而胜者将拥有败者基础经济力的附加经济力。

  5. 士兵伤亡 城堡的士兵也会参与城堡之间的战斗,当然他们也会遭受伤亡。

  6. 战斗冷却 战斗结束后,双方都会进入战斗冷却期,在此期间无法开始新的战斗,也不会受到攻击。

02_连接到Sui网络 - 课程学习时长~约10分钟

介绍sui

在上一节中,我们对城堡游戏的整体设计有了基本了解。本节中,我们将了解 Sui 网络。

什么是 Sui?

根据他们网站的介绍:

Sui 是一个第一层区块链和智能合约平台,旨在使数字资产所有权变得快速、安全且无需授权。

让我们了解 Sui 网络的一些核心概念和核心特性。

  1. 共识机制 Sui 使用委托权益证明(DPoS)来确定处理交易的验证者集合。这不同于其他像比特币这样的工作量证明(POW)共识网络,Sui 验证者必须持有一定数量的 SUI。

  2. Sui 上的 Move Move 是一种平台无关的语言,用于多个区块链。Sui 也利用了 Move 语言在智能合约开发中的特性。

    Sui 上的 Move 与其他区块链上的 Move 有一些重要区别:

    • Sui 使用其自己的以对象为中心的全局存储
    • 地址表示对象 ID
    • Sui 对象具有全局唯一 ID
    • Sui 具有模块初始化器(init 函数)
    • Sui 入口点以对象引用作为输入

    请阅读 Sui 文档 以进一步了解这些区别。

  3. 以对象为中心 在 Sui 上,一切都是对象,对象是基本的数据存储单元,而不是账户。

    对象设计有不同的所有权:专有和共享。通常,一个对象由一个地址拥有,例如一个 NFT。一些对象具有共享所有权,可以由多个账户修改。

  4. 并行交易处理 区块链上的大多数交易是“简单的”,如资产转移、点对点支付、铸造 NFT。这些简单交易是独立的,不需要任何特定顺序。Sui 对这些交易进行了优化, 由于它们是独立的,不需要全局共识来实现总排序,Sui 并行处理简单交易。

预备条件和安装

在本课中,我们将探讨在本地环境中安装 Sui 的过程。

本课内容参考了官方的 Sui 安装文档。本课将重点介绍如何通过从源代码构建来安装 Sui。 官方安装文档还提供了另外两种安装 Sui 的方法,即通过 Homebrew 安装或直接从发布的二进制文件安装。

Sui 支持多种操作系统:

•	Linux - Ubuntu 版本
•	macOS - macOS Monterey
•	Microsoft Windows (10 和 11)

在构建源代码和安装 Sui 二进制文件之前,有一些环境的先决条件需要满足。

预编译好的安装

1. macOS brew

brew install sui

1. Rust 和 Cargo

1.1 Linux 和 macOS

Sui 需要 Rust 和 Cargo,建议使用 rustup 安装 Rust。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

1.2 Windows

在 Windows 系统中,可以使用官方安装包下载并安装 Rust。安装过程中,可能需要根据提示安装 Visual Studio C++ Build 工具。

2. 环境特定的先决条件

2.1 Linux

sudo apt-get update
2.1.1 cURL
sudo apt-get install curl
curl --version
2.1.2 Git
sudo apt-get install git-all
2.1.3 CMake
sudo apt-get install cmake
2.1.4 GCC
sudo apt-get install gcc
2.1.5 libssl-dev
sudo apt-get install libssl-dev

如果您已安装 OpenSSL,您可能还需要安装 pkg-config

sudo apt-get install pkg-config
2.1.6 libclang-dev
sudo apt-get install libclang-dev
2.1.7 libpq-dev
sudo apt-get install libpq-dev
2.1.8 build-essential
sudo apt-get install build-essential

2.2 macOS

2.2.1 Brew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
2.2.2 cURL
brew install curl
2.2.3 CMake
brew install cmake
2.2.4 Git
brew install git

2.3 Windows

2.3.1 cURL

cURL 已经在 Windows 11 中预安装。如果您使用其他版本的 Windows,可以从 https://curl.se/windows/ 下载并安装。

2.3.2 Git

从 https://git-scm.com/download/ 下载并安装。

2.3.3 CMake

从 https://cmake.org/download/ 下载并安装。

2.3.4 Protocol Buffers

从 https://github.com/protocolbuffers/protobuf/releases 下载,根据您的系统类型选择 xxx-win32.zipxxx-win64.zip

解压并将 \bin 文件夹添加到您的 PATH 系统变量中。

2.3.5 其他

LLVM编译器基础设施。寻找一个名称类似于 LLVM-15.0.7-win64.exeLLVM-15.0.7-win32.exe 的文件。

3. 安装 Sui 二进制文件

通过运行以下命令来安装或升级 Sui 二进制文件。

cargo install --locked --git https://github.com/MystenLabs/sui.git --branch devnet sui

请注意,sui 控制台命令在 Windows PowerShell 中无法运行。

4. 额外建议

在升级时,建议从官方发布版本中下载 Sui 二进制文件,而不是从源代码升级,因为构建过程可能会耗费大量时间。

官方文档的“从二进制文件安装”部分中,明确说明了每个二进制文件在发布资产中的用途。选择主 Sui 二进制文件并替换位于 ~/.cargo/bin 中的现有文件。

开发环境

在学习了上一课后,您已经在您的环境中安装了 Sui。当您准备在 Sui 上开发智能合约时,需要选择一个便捷的工具(IDE)。

在本课中,我们将介绍 3 个推荐的用于 Sui 上 Move 开发的 IDE。

1. ChainIDE Studio

ChainIDE Studio 是 ChainIDE 产品系列的一个 IDE 产品。它是一个基于云的多链 IDE,适用于 web3 开发者。

CIDE Studio 最近添加了对 Sui 链的集成,并提供了多个 Sui 上的示例合约项目。

2. Visual Studio Code

VSCode 是许多程序员使用的流行代码编辑器。对于在 Sui 上开发 Move 项目,推荐安装 Mysten Labs 的 “Move” 语言支持扩展。此扩展兼容 Move 2024 版本。

安装“Move”扩展的步骤如下:

  1. 打开 Visual Studio Code (VSCode)。
  2. 点击扩展图标进入扩展视图。
  3. 在扩展视图中,在搜索栏中搜索“Mysten”。
  4. 点击安装按钮进行安装。

MOVE

3. MoveCastle 课程内置 IDE

作为另一种选择,我们集成了一个内置 IDE 以提升您的学习体验。首次进入课程时,该 IDE 将自动启动,提供一个预先安装了 sui 二进制文件的环境。 由 ChainIDE Studio 提供支持,您可以将其用作远程工作空间,具有管理项目文件和访问类 Unix 环境(终端)的功能。此外,它已预配置为适应 Sui 链, 确保为您的需求定制的无缝开发环境。

Sui 钱包和水龙头

在连接到 Sui 网络之前,您需要先创建您的账户(地址)。无论您使用 Sui 客户端 CLI 还是 Sui 钱包,创建账户都非常方便。

为了您的方便,我们在课程平台中集成了 ChainIDE studio 工作区以支持 Sui。只需展开当前页面的右侧部分,即可显示一个空工作区。点击“工具”部分下的“终端”按钮,等待终端初始化。此终端环境已预先安装了 Sui 二进制文件。您可以通过执行 sui --version 来检查 Sui 版本。请随意在此环境中探索和实验。

1. 创建账户

使用 Sui 客户端 CLI,输入以下命令生成一个新的 Sui 地址:

sui client new-address ed25519

在此命令中,ed25519 参数是密钥对方案。您可以在此文档中找到有关 sui client 命令的更多子命令详情。

执行此命令后将得到以下输出: new-address

  • 地址:账户地址。
  • 密钥方案:密钥对方案。
  • 恢复短语:通常称为助记词,是 Sui 钱包的备份。它可以导入到任何 Sui 钱包中,您必须记住它。

现在账户已经创建并自动添加到 Sui 客户端,运行以下命令进行检查:

sui client addresses

addresses

如果您在 Sui 客户端中有其他地址,可以切换到您刚刚创建的地址:

sui client switch --address {your_address}

2. 使用 Sui 钱包

使用 Sui 钱包,您可以轻松管理在 Sui 网络上的资产。

要安装 Sui钱包,只需在 Chrome Web Store 上安装 Chrome 浏览器扩展,访问 Sui 钱包扩展页面,点击“添加到 Chrome”,然后点击“添加扩展”。

我们已经创建了账户,点击“导入现有钱包”并粘贴账户的恢复助记词。然后,您需要设置 Sui 钱包的密码。

3. 从水龙头获取 SUI 代币

Sui 水龙头是一个工具,您可以请求免费的测试 SUI 代币来与 Sui Devnet 和 Testnet 交互,这是我们开发和测试中必不可少的。

获取 SUI 代币的更多方法:https://docs.sui.io/guides/developer/getting-started/get-coins

使用钱包请求测试代币非常简单。在钱包设置中将钱包网络切换到 Devnet,然后您可以看到“大按钮‘请求 Devnet SUI 代币’”,点击它,您将获得10个代币。 request

返回到 Sui 客户端 CLI,您还可以使用以下命令检查您的代币余额:

sui client gas

gas

在 Sui Explorer 上检查

区块链浏览器 是 Sui 生态系统中的重要工具。它是一个综合平台,展示在 Sui 网络上生成的所有内容,如交易、账户、对象等。

探索 https://suivision 或者 suiscan 并搜索您在上一课中创建的账户(地址)。在页面上,导航到 Owned Objects 部分,查看上一课中请求的10个 SUI 代币。

owned-objects

在账户地址页面底部的 Transaction Blocks 部分,您会找到一个特定的交易块,其中包含有关由 Devnet 水龙头发送的 10 个 SUI 代币转移的详细信息。点击该块链接以查看交易详情。

03_Sui项目结构-包和模块 - 课程学习时长~约8分钟

初始化您的城堡项目

在上一节中设置好我们的开发环境后,现在我们准备深入了解 Sui 项目是如何工作的。

1. 包和模块

(package) 是 Sui 网络上的基本部署单元。它由一个或多个模块组成,这些模块指定与链上对象的交互。

在 Move 语言中,模块使用 module 关键字定义,包括类型和函数。在提供的示例中:

module hello_world::hello_world {
    // 内容
}

第一个 hello_world 表示包名,第二个 "hello_world" 表示模块名。

2. 创建包

Sui 客户端 CLI 提供了一个方便的工具来快速创建 Sui 包。要创建一个名为 move_castle 的新包,请打开终端并执行以下命令:

sui move new move_castle

如果您使用的是 MS Windows,请使用 cmd 而不是 PowerShell。执行该命令后,将生成一个名为 move_castle 的文件夹。 在该文件夹中,您会找到一个 source 文件夹和一个 Move.toml 文件。我们将在下一课中详细了解它们。

查看项目文件

在上一课中,我们在 Sui 上生成了一个名为 "move_castle" 的初始 Move 项目。现在,让我们来探索项目的内容。

目录结构清晰简单:

move_castle
├── Move.toml
├── sources
│   └── move_castle.move
└── tests
    └── move_castle_tests.move

项目文件夹的结构如下:

  1. 项目根文件夹,名为 move_castle
  2. 清单文件 Move.toml
  3. 包含 .move 合约文件的 sources 子文件夹
  4. 包含测试文件的 tests 子文件夹

Move.toml - 项目清单

检查 Move.toml 清单文件的内容,我们发现几个部分:

1. package

package 部分提供有关当前包的信息,包括其名称(move_castle)和 Move 版本(2024.beta)。

[package]
name = "move_castle"
edition = "2024.beta"

2. dependencies

dependencies 部分指定项目的外部依赖项。在本例中,项目依赖于 Sui 标准库(Sui)。该依赖项包括有关其来源的详细信息,从指定的 GitHub 存储库和修订版中获取。

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }

在我们的课程中,我们将专注于 Sui devnet,因此将 rev = "framework/testnet" 更新为 rev = "framework/devnet"

3. addresses

addresses 部分定义包地址的别名。在这种情况下,当前包设置为地址 0x0,这将把包发布到新的地址。

[addresses]
move_castle = "0x0"

定义模块 - 创建 castle.move

sui move new 工具已经创建了一个包含默认 move_castle 模块的 move_castle.move 文件。在查看其内容后,删除此文件并创建我们的自定义模块。

对于我们的 Move Castle 游戏,我们将从定义一个 "castle" 模块开始。在 sources 文件夹中创建一个名为 castle.move 的新 Move 文件:

move_castle
├── Move.toml
└── sources
    └── castle.move

模块的定义语法如下:

module move_castle::castle {
    
}

现在我们有了一个包含一个模块的完整 Sui 包,尽管目前该模块是空的。

04_构建、测试、发布 - 课程学习时长~约8分钟

构建包

在上一节中,我们创建了一个初始包。在将其发布到 Sui 网络之前,我们需要先进行构建。

在包的根文件夹中,运行以下命令:

cd move_castle
sui move build

build

根据输出,构建过程完成了以下三件事:

  1. 更新依赖仓库。
  2. 包含依赖库(sui-framework 和 Move 标准库)。
  3. 构建。

构建完成后,我们在包的根目录中发现了两个新文件夹/文件: ll

build 文件夹包含包编译的结果,如字节码和解析的依赖项等。

Move.lock 也是一个 TOML 文件,与 Move.toml 类似,它记录了包的详细信息、依赖项和编译信息,用于链上源代码验证等任务。该文件确保了构建的一致性,应包括在版本控制中。更多详细信息可以在 Move.lock 文档 中找到。

单元测试

Sui 提供了一个测试框架,以便在 Move 的开发过程中进行单元测试。Move 单元测试本质上是一个带有 #[test] 注释的无参数公共函数。

由于 sui move new 创建了 tests 文件夹和一个示例测试文件,我们将其重命名为 castle_tests.move 并取消文件内的注释内容:

cd tests
mv move_castle_tests.move castle_tests.move
#[test_only]
module move_castle::castle_tests {
    // 取消此行的注释以导入模块
    // use move_castle::move_castle;

    const ENotImplemented: u64 = 0;

    #[test]
    fun test_move_castle() {
        // pass
    }

    #[test, expected_failure(abort_code = ::move_castle::castle_tests::ENotImplemented)]
    fun test_move_castle_fail() {
        abort ENotImplemented
    }
}

不要忘记将模块名称重命名为 module move_castle::castle_tests

#[test_only] 注释表示 castle_tests 模块仅存在于测试范围内,不会在包构建和发布时包含。此注释也可以应用于函数。

返回包的根目录,并执行以下命令以运行测试:

sui move test

test

执行后,您将看到测试输出,如果一切正常,结果应为 OK

作为附加步骤,尝试将 assert! 语句中的条件修改为 false,并观察输出。这将帮助您了解测试框架如何处理失败情况。

发布包

我们已经成功编译并测试了 Sui 包。在本课中,我们将探讨如何将其发布到 Sui devnet。

1. 预检查

在使用 Sui 客户端 CLI 部署之前,必须进行一系列检查:

1. 验证目标网络的 Sui 依赖项

使用以下命令检查您是否在 devnet 上:

sui client env

确保项目的 Move.toml 中指定的 Sui 依赖项与您的目标网络一致。对于本课程中部署到 Sui devnet,请查看上一课中 "Move.toml" 介绍的 Sui 依赖项。默认的存储库分支设置为 framework/testnet,但是需要更新为 framework/devnet

可以尝试在不同网络上进行部署,以在本课结束时观察潜在的错误。

2. 确保您有足够的 Gas

执行以下命令检查您的地址余额:

sui client gas

如果余额为空,请按照上一课中的步骤请求水龙头以获取一些代币。

2. 部署

要将包发布到 Sui 网络,可以使用 Sui 客户端 CLI 的以下命令:

sui client publish

部署输出是全面的。

gas-budget

输出包括几个部分:

  1. 交易摘要:在 Sui 概念中,交易的唯一“ID”。
  2. 交易数据:交易的数据。
  3. 交易效果:交易的详细信息,如对象更改、gas 费用等。
  4. 对象更改:对象更改的详细信息,包含创建的、变更的和发布的对象。您可以在“Published Objects”中看到包对象。
  5. 余额变化:地址余额的变化,在这种情况下,唯一的变化是 gas 费用。

3. 后检查

包已经发布到 Sui devnet,让我们在 "Sui explorer" 上检查它。

访问 SuiScan,使用输出中打印的“package id”在 Sui explorer 上查看包的详细信息。

4. 关于 Sui 二进制版本

当将包发布到 Sui 的特定网络(主网、测试网或开发网)时,必须将本地依赖版本与链上依赖版本对齐,如下所示:

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/devnet" }

此配置指定了 sui-framework 的 framework/devnet 版本。尝试将此包发布到与指定版本不同的网络(例如测试网)将导致源代码验证错误,如以下错误消息所示: error

为了解决这个问题,您可以临时使用 --skip-dependency-verification 标志运行 sui client publish 命令。然而,更可靠的解决方案是将 Sui 二进制文件更新到与目标网络匹配的正确版本。这意味着从 Sui 发布页面下载适用于您的目标网络(主网、测试网或开发网)的适当 Sui二进制文件,并替换现有的二进制文件(位于 ~/.cargo/bin 目录中)。

02_Move 基础 - 构建您的城堡框架

  • 本课程共四章节,总学习时长约44分钟

目录: - 类型 -原始类型 -自定义类型和能力 -嵌套结构体 -集合与泛型 -动态字段 - 控制结构 -条件语句 -While和Loop -Abort和Assert - 函数 -函数可见性 -初始化函数 -入口函数 -参数传递 - 操作与数学 -基本操作 -Sui框架简介 -Math -随机数

01_类型 - 课程学习时长~约13分钟

原始类型

在本章中,我们的重点是掌握 Sui 上 Move 的基本知识。

Move 是一种开源编程语言,最初设计用于构建安全智能合约以支持 Diem 区块链。随后,包括 Aptos、Starcoin 以及当然还有 Sui 在内的多个区块链采用了 Move 语言。

与任何标准编程语言一样,Move 也有其一套原始类型,本课将深入探讨这些原始类型。

1. 整数

Move 提供了六种无符号整数类型:u8u16u32u64u128u256。这些类型的取值范围从 0 到由具体类型大小决定的最大值。

类型值范围
uint8, u80 到 2^8 - 1
uint8, u160 到 2^16 - 1
uint8, u320 到 2^32 - 1
uint8, u640 到 2^64 - 1
uint8, u1280 到 2^128 - 1
uint8, u2560 到 2^256 - 1

创建一个单元测试来进行一些测试。

#[test]
fun integer_test() {
    // 定义变量并设置它们的类型
    let a: u8 = 8;
    let b: u16 = 18;
    let c: u32 = 188;
    let d: u64 = 1888;
    let e: u128 = 18888;
    let f: u256 = 188888;

    // 为变量赋值
    let g = 100u64;

    // 相同整数类型可以进行比较
    assert!(d > g, 0);

    // 不同类型的比较会导致编译错误
    assert!(a < b, 0); // 编译错误!

    // 不同类型之间的数学运算也是不允许的
    assert!(a + b > 0, 0); // 编译错误!

    // 或者您可以与没有类型的直接值进行比较
    assert!(c < 200, 0);
}

当您需要在 Move 中处理不同的整数类型时,语言包含 as 操作符。您可以使用 as 将一种整数类型转换为另一种类型。

// 使用 `as` 转换整数类型
assert!(_e < (_f as u128), 0);

2. 布尔值

布尔类型很简单,它们只有两个常量值:truefalse

let a: bool = true;
let b = false;
assert!(a != b, 0);

自定义类型和能力 - 创建 Castle 结构体

在 Move 中,用户可以使用 struct 关键字创建自定义类型。由 struct 定义的结构体通过允许在其中分组各种数据字段,作为组织数据的基础元素。

在面向对象编程语言中,对象由其属性定义。同样在 Sui 的 Move 中,您可以使用 struct 定义自定义对象。

1. 自定义类型 - struct

现在让我们创建 castle 结构体。在 castle.move 文件中:

module move_castle::castle {
    use std::string::{Self, String};

    /// The castle struct
    public struct Castle {
        name: String,
        description: String,
        serial_number: u64,
    }
}

castle 结构体包含三个属性:

  • name:城堡的名称
  • description:城堡的描述
  • serial_number:城堡的序列号,影响城堡的视觉呈现和核心游戏数据

我们将在后面的课程中使用 castle 结构体创建 Sui 对象,现在我们只介绍结构体本身。

2. 带有能力的类型

在 Sui 的 Move 中,与类型相关的另一个基本概念是能力。每种类型(结构体)最多有四种能力,这些能力定义了该类型对象在 Sui 运行时中的行为。

四种能力是:

  • copy - 对象可以被复制或克隆。
  • drop - 对象可以被销毁。
  • key - 对象可以通过其对象 ID 进行索引。
  • store - 对象可以存储在全局存储中。

在 Sui 的 Move 中,具有 keystore 能力的自定义类型被认为是资产。例如,NFT 是一种资产,存储在 Sui 的链上存储中, 并且可以在账户之间转移。在我们的例子中,城堡是资产。

因此,我们需要将 castle 结构体修改为:

module move_castle::castle {
    use std::string::{Self, String};

    /// The castle struct
    public struct Castle has key, store {
        id: UID,
        name: String,
        description: String,
        serial_number: u64,
    }
}

您是否注意到一个特殊字段 id: UID 被添加到 Castle 结构体中?这是必须的:具有 key 能力的结构体的第一个字段必须是 id: UID,以便在链上存储对象的唯一地址。

嵌套结构体 - 城堡数据结构体

在上一课中,我们学会了如何创建一个结构体并创建了表示城堡对象的 "Castle" 结构体。 在 Move Castle 游戏中,城堡配备了特定的游戏数据。本课中,我们将深入创建一个单独的城堡数据结构体,并探讨嵌套结构体的概念。

为了在 Sui 的 Move 中方便地使用嵌套结构体,一种常见做法是“包装”。在这种方法中,定义了外层的“包装”结构体 CastleData。 以下是在 sources/ 下创建 core.move 模块,其中介绍了 CastleData 结构体,并填充了基本的游戏属性:

module move_castle::core {
    use sui::object::{Self, UID, ID};

    public struct CastleData has store {
        id: ID,
        size: u64,
        race: u64,
        level: u64,
        experience_pool: u64,
    }
}

与我们在 Castle 结构体中使用的 UID 不同,ID 不是全局唯一的,您可以从 UID 创建 ID,并且 ID 可以被复制和丢弃。有关详细信息,请查看 sui-frameworkobject.move 的源代码。

Move 2024.beta 版自动包含 sui::object 模块的别名。这意味着您可以从代码中删除 use sui::object::{Self, UID, ID}; 这一行。

在游戏机制中,城堡的核心属性包括尺寸、种族、等级和升级所需的经验池。

还有两个核心方面:经济和战斗机制。

让我们创建 Economy 结构体:

public struct Economy has store {
    treasury: u64,
    base_power: u64,
    settle_time: u64
}

economy 字段集成到现有的 CastleData 结构体中:

public struct CastleData has store {
    id: ID,
    size: u64,
    race: u64,
    level: u64,
    experience_pool: u64,
    economy: Economy,
}

现在,将 CastleData 结构体扩展以包括用于战斗机制的 Millitary 结构体:

public struct CastleData has store {
    id: ID,
    size: u64,
    race: u64,
    level: u64,
    experience_pool: u64,
    economy: Economy,
    millitary: Millitary,
}

public struct Millitary has store {
    attack_power: u64,
    defense_power: u64,
    total_attack_power: u64,
    total_defense_power: u64,
    soldiers: u64,
    battle_cooldown: u64,
}

对于更复杂的情况,您可以更深入地嵌套结构体。向 Economy 结构体添加两个额外字段:

use std::vector;

public struct Economy has store {
    treasury: u64,
    base_power: u64,
    settle_time: u64,
    soldier_buff: EconomicBuff,
}

public struct EconomicBuff has copy, store, drop {
    debuff: bool,
    power: u64,
    start: u64,
    end: u64,
}

您是否注意到这里引入了一个 vector?这是 Move 语言提供的集合支持。我们将在下一课中深入探讨其用法。顺便提一下,std::vector 模块也是默认提供的。

集合与泛型 - 经济增益

与大多数编程语言一样,集合是 Sui 上 Move 中一个基本且关键的概念。

基础的集合支持 std::vector 来自 Move 标准库,除此之外,Sui 还为不同的数据分组场景提供了各种集合类型。

向量

要使用常用的 std::vector 类型,可以从创建一个空向量开始:

let v = vector::empty<T>();

尖括号内的 T 来自 Move 语言的核心概念 - 泛型类型。 泛型类型使用占位符或符号来表示不同的类型,使代码在不指定确切类型的情况下也能运行。

在向量的情况下,它们可以容纳任何类型的数据:

let v1 = vector::empty<u64>();
let v2 = vector::empty<String>();
let v3 = vector::empty<Castle>();

但是,需要注意的是,一旦创建了一个向量,就不能再向其中添加与定义类型不同的元素。例如,尝试向 v1 添加字符串或向 v2 添加整数会导致错误。

回到 Move Castle 游戏中,根据游戏机制,士兵将对城堡的经济产生积极影响,城堡在战斗后将获得经济补偿或惩罚,我们可以使用“增益”来实现这些机制。

定义经济增益结构体,并将其添加到 core.move 中城堡数据的经济字段中:

public struct Economy has store {
    treasury: u64,
    base_power: u64,
    settle_time: u64,
    soldier_buff: EconomicBuff,
    battle_buff: vector<EconomicBuff>,
}

需要注意的是,向量的最大大小为1000。

sui::table::Tablesui-framework 提供的一种类似映射的集合类型。与向量类似,表的键和值字段可以是指定类型或泛型类型,但表集合中的所有值和所有键必须是相同的类型。

// 新建空表
let table1 = table::new(&ctx);
// 添加键和值
table::add(&mut table1, 0, false);
// 检索值
let value = table::borrow(&table1, 0);
assert!(value == &false, 0);

表的 new 函数的输入参数 &ctx 是入口函数的事务上下文,表使用它来生成表的 UID。我们将在后面的课程中介绍事务上下文和入口函数。

其他集合类型 Sui 框架提供的各种集合类型介绍可以在官方文档中找到:https://docs.sui.io/concepts/sui-move-concepts/collections

动态字段 - 存储城堡数据

Move 提供了一项独特的功能,允许在 Sui 对象初始构建后添加字段,这超出了类型定义中声明的字段的预定义结构的限制。动态字段具有在运行时添加或删除字段的能力。

有两种类型的动态字段:

  1. 动态字段
  2. 动态对象字段

动态字段

sui::dynamic_field 可以向具有 field id: UID 的 Sui 对象添加任何具有 store 能力的值。

为了说明其用法,让我们深入探讨 Move Castle 游戏的 Game Store 组件。 由于在 Sui 中对象可见性的考虑(将在后续课程中介绍),将每个城堡的某些游戏数据存储在城堡对象中是不切实际的。 这尤其与战斗机制有关,因为战斗机制会影响双方城堡的状态。因此,需要一个集中存储库来存储共享的游戏数据。

core.move 中,我们添加一个 GameStore 结构体:

/// 持有游戏信息
public struct GameStore has key, store {
    id: UID,
    small_castle_count: u64, // 小型城堡数量限制
    middle_castle_count: u64, // 中型城堡数量限制
    big_castle_count: u64, // 大型城堡数量限制
    castle_ids: vector<ID>, // 存储所有城堡对象 ID
}

我们将在未来的课程中将 GameStore 对象发布为共享对象。

在创建城堡对象时,我们需要将城堡游戏数据作为动态字段添加到游戏存储中:

let castle_data = CastleData {...}; // 省略城堡数据构建
dynamic_field::add(&mut game_store.id, castle_object_id, castle_data);

在这种情况下,动态字段的键是城堡对象的 ID,字段值是城堡数据。

要检索城堡数据,可以使用以下代码:

let castle_data = dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, castle_object_id);
(使用动态字段存储城堡数据仅用于教学目的,并不是存储城堡数据的基本方法。)

动态对象字段

类似于动态字段,sui::dynamic_object_field 要求字段值必须是 Sui 对象。这意味着动态对象字段值必须具有 keystore 能力。

对动态对象字段的操作与动态字段类似。主要区别在于,动态对象字段值是具有 id: UID 字段的 Sui 对象。

要获得更全面的理解和详细示例,请参阅以下介绍:Move 中的动态对象字段

02_控制结构 - 课程学习时长~约8分钟

条件语句

在 Sui 的 Move 中,条件结构是常见的 if ... else ... 表达式。有几种使用 if ... else ... 表达式的方法。

单个 if 表达式

if (a > b) {
    c = a + b;
    d = a - b;
};

如果 true 分支仅包含一行,可以简化为:

if (a > b) c = a + b;

带有 else 的 if 表达式

if (a > b) {
    c = a + b;
    d = a - b;
} else {
    e = a + 1;
};

还有不带大括号的简化版本:

if (a > b) c = a + b else e = a + 1;

多分支表达式

if (a < 0) {
    b = b + 5;
} else if (a < 10) {
    b = b + 10;
};

产生值 条件表达式可以产生值:

let x: u64 = if (a > b) c else d;

在这种情况下,表达式的“返回”值(c 和 d)的类型必须与接收器(x: u64)的类型相同。

else 分支是必要的,如果缺少 else 分支,我们将得到类型不兼容的错误,因为缺少的 else 分支默认为 () 而不是 u64。

如果分支中有多行:

let x: u64 = if (a > b) {
    c = a + b;
    c + 1
} else {
    d = a - b;
    d + 1
};

您可能已经注意到,每个分支的最后一行没有以 ; 结尾。这类似于函数中的返回值。

while 和 loop

在 Sui 的 Move 中,循环结构由 while 和 loop 结构提供。

1. while

如果条件块为真,while 结构将重复执行主体。

#[test]
fun while_test() {
    let mut sum = 0;
    let mut counter = 0;
    while (counter < 5) {
        counter = counter + 1;
        sum = sum + 1;
    };
    assert!(sum == 5, 0);
}

在上面的代码示例中,sum 将重复加 1,直到条件 counter < 5 为假;

循环结构也支持 break 和 continue 控制表达式;

1.1 break

break 表达式将强制循环提前终止,而不等待条件表达式为假。

#[test]
fun while_break_test() {
    let mut sum = 0;
    let mut counter = 0;
    while (counter < 5) {
        counter = counter + 1;
        sum = sum + 1;
        if (sum == 3) {
            break;
        }
    };
    assert!(sum == 3, 0);
}

1.2 continue

continue 表达式跳过当前循环的其余部分,并直接开始下一次循环。

#[test]
fun while_continue_test() {
    let mut sum = 0;
    let mut counter = 0;
    while (counter < 5) {
        counter = counter + 1;
        if (counter == 2) {
            continue;
        };
        sum = sum + 1;
    };
    assert!(sum == 4, 0);
}

在示例中,sum 加 1 被跳过了一次。

2. loop

通过 loop 表达式的循环结构是一个无限循环,除非有 break 停止它。

loop {
    sum = sum + 1;
};

等效于

while (true) {
    sum = sum + 1;
};

abort 和 assert

在 Sui 上的事务中,对全局存储的更改是“全有或全无”的。更改只有在区块成功消化后才会被确认。如果在执行期间事务遇到错误,则其中的任何更改都将被丢弃。

除了除以 0 值等意外错误外,开发人员还可以通过表达式 abortassert 主动使事务失败。

1. abort

abort 可以中止当前事务的执行,它接受一个 u64 类型的中止代码作为参数。

abort 66

当函数执行遇到 abort 66 时,事务将被中止并以错误结束:

public entry fun testAbort(demo: &mut Demo, input: u64) {
    if (input > 100) {
        abort 66
    };
    demo.v = demo.v + 1;
}

abort

您可以找到中止代码 66 以及中止发生的函数(testAbort)。

2. assert

assert 表达式也可以通过中止代码中止事务,不同于 abort 表达式,它接受两个参数:一个条件(bool)和一个中止代码(u64)。

assert!(a == b, 66);

注意 assert! 符号,这将 assert 与函数调用区分开来。assert 类似于宏或语法糖,使上面的示例等效于:

if (a == b) {

} else {
    abort 66
};

在测试中尝试:

public entry fun testAssert(demo: &mut Demo, input: u64) {
    assert!(input > 100, 66);
    demo.v = demo.v + 1;
}

abort

您可以看到 testAssert 函数调用以代码 66 中止,错误日志与 abort 表达式的示例相同。

03_函数 - 课程学习时长~约10分钟

函数可见性

我们在前面的课程中已经见过一些使用函数的示例。本节中我们将进一步讨论函数。

一个函数使用 fun 关键字声明,后跟函数名、类型参数、输入参数、返回类型和函数体。

fun my_function<A_Type>(input1: u64, input2: A_Type): (A_Type, bool) {
    input2.x = input2.x + input1;
    (input2, true)
}

函数可以具有不同的可见性:privatepublicpublic(package)

1. private

如果我们没有明确指定,private 是函数的默认可见性。private 函数只能在声明它的模块内部访问。

module move_castle::module_a {
    fun foo(): u64 { 0 }
}

module move_castle::module_b {
    use move_castle::module_a;
    fun call_foo() {
        module_a::foo(); // error[E04001]: restricted visibility
    }
}

2. public

public 函数可以被任何模块中的任何函数调用,如果我们将函数 foo 改为 public,则允许在 call_foo 中调用它。

module move_castle::module_a {
    public fun foo(): u64 { 0 }
}

module move_castle::module_b {
    use move_castle::module_a;
    fun call_foo() {
        module_a::foo(); // 可行
    }
}

3. public(package)

public(package) 修改后的函数只允许在定义它们的同一个包内调用。

初始化函数

在模块发布期间,Sui 为开发人员提供了使用模块初始化器初始化模块状态的支持,也称为“init 函数”。此函数将仅在发布过程中调用一次。

“init 函数”遵循以下约定:

  • 命名为 init
  • 最后一个参数类型为 &mut TxContext
  • 没有返回值
  • 可见性为 private

例如:

module move_castle::core {
    fun init(ctx: &mut TxContext) {
    
    }
}

“init 函数”的典型用法是能力设计模式(Capability Design Pattern)

能力设计模式允许通过“能力”对象授权操作。通过在模块初始化时将能力对象转移到授权地址,可以将需要授权的交互(函数)限制为只能由能力对象的所有者调用。

在我们的 Move Castle 游戏中,我们可以设计一些游戏设置,这些设置在游戏发布后可以调整。更新设置的功能必须只能由游戏管理员调用。

我们可以在 core.move 模块中添加一个 AdminCap 结构体,然后在 init 函数中将一个 AdminCap 对象转移给发布者。

module move_castle::core {
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    
    /// 修改游戏设置的能力
    struct AdminCap has key {
        id: UID
    }

    /// 模块初始化器创建唯一的一个 AdminCap 并将其发送给发布者
    fun init(ctx: &mut TxContext) {
        transfer::transfer(
            AdminCap{id: object::new(ctx)},
            tx_context::sender(ctx)
        );
    }
}

入口函数

入口函数是一个允许从链外世界调用的“入口点”,例如 Sui 客户端 CLI、RPC API 或 SDK。

有两种方式将函数暴露给链外世界:

  • 添加 entry 修饰符
  • 添加 public 修饰符
module move_castle::example {
    use sui::transfer;
    
    public struct NFT has key, store {
        id: UID,
        value: u64
    }
    
    /// 从 `entry` 函数领取一个 NFT 对象
    entry fun claim_from_entry(value: u64, ctx: &mut TxContext) {
        transfer::public_transfer(
            NFT {id: object::new(ctx), value: 0},
            tx_context::sender(ctx)
        );
    }
    
    /// 从 `public` 函数领取一个 NFT 对象
    public(package) fun claim_from_public(value: u64, ctx: &mut TxContext) {
        transfer::public_transfer(
            NFT {id: object::new(ctx), value: 1},
            tx_context::sender(ctx)
        );
    }
}

发布示例模块并使用 Sui 客户端 CLI 调用 claim_from_entryclaim_from_public 函数进行一些测试。

sui client call --package 0xXXX --module example --function xxx ...

除了将函数设为“入口点”,entrypublic 的区别还包括:

  • entry 限制调用必须来自链外世界,而不是其他模块。
  • entry 函数可以有返回值,但其类型必须具有 drop 能力。
  • public 函数可以被其他模块调用。

请访问官方文档 入口函数 以获得更详细的介绍。

参数传递

函数总是带有参数,函数的参数可以是值、不可变引用 & 或可变引用 &mut。您可以在 Move书中找到引用的介绍。

1. 值参数

函数接受的值参数,值必须在函数结束时被消耗。消耗的方式有三种:

  • 传递给另一个函数。
  • 返回。
  • 销毁(丢弃,删除)。 (原始类型的值没有这些问题,因为它们是可复制和可丢弃的。)
fun transfer(castle: Castle, receiver: address) {
    // 传递值参数
    transfer::public_transfer(castle, owner);
}

fun get(castle: Castle): Castle {
    // 返回输入值
    // 这只是为了演示没有实际意义,如果我们想修改一个对象,
    // 通常使用可变引用 &mut。
    castle
}

fun destroy(castle: Castle) {
    // 销毁输入对象。我们将在后面的课程中深入探讨。
    let Castle {id, field1: _, field2: _, field3: _} = castle;
    object::delete(id);
}

fun wrong_use(castle: Castle) {
    // 错误!error[E06001]: 未使用的值没有 'drop'
    castle.level = castle.level + 1;
}

2. 不可变引用 &

不可变引用通常用于读取对象。

public(package) fun get_castle_race(castle: &Castle): u64 {
    castle.race
}

3. 可变引用 &mut

可变引用用于修改对象。

public(package) fun upgrade_castle(castle: &mut Castle) {
    castle.level = castle.level + 1;
}

要将可变引用转换为不可变引用,请使用 freeze(r)

public(package) fun upgrade_castle(castle: &mut Castle) {
    castle.level = castle.level + 1;
    
    let race = get_castle_race(freeze(castle));
    ...
}

04_操作与数学 - 课程学习时长~约13分钟

基本操作

在本课中,我们将学习对原始类型整数和布尔值的基本操作。

本课内容参考自 Move Book 中的整数布尔值章节。

1. 整数

1.1 算术运算

Move 支持整数之间的 +-*/% 运算。以下是一些限制:

  • 两边必须是相同类型。
  • 运算结果不能超出当前类型范围,既不能低于最小值 0(我们只有无符号整数),也不能超过最大值。
  • 不能除以零。
语法操作中止条件
+加法结果对于整数类型来说太大
-减法结果小于零
*乘法结果对于整数类型来说太大
/取整除法除数为 0
%模除法除数为 0

1.2 按位运算

语法操作描述
&按位与对每个位执行布尔与操作
按位或
^按位异或对每个位执行布尔异或操作

1.3 位移运算

如果要移位的位数等于或超过相应无符号整数类型(u8u16u32u64u128u256)的位宽,位移可能会导致中止。

语法操作中止条件
<<左移要移位的位数大于整数类型的大小
>>右移要移位的位数大于整数类型的大小

1.4 比较运算

语法操作
==等于
!=不等于
<小于
>大于
<=小于等于
>=大于等于

1.5 类型转换

一种大小的整数类型可以转换为另一种大小。转换仅限于整数类型,必须强调的是,转换不涉及截断。如果结果超过目标类型的容量,转换操作将导致中止。

// 转换为更大类型
let a: u8 = 8;
let b: u16 = (a as u16);
let c = 100u32 + (a as u32);

2. 布尔值

2.1 逻辑运算

语法描述等价表达式
&&短路逻辑与p && q 等价于 if (p) q else false
!逻辑非!p 等价于 if (p) false else true

Sui 框架简介

在深入了解数学工具之前,有必要先了解 Sui 框架是什么。

Sui 框架是两个核心链上库之一(另一个是 Move 标准库)。Sui 框架提供了一系列便捷的模块,使开发人员能够构建综合应用程序。

是 Sui 框架每个模块的文档及其源代码仓库目录。

如果您使用的是 VSCode + sui-move-analyzer,您可以通过右键单击模块,然后点击“跳转到定义”轻松跳转到 Sui 框架的源代码。

battle math

使用 0x2::math

Move 语言提供了基本的整数算术运算(+-*/%)。Sui 框架中的 sui::math 模块提供了一些其他基本的数学运算,如 powsquare

1. 最大值和最小值

sui::math 包含两个函数,可以轻松从两个 uint64 值中提取最大值或最小值:

sui::math::max(1, 2); // 结果为 2
sui::math::min(1, 2); // 结果为 1

2. 差值

计算绝对值的函数:

sui::math::diff(1, 2); // 结果为 1
sui::math::diff(2, 1); // 结果为 1

3. 平方根和 sqrt_u128

获取 uint64uint128 输入值的最近较小整数平方根:

sui::math::sqrt(9); // 结果为 3
sui::math::sqrt(8); // 结果为 2

8 的平方根接近 2.82,如果您想获得更精确的结果,可以这样尝试:

sui::math::sqrt(8 * 10000); // 结果为 282

然后可以通过 282 / 1000 获得更精确的结果。

4. 向上取整除法

从除法运算中获取向上取整的结果:

sui::math::divide_and_round_up(5, 3); // 结果为 2

请访问 math 模块文档以获取更详细的介绍。

随机生成城堡序列号

我们在之前的课程中已经学习了整数和运算。现在,让我们应用这些知识来生成城堡的序列号。

正如在课程开始时讨论的那样,城堡序列号对城堡的视觉呈现和游戏数据有重大影响。它是关键的,并且在建造城堡时随机分配。

sources 文件夹中创建一个新的模块 move_castle::utils。在这个模块中,包含一个名为 generate_castle_serial_number 的函数。

// sources/utils.move

module move_castle::utils {

    public(package) fun generate_castle_serial_number(size: u64, id: &UID): u64 {
    
    }
}

让我们重新审视序列号的设计:

Logo

第一个数字是“size”,它是用户输入的参数,我们需要生成一个5位整数。所选算法包括从城堡的 UID 哈希值中获取一个5位整数值(使用取模操作)。

// 对城堡的 UID 进行哈希。
let hash = hash::sha2_256(object::uid_to_bytes(id));

生成的哈希是一个长度为32的 vector<u8>,将其转换为 u64 整数:

let result_num: u64 = 0;
// 将哈希向量转换为 u64。
while (vector::length(&hash) > 0) {
    let element = vector::remove(&mut hash, 0);
    result_num = ((result_num << 8) | (element as u64));
};

执行取模操作以仅保留整数的最后5位数字:

// 保留最后5位数字。
result_num = result_num % 100000u64;

最后,将“size”数字连接并返回完整的序列号。整个函数应如下所示:

/// 生成城堡的序列号。
public(package) fun generate_castle_serial_number(size: u64, id: &UID): u64 {
    // 对城堡的 UID 进行哈希。
    //let mut hash = hash::sha2_256(object::uid_to_bytes(id));
    //原教程使用hash::sha2_256但以淘汰,更新至keccak256
    let mut hash = hash::keccak256(&object::uid_to_bytes(id));

    let mut result_num: u64 = 0;
    // 将哈希向量转换为 u64。
    while (vector::length(&hash) > 0) {
        let element = vector::remove(&mut hash, 0);
        result_num = ((result_num << 8) | (element as u64));
    };

    // 保留最后5位数字。
    result_num = result_num % 100000u64;

    // 连接 size 数字。
    size * 100000u64 + result_num
}

添加一些测试并运行它:

// utils_tests.move

#[test_only]
module move_castle::utils_tests {
    use sui::test_scenario::Self;
    use move_castle::utils;
    use std::debug::print;

    #[test]
    fun serial_number_test() {
        let sender = @0xABC;

        let scenario_val = test_scenario::begin(sender);
        let scenario = &mut scenario_val;
        {
            let ctx = test_scenario::ctx(scenario);
            let uid = object::new(ctx);
            let result = utils::generate_castle_serial_number(0, &mut uid);
            print(&result);
            assert!(result >= 0, 0);
            assert!(result < 100000, 0);
            object::delete(uid);
        };

        test_scenario::end(scenario_val);
    }
}

03_Sui对象

  • 本课程共三章节,总学习时长约24分钟

目录: - 对象基础 -定义对象 -创建对象 -储存对象 -删除对象 - 对象所有权 -归属对象 -共享对象 -转移 - 对象展示 -对象展示

01_对象基础 - 课程学习时长~约12分钟

定义对象

Sui 对象是 Sui 生态系统的基本构建块,它们赋予 Sui 动态灵活性、安全的所有权、强大的能力和极速的交易。

要深入了解 Sui 对象,我们首先定义一个对象。在我们的游戏中,城堡代表这样一个对象——它具有独特性、所有权和独特的属性。

虽然我们之前在“自定义类型和能力”课程中介绍了 Castle 结构体,但在本课中,我们需要再次结构化 Castle 结构体,以了解有关结构体的详细信息。

让我们通过使用 struct 关键字来结构化城堡,开始探索 Sui 对象。

public struct Castle {

}

城堡是独特的并且存储在 Sui 上,所以它具有 keystore 能力。

public struct Castle has key, store {
    id: UID,
}

城堡如设计所示具有序列号,当然,它也有名称和描述。

public struct Castle has key, store {
    id: UID,
    name: String,
    description: String,
    serial_number: u64,
}

到目前为止,已经定义了一个简单的城堡结构。我们将在课程进展过程中根据需要添加一些其他属性。

castle.move 中:

module move_castle::castle{
    use std::string::{Self, String};

    /// The castle.
    public struct Castle has key, store {
        id: UID,
        name: String,
        description: String,
        serial_number: u64,
    }

}

创建对象

现在我们有了 Castle 结构体,让我们在本课中创建一个城堡对象。

首先在 castle.move 中添加 build_castle 入口函数。

entry fun build_castle(size: u64, name_bytes: vector<u8>, desc_bytes: vector<u8>, ctx: &mut TxContext) {

}

输入参数:

  • size: 城堡大小,1 - 小型,2 - 中型,3 - 大型。
  • name_bytes: 以字节形式表示的城堡名称。类型 u8 向量是接受链外调用的字符串的方式。
  • desc_bytes: 以字节形式表示的城堡描述。
  • ctx: 当前事务上下文。

城堡对象具有唯一 ID,可以通过 sui::object::new 函数创建。

let obj_id = object::new(ctx);

然后我们需要生成城堡的序列号。调用我们在前一课中编写的函数。

use move_castle::utils;

let serial_number = utils::generate_castle_serial_number(size, &obj_id);

现在我们有了城堡需要的所有属性,创建城堡对象。

use std::string::{Self, utf8, String};

let castle = Castle {
    id: obj_id,
    name: string::utf8(name_bytes),
    description: string::utf8(desc_bytes),
    serial_number: serial_number,
};

最后,将城堡对象转移给事务发送者(城堡所有者)。

use sui::transfer;

let owner = tx_context::sender(ctx);
transfer::public_transfer(castle, owner);

一个完整的 build_castle 函数

存储对象

Sui 使用自己的以对象为中心的全局存储来避免扩展问题。 Sui 的存储是基于对象的,而不是基于使用键值对等数据结构的账户。

对象使用唯一标识符并支持并行事务。标识符表示为地址,一个 32 字节的标识符。对象的地址包装在 id: UID 中。 key 能力表示对象结构体的第一个字段是 id: UID,确保唯一地址。

要存储一个对象(的值),在 Sui 的 Move 中,使用 store 能力标记将存储在 Sui 链上存储中的对象。

public struct StoredObject has store {
    v: u64,
}

以下是关于 store 能力的一些规则:

  • 具有 store 能力的结构体,其所有嵌套结构体也需要具有 store 能力。
  • 具有 key 能力的结构体需要具有 store 能力。

删除对象

删除对象分为两个步骤:

  1. 解包对象并检索其 ID。
  2. 删除对象 ID。

1. 解包

有一种特定的方法来解包对象:

public struct Demo has key, store {
    id: UID,
    value: u64,
}

entry fun destroy(demo: Demo) {
    // 1. 解包对象。
    let Demo {id, value: _} = demo;
}

在解包时检索对象 ID,_ 符号表示忽略从解包中接收 value 字段。_ 还通常用于忽略多值返回函数的返回值:

// 计算和与积。
fun calculate(a: u64, b:u64): (u64, u64) {
    (a + b, a * b)
}

fun call() {
    // 只想使用 calculate 函数结果中的和。
    let (sum, _) = calculate(1, 2);
}

2. 删除对象 ID

由于我们从解包中检索到了对象 ID,使用 object::delete 删除 ID,然后 Demo 对象就被删除了。

完整的删除对象函数应为:

entry fun destroy(demo: Demo) {
    // 1. 解包对象。
    let Demo {id, value: _} = demo;
    // 2. 删除对象 ID。
    object::delete(id);
}

02_对象所有权 - 课程学习时长~约6分钟

归属对象

从所有权的角度来看,Sui 对象可以分为两种类型:

  • 归属对象
  • 共享对象

一个归属对象的所有者可以是一个地址或另一个对象。

1. 由地址拥有

当我们将一个新建的对象转移到一个地址时,该对象将由该地址拥有。

示例是我们在上一课“创建对象”中使用的代码:

// 将城堡对象转移给所有者。
let owner = tx_context::sender(ctx);
transfer::public_transfer(castle, owner);

由地址拥有的对象不能被除所有者之外的其他人修改或删除。只有城堡的所有者可以将城堡转移给其他人或更新城堡的属性。

2. 由对象拥有

当使用 dynamic_fielddynamic_object_field 时,会出现对象所有的对象。 例如,在课程“动态字段 - 存储城堡数据”中,我们使用 sui::dynamic_field 将城堡数据存储到 GameStore 中。 当一个城堡被建造时,其对应的 CastleData 将由一个类型为 dynamic_field::Field 的对象包装,并由 GameStore 对象拥有。

构建城堡时创建的不同类型的对象: object

共享对象

在所有权中,另一种类型的对象是共享对象,共享对象没有专属所有者。

1. 共享不可变对象

已发布的包是一个不可变对象,您可以手动将对象设为不可变:

transfer::freeze_object(obj);

2. 共享可变对象

共享可变对象可以被任何人修改。

正如我们在上一课中提到的,一个由地址拥有的对象除了其所有者之外不能被修改。 在我们的城堡游戏中,我们有涉及不同所有者的城堡的战斗机制。 如果我们将城堡游戏数据放在城堡对象中,我们无法在“战斗”互动的战斗结算中修改对手的城堡。因此,我们需要利用可变对象。

core.move 中找到我们之前创建的 GameStore 结构体。

/// 存储游戏信息
public struct GameStore has key, store {
    id: UID,
    small_castle_count: u64, // 小型城堡数量限制
    middle_castle_count: u64, // 中型城堡数量限制
    big_castle_count: u64, // 大型城堡数量限制
    castle_ids: vector<ID> // 存储所有城堡对象 ID
}

init 函数中初始化并将其设为共享:

fun init(ctx: &mut TxContext) {
    use std::vector;
    ...

    transfer::share_object(
        GameStore{
            id: object::new(ctx),
            small_castle_count: 0,
            middle_castle_count: 0,
            big_castle_count: 0,
            castle_ids: vector::empty<ID>()
        }
    );
}

发布包后,我们可以在控制台中找到创建的共享 GameStore 对象。 object

转移

归属对象可以由所有者转移到另一个地址。

sui::transfer::transfer(obj, recipient);

我们可以提供一个转移入口函数,供玩家将城堡转移给其他人。

castle.move 中创建一个 transfer_castle 入口函数:

#[allow(lint(custom_state_change))]
/// 转移城堡
entry fun transfer_castle(castle: Castle, to: address) {
    transfer::transfer(castle, to);
}

03_对象展示 - 课程学习时长~约6分钟

对象展示

Sui 对象展示是一种标准,它使链上资产的链外可视化表示成为可能。这在 NFT 项目中非常有用。

在启用结构体的展示之前,需要一个归属的发布者(Publisher)对象。发布者是“一次性见证”(One-Time Witness)模式的实现, 我们将在后续课程中介绍更多细节。查看发布者主题以了解更多信息。

sui::display::Display<T> 使用一组带有对象属性占位符的命名模板,例如:

{
    "name": "{name}",
    "image_url": "https://xxx/{image_id}"
}

nameimage_id 必须是对象类型 T 的属性。

官方文档建议的属性包括:

  • name - 对象名称。
  • description - 对象描述。
  • link - 项目中的对象链接。
  • image_url - 对象的视觉图像 URL。
  • thumbnail_url - 用作预览的小图像的 URL。
  • project_url - 项目链接。
  • creator - 对象创建者。

在我们的 Move Castle 游戏中,模板应为:

{
    "name": "{name}",
    "link": "https://movecastle.info/castles/{serial_number}",
    "image_url": "https://images.movecastle.info/static/media/castles/{image_id}.png",
    "description": "{description}",
    "project_url": "https://movecastle.info",
    "creator": "Castle Builder"
}

城堡图像是预生成的,托管在我们的项目网站 "https://movecastle.info" 下。image_id 是城堡序列号的视觉部分。

utils.move 模块中添加一个 serial_number_to_image_id 函数,以从序列号中检索图像 ID:

use std::string::{Self, String};

public(package) fun serial_number_to_image_id(serial_number: u64): String {
    let id = serial_number / 10 % 10000u64;
    u64_to_string(id, 4)
}

/// 将 u64 转换为字符串,如果长度小于固定长度,则在前面加 "0"
public(package) fun u64_to_string(n: u64, fixed_length: u64): String {
    let mut result: vector<u8> = vector::empty<u8>();
    if (n == 0) {
        vector::push_back(&mut result, 48);
    } else {
        while (n > 0) {
            let digit = ((n % 10) as u8) + 48;
            vector::push_back(&mut result, digit);
            n = n / 10;
        };

        // 在字符串前面添加 "0" 直到达到固定长度。
        while (vector::length(&result) < fixed_length) {
            vector::push_back(&mut result, 48);
        };

        vector::reverse<u8>(&mut result);
    };
    string::utf8(result)
}

这个函数从序列号中提取中间的 4 位数字。

castle.move 中,我们需要添加 OTW 逻辑,并声明 Publisher 对象:

module move_castle::castle {
    use sui::package;

    /// 模块的一次性见证,它必须是模块中的第一个结构体,
    /// 并且它的名称应该与模块名称相同,但全部大写。
    public struct CASTLE has drop {}
    
    fun init(otw: CASTLE, ctx: &mut TxContext) {
        let publisher = package::claim(otw, ctx);
        transfer::public_transfer(publisher, tx_context::sender(ctx));
    }

}

然后我们需要新建一个 Display<Castle> 对象,因此 castle.move 中的整个 init 函数应为:

module move_castle::castle {
    use std::string::{Self, utf8, String};
    use sui::package;
    use sui::display;
    
    /// 模块的一次性见证,它必须是模块中的第一个结构体,
    /// 并且它的名称应该与模块名称相同,但全部大写。
    public struct CASTLE has drop {}
    
    fun init(otw: CASTLE, ctx: &mut TxContext) {
        let keys = vector[
            utf8(b"name"),
            utf8(b"link"),
            utf8(b"image_url"),
            utf8(b"description"),
            utf8(b"project_url"),
            utf8(b"creator"),
        ];

        let values = vector[
            utf8(b"{name}"),
            utf8(b"https://movecastle.info/castles/{serial_number}"),
            utf8(b"https://images.movecastle.info/static/media/castles/{image_id}.png"),
            utf8(b"{description}"),
            utf8(b"https://movecastle.info"),
            utf8(b"Castle Builder"),
        ];

        let publisher = package::claim(otw, ctx);
        let mut display = display::new_with_fields<Castle>(&publisher, keys, values, ctx);

        display::update_version(&mut display);

        transfer::public_transfer(publisher, tx_context::sender(ctx));
        transfer::public_transfer(display, tx_context::sender(ctx));
    }
}

别忘了在 Castle 结构体中添加 image_id 属性:

module move_castle::castle {

    /// 城堡
    public struct Castle has key, store {
        id: UID,
        name: String,
        description: String,
        serial_number: u64,
        image_id: String,
    }
    
    /// 创建新城堡
    entry fun build_castle(...) {
        ...
    
        // 生成序列号和图像 ID
        let serial_number = utils::generate_castle_serial_number(size, &obj_id);
        let image_id = utils::serial_number_to_image_id(serial_number);
        
        // 新建城堡
        let castle = Castle {
            id: obj_id,
            name: string::utf8(name_bytes),
            description: string::utf8(desc_bytes),
            serial_number: serial_number,
            image_id: image_id,
        };
        
        ...
    }
}

将您的包部署到测试网开发网,建造一个城堡并在 Sui Explorer 上查看。

04_完成城堡项目

  • 本课程共四章节,总学习时长约38分钟

目录: - 准备城堡游戏数据 -准备城堡游戏数据 - 数学 -时间经济系统 -准备战斗 -随机敌人 -战斗结算 -升级 - 改进 -自定义错误 -发布事件 - 发布与调用 -发布与调用

01_准备城堡游戏数据 - 课程学习时长~约6分钟

准备城堡游戏数据

在学习了 Move 和 Sui 的基础知识后,本章将引导我们完成 Move Castle 游戏。

在深入了解游戏机制核心逻辑的数学之前,我们首先需要为 Move Castle 游戏准备游戏数据。 我们在之前的课程中已经介绍了 GameStoreCastleData,现在,我们只需要在建造城堡时初始化它们。

要确定城堡的初始游戏数据(例如攻击力和防御力),需要依赖于城堡的大小和种族。在 core.move 模块中添加一个 get_initial_attack_defense_power 函数:

/// 通过种族获取初始攻击力和防御力
fun get_initial_attack_defense_power(race: u64): (u64, u64) {
    let (attack, defense);

    if (race == 0) {
        (attack, defense) = (1000, 1000);
    } else if (race == 1) {
        (attack, defense) = (500, 1500);
    } else if (race == 2) {
        (attack, defense) = (1500, 500);
    } else if (race == 3) {
        (attack, defense) = (1200, 800);
    } else if (race == 4) {
        (attack, defense) = (800, 1200);
    } else {
        abort 0
    };

    (attack, defense)
}

代码中的数字源自游戏数据设计。然而,很明显,这段代码的实现充满了“魔法数字”,使审阅者或项目维护者在阅读代码时难以理解其含义。 一个广泛采用的良好实践是将这些数字定义为常量

/// 通过种族获取初始攻击力和防御力
fun get_initial_attack_defense_power(race: u64): (u64, u64) {
    let (attack, defense);

    if (race == CASTLE_RACE_HUMAN) {
        (attack, defense) = (INITIAL_ATTCK_POWER_HUMAN, INITIAL_DEFENSE_POWER_HUMAN);
    } else if (race == CASTLE_RACE_ELF) {
        (attack, defense) = (INITIAL_ATTCK_POWER_ELF, INITIAL_DEFENSE_POWER_ELF);
    } else if (race == CASTLE_RACE_ORCS) {
        (attack, defense) = (INITIAL_ATTCK_POWER_ORCS, INITIAL_DEFENSE_POWER_ORCS);
    } else if (race == CASTLE_RACE_GOBLIN) {
        (attack, defense) = (INITIAL_ATTCK_POWER_GOBLIN, INITIAL_DEFENSE_POWER_GOBLIN);
    } else if (race == CASTLE_RACE_UNDEAD) {
        (attack, defense) = (INITIAL_ATTCK_POWER_UNDEAD, INITIAL_DEFENSE_POWER_UNDEAD);
    } else {
        abort 0
    };

    (attack, defense)
}

/// 城堡种族 - 人类
const CASTLE_RACE_HUMAN : u64 = 0;
/// 城堡种族 - 精灵
const CASTLE_RACE_ELF : u64 = 1;
/// 城堡种族 - 兽人
const CASTLE_RACE_ORCS : u64 = 2;
/// 城堡种族 - 哥布林
const CASTLE_RACE_GOBLIN : u64 = 3;
/// 城堡种族 - 僵尸
const CASTLE_RACE_UNDEAD : u64 = 4;

/// 初始攻击力 - 人类城堡
const INITIAL_ATTCK_POWER_HUMAN : u64 = 1000;
/// 初始攻击力 - 精灵城堡
const INITIAL_ATTCK_POWER_ELF : u64 = 500;
/// 初始攻击力 - 兽人城堡
const INITIAL_ATTCK_POWER_ORCS : u64 = 1500;
/// 初始攻击力 - 哥布林城堡
const INITIAL_ATTCK_POWER_GOBLIN : u64 = 1200;
/// 初始攻击力 - 僵尸城堡
const INITIAL_ATTCK_POWER_UNDEAD : u64 = 800;

/// 初始防御力 - 人类城堡
const INITIAL_DEFENSE_POWER_HUMAN : u64 = 1000;
/// 初始防御力 - 精灵城堡
const INITIAL_DEFENSE_POWER_ELF : u64 = 1500;
/// 初始防御力 - 兽人城堡
const INITIAL_DEFENSE_POWER_ORCS : u64 = 500;
/// 初始防御力 - 哥布林城堡
const INITIAL_DEFENSE_POWER_GOBLIN : u64 = 800;
/// 初始防御力 - 僵尸城堡
const INITIAL_DEFENSE_POWER_UNDEAD : u64 = 1200;

同样,初始经济实力依赖于城堡的大小,在同一个模块中添加 get_initial_economic_power 函数:

// 通过城堡大小获取初始经济实力
fun get_initial_economic_power(size: u64): u64 {
    let power;
    if (size == CASTLE_SIZE_SMALL) {
        power = INITIAL_ECONOMIC_POWER_SMALL_CASTLE;
    } else if (size == CASTLE_SIZE_MIDDLE) {
        power = INITIAL_ECONOMIC_POWER_MIDDLE_CASTLE;
    } else if (size == CASTLE_SIZE_BIG) {
        power = INITIAL_ECONOMIC_POWER_BIG_CASTLE;
    } else {
        abort 0
    };
    power
}

/// 城堡大小 - 小型
const CASTLE_SIZE_SMALL : u64 = 1;
/// 城堡大小 - 中型
const CASTLE_SIZE_MIDDLE : u64 = 2;
/// 城堡大小 - 大型
const CASTLE_SIZE_BIG : u64 = 3;

/// 初始经济实力 - 小型城堡
const INITIAL_ECONOMIC_POWER_SMALL_CASTLE : u64 = 100;
/// 初始经济实力 - 中型城堡
const INITIAL_ECONOMIC_POWER_MIDDLE_CASTLE : u64 = 150;
/// 初始经济实力 - 大型城堡
const INITIAL_ECONOMIC_POWER_BIG_CASTLE : u64 = 250;

别忘了初始化城堡的总攻击/防御力,它由基础力量和士兵的力量组成。士兵的攻击力和防御力取决于他们的种族,要获得单个士兵的力量,在 core.move 中添加这个函数:

/// 城堡单个士兵的攻击力和防御力
public(package) fun get_castle_soldier_attack_defense_power(race: u64): (u64, u64) {
    let soldier_attack_power;
    let soldier_defense_power;
    if (race == CASTLE_RACE_HUMAN) {
        soldier_attack_power = SOLDIER_ATTACK_POWER_HUMAN;
        soldier_defense_power = SOLDIER_DEFENSE_POWER_HUMAN;
    } else if (race == CASTLE_RACE_ELF) {
        soldier_attack_power = SOLDIER_ATTACK_POWER_ELF;
        soldier_defense_power = SOLDIER_DEFENSE_POWER_ELF;
    } else if (race == CASTLE_RACE_ORCS) {
        soldier_attack_power = SOLDIER_ATTACK_POWER_ORCS;
        soldier_defense_power = SOLDIER_DEFENSE_POWER_ORCS;
    } else if (race == CASTLE_RACE_GOBLIN) {
        soldier_attack_power = SOLDIER_ATTACK_POWER_GOBLIN;
        soldier_defense_power = SOLDIER_DEFENSE_POWER_GOBLIN;
    } else if (race == CASTLE_RACE_UNDEAD) {
        soldier_attack_power = SOLDIER_ATTACK_POWER_UNDEAD;
        soldier_defense_power = SOLDIER_DEFENSE_POWER_UNDEAD;
    } else {
        abort 0
    };

    (soldier_attack_power, soldier_defense_power)
}

/// 通过种族和士兵数量获取初始士兵的攻击力和防御力
fun get_initial_soldiers_attack_defense_power(race: u64, soldiers: u64): (u64, u64) {
    let (attack, defense) = get_castle_soldier_attack_defense_power(race);
    (attack * soldiers, defense * soldiers)
}

/// 士兵攻击力 - 人类
const SOLDIER_ATTACK_POWER_HUMAN : u64 = 100;
/// 士兵防御力 - 人类
const SOLDIER_DEFENSE_POWER_HUMAN : u64 = 100;
/// 士兵攻击力 - 精灵
const SOLDIER_ATTACK_POWER_ELF : u64 = 50;
/// 士兵防御力 - 精灵
const SOLDIER_DEFENSE_POWER_ELF : u64 = 150;
/// 士兵攻击力 - 兽人
const SOLDIER_ATTACK_POWER_ORCS : u64 = 150;
/// 士兵防御力 - 兽人
const SOLDIER_DEFENSE_POWER_ORCS : u64 = 50;
/// 士兵攻击力 - 哥布林
const SOLDIER_ATTACK_POWER_GOBLIN : u64 = 120;
/// 士兵防御力 - 哥布林
const SOLDIER_DEFENSE_POWER_GOBLIN : u64 = 80;
/// 士兵攻击力 - 僵尸
const SOLDIER_ATTACK_POWER_UNDEAD : u64 = 120;
/// 士兵防御力 - 僵尸
const SOLDIER_DEFENSE_POWER_UNDEAD : u64 = 80;

然后我们在 core.move 模块中添加一个 init_castle_data 函数,并初始化 CastleData 对象:

/// 初始化城堡数据
public(package) fun init_castle_data(id: ID,
                            size: u64,
                            race: u64,
                            current_timestamp: u64,
                            game_store: &mut GameStore) {
    // 1. 获取初始力量并初始化城堡数据
    let (attack_power, defense_power) = get_initial_attack_defense_power(race);
    let (soldiers_attack_power, soldiers_defense_power) = get_initial_soldiers_attack_defense_power(race, INITIAL_SOLDIERS);
    let castle_data = CastleData {
        id: id,
        size: size,
        race: race,
        level: 1,
        experience_pool: 0,
        economy: Economy {
            treasury: 0,
            base_power: get_initial_economic_power(size),
            settle_time: current_timestamp,
            soldier_buff: EconomicBuff {
                debuff: false,
                power: SOLDIER_ECONOMIC_POWER * INITIAL_SOLDIERS,
                start: current_timestamp,
                end: 0
            },
            battle_buff: vector::empty<EconomicBuff>()
        },
        millitary: Millitary {
            attack_power: attack_power,
            defense_power: defense_power,
            total_attack_power: attack_power + soldiers_attack_power,
            total_defense_power: defense_power + soldiers_defense_power,
            soldiers: INITIAL_SOLDIERS,
            battle_cooldown: current_timestamp
        }
    };
}

/// 初始士兵数量
const INITIAL_SOLDIERS : u64 = 10;
/// 士兵经济力量
const SOLDIER_ECONOMIC_POWER : u64 = 1;

还记得我们之前学过的动态字段吗?让我们通过使用 动态字段 将城堡数据存储在游戏存储对象下面来实践它:

use sui::dynamic_field;

// 2. 存储城堡数据
dynamic_field::add(&mut game_store.id, id, castle_data);

还需要更新其他一些游戏数据:

// 3. 更新城堡 ID 和城堡数量
vector::push_back(&mut game_store.castle_ids, id);
if (size == CASTLE_SIZE_SMALL) {
    game_store.small_castle_count = game_store.small_castle_count + 1;
} else if (size == CASTLE_SIZE_MIDDLE) {
    game_store.middle_castle_count = game_store.middle_castle_count + 1;
} else if (size == CASTLE_SIZE_BIG) {
    game_store.big_castle_count = game_store.big_castle_count + 1;
} else {
    abort 0
};

因此,整个 init_castle_data 函数如下:

/// 初始化城堡数据
public(package) fun init_castle_data(id: ID,
                            size: u64,
                            race: u64,
                            current_timestamp: u64,
                            game_store: &mut GameStore) {
    // 1. 获取初始力量并初始化城堡数据
    let (attack_power, defense_power) = get_initial_attack_defense_power(race);
    let (soldiers_attack_power, soldiers_defense_power) = get_initial_soldiers_attack_defense_power(race, INITIAL_SOLDIERS);
    let castle_data = CastleData {
        id: id,
        size: size,
        race: race,
        level: 1,
        experience_pool: 0,
        economy: Economy {
            treasury: 0,
            base_power: get_initial_economic_power(size),
            settle_time: current_timestamp,
            soldier_buff: EconomicBuff {
                debuff: false,
                power: SOLDIER_ECONOMIC_POWER * INITIAL_SOLDIERS,
                start: current_timestamp,
                end: 0
            },
            battle_buff: vector::empty()
        },
        millitary: Millitary {
            attack_power: attack_power,
            defense_power: defense_power,
            total_attack_power: attack_power + soldiers_attack_power,
            total_defense_power: defense_power + soldiers_defense_power,
            soldiers: INITIAL_SOLDIERS,
            battle_cooldown: current_timestamp
        }
    };

    // 2. 存储城堡数据
    dynamic_field::add(&mut game_store.id, id, castle_data);

    // 3. 更新城堡 ID 和城堡数量
    vector::push_back(&mut game_store.castle_ids, id);
    if (size == CASTLE_SIZE_SMALL) {
        game_store.small_castle_count = game_store.small_castle_count + 1;
    } else if (size == CASTLE_SIZE_MIDDLE) {
        game_store.middle_castle_count = game_store.middle_castle_count + 1;
    } else if (size == CASTLE_SIZE_BIG) {
        game_store.big_castle_count = game_store.big_castle_count + 1;
    } else {
        abort 0
    };
}

在建造城堡时初始化城堡数据,在 castle.move 中的 build_castle 函数中添加以下逻辑:

use sui::clock::{Self, Clock};
use move_castle::core::{Self, GameStore};

/// 建造城堡
entry fun build_castle(size: u64, name_bytes: vector<u8>, desc_bytes: vector<u8>, clock: &Clock, game_store: &mut GameStore, ctx: &mut TxContext) {
    ...
    // 新的城堡对象
    let castle = Castle {...

    // 新的城堡游戏数据
    let id = object::uid_to_inner(&castle.id);
    let race = get_castle_race(serial_number);
    core::init_castle_data(
        id, 
        size,
        race,
        clock::timestamp_ms(clock),
        game_store
    );

    // 将城堡对象转移给所有者
    ...
}

/// 获取城堡种族
public fun get_castle_race(serial_number: u64): u64 {
    let mut race_number = serial_number % 10;
    if (race_number >= 5) {
        race_number = race_number - 5;
    };
    race_number
}

02_数学 - 课程学习时长~约23分钟

时间获取收益 - 基于时间的经济系统

在本课中,我们将实现游戏的经济系统。

经济系统的介绍可以简化为:

treasury = last_settlement_treasury + economic_power * (current_time - last_settlement_time)

金库目前在游戏设计中只能用于招募士兵。

经济实力代表城堡在单位时间内积累的金库数量。经济实力有两种类型:城堡基础经济实力和额外经济实力。 基础经济实力取决于城堡的大小和等级,随着城堡升级而增加。而额外经济实力受士兵和战斗效果的影响。 士兵提供正面的额外经济实力,赢得战斗也会带来正面的力量加成。然而,失败的战斗会导致负面的力量加成。因此,额外经济实力也可以实现为“增益”。

链上没有触发器可以在特定时期自动收集每个城堡的金库,因此城堡的经济需要手动结算。 然而,在两次结算之间,由于城堡升级、士兵招募或参与战斗,城堡的“总”经济实力会发生变化。这使得新的结算计算变得复杂。

如前所述,士兵和战斗的额外经济实力实现为“增益”。一个增益包含其生效时间的信息。 在结算期间,基础经济实力贡献的金库和每个增益在其区间与结算区间的交集内的贡献都要计算在内。

处理基础经济实力变化的问题相对简单。可以在城堡升级时触发自动结算。由于升级是手动行为,这是结算基础经济实力的良好时机。

首先,计算一段时间内的经济收益是简单的,在 core.move 中:

use sui::math;

/// 根据力量和时间段(1 分钟)计算经济收益。
fun calculate_economic_benefits(start: u64, end: u64, power: u64): u64 {
    math::divide_and_round_up((end - start) * power, 60u64 * 1000u64)
}

代码中的输入 startend 是以毫秒为单位的时间戳。要获取当前时间戳,我们需要使用部署在 0x6Clock 对象。 在 core.move 中创建一个 settle_castle_economy_inner 函数:

/// 结算城堡的经济,内部方法
public(package) fun settle_castle_economy_inner(clock: &Clock, castle_data: &mut CastleData) {
    use sui::clock::{Self, Clock};

    let current_timestamp = clock::timestamp_ms(clock);
}

现在尝试计算基础经济实力的经济收益,并更新 CastleData

// 1. 计算基础力量收益
let base_benefits = calculate_economic_benefits(castle_data.economy.settle_time, current_timestamp, castle_data.economy.base_power);
castle_data.economy.treasury = castle_data.economy.treasury + base_benefits;
castle_data.economy.settle_time = current_timestamp;

接着计算士兵增益:

// 2. 计算士兵增益
let soldier_benefits = calculate_economic_benefits(castle_data.economy.soldier_buff.start, current_timestamp, castle_data.economy.soldier_buff.power);
castle_data.economy.treasury = castle_data.economy.treasury + soldier_benefits;
castle_data.economy.soldier_buff.start = current_timestamp;

士兵数量也可以在招募和战斗时发生变化,但我们不会维护多个士兵增益,因此在招募和战斗结算前也需要进行经济结算。

计算战斗增益是复杂的,有多个战斗增益,并且可以是正面或负面的:

// 3. 计算战斗增益
if (!vector::is_empty(&castle_data.economy.battle_buff)) {
    let length = vector::length(&castle_data.economy.battle_buff);
    let mut expired_buffs = vector::empty<u64>();
    let mut i = 0;
    while (i < length) {
        let buff = vector::borrow_mut(&mut castle_data.economy.battle_buff, i);
        let mut battle_benefit;
        if (buff.end <= current_timestamp) {
            vector::push_back(&mut expired_buffs, i);
            battle_benefit = calculate_economic_benefits(buff.start, buff.end, buff.power);
        } else {
            battle_benefit = calculate_economic_benefits(buff.start, current_timestamp, buff.power);
            buff.start = current_timestamp;
        };
        if (buff.debuff) {
            castle_data.economy.treasury = castle_data.economy.treasury - battle_benefit;
        } else {
            castle_data.economy.treasury = castle_data.economy.treasury + battle_benefit;
        };
        i = i + 1;
    };
    // 移除过期增益
    while(!vector::is_empty(&expired_buffs)) {
        let expired_buff_index = vector::remove(&mut expired_buffs, 0);
        vector::remove(&mut castle_data.economy.battle_buff, expired_buff_index);
    };
    vector::destroy_empty<u64>(expired_buffs);
}

完成了,完整的经济结算函数应为:

// core.move

/// 结算城堡的经济,内部方法
public(package) fun settle_castle_economy_inner(clock: &Clock, castle_data: &mut CastleData) {
    let current_timestamp = clock::timestamp_ms(clock);

    // 1. 计算基础力量收益
    let base_benefits = calculate_economic_benefits(castle_data.economy.settle_time, current_timestamp, castle_data.economy.base_power);
    castle_data.economy.treasury = castle_data.economy.treasury + base_benefits;
    castle_data.economy.settle_time = current_timestamp;

    // 2. 计算士兵增益
    let soldier_benefits = calculate_economic_benefits(castle_data.economy.soldier_buff.start, current_timestamp, castle_data.economy.soldier_buff.power);
    castle_data.economy.treasury = castle_data.economy.treasury + soldier_benefits;
    castle_data.economy.soldier_buff.start = current_timestamp;

    // 3. 计算战斗增益
    if (!vector::is_empty(&castle_data.economy.battle_buff)) {
        let length = vector::length(&castle_data.economy.battle_buff);
        let mut expired_buffs = vector::empty<u64>();
        let mut i = 0;
        while (i < length) {
            let buff = vector::borrow_mut(&mut castle_data.economy.battle_buff, i);
            let mut battle_benefit;
            if (buff.end <= current_timestamp) {
                vector::push_back(&mut expired_buffs, i);
                battle_benefit = calculate_economic_benefits(buff.start, buff.end, buff.power);
            } else {
                battle_benefit = calculate_economic_benefits(buff.start, current_timestamp, buff.power);
                buff.start = current_timestamp;
            };

            if (buff.debuff) {
                castle_data.economy.treasury = castle_data.economy.treasury - battle_benefit;
            } else {
                castle_data.economy.treasury = castle_data.economy.treasury + battle_benefit;
            };
            i = i + 1;
        };

        // 移除过期增益
        while(!vector::is_empty(&expired_buffs)) {
            let expired_buff_index = vector::remove(&mut expired_buffs, 0);
            vector::remove(&mut castle_data.economy.battle_buff, expired_buff_index);
        };
        vector::destroy_empty<u64>(expired_buffs);
    }
}    

最后,在当前模块中添加一个封装函数,并在 castle.move 中添加一个入口函数:

// core.move

/// 结算城堡的经济,包括胜利奖励和失败惩罚
public(package) fun settle_castle_economy(id: ID, clock: &Clock, game_store: &mut GameStore) {
    settle_castle_economy_inner(clock, dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, id));
}

// castle.move

/// 结算城堡的经济
entry fun settle_castle_economy(castle: &Castle, clock: &Clock, game_store: &mut GameStore) {
    core::settle_castle_economy(object::id(castle), clock, game_store);
}

准备战斗了吗? - 招募你的士兵

在上一课中,我们介绍了金库的概念。目前,在游戏设计中,金库的唯一用途是招募士兵。

士兵的作用如下:

额外的经济实力。 额外的攻击力。 额外的防御力。 根据游戏机制,城堡可以容纳士兵的数量上限取决于其大小。在 core.move 中:

/// 每个城堡的最大士兵数 - 小城堡
const MAX_SOLDIERS_SMALL_CASTLE : u64 = 500;
/// 每个城堡的最大士兵数 - 中城堡
const MAX_SOLDIERS_MIDDLE_CASTLE : u64 = 1000;
/// 每个城堡的最大士兵数 - 大城堡
const MAX_SOLDIERS_BIG_CASTLE : u64 = 2000;

添加一个函数以便更方便地获取限制:

/// 根据城堡大小获取士兵限制
fun get_castle_soldier_limit(size: u64) : u64 {
    let soldier_limit;
    if (size == CASTLE_SIZE_SMALL) {
        soldier_limit = MAX_SOLDIERS_SMALL_CASTLE;
    } else if (size == CASTLE_SIZE_MIDDLE) {
        soldier_limit = MAX_SOLDIERS_MIDDLE_CASTLE;
    } else if (size == CASTLE_SIZE_BIG) {
        soldier_limit = MAX_SOLDIERS_BIG_CASTLE;
    } else {
        abort 0
    };
    soldier_limit
}

core.move 中创建 recruit_soldiers 函数:

/// 城堡使用金库招募士兵
public(package) fun recruit_soldiers (id: ID, count: u64, clock: &Clock, game_store: &mut GameStore) {
    
}

并在 castle.move 中添加调用入口:

/// 城堡使用金库招募士兵
entry fun recruit_soldiers(castle: &Castle, count: u64, clock: &Clock, game_store: &mut GameStore) {
    core::recruit_soldiers(object::id(castle), count, clock, game_store);
}

首先需要从游戏存储中获取城堡数据:

// 1. 借用城堡数据
let castle_data = dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, id);

然后检查士兵数量限制:

// 2. 检查数量限制
let final_soldiers = castle_data.millitary.soldiers + count;
assert!(final_soldiers <= get_castle_soldier_limit(castle_data.size), 0);

并检查金库是否充足:

/// 每个士兵的价格
const SOLDIER_PRICE : u64 = 100;

// 3. 检查金库是否充足
let total_soldier_price = SOLDIER_PRICE * count;
assert!(castle_data.economy.treasury >= total_soldier_price, 0);

不要忘记在更新士兵之前结算经济。

// 4. 结算经济
settle_castle_economy_inner(clock, castle_data);

然后更新金库和士兵:

// 5. 更新金库和士兵
castle_data.economy.treasury = castle_data.economy.treasury - total_soldier_price;
castle_data.millitary.soldiers = final_soldiers;

最后更新士兵增益:

// 6. 更新士兵经济实力增益
castle_data.economy.soldier_buff.power = SOLDIER_ECONOMIC_POWER * final_soldiers;
castle_data.economy.soldier_buff.start = clock::timestamp_ms(clock);

因此,完整的函数应为:

/// 城堡使用金库招募士兵
public(package) fun recruit_soldiers (id: ID, count: u64, clock: &Clock, game_store: &mut GameStore) {
    // 1. 借用城堡数据
    let castle_data = dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, id);

    // 2. 检查数量限制
    let final_soldiers = castle_data.millitary.soldiers + count;
    assert!(final_soldiers <= get_castle_soldier_limit(castle_data.size), 0);

    // 3. 检查金库是否充足
    let total_soldier_price = SOLDIER_PRICE * count;
    assert!(castle_data.economy.treasury >= total_soldier_price, 0);

    // 4. 结算经济
    settle_castle_economy_inner(clock, castle_data);

    // 5. 更新金库和士兵
    castle_data.economy.treasury = castle_data.economy.treasury - total_soldier_price;
    castle_data.millitary.soldiers = final_soldiers;

    // 6. 更新士兵经济实力增益
    castle_data.economy.soldier_buff.power = SOLDIER_ECONOMIC_POWER * final_soldiers;
    castle_data.economy.soldier_buff.start = clock::timestamp_ms(clock);
} 

/// 每个士兵的价格
const SOLDIER_PRICE : u64 = 100;

攻击!- 随机选择敌人

激动人心的部分来了:战斗!战斗系统是一个复杂的工作,我们将分两课来讲解。在本课中,我们将重点讲解如何随机选择一个敌人进行攻击以及如何确定胜负。

为了简化,每场战斗涉及两个城堡。攻击方城堡主动发起战斗,而防御方城堡被动参与战斗。然而,匹配过程是随机的,这意味着攻击者不能选择特定的城堡进行攻击。

战斗结束后,双方都进入战斗冷却状态。胜方进入1小时的冷却期,此期间内城堡不能再发起战斗或被攻击。另一方面,败方则进入4小时的冷却期。

1. 随机选择敌人

首先,我们需要从游戏存储中的城堡ID池中随机选择一个敌人城堡ID。让我们在 core.move 中添加一个函数。

// 随机选择一个目标城堡ID
public(package) fun random_battle_target(from_castle: ID, game_store: &GameStore, ctx: &mut TxContext): ID {
    
}

随机数学类似于生成城堡序列号,这次我们需要从城堡ID向量中生成一个随机索引,在 utils.move 中添加一个 random_in_range 函数:

public(package) fun random_in_range(range: u64, ctx: &mut TxContext): u64 {
    let uid = object::new(ctx);
    let mut hash = hash::sha2_256(object::uid_to_bytes(&uid));
    object::delete(uid);

    let mut result_num: u64 = 0;
    while (vector::length(&hash) > 0) {
        let element = vector::remove(&mut hash, 0);
        result_num = (result_num << 8) | (element as u64);
    };
    result_num = result_num % range;

    result_num
}

填充 random_battle_target 函数如下:

use move_castle::utils;

// 随机选择一个目标城堡ID
public(package) fun random_battle_target(from_castle: ID, game_store: &GameStore, ctx: &mut TxContext): ID {
    let total_length = vector::length<ID>(&game_store.castle_ids);
    assert!(total_length > 1, 0);

    let mut random_index = utils::random_in_range(total_length, ctx);
    let mut target = vector::borrow<ID>(&game_store.castle_ids, random_index);

    while (object::id_to_address(&from_castle) == object::id_to_address(target)) {
        // 重新随机直到不相等
        random_index = utils::random_in_range(total_length, ctx);
        target = vector::borrow<ID>(&game_store.castle_ids, random_index);
    };

    object::id_from_address(object::id_to_address(target))
}

如你所见,我们确保池中至少有一个以上的城堡,并确保不会攻击自己。

“战斗”数据依赖于城堡的数据,在 core.move 中添加一个公共函数以帮助获取城堡数据:

public(package) fun fetch_castle_data(id1: ID, id2: ID, game_store: &mut GameStore): (CastleData, CastleData) {
    let castle_data1 = dynamic_field::remove<ID, CastleData>(&mut game_store.id, id1);
    let castle_data2 = dynamic_field::remove<ID, CastleData>(&mut game_store.id, id2);
    (castle_data1, castle_data2)
}

sources 目录下创建一个 battle.move 文件,插入一个战斗入口函数。

module move_castle::battle {
    use sui::clock::{Self, Clock};
    
    use move_castle::castle::Castle;
    use move_castle::core::{Self, GameStore};

    entry fun battle(castle: &Castle, clock: &Clock, game_store: &mut GameStore, ctx: &mut TxContext) {
        // 1. 随机选择一个目标
        let attacker_id = object::id(castle);
        let target_id = core::random_battle_target(attacker_id, game_store, ctx);

        // 2. 获取城堡数据
        let (attacker, defender) = core::fetch_castle_data(attacker_id, target_id, game_store);
    }

}

我们现在有了双方,检查他们的冷却状态:

// 3. 检查战斗冷却
let current_timestamp = clock::timestamp_ms(clock);
assert!(core::get_castle_battle_cooldown(&attacker) < current_timestamp, 0);
assert!(core::get_castle_battle_cooldown(&defender) < current_timestamp, 0);

core.move 中的 get_castle_battle_cooldown 函数:

public(package) fun get_castle_battle_cooldown(castle_data: &CastleData): u64 {
    castle_data.millitary.battle_cooldown
}

为什么我们不将所有战斗相关组件放在 battle.move 中?原因在于一个规则:结构字段不能在其定义模块外访问。例如,代码 castle_data.race 仅在 core.move 中有效。

2. 总攻击/防御力量

如果他们被允许战斗,那么是时候决定胜利者了。计算围绕总攻击力和总防御力进行。类似于经济实力,总攻击力或防御力由城堡的基础攻击/防御力和士兵提供的力量组成。

core.move 中,添加函数来计算城堡的总士兵攻击力和防御力:

/// 城堡的总士兵攻击力
public(package) fun get_castle_total_soldiers_attack_power(castle_data: &CastleData): u64 {
    let (soldier_attack_power, _) = get_castle_soldier_attack_defense_power(castle_data.race);
    castle_data.millitary.soldiers * soldier_attack_power
}

/// 城堡的总士兵防御力
public(package) fun get_castle_total_soldiers_defense_power(castle_data: &CastleData): u64 {
    let (_, soldier_defense_power) = get_castle_soldier_attack_defense_power(castle_data.race);
    castle_data.millitary.soldiers * soldier_defense_power
}

并将基础攻击/防御力量添加为总力量:

/// 城堡的总攻击力(基础+士兵)
public(package) fun get_castle_total_attack_power(castle_data: &CastleData): u64 {
    castle_data.millitary.attack_power + get_castle_total_soldiers_attack_power(castle_data)
}

/// 城堡的总防御力(基础+士兵)
public(package) fun get_castle_total_defense_power(castle_data: &CastleData): u64 {
    castle_data.millitary.defense_power + get_castle_total_soldiers_defense_power(castle_data)
}

由于士兵数量的变化(如招募士兵)会导致总攻击/防御力量的变化,因此我们需要在那时更新它们。在 core 模块的 recruit_soldiers 函数结束时:

public(package) fun recruit_soldiers (id: ID, count: u64, clock: &Clock, game_store: &mut GameStore) {
...

    // 7. 更新总攻击/防御力量
    castle_data.millitary.total_attack_power = get_castle_total_attack_power(freeze(castle_data));
    castle_data.millitary.total_defense_power = get_castle_total_defense_power(freeze(castle_data));
} 

3. 种族优势

正如我们在第一课的“战斗系统”中介绍的那样,种族之间存在战斗优势。拥有优势的一方可以享受50%的数值加成(攻击方的攻击力或防御方的防御力)。

我们需要一个函数来检查攻击者或防御者是否具有种族优势,在 core.move 中:

// 如果有种族优势
public(package) fun has_race_advantage(castle_data1: &CastleData, castle_data2: &CastleData): bool {
    let c1_race = castle_data1.race;
    let c2_race = castle_data2.race;

    let has;
    if (c1_race == c2_race) {
        has = false;
    } else if (c1_race < c2_race) {
        has = (c2_race - c1_race) == 1;
    } else {
        has = (c1_race - c2_race) == 4;
    };

    has
}

种族代码按种族优势顺序定义。

现在我们可以计算最终的攻击力和防御力,在 battle 函数中:

use sui::math;

// 4. 战斗
// 4.1 计算总攻击力和防御力
let mut attack_power = core::get_castle_total_attack_power(&attacker);
let mut defense_power = core::get_castle_total_defense_power(&defender);
if (core::has_race_advantage(&attacker, &defender)) {
    attack_power = math::divide_and_round_up(attack_power * 15, 10)
} else if (core::has_race_advantage(&defender, &attacker)) {
    defense_power = math::divide_and_round_up(defense_power * 15, 10)
};

然后我们确定赢家和输家:

// 4.2 确定胜负
let (winner, loser);
if (attack_power > defense_power) {
    winner = attacker;
    loser = defender;
} else {
    winner = defender;
    loser = attacker;
};
let winner_id = core::get_castle_id(&winner);
let loser_id = core::get_castle_id(&loser);

core.move 中的 get_castle_id 函数:

public(package) fun get_castle_id(castle_data: &CastleData): ID {
    castle_data.id
}

清理战场 - 战斗结算

在上一课中,我们终于确定了赢家,现在是清理战场的时候了,赢家得到奖励,输家受到惩罚。

战斗对双方城堡的影响包括:

  • 战斗冷却
  • 士兵伤亡
  • 掠夺在战斗冷却期间由失败者的基础经济能力提供的利益
  • 胜者获得经验值

让我们继续完成 battle 函数。我们将分别结算赢家和输家。

1. 赢家

我们需要首先结算城堡的经济:

// 5. 战斗结算   
// 5.1 结算赢家
core::settle_castle_economy_inner(clock, &mut winner);

士兵剩余和士兵损失的计算方式如下:

use move_castle::utils;

let winner_solders_total_defense_power = core::get_castle_total_soldiers_defense_power(&winner);
let loser_solders_total_attack_power = core::get_castle_total_soldiers_attack_power(&loser);
let winner_soldiers_left;
if (winner_solders_total_defense_power > loser_solders_total_attack_power) {
    let (_, winner_soldier_defense_power) = core::get_castle_soldier_attack_defense_power(core::get_castle_race(&winner));
    winner_soldiers_left = math::divide_and_round_up(winner_solders_total_defense_power - loser_solders_total_attack_power, winner_soldier_defense_power);
} else {
    winner_soldiers_left = 0;
};
let winner_soldiers_lost = core::get_castle_soldiers(&winner) - winner_soldiers_left;

上面的代码中使用的 get_castle_race 函数在 core 模块中:

public(package) fun get_castle_race(castle_data: &CastleData): u64 {
    castle_data.race
}

utils.move 中的 abs_minus 函数是:

public(package) fun abs_minus(a: u64, b: u64): u64 {
    let result;
    if (a > b) {
        result = a - b;
    } else {
        result = b - a;
    };
    result
}

core.move 中的 get_castle_soldiers 是:

public(package) fun get_castle_soldiers(castle_data: &CastleData): u64 {
    castle_data.millitary.soldiers
}

胜利者获得的经验值取决于城堡的等级,根据设计,在 core.move 中添加这个常量向量和函数:

/// 根据胜利者等级在战斗中获得的经验值 1 - 10
const BATTLE_EXP_GAIN_LEVELS : vector<u64> = vector[25, 30, 40, 55, 75, 100, 130, 165, 205, 250];

public fun battle_winner_exp(castle_data: &CastleData): u64 {
    let battle_exp_map = BATTLE_EXP_GAIN_LEVELS;
    *vector::borrow<u64>(&battle_exp_map, castle_data.level)
}

回到 battle 函数,添加:

let winner_exp_gain = core::battle_winner_exp(&winner);

被掠夺的失败者基础经济能力应该是:

let reparation_economic_power = core::get_castle_economic_base_power(&loser);

更新赢家的城堡数据:

core::battle_settlement_save_castle_data(
    game_store,
    winner, 
    true, 
    current_timestamp + BATTLE_WINNER_COOLDOWN_MS,
    reparation_economic_power,
    current_timestamp,
    current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME,
    winner_soldiers_left,
    winner_exp_gain
);

const BATTLE_WINNER_COOLDOWN_MS : u64 = 30 * 1000; // 30秒
const BATTLE_LOSER_ECONOMIC_PENALTY_TIME : u64 = 2 * 60 * 1000; // 2分钟

core.move 中:

public(package) fun get_castle_economic_base_power(castle_data: &CastleData): u64 {
    castle_data.economy.base_power
}

// 计算士兵经济能力
public(package) fun calculate_soldiers_economic_power(count: u64): u64 {
    SOLDIER_ECONOMIC_POWER * count
}

/// 结算战斗
public(package) fun battle_settlement_save_castle_data(game_store: &mut GameStore, mut castle_data: CastleData, win: bool, cooldown: u64, economic_base_power: u64, current_timestamp: u64, economy_buff_end: u64, soldiers_left: u64, exp_gain: u64) {
    // 1. 战斗冷却
    castle_data.millitary.battle_cooldown = cooldown;
    // 2. 士兵剩余
    castle_data.millitary.soldiers = soldiers_left;
    castle_data.economy.soldier_buff.power = calculate_soldiers_economic_power(soldiers_left);
    castle_data.economy.soldier_buff.start = current_timestamp;
    // 3. 士兵导致的总攻击/防御力量
    castle_data.millitary.total_attack_power = get_castle_total_attack_power(&castle_data);
    castle_data.millitary.total_defense_power = get_castle_total_defense_power(&castle_data);
    // 4. 获得经验值
    castle_data.experience_pool = castle_data.experience_pool + exp_gain;
    // 5. 经济增益
    vector::push_back(&mut castle_data.economy.battle_buff, EconomicBuff {
        debuff: !win,
        power: economic_base_power,
        start: current_timestamp,
        end: economy_buff_end,
    });
    // 6. 放回表中
    dynamic_field::add(&mut game_store.id, castle_data.id, castle_data);
}

这不是一个好的实践,这个函数有太多参数。

2. 输家

同样,在结算失败者的战斗结果之前,有必要结算其经济:

// 5.2 结算输家
core::settle_castle_economy_inner(clock, &mut loser);

输家的数字相当简单,士兵保持为0,获得的经验值为0。

let loser_soldiers_left = 0;
let loser_soldiers_lost = core::get_castle_soldiers(&loser) - loser_soldiers_left;
core::battle_settlement_save_castle_data(
    game_store,
    loser, 
    false, 
    current_timestamp + BATTLE_LOSER_COOLDOWN_MS,
    reparation_economic_power,
    current_timestamp,
    current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME,
    loser_soldiers_left,
    0
);

const BATTLE_LOSER_COOLDOWN_MS : u64 = 2 * 60 * 1000; // 2分钟

最后,让我们重新审视完整的 battle 函数:

entry fun battle(castle: &Castle, clock: &Clock, game_store: &mut GameStore, ctx: &mut TxContext) {
    // 1. 随机选择一个目标
    let attacker_id = object::id(castle);
    let target_id = core::random_battle_target(attacker_id, game_store, ctx);

    // 2. 获取城堡数据
    let (attacker, defender) = core::fetch_castle_data(attacker_id, target_id, game_store);

    // 3. 检查战斗冷却时间
    let current_timestamp = clock::timestamp_ms(clock);
    assert!(core::get_castle_battle_cooldown(&attacker) < current_timestamp, EBattleCooldown);
    assert!(core::get_castle_battle_cooldown(&defender) < current_timestamp, EBattleCooldown);

    // 4. 战斗
    // 4.1 计算总攻击力和防御力
    let mut attack_power = core::get_castle_total_attack_power(&attacker);
    let mut defense_power = core::get_castle_total_defense_power(&defender);
    if (core::has_race_advantage(&attacker, &defender)) {
        attack_power = math::divide_and_round_up(attack_power * 15, 10)
    } else if (core::has_race_advantage(&defender, &attacker)) {
        defense_power = math::divide_and_round_up(defense_power * 15, 10)
    };

    // 4.2 确定胜负
    let (mut winner, mut loser);
    if (attack_power > defense_power) {
        winner = attacker;
        loser = defender;
    } else {
        winner = defender;
        loser = attacker;
    };
    let winner_id = core::get_castle_id(&winner);
    let loser_id = core::get_castle_id(&loser);

    // 5. 战斗结算   
    // 5.1 结算胜者
    core::settle_castle_economy_inner(clock, &mut winner);
    let winner_solders_total_defense_power = core::get_castle_total_soldiers_defense_power(&winner);
    let loser_solders_total_attack_power = core::get_castle_total_soldiers_attack_power(&loser);
    let winner_soldiers_left;
    if (winner_solders_total_defense_power > loser_solders_total_attack_power) {
        let (_, winner_soldier_defense_power) = core::get_castle_soldier_attack_defense_power(core::get_castle_race(&winner));
        winner_soldiers_left = math::divide_and_round_up(winner_solders_total_defense_power - loser_solders_total_attack_power, winner_soldier_defense_power);
    } else {
        winner_soldiers_left = 0;
    };
    let winner_soldiers_lost = core::get_castle_soldiers(&winner) - winner_soldiers_left;
    let winner_exp_gain = core::battle_winner_exp(&winner);
    let reparation_economic_power = core::get_castle_economic_base_power(&loser);
    core::battle_settlement_save_castle_data(
        game_store,
        winner, 
        true, 
        current_timestamp + BATTLE_WINNER_COOLDOWN_MS,
        reparation_economic_power,
        current_timestamp,
        current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME,
        winner_soldiers_left,
        winner_exp_gain
    );

    // 5.2 结算败者
    core::settle_castle_economy_inner(clock, &mut loser);
    let loser_soldiers_left = 0;
    let loser_soldiers_lost = core::get_castle_soldiers(&loser) - loser_soldiers_left;
    core::battle_settlement_save_castle_data(
        game_store,
        loser, 
        false, 
        current_timestamp + BATTLE_LOSER_COOLDOWN_MS,
        reparation_economic_power,
        current_timestamp,
        current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME,
        loser_soldiers_left,
        0
    );

    // 6. 触发事件
    event::emit(CastleBattleLog {
        attacker: attacker_id,
        winner: winner_id,
        loser: loser_id,
        winner_soldiers_lost: winner_soldiers_lost,
        loser_soldiers_lost: loser_soldiers_lost,
        reparation_economic_power: reparation_economic_power,
        battle_time: current_timestamp,
        reparation_end_time: current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME
    });
}

const BATTLE_WINNER_COOLDOWN_MS : u64 = 30 * 1000; // 30 秒
const BATTLE_LOSER_COOLDOWN_MS : u64 = 2 * 60 * 1000; // 2 分钟
const BATTLE_LOSER_ECONOMIC_PENALTY_TIME : u64 = 2 * 60 * 1000; // 2 分钟

是时候变得更强了——升级你的城堡

在战斗之后,假设获胜者现在有足够的经验点数进行升级。城堡的等级设计从1到10,每个等级需要不同的升级经验点数。

在core.move中添加所需的经验点常量:

/// 等级2-10所需的经验点数
const REQUIRED_EXP_LEVELS : vector<u64> = vector[100, 150, 225, 338, 507, 760, 1140, 1709, 2563];

在core.move中创建一个upgrade_castle函数并获取城堡数据:

/// 消耗经验池中的经验点来升级城堡
public(package) fun upgrade_castle(id: ID, game_store: &mut GameStore) {
    // 1. 获取城堡数据
    let castle_data = dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, id);
}

消耗经验点并进行升级,重复这一过程直到达到最大等级,或经验点不足为止:

// 2. 如果经验足够,持续升级
let mut initial_level = castle_data.level;
let mut exp_level_map = REQUIRED_EXP_LEVELS;
while (castle_data.level < MAX_CASTLE_LEVEL) {
    let exp_required_at_current_level = *vector::borrow(&exp_level_map, castle_data.level - 1);
    if(castle_data.experience_pool < exp_required_at_current_level) {
        break
    };

    castle_data.experience_pool = castle_data.experience_pool - exp_required_at_current_level;
    castle_data.level = castle_data.level + 1;
};

/// 最大城堡等级
const MAX_CASTLE_LEVEL : u64 = 10;

如果城堡升级了,更新其基础经济力量、基础攻击和防御力量。需要重新计算它们。

基础经济力量通过其初始力量计算,每升一级增加20%。在core.move中:

/// 计算城堡的基础经济力量
fun calculate_castle_base_economic_power(castle_data: &CastleData): u64 {
    let initial_base_power = get_initial_economic_power(castle_data.size);
    let level = castle_data.level;
    math::divide_and_round_up(initial_base_power * 12 * math::pow(10, ((level - 1) as u8)), 10)
}

对于基础攻击力和基础防御力,除了每级增加20%,还有一个大小系数需要乘以。在 core.move 中:

/// 获取城堡大小系数
fun get_castle_size_factor(castle_size: u64): u64 {
    let factor;
    if (castle_size == CASTLE_SIZE_SMALL) {
        factor = CASTLE_SIZE_FACTOR_SMALL;
    } else if (castle_size == CASTLE_SIZE_MIDDLE) {
        factor = CASTLE_SIZE_FACTOR_MIDDLE;
    } else if (castle_size == CASTLE_SIZE_BIG) {
        factor = CASTLE_SIZE_FACTOR_BIG;
    } else {
        abort 0
    };
    factor
}

/// 城堡大小系数 - 小
const CASTLE_SIZE_FACTOR_SMALL : u64 = 2;
/// 城堡大小系数 - 中
const CASTLE_SIZE_FACTOR_MIDDLE : u64 = 3;
/// 城堡大小系数 - 大
const CASTLE_SIZE_FACTOR_BIG : u64 = 5;

然后计算攻击力/防御力:

/// 根据等级计算城堡的基础攻击力和基础防御力
/// 基础攻击力 = (城堡大小系数 * 初始攻击力 * (1.2 ^ (等级 - 1)))
/// 基础防御力 = (城堡大小系数 * 初始防御力 * (1.2 ^ (等级 - 1)))
fun calculate_castle_base_attack_defense_power(castle_data: &CastleData): (u64, u64) {
    let castle_size_factor = get_castle_size_factor(castle_data.size);
    let (initial_attack, initial_defense) = get_initial_attack_defense_power(castle_data.race);
    let attack_power = math::divide_and_round_up(castle_size_factor * initial_attack * 12 * math::pow(10, ((castle_data.level - 1) as u8)), 10);
    let defense_power = math::divide_and_round_up(castle_size_factor * initial_defense * 12 * math::pow(10, ((castle_data.level - 1) as u8)), 10);
    (attack_power, defense_power)
}

回到 upgrade_castle 函数:

// 3. 如果升级了,更新力量
if (castle_data.level > initial_level) {
    let base_economic_power = calculate_castle_base_economic_power(freeze(castle_data));
    castle_data.economy.base_power = base_economic_power;

    let (attack_power, defense_power) = calculate_castle_base_attack_defense_power(freeze(castle_data));
    castle_data.millitary.attack_power = attack_power;
    castle_data.millitary.defense_power = defense_power;
}

还记得freeze吗?如果需要,请查看“参数传递”一课。

检查完整的升级城堡函数:

/// 消耗经验池中的经验点来升级城堡
public(package) fun upgrade_castle(id: ID, game_store: &mut GameStore) {
    // 1. 获取城堡数据
    let castle_data = dynamic_field::borrow_mut<ID, CastleData>(&mut game_store.id, id);

    // 2. 如果经验足够,持续升级
    let initial_level = castle_data.level;
    let exp_level_map = REQUIRED_EXP_LEVELS;
    while (castle_data.level < MAX_CASTLE_LEVEL) {
        let exp_required_at_current_level = *vector::borrow(&exp_level_map, castle_data.level - 1);
        if(castle_data.experience_pool < exp_required_at_current_level) {
            break
        };

        castle_data.experience_pool = castle_data.experience_pool - exp_required_at_current_level;
        castle_data.level = castle_data.level + 1;
    };

    // 3. 如果升级了,更新力量
    if (castle_data.level > initial_level) {
        let base_economic_power = calculate_castle_base_economic_power(freeze(castle_data));
        castle_data.economy.base_power = base_economic_power;

        let (attack_power, defense_power) = calculate_castle_base_attack_defense_power(freeze(castle_data));
        castle_data.millitary.attack_power = attack_power;
        castle_data.millitary.defense_power = defense_power;
    }
}

最后,在 castle.move 中添加一个入口:

/// 升级城堡
entry fun upgrade_castle(castle: &Castle, game_store: &mut GameStore) {
    core::upgrade_castle(object::id(castle), game_store);
}

03_改进 - 课程学习时长~约7分钟

自定义错误

在上一课“abort和assert”中,我们学习了如何在需要的特定情况下故意“抛出”错误。当错误被抛出后,应用程序需要解析错误信息,并以设计的方式表现,例如弹出错误模态窗口。

应用程序如何识别特定错误呢?中止代码是关键,在一个模块中,你需要管理所有中止情况,并用中止代码标识它们,将它们声明为常量:

// battle.move

/// 战斗双方之一或双方都处于战斗冷却中
const EBattleCooldown: u64 = 0;

中止代码或错误代码的命名约定是驼峰命名法,以大写字母E开头。

在特定情况下使用常量中止:

// battle.move

entry fun battle(...) {
...
    // 3. 检查战斗冷却
    let current_timestamp = clock::timestamp_ms(clock);
    assert!(core::get_castle_battle_cooldown(&attacker) < current_timestamp, EBattleCooldown);
    assert!(core::get_castle_battle_cooldown(&defender) < current_timestamp, EBattleCooldown);
...
}

core.move 中的其他中止情况:

/// 士兵数量超过限制
const ESoldierCountLimit: u64 = 0;

/// 招募士兵的资金不足
const EInsufficientTreasury: u64 = 1;

/// 战斗城堡数量不足
const ENotEnoughCastles: u64 = 2;

情况:

public(package) fun recruit_soldiers(...) {
...
    // 2. 检查数量限制
    let final_soldiers = castle_data.millitary.soldiers + count;
    assert!(final_soldiers <= get_castle_soldier_limit(castle_data.size), ESoldierCountLimit);

    // 3. 检查资金充足性
    let total_soldier_price = SOLDIER_PRICE * count;
    assert!(castle_data.economy.treasury >= total_soldier_price, EInsufficientTreasury);
...
}

public fun random_battle_target(...) {
    let total_length = vector::length<ID>(&game_store.castle_ids);
    assert!(total_length > 1, ENotEnoughCastles);
...
}

Castle.move 中,我们漏掉了一个情况,当建造城堡时,每个城堡大小都有数量限制,我们需要在build_castle 函数的开头进行数量检查。

core.move 中添加一个函数来检查每种大小城堡的数量:

public(package) fun allow_new_castle(size: u64, game_store: &GameStore): bool {
    let allow;
    if (size == CASTLE_SIZE_SMALL) {
        allow = game_store.small_castle_count < CASTLE_AMOUNT_LIMIT_SMALL;
    } else if (size == CASTLE_SIZE_MIDDLE) {
        allow = game_store.middle_castle_count < CASTLE_AMOUNT_LIMIT_MIDDLE;
    } else if (size == CASTLE_SIZE_BIG) {
        allow = game_store.big_castle_count < CASTLE_AMOUNT_LIMIT_BIG;
    } else {
        abort 0
    };
    allow
}

/// 城堡数量限制 - 小
const CASTLE_AMOUNT_LIMIT_SMALL : u64 = 500;
/// 城堡数量限制 - 中
const CASTLE_AMOUNT_LIMIT_MIDDLE : u64 = 300;
/// 城堡数量限制 - 大
const CASTLE_AMOUNT_LIMIT_BIG : u64 = 200;

castle.move 中的 build_castle 函数中添加断言:

entry fun build_castle(...) {
    // 城堡数量检查
    assert!(core::allow_new_castle(size, game_store), ECastleAmountLimit);
...
}

const ECastleAmountLimit: u64 = 0;

发布事件

Sui 中的另一个重要概念是事件。 事件可以用于跟踪链上的活动,这在开发人员希望跟踪用户在特定合约交互(例如 NFT 铸造)期间的行为时特别有价值。

在我们的 Move Castle 游戏中,建造城堡的行为类似于铸造过程。因此,记录和跟踪此类事件非常重要。

在发布事件之前,你必须定义其结构,在 castle.move 中:

/// Event - castle built
public struct CastleBuilt has copy, drop {
    id: ID,
    owner: address,
}

然后使用 event::emit 来发布事件,在 build_castle 函数中:

use sui::event;

entry fun build_castle(...) {
    ...
    // 将城堡对象转移给拥有者。
    let owner = tx_context::sender(ctx);
    transfer::public_transfer(castle, owner);
    event::emit(CastleBuilt{id: id, owner: owner});
}

很简单,对吧?在 Sui testnet 上试试。

Sui 客户端 CLI 输出:

CLI_output

Sui explorer 交易页面:

Explorer

同样地,在战斗结束时发布战斗结果。在 battle.move 中添加事件结构:

/// Battle event
public struct CastleBattleLog has store, copy, drop {
    attacker: ID,
    winner: ID,
    loser: ID,
    winner_soldiers_lost: u64,
    loser_soldiers_lost: u64,
    reparation_economic_power: u64,
    battle_time: u64,
    reparation_end_time: u64
}

battle 函数的末尾发布事件:

use sui::event;

entry fun battle(castle: &Castle, clock: &Clock, game_store: &mut GameStore, ctx: &mut TxContext) {
    ...
    // 6. 发布事件
    event::emit(CastleBattleLog {
        attacker: attacker_id,
        winner: winner_id,
        loser: loser_id,
        winner_soldiers_lost: winner_soldiers_lost,
        loser_soldiers_lost: loser_soldiers_lost,
        reparation_economic_power: reparation_economic_power,
        battle_time: current_timestamp,
        reparation_end_time: current_timestamp + BATTLE_LOSER_ECONOMIC_PENALTY_TIME
    });
}

04_发布与调用 - 课程学习时长~约2分钟

发布与调用

在本指南中,我们将逐步介绍如何部署Move Castle游戏合约、设置前端、管理端口转发以及探索创建的城堡。让我们简化步骤以提高清晰度和效率。

我们在本课程的右侧IDE中部署合约和前端,但当然,你也可以使用本地环境进行部署。

1. 发布软件包

启动本课程右侧的IDE面板,等待环境初始化。

点击“工具”部分下的“终端”按钮,打开终端。 Tools Terminal

导航到已完成的Move Castle智能合约文件夹,并将软件包发布到开发网络。

cd move_castle
sui client publish

发布后,记下GameStore对象ID和软件包ID,它们将在接下来的步骤中使用。 publish_log

对于那些更喜欢直接方法或可能遗漏了一些步骤的人来说,本课程的完整合约已在我们的仓库中提供。只需按照以下步骤获取:

首先,将仓库克隆到本地计算机:

git clone https://github.com/WhiteMatrixTech/move-castle-contract.git

如果尚未配置SUI钱包,请参考此链接进行配置: Sui Wallet and Faucet

然后发布:

cd move-castle-contract
sui client publish

2. 部署游戏前端

我们已经为Move Castle游戏项目开发了前端。现在,我们的任务是在ChainIDE工作室沙箱中部署它,设置端口转发,并最终访问托管的前端页面。

在工作区文件夹中,从提供的仓库克隆Move Castle游戏的前端项目:

git clone https://github.com/WhiteMatrixTech/movecastle-sui-demo-frontend

frontend-git

在工作室中审查前端代码(如果需要)。 frontend-code

更新 src/utils/const.ts 中的包ID和 GameStore 对象ID。 cconst.ts

如果你的合约部署到了测试网,请将 targetNetwork 常量更新为测试网。同样,对于部署在主网上的合约,请将常量设置为主网。

构建并运行前端项目:

cd movecastle-sui-demo-frontend
npm install
npm run build
npm run serve

服务器运行后,请记下控制台日志中的端口号(通常为3000)。

3. 管理端口转发

ChainIDE工作室提供了一种简便的方法来暴露在沙箱中运行的Web服务。

在工作室中,找到"TOOLS"部分或右侧工具面板中的"Port Manager"按钮。 Port_Manager

为端口3000添加端口转发。 Port_3000 press_button

点击按钮以访问你的游戏前端服务!

4. 创建和探索城堡

Create

连接您的 Sui 钱包,点击“登录”按钮,然后点击“创建新城堡”以开始创建城堡。 Attributes

填写城堡名称、描述并选择尺寸。确认并批准交易。 Build

探索您创建的城堡及其属性。 Explore

您还可以选择创建更多城堡并进行战斗。

注意:如果由于战斗冷却时间而全球城堡数量过少,战斗可能会失败。

希望您在学习和使用 Move on Sui 进行开发的过程中愉快且有所收获!

04_发布与调用 - 课程学习时长~约2分钟

能力设计模式

在前四章中,我们学习了Sui上的Move基础知识和一些进阶知识,并完成了Move Castle项目。 在本章中,我们将学习一些重要的Sui概念和标准,这些概念和标准通常在常见的Sui智能合约项目中使用。

能力设计模式允许模块对操作进行授权。这是通过在模块初始化时将“能力”对象发送到授权地址, 并在需要授权的函数中要求该“能力”对象作为参数来实现的。然后,只有拥有“能力”对象的函数调用者才有权限调用被授权的函数。

module test::test_cap {
    use sui::transfer;
    
    /// 管理员能力类型。
    public struct AdminCap has key { id: UID }
    
    /// 创建并将管理员能力对象发送给包发布者。
    fun init(ctx: &mut TxContext) {
        transfer::transfer(AdminCap {
            id: object::new(ctx)
        }, tx_context::sender(ctx))
    }
    
    /// 需要AdminCap对象拥有者作为调用者的授权函数
    entry fun authorized_fun(_: &AdminCap, ctx: &mut TxContext) {
        ...
    }
}

典型的能力实现是定义在sui::coin中的TreasuryCap。

如果你感兴趣,可以考虑在core.move中使用AdminCap。定义游戏数据中的可设置属性,并公开一个编辑函数。使用能力设计模式来授权函数调用实现编辑功能。

一次性见证者

一次性见证者(One-Time Witness,OTW)设计非常巧妙。 OTW对象是在模块发布时创建的唯一对象,并在使用时被消费(删除),以证明所有权并保证操作的唯一性。

典型的使用场景是在sui::coin模块中创建货币,当然,币只能被创建一次。

OTW类型定义有以下两个规则:

  • 名称与模块同名,但大写。
  • 具有drop能力。
  • 没有字段。

OTW对象在初始化时创建,并作为init函数的第一个参数接收。

module test::otw {

    /// 名称与模块名称匹配
    public struct OTW has drop {}

    /// 作为第一个参数接收
    fun init(witness: OTW, ctx: &mut TxContext) {
        ...
    }
}

要利用OTW对象来保证唯一性和一次性使用,可以使用sui::types::is_one_time_witness(&witness)来测试一个对象是否为OTW对象。

assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness);

赞助交易

赞助交易指的是由赞助方承担交易的燃气费, 这样用户无需购买代币即可进行链上活动。这对那些刚接触Sui或web3世界的新用户非常友好。赞助交易包括用户发起的赞助交易、赞助方发起的赞助交易以及双签名交易。

官方文档介绍了使用Rust SDK的代码示例来实现赞助交易,但这些内容无法在本课程中涉及。不过, Sui Typescript SDK中的赞助交易块(Sponsored Transaction Blocks)也提供了一种实现赞助交易的方法。

Sui Kiosk

Kiosk 是一个在 Sui 上为商业应用设计的去中心化系统。Kiosk 设计了三种在商业行为中的角色:

  • Creator - 产品创建者,可以管理转让策略。
  • Kiosk Owner - 卖家,当前产品的拥有者。
  • Buyer - 想要购买产品的人。

卖家可以创建自己的 kiosk,添加并列出带价格的商品,买家可以通过 kiosk 购买列出的商品。

一个简单的通过 kiosks 列出并购买商品的流程如下:

  1. 卖家和买家都创建他们自己的 0x2::kiosk::Kiosk 对象以及 KioskOwnerCap
  2. 卖家将商品添加到自己的 kiosk,商品将以动态字段的形式移动到 kiosk 下。
  3. 买家通过卖家的 kiosk 购买商品。
  4. 解决由商品(类型)创建者创建的转让策略。
  5. 购买完成,商品被移动到买家的 kiosk。
  6. 买家从 kiosk 中提取商品到自己的地址。

当你构建 kiosk 应用程序时,需要使用 kiosk SDK,因为在执行复杂的 kiosk 操作(如购买和解决策略)时需要编程交易块。