SOLANA Unity 3D开发详解

在本实践指南中,我们将深入研究 Anchor、Unity、Phantom 和 Solana UnitySDK 的强大组合来开发3D游戏。

SOLANA Unity 3D开发详解
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK

在本教程中,我们将使用 Unity、Solana UnitySDK、Anchor 和 Phantom。 如果您已经熟悉这些工具,请随时跳到步骤 1。对于那些不熟悉这些技术的人,请不要担心 — 我将提供快速介绍,以帮助你快速上手。

1、开发环境搭建

首先安装前面提到的开发工具。

1.1 Solana CLI

打开终端并输入以下命令 - 请确保您安装的是 1.16.1 版本,而不是更高或更低版本。 由于与 Anchor 存在潜在兼容性问题,建议使用此特定版本。

MacOS、Linux和WSL

sh -c “$(curl -sSfL https://release.solana.com/v1.16.1/install)”

Windows

cmd /c “curl https://release.solana.com/v1.16.1/solana-install-init-x86_64-pc-windows-msvc.exe — output C:\solana-install-tmp\solana-install-init.exe — create-dirs”

C:\solana-install-tmp\solana-install-init.exe v1.16.1

完成后,转到终端并使用 solana --version 检查版本。

1.2 Anchor CLI

要安装 Anchor,首先我们需要安装 Rust。 要安装 Rust,请打开终端并输入以下命令 -

MacOS、Linux 和 WSL

curl — proto ‘=https’ — tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows

可以直接从此处下载 .exe 安装程序。 下载后,运行 .exe 并安装它。

完成后,转到终端并通过一一键入以下命令 rustc --versioncargo --version 检查 Rust 和 Cargo 的版本号。 如果看起来像这样,那么我们就可以继续前进了!

现在我们将安装 Anchor,在终端中输入以下命令 -

MacOS、Linux、WSL 和 Windows

cargo install — git https://github.com/coral-xyz/anchor avm — locked — force

avm install latest

avm use latest

如果您看到任何错误消息,或者安装 Anchor 时遇到问题,请查看此处的官方指南,以获取任何更新的修复程序! 此时制作本教程的最新版本是0.28.0。 如果您稍后遇到锚点问题,请尝试切换到此版本!

完成后,转到您的终端并输入 anchor --version,如果它看起来像这样,我们就可以继续前进了!

1.3 Phantom

前往 phantom 的官方网站并下载钱包扩展程序。 创建一个新钱包,我们就可以开始了。 如果您不确定如何创建新钱包,请查看此 YouTube 视频

2、项目设置

在您最喜欢的位置创建一个新文件夹并根据需要命名,进入该文件夹并使用 git init 初始化 git。 如果你通过终端完成所有这些操作,它应该看起来像这样 -

完成后,我们将创建 Anchor 项目和 Unity 项目。 对于锚点,请使用以下命令 -

anchor init <project-name>

对于 Unity,打开 Unity Hub 并在同一目录中创建一个新项目。如果还没有安装 Unity,请查看此 YouTube 教程

现在我们有了这些,两个新文件夹!

哈哈! 最后我们现在可以开始编码了!

3、使用 Anchor 制作智能合约

我们将保持本教程非常简单,并编写一个非常基本的 Anchor 合约。 首先,在锚项目文件夹中打开终端并输入 code 。 ,或者您可以打开 Visual Studio Code,然后转到“文件”>“打开文件夹”,然后选择后端文件夹。 如果您尚未安装 VS Code,可以从此处安装。

在 VS Code 中,转到 Anchor.toml 并将 seeds 设置为 true 。 用非常简单的语言将种子设置为 true 可以帮助我们从前端传递更少的数据。 如果程序可以自行计算出很少的帐户,那么它就会这样做。

然后导航到 programs > [Your project name] > src > lib.rs

这是我们编写合约的地方。该脚本有 5 个主要部分 -

  • Imports

在最顶部,我们导入我们需要的所有依赖项。 对于这个简单的例子,我们只需要使用 anchor_lang::prelude::*;

  • Program Id

这是声明程序 id 的地方! 这个id帮助我们标记我们的程序,这样我们就可以从任何其他地方轻松找到它,比如前端。

重要提示 - 这个预填充的 id 只能在 Localnet 中使用,一旦将合约部署到 devnet 或 mainnet,我们将获得一个新的程序 id,然后在前端使用它。
  • #[program]

这就是我们合约的逻辑所在。 我们把所有的交互和功能以及修改都写在这里。

  • #[derive(Accounts)]

这是上下文结构! 这是Anchor所独有的,它会进行所有必要的安全检查。 让生活更轻松。 我们稍后会看到如何实现这些。 您可以在同一脚本/程序中拥有多个上下文结构。

  • #[account]

这些就是账户本身,如果您听说过“Solana 中的一切都是账户”这句话,那么就是这些。 讽刺的是,你并不是用默认代码编写的。 但不用担心,我们会自己添加它们。 您可以在同一脚本/程序中拥有多个帐户结构。

现在,让我们设置 UserProfile Account 结构,这定义了我们的用户配置文件帐户的结构。 我们将添加 3 个字段:name、username和email。 如果您愿意,可以继续添加更多!

#[account]
pub struct UserProfile 
{
    pub name: String,
    pub username: String,
    pub email: String
}

让我们将 #[program]部分中的函数名称从 initialize更改为 init_userprofile。 并将 Context<Initialize> 更改为  Context<InitUserProfile> 。 然后我们将添加姓名、用户名和电子邮件参数! 如果您还有更多,请也添加它们。 之后我们将定义 InitUserProfile。

#[program]
pub mod user_profile_backend 
{
    use super::*;

    pub fn init_userprofile(ctx: Context<InitUserProfile>, name: String, username: String, email: String) -> Result<()> 
    {
        Ok(())
    }
}

现在我们将编写 InitUserProfile 。 我们可以将默认上下文结构名称从 Initialize 更改为 nitUserProfile。 在这里,我们需要访问传递给 init_userprofile 的参数,我们可以通过使用 #[instruction(name: String, username: String, email: String)] 传递它们来做到这一点。 请记住,它们需要按照与函数中传递的顺序相同的顺序传递。 上下文结构的骨架看起来像这样:

现在让我们继续填写。 首先,我们将初始化 UserProfile 帐户,然后将付款人添加为用户,然后添加系统程序,这是默认需要的。

#[derive(Accounts)]
#[instruction(name: String, username: String, email: String)]
pub struct InitUserProfile<'info> 
{
    #[account(init, payer = user, seeds=[b"User", user.key().as_ref()], bump, space = 8 + 4 + name.len() + 4 + username.len() + 4 + email.len())]
    pub user_profile: Account<'info, UserProfile>,

    #[account(mut)]
    pub user: Signer<'info>,

    pub system_program: Program<'info, System>
}

哇! 一下子就发生了很多事情,他们到底做了什么? 如果您已经知道,如果不知道的话,可以跳到下一部分!

  • info

这在 Rust 中称为生命周期,这不是 Solana 特有的,而是与 Rust 本身相关。 它可以帮助 Rust 了解这些变量应该可以访问多长时间。 如果您想了解更多信息,请查看此内容。

  • init

这告诉 Anchor 我们是第一次初始化该帐户。 如果之前已经初始化过,并且再次尝试调用 init,则会失败,错误代码为 0x01,这意味着该帐户已存在。 这是针对重新初始化攻击的安全检查!

  • payer

这告诉Anchor谁将为这笔交易付款。 在这里,我们将其设置为我们在其下面定义的用户。

  • seedsbump

种子(seeds)用于生成PDA(程序派生帐户),它是这个非常具体的PDA的唯一标识符。 这是一个需要理解的稍微高级的主题,但对于 Solana 编程来说非常重要。 如果您想了解更多信息,请查看此内容。 无需使用太多术语,Solana 使用 ed25519 曲线来创建帐户。 如果一个账户在曲线上,那么它是一个普通账户,如果它不在曲线上,那么它是一个 PDA。 如果万一我们的种子使 PDA 落在曲线上,我们会使用 bump将其推离曲线。 看这个图。

在我们的程序中,我们将种子设置为 User 及其 Publickey 。 由于每个人的公钥都不同,这保证了我们拥有唯一的 PDA。

  • space

我们需要定义我们的程序在区块链中需要多少空间。 8 总是被添加,它被称为anchor鉴别器,默认情况下是必需的。 这就是我们将 init_userprofile 函数中的参数导入到该结构中的原因。 字符串可以有任意长度并且不固定,因此我们需要即时计算出它们的长度。 String 还需要 +4,而其他固定大小类型(如 u64 或 Pubkey )不需要这样做。 最终,使用初始的 8 作为锚鉴别符和 3 个字符串,最终的大小应该类似于 8 + 4 + name.len() + 4 + username.len() + 4 + email.len()

  • mut

这告诉 Anchor 我们即将改变/更改此帐户。 用户被标记为 mut,因为他们将为此交易付费,从而导致他们的帐户发生变化。 如果它没有被标记为 mut ,我们将无法从他们的账户中扣除 sol,整个交易将会失败。

这是很多,但我们仍然有几个步骤需要完成我们的合同。 但它们很容易! 让我们首先初始化函数中的所有字段。

    pub fn init_userprofile(ctx: Context<InitUserProfile>, name: String, username: String, email: String) -> Result<()> 
    {
        let user_profile = &mut ctx.accounts.user_profile;

        user_profile.name = name;
        user_profile.username = username;
        user_profile.email = email;
         
        Ok(())
    }

最后整个文件应该是这样的——

让我们构建anchor程序,方法是在 VS Code Terminal > New Terminal 中打开终端并输入anchor build。 如果一切顺利,您将看到“完成”。

让我们在本地网络中测试您的代码,为此,我们首先转到 `tests > [your projet name].ts

我们将修改此脚本来进行测试! 像这样的东西

import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { UserProfileBackend } from "../target/types/user_profile_backend";
import { expect } from "chai";

describe("user-profile-backend", () => {
  let provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.UserProfileBackend as Program<UserProfileBackend>;

  it("Initialize User", async () =>
  {
    const [userProfilePDA] = web3.PublicKey.findProgramAddressSync([Buffer.from("User"), provider.publicKey.toBuffer()], program.programId);

    const name = "Prasanta";
    const username = "Memxor";
    const email = "test@gmail.com";

    await program.methods.initUserprofile(name, username, email).rpc();
    const userInfoAccount = await program.account.userProfile.fetch(userProfilePDA);

    expect(userInfoAccount.name).to.equal(name);
    expect(userInfoAccount.username).to.equal(username);
    expect(userInfoAccount.email).to.equal(email);
  });
});
  • web3.PublicKey.findProgramAddressSync

我们使用它来将用户的 PDA 地址获取到我们的测试中,我们传递与 Anchor 程序中相同的种子。 它们可能看起来不同,因为测试是在 TypeScript 中完成的,而我们是用 Rust 编写锚程序的,但本质上它们是相同的。 我们还传递程序 ID,以便它知道在哪个程序中查找此 PDA。

  • program.account.userProfile.fetch

我们用它来使用我们在测试开始时获得的 PDA 地址来获取 PDA 本身。

  • expect

我们使用expect来检查PDA持有的内容实际上等于我们期望它持有的内容。

现在返回终端并输入 anchor test,如果你得到类似的东西,那么你的测试运行成功!

让我们将该合约部署到 devnet。 为此,请转至 Anchor.toml 并将集群更改为 Devnet。

现在转到您的终端并输入 anchor deploy。 如果您收到错误消息 :

Error: Account AXC9j7a....................nuhr1 has insufficient funds for spend (3.32563416 SOL) + fee (0.0012 SOL) 

这意味着您没有足够的 devnet sol。 可以通过 solana Balance 检查您的余额。 如果低于建议数量,请通过执行如下命令为自己空投一些 Solana:

solana airdrop 2 AXC9j7a....................nuhr1 

一旦完成! 再次运行 anchor deploy。 那么它应该看起来像这样。

复制程序 ID 并将其保存在某处,我们很快就会再次需要它! 这是我们的 Devnet 程序 ID。

4、Unity开发

首先转到 Unity Asset Store 并将 Solana UnitySDK 添加到您的资产中。 然后打开 Unity 项目并转到 Window > Package Manager > Packages: My Assets ,然后搜索 Solana。

单击“下载”,完成后单击“导入”。 现在我们的项目中已经有了 Solana SDK!

首先,我们需要从我们编写到Unity中的锚程序中获取我们的IDL。 为此,请打开终端并键入以下命令 dotnet tool install Solana.Unity.Anchor.Tool 。 它应该看起来像这样——

完成后,我们将使用此工具将 JSON IDL 文件转换为 C#。 让我们回到我们的 Anchor 项目,在 VS Code 的终端中输入以下命令 dotnet anchorgen -i target/idl/[your project name].json -o userProfileIDL.cs 。 它应该看起来像这样——

如果操作正确,我们将看到生成了一个新的 .cs 文件!

现在转到 Unity,在 Hirerchy 中创建一个新的空 GameObject 并将其命名为 Solana,向其中添加 Web3 脚本。

使用这些设置创建一个新画布 -

创建两个新按钮,一个用于登录,一个用于注销,彼此叠在一起。 并将注销按钮设置为关闭。 我们将从脚本中切换它们。 另一个文本字段显示我们的公钥。

现在创建一个名为 Scripts 的新文件夹,并添加一个名为 Authentication 的新脚本。 在 VS Code 中打开脚本。 在这里我们将添加所有与身份验证相关的功能。 我们首先添加两个按钮,然后添加 OnLogin 和 OnLogout 的监听器,以及将由我们的按钮调用的登录和注销函数。 我们还将添加一个文本字段来在登录后显示我们的公钥

using Solana.Unity.SDK;
using Solana.Unity.Wallet;
using UnityEngine;
using UnityEngine.UI;

public class Authentication : MonoBehaviour
{
    [SerializeField] private Button loginButton;
    [SerializeField] private Button logoutButton;
    [SerializeField] private TextMeshProUGUI publicKeyText;

    private void Awake() 
    {
        loginButton.onClick.AddListener(() => Login());
        logoutButton.onClick.AddListener(() => Logout());
    }

    private void OnEnable()
    {
        Web3.OnLogin += OnLogin;
        Web3.OnLogout += OnLogout;
    }

    private void OnDisable() 
    {
        Web3.OnLogin -= OnLogin;
        Web3.OnLogout -= OnLogout;
    }

    public void Login()
    {
        Debug.Log("Login");
        await Web3.Instance.LoginWalletAdapter();
        publicKeyText.text = Web3.Account.PublicKey.ToString();
    }

    public void Logout()
    {
        Debug.Log("Logout");
        Web3.Instance.Logout();
    }

    private void OnLogin(Account account)
    {
        loginButton.gameObject.SetActive(false);
        logoutButton.gameObject.SetActive(true);
    }

    private void OnLogout()
    {
        loginButton.gameObject.SetActive(true);
        logoutButton.gameObject.SetActive(false);
    }
}

让我们继续在检查器上分配这些按钮,将身份验证脚本添加到同一个 Solana 游戏对象并添加按钮。

为了检查到目前为止一切是否正常,让我们构建它并检查是否可以连接到钱包。 转到 File > Build Settings ,确保您处于 WebGL 中,通过单击 Add Open Scenes 按钮添加当前场景,然后单击 build。

一旦构建完成,我们将在本地托管它,在本教程中我们将使用 Servez,您可以在此处下载。 下载后,打开它,选择构建 WebGL 的文件夹,单击“开始”并启动浏览器。

游戏打开后点击登录

选择 Phantom 并连接到游戏!

成功SSSSSS!!!!!!!!!!!! 我现在可能比你更幸福! 如果您也高兴的话,请将本课程分享给其他人,让更多人从中受益!

好吧! 跨过了一个障碍,现在我们需要从 Unity 调用您的锚定程序。 让我们回到Unity。 并创建一个名为 SolanaManager 的新脚本。 我们将首先声明 Devnet 程序 ID,并在登录后使用我们在锚定程序中使用的相同种子获取 UserProfile PDA。

using System.Text;
using Solana.Unity.Wallet;
using UnityEngine;
using Solana.Unity.SDK;

public class SolanaManager : MonoBehaviour
{
    public static PublicKey programId = new("71XE3Cj3mMdunqP7LaTQjuUNe7EA7qHbuD6YJFJao2Rq");
    private PublicKey userProfilePDA;

    private void Awake()
    {
        Web3.OnLogin += _ => {
            PublicKey.TryFindProgramAddress(new[]{
            Encoding.UTF8.GetBytes("User"),
            Web3.Account.PublicKey
            }, programId, out userProfilePDA, out var bump);
        };
    }
}

让我们将之前制作的 C# IDL 放入项目的 Scripts 文件夹中。

现在,让我们创建两个新面板,一个用于显示已创建用户的详细信息,另一个用于新用户输入其详细信息。

显示现有用户详细信息

输入新用户详细信息

让我们在 SolanaManager 脚本中分配它们的值。

   [Header("New User Profile")]
    public GameObject createUserProfilePanel;
    public TMP_InputField newName;
    public TMP_InputField newUsername;
    public TMP_InputField newEmail;
    public Button submitButton;

    [Header("Display User Profile")]
    public GameObject displayUserProfilePanel;
    public TextMeshProUGUI displayName;
    public TextMeshProUGUI displayUsername;
    public TextMeshProUGUI displayEmail;

在检查器中分配它们并将两个面板设置为关闭。

现在我们将在 SolanaManager 脚本中添加带有 IDL 的交互逻辑。 让我们编写一个名为 OnSubmitButton 的函数,并将其添加为按钮的侦听器。

将其添加到“Awake”中,

submitButton.onClick.AddListener(() => OnSubmitButton());

然后添加OnSubmitButton函数。

    private async void OnSubmitButton()
    {
        InitUserprofileAccounts accounts = new()
        {
            UserProfile = userProfilePDA,
            User = Web3.Account.PublicKey,
            SystemProgram = SystemProgram.ProgramIdKey
        };

        var rpcClient = ClientFactory.GetClient(Cluster.DevNet);
        TransactionInstruction ix = UserProfileBackendProgram.InitUserprofile(accounts, newName.text, newUsername.text, newEmail.text, programId);
        var recentHash = (await rpcClient.GetRecentBlockHashAsync()).Result.Value.Blockhash;

        var tx = new TransactionBuilder().
            SetFeePayer(Web3.Account).
            AddInstruction(ix).
            SetRecentBlockHash(recentHash).
            Build(Web3.Account);
        await rpcClient.SendTransactionAsync(tx);
        Invoke(nameof(TryGetUserProfile), 5);
    }

在这里,我们首先设置帐户,如果您还记得主程序中包含 UserProfile 、 User 和 SystemProgram 。 我们将 UserProfile 分配给我们在脚本开头获取的 PDA。 我们的钱包帐户公钥作为用户和系统程序,我们可以通过执行 SystemProgram.ProgramIdKey 来获取。 接下来我们制定交易指令并获取最新的 Blockhash。 然后,我们使用 TransactionBuilder 构建交易,添加 FeePayer 作为我们的帐户,添加刚刚创建的指令,添加最近的区块哈希,然后使用我们的帐户再次构建。 我们都准备好了! 只需调用 SendTransactionAsync 函数即可。

接下来,我们将添加 TryGetUserProfile 以从 UserAccount 获取信息。

    private async void TryGetUserProfile()
    {
        var backendClient = new UserProfileBackendClient(Web3.Rpc, Web3.WsRpc, programId);
        var res = await backendClient.GetUserProfileAsync(userProfilePDA);
        if (res.ParsedResult == null)
        {
            createUserProfilePanel.SetActive(true);
            displayUserProfilePanel.SetActive(false);
        }
        else
        {
            displayName.text = $"Name: {res.ParsedResult.Name}";
            displayUsername.text = $"Username: {res.ParsedResult.Username}";
            displayEmail.text = $"Email: {res.ParsedResult.Email}";
            createUserProfilePanel.SetActive(false);
            displayUserProfilePanel.SetActive(true);
        }
    }

获得 PDA 地址后,将其添加到 Awake 中。 总而言之,整个脚本现在应该看起来像这样。

using System.Text;
using Solana.Unity.Wallet;
using UnityEngine;
using Solana.Unity.SDK;
using Solana.Unity.Rpc.Models;
using UserProfileBackend.Program;
using UserProfileBackend;
using Solana.Unity.Rpc;
using Solana.Unity.Rpc.Builders;
using TMPro;
using UnityEngine.UI;
using Solana.Unity.Programs;

public class SolanaManager : MonoBehaviour
{
    public static PublicKey programId = new("71XE3Cj3mMdunqP7LaTQjuUNe7EA7qHbuD6YJFJao2Rq");
    private PublicKey userProfilePDA;

    [Header("New User Profile")]
    public GameObject createUserProfilePanel;
    public TMP_InputField newName;
    public TMP_InputField newUsername;
    public TMP_InputField newEmail;
    public Button submitButton;

    [Header("Display User Profile")]
    public GameObject displayUserProfilePanel;
    public TextMeshProUGUI displayName;
    public TextMeshProUGUI displayUsername;
    public TextMeshProUGUI displayEmail;

    private void Awake()
    {
        submitButton.onClick.AddListener(() => OnSubmitButton());
        Web3.OnLogin += _ => {
            PublicKey.TryFindProgramAddress(new[]{
            Encoding.UTF8.GetBytes("User"),
            Web3.Account.PublicKey
            }, programId, out userProfilePDA, out var bump);
            TryGetUserProfile();
        };
    }

    private async void TryGetUserProfile()
    {
        var backendClient = new UserProfileBackendClient(Web3.Rpc, Web3.WsRpc, programId);
        var res = await backendClient.GetUserProfileAsync(userProfilePDA);
        if (res.ParsedResult == null)
        {
            createUserProfilePanel.SetActive(true);
            displayUserProfilePanel.SetActive(false);
        }
        else
        {
            displayName.text = $"Name: {res.ParsedResult.Name}";
            displayUsername.text = $"Username: {res.ParsedResult.Username}";
            displayEmail.text = $"Email: {res.ParsedResult.Email}";
            createUserProfilePanel.SetActive(false);
            displayUserProfilePanel.SetActive(true);
        }
    }

    private async void OnSubmitButton()
    {
        InitUserprofileAccounts accounts = new()
        {
            UserProfile = userProfilePDA,
            User = Web3.Account.PublicKey,
            SystemProgram = SystemProgram.ProgramIdKey
        };

        var rpcClient = ClientFactory.GetClient(Cluster.DevNet);
        TransactionInstruction ix = UserProfileBackendProgram.InitUserprofile(accounts, newName.text, newUsername.text, newEmail.text, programId);
        var recentHash = (await rpcClient.GetRecentBlockHashAsync()).Result.Value.Blockhash;

        var tx = new TransactionBuilder().
            SetFeePayer(Web3.Account).
            AddInstruction(ix).
            SetRecentBlockHash(recentHash).
            Build(Web3.Account);
        await rpcClient.SendTransactionAsync(tx);
        Invoke(nameof(TryGetUserProfile), 5);
    }
}

我们完成了! 让我们构建并测试它!

5、测试

点击登录

选择Phantom

输入您的姓名、用户名和电子邮件,然后单击“提交”!

OK! 就这样,现在尝试刷新页面并登录。 它应该不再要求您填写详细信息,而是直接向您显示您在上一步中提供的详细信息!

恭喜! 你今天表现得真好! 去给自己买点好吃的吧,你赚到了!

6、结束语

我想说的是,我对你能走到这一步感到非常印象深刻! Solana 编程并不是最简单的区块链。 它非常复杂,但是一旦你开始掌握它,它就会变得更容易!

整个项目的 GitHub 存储库可在此处获取。


原文链接:Step-by-Step Guide: Setting up a User Profile with Solana and Unity

DefiPlot翻译整理,转载请标明出处

通过 NowPayments 打赏