スクロールの実装 - Siv3D/Reference-JP GitHub Wiki

スクロールの考え方

俯瞰視点のゲームで画面上のキャラクターに指をあてると、ゲーム内でどれだけ動こうが実はキャラクターは画面の中心にいることがわかります。(ステージの端だとまた別ですが)

なので、基本的な考えとしてはキャラクターは常に画面の中心に置き、それ以外のオブジェクトを逆向きに動かすことで実現できます。

しかし、本当に逆向きに動かしてると訳がわからなくなってくるので、座標の上では画面外まで動かし、描画する時だけプレイヤーが画面の中心になるように引っ張ってきます。

キャラを (0, 0) に固定する

いきなり中心は難しいのでキャラを (0, 0)、つまり画面左上に固定します。

// 変更がない部分は省略

class Block
{
public:

	// プレイヤーの現在位置を更新する関数
	void setPlayerPos(const Vec2& pos)
	{
		m_playerPosition = pos;
	}

	// 描画をする関数(描画操作以外行わないこと.)
	void draw()
	{
		m_region.movedBy(-m_playerPosition)(m_texture).draw();
	}

private:

	// プレイヤーの現在の位置
	Vec2 m_playerPosition;
};

class Player
{
public:

	// 位置を取得する関数
	Vec2 getPos()
	{
		return m_position;
	}
	
	// 描画をする関数(描画操作以外行わないこと.)
	void draw()
	{
		RectF(Vec2(-72.5, -200), 145, 200)(m_texture).draw();
	}
};

void Main()
{
	while (System::Update())
	{
		for (size_t i = 0; i < blocks.size(); i++)
		{
			blocks[i].setPlayerPos(player.getPos());
			blocks[i].update();
		}
	}
}

プレイヤーの描画から見ていきます。

(0, 0)に固定するのですが、キャラの足元を(0, 0)に固定したいので、実際に指定する座標はその左上です。
ゲーム内のキャラの座標 m_position を描画に使用していませんが、画面中心に固定しているのでそれで正しいです。

次にブロックの描画を見てみます。

movedBy関数は引数に指定されただけ動かしたRectFを返す関数です。
今回はプレイヤーの動く向きの逆に動かしたいので、座標にマイナスを付けた値だけ動かします。

そのために、ブロックからプレイヤーの座標を知る必要があるので、PlayerクラスにgetPos関数、BlockクラスにsetPlayerPos関数を用意します。

ここで注意してほしいのが、moveBy関数ではなくmovedBy関数を用いることです。
moveBy関数はmovedBy関数と違ってもとのRectFを変更します。今回はもとのデータは変更せずに、描画する時だけ動かしたいのでmovedBy関数を使用します。

実行してみると、画面左上にキャラを固定してスクロールさせているので、見事にどれだけ動かしてもSiv3D君が見えません。しかし、ちゃんとスクロールが実装できたのがわかると思います。

キャラを画面中心に固定する

流石にこれは鬼畜ゲー過ぎるので、わかりやすく画面中心に固定します。
とは言っても、実はここまで来れば一発です。

Window::Center関数で画面の中心の座標を取得できます。なので、(0, 0)に固定するために指定した座標にWindow::Center関数の値を足すだけで画面の中心にSiv3D君が現れます。

class Block
{
	void draw()
	{
		m_region.movedBy(-m_playerPosition + Window::Center())(m_texture).draw();
	}
}

class Player
{
	void draw()
	{
		RectF(Vec2(-72.5, -200) + Window::Center(), 145, 200)(m_texture).draw();
	}
};

まとめと改良案

3回に渡り、長々とお付き合いいただきありがとうございました!

せっかくなので、このサンプルの改良案とその技術について触れようと思います。個人的に上に書いたもの程、簡単に実装できると思います。

1. ゲームオーバーの実装

プレイヤーのy座標が一定を超える(下に落ち過ぎる)とアプリを終了する等で実現できます。

2. 速度を実装する

サンプルは移動分をそのまま座標に足していますが、新たにメンバ変数として速度を作り、毎ループ座標に速度を加える設計にして、キーボードの入力や重力を速度に加えるようにすれば、加速度などを実装できるようになります。

3. キャラの向きを変える

キャラの画像を向きによって複数用意し、進行方向によって描画する画像を切り替えると、よりゲームっぽくなります。

4. 下以外の当たり判定を実装する

サンプルは下方向のみに当たり判定があるので、それを上下左右に適応し、衝突した方向の速度を0にすることで実装できます。

5. 乗ると落ちるブロックを作る

プレイヤーが乗ったのを検知したら、ブロック側のupdate関数内でプレイヤーを落とす時と同じ処理をすれば実装できます。

6. ブロックを削除できるようにする

ブロックにメンバ変数として死亡フラグを持たせ、Siv3DのErase_if関数と組み合わせることにより実装できます。

7. アイテムを実装する

基本的にはブロックと同じで、プレイヤーが触れたら消えて、何かの効果を発揮するようにすれば実装できます。

8. シーン管理をする

実際のゲームはタイトル画面やリザルト画面などがゲーム画面以外にも存在し、それらが切り替わっていると思います。

HamFrameworkのSceneManagerに対応させると、簡単にそれらが実装できます。

9. ポーズを実装する

早期リターンを用いてupdate関数を途中で終了することによって実装できます。

10. 複数のクラスをまとめて扱う

今はPlayerとBlockだけでしたが、今後たくさんのクラスを実装した時に、その度にArrayを追加するのは大変です。なので、それらをまとめて扱えるようにしましょう。

その前に、今はPlayerクラスはcheckGround関数、BlockクラスはsetPlayerPos関数を使用していますが、ポインタや参照を用いることによって、引数で渡していたものをメンバに保存させると、Main関数からこれらの関数を呼ぶ必要がなくなります。

となると、PlayerクラスとBlockクラスはupdate関数、draw関数を有しているという共通点が生まれます。なので、これらの関数を有する親クラスを作成、PlayerとBlockはその子クラスとして継承させることができます。

後はMain関数にあったArrayをArray<"親クラスのポインタ">とすると一つのArrayで管理することが可能になります。

この例の場合は微妙ですが、落ちるブロックと普通のブロックをまとめて扱う等はすごく便利だと思います。

見本

# include <Siv3D.hpp>

class Block
{
public:

	Block() {}

	Block(const RectF& region) :
		m_region(region),
		m_texture(L"Example/Brick.jpg") {}

	// プレイヤーの現在位置を更新する関数
	void setPlayerPos(const Vec2& pos)
	{
		m_playerPosition = pos;
	}

	// 描画以外の操作をする関数
	void update() {}

	// 点との当たり判定を取る関数
	bool intersects(const Vec2 &shape) const
	{
		return m_region.intersects(shape);
	}

	// 描画をする関数(描画操作以外行わないこと.)
	void draw()
	{
		m_region.movedBy(-m_playerPosition + Window::Center())(m_texture).draw();
	}


private:

	// ブロックの領域
	RectF m_region;

	// ブロックのテキスチャ(画像)
	Texture m_texture;

	// プレイヤーの現在の位置
	Vec2 m_playerPosition;
};


class Player
{
public:

	Player() :
		m_position(100, 200),
		m_texture(L"Example/Siv3D-kun.png"),
		m_isGrounded(false),
		m_jumpFrame(0) {}

	// 位置を取得する関数
	Vec2 getPos()
	{
		return m_position;
	}

	// 地面に接しているかを更新する関数
	void checkGround(const Array<Block>& blocks)
	{
		m_isGrounded = false;

		for (size_t i = 0; i < blocks.size(); i++)
		{
			if (blocks[i].intersects(m_position))
			{
				m_isGrounded = true;
			}
		}
	}

	// 描画以外の操作をする関数
	void update()
	{
		if (m_isGrounded)
		{
			if (Input::KeySpace.clicked && m_jumpFrame <= 0)
			{
				m_jumpFrame = 30;
			}
		}
		else
		{
			m_position.y += 10.0;
		}

		if (m_jumpFrame > 0)
		{
			m_position.y -= 20.0;
			m_jumpFrame--;
		}
		if (Input::KeyRight.pressed)
		{
			m_position.x += 5.0;
		}
		if (Input::KeyLeft.pressed)
		{
			m_position.x -= 5.0;
		}
	}

	// 描画をする関数(描画操作以外行わないこと.)
	void draw()
	{
		RectF(Vec2(-72.5, -200) + Window::Center(), 145, 200)(m_texture).draw();
	}

private:

	// プレイヤーの座標
	Vec2 m_position;

	// プレイヤーのテクスチャ(画像)
	Texture m_texture;

	// 地面に接しているか否か
	bool m_isGrounded;

	// 残りのジャンプ時間
	int m_jumpFrame;
};


void Main()
{
	Window::Resize(1280, 720);

	Texture background(L"Example/Windmill.png");
	Player player;
	Array<Block> blocks;

	blocks.push_back(Block({-400, 400, 200, 200}));
	blocks.push_back(Block({-200, 400, 200, 200}));
	blocks.push_back(Block({0, 400, 200, 200}));
	blocks.push_back(Block({200, 400, 200, 200}));
	blocks.push_back(Block({200, 200, 200, 200}));
	blocks.push_back(Block({400, 400, 200, 200}));
	blocks.push_back(Block({800, 400, 200, 200}));
	blocks.push_back(Block({1000, 400, 200, 200}));
	blocks.push_back(Block({1300, 200, 400, 30}));

	while (System::Update())
	{
		for (size_t i = 0; i < blocks.size(); i++)
		{
			blocks[i].setPlayerPos(player.getPos());
			blocks[i].update();
		}

		player.checkGround(blocks);
		player.update();

		// 実際には縦横比を合わせるように.
		Rect(Window::Size())(background).draw();

		for (size_t i = 0; i < blocks.size(); i++)
		{
			blocks[i].draw();
		}

		player.draw();
	}
}

← 前の章へ戻る | - 目次 - ||


Written by あさちゅん

⚠️ **GitHub.com Fallback** ⚠️