ボスとのバトルフィールドを作ろう
💥 ボスと戦う
前回のレッスンで、シナリオの1と2を実装しました。
✅ シナリオ 1. ユーザーが Web アプリケーションにログインしていない場合
👉 WEBアプリ上に、"Connect Wallet to Get Started" ボタンを表示します。
✅ シナリオ 2. ユーザーは Web アプリケーションにログインしており、かつ NFT キャラクターを持っていない場合
👉 WEBアプリ上に、SelectCharacter
コンポーネントを表示します。
これから、ボスとのバトルフィールド「Arena
」を作成し、シナリオ3を実装していきます。
🔥 シナリオ 3. ユーザーは Web アプリケーションにログインしており、かつ NFT キャラクターを持っている場合
👉 WEBアプリ上に、「Arena Component」を表示します。
- 「Arena Component」は、プレイヤーがボスと戦う場所です。
まず、ターミナル上でclient/src/Components/Arena
フォルダに移動して、index.js
という名前の新しいファイルを作成しましょう。
Arena
フォルダにはArena.css
ファイルが含まれています。
Webアプリケーションの構築が完了したら、CSSのスタイリングを楽しんでください ✨
🏰 Arena
を作成する
次に、client/src/Components/Arena/index.js
を開き、下記のコードを貼り付けましょう。
import React, { useEffect, useState } from "react";
import { ethers } from "ethers";
import { CONTRACT_ADDRESS, transformCharacterData } from "../../constants";
import myEpicGame from "../../utils/MyEpicGame.json";
import "./Arena.css";
// フロントエンドにNFTキャラクターを表示するため、characterNFTのメタデータを渡します。
const Arena = ({ characterNFT }) => {
// コントラクトのデータを保有する状態変数を初期化します。
const [gameContract, setGameContract] = useState(null);
// ページがロードされると下記が実行されます。
useEffect(() => {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const gameContract = new ethers.Contract(
CONTRACT_ADDRESS,
myEpicGame.abi,
signer
);
setGameContract(gameContract);
} else {
console.log("Ethereum object not found");
}
}, []);
return (
<div className="arena-container">
{/* ボス */}
<p>ボスを表示します。</p>
{/* NFT キャラクター */}
<p>NFT キャラクターを表示します。</p>
</div>
);
};
export default Arena;
Arena
コンポーネント準備ができたので、App.js
に戻って、シナリオ3を実装していきます。
シナリオ 3. ユーザーは Web アプリケーションにログインしており、かつ NFT キャラクターを持っている場合
👉 WEBアプリ上に、「Arena Component」を表示します。
- 「Arena Component」は、プレイヤーがボスと戦う場所です。
まず、Arena
をインポートするため、App.js
の先頭に、下記を追加しましょう。
import Arena from "./Components/Arena";
次に、renderContent
関数を下記のように更新しましょう。
// レンダリングメソッド
const renderContent = () => {
// シナリオ1.
// ユーザーがWEBアプリにログインしていない場合、WEBアプリ上に、"Connect Wallet to Get Started" ボタンを表示します。
if (!currentAccount) {
return (
<div className="connect-wallet-container">
<img src="https://i.imgur.com/TXBQ4cC.png" alt="LUFFY" />
<button
className="cta-button connect-wallet-button"
onClick={connectWalletAction}
>
Connect Wallet to Get Started
</button>
</div>
);
// シナリオ2.
// ユーザーはWEBアプリにログインしており、かつ NFT キャラクターを持っていない場合、WEBアプリ上に、"SelectCharacter Component" を表示します。
} else if (currentAccount && !characterNFT) {
return <SelectCharacter setCharacterNFT={setCharacterNFT} />;
// シナリオ3.
// ユーザーはWEBアプリにログインしており、かつ NFT キャラクターを持っている場合、
// Arena でボスと戦います。
} else if (currentAccount && characterNFT) {
return <Arena characterNFT={characterNFT} />;
}
};
Webアプリケーションを更新すると、「アリーナ」コンポーネントに直接移動します。
フロントエンドが下記のように表示されていれば、ここまでの実装は成功です。
😈 スマートコントラクトからボスを取得する
それでは、Arena
コンポーネントに、ボスのデータを取得していきましょう。
SelectCharacter
コンポーネントで、NFTキャラクターのデータを取得した方法と同じ要領で進めていきます。
まず、Arena
コンポーネントの中のconst [gameContract, setGameContract] = useState(null);
の直下に下記を追記してください。
// ボスのメタデータを保存する状態変数を初期化します。
const [boss, setBoss] = useState(null);
// ページがロードされると下記が実行されます。
useEffect(() => {
// コントラクトからボスのメタデータを取得し、bossを設定する非同期関数 fetchBoss を設定します。
const fetchBoss = async () => {
const bossTxn = await gameContract.getBigBoss();
console.log("Boss:", bossTxn);
setBoss(transformCharacterData(bossTxn));
};
if (gameContract) {
// コントラクトの準備ができたら、ボスのメタデータを取得します。
fetchBoss();
}
}, [gameContract]);
上記の実装が完了したら、Webアプリケーション上でConsoleを開いて、ボスのデータが読み込まれていることを確認しましょう。
🙀 ボスをフロントエンドにレンダリングする
まず、Arena/index.js
に向かい、const [boss, setBoss] = useState(null);
の直下に下記を追加しましょう。
// NFTキャラクターがボスを攻撃する際に使用する関数を定義します。
const runAttackAction = async () => {};
次に、Arena/index.js
のreturn();
の中身を下記のように更新しましょう。
return (
<div className="arena-container">
{/* ボスをレンダリングします */}
{boss && (
<div className="boss-container">
<div className={`boss-content`}>
<h2>🔥 {boss.name} 🔥</h2>
<div className="image-content">
<img src={boss.imageURI} alt={`Boss ${boss.name}`} />
<div className="health-bar">
<progress value={boss.hp} max={boss.maxHp} />
<p>{`${boss.hp} / ${boss.maxHp} HP`}</p>
</div>
</div>
</div>
<div className="attack-container">
<button className="cta-button" onClick={runAttackAction}>
{`💥 Attack ${boss.name}`}
</button>
</div>
</div>
)}
{/* NFT キャラクター */}
<p>NFT キャラクターを表示します。</p>
</div>
);
ローカルサーバーで、Webアプリケーションを開き、下記のようにボスがArena
にレンダリングされていることを確認してください。
🛡 NFT キャラクターをArena
にレンダリングする
ボスとのバトルフィールドであるArena
に、NFTキャラクターをレンダリングしましょう。
Arena/index.js
のreturn();
の中身を下記のように更新しましょう。
return (
<div className="arena-container">
{/* ボスをレンダリングします */}
{boss && (
<div className="boss-container">
<div className={`boss-content`}>
<h2>🔥 {boss.name} 🔥</h2>
<div className="image-content">
<img src={boss.imageURI} alt={`Boss ${boss.name}`} />
<div className="health-bar">
<progress value={boss.hp} max={boss.maxHp} />
<p>{`${boss.hp} / ${boss.maxHp} HP`}</p>
</div>
</div>
</div>
<div className="attack-container">
<button className="cta-button" onClick={runAttackAction}>
{`💥 Attack ${boss.name}`}
</button>
</div>
</div>
)}
{/* NFT キャラクター をレンダリングします*/}
{characterNFT && (
<div className="players-container">
<div className="player-container">
<h2>Your Character</h2>
<div className="player">
<div className="image-content">
<h2>{characterNFT.name}</h2>
<img
src={characterNFT.imageURI}
alt={`Character ${characterNFT.name}`}
/>
<div className="health-bar">
<progress value={characterNFT.hp} max={characterNFT.maxHp} />
<p>{`${characterNFT.hp} / ${characterNFT.maxHp} HP`}</p>
</div>
</div>
<div className="stats">
<h4>{`⚔️ Attack Damage: ${characterNFT.attackDamage}`}</h4>
</div>
</div>
</div>
</div>
)}
</div>
);
ローカルサーバーで、Webアプリケーションを開き、下記のようにあなたのNFTキャラクターがArena
にレンダリングされていることを確認してください。
🥊 ボスとのバトルを実装する
これから、ボスとのバトルを実装していきます。
Arena/index.js
を下記のように更新していきましょう。
// コントラクトのデータを保有する状態変数を初期化します。
const [gameContract, setGameContract] = useState(null);
// ボスのメタデータを保存する状態変数を初期化します。
const [boss, setBoss] = useState(null);
// 攻撃の状態を保存する変数を初期化します。
const [attackState, setAttackState] = useState("");
// ボスを攻撃する関数を設定します。
const runAttackAction = async () => {
try {
// コントラクトが呼び出されたことを確認します。
if (gameContract) {
// attackState の状態を attacking に設定します。
setAttackState("attacking");
console.log("Attacking boss...");
// NFT キャラクタ ーがボスを攻撃します。
const attackTxn = await gameContract.attackBoss();
// トランザクションがマイニングされるまで待ちます。
await attackTxn.wait();
console.log("attackTxn:", attackTxn);
// attackState の状態を hit に設定します。
setAttackState("hit");
}
} catch (error) {
console.error("Error attacking boss:", error);
setAttackState("");
}
};
更新したのは、以下の2点です。
1 . const [boss, setBoss] = useState(null);
の直下に、下記を追加。
// 攻撃の状態を保存する変数を初期化します。
const [attackState, setAttackState] = useState("");
attackState
はバトル中にアニメーションを発生させるために追加しています。
setAttackState
を使用すると、attackState
に、下記3つの状態のいずれかを保存できます。
-
attacking
: 攻撃をした後、トランザクションが完了するのを待っている状態 -
hit
: ボスに攻撃がヒットした状態 -
''
: デフォルトの状態
client/src/Components/Arena/Arena.css
を開いて、attacking
やhit
を調べてみてください。
- アニメーションのためのCSSが設定されています ✨
2 . runAttackAction
関数の中身を更新。
これから、Webアプリケーションに上記の実装を連携させていきます。
Arena/index.js
の中のreturn();
の中身を見ていきましょう。
{boss ..}
の中身を下記のように更新しましょう。
return (
<div className="arena-container">
{/* ボスをレンダリングします */}
{boss && (
<div className="boss-container">
{/* attackState 追加します */}
<div className={`boss-content ${attackState}`}>
<h2>🔥 {boss.name} 🔥</h2>
<div className="image-content">
<img src={boss.imageURI} alt={`Boss ${boss.name}`} />
<div className="health-bar">
<progress value={boss.hp} max={boss.maxHp} />
<p>{`${boss.hp} / ${boss.maxHp} HP`}</p>
</div>
</div>
</div>
<div className="attack-container">
<button className="cta-button" onClick={runAttackAction}>
{`💥 Attack ${boss.name}`}
</button>
</div>
</div>
)}
{/* ここまでを更新 */}
...
</div>
);