0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブロックチェーンゲームの作り方7 ゲーム内通貨購入

Last updated at Posted at 2025-01-02

Previous << 6 - プレイヤー登録
Next >> 8 - デッキ編集

Flowブロックチェーンの強みは実際に自分の強みをお金に変えられること

他業種からIT業界に転身した人なら、ITの知識を活用すれば自分一人でお金を稼げることをいつか知ることになります。

しかし、競争の激しい分野はそれだけ稼ぐのが難しくなります。

でもやりがいは残ります。経済の隆盛は様々な要因が絡みます。その中で伸びる業界、というのはお金を稼げる、と注目される業界です。(2010年頃はそれはそれはGoogleやFacebookのニュースが沢山Bloomberg記事に載ったものです。ITエンジニアとしてはBloombergが技術情報を提供してくれることに不思議な気持ちでした)

Flowブロックチェーンは世界中の人に対して、自分が作ったサービスでお金を稼ぐことができます。金融決済処理を安全で簡単に24時間、コストをかけずに世界中に提供できるためです。

安全でコストをかけない金融決済処理を自分1人だけで作れるのがFlowを選ぶメリットです。

以下のコードを見てください、ストレージからお金を引き出しています。Linear Typesという特殊なコードの実装でそれをスマートコントラクトに渡します。スマートコントラクトにはサービスを展開した人の入金先がありますのでそこに入金します。
このコードはブロックチェーン上で動き、そのコンピュータを用意する必要はありません。世界中で24時間そのコードは動き続け、コンピュータが止まることの心配および運営費用の心配をする必要はありません。トランザクション費用はウォレットが代わりに払ってくれます。是非、サンプルアプリを動かしてその凄さを実感してみてください

      transaction() {
        prepare(signer: auth(BorrowValue) &Account) {
          let payment <- signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!.withdraw(amount: 2.0) as! @FlowToken.Vault

          let player = signer.storage.borrow<&AwesomeCardGame.Player>(from: /storage/AwesomeCardGamePlayer)
              ?? panic("Could not borrow reference to the Owner's Player Resource.")
          player.buy_en(payment: <- payment)
        }        

Linear TypesはAptos, Sui, Flowブロックチェーンに共通する資産を実際に移動させるロジックです

Why Flow

FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。

ということでやっていきます、P2P(ピアツーピア)決済アプリ開発!

💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。

現在のゲーム内通貨残高を表示する

AwesomeCardGame.cdcPlayerリソースの定義に以下のようにget_player_scoreメソッドを追加します。

AwesomeCardGame.cdc
           :

  access(all) resource Player {
    access(all) let player_id: UInt
    access(all) let nickname: String

    init(nickname: String) {
      AwesomeCardGame.totalPlayers = AwesomeCardGame.totalPlayers + 1
      self.player_id = AwesomeCardGame.totalPlayers
      self.nickname = nickname

      AwesomeCardGame.playerList[self.player_id] = CyberScoreStruct(player_name: nickname)
    }

    /* 以下を追加 */
    access(all) fun get_player_score(): CyberScoreStruct {
      return AwesomeCardGame.playerList[self.player_id]!
    }
  }

           :

デプロイ済みのAwesomeCardGameスマートコントラクトの更新自体は以下のコマンドでできるのですが

flow accounts update-contract ./AwesomeCardGame.cdc

実はリソースをストレージに保存済みである場合、(Day6でPlayerリソースをアカウントストレージに保存していましたので)リソースが古い状態でありget_player_scoreを呼び出そうとしてもunknown memberとなり呼び出しに失敗します。
そのためエミュレータアカウントがリソースを持たない状態まで戻す必要があり、一度エミュレータを止めて、エミュレータを再起動する必要があります。そうすると、アカウントのリソースも消えてなくなります。
よって、エミュレータアカウントにリソースを保存した状態で、スマートコントラクトのリソース定義を変更した場合の確認手順は

  1. エミュレータを止める
  2. flow emulator -vコマンドでエミュレータを起動する
  3. flow dev-walletコマンドでDev Walletを起動する
  4. flow project deployコマンドでAwesomeCardGameスマートコントラクトをデプロイする
  5. 新規登録ボタンを押してPlayerリソースを保存する
  6. これでもうまくいかない時はブラウザ上でDevWalletのログインをし直す(ログイン情報が古い為)

となります。

Day5のFlowToken所持金取得スクリプトを以下のように加筆します。

get_balance.cdc
    import "AwesomeCardGame"
    import "FlowToken"
    import "FungibleToken"

    access(all) fun main(address: Address): [AnyStruct] {
      let data: [AnyStruct] = []
      let vaultRef = getAccount(address).capabilities
          .borrow<&FlowToken.Vault>(/public/flowTokenBalance)
        ?? panic("Something wrong happened.")
      let cap = getAccount(address).capabilities
          .borrow<&AwesomeCardGame.Player>(/public/AwesomeCardGamePlayer)
        ?? panic("Doesn't have capability!")

      data.append(vaultRef.balance)
      data.append(cap.get_player_score().cyber_energy)
      return data
    }

flowTokenBalanceはUFix64、ゲーム内通貨(cyber_energy)はUInt8であるため、一度にこれらの値を返却できるように、返却値の型を[AnyStruct]としました。AnyStructは全ての一般型の上位互換型です。

これをfcl.queryで取得できるようにして、次に+page.svelteを以下のように修正します。

+page.svelte
           :
let flowBalance;
let cyberEnergyBalance; <- 追加
let hasResource;
config({
  'flow.network': network,
  'accessNode.api': 'http://localhost:8888',
  'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
currentUser.subscribe(async (user) => {
  walletUser = user;
  if (user.addr) {
    hasResource = await isRegistered(user.addr);
    if (hasResource) { <- 修正
      const [flowTokenBalance, cyberEnergy] = await getBalance(user.addr); <- 修正
      flowBalance = flowTokenBalance; <- 修正
      cyberEnergyBalance = cyberEnergy; <- 修正
    }
  }
});
</script>
{#if !walletUser?.addr}
  <button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
  FLOW残高: {flowBalance} / ゲーム内通貨: {cyberEnergyBalance} <- 修正
           :

この状態で画面を表示すると以下のようにゲーム内通貨の残高を表示できます。
スクリーンショット 2025-01-02 10.08.24.png

リソースのニックネームも一緒に表示する

Day6でPlayerリソースを新規登録した時にnicknameも登録していたのでそれも一緒に画面に表示します。

get_balance.cdc
           :
      data.append(vaultRef.balance)
      data.append(cap.get_player_score().cyber_energy)
      data.append(cap.get_player_score().player_name) <- 追加
      return data
    }
+page.svelte
           :
let playerName; <- 追加
let flowBalance;
let cyberEnergyBalance;
let hasResource;
config({
  'flow.network': network,
  'accessNode.api': 'http://localhost:8888',
  'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
currentUser.subscribe(async (user) => {
  walletUser = user;
  if (user.addr) {
    hasResource = await isRegistered(user.addr);
    if (hasResource) {
      const [flowTokenBalance, cyberEnergy, pName] = await getBalance(user.addr); <- 修正
      flowBalance = flowTokenBalance;
      cyberEnergyBalance = cyberEnergy;
      playerName = pName; <- 追加
    }
  }
});
</script>
{#if !walletUser?.addr}
  <button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
  <b>{playerName}さん</b> FLOW残高: {flowBalance} / ゲーム内通貨: {cyberEnergyBalance}<br> <- 修正
           :

この状態で画面を表示すると以下のようにプレイヤーが登録したnicknameも表示することができます。
スクリーンショット 2025-01-02 10.19.19.png

ゲーム内通貨を購入する

では画面上に表示されているFLOW残高を使ってゲーム内通貨を購入していきます。

なぜFLOWがあるのにゲーム内通貨を購入するかというと、それはFLOWはアカウントのストレージにあるのでユーザーの許可なしには動かせないのに対して、ゲーム内通貨はスマートコントラクトの中にあるのでAdminリソースが自由に変更できるからです。

なぜ、これが利点かというと、アカウントのストレージにFLOWがあるということは、これを引き出すときには必ずウォレットのポップアップでApproveボタンを押す必要があります。

これが割と面倒くさいのです。ポップアップでApproveするとFLOWを自分のウォレットから引き出すことができるのですが、ゲームは短時間に何回も入金する場合があります。その度にポップアップでApproveするのは面倒くさいので、あらかじめゲーム3~4回分ぐらいあらかじめ購入できるように、ゲーム内通貨が登場します。

AwesomeCardGame.cdcPlayerリソースの定義に以下のようにbuy_enメソッドを追加します。

AwesomeCardGame.cdc
           :

  access(all) resource Player {
    access(all) let player_id: UInt
    access(all) let nickname: String

    init(nickname: String) {
      AwesomeCardGame.totalPlayers = AwesomeCardGame.totalPlayers + 1
      self.player_id = AwesomeCardGame.totalPlayers
      self.nickname = nickname

      AwesomeCardGame.playerList[self.player_id] = CyberScoreStruct(player_name: nickname)
    }

    access(all) fun get_player_score(): CyberScoreStruct {
      return AwesomeCardGame.playerList[self.player_id]!
    }

    /* 以下を追加 */
    access(all) fun buy_en(payment: @FlowToken.Vault) {
      pre {
        payment.balance == 2.0: "payment is not 2FLOW coin."
        AwesomeCardGame.playerList[self.player_id] != nil: "CyberScoreStruct not found."
      }
      AwesomeCardGame.FlowTokenVault.borrow()!.deposit(from: <- payment)
      if let cyberScore = AwesomeCardGame.playerList[self.player_id] {
        cyberScore.set_cyber_energy(new_value: cyberScore.cyber_energy + 100)
        AwesomeCardGame.playerList[self.player_id] = cyberScore
      }
    }
  }

           :

すみません、entitlementによるアクセス制限の実装を忘れてました(access(all)はよい実装ではないです)。entitlementについてはDay3を参照して下さい🙇

ユーザーから受け取った2FLOWをAdminのVaultに入れて(deposit)、代わりにユーザーのゲーム内通貨(cyber_energy)をプラス100しています。(このAdminのVaultに入ったFLOWはDay4のやり方で取引所とかにすぐに送金・換金できます!)

AwesomeCardGame.cdcCyberScoreStructに以下のようにset_cyber_energyというSetter関数を追加します。
(access(all)のStruct(構造体)のパラメータは、Setter関数が無いと、更新しようとする時にエラーになるためです)

AwesomeCardGame.cdc
           :
  access(all) struct CyberScoreStruct {
    access(all) let player_name: String
    access(all) var score: [{UFix64: UInt8}]
    access(all) var win_count: UInt
    access(all) var loss_count: UInt
    access(all) var ranking_win_count: UInt
    access(all) var ranking_2nd_win_count: UInt
    access(all) var period_win_count: UInt
    access(all) var period_loss_count: UInt
    access(all) var cyber_energy: UInt8
    access(all) var balance: UFix64

    /* 以下を追加 */
    access(contract) fun set_cyber_energy(new_value: UInt8) {
      self.cyber_energy = new_value
    }

    init(player_name: String) {
      self.player_name = player_name
      self.score = []
      self.win_count = 0
      self.loss_count = 0
      self.ranking_win_count = 0
      self.ranking_2nd_win_count = 0
      self.period_win_count = 0
      self.period_loss_count = 0
      self.cyber_energy = 0
      self.balance = 0.0
    }
  }

           :

このbuy_en関数を実行するトランザクションは以下のようになります。

      import "AwesomeCardGame"
      import "FlowToken"
      import "FungibleToken"

      transaction() {
        prepare(signer: auth(BorrowValue) &Account) {
          let payment <- signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!.withdraw(amount: 2.0) as! @FlowToken.Vault

          let player = signer.storage.borrow<&AwesomeCardGame.Player>(from: /storage/AwesomeCardGamePlayer)
              ?? panic("Could not borrow reference to the Owner's Player Resource.")
          player.buy_en(payment: <- payment)
        }
        execute {
          log("success")
        }
      }

ユーザーのウォレットから2FLOW引き出し、それを先ほどのメソッド、buy_enメソッドに渡しています。なぜborrowをしているのか、と不思議に思われるかもしれませんが、リソースをストレージの中から取り出して、リソースのbuy_enメソッドを実行し、リソースを再びストレージに格納し直してもいいです。ただし、それはコンピューティングコストがかかり、トランザクションフィーが上がります。そこで、トランザクションフィーを安く抑えるために、ストレージ内のリソースをborrowによって(C言語のポインタのように)参照だけ取得しています。また、再格納が必要ないのでコード量も減ります。buy_enメソッドのアクセス制御がaccess(all)ではなく、entitlementを設定していればbuy_enメソッドはstorage内のリソース参照からしか実行できず、つまりリソースの持ち主からしか実行できなくなります。まぁ代わりにゲーム料金を払ってくれるぐらいで、今回はaccess(all)でも害がないですが。。

これをjsで呼ぶ必要がありますので、そのメソッドは以下のようになります。

transactions.js
import { mutate, authz } from "@onflow/fcl";

export const buyCyberEn = async function () {
  const txId = await mutate({
    cadence: `
      import "AwesomeCardGame"
      import "FlowToken"
      import "FungibleToken"

      transaction() {
        prepare(signer: auth(BorrowValue) &Account) {
          let payment <- signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!.withdraw(amount: 2.0) as! @FlowToken.Vault

          let player = signer.storage.borrow<&AwesomeCardGame.Player>(from: /storage/AwesomeCardGamePlayer)
              ?? panic("Could not borrow reference to the Owner's Player Resource.")
          player.buy_en(payment: <- payment)
        }
        execute {
          log("success")
        }
      }
    `,
    args: (arg, t) => [],
    proposer: authz,
    payer: authz,
    authorizations: [authz],
    limit: 999,
  });
  console.log(txId);
  return txId;
};

+page.svelteに以下のコードを追加します

+page.svelte
import { createPlayer, buyCyberEn } from '../transactions'

           :
  {#if hasResource}
    Playerリソース作成済みです
    <br>
    <button onclick={buyCyberEn}>CyberEnergy購入</button>
  {/if}

これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。

結果:
スクリーンショット 2025-01-02 12.24.45.png

結果は、Flow残高が減ってないけど、ゲーム内通貨が100に増えています。

これは、スマートコントラクトをデプロイした人(emulator-account)がPlayerリソースを使って購入した人と同一人物なので、購入処理で一度2FlowTokenを引き出しはしたものの、スマートコントラクトの中でFlowTokenVaultに2FlowTokenを預け入れたので、結局同じ額に戻ってしまった結果でした。

そこで一度ログアウトして新しいアカウントを作成してみます。新しいアカウントでログインしPlayerリソースを作成・保存し、それを通じてゲーム内通貨購入を試してみます。

Create New Accountをクリックします。
スクリーンショット 2025-01-02 12.32.00.png

Createをクリックします:
スクリーンショット 2025-01-02 12.33.03.png

アカウントが作られました。(FlowTokenもたくさん持っています。)
スクリーンショット 2025-01-02 12.33.45.png
(何故舌を出す... Lukeの仕業か..)

Account Aでログインして、新規登録します。(nicknameはコード直打ちでしたが少し変更しました)
スクリーンショット 2025-01-02 12.37.10.png

これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。

結果
スクリーンショット 2025-01-02 12.39.47.png

2FLOWを支払ってゲーム内通貨100(CyberEnergy)を購入しました。(今度はちゃんとFLOW残高が2減っています)

ここで、元のemulator-accountのアカウントに戻るとどうなるでしょう?一度ログアウトしてemulator-accountのアカウントでログインし直してみましょう。

結果:
スクリーンショット 2025-01-02 12.42.21.png

前は999999999.995だったのが1000000001.994に増えてます。つまり、1.999FLOW増えたことになっています。(0.001は先ほどアカウントを作成した時に負担したトランザクション費用です)

エミュレータが計算する0.001FLOWなどのトランザクション費用は考えられる限り最も高い費用が設定されています。実際は0.000003FLOW未満になると考えてよく、非常に少額になります。(Flowの転送だけなら$0.00000185FLOWだけです。)

これでピアツーピア決済アプリの中枢部分が実装できたことになります。

Mainnetにデプロイしたらもうあなたは世界レベルの決済アプリ事業者の一員です。
(MainnetデプロイはFlow CLIで鍵を作成して、Flow CLIでデプロイして行います)


この記事のソースコードはこちらにあります。


Previous << 6 - プレイヤー登録

Flow blockchain / Cadence version1.0ドキュメント

Next >> 8 - デッキ編集

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?