eSportsを作る上でブロックチェーンに接続する部分について解説していきます。
ブロックチェーンゲームを作る上で、開発ツールが揃っている点や、スマートコントラクトを書く言語の書きやすさ、ビルドまで手が止まらずに開発を行える点などは重要な選定基準です。
Flow blockchainはこれらの利点が他のブロックチェーンに比べて圧倒的に優れているので、このブロックチェーンを使って私は開発していきます。また、ブロックチェーンのコア開発者がGithub上でIssueに迅速に対応しているかなどのメンテナンスもしっかりしている点も重要な選定基準です。
スマートコントラクトを書いてゲームプレイヤーの情報を記録
以下のコマンドを実行してFlow CLIを最新にします(インストールします)。
brew install flow-cli # Mac用のコマンド
flow versionコマンドを実行すると、私の場合はVersion: v2.2.19
と表示されました。
Flowプロジェクトの初期化処理
以下のコマンドを実行します。
flow init
このコマンドを実行すると、インストールしたいコアコントラクトについて以下のように聞かれます。
必要なのは主要な上記6つぐらいでしょうか。これらはエミュレータ環境にインストールされ、スマートコントラクトをエミュレータにデプロイする時に依存関係として必要になるのだと思うのですが、エミュレータを起動すると大体全部見えているので少し利用場所が不明です。
コマンドが完了するとプロジェクトルートで実行したのであればその階層にフォルダが作成されます。エミュレータを起動するのであればこのフォルダに入ってコマンドを実行することになります。その為、私はフォルダ名をflow_blockchainに変更しました。
エミュレータを立ち上げる
Terminalで新しいタブを開いて以下のコマンドを実行することでエミュレータが起動します。
cd flow_blockchain
flow emulator
スマートコントラクトを書いていく
私の環境ではflow_blockchain/cadence/contracts/
にCounter.cdcがありました。
これを利用して書き変えていきます。ファイル名をOragaESports.cdcに変更します。
flow_blockchain/flow.json
内のCounterとなっているところもOragaESportsに書き換えておきます。
試しに現在の状態でデプロイができるか確認してみます。flow.json
に以下を追記します。
"deployments": {
"emulator": {
"emulator-account": ["OragaESports"]
}
},
そして以下のコマンドを実行します。(現在のTerminalはエミュレータが動いているはずなので新しいタブを開いて実行します)
flow project deploy
OragaESports -> 0xf8d6e0586b0a20c7
のように表示されていればデプロイされています。
ユーザーの所持金を表示
まずはスマートコントラクトがなくてもできることを実装します。
ゲームの開始画面になったらウォレットに接続するように促します。そのためにFlow blockchainのウォレットなどに繋ぐためのライブラリをインストールします。
npm i @onflow/fcl
ユーザーの所持金を表示するスクリプトを用意します。
import { query } from "@onflow/fcl";
export const getBalance = async function (address) {
const result = await query({
cadence: `
import "FlowToken"
import "FungibleToken"
access(all) fun main(address: Address): UFix64 {
let vaultRef = getAccount(address).capabilities
.borrow<&FlowToken.Vault>(/public/flowTokenBalance)
?? panic("Something wrong happened.")
return vaultRef.balance
}
`,
args: (arg, t) => [arg(address, t.Address)],
});
return result;
};
このコードをflow_blockchain/scripts.jsに用意します。
画面に表示したいファイルに対し以下のimport文を書きます。(パスは構成に合わせて変更してください)
import { config, authenticate, currentUser } from '@onflow/fcl';
import { getBalance } from '../../flow_blockchain/scripts';
import flowJSON from '../../flow_blockchain/flow.json';
Flow blockchainはウォレット接続にウォレットDiscovery方式を採用しています。その接続形式をconfig関数の中で書きます。以下のコードを書いてください。
config({
'flow.network': 'emulator',
'accessNode.api': 'http://localhost:8888',
'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
let flowBalance;
currentUser.subscribe(async (user) => {
if (user.addr) {
flowBalance = await getBalance(user.addr);
} else {
}
});
このコードで、現在ウォレットに接続しているユーザー情報を、取得することができます。上記のelse文の中にウォレットにログインしていない場合の処理が来ます。
ウォレットにログインしてもらえなければeSportsも成立しないので以下のコードを書いてウォレットにログインしてもらいます。
(./Dialog.svelteファイル)
<script>
export let dialog;
</script>
<dialog bind:this={dialog} on:close>
<div>You need a crypto wallet.<br>(You can sign out anytime)</div>
<slot />
</dialog>
(ゲーム画面のファイル)
import Dialog from './Dialog.svelte';
} else {
modal.showModal();
}
</script>
<Dialog bind:dialog={modal}>
<button on:click={() => {
authenticate()
modal.close()
}}>SignIn</button>
</Dialog>
dialogについては現在のweb仕様ですでに確立されているものを利用しただけであり、この状態で画面をもう一度表示すると、以下が表示されるはずです。
( 緑色なのはそのCSSがDialog.svelteに書かれています。また、それ以外に画面に出ている数値はまだ固定値です。これからブロックチェーンから取得した値に置き換えていきます。)
ウォレットログイン
ボタンを押します。
の前に..まだエミュレータしか起動していなかったですね。
危ない、危ない、この状態でボタンを押してもエラーになります。Googleで検索してもなかなか情報はありませんので、手順をちゃんと踏んでいるか気をつけましょうね..。flow emulatorを実行しているターミナルの別タブで以下を実行しておきます。
flow dev-wallet
これでエミュレータ用のウォレットDiscoveryが準備できました。(エラー内容を見ても一般論しか書いていないのでまず分かりません。手順をしっかり確認することが大事です。)
ボタンを押すとこの画面が出ますので、ログインをユーザーにしてもらって所持金を見てみましょう。(ちなみに格好悪いのはエミュレータ専用だからです。)
もう一度2つ上のソースコードを見てみます。
let flowBalance;
currentUser.subscribe(async (user) => {
if (user.addr) {
flowBalance = await getBalance(user.addr);
} else {
modal.showModal();
}
});
こうなっていました。ユーザーがログインするとこのsubscribeコールバックはもう一度呼ばれます。(一度目は画面初期表示時に呼ばれていました。)
すると、ログインした状態ですから、userがあります。その中にはaddr(ブロックチェーンのユーザーが持つアドレス)があり、今度はif文の中を通ります。そしてflow_blockchain/scripts.js
に書いたgetBalance
のソースコードが実行されます。そこではfclを使ってブロックチェーンと通信し、ユーザーのクリプト残高を取得しています。それを画面に表示します。
<p class="allura">
Your Balance: <img src="/assets/flow_fire.png" alt="$FLOW" />
<span class="flow_balance">1.2</span>
</p>
ここをこうします。
<p class="allura">
Your Balance: <img src="/assets/flow_fire.png" alt="$FLOW" />
<span class="flow_balance">{Math.trunc(flowBalance * 100) / 100}</span>
</p>
この状態で画面を表示します。(少数2桁ではなく3桁ぐらいまでは表示したほうがいいですね。)
(エミュレータによるユーザですので非常~にお金持ちです)
実際は100000も持ってたら結構富豪。
はい、所持金残高を表示できました。次からはスマートコントラクトを書いていきます。
スマートコントラクトを設計する
必要な要件はシンプルです。
- お金を入金できる。ゲームを開始できる。
- ゲームに勝利した時に賞金を受け取れる
- ゲームに負けた場合は賞金額に1FLOWを上乗せする
- FLOWを持っていない人のために1回だけ無料プレイできるようにTip jar機能を用意する(Tip jarから1FLOWを取り出してそれでゲームする)
- Tip jarに寄付(1FLOW)できる
- 勝敗の記録をブロックチェーンに残せる
以上です。ですが、とっても強力な機能です。
順番に書いていきましょう。まず、勝敗の記録を残すためにはユーザ情報が必要ですね。
import "FlowToken"
import "FungibleToken"
access(all) contract OragaESports {
書き出しはこうします。eSportsで、ネイティブ通貨をやり取りする場合はFlowToken
とFungibleToken
が必要です。これ以外にも例えばFlowScanで資産を移動したときにその画像をFlowScanの中で表示させたい場合はMetadataViews
やViewResolver
、NonFungibleToken
などをimportします(おそらく..です。私はそこは詳しくありません)。とりあえず、FlowScanでリソースの画像が出なくてもいい、と言う場合は上記2つのコントラクトだけimportします。
import "FlowToken"
import "FungibleToken"
access(all) contract OragaESports {
access(self) var totalCount: UInt
access(self) let GamerFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}
access(all) event GamerCreatted(gamer_id: UInt)
access(all) resource Gamer {
access(all) let gamer_id: UInt
access(all) let nickname: String
init(nickname: String) {
OragaESports.totalCount = OragaESports.totalCount + 1
self.gamer_id = OragaESports.totalCount
self.nickname = nickname
emit GamerCreatted(gamer_id: self.gamer_id)
}
}
access(all) fun createGamer(nickname: String, flow_vault_receiver: Capability<&{FungibleToken.Receiver}>): @OragaESports.Gamer {
let gamer <- create Gamer(nickname: nickname)
if (OragaESports.GamerFlowTokenVault[gamer.gamer_id] == nil) {
OragaESports.GamerFlowTokenVault[gamer.gamer_id] = flow_vault_receiver
}
return <- gamer
}
init() {
self.totalCount = 0
self.GamerFlowTokenVault = {}
}
}
ここまで書いて、デプロイします。flow project deploy
コマンドを叩きます。もし、すでにコントラクトが存在しています、というエラーが出たらエミュレータを再起動してもう一回デプロイコマンドを実行します。
ここで実装したのは要件のうち、
・記録が残せるようにプレイヤーの名前を保存する(ゲームプレイヤーの情報は連番のIDで管理)
・ゲームに勝利した時に賞金を受け取れるようにゲームユーザの入金口をスマートコントラクト内に保存する
・イベントを飛ばしてブロックチェーン(FlowScanなど)に残せる記録とする
です。このコントラクトを使ってゲームプレイヤーに名前を入力してもらって入金口を保存してもらいます。
次はフロントエンドに戻ってきます。2つのメソッドを実装します。
- 名前を登録しているか(=リソースを持っているか)をチェックするメソッド
- 名前と入金口を登録するメソッド
1のメソッドでチェックをして、それがfalseであれば2を実行します。
flow_blockchain/scripts.js
のgetBalance
メソッドの下に以下を実装します。
export const isRegistered = async function (address) {
const result = await query({
cadence: `
import "OragaESports"
access(all) fun main(address: Address): &OragaESports.Gamer? {
return getAccount(address).capabilities.get<&OragaESports.Gamer>(/public/OragaESportsGamer).borrow()
}
`,
args: (arg, t) => [arg(address, t.Address)],
});
return result;
};
ゲーム画面のファイルにisRegistered
のimportを追加します。
import { getBalance, isRegistered } from '../../flow_blockchain/scripts';
そして残高を取得するタイミングと同時にリソースを持っているか確認します。
let flowBalance;
let hasResource;
let modal;
currentUser.subscribe(async (user) => {
if (user.addr) {
flowBalance = await getBalance(user.addr);
hasResource = await isRegistered(user.addr);
console.log(hasResource)
} else {
modal.showModal();
}
});
この状態でゲーム画面を表示します。コンソールログには以下のように表示されます。
null
ですね。hasResource
がnull
であれば登録するロジックを実装します。
flow_blockchain/transactions.js
と言うファイルを作りcreateGamerというメソッドを実装します。
import { mutate, authz } from "@onflow/fcl";
export const createGamer = async function (nickname) {
const txId = await mutate({
cadence: `
import "OragaESports"
import "FlowToken"
import "FungibleToken"
transaction(nickname: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let FlowTokenReceiver = signer.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
signer.storage.save(<- OragaESports.createGamer(nickname: nickname, flow_vault_receiver: FlowTokenReceiver), to: /storage/OragaESportsGamer)
let cap = signer.capabilities.storage.issue<&OragaESports.Gamer>(/storage/OragaESportsGamer)
signer.capabilities.publish(cap, at: /public/OragaESportsGamer)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [arg(nickname, t.String)],
proposer: authz,
payer: authz,
authorizations: [authz],
limit: 999,
});
console.log(txId);
return txId;
};
引数に名前(ニックネーム)を渡し、スマートコントラクトのcreateGamerメソッドを実行しています。同時にユーザの入金先を引数に渡しています。
createGamerメソッドはリソースを返します。資産というほど大したものではないですが、あなたはこのゲームのユーザですよ、と言うことを示すものをウォレットに保存します。 <-
(アロー記号)はLinear Typesと言って、元の位置からオブジェクトがなくなり新しい場所にのみオブジェクトが存在できるコンピュータサイエンスの一つの機能です。(プログラミング言語ではRustが最初からこの機能を持ちます)
あとはこれを実行するようにダイアログを表示して名前を入力してもらいます。
こんな感じです。ここでボタンを押した時、ダイアログを閉じて一つ上のコードのcreateGamerを実行します(以下コード)。また、1.5秒おきにisRegisteredメソッドを実行しています。gameUserはcurrentUser.subscribe
のuser
のことです。
import { config, authenticate, currentUser, tx } from '@onflow/fcl';
import { createGamer } from '../../flow_blockchain/transactions'
setInterval(async () => {
if (gameUser?.addr) {
hasResource = await isRegistered(gameUser.addr);
console.log(hasResource);
}
}, 1500);
<button on:click={async () => {
modal2.close()
const txId = await createGamer(playerName == '' ? 'Game Player' : playerName);
tx(txId).subscribe((res) => {
console.log(`tx status: ${res}`);
});
}}>Set name</button>
ここで一つアクシデントがあったので、エミュレータではなく、ちょっと早いですけど、Testnetで確認してみます。(どのみちGraphQL接続等をするとTestnetにデプロイが必要ですし、大して手間は変わりません。)
テストネットにデプロイする
テストネットではcontractbrowser.comやFlowScanで名前が出てしまうのでコントラクト名のマスキングが必要です。
こんな感じでVSCodeの一括変換を利用して全部名前をOragaESportsからTestnetTest1にします。
(名前をTetnetTestにしたのはテストネットにはTestnetと言う名前のコントラクトがわんさかあるので検索しても探し当てられにくいからです。後ろの1の意味はそのうち分かります。これが20くらいになるので、頻繁にこの作業が必要になります。
この時の注意事項としては、フロントエンドだけ変更してバックエンドを変更し忘れることがよくあります。パラレルにトランザクションが一つ前のスマートコントラクトに対して実行されるので気づきにくいのです。)
変更したあと、一応エミュレータでもデプロイできるか確認します。
注: flow.json
のcontracts
のsource
までTestnetTest1.cdcに変わっているのでそこだけOragaESports.cdcに戻しておきます。
公開鍵と秘密鍵を生成する
以下のコマンドを実行することで生成されます。
ブロックチェーンエンジニアの気分になれます。
flow keys generate --sig-algo "ECDSA_secp256k1"
公式翻訳ページではP-256曲線アルゴリズムもあるようですが、私は公式と直接の知り合いであるJacob君のビデオにあったsecp256k1アルゴリズムの方が慣れているので、こちらで生成します。
すると
Private Key dkflajf****
Public Key b3*******
Mnemonic 要らんやろ.. (元々は非推奨)
Derivation Path ***
Signature Algorithm ECDSA_secp256k1
と表示されます。(絶対にブログとかでキャプチャしたものを晒さないで)必要なのは秘密鍵(Private Key)。それと公開鍵(Public Key)です。秘密鍵をtestnet-account.pkeyファイル(新規で作成)にしまいます。
.gitignoreにtestnet-account.pkeyを追加してGithubにコミットされないか確認します。
git status
コマンドを実行して、以下のようにtestnet-account.pkeyが出てこなければOK。
Testnetのアカウントを作成しましょう
これはテストネットでは専用のWebサイトがあります。
このサイトを開き、以下の赤枠に公開鍵を入力します。そして Create Accountを押します。(秘密鍵はトランザクション実行する時以外使わないので絶対に人に教えたりサイトに入力しないでね!)
するとAccount Address Generated!と表示されますのでCOPY ADDRESSボタンを押してコピーします。(ついでに100,000トークンも付与されます)(簡単なので自分でやってみて)
私のテストネットのアカウントが生成できました。0x975b04756864e9ea
ここを参考に以下のようにflow.json
に設定します。
"accounts": {
"testnet-account": {
"address": "0x975b04756864e9ea",
"key": {
"type": "file",
"location": "testnet-account.pkey"
"index": 0,
"signatureAlgorithm": "ECDSA_secp256k1",
"hashAlgorithm": "SHA3_256",
}
}
}
同様にflow.json
のdeploymentも以下のようにします。
"deployments": {
"emulator": {
"emulator-account": ["TestnetTest1"]
},
"testnet": {
"testnet-account": ["TestnetTest1"]
}
},
Let’s Deploy
flow project deploy --network testnet
コマンドを実行します。成功すると以下のように表示されます。
ちなみに少し詰まりました。一番悩んだのはflow.jsonのこの部分ですね。
"key": {
"type": "file",
"location": "testnet-account.pkey"
"index": 0,
"signatureAlgorithm": "ECDSA_secp256k1",
"hashAlgorithm": "SHA3_256",
}
Jacob君のビデオと公式のサイトを見比べながら設定しました。(秘密鍵はテストネットでもgithubに上げたくないんですよ…(flow.json
は上がる))
テストネットにデプロイしたスマートコントラクトに接続
せっかくなのでBloctoウォレットに接続してみます。Flow blockchainの最初の商用サービスはNBA Top Shotですが、ある意味最初のサービスはBloctoウォレットです。台湾人が作成したものなので日本人と親近感があります。
記事が長くなってきましたが構わず続けます。
ゲーム画面のロジックの一番最初に書いていたconfig
を変更します。
--これを--
config({
'flow.network': 'emulator',
'accessNode.api': 'http://localhost:8888',
'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
--こう変更します。--
config({
'flow.network': 'testnet',
'accessNode.api': 'https://rest-testnet.onflow.org',
'discovery.wallet': 'https://wallet-v2-dev.blocto.app/-/flow/authn',
'app.detail.title': 'Oraga eSports',
'app.detail.icon': 'https://oraga-esports.com/assets/MMO%20RPG.png',
}).load({ flowJSON });
accessNode
のapi
は公式のアクセスノードのURLを指定しています。もし、どこか他のアクセスノードと契約している場合はそのURLに変更してください。
discovery.wallet
はBloctoのURLを使用しています。Bloctoの公式ドキュメントに書かれています。
あとアプリのタイトルとアイコンを設定しています。(設定は他にもあるかもしれません)
Bloctoウォレットを呼び出す
ここで一つ作業があります。今現在開発中の画面は、エミュレータのウォレットにログインしています。この状態で上記のようにテストネットに接続を変更しようとするとエラーになります。
どこでもいいので以下のように最初にウォレットからログアウトしてしまうコードを書いて、動かして強制ログアウトします。ついでにウォレットからSign Outする機能も実装しましょう。
/* importにunauthenticateを追加 */
import { config, authenticate, currentUser, tx, unauthenticate } from '@onflow/fcl';
unauthenticate()
一度ログアウトした状態でゲーム画面を開きます。
ボタンを押します。
このようにBloctoにSignInするための画面が出ます。私はiPhoneユーザなのでとりあえず🍎アイコンをクリックしてBloctoアカウント(テストネット)を作成します。
ログインできました。あっそうか、今新たに作ったアカウントはトークンを持っていないのか。Bloctoがアカウントを作ってくれているからそれにあとでトークンを増やしてあげないといけないですね。
でもテストネットでも簡単ですよね! エミュレータは開発がゴニョゴニョしているからテストネットの方が簡単、と言うのは良くある話です。
誰でも最初は0.001FLOWを持っているので少数3桁まで出すようにします。
出ました。この0.001FLOWはBloctoが出してくれてるんですよ。Mainnetではサービス使って恩返ししたいですね。(アカウント作成費用を補助するFundがあるとFlowのドキュメントに書いてあったからBloctoじゃなくて、そこが出してるのかもしれない。。)
では、ボタンを押してブロックチェーンにこのアカウントをGamerとして登録します。
このようにユーザに承認を要求する画面が出ます。ここのトランザクション承認画面は安全にする方法があります。それがInteraction Templates(通称FLIX)です。どう安全にするかというと、トランザクションをApproveすることによってトークンを支払うケースもあるのですが、それが意地悪なサイトだったら大金を支払うことになるかもしれません。それをあらかじめ審査してこのトランザクションは安全ですよ!とお墨付きを与えることができるのがInteraction Templatesです。
Approveを押します。
コンソールログにはこのように表示されます。1回で成功ですね。
null
から {uuid:’213305…}
と表示されるのは1.5秒おきに実施している以下のコードによるものですね。
console.log(hasResource);
tx statusは[object Object]
になってしまっているので
const txId = await createGamer(playerName ?? 'Game Player');
tx(txId).subscribe((res) => {
console.log(`tx status: ${res}`);
});
の部分は
const txId = await createGamer(playerName ?? 'Game Player');
tx(txId).subscribe((res) => {
console.log('tx status:', res);
});
に変更した方が良さそうです。
ゲームを開始するための費用を回収する(設計)
今作成しているゲームはeSportsですから賞金が必要です。その費用はゲームのプレイ料金から出します。
この費用を払ったらゲームがスタートするようにロジックを変更します。
もう一度スマートコントラクトを設計します。
import "FlowToken"
import "FungibleToken"
access(all) contract TestnetTest1 {
access(self) var totalCount: UInt
access(self) let GamerFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}
access(all) event GamerCreatted(gamer_id: UInt)
access(all) resource Gamer {
access(all) let gamer_id: UInt
access(all) let nickname: String
init(nickname: String) {
TestnetTest1.totalCount = TestnetTest1.totalCount + 1
self.gamer_id = TestnetTest1.totalCount
self.nickname = nickname
emit GamerCreatted(gamer_id: self.gamer_id)
}
}
access(all) fun createGamer(nickname: String, flow_vault_receiver: Capability<&{FungibleToken.Receiver}>): @TestnetTest1.Gamer {
let gamer <- create Gamer(nickname: nickname)
if (TestnetTest1.GamerFlowTokenVault[gamer.gamer_id] == nil) {
TestnetTest1.GamerFlowTokenVault[gamer.gamer_id] = flow_vault_receiver
}
return <- gamer
}
init() {
self.totalCount = 0
self.GamerFlowTokenVault = {}
}
}
(CadenceはSwiftによく似ているので、スニペットにコードを貼り付けるとよくSwift言語、と認識されます。)
今スマートコントラクトはこのようになっています。まず、支払ってもらったゲーム費用を保存する金庫(vault)を用意します。
8行目に以下を、
access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
一番下のinitの中を以下のようにします。
init() {
self.totalCount = 0
self.GamerFlowTokenVault = {}
self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
}
これで一度デプロイできるか確認します。flow project deploy
コマンドを叩いてエミュレータにデプロイします。既にコントラクトがある、と言われるはずなのでエミュレータを再起動してもう一度実行します。
エミュレータでデプロイできることが確認できたので、次は情報を取得するための構造体(Struct
)を作ります。Struct
はResource
とほぼ一緒ですが、データのコピーができる点が違います。
resource Gamer
の上に以下のコードを書きます。
access(self) let gamersInfo: GamersInfo
/* [Struct] GamersInfo */
access(all) struct GamersInfo {
access(contract) var currentPrize: UInt
access(contract) var tryingPrize: {UInt: UInt}
access(contract) var prizeWinners: {UInt: GamerPrizeInfo}
access(contract) var tipJarBalance: UInt
access(contract) fun setCurrentPrize(added: UInt, gamerId: UInt, paid: Bool): UInt {
if (paid == false) {
self.currentPrize = self.currentPrize + added
self.unsetTryingPrize(gamerId: gamerId)
return 0
} else{
if let paidPrize = self.tryingPrize[gamerId] {
if (self.currentPrize - paidPrize > 0) {
self.currentPrize = self.currentPrize - paidPrize
self.unsetTryingPrize(gamerId: gamerId)
if let prizeHistory = self.prizeWinners[gamerId] {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize + prizeHistory.prize, nickname: nil)
} else {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize, nickname: nil)
}
return paidPrize + added
}
}
panic("Error.")
}
}
access(contract) fun unsetTryingPrize(gamerId: UInt) {
self.tryingPrize[gamerId] = 0
}
access(contract) fun setTryingPrize(gamerId: UInt) {
self.tryingPrize[gamerId] = self.currentPrize
}
access(contract) fun setTipJarBalance(value: UInt, tipped: Bool) {
if (tipped) {
self.tipJarBalance = self.tipJarBalance + value
} else if (self.tipJarBalance - value > 0) {
self.tipJarBalance = self.tipJarBalance - value
} else {
self.tipJarBalance = 0
}
}
access(contract) fun setPrizeWinners(gamerId: UInt, prize: UInt, nickname: String?) {
if let prizeWinnerInfo = self.prizeWinners[gamerId] {
prizeWinnerInfo.setPrize(prize: prize)
if (prize > 0) {
prizeWinnerInfo.setTotalCountUp()
}
self.prizeWinners[gamerId] = prizeWinnerInfo
} else {
if (nickname != nil) {
self.prizeWinners[gamerId] = GamerPrizeInfo(prize: prize, gamerId: gamerId, nickname: nickname!)
}
}
}
init() {
self.currentPrize = 0
self.tryingPrize = {}
self.prizeWinners = {}
self.tipJarBalance = 0
}
}
/* [Struct] GamerPrizeInfo */
access(all) struct GamerPrizeInfo {
access(contract) var prize: UInt
access(contract) let gamerId: UInt
access(contract) let gamerName: String
access(contract) var totalCount: UInt
access(contract) fun setPrize(prize: UInt) {
self.prize = self.prize + prize
}
access(contract) fun setTotalCountUp() {
self.totalCount = self.totalCount + 1
}
init(prize: UInt, gamerId: UInt, nickname: String) {
self.prize = prize
self.gamerId = gamerId
self.gamerName = nickname
self.totalCount = 1
}
}
access(all) fun getGamersInfo(): GamersInfo {
return self.gamersInfo
}
2つStruct
を用意しました。長く見えますが、init
以外の関数は全てSetter関数です。Struct
は宣言時必ずaccess(all)
スコープであり(Struct
の「型」を外部から取得するため)、そのためStruct
のフィールドの値を変更する必要がある場合には必ずaccess(contract)
より狭いスコープのSetter関数を用意する必要があります。
せっかくSetterとして関数を用意するなら必要なロジックも入れておこう、と賞金のセット、取得のロジックも設計して一緒に埋め込みました。何か不整合でロジックを先に進められない時にはpanic
関数を呼び出します。するとトランザクションが失敗し、後でFlowScanなどで確認した時に、何かエラーが発生しているな、と確認することができます。
そして、ゲームにログインしていない人でも賞金が確認できるようにpublicなgetGamersInfo
関数を最後に定義しました。
次に、一番下のinit
関数を以下のようにします。
init() {
self.totalCount = 0
self.GamerFlowTokenVault = {}
self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
self.gamersInfo = GamersInfo()
}
これは直前にgamersInfo
をフィールドとしてスマートコントラクトに定義したので、それをinit
内で初期化する必要がある為です。
GamerPrizeInfo
はGamersInfo
構造体の中で使用するだけなのでフィールドには定義していません。(※構造体の中で構造体の型が必要な時は新たにStruct
の型宣言をする必要があります。)
これでゲームにログインしていない人でもゲーム情報を取得できるようになりました。取得してみます。
現在の賞金額などを取得する
/flow_blockchain/scripts.js
に以下を追加します。
export const getGamersInfo = async function () {
const result = await query({
cadence: `
import "TestnetTest1"
access(all) fun main(): TestnetTest1.GamersInfo {
return TestnetTest1.getGamersInfo()
}
`,
});
return result;
};
そしてゲーム画面には以下のようにロジックを埋め込みます。
import { getBalance, isRegistered, getGamersInfo } from '../../flow_blockchain/scripts';
let currentSituation;
setInterval(async () => {
if (gameUser?.addr) {
hasResource = await isRegistered(gameUser.addr);
}
currentSituation = await getGamersInfo();
console.log(currentSituation);
}, 1500);
1.5秒おきにgetGamersInfoを呼び出すようにしました。
では実際にデプロイして確認してみます。
flow project deploy --update --network testnet
既に存在するコントラクトに更新するときは --updateフラグをつけます。
実行結果は以下のようなのが沢山出ます。
スマートコントラクトは不変であることが大事ですので、新しいフィールドを追加したり、現在あるフィールドを変更してしまうと、もう更新ができません。
そこで名前を変えてデプロイします。以下のようにまた、一括してコントラクト名を変更します。
そして今度は - - updateフラグをつけずにデプロイします。
デプロイできました。
ではゲーム画面で見てみましょう。
取得できました。今回も1回で取れたので、慣れれば非常に簡単に実装できます。1.5秒おきに現在の賞金額などを取得しています。
ゲーム画面のロジックの以下のようになっていました。
Current Prize: <img src="/assets/flow_fire.png" alt="$FLOW" />
<span class="prize">2</span>
<span class="unit">($FLOW)</span>
ここを以下のようにします。
Current Prize: <img src="/assets/flow_fire.png" alt="$FLOW" />
<span class="prize">{(parseInt(currentSituation?.currentPrize ?? 0)) + 1}</span>
<span class="unit">($FLOW)</span>
基本的にスマートコントラクトから取得する値はUInt
となっていても文字列で取得されます。そのためparseInt
などが必要になっています。
+1
しているのはこれから自分がお金を入れた分も賞金に含まれるためです。
実際に画面を見てみましょう。
ログインしていない状態でも賞金額が1($FLOW)と表示されました。
(Your BalanceがNaNになってますね。このままでもいいかな..)
長くなりましたので次の記事でゲーム開始ロジックを説明します。
最後に
今回主に実装した箇所のソースコードは以下のURLにあります。
次の記事 >
< 前回の記事