10
0

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用ゲームUIライブラリ「NocoUI」の紹介

Last updated at Posted at 2025-12-09

この記事はSiv3D Advent Calendar 2025の10日目の記事です。

はじめに

Siv3D v0.6向けのゲームUIライブラリ「NocoUI」を開発したので紹介します。

NocoUIでは、ビジュアルエディタ(NocoEditor)上で.nocoファイルとしてUIを編集・保存し、それをプログラム上で実行時に読み込んで利用できます。

▼ NocoEditorの見た目。NocoEditor自体もSiv3DとNocoUIで実装されています!
image.png

作成した.nocoファイルは、下記のようなコードを書いてSiv3Dのプログラム上から簡単に利用できます。

#include <Siv3D.hpp>
#include <NocoUI.hpp>

void Main()
{
    // NocoUIを初期化
    noco::Init();

    // ファイルからCanvasを読み込み
    const auto canvas = noco::Canvas::LoadFromFile(U"canvas.noco");

    while (System::Update())
    {
        // Canvasを更新
        canvas->update();

        // Canvasを描画
        canvas->draw();
    }
}

なぜ、UIライブラリが必要か?

Siv3Dは他のゲームエンジン(Unity、Unreal Engine等)とは異なり、ゲーム画面を視覚的に編集するエディタを持たないのが大きな特徴です。

処理がソースコードだけで完結するため、コードを読めば一連の処理の流れを把握できるのが大きな利点です。

とは言え、ゲームUIは複雑化するにつれソースコードだけで実装するのが徐々に大変になりやすいです。

1. 座標や色などの定数の扱いが大変

一般的なコーディング原則として、マジックナンバーは避けて定数化すべきとよく言われます。名前を付けることで値の意味が分かりやすくなるほか、後から変更もしやすくなります。

一方、ゲームUIでは、この原則を守って座標や色の一つひとつをソースコード上に全て定数化するのは非常に大変で、現実的ではありません。数が多く、名前を付けるだけでも時間がかかります。

大量の定数!
constexpr Point IconPosition{ 20, 20 }; // プレイヤーアイコン位置
constexpr Size IconSize{ 80, 80 }; // プレイヤーアイコンの大きさ
constexpr int32 PlayerNameFontSize = 36; // プレイヤー名のフォントサイズ
constexpr Color PlayerNameFontColor = Palette::Black; // プレイヤー名の文字色
constexpr int32 MarginBetweenIconAndName = 10; // プレイヤーアイコンとプレイヤー名の間のマージン(px)
constexpr Point PlayerNamePosition = IconPosition.movedBy(IconSize.x + MarginBetweenIconAndName, 0); // プレイヤー名の位置
constexpr Size PlayerPlateSize{ 600, 120 }; // プレイヤー情報プレートの大きさ
constexpr Color PlayerPlateBackgroundColor{ 240, 240, 240 }; // プレイヤー情報プレートの背景色
constexpr int32 PlayerPlateMarginY = 10; // プレイヤー情報プレート同士の縦のマージン(px)
constexpr Point PlayerPlateContainerPosition{ 200, 20 }; // プレイヤー情報プレートを並べるコンテナの位置
constexpr Size PlayerPlateContainerSize{ 640, 400 }; // プレイヤー情報プレートを並べるコンテナの大きさ

// ...このような定数が数十〜数百行続く

かつ、ソースコードからは視覚的な情報が得られないため、画面上のどこを指す値なのか分かりづらいという問題もあります。

2. 調整時の再コンパイルと画面確認が大変

定数に変更を加えると、プログラムの再コンパイルが必要になります。
そのため、座標などの細かい見た目調整の際、都度再コンパイルの時間を待つ必要が出てきます。

さらに、コンパイルした後、実際にゲームを実行してその画面まで到達する必要があります。
特にステージクリア画面などの特定の状況でのみ表示される画面は、スキップできる仕組みがないと毎回ゲームを手動プレイしないといけなくなり大変です。
(そして、そこでついついプレイにのめりこんで開発の手が止まってしまうのは、個人ゲーム開発あるあるですね…)

解決策: 座標や色などの値はソースコードの外で管理しよう!

上記の問題へ対処するために、ゲーム画面のUIをソースコードではなく実行時に読み込むアセットとして扱い、エディタ上で視覚的に編集・プレビューできるNocoUIを開発しました。

これにより、以下の課題を解決することができます。

  • 定数を定義するのが大変
    → アセットとして作成するため、ソースコード上に座標などの定数定義がいらなくなる
     
  • UI調整の度に再コンパイルの時間がかかる
    → 実行時に読み込むため、プログラムの再コンパイルが必要ない
     
  • UI調整の度にゲームプレイしてその画面に到達するのが大変
    → エディタ上でプレビューできるため、見た目確認のためのゲームプレイが必要ない

NocoUIの基本要素

まずは、NocoUIの基本要素について紹介します。

キャンバス(Canvas)

キャンバス(Canvas) とは、UI全体を管理する要素です。

NocoEditorで作成し、.nocoファイル(JSON形式)として保存できます。
これをプログラム上で読み込んで利用できます。

ノード(Node)

ノード(Node) とは、キャンバス上に配置できる、位置とサイズを持つ要素です。

ノード同士は親子関係を持つことができ、子ノードは親ノードの位置やサイズを基準に配置されます。

ノードの挿入方法

NocoEditorのウィンドウ左側で右クリック→「新規ノード」をクリックすると新しいノードを挿入できます。

image.png

ノードの位置とサイズの設定

ノードの位置とサイズは、リージョン(Region) と呼ばれる設定項目で指定できます。

基本的なUIであれば、typeとanchorはデフォルトのまま サイズ(size)位置(posDelta) のみを使用する形で問題ありません。

image.png

補足: リージョンについての詳細説明

リージョンには下記の2種類があります。
これらを使い分けることで、ウィンドウサイズが変わった場合のためのレスポンシブ対応(Edge-to-Edge対応)をしたり、要素を横や縦に並べたりすることが可能です。

アンカーリージョン(AnchorRegion)

  • ゲームUIではこちらを使用することが多いです。
  • 親要素の上下左右の座標をもとにしたアンカー座標をもとに、位置とサイズを指定します。
  • 親に指定された子レイアウト(Children Layout)の設定は無視されます。
  • 意味としては、CSSのposition: absoluteに近いです。Unity(uGUI)やUnreal Engine(UMG)で基本的な配置方法として採用されているアンカー機能と同じ仕組みです。

インラインリージョン(InlineRegion)

  • 要素を横や縦に並べたいときに使います。
  • 親要素に指定された子レイアウト(Children Layout)に従って、順番に配置します。
  • 意味としては、CSSのposition: relativeに近いです。

InlineRegionをもつ子要素の並べ方は、親要素の「Children Layout」(子レイアウト)設定で下記の3種類から選択できます。

  • FlowLayout:
    • 左上から順に並べて、右端に到達したら折り返します。
  • HorizontalLayout:
    • 左から右へ並べます。右端に到達しても折り返しません。
    • 主に、横方向のリスト表示に使用します。
  • VerticalLayout:
    • 上から下へ並べます。下端に到達しても折り返しません。
    • 主に、縦方向のリスト表示に使用します。

↓ FlowLayout(左上から並べる)とInlineRegionを利用した場合の例
image.png

コンポーネント(Component)

コンポーネントとは、ノードに追加してさまざまな処理や描画を行う要素です。

ノードにコンポーネントを追加することで、画像や図形、テキストの描画などが可能になります。

コンポーネントの追加方法

NocoEditor上でノードを選択し、右側の「+ コンポーネントを追加」ボタンをクリックし、挿入したいコンポーネントの種類を選びます。

image.png

コンポーネントにはさまざまな種類があります。
例えば、RectRendererを挿入すると、ノードに対して四角形を描画することができます。
各プロパティ(設定項目)にマウスカーソルを当てると、その説明を確認できます。

image.png

パラメータ(Params)

パラメータ(Params) は、各コンポーネントのプロパティ値を外部から変更するための仕組みです。

プログラム上から動的に変更したい値はあらかじめパラメータとして定義しておくことで、プログラム上から各コンポーネントに直接アクセスしなくても簡単に値をセットすることができるようになります。

image.png

パラメータの指定方法

プロパティにパラメータの値を反映するには、プロパティを右クリックし、「参照パラメータを選択」をクリックします。

image.png

以下のダイアログが開き、パラメータの新規作成や、既存のパラメータの選択ができます。

image.png

なお、ノード表示有無に対しても参照パラメータを指定することができます。こちらは、ノードのチェックボックスを右クリック→「参照パラメータを選択」から開くことができます。

image.png

プログラムからのパラメータ値変更

プログラム上からは、以下のようにパラメータ名指定で動的に値を設定することができます。

以下の2通りの書き方が利用できます。

setParamValue関数で個別にパラメータ値を設定
canvas->setParamValue(U"backgroundColor", Color{ 96, 128, 255 });
canvas->setParamValue(U"buttonText", U"ボタンテキスト");
canvas->setParamValue(U"cornerRadius", 20);
もしくは、setParamValues関数で一括設定することも可能
canvas->setParamValues({
    { U"backgroundColor", Color{ 96, 128, 255 } },
    { U"buttonText", U"ボタンテキスト" },
    { U"cornerRadius", 20 },
});

実践編: ブロック崩しにUIを付けてみよう!

ここからは実際に既存のゲームにNocoUIを使ってUIを付けてみましょう!

今回はシンプルな例として、お馴染みSiv3D公式サンプルの「ブロックくずし」を拡張し、スコア・ライフ・ゲームオーバー表示のUIを追加してみます。

説明の都合上、あらかじめゲーム全体をBrickBreakerクラスとしてupdate関数とdraw関数に分けた上で、スコアと残ライフを実装した下記のサンプルをベースに実装していきます。

# include <Siv3D.hpp>

void Main()
{
	BrickBreaker game;

	while (System::Update())
	{
		game.update();
		game.draw();
	}
}
BrickBreakerクラスの内容
class BrickBreaker
{
private:
	// 1 つのブロックのサイズ
	static constexpr Size BrickSize{ 40, 20 };

	// ボールの速さ(ピクセル / 秒)
	static constexpr double BallSpeedPerSec = 480.0;

	// ボールの速度
	Vec2 m_ballVelocity{ 0, -BallSpeedPerSec };

	// ボール
	Circle m_ball{ 400, 400, 8 };

	// ブロックの配列
	Array<Rect> m_bricks;

	// スコア
	int32 m_score = 0;

	// ライフ
	int32 m_life = 3;

	// ゲームオーバーしたかどうか
	bool m_isGameOver = false;

	// パドルの矩形
	Rect getPaddle() const
	{
		return Rect{ Arg::center(Cursor::Pos().x, 500), 60, 10 };
	}

	// ボールを初期位置・初期速度に戻す
	void resetBall()
	{
		m_ball.set(400, 400, 8);
		m_ballVelocity = Vec2{ 0, -BallSpeedPerSec };
	}

public:
	BrickBreaker()
	{
		for (int32 y = 0; y < 5; ++y)
		{
			for (int32 x = 0; x < (Scene::Width() / BrickSize.x); ++x)
			{
				m_bricks << Rect{ (x * BrickSize.x), (60 + y * BrickSize.y), BrickSize };
			}
		}
		resetBall();
	}

	// 毎フレームの更新
	void update()
	{
		if (m_isGameOver)
		{
			// 必要ならここでリスタート処理などを書く
			return;
		}

		// パドル
		const Rect paddle = getPaddle();

		// ボールを移動させる
		m_ball.moveBy(m_ballVelocity * Scene::DeltaTime());

		// ブロックを順にチェックする
		for (auto it = m_bricks.begin(); it != m_bricks.end(); ++it)
		{
			// ブロックとボールが交差していたら
			if (it->intersects(m_ball))
			{
				// ブロックの上辺、または底辺と交差していたら
				if (it->bottom().intersects(m_ball) || it->top().intersects(m_ball))
				{
					// ボールの速度の Y 成分の符号を反転する
					m_ballVelocity.y *= -1;
				}
				else // ブロックの左辺または右辺と交差していたら
				{
					// ボールの速度の X 成分の符号を反転する
					m_ballVelocity.x *= -1;
				}

				// ブロックを配列から削除する(イテレータは無効になる)
				m_bricks.erase(it);

				// スコア加算(任意)
				m_score += 10;

				// これ以上チェックしない
				break;
			}
		}

		// 天井にぶつかったら
		if ((m_ball.y < 0) && (m_ballVelocity.y < 0))
		{
			// ボールの速度の Y 成分の符号を反転する
			m_ballVelocity.y *= -1;
		}

		// 左右の壁にぶつかったら
		if (((m_ball.x < 0) && (m_ballVelocity.x < 0))
			|| ((Scene::Width() < m_ball.x) && (0 < m_ballVelocity.x)))
		{
			// ボールの速度の X 成分の符号を反転する
			m_ballVelocity.x *= -1;
		}

		// パドルにあたったら
		if ((0 < m_ballVelocity.y) && getPaddle().intersects(m_ball))
		{
			const Rect paddleNow = getPaddle();

			// パドルの中心からの距離に応じてはね返る方向(速度ベクトル)を変える
			m_ballVelocity = Vec2{ (m_ball.x - paddleNow.center().x) * 10, -m_ballVelocity.y }
			.setLength(BallSpeedPerSec);
		}

		// ボールが画面下に出たら
		if (m_ball.y > Scene::Height() && m_life > 0)
		{
			// ライフを減らす
			--m_life;

			if (m_life == 0)
			{
				// 残りライフがなければゲームオーバー
				m_isGameOver = true;
			}
			else
			{
				// ボールを初期位置に戻す
				resetBall();
			}
		}
	}

	// 毎フレームの描画
	void draw() const
	{
		// マウスカーソルを非表示にする
		Cursor::RequestStyle(CursorStyle::Hidden);

		// すべてのブロックを描画する
		for (const auto& brick : m_bricks)
		{
			// ブロックの Y 座標に応じて色を変える
			brick.stretched(-1).draw(HSV{ brick.y - 40 });
		}

		// ボールを描く
		m_ball.draw();

		// パドルを描く
		getPaddle().rounded(3).draw();
	}

	bool isGameOver() const noexcept
	{
		return m_isGameOver;
	}

	int32 life() const noexcept
	{
		return m_life;
	}

	int32 score() const noexcept
	{
		return m_score;
	}
};

NocoEditorでのUI作成

NocoEditorで、以下のようなUIを作成してみましょう。

image.png

追加するノード・コンポーネントは以下の通りです。

  • ScoreHeading
    • size: (100, 40)
    • posDelta: (-340, -270)
    • Labelコンポーネントを追加して、以下を設定
      • text: "SCORE:"
      • fontSize: 24
  • ScoreNumber
    • size: (75, 54)
    • posDelta: (-260, -272)
    • Labelコンポーネントを追加して、以下を設定
      • text: "0"
      • fontSize: 36
  • Life1Life2Life3
    • size: (50, 50)
    • posDelta: それぞれ順に、(365, -270)、(310, -270)、(255, -270)
    • ShapeRendererコンポーネントを追加して、以下を設定
      - shapeType: Heart
      - fillColor: (255, 128, 128, 255)
  • GameOver
    • size: (500, 100)
    • posDelta: (0, 0)
    • Labelコンポーネントを追加して、以下を設定
      • text: "GAME OVER"
      • fontSize: 48

Canvasに以下のパラメータを追加し、対応するプロパティに参照パラメータを指定しておきます。

image.png

  • isGameOverパラメータ(Bool型)
    • GameOverノードの表示有無のチェックボックスへ指定
  • life1life2life3パラメータ(Bool型)
    • それぞれ、Life1Life2Life3ノードの表示有無のチェックボックスへ指定
  • scoreパラメータ(String型)
    • ScoreNumberノードが持つLabelコンポーネントのtextプロパティへ指定

※ 完成品のnocoファイルは以下からダウンロードできます。
https://gist.github.com/m4saka/c2612cc176b045f79d48a3377519a0a7

プログラム上での実装

まずは、Canvasをロードして毎フレーム更新・描画するコードを記述しましょう。
以下の行を追加してください。

 # include <Siv3D.hpp>
+# include <NocoUI.hpp>

 void Main()
 {
+    // NocoUIライブラリを初期化
+    noco::Init();
+    
     BrickBreaker game;

+    // Canvasを読み込み
+    auto canvas = noco::Canvas::LoadFromFile(U"game.noco");

     while (System::Update())
     {
         game.update();
+        canvas->update();
+
         game.draw();
+        canvas->draw();
     }
 }

これで、ゲーム画面上にUIが表示されるようになりました!
しかし、まだパラメータを反映していないので、スコア・ライフ・ゲームオーバー表示が切り替わっていません。

以下のように、パラメータを反映する処理を追加しましょう。

 # include <Siv3D.hpp>
 # include <NocoUI.hpp>

 void Main()
 {
     // NocoUIライブラリを初期化
     noco::Init();
     
     BrickBreaker game;

     // Canvasを読み込み
     auto canvas = noco::Canvas::LoadFromFile(U"game.noco");

     while (System::Update())
     {
         game.update();
+
+        canvas->setParamValues({
+            { U"isGameOver", game.isGameOver() },
+            { U"life1", game.life() >= 1 },
+            { U"life2", game.life() >= 2 },
+            { U"life3", game.life() >= 3 },
+            { U"score", ToString(game.score()) },
+        });
         canvas->update();
 
         game.draw();
         canvas->draw();
     }
 }

パラメータ設定はCanvasのupdate呼び出しの手前に書きます。
なお、scoreパラメータはString型のためToString関数で文字列変換する必要がある点に注意しましょう。

これで、ゲームの状況がUIに反映されるようになりました!

素材を用意して見た目を変えてみる

以下のような画像素材を用意してみました。
これらを使って、見た目を変えてみましょう。

score_heading.png life_heading.png life.png
game_over.png
score_num.png

基本的な画像表示にはSpriteコンポーネント、画像をビットマップフォントとして文字表示する場合にはTextureFontLabelコンポーネントを使用します。

image.png

  • ScoreHeading
    • Spriteコンポーネントに付け替え、以下を設定
      • textureFilePath: "score_heading.png"
  • ScoreNumber
    • posDelta: (-240, -270) へ変更
    • TextureFontLabelコンポーネントに付け替え、以下を設定
      • textプロパティにscoreパラメータへの参照を指定
      • characterSize: (28, 36)
      • horizontalAlign: Center
      • verticalAlign: Middle
      • horizontalOverflow: Overflow
      • characterSet: "0123456789"
      • textureFilePath: "score_num.png"
      • textureCellSize: (28, 36)
      • textureGridColumns: 1
      • textureGridRows: 10
  • LifeHeading(追加)
    • size: (100, 40)
    • posDelta: (168, -270)
    • Spriteコンポーネントを追加し、以下を設定
      • textureFilePath: "life_heading.png"
  • Life1Life2Life3
    • Spriteコンポーネントに付け替え、以下を設定
      • textureFilePath: life.png
  • GameOver
    • Spriteコンポーネントに付け替え、以下を設定
      • textureFilePath: game_over.png

※ 完成品のnocoファイルは以下からダウンロードできます。
https://gist.github.com/m4saka/b60b30555cc1756f5eb57a2f51e9b846

これで以下のように、よりデジタル感のある見た目になりました!

ここで重要なポイントは、見た目変更のために一切ソースコードを変更する必要がなかった点です。

パラメータ定義さえ変更しないようにしておけば、ソースコードに手を加えることなく自由に見た目を変更することができます。

そのため、あらかじめパラメータ定義だけ決めておいて後はグラフィック担当者がUIの見た目を作成する、といった完全分業も可能になります。

まとめ

Siv3D用ゲームUIライブラリ「NocoUI」を紹介しました。

  • ノード(Node) とは、位置とサイズを持つ要素のこと
  • ノードに対して コンポーネント(Component) を追加することで、さまざまな見た目にすることができる
  • ビジュアルエディタ(NocoEditor)上で編集したUIを.nocoファイルとして書き出して利用する
    • 座標などをソースコード上に定数化する必要がなくなる
    • 実行時に読み込むため、編集時に再コンパイルが必要ない
  • パラメータ(Params) 機能を使えば、プログラム上からUIへ値を反映できる
    • コンポーネントのプロパティに対してパラメータ参照を指定すると、自動的にパラメータの値が反映される
    • パラメータ定義への変更さえなければ、ソースコードへの変更ゼロでゲーム全体の見た目を変更することが可能
10
0
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
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?