クリスマスっぽいゲーム作ってみた

# はじめに
**Advent Calendar初参戦** 兼 **Qiita初投稿** 兼 **プログラミング関連の公開記事初投稿**です！
どっきどきの初投稿記事が自作ゲームの紹介記事です（笑）
どうか温かい目で見てください……

# 作ったゲーム
この記事が投稿された今日(12月22日)はクリスマスに近いので、クリスマスらしいゲームを作ってみました！
クリスマスといえば、サンタクロース… プレゼント… 子供にばれないように~~親~~サンタが渡す…

ということなので、**サンタが子供にばれないようにプレゼントを渡すゲーム**を作りました！（ゲーム名は「サンタさんのおしごと」です。）

https://twitter.com/takara2314/status/1473306980077150208

しっかりと子供の近くにプレゼントを設置できたら成功、プレゼントを設置する前に子供に見つかってしまったら失敗になります。
また、ブロックの設置やカメラの変更が可能なので、個性的なコースでプレゼントをお届けすることもできます。

# 遊んでみる
このリンク先からダウンロードし、インストールしてください。

https://github.com/takara2314/santa-game/releases/tag/v1.0.0

# 開発環境
- OpenSiv3D v0.6.3
- C++20
- Visual Studio 2022
- Windows 10

# 実装
お急ぎの方はGitHubリポジトリを見た方が早いかもしれません。

https://github.com/takara2314/santa-game

C++の **OpenSiv3D** という、絵や音を使ったインタラクティブなアプリを簡単に作れる、フレームワークを使いました。
サンタや子供、ワールド等はクラス化しており、処理は極力それぞれのクラスのメソッドに書くようにしました。ソースファイル分けも積極的に行っています。
（なんかその方がかっこ良く見えるからです（笑））

## 主要なプログラム全部
力を入れた、サンタ(Santa)と子供(Child)のクラスのソースファイル全部を掲載します。

<details><summary>Humanクラス（SantaとChildの親クラス）</summary><div>
[Human.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/main/santa-game/Human.cpp)

```C++
#include "Common.hpp"
#include "Human.hpp"


///////////////////////////////////
//  コンストラクタ
///////////////////////////////////
Human::Human() {}


///////////////////////////////////
//  座標を受け取る
///////////////////////////////////
Vec3 Human::get_position()
{
	return position;
}


// 小数部分だけ取り出す
double Human::pick_decimal(double num)
{
	return num - static_cast<int>(num);
}



///////////////////////////////////
//  向きを変更
///////////////////////////////////
void Human::change_angle(int from, int to)
{
	if (from == 1 && to == 2 && direction == 0)
	{
		direction = 1;
	}
	else if (from == 1 && to == 2 && direction == 1)
	{
		direction = 3;
	}
	else if (from == 1 && to == 2 && direction == 2)
	{
		direction = 0;
	}
	else if (from == 1 && to == 2 && direction == 3)
	{
		direction = 2;
	}
	else if (from == 2 && to == 1 && direction == 0)
	{
		direction = 2;
	}
	else if (from == 2 && to == 1 && direction == 1)
	{
		direction = 0;
	}
	else if (from == 2 && to == 1 && direction == 2)
	{
		direction = 3;
	}
	else if (from == 2 && to == 1 && direction == 3)
	{
		direction = 1;
	}
}


///////////////////////////////////
//  描画処理
///////////////////////////////////
void Human::draw(int angle)
{
	ScopedRenderStates2D renderState(SamplerState::ClampNearest);

	// 描画座標
	Vec2 draw_pos{};

	switch (angle)
	{
	case 1:
		draw_pos = Vec2{ position.x, MAX_Y - position.y };
		break;
	case 2:
		draw_pos = Vec2{ position.z, MAX_Y - position.y };
		break;
	}

	// 両足の状態
	size_t walk_progress;
	if ((direction == 0 || direction == 3) && angle == 1)
	{
		walk_progress = static_cast<size_t>((position.z - static_cast<int>(position.z)) * 3);
	}
	else if ((direction == 1 || direction == 2) && angle == 1)
	{
		walk_progress = static_cast<size_t>((position.x - static_cast<int>(position.x)) * 3);
	}
	else if ((direction == 0 || direction == 3) && angle == 2)
	{
		walk_progress = static_cast<size_t>((position.x - static_cast<int>(position.x)) * 3);
	}
	else
	{
		walk_progress = static_cast<size_t>((position.z - static_cast<int>(position.z)) * 3);
	}

	// 人間を描画
	skin(
		Vec2{ 32 * walk_progress, 32 * direction },
		32,
		32
	)
		.scaled(2.5)
		.drawAt(
			(draw_pos * ONE_PIXEL)
				.movedBy(Vec2{ ONE_PIXEL / 2, -40 })
		);
}
```
</div></details>

<details><summary>Santaクラス（Humanの子クラス）</summary><div>
[Santa.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/main/santa-game/Santa.cpp)

```C++
#include "Common.hpp"
#include "Santa.hpp"
#include "Human.hpp"


///////////////////////////////////
//  コンストラクタ
///////////////////////////////////
Santa::Santa(Texture skin, double y_acceleration, vector<Item> items)
{
	Human::skin = skin;
	Human::position = Vec3{ 0.0, 3.0, 1.0 };
	m_y_acceleration = y_acceleration;
	m_items = items;

	m_inventory[0] = {
		m_items[5],
		1
	};

	m_inventory[1] = {
		m_items[1],
		16
	};
}


///////////////////////////////////
//  移動処理
///////////////////////////////////
void Santa::move(int direction, double quantity, Collision collisions, WorldData world_data, Audio& init_walk_sound, int angle)
{
	Vec3 position = Human::position;

	// 向きを更新
	Human::direction = direction;

	// 移動量をフレーム経過依存に
	quantity *= Scene::DeltaTime();

	// アングル1の時の位置操作
	if (angle == 1)
	{
		switch (Human::direction)
		{
		case 0:
			position.z -= quantity;
			break;
		case 1:
			position.x -= quantity;
			break;
		case 2:
			position.x += quantity;
			break;
		case 3:
			position.z += quantity;
			break;
		}
	}

	// アングル2の時の位置操作
	if (angle == 2)
	{
		switch (Human::direction)
		{
		case 0:
			position.x += quantity;
			break;
		case 1:
			position.z -= quantity;
			break;
		case 2:
			position.z += quantity;
			break;
		case 3:
			position.x -= quantity;
			break;
		}
	}

	// 不の方向に行かないようにする
	position.x = Max(position.x, 0.0);
	position.y = Max(position.y, 0.0);
	position.z = Max(position.z, 0.0);

	// 限界座標-1より大きい移動であればキャンセル
	if (position.x > MAX_X - 1 || position.y > MAX_Y - 1 || position.z > MAX_Z - 1)
	{
		return;
	}

	size_t x = static_cast<size_t>(position.x + 0.5);
	size_t y = static_cast<size_t>(position.y);
	size_t z = static_cast<size_t>(position.z + 0.5);

	// yの小数部分が0.9~1.0ならインクリメントして考える
	if (Human::pick_decimal(position.y) >= 0.9 && Human::pick_decimal(position.y) < 1.0)
	{
		y++;
	}

	// 足元か頭上に当たり判定があればキャンセル
	if (angle == 1)
	{
		if (Human::direction == 1)
		{
			double temp = position.x + 0.5 - Human::width / 2;
			x = static_cast<size_t>(temp < 0 ? 0 : temp);
		}
		else if (Human::direction == 2)
		{
			x = Min(static_cast<size_t>(position.x + 0.5 + Human::width / 2), static_cast<size_t>(MAX_X - 1));
		}
	}
	else
	{
		if (Human::direction == 1)
		{
			double temp = position.z + 0.5 - Human::width / 2;
			z = static_cast<size_t>(temp < 0 ? 0 : temp);
		}
		else if (Human::direction == 2)
		{
			z = Min(static_cast<size_t>(position.z + 0.5 + Human::width / 2), static_cast<size_t>(MAX_Z - 1));
		}
	}


	bool collision_underfoot = collisions[x][y][z];

	size_t y_overhead = Min(static_cast<size_t>(position.y + Human::height), static_cast<size_t>(MAX_Y - 1));
	
	bool collision_overhead = collisions[x][y_overhead][z];

	if (collision_underfoot || collision_overhead)
	{
		return;
	}

	// 歩行音
	if (y > 0 && world_data[x][y - 1][z].name != U"空気")
	{
		world_data[x][y - 1][z].walk_sound.play();
	}
	else
	{
		init_walk_sound.play();
	}

	// 適応
	Human::position = position;
}


///////////////////////////////////
//  ジャンプ
///////////////////////////////////
void Santa::jump(Collision collisions)
{
	Vec3 position = Human::position;

	if (m_is_ground && !m_is_jump)
	{
		m_is_jump = true;
		m_is_ground = false;
		m_y_speed = jump_speed * 0.05;
		position.y += m_y_speed;
	}

	// 高度調整
	position.y = Min(position.y, static_cast<double>(MAX_Y - 1));
	position.y = Max(position.y, 0.0);

	// 適応
	Human::position = position;
}


///////////////////////////////////
//  y座標位置変更
///////////////////////////////////
void Santa::move_y(Collision collisions)
{
	Vec3 position = Human::position;

	// ジャンプ中
	if (m_is_jump)
	{
		m_y_speed -= Scene::DeltaTime() * m_y_acceleration * 0.05;

		if (m_y_speed <= 0)
		{
			m_is_jump = false;
		}

		// <身長>ブロック上にブロックがあればキャンセル
		size_t x = static_cast<size_t>(position.x + 0.5);
		size_t y = Min(static_cast<size_t>(position.y + Human::height), static_cast<size_t>(MAX_Y - 1));
		size_t z = static_cast<size_t>(position.z + 0.5);

		bool collision = collisions[x][y][z];

		if (collision)
		{
			m_is_jump = false;
			m_y_speed = 0.0;
		}

		position.y += m_y_speed;
	}

	// ジャンプ落下中
	if (!m_is_jump && !m_is_ground)
	{
		m_y_speed -= Scene::DeltaTime() * m_y_acceleration * 0.05;
		position.y += m_y_speed;
	}

	// 落下して地面についた
	else if (!m_is_jump)
	{
		m_y_speed = 0.0;
		if (Human::pick_decimal(position.y) >= 0.5)
		{
			position.y = static_cast<int>(position.y) + 1.0;
		}
	}

	// 高度調整
	position.y = Min(position.y, static_cast<double>(MAX_Y) - 1);
	position.y = Max(position.y, 0.0);

	// 適応
	Human::position = position;
}


///////////////////////////////////
//  接地判定
///////////////////////////////////
void Santa::check_ground(Collision collisions)
{
	// ジャンプ中は非対象
	if (m_is_jump)
	{
		return;
	}

	// 0は絶対ある
	if (position.y == 0)
	{
		m_is_ground = true;
		return;
	}

	// その位置に当たり判定があれば
	bool collision = collisions
		[static_cast<size_t>(position.x + 0.5)]
		[static_cast<size_t>(position.y)]
		[static_cast<size_t>(position.z + 0.5)];

	if (collision)
	{
		m_is_ground = true;
		return;
	}

	m_is_ground = false;
}


///////////////////////////////////
//  インベントリを取得
///////////////////////////////////
Inventory Santa::get_inventory()
{
	return m_inventory;
}


///////////////////////////////////
//   アイテムを与える
///////////////////////////////////
void Santa::give_item(int selection, Item& item, int quantity)
{
	Slot slot = m_inventory[selection];

	struct LocalFunc
	{
		static bool insert_slot(Inventory& inventory, Item& item, int quantity, int selection)
		{
			// そのスロットに何もない場合はそこに入れる
			if (inventory[selection].item.name == U"空気")
			{
				inventory[selection].item = item;
				inventory[selection].quantity = quantity;
				return true;
			}

			// アイテムが一致した場合は加算
			if (inventory[selection].item.name == item.name)
			{
				inventory[selection].quantity += quantity;
				return true;
			}

			return false;
		}
	};

	// 選択中のスロットに入れれるか
	if (LocalFunc::insert_slot(m_inventory, item, quantity, selection))
	{
		return;
	}

	// 他のスロットに入れれれば入れる
	for (int i = 0; i < m_inventory.size(); ++i)
	{
		if (LocalFunc::insert_slot(m_inventory, item, quantity, i))
		{
			return;
		}
	}

	// もうどうしようもない
	return;
}


///////////////////////////////////
//  アイテムを削除する
///////////////////////////////////
void Santa::remove_item(int selection, int quantity)
{
	Slot slot = m_inventory[selection];

	// もう消せそうな数のとき
	if (slot.quantity - quantity <= 0)
	{
		m_inventory[selection].item = Item{};
		m_inventory[selection].quantity = 0;
		return;
	}

	m_inventory[selection].quantity -= quantity;
}
```
</div></details>

<details><summary>Childクラス（Humanの子クラス）</summary><div>
[Child.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/main/santa-game/Child.cpp)

```C++
#include "Common.hpp"
#include "Child.hpp"
#include "Human.hpp"


///////////////////////////////////
//  コンストラクタ
///////////////////////////////////
Child::Child(Texture skin)
{
	Human::skin = skin;
	Human::position = Vec3{ 4.0, 4.0, 1.0 };
}


///////////////////////////////////
//  ランダムで視点変更
///////////////////////////////////
void Child::change_direction_random()
{
	for (;;)
	{
		int direction = Random(3);
		if (Human::direction != direction)
		{
			Human::direction = direction;
			break;
		}
	}
};


///////////////////////////////////
//  描画
///////////////////////////////////
void Child::draw(int angle, Vec3 player_position)
{
	ScopedRenderStates2D renderState(SamplerState::ClampNearest);

	// 描画座標
	Vec2 draw_pos{};
	// 透明度
	double opacity = 0.0;

	switch (angle)
	{
	case 1:
		draw_pos = Vec2{ Human::position.x, MAX_Y - Human::position.y };
		if (player_position.z <= Human::position.z)
		{
			opacity = Max(1.0 - (Human::position.z - player_position.z) * 0.3, 0.0);
		}
		break;
	case 2:
		draw_pos = Vec2{ Human::position.z, MAX_Y - Human::position.y };
		if (player_position.x >= Human::position.x)
		{
			opacity = Max(1.0 - (player_position.x - Human::position.x) * 0.3, 0.0);
		}
		break;
	}

	// 人間を描画
	skin(
		Vec2{ 32, 32 * direction },
		32,
		32
	)
		.scaled(2.5)
		.drawAt(
			(draw_pos * ONE_PIXEL)
				.movedBy(Vec2{ ONE_PIXEL / 2, -40 }),
			ColorF(1.0, opacity)
		);
}


///////////////////////////////////
//  障害物を検知
///////////////////////////////////
bool Child::m_detect_collison(int i, int axis, Collision collisions)
{
	int y = static_cast<size_t>(Human::position.y);
	i = Max(0, i);
	i = axis == 0 ? Min(i, MAX_X - 1) : Min(i, MAX_Z - 1);

	switch (axis)
	{
	case 0:
		if (collisions[i][y][static_cast<size_t>(Human::position.z + 0.5)])
		{
			return true;
		}
		break;

	case 1:
		if (collisions[static_cast<size_t>(Human::position.x + 0.5)][y][i])
		{
			return true;
		}
		break;
	}

	return false;
}


///////////////////////////////////
//  サンタ検知
///////////////////////////////////
bool Child::detect_santa(Vec3 player_position, Collision collisions)
{
	struct LocalFunc
	{
		static bool detect_collison(int i, int axis, Vec3 child_position, Collision collisions)
		{
			int y = static_cast<size_t>(child_position.y);
			i = Max(0, i);
			i = axis == 0 ? Min(i, MAX_X - 1) : Min(i, MAX_Z - 1);

			switch (axis)
			{
			case 0:
				if (collisions[i][y][static_cast<size_t>(child_position.z + 0.5)])
				{
					return true;
				}
				break;

			case 1:
				if (collisions[static_cast<size_t>(child_position.x + 0.5)][y][i])
				{
					return true;
				}
				break;
			}

			return false;
		}

		static bool detected_event(Vec3 child_position, Vec3 player_position, double detect_height, int i, int axis)
		{
			for (int j = child_position.y - detect_height; j < child_position.y + detect_height; ++j)
			{
				j = Max(0, j);
				j = Min(j, MAX_Y - 1);
				i = Max(0, i);
				i = axis == 0 ? Min(i, MAX_X - 1) : Min(i, MAX_Z - 1);

				size_t x = static_cast<size_t>(player_position.x + 0.5);
				size_t y = static_cast<size_t>(player_position.y);
				size_t z = static_cast<size_t>(player_position.z + 0.5);

				size_t x_child = static_cast<size_t>(child_position.x + 0.5);
				size_t z_child = static_cast<size_t>(child_position.z + 0.5);

				switch (axis)
				{
				case 0:
					if (x == i && y == j && z == z_child)
					{
						return true;
					}
					break;

				case 1:
					if (z == i && y == j && x == x_child)
					{
						return true;
					}
					break;
				}
			}
			return false;
		}
	};

	switch (Human::direction)
	{
	case 0:
		for (int i = Human::position.z - 1; i > Human::position.z - m_detect_length_santa; --i)
		{
			if (LocalFunc::detected_event(Human::position, player_position, m_detect_height, i, 1))
			{
				return true;
			}
			if (m_detect_collison(i, 0, collisions))
			{
				break;
			}
		}
		break;

	case 1:
		for (int i = Human::position.x - 1; i > Human::position.x - m_detect_length_santa; --i)
		{
			if (LocalFunc::detected_event(Human::position, player_position, m_detect_height, i, 0))
			{
				return true;
			}
			if (m_detect_collison(i, 1, collisions))
			{
				break;
			}
		}
		break;

	case 2:
		for (int i = Human::position.x + 1; i < Human::position.x + m_detect_length_santa; ++i)
		{
			if (LocalFunc::detected_event(Human::position, player_position, m_detect_height, i, 0))
			{
				return true;
			}
			if (LocalFunc::detect_collison(i, 1, Human::position, collisions))
			{
				break;
			}
		}
		break;

	case 3:
		for (int i = Human::position.z + 1; i < Human::position.z + m_detect_length_santa; ++i)
		{
			if (LocalFunc::detected_event(Human::position, player_position, m_detect_height, i, 1))
			{
				return true;
			}
			if (LocalFunc::detect_collison(i, 0, Human::position, collisions))
			{
				break;
			}
		}
		break;
	}

	return false;
}


///////////////////////////////////
//  プレゼント検知
///////////////////////////////////
bool Child::detect_present(Collision collisions, WorldData world_data)
{
	struct LocalFunc
	{
		static bool detected_event(Vec3 child_position, WorldData world_data, double detect_height, int i, int axis)
		{
			for (int j = child_position.y - detect_height; j < child_position.y + detect_height; ++j)
			{
				j = Max(0, j);
				j = Min(j, MAX_Y - 1);
				i = Max(0, i);
				i = axis == 0 ? Min(i, MAX_X - 1) : Min(i, MAX_Z - 1);

				size_t x_child = static_cast<size_t>(child_position.x + 0.5);
				size_t z_child = static_cast<size_t>(child_position.z + 0.5);

				switch (axis)
				{
				case 0:
					if (world_data[i][j][z_child].name == U"プレゼントボックス")
					{
						return true;
					}
					break;

				case 1:
					if (world_data[x_child][j][i].name == U"プレゼントボックス")
					{
						return true;
					}
					break;
				}
			}
			return false;
		}
	};

	switch (Human::direction)
	{
	case 0:
		for (int i = Human::position.z - 1; i > Human::position.z - m_detect_length_present; --i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 1))
			{
				return true;
			}
			if (m_detect_collison(i, 0, collisions))
			{
				break;
			}
		}
		break;

	case 1:
		for (int i = Human::position.x - 1; i > Human::position.x - m_detect_length_present; --i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 0))
			{
				return true;
			}
			if (m_detect_collison(i, 1, collisions))
			{
				break;
			}
		}
		break;

	case 2:
		for (int i = Human::position.x + 1; i < Human::position.x + m_detect_length_present; ++i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 0))
			{
				return true;
			}
			if (m_detect_collison(i, 1, collisions))
			{
				break;
			}
		}
		break;

	case 3:
		for (int i = Human::position.z + 1; i < Human::position.z + m_detect_length_present; ++i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 1))
			{
				return true;
			}
			if (m_detect_collison(i, 0, collisions))
			{
				break;
			}
		}
		break;
	}

	return false;
}
```
</div></details>

## 工夫したところ・手間がかかったところ
### 移動をリアルっぽくするところ
向き変更メソッドで、カメラ変更したときに、現在の視点情報が次のカメラでも引き継げるようにしています。

描画メソッドでは、座標の小数部分によって両足の状態が変わるようになっています。
これによって、歩行時に歩いて見えるように描画されます。

[Human.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/dcf2d3a49b5fe472bb45c32979fce1e81d800616/santa-game/Human.cpp#L68-L118)

```c++
///////////////////////////////////
//  描画処理
///////////////////////////////////
void Human::draw(int angle)
{
	ScopedRenderStates2D renderState(SamplerState::ClampNearest);

	// 描画座標
	Vec2 draw_pos{};

	switch (angle)
	{
	case 1:
		draw_pos = Vec2{ position.x, MAX_Y - position.y };
		break;
	case 2:
		draw_pos = Vec2{ position.z, MAX_Y - position.y };
		break;
	}

	// 両足の状態
	size_t walk_progress;
	if ((direction == 0 || direction == 3) && angle == 1)
	{
		walk_progress = static_cast<size_t>((position.z - static_cast<int>(position.z)) * 3);
	}
	else if ((direction == 1 || direction == 2) && angle == 1)
	{
		walk_progress = static_cast<size_t>((position.x - static_cast<int>(position.x)) * 3);
	}
	else if ((direction == 0 || direction == 3) && angle == 2)
	{
		walk_progress = static_cast<size_t>((position.x - static_cast<int>(position.x)) * 3);
	}
	else
	{
		walk_progress = static_cast<size_t>((position.z - static_cast<int>(position.z)) * 3);
	}

	// 人間を描画
	skin(
		Vec2{ 32 * walk_progress, 32 * direction },
		32,
		32
	)
		.scaled(2.5)
		.drawAt(
			(draw_pos * ONE_PIXEL)
				.movedBy(Vec2{ ONE_PIXEL / 2, -40 })
		);
}
```

### サンタのジャンプ・落下
サンタの頭上に障害物がないことを確認して、**鉛直投げ上げ運動**を実行します。
高校物理を勉強した方ならわかると思いますが、図のような感じのやつです。

![鉛直投げ上げ運動](https://i0.wp.com/studyphys.com/wp-content/uploads/2019/02/tiny_mech_ver-throw-up_p1.png?w=986&ssl=1)
（引用:　https://studyphys.com/ver-throw-up/ ）

鉛直投げ上げ運動を行ったときの、高さや速さは計算で求めることができます。
このゲームでは、一定の高さになるまで、微小な高さ変動を速さが0になるまで毎フレーム繰り返しています。
微小な変動を毎フレーム（数ミリ秒ごとに実行）… **微分積分**
よって、毎フレームごとに、鉛直投げ上げ運動の高さの公式を微分したもの（速さ）を高さに加算していって、ジャンプ・落下を実装しています。

$$ \frac{d(-\frac{1}{2}gt^2+v_0t)}{dt} = -gt+v_0 $$

また、落下は接地でないと判定されたときに実行されます。
接地の判定条件は、下に当たり判定がないことです。

書いていて思ったのですが、物理空間を定義するための、`P2World`クラスを使えば、もう少し簡単に実装できたかもしれませんね。

[Santa.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/dcf2d3a49b5fe472bb45c32979fce1e81d800616/santa-game/Santa.cpp#L155-L235)

```C++
///////////////////////////////////
//  ジャンプ
///////////////////////////////////
void Santa::jump(Collision collisions)
{
	Vec3 position = Human::position;

	if (m_is_ground && !m_is_jump)
	{
		m_is_jump = true;
		m_is_ground = false;
		m_y_speed = jump_speed * 0.05;
		position.y += m_y_speed;
	}

	// 高度調整
	position.y = Min(position.y, static_cast<double>(MAX_Y - 1));
	position.y = Max(position.y, 0.0);

	// 適応
	Human::position = position;
}


///////////////////////////////////
//  y座標位置変更
///////////////////////////////////
void Santa::move_y(Collision collisions)
{
	Vec3 position = Human::position;

	// ジャンプ中
	if (m_is_jump)
	{
		m_y_speed -= Scene::DeltaTime() * m_y_acceleration * 0.05;

		if (m_y_speed <= 0)
		{
			m_is_jump = false;
		}

		// <身長>ブロック上にブロックがあればキャンセル
		size_t x = static_cast<size_t>(position.x + 0.5);
		size_t y = Min(static_cast<size_t>(position.y + Human::height), static_cast<size_t>(MAX_Y - 1));
		size_t z = static_cast<size_t>(position.z + 0.5);

		bool collision = collisions[x][y][z];

		if (collision)
		{
			m_is_jump = false;
			m_y_speed = 0.0;
		}

		position.y += m_y_speed;
	}

	// ジャンプ落下中
	if (!m_is_jump && !m_is_ground)
	{
		m_y_speed -= Scene::DeltaTime() * m_y_acceleration * 0.05;
		position.y += m_y_speed;
	}

	// 落下して地面についた
	else if (!m_is_jump)
	{
		m_y_speed = 0.0;
		if (Human::pick_decimal(position.y) >= 0.5)
		{
			position.y = static_cast<int>(position.y) + 1.0;
		}
	}

	// 高度調整
	position.y = Min(position.y, static_cast<double>(MAX_Y) - 1);
	position.y = Max(position.y, 0.0);

	// 適応
	Human::position = position;
}
```

### プレゼントの検知
視点チェックメソッドでは、以下の画像のように目先の一定距離+一定の高さに目標とするものがあるかどうかを確かめます。
![視点チェック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613648/f4c24ebf-291f-f886-2553-69795b41c843.png)

例えば目先に障害物（当たり判定）がある場合、この先のブロックのサーチを中断します。

[Child.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/dcf2d3a49b5fe472bb45c32979fce1e81d800616/santa-game/Child.cpp#L241-L341)

```C++
///////////////////////////////////
//  プレゼント検知
///////////////////////////////////
bool Child::detect_present(Collision collisions, WorldData world_data)
{
	struct LocalFunc
	{
		static bool detected_event(Vec3 child_position, WorldData world_data, double detect_height, int i, int axis)
		{
			for (int j = child_position.y - detect_height; j < child_position.y + detect_height; ++j)
			{
				j = Max(0, j);
				j = Min(j, MAX_Y - 1);
				i = Max(0, i);
				i = axis == 0 ? Min(i, MAX_X - 1) : Min(i, MAX_Z - 1);

				size_t x_child = static_cast<size_t>(child_position.x + 0.5);
				size_t z_child = static_cast<size_t>(child_position.z + 0.5);

				switch (axis)
				{
				case 0:
					if (world_data[i][j][z_child].name == U"プレゼントボックス")
					{
						return true;
					}
					break;

				case 1:
					if (world_data[x_child][j][i].name == U"プレゼントボックス")
					{
						return true;
					}
					break;
				}
			}
			return false;
		}
	};

	switch (Human::direction)
	{
	case 0:
		for (int i = Human::position.z - 1; i > Human::position.z - m_detect_length_present; --i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 1))
			{
				return true;
			}
			if (m_detect_collison(i, 0, collisions))
			{
				break;
			}
		}
		break;

	case 1:
		for (int i = Human::position.x - 1; i > Human::position.x - m_detect_length_present; --i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 0))
			{
				return true;
			}
			if (m_detect_collison(i, 1, collisions))
			{
				break;
			}
		}
		break;

	case 2:
		for (int i = Human::position.x + 1; i < Human::position.x + m_detect_length_present; ++i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 0))
			{
				return true;
			}
			if (m_detect_collison(i, 1, collisions))
			{
				break;
			}
		}
		break;

	case 3:
		for (int i = Human::position.z + 1; i < Human::position.z + m_detect_length_present; ++i)
		{
			if (LocalFunc::detected_event(Human::position, world_data, m_detect_height, i, 1))
			{
				return true;
			}
			if (m_detect_collison(i, 0, collisions))
			{
				break;
			}
		}
		break;
	}

	return false;
}
```

### ブロックの設置
マウスでクリックした場所にブロックを設置できるようにしました。
クリックした場所に必ず設置されるわけではなく、その場所が空気でなかったときや、プレイヤーの位置から一定距離内でなければイベントはキャンセルされます。

**マウスの位置とプレイヤーの描画座標を引いた絶対値**が一定より離れているかが、判定のポイントです。

またそのブロックを設置したときに、そのブロックの効果音を鳴らすことでリアルさを追求しました。

[World.cpp (GitHub)](https://github.com/takara2314/santa-game/blob/dcf2d3a49b5fe472bb45c32979fce1e81d800616/santa-game/World.cpp#L184-L259)

```c++
///////////////////////////////////
//  ブロックの設置
///////////////////////////////////
bool World::set_block(int mouse_x, int mouse_y, Vec3 player_pos, Item& block)
{
	// 設置しようとしているブロックが空気ならキャンセル
	if (block.name == U"空気")
	{
		return false;
	}

	size_t x, y, z;
	if (m_angle == 1)
	{
		x = static_cast<size_t>(mouse_x / ONE_PIXEL);
		y = static_cast<size_t>((MAX_Y * ONE_PIXEL - mouse_y) / ONE_PIXEL);
		z = static_cast<size_t>(player_pos.z + 0.5);
	}
	else
	{
		x = static_cast<size_t>(player_pos.x + 0.5);
		y = static_cast<size_t>((MAX_Y * ONE_PIXEL - mouse_y) / ONE_PIXEL);
		z = static_cast<size_t>(mouse_x / ONE_PIXEL);
	}

	// プレイヤーの位置には設置できない
	if (
		x == static_cast<size_t>(player_pos.x + 0.5)
		&& y == static_cast<size_t>(player_pos.y)
		&& z == static_cast<size_t>(player_pos.z + 0.5))
	{
		return false;
	}

	// プレイヤーの位置から離れすぎているところには設置できない
	int player_pos_x_draw = (player_pos.x + 0.5) * ONE_PIXEL;
	int player_pos_y_draw = (MAX_Y - 1 - player_pos.y) * ONE_PIXEL;
	int player_pos_z_draw = (player_pos.z + 0.5) * ONE_PIXEL;

	if (m_angle == 1)
	{
		if (abs(mouse_x - player_pos_x_draw) >= m_max_place_limit * ONE_PIXEL)
		{
			return false;
		}
		if (abs(mouse_y - player_pos_y_draw) >= m_max_place_limit * ONE_PIXEL)
		{
			return false;
		}
	}
	else
	{
		if (abs(mouse_x - player_pos_z_draw) >= m_max_place_limit * ONE_PIXEL)
		{
			return false;
		}
		if (abs(mouse_y - player_pos_y_draw) >= m_max_place_limit * ONE_PIXEL)
		{
			return false;
		}
	}

	// 既にブロックが設置されているならキャンセル
	if (world_data[x][y][z].name != U"空気")
	{
		return false;
	}

	// 設置音
	block.touch_sound.play();

	world_data[x][y][z] = block;
	collisions[x][y][z] = true;

	return true;
}
```

# まとめ
僕のC++歴は9か月ほど（空白期間6か月）ですが、OpenSiv3Dのおかげで簡単な構文の組み合わせだけでもゲームが作れました。久しぶりに Visual Studio で C++ を使って開発したので、1週間ほど時間がかかりましたが、クリスマスシーズンにこのようなカタチに残るものを作れてよかったです。

# 制作裏話
（公開記事なので裏話でもなんでもないですが）
`OpenSiv3D`の`3D`は3Dゲームのことだと元々思っていたので、はむかって2Dゲームを作れば面白いのでは？と思っていました！！（もちろん冗談です）

最初は3Dゲームのつもりで作ろうと思いましたが、OpenSiv3Dの3Dレンダーに関する知識がないのと記事投稿日が迫っていたので、泣く泣く2Dを選択しました。

次は3Dのゲームを何か作ってみようと思います！