LoginSignup
56

More than 1 year has passed since last update.

僕の考えたメタバースの世界を実装してみる

Last updated at Posted at 2021-12-13

ABEJA Advent Calendarの13日目です。普段はABEJAにて機械学習のエンジニア的な事をやっております。過去にはVIPちゃんねるとか株AIとかを書いていた者です。ツイッター界では@peisukeという名前で活動しております。今回は深くて浅い訳があってメタバースネタでやってみます。前半のポエムが長くなりそうなので、先に流れを示しますと、以下になります。

  • 僕がメタバースに関して思いついたこと
  • Atomic Swapの技術について紹介
  • UnityとSolidityで僕の考えたメタバースの簡易版を作ってみた

先にネタを思いついてしまったものの、実はUnityとブロックチェーン、両方とも初めて触るので、あまり技術的に難しいことは出来ないですし、間違った事も言っているかもしれませんが、なるべく優しくしてね。2週間で頑張った!寝てない!

追記0:僕の考えたは言い過ぎだった説もありつつ、あんまり似た考えを見たこともないので、一旦僕の考えたにしておこう。
追記1:メタバースに仮想通貨不要論については最後の余談1に。
追記2:記事が長くなっちゃったので先にこんなの作ったよってイメージ、UnityでWebGLしてブラウザ上で独自仮想通貨をAtomicSwapで安全に交換するやつです。
image.png

背景

まずは少し長めのポエムです。今回のテーマに至った背景と、メタバースって何があるとよいのかな?ってのを考えた話。もちろん、あくまで「僕の考えた」なので、全然これが正しいとかいう話じゃないですので、あしからずです。

個人的にメタバースについて謎に思っていた事

最近、色んな所でメタバースという単語を聞くようになってきましたね。僕も少しだけですがVRChatで遊んでみたり、Immersedの中で画面広げて仕事したりと、最近のVR事情は面白くなってきたなぁと思います。さっきもVRKert行ってすげーーってなっていました。ちなみに何気にOculus4台目・・・。一方、仮想通貨界隈でもメタバースというキーワードが流行り始め、SANDなどの関連コインなんかも登場しています。そちらでは、ネット上で土地を持って独自の経済活動をするみたいな流れですね。実際の所は、値段が高すぎて手も出ませんが、あっちはあっちで面白そう。

メタバースに関して、僕個人としてはずっと疑問がありまして。VR界隈において、例えばVRChatなどを以てメタバースだという話は少なからず聞きます。ただ、それだけ聞く限り、中に入れるどうぶつの森とあんまり変わらない気がするなと(失礼)。確かにどうぶつの森は面白いですが、だからといってFacebookが社名を変えるほどのゲームチェンジになるのかなぁって疑問に思ってました。加えて、仮想通貨については更に良く分からず、ネット上でNFTで自分の資産性を証明できたら何でメタバースなん?ってずっと謎に感じていました。

そんな中、ある日、僕の中で一つの事件というか、大きな気付きがありました。仮想通貨クラスタの知人が「メタバースはコミュニティ」だって言ってたんですよ。聞いた時は「ここで新しい概念!?」と思ったんだけど、それからちょっと真剣に考えてみた所、愚地独歩並の「これかァ」って感じになりましたよ。僕の中で、VRと仮想通貨とFacebookが繋がった瞬間でした。ちょっとあまりにも感動したので、後日アドカレネタを決める際に「僕の考えた最低限のメタバース」をテーマにすることにしました。以下に詳細について述べます。

(僕の中で勝手に)VRと仮想通貨が繋がった話

さて、メタバースの定義は色々ありますが、多くの人の意見の共通項として「仮想空間で生活する」ってのがあるかと思います。そこで、じゃあ生活って何だろう?って事を考えてみました。安直に、リアルな空間の再現やモノとのインタラクションなど、通常僕らが生活している様相をそのまま仮想空間に当てはめるという世界観も、確かに生活とは言えるんですが。じゃあ「現実世界でモノとインタラクション出来ない人は、生活できていないことになっちゃわない?」って思ってしまい、人間らしい生活のコアって結構難しいテーマなんじゃないかなって思います。なので(以下省略)・・・。

とはいえ、ここはあくまで技術系アドベントカレンダー。人間について長々と語っても仕方ないので、細かな議論はすっ飛ばし、色々考えた結果に行きましょう。僕の考えたメタバース3要件はこちら。

  • 人と人が同じ空間で活動しコミュニケーションを取れる
  • 活動の結果として何らかの価値を築くことができる
  • コミュニケーションを通じて価値同士を交換できる

という3つが大事なんじゃないかなって考えました。なお、ここでの交換というのは、10ETH=1BTCみたいな当価値の通貨の交換ではなく、物と物の個人的なやり取り全般を想定しています。いきなりこんな話だとちょっと飛躍してるので、異論も沢山出るかと思いますが、一旦はコレはコレとして進めます。ちょっと抽象的なので具体化してみると、例えば仕事であれば、上司や顧客とコミュニケーションをし、仕事の結果をプログラム等として残し、その労働時間をお金と交換する、と言えます。多くの場合においてはコミュニケーションの重要性が殆どだったりするのですが、「生活」まで考えを広げると下の2つの要件も考える必要があるかなって考えています。

こう考えると、VR界隈と仮想通貨界隈の議論が矛盾しないし、そして何と言っても、それらを支えるのが前述したような「コミュニティ」って事なんじゃないかな、って思いました(余談1)。

image.png

作るべきもの

現在のVRや仮想通貨、SNSにおいてあまり語られていない技術があります。それは「価値の交換」です。仮想通貨の取引においては、分散型の取引所は存在するものの、交換する通貨にはある程度の流動性が必要になります。また、そもそも市場に流れていない通貨の交換ができません。取引所が扱っていれば交換は可能ですが、任意のゲームの装備などを自由に交換する術まではないんですよね。資産を自由に交換できれば、これまで中央集権的にゲーム内でしか受け渡し出来なかった価値あるモノを、別のゲームのモノとも交換できるようになります。つまり、例えゲームが死んでも、自分が生み出した資産は(金銭的価値はさておき)残るわけです、まるで現実の世界みたいじゃありません?(余談2)

というわけで、今回のアドベントカレンダーでは、上記3要件を備えたメタバースな空間を作っていくこととしましょう。具体的には、

  • オリジナルの通貨を発行する
  • お互いがコミュニケーションできるバーチャルな空間を作る
  • 安全に資産を交換できる仕組みを作る

の3つです。1番目については、イーサリアムプラットフォーム上で何らかの通貨を発行することにしましょう。通貨って言うとお金のイメージが強いですが、通貨だけではなくゲームの装備みたいなのもイメージしています。鋼の鎧を10個保有している、みたいな感じです。2番目については、Unityを使い、ネットワーク上で、自分のアバターを持ち他人とコミュニケーションできるようにしてみます。3つめが肝心、資産を安全に交換できる仕組みとして「Atomic Swap」という技術があります。今回はその技術の紹介と実装もしていきます。

Atomic Swapについて

ブロックチェーン上のプログラミングについて

そもそもブロックチェーンにおけるプログラミングって何なの?って思う人が多いと思うので、まずはそれを説明していきます。仮想通貨の種類やその原理などはここでは省き、アプリを開発するエンジニア目線でのみ記載します。大雑把に言えば、開発者が何らかのコードを書き、ツールを使ってデプロイすると、ネットの何処かにエンドポイント的なものが作られます。そこに対してユーザーからのリクエストを通し、データを記録・更新をする際に、チェーン上(インターネットの何処かにあるストレージ的な所)に格納されます。コードはSolidityというJavaScriptに似た言語で書かれたものになります。具体的に簡単な送金の例を以下に示します。ちょっとザルですが、送金するコードのイメージは以下のようなものになります。

// キーバリューの仕組み、data[id]とすると、moneyの値を取得できる
mapping(address id => int money) public wallet;

// 利用者から(例えばブラウザのjsスクリプトから)呼ばれる関数
function sendMoney(address reciever, int x) public returns (bool) {
    //  requireで条件を満たしているかをチェック
    //  msg.senderは呼び出した人のid
    //  以下の場合は、呼び出した人の財布にx円以上入っているかを確認
    require(wallet[msg.sender] >= x);

    //  呼び出した人の財布からx円を移し替え
    wallet[msg.sender] -= x;
    wallet[reciever] += x;

    //  「この処理を実行したよ」という証拠をチェーン上に残す
    emit SendMoney(msg.sender, reciever, x);

    return true;
}

このようにすると、キーバリューのデータや配列のデータなどがチェーン上で管理されます。また、requireを使うことで制限を掛けてやることで不正な取引がされないようにします。上記の例の場合は自分の財布以上にお金を渡せないようにチェックします。バックエンドのコードは前述のようにSolidityで書きますが、クライアント側でこれを利用する際は、web3jsというJavaScriptのライブラリを通してブラウザから呼び出すことができます。

Atomic Swapの手順

Atomic Swapとは、第3者を介さずに資産を安全に交換するための仕組みです。従来は、どこかの取引所などが、資産交換の管理をする必要があるため、ある程度の制約がどうしても発生してしまうものでした。例えばNFTを購入するときなどは、OpenSeaのようなサービスが仲介し、通貨とNFTを交換するようなものです。ここではAtomic Swapの技術的な解説をしていきましょう。ちなみに、もっとちゃんとした解説はこちら

まず、ここではOpenerが交換を依頼しCloserがそれを承認するものとします。例えば、Openerが100円と1ドルを交換たいとCloserにリクエストしたものとします。これに対しCloserが承認すると交換が実行されます。通常の送金機能でこれを実行しようとすると、Opnerが100円を送った際にCloserがそれを受け取って逃げることも出来てしまいます。Atomic Swapは、この交換を第3者を介さずに安全に実施するという方法です。

手順としては、まずOpenerは交換に使いたい自身のコインの量をチェーン上に預け、また交換リクエストを同じくチェーンに記録します。

image.png

続いて、Closerは交換リクエストを確認し、交換に応じるのであれば自身のコインをOpenerに送金するとともに、Openerが預けたコインを引き出します。

image.png

Atomic Swapのコード解説

実際にAtomic Swapのコードについて解説していきます。なおソースコードの大部分はこちらから持ってきました。部分的に今回の仕組みを実装するために改変ています。また、今回は簡単のためERC20(イーサリアムプラットフォーム上の規格の一つ)を対象にします。少し修正すればNFTのようなERC721などにも応用可能です。

交換リクエスト

交換リクエストは、交換リクエストのキーを_swapIDとし、通貨や交換する量などを引数に呼び出します。なお、addressとは個々のユーザに紐づくIDとなります。Openerは送金許可した量を確認の上で、チェーン上に送金を行っておきます。チェーン上に送金するってなんぞ?と思われると思いますが、チェーンにint型でチェーン自体の財布の値をもっておき、そこに自分の財布から数値を付け替えているだけです。その上で、Swapという構造体に取引の情報を保存しておきます。

function open(bytes32 _swapID, 
              uint256 _openValue, address _openContractAddress, 
              uint256 _closeValue, address _closeTrader, address _closeContractAddress) public onlyInvalidSwaps(_swapID) {
    // Transfer value from the opening trader to this contract.
    ERC20 openERC20Contract = ERC20(_openContractAddress);
    require(msg.sender != _closeTrader);
    require(_openValue <= openERC20Contract.allowance(msg.sender, address(this)));
    require(openERC20Contract.transferFrom(msg.sender, address(this), _openValue));

    // Store the details of the swap.
    Swap memory swap = Swap({
        openValue: _openValue,
        openTrader: msg.sender,
        openContractAddress: _openContractAddress,
        closeValue: _closeValue,
        closeTrader: _closeTrader,
        closeContractAddress: _closeContractAddress
    });
    swaps[_swapID] = swap;
    swapStates[_swapID] = States.OPEN;

    //emit Open(_swapID, _closeTrader);
    emit Open(_swapID, msg.sender, _closeTrader);
}

なお、取引の情報には、OpenerとCloserのアドレス、トークン、

struct Swap {
    uint256 openValue;
    address openTrader;
    address openContractAddress;
    uint256 closeValue;
    address closeTrader;
    address closeContractAddress;
}

交換に応じる

交換に応じる時は、OpenerがCloserに対して_swapIDを送り、それをキーにして交換をクローズをします。なお_swapIDが他人にバレて他の人が実行しようとしてもrequireで弾かれるので安心です。手順としては、まずCloserが十分量の送金許可を出しているかをチェックし、Openerに送金を行います。続いて、チェーン上に預けていたOpenerが送金した分をCloserが引き出します。これにて交換が完了します。

function close(bytes32 _swapID) public onlyOpenSwaps(_swapID) {
    // Close the swap.
    Swap memory swap = swaps[_swapID];

    // Transfer the closing funds from the closing trader to the opening trader.
    ERC20 closeERC20Contract = ERC20(swap.closeContractAddress);
    require(swap.closeValue <= closeERC20Contract.allowance(swap.closeTrader, address(this)));
    require(closeERC20Contract.transferFrom(swap.closeTrader, swap.openTrader, swap.closeValue));

    //require(msg.sender == swap.openTrader);
    swapStates[_swapID] = States.CLOSED;

    // Transfer the opening funds from this contract to the closing trader.
    ERC20 openERC20Contract = ERC20(swap.openContractAddress);
    require(openERC20Contract.transfer(swap.closeTrader, swap.openValue));

    emit Close(_swapID, swap.openTrader, swap.closeTrader);
}

開発およびデプロイについて

ブロックチェーンのアプリを開発してデプロイする手順は他の記事を見れば無限に書いてあるので、以下に使ったツールと概要だけ記載しておきます。Metamaskで財布を作って、Ganancheでローカルのネットワークを作って、truffleでプロジェクトを作り、コンパイル・テストからのデプロイ、という流れになります。他にもRemixを使うというのも良さそうですね。

  • Metamask
    • 仮想通貨の財布、ブラウザのプラグインとして使う
    • チェーンに記録されている自分の現在の資金量や過去の取引を確認したり、Web側から送金処理のリクエストがあった場合、Metamask上から承認するなどが出来る
  • truffle
    • イーサリアムのアプリケーションを開発するフレームワーク
    • プロジェクトの作成、コンパイル、テスト、デプロイなどの機能を持つ
  • Ganache
    • ローカルにチェーンのネットワークを構築するテスト用のツール
    • 当面はこのチェーンにプログラムをデプロイしていきます
  • Remix
    • 開発やデプロイをWebのIDEで実施できるサービス

メタバースの世界を作ろう

Unityとブロックチェーンを両方知っている人はそんなには多くないと思うので、技術的な部分に触れていこうかと思います。本節では、全体の設計、バックエンドの仕組み構築、Unityによるフロントの構築について記載していきます。

全体の設計

今回はフロントをUnityで実装し、WebGLで出力することとします。WebGLの中でブラウザ上でJavaScriptを呼べるので、そこからバックエンドのチェーンにアクセスしたり、ブラウザに入れたMetaMaskと繋ぎます。ちょっと複雑になってくるので整理のために登場人物を出すとこんな感じになります。うーん、沢山いますね。

image.png

さて、今回はUnityとSolidityを使うということで、設計を考えてみましょう。フロント側としては、マルチプレイヤーでの通信を介し、空間内を散歩する機能とチャット機能を実装します。バックエンド関連については、通貨の送付および交換の機能、交換リクエストの確認、ユーザ情報の登録機能を作ることとします。

Solidityによるバックエンド

今回は大きく分けて3つの機能を作りました。まずはユーザ情報を登録・変更する機能、最後に今回向けに用意した独自のコイン、そしてコアとなるAtomicSwapです。AtomicSwapは先程説明したので、最初の2つをそれぞれ見ていきましょう。

ユーザ登録・変更

まずデータとしては、ウォレットのアドレスから名前への参照と、その逆を用意しておきます。

mapping (address => string) private addressToName;
mapping (string => address) private nameToAddress;

細かいSolidityの構文についてはここでは触れないので、心の目で見てください。こちらは先程の辞書を呼び出して名前とアドレスを確認する機能です。ちなみにviewというのは、チェーンに対して書き込みが行われない事を示す予約語です。

function getName(address _id) external view returns (string memory) {
    return (addressToName[_id]);
}

function getAddress(string memory _name) external view returns (address) {
    return (nameToAddress[_name]);
}

続いて名前の変更のコードです。名前を変更する時は、過去に使っていた辞書を初期化するとともに新しく登録するという流れです。名前の変更履歴はチェーン上にイベントとして記録しておきます。emitを使うことで記録が出来ます。

event SetName(address indexed _addr, string _str);

function setName(string memory _name) external returns (bool ok) {
    require(nameToAddress[_name] == address(0));

    nameToAddress[addressToName[msg.sender]] = address(0);
    addressToName[msg.sender] = _name;
    nameToAddress[_name] = msg.sender;

    emit SetName(msg.sender, _name);

    return true;
}

独自トークンの発行

トークンの発行はopenzeppelinというパッケージを使うことでメチャクチャ簡単です。npmでopenzeppelinをインストールしたら、後はコンストラクタでデプロイ時の通貨発行量だけ決めてやります。今回は名前を天一にしました。天一美味しいよね。親クラスのERC20って所に実装が含まれています。

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

contract TenichiToken is ERC20 {
  constructor (uint256 initialSupply) ERC20("Tenich", "T1") {
    _mint(msg.sender, initialSupply * 10 ** uint(decimals()));
  }
}

Unityによるフロント

今回は、メタバースというテーマで、特にコミュニケーションとチェーン関連にフォーカスするので、それと直接関係ない部分は省略します。Unity内では、コミュニケーションおよびバックエンドとの通信に絞って書いていきます。

マルチプレイヤーについて

今回はPhotonっていうリアルタイムマルチプレイのためのUnityパッケージを使うことにしました。Photonを使うことによりWebGL向けのビルドができ、Unityで開発したアプリを簡単にブラウザ上に移植できます。今回は、自身のアバターがカーソルキーで空間内を移動できつつカメラがそれを自動追尾し、そのマルチプレイヤー版を作っていきましょう。まずプレイヤーは今回はこんな簡易なものににしてみます。

username.png

プレイヤーは頭上にユーザ名のみ備えます。また追従用のカメラを設定しておきます。ここにスクリプトをアタッチして、カメラ・テキストなどを登録しておきます。追従カメラは、対象物のすぐ後ろを走るものとして、以下のスクリプトを持つものとします。以下のコードに示すように、カメラは常にターゲットの後ろの位置になるようにしつつ、ターゲットの位置に視線を向けます。

void Update() {
        if(localPlayerTarget && cameraToTarget) {
              Vector3 targetPos =  cameraToTarget.position + localPlayerTarget.forward * offset.z + 
                                    localPlayerTarget.up * offset.y + localPlayerTarget.right * offset.x;

          Quaternion newRotation = Quaternion.LookRotation(cameraToTarget.position - targetPos,Vector3.up );
              transform.rotation =  Quaternion.Lerp(transform.rotation,newRotation,damping*Time.deltaTime);
          transform.position = Vector3.Lerp(transform.position,targetPos,damping * Time.deltaTime);
      }

    }

これをマルチプレイ対応にしていきましょう。Photonでは、オブジェクトを作成する時に、PhotonNetwork.Instantiateとしてプレハブ名を入れると、自動でそのオブジェクトをネットに登録し、動機が行われます。移動のコントロールについてはPhotonViewから、自分のオブジェクトかどうかを調べ、後に記載するチャットへのフォーカスが当たっていなければ、キー判定と移動を行います。なお、他のプレイヤーとの移動の同期については、プレハブにPhotonViewとPhotonTransformViewを追加することで実現できるようです。

if( m_photonView.IsMine) {
              GameObject selectingObject = eventSystem.currentSelectedGameObject;
              if (selectingObject == null) {
                    Move();
              }
              userName.text = PhotonNetwork.LocalPlayer.NickName;
    }
}

チャット

チャットについてもPhotonViewの機能を使ってメッセージを送信します。まずは、チャットを管理するキャンバスなりにスクリプトとPhotonViewコンポーネントをアタッチします。チャットを送信するボタンを押すと、以下のスクリプトを通しチャットを全員に送信します。受信側の関数名をChatにしておきます。

public void SendChat() {
    //chatRPC
    photonView.RPC("Chat", RpcTarget.All, inputText.text);

    //送信後、入力欄を空にし、スクロール最下位置に移動
    inputText.text = "";
}

受信側はメソッドの前に[PunRPC]をつけてやれば、誰かがチャットを送信すると、このメソッドが起動します。RecieveChatでチャットのテキストボックスを更新します(ここでは割愛します)。

[PunRPC]
public void Chat(string newLine, PhotonMessageInfo mi) {
    if (messages.Count > 100) {
        messages.RemoveRange(0, messages.Count - 100);
    }
    ReceiveChat(newLine, mi);
}

なおちょっとした苦労ですが、チャットが更新された時に自動スクロールが出来なくて困りましたが、以下のようにすることで解決できました。チャット更新後にScrollChatを呼び出します。ちなみにForceUpdateCanvasesを何度も呼んでますが、チャットが更新される前にスクロールが更新されてしまう事が原因らしく、何度も更新を掛ける事で無理やり解決しているみたいです。

private void ScrollChat() {}
    StartCoroutine(ForceScrollDown());
    Canvas.ForceUpdateCanvases();
}

IEnumerator ForceScrollDown () {
    yield return new WaitForEndOfFrame ();
    Canvas.ForceUpdateCanvases ();
    scrollRect.gameObject.SetActive(true);
    scrollRect.verticalNormalizedPosition = 0f;
    scrollRect.verticalScrollbar.value = 0;
    Canvas.ForceUpdateCanvases ();
}

image.png

フロント側のJavaScriptファイルとの接続

チャットやプレイヤーの動作ログのように、記録として残さなくて良いものは、Unityの中で完結させました。ようやくここからブロックチェーンとの接続を行います。バックエンドとの接続は、JavaScriptを介して行います。UnityからJavaScriptを呼び出す機能としてjslibというのがあります。ただ、直接jsファイルの関数を叩けなかったり、WebGLビルドする度に2〜3分待つ必要があり結構辛いです。ですので、jslibから関数名と渡したい引数を持ったイベントを発行して、js側でイベントをキャッチする仕組みにしてやります。汎用的なイベント発行をコールすれば良く、js側の変更があってもUnity側は変わらないため、再ビルドが不要になります。ビルドして出来上がった後のindex.htmlとjsファイルを直接編集することで一通りの機能を実装してから、最後にテンプレートを作ってやりましょう。

C#側の汎用のイベント発行スクリプトは以下のようになります。jslibを呼び出すためのガワの部分になります。jsファイル側の関数名をmethodName、任意の引数を与えられるようにjsonをstring型にしたものを渡しておきます。

[DllImport("__Internal")]
private static extern void execute(string methodName, string parameter);

public void Execute(string methodName, string parameter = "{}")
{
    execute(methodName, parameter);
}

jslib側はこんな感じです。すみません、こちらのコードを利用させていただきました。関数名と引数を入れたargsmentStringをパラメータとして、unityMessageというイベントを発行してやります。

mergeInto(LibraryManager.library, {
  execute: function(methodName, parameter) {
    // jsの文字列に変換する
    methodName = Pointer_stringify(methodName)
    parameter = Pointer_stringify(parameter)

    // 実行するメソッド名とパラメータをまとめる
    var jsonObj = {}
    jsonObj.methodName = methodName
    jsonObj.parameter = parameter

    var argsmentString = JSON.stringify(jsonObj)
    var event = new CustomEvent('unityMessage', { detail: argsmentString })
    window.dispatchEvent(event)
  }
});

あとはJavaScript側で自由にコードを呼び出せるようになります。なお、予め、Unity側から渡すパラメータにコールバック関数の情報を入れておくことで、SendMessageでunity側に結果を返すこともできます。

function hogeMethod(parameter) {
  console.log(parameter)
  unityInstance.SendMessage(parameter.callbackGameObjectName, parameter.callbackFunctionName)
}

function recieveMessage(event) {
  var data = JSON.parse(event.detail)
  var methodName = data.methodName
  var parameter = data.parameter
  try {
    parameter = JSON.parse(parameter)
  } catch (e) {
    parameter = null
  }
  // C#から指定されているメソッドを呼び出しparameterを渡す
  eval(`${methodName}(parameter)`)
}

// unityMessageというCustomEventを受け取る
window.addEventListener('unityMessage', recieveMessage, false)

JavaScriptとチェーンの接続

次はWeb側のスクリプトを書いていきましょう。全ての機能を書くと長くなってしまうので一部だけ紹介します。まずはログイン時のコネクションからです。ブラウザを開いたらウォレットに接続し、ウォレットのアドレスを取得します。以下のコードの前半がブラウザに接続した時の初期化、後半がアドレスの取得です。ブロックチェーンと接続するためのライブラリがWeb3というもので、これを初期化します。また、ユーザ情報に関するコントラクト、アトミックスワップに関するコントラクトを初期化します。connectが呼ばれると、MetaMaskに問い合わせてアカウント情報を取得します。

$(document).ready(() => {
  if (typeof window.ethereum !== 'undefined') {
    web3js = new Web3(Web3.givenProvider || "ws://localhost:7545");
    contractUserinfo = new web3js.eth.Contract(userinfoABI, userinfoSwapAddress);
    contractAtomicSwap = new web3js.eth.Contract(atomicSwapABI, atomicSwapAddress);
  } else {
    alart("Install Metamask");
  }
});

async function connect() {
  try {
    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
    userAccount = accounts[0];
  } catch (error) {
    alert(error.message);
    return -1;
  }
  return 0;
}

まずはユーザ名登録のコードを見てみましょう。チェーンにデプロイした機能を呼び出す場合は、先程初期化したコントラクトから、methods.関数名().send({from: アドレス})としてやります。なお、チェーン側の情報のアップデートがない場合は、sendではなくcallとします。以下の場合は、userAccountのアカウントでsetName関数にnameを与えるという意味になります。

async function set_username(name) {
  try {
    await contractUserinfo.methods.setName(name).send({from: userAccount});
  } catch (error) {
    alert(error.message);
    return -1;
  }
  return 0;
}

今回の肝であるAtomic Swapの呼び出しを見てみましょう。長い!汚い!ごめんなさい!もうこの辺まで実装している時は完全にグロッキーだったんですよ。大きくは4つの手順から成ります。(1)は交換先のユーザ名からアドレスを引く処理です。チェーンに変更を掛けないので、callを呼んでいる事も確認できますね。(2)は交換リクエストのIDの生成です、OpenerとCloserはこのIDを使って交換の内容を参照します。(3)は交換したい数量の計算です。実は仮想通貨は、1枚の通貨を送ろうとした場合、その中身は1e+18とかの大きな数値だったりします。ちなみに最小の単位を1weiと言ったりします。1枚の通貨が1e+18weiだった場合、ユーザが15枚通貨を送る場合はその15倍になります。桁数がめちゃめちゃ多いので、普通に計算するとオーバーフローしてしまうためBigNumberにしてやった上で、バックエンドに送信するために16進数に変換するという処理を行います。(4)最後に、Opener側がコインを預けて良いよというApproveを行った上で、リクエストを出してやります。

async function request_swap(name, openToken, openValue, closeToken, closeValue) {
  try {
    // (1)
    var targetAddress = await contractUserinfo.methods.getAddress(name).call();

    // (2)
    const swapSeed = Math.random().toString(36) + Math.random().toString(36);
    const swapID = '0x' + keccak256(swapSeed).toString('hex');

    // (3)
    var openContract = new web3js.eth.Contract(tokenABI, openToken);
    const openDecimals = await openContract.methods.decimals().call();
    const openBigValue = BigNumber(openValue).times(BigNumber(10).pow(openDecimals));
    const openBigValueHex = "0x" + openBigValue.toString(16);

    var closeContract = new web3js.eth.Contract(tokenABI, closeToken);
    const closeDecimals = await closeContract.methods.decimals().call();
    const closeBigValue = BigNumber(closeValue).times(BigNumber(10).pow(closeDecimals));
    const closeBigValueHex = "0x" + closeBigValue.toString(16);

    // (4)
    await openContract.methods.approve(atomicSwapAddress, openBigValueHex).send({from: userAccount});
    await contractAtomicSwap.methods.open(swapID, openBigValueHex, openToken, closeBigValueHex, targetAddress, closeToken).send({from: userAccount});
  } catch (error) {
    alert(error.message);
    return -1;
  }
  return 0;
}

長くて面倒になりますが、過去にチェーン上に登録されたリクエストのうち、まだ完了していないものをフィルタして取得してやります。ここでfilterパラメータを使うことで、自分に関するもののみを取得sてきます。真ん中のごちゃごちゃしている奴は、SwapIDから詳細情報を取って来たり、詳細情報にかかれているweiを元の通貨の単位に戻す処理です(get_swap_info)。

async function get_requested_swap_impl(trader) {
  var view = []
  try {
    const ret = await contractAtomicSwap.getPastEvents("Open", { filter: {[trader]: userAccount}, fromBlock: 0 });

    const swapID = ret.map(item => item['returnValues'][0])
    const opened = await Promise.all(swapID.map(async item => await (contractAtomicSwap.methods.isOpened(item).call())))
      .then(bits => swapID.filter(i => bits.shift()))
    const data = await Promise.all(opened.map(async item => await contractAtomicSwap.methods.check(item).call()))
    view = await Promise.all(data.map(async item => await get_swap_info(item)))

    for (var i = 0; i < view.length; i++) {
      view[i]['swapID'] = opened[i]
    }
  } catch (error) {
    alert(error.message);
  }

  return view
}

get_swap_infoの実装は以下のようになっています。交換リクエストを呼び出す時と反対の処理ですね。これにて、交換リクエストの送信と、送信出来たかどうかの確認が出来ました。

async function get_swap_info(data) {
  const openTraderName = await contractUserinfo.methods.getName(data['openTrader']).call();
  const closeTraderName = await contractUserinfo.methods.getName(data['closeTrader']).call();

  var openContract = new web3js.eth.Contract(tokenABI, data['openContractAddress']);
  const openDecimals = await openContract.methods.decimals().call();
  const openValue = BigNumber(data['openValue'].toString()).div(BigNumber(10).pow(openDecimals)).toNumber();
  const openSymbol = await openContract.methods.symbol().call()

  var closeContract = new web3js.eth.Contract(tokenABI, data['closeContractAddress']);
  const closeDecimals = await closeContract.methods.decimals().call();
  const closeValue = BigNumber(data['closeValue'].toString()).div(BigNumber(10).pow(closeDecimals)).toNumber();
  const closeSymbol = await closeContract.methods.symbol().call()

  return {
    'openTraderName': openTraderName,
    'closeTraderName': closeTraderName,
    'openValue': openValue,
    'openSymbol': openSymbol,
    'closeValue': closeValue,
    'closeSymbol': closeSymbol
  }
}

交換リクエストを受け取るCloserは以下の処理を行います。こちらも自身が送る通貨量を承認するためにwei単位に変換します。後は、承認→closeを行って交換完了となります!長くなるのでここでは触れませんが、この他に、名前の変更・アカウント削除・交換リクエスト取り消しを実装しました。

async function close_swap(swapID) {
  try {
    const data = await contractAtomicSwap.methods.check(swapID).call();

    var closeContract = new web3js.eth.Contract(tokenABI, data['closeContractAddress']);
    const closeBigValue = BigNumber(data['closeValue'].toString());
    const closeBigValueHex = "0x" + closeBigValue.toString(16);

    await closeContract.methods.approve(atomicSwapAddress, closeBigValueHex).send({from: userAccount});
    await contractAtomicSwap.methods.close(swapID).send({from: userAccount});
  } catch (error) {
    alert(error.message);
    return -1;
  }
  return 0
}

動作確認!

ログイン画面はこちら。Connectを押すとウォレットのアカウントを読み取り、仮想空間に入ることができる。タイトルは凄く悩んだ挙げ句ネタ性が少ないけど、思いついちゃったのでネタバースにしました。フォントだけお気に入り。なんかこう、おじさん的な未来感あるよね。

image.png

中にに入ってるとこんな感じ。殺風景だけど許してね。右上が各種設定ボタン、左下がチャット、左上が取引ボタン。後でスクショ出します。前後と回転方向に動くことができます。

image.png

お、別プレイヤーが入ってきたぞ。チャットしてみよう(自作自演)。

image.png

というわけで、お金を送ってみよう。最初の状態はこうなっている。とりあえず独自通貨を2つほど作り、1000T1(天一)と、10000CRY(カレー)を持っている事にします。大金持ち!

image.png

ここから100天一くらい送ってみましょう。

image.png

するとMetaMaskから確認のウィンドウが開き、確認ボタンを押すと送付が完了する。手数料なんと6ドル。でもテストネットなので大丈夫。

image.png

100T1減って900T1になりました。送信は成功。

image.png

また、送付先のアカウントに、100T1送られているのが確認できました。画面サイズの問題でアカウント名が見えないですが、成功してますよ。

image.png

次に、通貨の交換をしてみましょう。今送った100CRY(カレーコイン)を送って、先程の100T1を返してもらうことにします。

image.png

再度、MetaMaskの承認画面が表れましたので承認しましょう。

image.png

100CRYがチェーンに預けられていることが分かります。

image.png

もう一方のアカウントに入ってみると、交換の申し出が通知されている事を確認できます。Approveを押して交換を承認をしましょう。

image.png

こちらのアカウントはT1コインを返送するので、T1コインに対するアクセス権を求められるのでOKします。

image.png

では、最初のアカウントを確認してみましょう。無事、大事な天一コインが返ってきていることを確認できました!

image.png

すんごい頑張ったのに、結構地味!!!!!!!!笑笑笑笑

おわりに

最近、世の中では色々なところでメタバースと言わるようになってきてますが、僕自身も改めてメタバースって何だろう、人の生活って何だろうっていう所から、メタバースってこういうものかなという、必要な3要素を定義してみました。で、「じゃあ作ってみよう!」という事で、簡単なものではありつつもUnityとSolidtyを使って具現化し、コアとなるチャットや資産の送付・交換の機能を実装しました。ログを見ると、愚地独歩になったのが11月28日だったので、そこから約2週間、UnityとSolidtyを初めて触りつつ、なんとかここまでたどり着けました、いやー疲れた〜。思いつきでやっただけなので、現時点では特に展望とかは無いです。でも、Unityでフロント書けるのは便利だし、ブロックチェーンの技術も面白いので、何かやりたいな〜と思ったりしてます。

おまけ

論旨と外れる事は上で書きませんでしたが、ここで色々考えてみる。

余談1

VRの世界を、あくまでコミュニケーションのツールとみなすか、そこで手に入れた物を資産として見なすかによって、受け止め方は変わるだろうなって思います。少なくともコミュニケーションを目的とするのであれば仮想通貨の議論は不要ですよね。しかも、お金に変わる資産と考えた瞬間に、なんだか面白さが減ってしまうっていうのもあります。僕の経験としても、大学生の時に、Ragnarok Onlineを結構やっていましたが、その中でレアアイテムを資産のように扱う(RTMはもちろん禁止ですが、コレクション的な扱い)人もいれば、僕なんかは装備は最低限で、どちらかというと年中チャットしてました。同じように、現実の世の中にも、純粋にコミュニケーションのためのコミュニティもあれば、資産に関する活動を含めたコミュニティもありますので、これらの考え方は共存できるものかなって思っています。

余談2

実のところ、ぶっちゃけ仮想通貨の技術いらんやんってなるケースは非常に多いとは思います。そんな中、僕が何故仮想通貨が大事かと思うかというと、将来的に真にデータを保有できる時代が来るんじゃないかなって考えているからです。今の所は、NFTと言ったって短いURL程度の文字列を自分のものと主張出来る程度ですし、データをチェーン上に保存と言ってもダダ漏れですから、まだまだ全然ダメなんですけどね。オンチェーンNFTとか技術の進歩も速いので、実は既にあるかもですが。いつの日か、自分のローカルPCに保存するノリでチェーン上にデータを保存できれば、Amazon Kindleのように読む権利を貸与されるのではなく、実世界で物理の本を買うようにデータを持てるようになるんじゃないかなって妄想してます。

余談3

書き忘れていたので追記。今の日本の法律の中では、暗号通貨等で資産を交換すると、その時点で利確とみなされるので確定申告が必要になります(が、正確なことはプロに聞きましょう)。そんなこともあり、金融的な価値に繋がるのは早々無いと思うけど、何かのゲームのアイテムと、別のゲームのアイテムと交換するとか、そのくらいのイメージでします。

余談4

MMORPGのRMTじゃんって言われたら、ぶっちゃけかなり近いです。ゲームにおいて換金するというのは、MMORPGでもリアルのゲームセンターでも、恐らく賭博法の関係でNGだったりするので、本記事をゲームに絞り、法律の話をしてしまうとややこしいですね。なお本記事限れば賭博性は無いので大丈夫な気はするけど。まぁ、その辺の議論は一旦さておき、資産の交換は生活における本質的な行動と考えており、そういった観点で読んでもらえると・・・。

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
56