4
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を学ぶと「設計」の何が変わるか

Last updated at Posted at 2025-12-03

はじめに

この記事はSiv3D Advent Calendar 2025、4日目の記事です

ゲームエンジンとして、他より基礎的で素朴だから、という理由で私はSiv3Dが好きです
これは、プリミティブな部分についての学習を助けるからです

この記事では、ゲームを開発し始める際の設計にとって、Siv3D、およびC++がどのように働くかを考えます
また、私自身の開発体験に基づく主観がかなり強いです

チームにどのゲームエンジンを提示するか?

まず、ゲームがリリースされることを期待して動きたい開発メンバーが多いでしょう

私は、この点では、Siv3Dは発展途上だと主張することしかできません

「モバイルプラットフォームにリリースできる?」
と訊かれた際に「はい」で答えられません

それでも私はSiv3Dが好きです
このゲームはリリースされない、という前提を置き、次の項に移ります

小さな物→大きな物の順に作る

このゲームのプレイヤーキャラクターには位置があるとします
位置があると、移動ができるようになり便利ですね

Main.cpp
double positionX;
double positionY;

位置をdoubleというプリミティブな型で持っています
そのため、他の物にも位置という概念が与えられる度、位置を移動させるメソッドを作成しなければいけません

DRYの原則(Don't Repeat Yourself)に反していますね

この小さな物を、大きな物にまとめてくれていることが分かるVec2クラスをSiv3Dは提供します

Main.cpp
Vec2 position;

「ベクトルを扱うクラスがゲームエンジンにあることは当たり前だ」と思う方も居ると思います

学習者にとってとてもありがたいことに、Siv3Dはオープンソースです
Vec2クラスについて、定義を確認することができます
このクラスはどのような正体をしているのか、学習者は知ることができます

Fwd.hpp
using Vec2 = Vector2D<double>;

先ほどdoubleで位置を定義した学習者は、doubleについては知っています
しかし、doubleを括っている<>については知りません

それでも学習者は、更にこのVector2Dの定義を確認できます

Fwd.hpp
/// @brief 2 次元のベクトル
/// @tparam Type ベクトルの要素の型
template <class Type> struct Vector2D;

/// @brief 2 次元のベクトルを、float 型の要素 x, y で表現するクラスです。
using Float2 = Vector2D<float>;

/// @brief 2 次元のベクトルを、double 型の要素 x, y で表現するクラスです。
using Vec2 = Vector2D<double>;

Vector2Dにはバリエーションがあるようです
<>内に括られているものを変更しても成り立つことを知ることができます

そして、変更された型について機能することを、日本語のコメントが伝えています
ドキュメントから学びたい時、それ自体が読めない言語で書かれていると、まずその言語を読めるようになる必要があります
Siv3Dの開発コミュニティには日本語で参加することができるため、日本語ではない外国語の能力が大きなハードルにならないことを価値に感じます

更に詳細にstruct Vector2Dの定義を確認することができます

Vector2D.hpp
template <class Type>
struct Vector2D
{
	/// @brief ベクトルの要素の型
	using value_type = Type;

	/// @brief ベクトルの次元数
	static constexpr size_t Dimension = 2;

	/// @brief ベクトルの X 成分
	value_type x;
	
	/// @brief ベクトルの Y 成分
	value_type y;

...中略

/// @brief 位置ベクトルを移動させます
/// @param _x X 成分の移動量
/// @param _y Y 成分の移動量
/// @return *this
constexpr Vector2D& moveBy(value_type _x, value_type _y) noexcept;

/// @brief 位置ベクトルを移動させます
/// @param v 移動量
/// @return *this
constexpr Vector2D& moveBy(Vector2D v) noexcept;

...中略

}

すると、位置のxとyはstructの中にセットで定義されていることが分かります
同時に振る舞う小さな物は、より大きな物に持たせておくというパターンについて学ぶことができます

そして、当初の目的であった「位置があると、移動ができるようになる」メソッドを見つけることができます
しかし、定義があるだけなので、どのように実装すれば移動できるようになるのか分かりません
これでは、自分で別の物を移動させたいと思った時に困ってしまいます

更に、このmoveByの実装を確認することができます

Vector2D.ipp
template <class Type>
inline constexpr Vector2D<Type>& Vector2D<Type>::moveBy(const value_type _x, const value_type _y) noexcept
{
	x += _x; y += _y;
	return *this;
}

template <class Type>
inline constexpr Vector2D<Type>& Vector2D<Type>::moveBy(const Vector2D v) noexcept
{
	x += v.x; y += v.y;
	return *this;
}

プリミティブなものを操作することで、移動させていることを知ることができます

ついに仕組みが分かったため、ゲームに位置と移動を実装する際の設計について学習者はヒントを得ています
また、「同時に振る舞う小さな物は、より大きな物に持たせておく」ようヒントを得ています

そのため、プレイヤーキャラクターの設計は次のように変更しやすくなるでしょう

Main.cpp
struct PlayerModel {
	Vec2 position;
	Vec2 velocity;

	void move() {
		position.moveBy(velocity);
	}
	
};

責任を持たせる

次に、このmoveメソッドは、誰が実行してくれるのでしょうか?

Unityのチュートリアルを見ると、Update()の中に移動処理を書けば良いということが分かります
簡単で便利です
しかし、そのUpdate()がどのように実行されているかは曖昧なままです

Siv3Dのプロジェクトを新規作成すると、例となるコードが既に書かれています

Main.cpp
while (System::Update())
{
	// テクスチャを描く | Draw the texture
	texture.draw(20, 20);

	// テキストを描く | Draw text
	font(U"Hello, Siv3D!🎮").draw(64, Vec2{ 20, 340 }, ColorF{ 0.2, 0.4, 0.8 });

}

UI要素を描いているwhileループを見つけられます
学習者はwhileが無限ループに使えることを知っているだけで、Update()を上手く実行してくれるものなのかどうかは分かりません

whileはbool型をチェックしているはずです
System::Update()の定義が確認できます

System.hpp
namespace System
{
	/// @brief 描画や入力情報などフレームを更新します
	/// @remark アプリケーション終了トリガーが発生するか内部で回復不能なエラーが発生した場合に false を返しますこの関数が false を返したらプログラムを終了させるべきです
	/// @return プログラムの続行の可否
	bool Update();

	/// @brief プログラムを終了させるためにこの直後の `System::Update()`  false を返すように設定します
	/// @remark この関数自体は終了処理を行わないためこの関数の呼び出しは必須ではありません
	void Exit() noexcept;
}

確かに、このSystem::Update()はboolを返していることが分かります
このUpdate()の詳細な定義はどうでしょう?

定義ファイル名で検索すると、Oleidl.hが使用されているように推測はできますが、詳細な動作について確認することは難しそうです

そのため、ひとまずこのSystem::Update()をループに用いればゲームは動作させられそうですが、詳細な設計を学ぶことはできません

よりプリミティブな、画面のピクセルの色が変更された後、それの上書きを程よい間隔で繰り返すことの責任は誰が持っているのでしょう?
「フレームを更新する」仕組みについて知りたい学習者は工夫が必要になりそうです
(私は、この描画の仕組みをJavaアプリケーションから学んだ覚えがあります)

しかし、詳細な仕組みは分からずとも、このwhileループにmoveメソッドを記述すればゲームが動作する、という「責任の所在」が明白です

加えて、このwhileループを持っているMainがエントリーポイントとして働いていることを伝えやすいです
このエントリーポイントに、アプリケーションの開始時に責任を持たせるべきものを明示できる点も、Siv3Dの魅力だと思います

いつ成功体験を与えるか?

繰り返しますが、ゲームがリリースされることを期待して動きたい開発メンバーが多いでしょう

「自分のゲームが上手く動作した」という成功体験は、学習者にモチベーションを与え、更に進んだ学習へと導きます

先ほどの、プロジェクトを新規作成した際のコードを別の視点から見てみます

Main.cpp
while (System::Update())
{
	// テクスチャを描く | Draw the texture
	texture.draw(20, 20);

	// テキストを描く | Draw text
	font(U"Hello, Siv3D!🎮").draw(64, Vec2{ 20, 340 }, ColorF{ 0.2, 0.4, 0.8 });

}

画面にUI要素を描くまでのステップが簡明に示されています
ゲーム上で、何らかの動く画面を表示するまでのハードルが低く、十分に補助されています

これは学習者に素早く成功体験を与えます
Siv3Dの良いところだと思います

それでは次に、この描画するべきUI要素が変更される時についてはどうでしょう?
プレイヤーキャラクターが変更される時、画像等のUI要素を変更したいとします

例えばC#では、eventキーワード、およびAction型が提供されています

PlayerNameChanger.cs
public class PlayerNameChanger
{
    public event Action<string> OnPlayerNameChanged;

    private PlayerView playerView;
    public PlayerView PlayerView { get => playerView; }

    public PlayerNameChanger()
    {
        playerView = new PlayerView();
        OnPlayerNameChanged += playerView.changePlayerName;
    }

    public void change()
    {
        OnPlayerNameChanged?.Invoke("newOne");
    }
}

これを使用した、Observerパターンの構成までが素早いです
プレイヤー名の変更がイベントを発火させ、そのイベントを購読する各UI要素が変更を行います

Updateによるポーリングを避けることができました
また、プレイヤー名の変更に対して同時に動作したいオブジェクト同士は参照し合う必要がありません
イベントのみを参照すればよいため、より疎結合になっています

この時、学習者は、自分のUIに関わるコーディングの選択肢が増えたことについて成功体験を得ることができます
代わりに、Action型の実装について知ることは難しいです

一方で、ObserverパターンをC++で構成するにはどうすればよいでしょう?
Action型に相当するものはありません
また、Action型を購読する側の構造もありません
これらを自作する必要があります

Main.cpp
class IObserver {
public:
	virtual void Update(const String& data) = 0;

};

class Subject {
private:
	Array<IObserver*> observer;
public:
	virtual void attach(IObserver* observer) = 0;

	void notify(const String& data) {
		for (IObserver* i : observer) {
			i->Update(data);
		}
	};

	void chageState() {
		notify(U"newOne");
	}

};

例えばこのように、
まず、純粋仮想関数のみを持つクラスを定義します
そして、それらを保持し、イベントの発火と購読をします
これがString型以外の多様な型について働く状態にできれば、機能しそうです

ここまでを自ら実装する必要があります
しかし、そもそも学習者はObserverパターンについてのヒントをSiv3D上の開発からは得ていない状態です
C#のAction型に相当するものや、UniRxが提供するSubject型に相当するものがあると便利だと思えるのは、開発の途上でそれらを使用する機会があるからです
そのため、学習者はObserverパターン自体を学習する機会が少なくなると思っています

学習者は、自分のUIに関わるコーディングの選択肢が増えたことについて成功体験を得ることが、より難しくなります
代わりに、純粋仮想関数を用いて定義だけを渡すことや、それらを内部的に配列で管理することについて学びます

重要なのは、ここで学習者が得ている成功体験は、自分のゲームのUI要素を上手く変更させることとは遠ざかったものであることです
「自分のゲームが上手く動作した」という成功体験を得るのは、まだ先のことになってしまいます

私はこれを、プリミティブな部分についての学習を助けることの功罪だと感じます

プリミティブなものを使った設計にコストを割くほど、ゲームを開発できた体験や、そこから得るモチベーションは遠ざかります

同時に、プリミティブなものを使った設計ができるようになるほど、その言語、ライブラリ、フレームワークに依存しない方法を選択肢に持つことができます

以上の特徴が、
ゲームを開発し始める際の設計にとっての、Siv3D、およびC++の働きだと感じます

参考文献

9. 画面と座標 - Siv3D
オブサーバー設計パターン

4
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
4
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?