【CryptZombies】レッスン4チャプター3:ゾンビ・バトル
テストの実行と新規コントラクトの作成(復習)
①ファイル冒頭にSolidity version ^0.4.19を用いることを宣言せよ。
②zombiehelper.solをimportせよ。
同じディレクトリ(./)内にzombiehelper.solがあればimportできます。
③ZombieBattleという名の新規contractを宣言せよ。これはZombieHelperを継承する。コントラクト本文は、今はまだ空のままにしておくのだ。
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
【CryptZombies】レッスン4チャプター2:Withdraw関数
Withdraw関数
withdraw関数は、Etherをコントラクトから引き出す関数です。
コントラクトに送られたEtherは、コントラクトのイーサリアム・アカウントに貯められます。コントラクトからEtherを引き出す関数を追加しない限りはそこに閉じ込められたままになってしまいます。
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
ownerとonlyOwner修飾詞をOwnableコントラクトから用いています。
transfer関数を使ってEtherをあるアドレスに送ることができ、this.balanceはコントラクトに溜まっている残高の総量を返します。なので、もし100人のユーザーが1Etherを私たちのコントラクトに支払ったとしたら、this.balanceは100Etherに等しくなります。
transferを使えば、どんなイーサリアムのアドレスにも送金可能です。例えば下にあるように、あるアイテムに対する支払いが多すぎた場合に、Etherをmsg.senderに送り返す関数を作ることもできます。
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
または購入者と販売者間のコントラクトにおいて、販売者のアドレスをストレージに保存しておいて、誰かが販売者のアイテムを購入する際に、購入者が支払った料金を販売者に送金することも可能です。
seller.transfer(msg.value)
このようにして分散型マーケットプレイスを持つことができるのです。
テストの実行
①我々のコントラクト内に、上記のGetPaidの例と同一のwithdraw関数を作成せよ。
②コントラクトの所有者として、我々がlevelUpFeeを設定できるような関数を作るのだ。
a.一つの引数uint _feeを受け取るsetLevelUpFeeという名の関数を作成せよ。これはexternalであり、onlyOwner修飾詞を用いることとする。
b.作成した関数にlevelUpFeeを設定し、_feeと同等となるようにせよ。
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
【CryptZombies】レッスン4チャプター1:Payable関数(修飾詞の復習)
修飾子の復習
可視性修飾詞
いつどこで関数を呼び出すかをコントロールするもの。
private:コントラクト内の別の関数からのみ呼び出される。
internal:そのコントラクトを継承したコントラクトからも呼び出すことができる。
external:コントラクト外からだけ呼び出すことができる。
public:コントラクト内部・外部どちらからでも呼び出すことができる。
状態修飾詞
関数がブロックチェーンとどのように作用し合うのか示すもの。
view:関数が動作しても、なんのデータも保存または変更されない。
pure:関数がブロックチェーンにデータを保存しないだけでなく、ブロックチェーンからデータを読み込むこともない。
※どちらもコントラクト外部から呼び出された場合はガスは不要。(ただし、コントラクト内にある別の関数から呼び出されるとガスが必要。)
modifier:カスタムできるもの。レッスン3で学んだonlyOwnerやaboveLevelがある。私たちは、これら修飾詞の関数への影響の仕方を決定するため、カスタムした理論を定義することが可能です。
これらの修飾詞は、全て以下のように一つの関数定義に組み込むことができます。
function test() external view onlyOwner anotherModifier {/* ... */}
payable修飾詞
payable関数は、Etherを受け取ることができる特別なタイプの関数です。
イーサリアムでは、お金(Ehter)もデータ(トランザクションの内容)も、コントラクト・コード自体も全てイーサリアム上にあるので、ウェブサーバー上でAPI関数を呼び出すとき、ファンクション・コール(関数呼び出し)に併せてお金の支払いが可能になります。
関数を実行するため、コントラクトへいくらかの支払いを要求することもできます。
contract OnlineStore { function buySomething() external payable { // 0.001etherがファンクション・コールに併せて送金されたかチェックします: require(msg.value == 0.001 ether); //OKであれば、関数を呼び出した者にデジタルアイテムを送付するための仕掛けをしておきます。 transferThing(msg.sender); } }
ここのmsg.valueは、コントラクトにどのくらいEtherが送られたかを見るやり方で、etherは組み込み単位です。
ではここでweb3.js(DAppのJavaScriptフロントエンド)から以下のように関数を呼び出した場合何が起こるでしょうか。
//Ethereum上のコントラクトにOnlineStoreポイントを想定した場合 OnlineStore.buySomething({from:web3.eth.defaultAccount, value:web3.utils.toWei(0.001)})
valueの部分を見てください。ここではJavaScriptのファンクション・コールでetherをどのくらい送るかを定めています。(0.001ether)
もしトランザクションを封筒のようなものと考えると、ファンクション・コールに渡すパラメーターは、封筒の中に入れた手紙の内容です。
そしてvalueを追加するのは、封筒の中に現金を入れるようなものです。受取人に手紙とお金が一緒に届けることができるのです。
※関数にpayable修飾詞がなく、Etherを上記のように送ろうとすると、その関数はトランザクションを拒否します。
テストの実行
①levelUpFeeという名前のuintを定義し、それが0.001etherと同様になるよう設定せよ。
uint levelUpFee = 0.001 ether;
②levelUpという名前の関数を作成せよ。これに一つのパラメーター_zombieId(uint)を渡せ。またexternalかつpayableとせよ。
function levelUp(uint _zombieId) external payable { }
③最初にmsg.valueがlevelUpFeeと同等であることを、この関数が要求(require)するようにせよ。
function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); }
④そしたらゾンビのlevelを増やすのだ。やり方はこうだ。zombies[_zombieId].level++
zombies配列の_zombieIdインデックスに該当するlevelにアクセスして、levelを増やします。
function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; }
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
【CryptZombies】レッスン3チャプター12:Forループ
getZombiesByOwner関数を素直に実装しようとするなら、オーナーからゾンビ軍団へのmappingをZombieFactoryコントラクトに持たせればOKのはずです。
mapping (address => uint[]) public ownerToZombies
新しいゾンビを作る度に、ownerToZombies[owner].push(zombieId)を使ってオーナーのゾンビ配列に追加していくだけです。するとgetZombiesByOwnerは非常にシンプルな関数になります。
function getZombiesByOwner(address _owner) external view returns (uint[]) {
return ownerToZombies[_owner];
}
この方法の問題点
ゾンビを誰かに譲る関数をあとで作成した時に問題が起きます。
その関数には次の動作が必要になります。
1.ゾンビを新しいオーナーのownerToZombies配列に追加する。
2.元のオーナーのownerToZombies配列からゾンビを削除する。
3.穴を埋めるために、元のオーナーの各ゾンビの配列の番号を変更する。
4.配列のlengthを1減らす。
ステップ3は、ゾンビの位置を全てずらすことになるので、ガスコストは非常に高額です。もしオーナーがゾンビを20体持っていて、最初のゾンビを誰かにあげたとします。すると残りの19体の配列番号を書き直さなくてはいけません。
この場合、view関数は外部から呼び出したときにガスコストがかからないので、getZombiesByOwner内でforループを使ってそのオーナーのゾンビ軍団の配列を作ってしまえばOKです。そうすればtransfer関数はstorage内の配列を並び替える必要がないため安く抑えられ。全体のコストも抑えられます。
forループを使う
SolidityのforループはJavaScriptと同じようなものです。
forループとは、「これだけの回数、同じ処理を繰り返す」という繰り返し処理を書くときに使う構文です。
<定型>
for (i = 0; i <【繰り返す回数】; i++) {
繰り返してやりたい処理
}
for (【1】,【2】,【3】) {
【4】
}
①変数「i」の中身は、最初は「0」【1】
②変数「i」の中身が5より小さい間、繰り返す【2】
③画面に変数「i」の中身を表示する【4】
④繰り返す度に変数「i」の中身に1足す【3】
偶数の数字を格納する配列の例:
function getEvens() pure external returns(uint[]) {
uint[] memory evens = new uint[](5);
// 新しい配列のインデックスをトラックする:
uint counter = 0;
// 1から10までループさせる:
for (uint i = 1; i <= 10; i++) {
// もし `i` が偶数なら...
if (i % 2 == 0) {
// 配列に格納する
evens[counter] = i;
// カウンタを増やして `evens`の空のインデックスにする:
counter++;
}
}
return evens;
}
この関数は[2, 4, 6, 8, 10]の配列を返します。
テストの実行
getZombiesByOwnerを完成させよ。forループでDApp内の全てのゾンビをループさせ、オーナーが一致するかどうかを判定し、result配列に格納して返却せよ。
①counterというuintを宣言し、0に設定せよ。この変数はresult配列のインデックスとして使用する。
②uint i = 0から始めて、i < zombies.length(※)までループするforループを宣言せよ。このループは配列内の全てのゾンビをイテレートする。
※lengthは、主に文字列の長さや配列の要素数を取得することができるプロパティのこと。zombies.lengthでzombies配列の要素数にアクセスできます。
③forループ内にifステートメントを作成し、zombieToOwner[i]が_ownerと一致するか判定せよ。2つのアドレスを比較することでチェックしているのだ。
zombieToOwnerマッピングのキーであるiで読み出せるaddress(バリュー)が_ownerと一致するか判定します。
④ifステートメント内部には以下を設定せよ。
1.result配列内にゾンビのIDを追加せよ。result[conter]をiと同等になるよう設定するだけでよい。
2.conterを1増やせ。(forループの例を参考にするのだ)
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
https://wa3.i-3-i.info/word15412.html
【JavaScript入門】lengthで文字列や配列の長さを取得する方法 | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト
【CryptZombies】レッスン3チャプター11:Storageのコストは高い
Solidityでstorageへの操作は高コストです。特に書き込みはとても高いです。
なぜかというと、データを書き込んだり、変更するたびに、それがすべてブロックチェーン上に永久に書き込まれるからです。
そこで、コストを抑えるために、絶対に必要な場合を除いてデータをstorageに書き込まないようにします。そのため、一見非効率なロジックを作ることがあります。例えば、単純に配列を保存するかわりに、関数を呼び出す毎にmemory上の配列を再構築する等です。
他のほとんどのプログラム言語では、大きなデータセットをループするのは高コストです。しかしSolidityでは(external view関数の場合は)その方がstorageを使うよりも低コストとなります。(view関数はガスが不要だから)
memory内で配列を宣言する
関数の中でmemoryキーワード付きで配列を生成すると、storageに書き込むことなく新しい配列を作ることができます。配列は関数内でのみ存在するので、storageの配列を更新するよりも圧倒的にガスのコストを抑えることができます。さらに外部から呼び出されるview関数の場合は、コストはかかりません。
function getArray() external pure returns(uint[]) {
// 長さ3の新しい配列をメモリ内にインスタンス化(注1&2)する
uint[] memory values = new uint[](3);(注3)
// 値を追加しよう
values.push(1);
values.push(2);
values.push(3);
// 配列を返す
return values;
}
注1)インスタンス化とは、オブジェクト(モノ(クラスやインスタンスをふんわりと表現したもの))指向のプログラミング言語で出てくる表現の一つで、クラス(設計図)からインスタンス(実体、実際の物)を作ることです。
例えば「名前、身長、体重」というクラスの場合、そのインスタンスは「佐藤、170、60」等と作られます。一つのクラスから複数のインスタンスを作ることができ、それぞれのインスタンスは異なる値を持つことができます。
注2)New演算子は、クラス(設計図)からインスタンス(実際に作ったもの)を作るときに使う演算子です。
例:obj = new ClassA();
注3)memoryの配列は必ず長さを指定して作成する必要があります(この例では3)。現在はstorage配列のようにarray.push()でサイズを変えることはできませんが、Solidityの将来のバージョンでは可能になるかも。
テストの実行
getZombiesByOwner関数では、特定のユーザーが保有している全てのゾンビを、uint配列として返したい。
①resultという名前のuintmemory変数を宣言せよ。
②それを新しいuint配列と同等になるよう設定せよ。配列の長さは_ownerの所有するゾンビ数とする。ゾンビ数はmappingからこのようにして参照できる:ownerZombieCount[_owner]
ownerZombieCountマンピングは、レッスン2チャプター2&3で次のように設定しています。
答えは次の通り。
③関数の最後でresultを返せ。今のところは空の配列で構わない。
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
https://wa3.i-3-i.info/word15939.html
【CryptZombies】レッスン3チャプター10:View関数でガスを節約
View関数はガスコストが不要
view関数を外部から呼び出す場合、ガスは一切かかりません。(external view)
なぜかというと、view関数がブロックチェーン上でなにも変更しないからです。ただデータを参照するのみ。
詳しくいうと、関数にviewとマークすることで、その関数を実行するにはローカルのイーサリアムノードに問い合わせるだけでよく、ブロックチェーン上にトランザクションを生成する必要がないことをweb3.jsに伝えられるためです。(トランザクションを生成すると全てのノードで実行する必要があり、ガスが必要になる)
※view関数が同じコントラクトの、view関数ではない別の関数から呼び出される場合、その呼び出しにガスのコストがかかります。その別の関数はイーサリアム上にトランザクションを生成するので、各ノードの検証が必要になるためです。view関数は外部から呼び出すときのみ、無料になります。
テストの実行
手持ちのゾンビ軍団すべてを返す関数を実装したい。あとで、この関数をweb3.jsから呼んで、ユーザープロフィールページ上にゾンビ軍団を表示できるようにするためです。
①getZombiesByOwnerという名前の関数を作成せよ。引数は_ownerという名前のaddress型とする。
②これをexternal view関数とし、ガスコストを使わずにweb3.jsから呼び出せるようにするのだ。
③関数はuint[](uintの配列)を返すように設定すること。
お疲れさまでした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network
【CryptZombies】レッスン3チャプター9:ゾンビ修飾子
aboveLevel修飾子を使って次のような関数を作ります。
・ゾンビのレベルが2以上なら、ユーザーは名前を変更できるようになる。
・ゾンビのレベルが20以上なら、カスタムDNAを与えることができるようになる。
// ユーザーの年齢を格納するマッピングだ:
mapping (uint => uint) public age;
// 一定の年齢よりユーザーの年齢が高いことを要件とする修飾子だ:
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_;
}
// 車の運転は16歳以上だな(米国の場合だ。日本は18歳だな)。
function driveCar(uint _userId) public olderThan(16, _userId) {
// 関数のロジックだ
}
テストの実行
①changeNameという関数を作成せよ。引数は_zombieId(uint)と、_newName(string)の2つとする。またexternalで宣言せよ。関数はaboveLevel修飾子を持ち、_levelパラメーターに2を渡すこと。(_zombieIdも忘れずに渡すようにな!)
aboveLevel修飾子は、_level(uint)と_zombieId(uint)の2つの引数を持っています。
ここでは、_levelパラメーターに2を渡します。
②この関数では、msg.senderがzombieToOwner[_zombieId]と同じであるかどうかを確認せよ。requireステートメントを使用すること。
zombieToOwnerマッピングの_zombieIdがmsg.senderと同じがどうかを確認します。
③さらにzombies[_zombieId].nameが_newNameと同等になるよう設定せよ。
zombies配列の「_zombieId」に該当する、Zombie構造体のnameプロパティが_newNameと同等になるように設定します。
④changeNameの下にchangeDnaという名前の別の関数を作成せよ。定義および内容はchangeNameとほとんど同じだが、2番目の引数を_newDna(uint)とし、aboveLevelの_levelパラメーターに20を渡すこと。もちろん、ゾンビの名前を設定する代わりに、ゾンビのdnaに_newDnaを設定すること。
③と同様に処理します。
zombies配列の「_zombieId」に該当する、Zombie構造体のdnaプロパティが_newDnaと同等になるように設定します。
お疲れ様でした!
<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network