1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Siv3Dでゲームを作る(Part1)

Last updated at Posted at 2024-07-07

「傘忘れちまった」の制作過程記事です

初心者向けのゲーム製作の解説記事にしようと思いましたが、以下の理由で断念しました。
・ゲーム制作はそもそも高難易度
・ソースコードが膨大すぎる
・文章を書く労力のすさまじさたるや
なるべく初心者にもわかるように頑張ったんだけどね……。

基礎的な内容を学びたいよ!という方は部会の講座とかSiv3DのチュートリアルとかC++の入門サイトとかを参考にしてください。この記事に比べれば100倍分かりやすいと思います。

また、この記事で制作しているゲーム「傘忘れちまった」の完成版は以下のサイトからダウンロードできます。
https://home.tcu-ctrl.jp/pastWorks/clx0qjva47035j3kuqfz2wp2b

この記事の読み進め方:
とりあえず散りばめられてるソースコードをコピペして実行してみてください。
ソースコードの内容をすべて理解する必要はありません。ちょくちょく分かる部分がある、という程度の理解で十分です。
吸収できそうな部分を吸収していきましょう。

第0章

手始めに登場人物の人と雨粒を用意します。
ソースコード:

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標
	Vec2 rainDropPos{ 400,200 };
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,300 };
	
	while (System::Update())
	{
		//雨粒の表示
		rainDropShape.setCenter(rainDropPos).draw(rainDropColor);
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

上記のプログラムを実行すると

すごい素朴ですね。
ここから完成形のイメージを膨らませていきましょう。

第1章 雨粒を落下させる

まず最初に雨粒の落下を実装しましょう。
雨粒が一定の速度で画面下方向に落ちていくのであれば、次のようなプログラムで実装できます。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標
	Vec2 rainDropPos{ 400,200 };
	//雨粒の落下速度
+	double rainDropSpeed = 50;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,300 };
	
	while (System::Update())
	{
        //雨粒の移動
+		rainDropPos.y += rainDropSpeed * Scene::DeltaTime();
		//雨粒の表示
		rainDropShape.setCenter(rainDropPos).draw(rainDropColor);
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

雨粒の落下速度としてrainDropSpeedを定義し、その数値を50(px)としました。
そして、while文の中では雨粒のy座標に1フレーム前からの移動距離を加算しています。
等速直線運動の式 $ y=\int Vdt $ のVがrainDropSpeedに、dtがScene::DeltaTime()に置き換わったと考えれば、ここで行っている処理は積分であると分かるのではないでしょうか。
頭に?が浮かんだ方は、速度×時間=距離の関係を思い出してみてください。
雨粒の速度はrainDropSpeed、1フレーム前からの経過時間はScene::DeltaTime()です。といことはrainDropSpeed×Scene::DeltaTime()は1フレーム前からの移動距離を表しているはずです。
それを毎フレーム加算していくので雨粒はrainDropSpeed(秒速50px)で移動します。

第2章 大量の雨粒を降らせる

雨粒を4粒に増やしてみます。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標
	Vec2 rainDropPos{ 100,200 };
+	Vec2 rainDropPos1{ 200,200 };
+	Vec2 rainDropPos2{ 300,200 };
+	Vec2 rainDropPos3{ 400,200 };
	//雨粒の落下速度
	double rainDropSpeed = 50;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,300 };
	
	while (System::Update())
	{
        //雨粒の移動
		rainDropPos.y += rainDropSpeed * Scene::DeltaTime();
+		rainDropPos1.y += rainDropSpeed * Scene::DeltaTime();
+		rainDropPos2.y += rainDropSpeed * Scene::DeltaTime();
+		rainDropPos3.y += rainDropSpeed * Scene::DeltaTime();
		//雨粒の表示
		rainDropShape.setCenter(rainDropPos).draw(rainDropColor);
+		rainDropShape.setCenter(rainDropPos1).draw(rainDropColor);
+		rainDropShape.setCenter(rainDropPos2).draw(rainDropColor);
+		rainDropShape.setCenter(rainDropPos3).draw(rainDropColor);
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

同じ要領で行を追加しました。
これで雨粒は4粒になりました、簡単ですね!
では、同じ要領で50粒に増やしましょう・・・・・・いや、冗談ですが。
よほど素直な人じゃないとこんな書き方はしないでしょう。
こういうときは「配列」というものを使います。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
+	//雨粒の座標の配列
+	Array<Vec2> rainDropPosArray;
+	//雨粒の座標をセット 50粒
+	for (int32 i = 0; i < 50; i++)
+	{
+       //配列の後ろに追加
+		rainDropPosArray << Vec2{ i * 10,0 };
+	}
	//雨粒の落下速度
	double rainDropSpeed = 50;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,300 };
	
	while (System::Update())
	{
+		for (int32 i = 0; i < rainDropPosArray.size(); i++)
+		{
+			//雨粒の移動
+			rainDropPosArray[i].y += rainDropSpeed * Scene::DeltaTime();
+			//雨粒の表示
+			rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
+		}
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

配列というのは複数の変数をまとめて管理するやつ、というイメージです。
Siv3DではArrayという動的配列が用意されているので、それを使います。
image.png
配列とfor文の合わせ技で大量の雨粒の座標に対する操作を簡潔に記述しています。
forってなんぞ?という方はぜひご自分で検索してみてください。
ざっくり言うとループ回数をiでカウントするwhile文をコンパクトにしたループの構文です。
image.png

"i++"って書いてあるやつは"i+=1"と同義です。
インクリメントっていう文法です。
ややこしくてごめんね。

第3章 雨らしくする

第2章までのプログラムを実行すると以下のようになります。

ここまでプログラムを書いても、悲しくなるくらい雨とは程遠いです。
膨大なプログラムによってゲームは成り立っているということが身に染みて分かりますね。
さぁ改善していきましょう。
手始めに、雨粒の生成タイミングをバラバラにして、生まれる位置を乱数で決めましょう。
ちなみに乱数っていうのはランダムな数字のことです。

いい感じですね。ソースコードは以下のように変えました。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標の配列
	Array<Vec2> rainDropPosArray;
+	//雨粒生成の時間間隔
+	double geneTimeInterval=0.4;
+	//生成タイマー
+	double geneTimer = 0;
	//雨粒の落下速度
	double rainDropSpeed = 50;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,300 };

	while (System::Update())
	{
+		//タイマー加算
+		geneTimer += Scene::DeltaTime();
+		//タイマーが生成インターバルをこえたら
+		if (geneTimer > geneTimeInterval)
+		{
+			//雨粒に新たな座標を追加 これで雨粒が増える
+			rainDropPosArray << Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
+			//タイマーを0にしてリセット
+			geneTimer = 0;
+		}

		for (int32 i = 0; i < rainDropPosArray.size(); i++)
		{
			//雨粒の移動
			rainDropPosArray[i].y += rainDropSpeed * Scene::DeltaTime();
			//雨粒の表示
			rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
		}
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

0.4秒に1度、雨粒に新たな座標を追加するようにしました。
追加される座標はRandomを使って、x座標が0からシーンの横幅、y座標が-50から-20になるようにしています。

第4章 プレイヤーを動かす

雨ばっか作っててもつまらないので、ここらでプレイヤーを操作できるようにしてみましょう。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標の配列
	Array<Vec2> rainDropPosArray;
	//雨粒生成の時間間隔
	double geneTimeInterval=0.4;
	//生成タイマー
	double geneTimer = 0;
	//雨粒の落下速度
	double rainDropSpeed = 50;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
+	Vec2 playerPos{ 400,500 };
+	//プレイヤーの動くスピード
+	double playerSpeed = 200;

	while (System::Update())
	{
		//タイマー加算
		geneTimer += Scene::DeltaTime();
		//タイマーが生成インターバルをこえたら
		if (geneTimer > geneTimeInterval)
		{
			//雨粒に新たな座標を追加 これで雨粒が増える
			rainDropPosArray << Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
			//タイマーを0にしてリセット
			geneTimer = 0;
		}
+		//プレイヤーをキー入力に応じて移動させる
+		if (KeyRight.pressed())
+		{
+			//右方向に移動
+			playerPos.x += playerSpeed * Scene::DeltaTime();
+		}
+		if (KeyLeft.pressed())
+		{
+			//左方向に移動
+			playerPos.x -= playerSpeed * Scene::DeltaTime();
+		}

		for (int32 i = 0; i < rainDropPosArray.size(); i++)
		{
			//雨粒の移動
			rainDropPosArray[i].y += rainDropSpeed * Scene::DeltaTime();
			//雨粒の表示
			rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
		}
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

→キーを押している間は右に、←キーを押している間は左に動くようプログラムしたのと、プレイヤーの初期位置を少し下に移動させてみました。
試しに雨粒を避けてみると楽しいと思います。

第5章 雨らしくする その2

すべての雨粒が同じ方向に、同じ速度で落ちていくのはなんか不自然ですね。
現実の雨粒がどのように落下しているのかは知りませんが、同方向同速度は少々機械的な気がします。第一、よけててつまらないです。
というわけで、雨粒に"方向"と"速度"という個性をつけていきます。
雨粒を真下に落下させているのは以下のプログラムです。

for (int32 i = 0; i < rainDropPosArray.size(); i++)
{
	//雨粒の移動
	rainDropPosArray[i].y += rainDropSpeed * Scene::DeltaTime();
	//雨粒の表示
	rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
}

rainDropPosArrayは雨粒の座標が並んだ配列です。
i番目の雨粒の座標のy成分に1フレームの移動分を加算しています。
座標のy成分だけが加算されて大きくなるので、雨粒は画面真下の方へ移動していくわけです。
雨粒それぞれの移動に個性をつける方法は一旦置いといて、まずは、真下にしか移動できないこの数式を改良しましょう。
以下のようにします。

for (int32 i = 0; i < rainDropPosArray.size(); i++)
{
	//雨粒の移動
    double rad=120_deg;
    Vec2 V = rainDropSpeed * Vec2{ Cos(rad),Sin(rad) };  //速度ベクトル
	rainDropPosArray[i] += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
	//雨粒の表示
	rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
}

120_degというのが出てきました。数字の後ろに"_deg"をつけると度数法から弧度法に変換されます(120_deg=120×$\frac{\pi}{180}$)。
雨粒の座標に対して2次元のベクトルを加算することでx,y両方の成分が変化していくので真下以外の方向にも移動できます。
今は方向を120_degとしているので、↙この方向に雨粒が移動していきます。角度と方向の対応は0°→、90°↓、180°←、270°↑となってます。

ちなみにお遊びですが、プログラムを下のように書き換えてみると面白い動きをしますよ。ぜひ試してみて、なんでそういう動きになるのか考えてみてね。

for (int32 i = 0; i < rainDropPosArray.size(); i++)
{
	//雨粒の移動
    double rad = Random(60_deg, 120_deg);
    Vec2 V = (rainDropSpeed + Random(100)) * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
    rainDropPosArray[i] += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
	//雨粒の表示
	rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
}

今やりたいのは、雨粒に固有の速度と方向を持たせること。
雨粒の座標は配列で管理してますから、それぞれの雨粒に対応した速度と方向を用意することを考えると、速度と方向も配列で管理する方法が思いつきます。

0番目の雨粒の座標は0番目の速度と方向を使って移動して、1番目の雨粒の座標は1番目の速度と方向を使って移動する...というように配列の番号で対応関係を作ります。
これで実装したのが以下のプログラムです。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
	/*	ここではゲームで使う変数などを宣言する */

	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨粒の座標の配列
	Array<Vec2> rainDropPosArray;
+	//雨粒の落ちていく方向(ラジアン)
+	Array<double> rainDropRads;
+	//雨粒の落ちてく方向の範囲
+	double radRange = 10_deg;
+	//雨粒の落ちていく速さ
+	Array<double> rainDropSpeeds;
+	//雨粒の速度の最小値
+	double rainDropMinSpeed=40;
+	//雨粒の速度の最大値
+	double rainDropMaxSpeed = 100;
	//雨粒生成の時間間隔
	double geneTimeInterval=0.4;
	//生成タイマー
	double geneTimer = 0;
	//プレイヤーの見た目
	Texture playerLooks{ U"🧍‍♂"_emoji };
	//プレイヤーの大きさ
	double playerScale = 0.7;
	//プレイヤーの座標
	Vec2 playerPos{ 400,500 };
	//プレイヤーの動くスピード
	double playerSpeed = 200;

	while (System::Update())
	{
		//タイマー加算
		geneTimer += Scene::DeltaTime();
		//タイマーが生成インターバルをこえたら
		if (geneTimer > geneTimeInterval)
		{
			//雨粒に新たな座標を追加 これで雨粒が増える
			rainDropPosArray << Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
+			//速度を追加
+			rainDropSpeeds << Random(rainDropMinSpeed, rainDropMaxSpeed);
+			//角度(ラジアン)追加
+			rainDropRads << 90_deg + Random(-radRange, radRange);
			//タイマーを0にしてリセット
			geneTimer = 0;
		}

		//プレイヤーをキー入力に応じて移動させる
		if (KeyRight.pressed())
		{
			//右方向に移動
			playerPos.x += playerSpeed * Scene::DeltaTime();
		}
		if (KeyLeft.pressed())
		{
			//左方向に移動
			playerPos.x -= playerSpeed * Scene::DeltaTime();
		}

		for (int32 i = 0; i < rainDropPosArray.size(); i++)
		{
			//雨粒の移動
+			double rad = rainDropRads[i];
+			Vec2 V = rainDropSpeeds[i] * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
+			rainDropPosArray[i] += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
			//雨粒の表示
			rainDropShape.setCenter(rainDropPosArray[i]).draw(rainDropColor);
		}
		//プレイヤーの表示
		playerLooks.scaled(playerScale).drawAt(playerPos);
	}
}

速度配列、方向配列を用意。
雨粒が追加されるときには速度配列、方向配列にも値を追加する。
雨粒を動かすときは座標配列、速度配列、方向配列のそれぞれ同じ番号のところを参照する。
これにより、雨粒に速度と方向という個性を持たせることができました。

第6章 「雨粒」を定義する

前章の内容はかなり機械的で、分かりずらかったのではないでしょうか?
雨粒それぞれに個性を持たせるために、座標速度方向の配列を用意しましたが、これは私たちの世界観にはあまり即していないでしょう。
雨粒が座標配列、速度配列、方向配列の中の自分に対応した要素で表現されている、そんな世界観私は許せません。雨粒一つ一つが座標、速度、方向などの要素を持っていて、雨はその雨粒の配列である、とするほうが現実世界に即している気がします。
図で説明すると以下の図1よりも図2のほうが直感的だと言いたいのです。

図1

図2

座標配列、速度配列、方向配列...という三つの配列ではなく座標、速度、方向を持った雨粒が並んだ配列にする、その方が高レベルなプログラムって感じがします。
ではどのように実装しましょう。まず雨粒を用意しなければ雨粒の配列は作れません。しかし残念なことにC++もSiv3Dも雨粒なんてものは用意していません。よって、雨粒配列を作るために、まずは雨粒というものを自分で作る必要があります。
何かしらの方法で雨粒を作って以下のような実装ができるようになることが、この章のゴールです。

# include <Siv3D.hpp> // Siv3D v0.6.14

void Main()
{
    /*省略*/

    //雨
    Array<アマツブ> rain;

    while(System::Update())
    {
        //タイマー加算
        geneTimer += Scene::DeltaTime();
        //タイマーが生成インターバルをこえたら
        if (geneTimer > geneTimeInterval)
        {
        	//雨粒を生成
            アマツブ rainDrop;
            
            /*ここでrainDropの座標、速度、方向などの設定を行うが、省略*/

            //生成した雨粒を配列(雨)に追加
            rain << rainDrop;
        	//タイマーを0にしてリセット
        	geneTimer = 0;
        }

        /*省略*/
    }
}

上のプログラム中の「アマツブ」のようなオリジナルな型は構造体というもので作ることができます。
構造体っていうのはひな形みたいなものです。
というわけで、雨粒もといRainDropという名前で構造体を作っていきましょう。


作り方、「struct 名前 {};」と書く。終わり!

# include <Siv3D.hpp> // Siv3D v0.6.14

//雨粒のひな型
struct RainDrop {};

void Main()
{
    /*省略*/
}

構造体はMain関数の外に書きます。
これでRainDrop(雨粒)というものがこの世界に定義されました。
しかし、構造体(struct)の"{}"の中に何も書いていないので、現時点ではRainDropは何も持たざるオブジェクトです。
上のほうで示した図2のように雨粒は少なくとも座標と速度と方向を持っています。
RainDropに座標、速度、方向を持たせるには次のようにします。

# include <Siv3D.hpp> // Siv3D v0.6.14

//雨粒のひな型
struct RainDrop
{
+	//座標
+	Vec2 pos;
+	//速度
+	double speed;
+	//角度
+	double rad;
};

void Main()
{
    /*省略*/
}

RainDropはVec2型のpos、double型のspeed、double型のradという変数を持つというような意味になります。
定義はこれで十分でしょう。ではこのRainDropを使ったプログラムを書いてみます。

# include <Siv3D.hpp> // Siv3D v0.6.14

+//雨粒のひな型
+struct RainDrop
+{
+	//座標
+	Vec2 pos;
+	//速度
+	double speed;
+	//角度
+	double rad;
+};

void Main()
{
    // 背景の色を設定する
    Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
    //雨粒の形
    Circle rainDropShape{ 0,0,5 };
    //雨粒の色
    ColorF rainDropColor{ 0.3,0.4,0.7 };
+   //雨
+   Array<RainDrop> rain;
    //雨粒の落ちてく方向の範囲
    double radRange = 10_deg;
    //雨粒の速度の最小値
    double rainDropMinSpeed=40;
    //雨粒の速度の最大値
    double rainDropMaxSpeed = 100;
    //雨粒生成の時間間隔
    double geneTimeInterval=0.4;
    //生成タイマー
    double geneTimer = 0;
    //プレイヤーの見た目
    Texture playerLooks{ U"🧍‍♂"_emoji };
    //プレイヤーの大きさ
    double playerScale = 0.7;
    //プレイヤーの座標
    Vec2 playerPos{ 400,500 };
    //プレイヤーの動くスピード
    double playerSpeed = 200;

    while(System::Update())
    {
        //タイマー加算
        geneTimer += Scene::DeltaTime();
        //タイマーが生成インターバルをこえたら
        if (geneTimer > geneTimeInterval)
        {
+        	//雨粒を生成
+           RainDrop rainDrop;
+           //座標をセット
+           rainDrop.pos = Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
+           //速度のセット
+           rainDrop.speed = Random(rainDropMinSpeed, rainDropMaxSpeed);
+           //方向のセット
+           rainDrop.rad = 90_deg + Random(-radRange, radRange);
            //生成した雨粒を配列(雨)に追加
            rain << rainDrop;
        	//タイマーを0にしてリセット
        	geneTimer = 0;
        }
    //プレイヤーをキー入力に応じて移動させる
    if (KeyRight.pressed())
    {
    	//右方向に移動
    	playerPos.x += playerSpeed * Scene::DeltaTime();
    }
    if (KeyLeft.pressed())
    {
    	//左方向に移動
    	playerPos.x -= playerSpeed * Scene::DeltaTime();
    }
    
    for (int32 i = 0; i < rain.size(); i++)
    {
+    	//雨粒の移動
+    	double rad = rain[i].rad;
+    	Vec2 V = rain[i].speed * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
+    	rain[i].pos += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
    	//雨粒の表示
    	rainDropShape.setCenter(rain[i].pos).draw(rainDropColor);
    }
    //プレイヤーの表示
    playerLooks.scaled(playerScale).drawAt(playerPos);
    }
}

座標、速度、方向を一つにまとめたRainDropという構造体を用いることで、プログラムが少しスッキリしました。
構造体は複数の変数を一つにまとめたオブジェクトを作る機能と考えてもらっても良いでしょう。
少しプログラムの補足をすると、"rainDrop.pos"のように".(ピリオド)"をつけるとその構造体が持つ変数を参照します。つまり、"rainDrop.pos"は"rainDrop"が持っている"pos"ということです。

第7章 「プレイヤー」を定義する

前節の内容を踏まえれば、プレイヤーも構造体にできるはずです。
プレイヤーに関する変数は、今のところ4つあります。
プレイヤーの見た目"playerLooks"、プレイヤーのスケール(大きさ)"playerScale"、プレイヤーの座標"playerPos"、プレイヤーの動くスピード"playerSpeed"、この4つを一つにまとめた"Player"という構造体を作ってみましょう。
つまり、以下のような構造体にすれば良いわけです。

struct Player
{
	//見た目
	Texture looks;
	//大きさ
	double scale;
	//座標
	Vec2 pos;
	//速度
	double speed;
};

今まで、変数名は「player+"何とか"」にしていましたが、このようにPlayer構造体の中に変数を定義してしまえばPlayerの変数であるということは自明なので、わざわざ変数名に"player"を付けたりはしません。
このPlayerを使ってプログラムを書き換えてみます。

# include <Siv3D.hpp> // Siv3D v0.6.14
//雨粒のひな型
struct RainDrop
{
	//座標
	Vec2 pos;
	//速度
	double speed;
	//角度
	double rad;
};
+//プレイヤーのひな型
+struct Player
+{
+	//見た目
+	Texture looks;
+	//大きさ
+	double scale;
+	//座標
+	Vec2 pos;
+	//速度
+	double speed;
+};

void Main()
{
	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨
	Array<RainDrop> rain;
	//雨粒の落ちてく方向の範囲
	double radRange = 10_deg;
	//雨粒の速度の最小値
	double rainDropMinSpeed = 40;
	//雨粒の速度の最大値
	double rainDropMaxSpeed = 100;
	//雨粒生成の時間間隔
	double geneTimeInterval = 0.4;
	//生成タイマー
	double geneTimer = 0;
+	//プレイヤーの構造体を使ってplayerを生成
+	Player player;
+	//見た目をセット
+	player.looks = Texture{ U"🧍‍♂"_emoji };
+	//大きさをセット
+	player.scale = 0.7;
+	//座標をセット
+	player.pos = Vec2{ 400,500 };
+	//画面内を動くスピードをセット
+	player.speed = 200;

	while (System::Update())
	{
		//タイマー加算
		geneTimer += Scene::DeltaTime();
		//タイマーが生成インターバルをこえたら
		if (geneTimer > geneTimeInterval)
		{
			//雨粒を生成
			RainDrop rainDrop;
			//座標をセット
			rainDrop.pos = Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
			//速度のセット
			rainDrop.speed = Random(rainDropMinSpeed, rainDropMaxSpeed);
			//方向のセット
			rainDrop.rad = 90_deg + Random(-radRange, radRange);
			//生成した雨粒を配列(雨)に追加
			rain << rainDrop;
			//タイマーを0にしてリセット
			geneTimer = 0;
		}
		//プレイヤーをキー入力に応じて移動させる
		if (KeyRight.pressed())
		{
+			//右方向に移動
+			player.pos.x += player.speed * Scene::DeltaTime();
		}
		if (KeyLeft.pressed())
		{
+			//左方向に移動
+			player.pos.x -= player.speed * Scene::DeltaTime();
		}

		for (int32 i = 0; i < rain.size(); i++)
		{
			//雨粒の移動
			double rad = rain[i].rad;
			Vec2 V = rain[i].speed * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
			rain[i].pos += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
			//雨粒の表示
			rainDropShape.setCenter(rain[i].pos).draw(rainDropColor);
		}
		//プレイヤーの表示
		player.looks.scaled(player.scale).drawAt(player.pos);
	}
}

さて、6章に引き続き、また構造体に書き換えるだけの章となってしまいました。
変数をまとめただけで実行結果は変わっていません。
「無意味なことさせやがってクソ野郎」と毒を吐きたくなる人もいるでしょう。
たしかに今のままでは構造体を使った旨味はそれほどありません。
しかし、関連したデータをまとめるという考え方はオブジェクト指向という、少し発展したプログラミングの基本的な考え方に繋がります。
オブジェクト指向はまだまだ先の概念ですので、ここで詳しく説明するということは避けますが、ようは構造体で置き換えたこのプログラムは少しイケた書き方ということです。
まぁ、少なくともplayerPosとかplayerSpeedみたいに、プレイヤーに関する変数が独立で存在しているより、Playerの中にposとspeedが存在しているという書き方のほうが好まれるのは何となく理解できるのではないでしょうか。

ちなみに

RainDrop rainDrop
Player player

上記のように「構造体名+変数名(スペルは構造体名と同じだが全部小文字)」というような宣言の仕方をしていますが、別に変数名の部分はなんでも構いません。
"Player satoshi"とか"Player takeshi"でもプログラムは何の問題もなく動きます。
あくまで私個人の意見ですが、自分好みの変数名を付けるようになることがプログラミング初心者脱却の第一歩だと思っています。
もし私の変数名が気に入らなければぜひ自分好みに改名してみてください。

(初心者あるあるなんですが、エラーが出たときに変数名を変えようとする人がいるんですよね...変数名はただのラベルなのでそこを変えてもエラーが治ることは基本的にはないのですが)

第8章 当たり判定を実装する

当たり判定を作る前に、雨粒がプレイヤーに当たったら何が起きるかを考えてみましょう。
雨粒がプレイヤーに当たると、その雨粒はゲーム画面から消えます。
ゲーム画面から消えるということは描画を行わないということです。
今、プログラムでは雨粒は配列"rain"の中に存在するデータです。ということはプレイヤーに当たった雨粒を"rain"の中から消してしまえば、その雨粒の存在が消え、描画もされなくなります。
配列の中から特定の要素を消す方法はいくつかありますが、今回はremove_atという命令を使ったやり方を紹介します。
下記のコードはremove_atを使った配列の要素の削除方法の簡単な例です。

//配列を作成 7つの数字をセットしておく
Array<int> arr{3,0,1,2,4,6,10};
//arr[2]の要素"1"を削除
arr.remove_at(2);

このremove_atを使って配列の中から条件を満たす要素を削除するには以下のようにします。

//配列を作成 7つの数字をセットしておく
Array<int> arr{3,0,1,2,4,6,10};
//配列の中から条件を満たす要素を消す
for(int32 i = 0; i < arr.size(); i++)
{
    if(要素を消す条件)
    {
        arr.remove_at(i);//要素を削除
        i--;//一つ戻る
    }
}

例えば3で割り切れる要素を削除する、という風にしたかったら"要素を消す条件"のところに"arr[i]%3==0"と入れます。なぜこれで条件を満たす要素を消せるのか、分からなければ先輩とか身近な頼れる人に聞いてみてください。
ここでは説明しません。
もう記事書くの疲れた...

――理解してなくてもゲームは作れるので先に進みましょう。
上記のやり方で、"プレイヤーに当たったか"というのを雨粒を消す条件にします。
あと、プレイヤーと雨粒で当たり判定を行うために、Player構造体にCircle型の変数hitCircleを持たせましょう。
以下がプレイヤーに当たった雨粒を消去するプログラムです。

# include <Siv3D.hpp> // Siv3D v0.6.14
//雨粒のひな型
struct RainDrop
{
	//座標
	Vec2 pos;
	//速度
	double speed;
	//角度
	double rad;
};
//プレイヤーのひな型
struct Player
{
	//見た目
	Texture looks;
	//大きさ
	double scale;
	//座標
	Vec2 pos;
	//速度
	double speed;
+	//当たり判定の円
+	Circle hitCircle;
};

void Main()
{
	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨
	Array<RainDrop> rain;
	
	//雨粒の落ちてく方向の範囲
	double radRange = 10_deg;
	//雨粒の速度の最小値
	double rainDropMinSpeed = 40;
	//雨粒の速度の最大値
	double rainDropMaxSpeed = 100;
	//雨粒生成の時間間隔
	double geneTimeInterval = 0.4;
	//生成タイマー
	double geneTimer = 0;
	//プレイヤーの構造体を使ってplayerを生成
	Player player;
	//見た目をセット
	player.looks = Texture{ U"🧍‍♂"_emoji };
	//大きさをセット
	player.scale = 0.7;
	//座標をセット
	player.pos = Vec2{ 400,500 };
	//画面内を動くスピードをセット
	player.speed = 200;
+	//当たり判定を設定 半径15px
+	player.hitCircle = Circle{ 15 };
	
    while (System::Update())
	{
		//タイマー加算
		geneTimer += Scene::DeltaTime();		
	
		//タイマーが生成インターバルをこえたら
		if (geneTimer > geneTimeInterval)
		{
			//雨粒を生成
			RainDrop rainDrop;
			//座標をセット
			rainDrop.pos = Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
			//速度のセット
			rainDrop.speed = Random(rainDropMinSpeed, rainDropMaxSpeed);
			//方向のセット
			rainDrop.rad = 90_deg + Random(-radRange, radRange);
			//生成した雨粒を配列(雨)に追加
			rain << rainDrop;
			//タイマーを0にしてリセット
			geneTimer = 0;
		}
		//プレイヤーをキー入力に応じて移動させる
		if (KeyRight.pressed())
		{
			//右方向に移動
			player.pos.x += player.speed * Scene::DeltaTime();
		}
		if (KeyLeft.pressed())
		{
			//左方向に移動
			player.pos.x -= player.speed * Scene::DeltaTime();
		}
+		//当たり判定の円の中心位置を更新。
+		player.hitCircle.center = player.pos;

		for (int32 i = 0; i < rain.size(); i++)
		{
			//雨粒の移動
			double rad = rain[i].rad;
			Vec2 V = rain[i].speed * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
			rain[i].pos += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
			//雨粒の表示
			rainDropShape.setCenter(rain[i].pos).draw(rainDropColor);
			
+           //プレイヤーに当たったら
+			if (rainDropShape.intersects(player.hitCircle))
+			{
+				rain.remove_at(i);//rain[i]を削除
+				i--;//一つ戻る
+			};
		}

		//プレイヤーの表示
		player.looks.scaled(player.scale).drawAt(player.pos);
+		//当たり判定の表示
+		player.hitCircle.draw(ColorF{0.9,0.6,0.6,0.5});
	}
}

第9章 ゲームオーバー条件、クリア条件

ゲームオーバーの条件は「一定以上濡れること」、クリア条件は「一定時間耐えること」とします。
次のような方法で実装します。
・Player構造体に濡れ度合いを持たせる。
・その変数がある値を超えたらゲームオーバーと表示
・ゲーム開始から経過した秒数を表す変数を作る
・その変数がある値を超えたらクリアと表示
文字を表示する方法はいくつかあります。一番お手軽なのはPrintを使う方法です。
Print(U"表示する文章");もしくはPrint<<U"表示する文章";で文字を表示することができます。

# include <Siv3D.hpp> // Siv3D v0.6.14
//雨粒のひな型
struct RainDrop
{
	//座標
	Vec2 pos;
	//速度
	double speed;
	//角度
	double rad;
};
//プレイヤーのひな型
struct Player
{
	//見た目
	Texture looks;
	//大きさ
	double scale;
	//座標
	Vec2 pos;
	//速度
	double speed;
	//濡れ度合
	double wet;
+	//当たり判定の円
+	Circle hitCircle;
};

void Main()
{
	// 背景の色を設定する
	Scene::SetBackground(ColorF{ 0.75,0.75,0.75 });
	//雨粒の形
	Circle rainDropShape{ 0,0,5 };
	//雨粒の色
	ColorF rainDropColor{ 0.3,0.4,0.7 };
	//雨
	Array<RainDrop> rain;
	
	//雨粒の落ちてく方向の範囲
	double radRange = 10_deg;
	//雨粒の速度の最小値
	double rainDropMinSpeed = 40;
	//雨粒の速度の最大値
	double rainDropMaxSpeed = 100;
	//雨粒生成の時間間隔
	double geneTimeInterval = 0.4;
	//生成タイマー
	double geneTimer = 0;
+	//クリア条件の時間 とりあえず20秒としておく
+	double timeForGameClear = 20;
+	//経過時間
+	double timer = 0;
+	//ゲームオーバー条件の濡れ度合いの値 とりあえず10
+	double wetnessThreshold=10;
+	//1回雨粒にあたるとプレイヤーの濡れ度合いが2.5上がる
+	double rainDropWetness = 2.5;
	//プレイヤーの構造体を使ってplayerを生成
	Player player;
	//見た目をセット
	player.looks = Texture{ U"🧍‍♂"_emoji };
	//大きさをセット
	player.scale = 0.7;
	//座標をセット
	player.pos = Vec2{ 400,500 };
	//画面内を動くスピードをセット
	player.speed = 200;
+	//濡れ度合いを初期化
+	player.wet = 0;
+	//当たり判定を設定 半径20
+	player.hitCircle = Circle{ 20 };

    while (System::Update())
	{		
+		//クリアの条件
+		if (timeForGameClear <= timer)
+		{
+			Print(U"Clear!");
+			//以降の処理を行わず、ループの先頭へ戻る
+			continue;
+		}
+		//ゲームオーバーの条件
+		if (wetnessThreshold <= player.wet)
+		{
+			Print(U"GameOver");
+			//以降の処理を行わず、ループの先頭へ戻る
+			continue;
+		}
		//タイマー加算
		geneTimer += Scene::DeltaTime();		
+		//経過時間を加算
+		timer += Scene::DeltaTime();
		
		//タイマーが生成インターバルをこえたら
		if (geneTimer > geneTimeInterval)
		{
			//雨粒を生成
			RainDrop rainDrop;
			//座標をセット
			rainDrop.pos = Vec2{ Random(0,Scene::Width()),Random(-50,-20) };
			//速度のセット
			rainDrop.speed = Random(rainDropMinSpeed, rainDropMaxSpeed);
			//方向のセット
			rainDrop.rad = 90_deg + Random(-radRange, radRange);
			//生成した雨粒を配列(雨)に追加
			rain << rainDrop;
			//タイマーを0にしてリセット
			geneTimer = 0;
		}
		//プレイヤーをキー入力に応じて移動させる
		if (KeyRight.pressed())
		{
			//右方向に移動
			player.pos.x += player.speed * Scene::DeltaTime();
		}
		if (KeyLeft.pressed())
		{
			//左方向に移動
			player.pos.x -= player.speed * Scene::DeltaTime();
		}
		//当たり判定の円の中心位置を更新。
		player.hitCircle.center = player.pos;

		for (int32 i = 0; i < rain.size(); i++)
		{
			//雨粒の移動
			double rad = rain[i].rad;
			Vec2 V = rain[i].speed * Vec2{ Cos(rad),Sin(rad) }; //速度ベクトル
			rain[i].pos += V * Scene::DeltaTime();       //速度ベクトル×dtを加算
			//雨粒の表示
			rainDropShape.setCenter(rain[i].pos).draw(rainDropColor);
			//プレイヤーに当たってたら
			if (rainDropShape.intersects(player.hitCircle))
			{
+				player.wet += rainDropWetness;//プレイヤーのwetに加算
				rain.remove_at(i);//rain[i]を削除
				i--;//一個下がる
			};
		}

		//プレイヤーの表示
		player.looks.scaled(player.scale).drawAt(player.pos);
		//当たり判定の表示
		player.hitCircle.draw(ColorF{ 1.0,0.4,0.4,0.9 });
	}
}

あとはパラメータを調整すればちゃんと遊べるになります。

これから追加していく要素について

これからゲームに追加していく要素を簡単にまとめておきます。
天候の変化
あるときは豪雨に、あるときは小雨に。
プレイヤー2
二人目のプレイヤー
コンピュータ
一人プレイの場合、プレイヤー2をコンピュータにする。
ジャンプ
ジャンプは大抵のゲームに用意されてますね。

傘を持つプレイヤーは濡れない。
略奪
傘を相手から奪えるモード。
略奪なしのモードも作る。
シーン
シーンを分ける。タイトルシーン・ゲームシーン。


とまぁ、追加したい要素が色々あるわけですが、1つの記事が長すぎてもよくないと思うので、続きは別の記事にまとめようと思います(いつかきっと)。

というわけで、ここまで読んでいただきありがとうございました。

1
1
0

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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?