Jelly Forest はブロックチェーン対応の2Dランナーゲームです。ゲームにはソーシャルサインイン、複数段階のアップグレード(上位のアップグレードには下位のアップグレードを材料として使用)、コスメティックアップグレードがあり、すべてが組み込み型のノンカストディアルスマートコントラクトウォレットに保存されます。プレイヤーに対してトランザクション署名のポップアップやガス代の支払いは発生しません。

Google Play からこちらでダウンロードできます!

なぜスマートコントラクトウォレットなのかはこちら

組み込みウォレットとは何かはこちら

このガイドでは、Jelly Forest をどのように開発したか、そしてSequence の Unity SDKを使ってご自身の web3 ゲームを作る方法を解説します!

1. ゲームループを作成する

最初のステップは基本的なゲームループの構築です。まずはマネタイズ戦略や、どのように web3 要素を活用するかも考えておきましょう!

ゲームループには、Unity Asset Store で Infinite Runner Engine を購入しました。このアセット内のデモシーン JellyForest を少し調整することで、iOS と Android で動作するビルドを作成できました。

2. ソーシャルサインインと Sequence の Embedded Wallet ソリューションを統合する

設定

  1. Package Manager を使って Sequence の Unity SDK をインストールする
  2. Sequence Builder Console にサインインする
  3. Builder Console でゲーム用のプロジェクトを作成する
  4. Builder Console で Embedded Wallet をセットアップする
  5. SequenceConfig スクリプタブルオブジェクト(インストール時に Package Manager の Samples メニューからインポート)に、Builder で追加した Google および Apple のクライアントIDと、WaaSConfigKey に設定キーを入力してください。
    • Android と iOS のクライアントIDは、それぞれのプラットフォームに正しく設定するのを忘れずに!
  6. Builder Console から取得した Builder API キーSettings > API Access Keysprod キーに入力してください。

ソーシャルサインイン

  1. プレイヤーがログインするための基本的なシーンを作成しましょう。
  2. Canvas を作成し、Canvas Scaler コンポーネントを追加して「Scale with Screen Size」UIスケールモードを使用します。これにより、LoginPanel や他の UI 要素がビルドターゲットを切り替えても自動的にスケーリングされます。
  3. LoginPanel プレハブを Canvas の下にシーン階層へドラッグします。これは Project ウィンドウの Packages > Sequence Embedded Wallet SDK > SequenceFrontend > Prefabs にあります。
  4. UI マネージャーを作成し、LoginPanelOpen を呼び出せるようにします。実装例はこちら
private void Start()

{

    LoginPanel loginPanel = GetComponentInChildren<LoginPanel>();

    if (loginPanel == null)

    {

        Debug.LogError("LoginPanel not found!");

    }

    loginPanel.Open();

}
  1. 階層内で LoginPanel プレハブとの参照を切り離し、シーンビューで自由に編集できるようにします。
    1. 階層内で LoginPanel ゲームオブジェクトを選択します。
    2. 階層内で LoginPanel ゲームオブジェクトを右クリックします。
    3. Prefab > Unpack Completely を選択します。
  2. LoginPanel をゲームのテーマに合わせてカスタマイズしましょう。

LoginPanel はソーシャルサインインのロジックをすべて自動で処理します。実装方法が気になる場合は、LoginPageOpenIdAuthenticator の実装を確認してみてください。 認証は Open ID Connect Implicit Flow を利用しています。

Sequence API でセッションを登録する

ソーシャルサインインが完了すると、自動的に Sequence WaaS(Wallet as a Service)API へセッション登録リクエストが送信されます。仕組みは以下の通りです:

ソーシャルサインインが完了すると、OpenIdAuthenticator.SignedIn イベントが発火します。これにより SequenceLogin.ConnectToWaaS で認可プロセスが始まります。

ユーザーのウォレットを取得する

ウォレットを取得するには、SequenceWallet.OnWalletCreated イベントに購読する必要があります。

SequenceWallet.OnWalletCreated += OnWalletCreatedHandler;

public void OnWalletCreatedHandler(SequenceWallet wallet) {

  // Do something

}

「Sequence Embedded Wallet SDK」の Package Manager ページの Samples から「Useful Scripts」経由で SequenceConnector をインポートすることを強くおすすめします。初期コードが多数含まれており、SDK とのやりとりに便利なインターフェースとして機能します。JellyForest でもこのコードを多用しました

JellyForest では、SequenceWallet.OnWalletCreated イベントが発火した際に次のシーンを読み込む LevelLoader MonoBehaviour も作成しました。

private void Awake()

{

    SequenceWallet.OnWalletCreated += OnWalletCreated;

}



private void OnWalletCreated(SequenceWallet wallet)

{

    SceneManager.LoadScene("MenuScene");

}

Sequence の Embedded Wallet ソリューションの認証の仕組みについては、ドキュメントブログ記事もご覧ください。

3. コレクティブルコントラクトをデプロイする

プレイヤーがサインインしてウォレットを持てるようになったので、コレクティブルを追加しましょう!

ERC1155 コントラクトの利用を強くおすすめします。ゲームに最適な柔軟なトークン規格です。Builder Console から監査済みの ERC1155 実装を簡単にデプロイできます:

Jelly Forest でもこの方法を採用しました。

スマートコントラクトをデプロイしたら、「Gas Sponsoring」ページでコントラクトアドレスを Sponsored Address として追加するのを忘れずに!これにより、ゲームのスマートコントラクトとやりとりする際、ユーザーのガス代が自動的にあなたのコンピュートクレジットで肩代わりされます。

4. リモートミンターをデプロイする

Builder Console でデプロイした ERC1155 コントラクトは、デフォルトでトークンをミントするための権限が必要です。一見面倒に思えるかもしれませんが、これはとても重要です!これがなければ、誰でもコントラクトの mint メソッドを呼び出して無限にゲーム内アイテムを手に入れることができてしまいます。

Sequence ウォレット(または他のウォレット)を持つサーバーをデプロイし、Builder でミント権限を付与しましょう。

Jelly Forest での実装例

Jelly Forest では、ゲームプレイ中に集めたコインがすべて ERC1155 トークンとしてミントされます。実装方法は以下の通りです:

  1. Cloudflare に登録します(ミントサービスのコードをホストするためですが、他の方法でも構いません)
  2. ターミナルやコマンドラインを開きます
  3. git clone https://github.com/0xsequence-demos/cloudflare-worker-sequence-relayer.git を実行し、続いて cd cloudflare-worker-sequence-relayer
  4. git checkout permissionedMinter
  5. pnpm install で依存関係をインストールします
  6. wrangler をインストールします
pnpm install wrangler --save-dev

alias wrangler='./node_modules/.bin/wrangler'

そしてログインします

wrangler login
  1. wrangler.toml を開きます
    1. name の文字列を変更してサーバー名を設定します
    2. 新しいEOAウォレットを作成し、秘密鍵をエクスポートします。どのEOAウォレットでも構いません。Metamaskを使えば簡単にウォレットのセットアップ秘密鍵のエクスポートができます。秘密鍵の取り扱いには十分ご注意ください。パソコンに平文で保存したり、バージョン管理にコミットしたりしないでください!この秘密鍵をPKEYに設定します。
    3. CONTRACT_ADDRESSを設定します。
    4. PROJECT_ACCESS_KEYを設定します。これは、先ほどBuilder ConsoleでSequenceConfigスクリプタブルオブジェクトをセットアップした際に取得した本番用APIキーです。
    5. CHAIN_HANDLEを設定します。これが何かわからない場合は、Builder ConsoleのNode Gatewayページで各ネットワークのCHAIN_HANDLEを確認できます。
  2. pnpm dev - これでサーバーがローカルにデプロイされます。どのlocalhostでデプロイされたかはコマンドラインに表示されます。
  3. 別のコマンドラインウィンドウを開きます。
  4. curl http://localhost:8787 - 表示されたlocalhostに置き換えてください。これでサーバーにリクエストを送ります。
  5. ローカルサーバーが動作しているコマンドライン上で、ミンターのウォレットアドレスがログに表示されるはずです。
  6. このアドレスにBuilder Consoleでミント権限を付与します。
    1. Contractsから該当するコントラクトを探し、クリックして開きます。
    2. Write Contractをクリックします。
    3. grantRoleを展開します。
    4. roleには0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6を入力します。これはMINTER_ROLEのKeccak-256ハッシュ値です。
    5. accountにはミンターのウォレットアドレスを貼り付けます。
  7. wrangler deploy - これでCloudflare Workerにコードがデプロイされ、ミント用のURLが発行されます。

これで準備完了です!サーバーにPOSTリクエストを送るときは、C#で定義されたボディを使います。proofはミントリクエストを送るクライアント側で生成されます。Unity SDKでは、MintingRequestProverによって実装されています。

5. ゲーム内トークンをプレイヤーのインベントリにミントする

パーミッション付きミンターサーバーのセットアップが完了したので、クライアント側(Made With Unityアプリ)を連携させ、プレイヤーにゲームプレイを通じてトークンを付与できるようにします。Unity SDKのPermissionedMinter.MintTokenメソッドを呼び出すことで、パーミッション付きミンターにリクエストを送信できます。

Jelly Forestでは、プレイヤーがレベルを進むごとに多くのコインを集めます。これらはすべてERC1155トークンです。プレイヤーに快適なUXを提供するために、まだ解決すべき課題がいくつかあります。

  1. ユーザーのインベントリにどのトークンや権利があるかを、どのようにチェーンから読み取るのでしょうか?
  2. ブロックチェーンのトランザクションは、Arbitrumのような一部のチェーンでは高速ですが、即時ではありません。コイン(または他のアイテム)を集めてから、ゲーム内のインベントリに反映されるまで数秒待つ必要があるのは、一般的に良いユーザー体験とは言えません。
  3. 一見すると、ユーザーがゲーム内でトークンを獲得するたびにトランザクションを送信したくなるかもしれません。しかし、特にJelly Forestのように大量のコイン(トークン)を集めるゲームでは、膨大な数のトランザクションが発生し、ガス代が非常に高額になってしまいます!

それでは、Jelly ForestでUnity SDKを使ってこれらの課題をどのように解決したか見ていきましょう!

1. チェーンの読み取り

特定ユーザーのウォレット内トークンを読み取るのは複雑な作業ですが、SequenceのIndexerを使えば簡単です。Unity SDKでも実装されています

こちらはJelly Forestで、Indexerを使ってプレイヤーのウォレットからゲームのERC1155コントラクト内の全トークンを読み取るコード例です。

private Dictionary<BigInteger, TokenBalance> _tokenBalances = new Dictionary<BigInteger, TokenBalance>();

private async Task GetTokenBalances(Page page = null)

{

    if (page == null)

    {

        page = new Page();

    }

    GetTokenBalancesReturn balances = await _indexer.GetTokenBalances(new GetTokenBalancesArgs(_userAddress, SequenceConnector.ContractAddress, false, page));

    int uniqueTokens = balances.balances.Length;

    for (int i = 0; i < uniqueTokens; i++)

    {

        _tokenBalances[balances.balances[i].tokenID] = balances.balances[i];

    }

    if (balances.page.more)

    {

        await GetTokenBalances(balances.page);

    }

}

2. キャッシュの構築

ブロックチェーンのトランザクションは即時反映されませんが、ユーザーには即時のフィードバックを提供したいので、シンプルなインメモリキャッシュを活用します。

Jelly Forestで最初にSequenceWalletを受け取った際、SequenceConnector(ゲーム内でSequence SDKとやり取りする主なインターフェース)がInventoryを作成します。

private void OnWalletCreated(SequenceWallet wallet)

{

    Wallet = wallet;

    Wallet.OnSendTransactionComplete += OnSendTransactionCompleteHandler;

    Wallet.OnSendTransactionFailed += OnSendTransactionFailedHandler;

    Wallet.OnSignMessageComplete += OnSignMessageCompleteHandler;

    Wallet.OnDeployContractComplete += OnDeployContractCompleteHandler;

    Wallet.OnDeployContractFailed += OnDeployContractFailedHandler;

    Wallet.OnDropSessionComplete += OnDropSessionCompleteHandler;

    Wallet.OnSessionsFound += OnSessionsFoundHandler;



    Inventory = new Inventory(Indexer, Wallet.GetWalletAddress(), ItemCatalogue); // [!code focus]



    _transactionQueuer.Setup(Wallet, Chain);

    _permissionedMinterTransactionQueuer.Setup(Wallet, Chain, "https://sequence-relayer-jelly-forest2.tpin.workers.dev/", ContractAddress);

}

このInventoryは、ゲーム内でシンプルなキャッシュとして使われます。作成時や必要に応じて、Indexerを使ってユーザーのウォレット内トークンを取得します。その後、ユーザーがトークンを獲得するたびにキャッシュ(Inventory)とオンチェーンデータを更新します。

Inventoryの全実装はこちらでご覧いただけます

3. トランザクションキューの活用

SequenceのUnity SDKには、とても柔軟なトランザクションキューイングシステムが用意されています。

Jelly Forestでは、PermissionedMinterTransactionQueuerのMonoBehaviourをSequenceConnectorのGameObjectにアタッチし、Awakeで参照を取得しています

この設定が完了したら、トークンを集めたときに「ミントトークン」を呼び出すだけです。

public class CollectibleToken : Coin

{

    protected override void ObjectPicked()

    {

        base.ObjectPicked();

        if (SequenceConnector.Instance == null || SequenceConnector.Instance.Wallet == null)

        {

            Debug.LogWarning("No minting will happen. Make sure SequenceConnector is in the scene and user is logged in.");

            return;

        }

        SequenceConnector.Instance.MintFungibleToken(); // [!code focus]

    }

}

これによりInventoryが更新され、ミントトランザクションがPermissionedMinterTransactionQueuerのキューに追加されます。PermissionedMinterTransactionQueuerは、可能な限りトランザクションを自動的にまとめて、ガス代を最小限に抑えます。

Jelly Forestでは、プレイヤーがゲームオーバーになるたび、ただし、30秒未満の間隔では送信されないように設定しています。

どのくらいの頻度でトランザクションを送信するべきでしょうか?

Unity SDKを使えば、これは技術的な問題というよりゲームデザインの問題になります。

TransactionQueuersは、X秒ごとに自動で送信、関数呼び出しで促されたときに送信(ただしY秒未満では送信しない)、または促されたときに最小間隔(Y秒)を無視して即時送信、など柔軟に設定できます。

トランザクションキューアの設定を決める際に考慮すべきポイントをいくつか挙げます:

  • トランザクション送信頻度が高いほど、ガス代も多くかかります。もちろん、選択するEVM互換ブロックチェーンによって、コストが許容範囲かどうかや送信できるトランザクション数・複雑さは大きく変わります。
  • トランザクション送信頻度が低いほど、ゲーム内の状態(キャッシュ)とオンチェーン情報のズレが大きくなります。もしトランザクションが失敗した場合、プレイヤー体験を損なわずに復旧する方法が必要です。

Jelly Forestの例では、ショップでのトランザクションはユーザーにとって非常に重要だと考えました。ユーザーがアップグレードや帽子を手に入れたと思っても、トランザクションが失敗して取り消す必要が出たり、正当に獲得していないアイテムを余分にミントする事態は避けたいと考えました。そのため、ショップページでは購入トランザクション(およびTransactionQueuers内の全トランザクション)が完了するまでユーザーに待ってもらう仕様にしました。

public async Task Buy()

{

    if (Status != ItemStatus.Available)

    {

        return;

    }



    if (SequenceConnector.Instance == null)

    {

        string error = "SequenceConnector not found. User has not logged in";

        Debug.LogError($"Failed to purchase shop item: {error}");

        OnFailedToPurchaseShopItem?.Invoke($"Failed to purchase shop item: {error}");

        return;



    SequenceConnector.Instance.AddToTransactionQueue(new PurchaseShopItemQueueableTransaction(this));

    TransactionReturn result = await SequenceConnector.Instance.SubmitQueuedTransactions(true, false); // [!code hl]

    if (result is SuccessfulTransactionReturn successfulTransactionReturn)

    {

        BurnTokensFromInventory();

        MintTokenInInventory()

        if (string.IsNullOrWhiteSpace(successfulTransactionReturn.txHash))

        {

            GetTransactionReceipt(successfulTransactionReturn);

        }

    }

    else if (result is FailedTransactionReturn failed)

    {

        string error = $"Transaction failed: {failed.error}";

        Debug.LogError(error);

        OnFailedToPurchaseShopItem?.Invoke($"Failed to purchase shop item: {error}");

    }

    else

    {

        throw new Exception("Unexpected transaction result type");

    }

}

6. ゲーム内トークンを他のトークンと交換してバーンする

Jelly Forestでは、コインや(場合によっては)下位のパワーアップをバーンすることで、パワーアップやコスメティックアイテムを購入できます。

この仕組みを有効化し、強制するために、シンプルなBurnToMintスマートコントラクトをデプロイしました。このコントラクトでは、特定のトークンIDに対してミント要件(必要なトークンIDとその数量)を指定できます。ERC1155トークンのバッチを受け取り、送信者がdataパラメータでミントしたいトークンIDを指定すると、コントラクトは各トークンIDの必要数を受け取ったかどうかを確認します。条件を満たしていれば、コントラクトはトークンをバーンし、指定されたトークンIDを送信者(ユーザー)にミントします。条件を満たさない場合は、トランザクションが失敗し、ロールバックされます。

このコントラクトには、Builder Consoleでゲームコントラクトのミント権限を付与しています。

  1. Contractsから該当するコントラクトを探し、クリックして開きます。
  2. Write Contractをクリックします。
  3. grantRoleを展開します。
  4. roleには0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6を入力します。これはMINTER_ROLEのKeccak-256ハッシュ値です。
  5. accountにはミンターのウォレットアドレスを貼り付けます。

:::danger 警告:上記で共有したBurnToMintスマートコントラクトは、第三者による監査を受けていません。再利用する際はご注意ください! :::

ユーザーがショップでアップグレードやコスメティックアイテムを購入すると、PurchaseShopItemQueueableTransactionSequenceConnector内のSequenceWalletTransactionQueuerに追加し、BurnToMintスマートコントラクトへトランザクションを送信します。

SequenceConnector.Instance.AddToTransactionQueue(new PurchaseShopItemQueueableTransaction(this));

7. ショップページの構築とミント要件の設定

Jelly Forestのショップページを作成し、各アップグレードや帽子の価格・ミント要件を設定する際、Scriptable Objectsを使ってShopItemsを定義することにしました。これにより、Inspectorでシリアライズできるため、調整や可視化が簡単です。また、これらのScriptable Objectを使って、各Itemの内容やトークンIDとの紐付けも行っています。

しかし、Scriptable Objectで定義したミント要件と、オンチェーンのBurnToMintコントラクトで定義したミント要件を同期させるのは手間がかかり、バグの原因にもなりやすいことが分かりました。

そこで、ShopItemのScriptable Object用にエディタ拡張を作成し、ボタンを追加しました。このボタンを押すと、オンチェーンで定義されているミント要件とScriptable Objectの内容が一致しているかを確認し、異なっていればScriptable Objectに合わせてBurnToMintコントラクトのミント要件を更新するトランザクションを送信します。トランザクションは、開発者のマシンに環境変数として保存された秘密鍵から作成されたEOAウォレットを使って送信されます。このEOAウォレットは、このコントラクトのオーナーです。

実際、ショップページは60秒ごと(およびページを開くたび)にスマートコントラクトへミント要件の変更を問い合わせ、UIを自動で更新しています。これにより、ゲームの経済バランスをアップデートなしでリアルタイムに調整できます!

下の動画をクリックしてください

ShopItemEditorExtensionの実装はこちらをご覧ください。

8. 購入したアイテムをゲーム内で活用する

これで、プレイヤーはログインしてウォレットを取得し、トークンを獲得し、そのトークンでアイテムを購入できるようになりました。あとは、プレイヤーがアイテムを欲しくなる理由を作るだけです。つまり、ここからはゲーム開発者としての腕の見せ所です。魅力的なパワーアップやコスメティックを作りましょう!

ゲーム内でトークンを活用するには、ユーザーが指定したトークンIDを十分に所有しているかを確認し、そのトークンの効果を適用するだけです。

Jelly Forestでは、いくつかのPowerUpTypesを定義し、各ItemPowerUpTypeとティアを割り当てています。そして、プレイヤーが所有する各タイプの最強パワーアップをInventoryから検索する仕組みを作っています。