こんにちは。トイロジックの新人プログラマーF.Sです。トイロジックでは毎年新入社員の研修として「iPhone用ゲーム開発研修」が行われています。本記事では、この研修で作成したスキルツリー周りの話をまとめてみたいと思います。

スキルツリーの解説

今回、私たちが作成したゲームは大量の敵から逃げながら、味方のクイモンを呼び寄せて敵をバク食いさせる爽快アクションゲーム「バクバクバック」です。

本作品ではゲームをプレイすることで手に入る経験値を利用してプレイヤーを永続的に強化できるスキルツリーが存在します。下記画像が実装されたスキルツリーの画像です。

スキルツリー中の各ノードをタッチすると詳細説明画面があらわれ、その画面中の取得ボタンを押すことでスキルの取得が行えます。ここで、スキルツリーには大きく分けて各スキルのノード各スキルをタッチした際の詳細説明現在の強化状況の3つの要素が必要となります。詳細説明の表示や強化状況の表示は主にテクスチャを入れ替えただけなので、ここでは各スキルのノードに焦点を当ててみようと思います。

シンプルなノードの実装

各ノードが持つべき情報は、1つ前のノード取得した際どうするか各テクスチャ情報取得に必要経験値の4つです。あとはこのノードが取得済みかどうかのbool値が必要な程度で、クラスとしては非常にシンプルです。軽く実装をまとめてみると以下のようになります。


class SkillNode
{
private:
	SkillNode* prevNode_ = nullptr;		// 前のノード 
	std::function<void()> onGet_;		// 取得した際に実行する関数 
	int needPoint_ = 0;			// 必要な経験値 
	char* textureName_;			// スキル説明用の画像名 
	bool isGet_;				// このスキルが取得済みかどうか
public:
	void SetNodeInfo(SkillNode* prev_node, std::function<void()> on_get, int need_point, char* texture_name)
	{
		prevNode_ = prev_node;
		onGet_ = on_get;
		needPoint_ = need_point;
		textureName_ = texture_name;
	}
	void SetIsGet(bool is_get) { isGet = is_get; }
	SkillNode* GetPrevNode() { return prevNode_; }
	std::function<void()> GetOnGet() { return onGet_; }
	int GetNeedPoint() { return needPoint_; }
	char* GetTextureName() { return textureName_; }
	bool GetIsGet() { return isGet_; }
};

上記パラメータのGet関数とSet関数があるだけのシンプルなクラスです。このクラスはあくまで必要そうな情報を簡潔に示したものであり、実際にはこのノードはボタンとして機能する必要があるため、このノードクラス自身がボタンクラスを継承しているか、ノードクラス内にボタンクラスを変数としてもっておく必要があります。

あとは、このノードが押された際に詳細説明を表示して、その中の取得ボタンを押した際にonGet_を実行すればノードの基本的な役割は果たせるのではないでしょうか?

スキルツリーの構築

ここからは、スキルツリーの構築周りについてまとめてみます。まずは次のコードをご覧ください。


class SkillTree
{
private:
	SkillNode* node_[15];				// 全スキルツリーのノード
	SkillNode* selectNode_ = nullptr;		// 現在選択中のスキルツリーのノード
	Button* getButton_ = nullptr;			// 詳細画面中の取得ボタン
	int havePoint_ = 1000;				// 所持経験値. セーブデータ等からとってくる
public:
	void SetUp()
	{
	
		for (auto& node : nodes_)
		{
			node = new SkillNode();

			// ボタンクラスが継承してあるとしてボタンが押された際の処理を追加
			node->Init([this]
				{
					selectNode_ = node; 
					ShowExplain();		// 詳細画面表示用関数
				});
		}

		nodes_[0]->SetNodeInfo(nullptr, [this] { AddLife(1); }, 10, "uin_tree_life");
		nodes_[1]->SetNodeInfo(nodes_[0], [this] { AddLife(1); }, 10, "uin_tree_life");
		nodes_[2]->SetNodeInfo(nodes_[1], [this] { AddLife(1); }, 10, "uin_tree_life_detail");
		nodes_[3]->SetNodeInfo(nodes_[1], [this] { GetExtraSkill("HEAL"); }, 10, "uin_tree_heal");
		nodes_[4]->SetNodeInfo(nodes_[1], [this] { AddLife(1); }, 10, "uin_tree_life");
	
		nodes_[5]->SetNodeInfo(nullptr, [this] { AddMoveSpeed(0.01f); }, 10, "uin_tree_speed");
		nodes_[6]->SetNodeInfo(nodes_[5], [this] { AddMoveSpeed(0.01f); }, 10, "uin_tree_speed");
		nodes_[7]->SetNodeInfo(nodes_[6], [this] { AddMoveSpeed(0.01f); }, 10, "uin_tree_speed_detail");
		nodes_[8]->SetNodeInfo(nodes_[6], [this] { GetExtraSkill("HEALTHDASH"); }, 10, "uin_tree_healthdash");
		nodes_[9]->SetNodeInfo(nodes_[6], [this] { AddMoveSpeed(0.01f); }, 10, "uin_tree_speed");
	
		nodes_[10]->SetNodeInfo(nullptr, [this] { ReduceCoolTime(0.01f); }, 10, "uin_tree_cooltime");
		nodes_[11]->SetNodeInfo(nodes_[10], [this] { ReduceCoolTime(0.01f); }, 10, "uin_tree_cooltime");
		nodes_[12]->SetNodeInfo(nodes_[11], [this] { ReduceCoolTime(0.01f); }, 10, "uin_tree_cooltime");
		nodes_[13]->SetNodeInfo(nodes_[11], [this] { GetExtraSkill("HEALTHCALL"); }, 10, "uin_tree_healthcall");
		nodes_[14]->SetNodeInfo(nodes_[11], [this] { ReduceCoolTime(0.01f); }, 10, "uin_tree_cooltime");

		// 取得ボタン(getButton_)にもボタンが押された際の処理を追加
		getButton_->Init([this]
			{
				if (!selectNode_)
					return;
				
				// 取得可能かどうか判定
				bool can_get = false;
				{
					auto* prev_node = selectNode_->GetPrevNode();
				
					if (!prev_node)
						can_get = true;
					else if (prev_node->GetIsGet())
						can_get = true;

					if (havePoint_ < selectNode->GetNeedPoint())
						can_get = false;
				}


				if (can_get)
				{
					selectNode_->SetIsGet(true);
					havePoint_ -= selectNode->GetNeedPoint();
					auto func = selectNode_->GetOnGet_();
					func();
				}			
			})
	}
};

今回簡略化のためにすべてのノードをコード上に記述しています。ここでは、あらかじめ決めておいたノード数分のリストに新たにノードを作成して保存、各々に対してSetNodeInfo関数を実行しています。

また、新人研修で作成していたボタンクラスはstd::functionを引数にできるInit関数が用意されており、このInit関数で設定した関数をボタン押下時に実行してくれます。

つまり、ノードを押した際に押されたノードを変数として持っておき取得ボタンを押した際に取得できるかどうかの判定を行い、そのノードのonGet_を実行すればいいのです。今回は簡略化して示しましたが、実際にはノード構造はパラメータファイル化して企画が設定できるようにすることになります。

 

補足:std::functionのメリット、デメリット

ここまで、std::functionを多用してきましたが、std::functionとは関数を設定できる変数です。多くの場合、関数の引数としてstd::functionを設定したうえでラムダ式を用いてその中身を記述します。


std::function<void()> func;		// 宣言はstd::function<返り値の型(引数の型)>

func = [](){ print("Hello world!"); };	// 関数の実装部分をラムダ式で記述
func();					// 実行

std::functionの大きな利点は最初に記述している通り、関数を変数として設定できる点です。ボタン押下時の処理はわかりやすい例です。ボタンクラスにstd::functionの変数を用意し、ボタン押下時にその関数を実行するようにすれば新たにボタンを作成したとしても、押下時の処理を記述するだけで済みます。

一方でデメリットとしては、引数や返り値が異なる関数は実行できない点です。先ほどの例ではstd::function<void()>としていましたが、例えば引数にint型を持ち、返り値がbool型の場合std::function<bool(int)>としなければなりません。そのため、ボタンクラスにstd::function<void()>の変数を用意した場合はその関数に引数を渡すことも、関数から返り値を受け取ることもできなくなります。

とはいえ、変数に関数を設定できるstd::functionはクラスの自由度を大幅に挙げているように感じます。事実、新人研修中のありとあらゆる箇所でstd::functionが利用されていたように思います。

std::functionさんには足を向けて寝れない気がします。

 

最後に

いかがでしたでしょうか。今回は簡単にではありますが、新人研修中に開発したスキルツリーのノード部分に焦点を当てて解説してみました。個人的にはこのノードの考え方はそのままstackやqueの考え方みたいで、ゲームの実装においてこの考え方を使うことがあるのかと感動していました。

今回の解説をそのまま運用できるかと言われるとおそらくそうではなく、実際にはテクスチャ情報は1枚では足りない、エフェクトやアニメーションも追加したい、1つ前のノードを複数用意したいなど諸々発展させないと運用は厳しいと思います。自分の望むようにカスタマイズして利用していただけたら幸いです。

最後まで読んでいただき、ありがとうございました。

著者紹介 F.S
2023年にトイロジックに新卒入社。現在は主にメニュー画面とツール作成を担当しています。

トイロジックでは現在、一緒に働くプログラマーを募集しています。

不明点などもお気軽にお問い合わせくださいフルリモート採用も行っております、ご応募お待ちしております!