コンテンツにスキップ

講師について

鈴木 遼 (すずき りょう)

  • C++ フレームワーク「Siv3D(シブスリーディー)」開発者。博士(工学)
  • 著書に C++ 入門書「冒険で学ぶ はじめてのプログラミング」(技術評論社)
  • 2013 年度 IPA 未踏、2022 年度 IPA 未踏アドバンスト

オープンソース活動

  • Siv3D | ゲームやメディアアートのための C++20 フレームワーク
  • siv::PerlinNoise | C++17 で実装された Perlin ノイズ
  • Xoshiro-cpp | C++17 で実装された Xoshiro 疑似乱数生成器
  • notebookjp | 低予算で高性能なノート PC の紹介サイト

イベントでの発表

Siv3D について

  • 2D/3D ゲームやアプリケーションを C++ コードで開発できるフレームワーク。
  • 思いついたアイデアを最短のコードと時間で形にすることを目的に開発。
  • Steam などで、ユーザによる Siv3D 製ゲームが公開されている。
2024 年に Steam で発表された Siv3D 製のゲーム

  • 昨年の全国高等専門学校プログラミングコンテスト(高専プロコン)競技部門で、優勝・準優勝・3 位のすべてのチームが大会で Siv3D を使用。
  • 大学におけるプログラミング授業や研究開発、ゲーム会社における研修、画像処理技術に関連するスタートアップ企業など、さまざまな場面で Siv3D の活用が広がる。
  • バンダイナムコスタジオと共催のゲームジャム(ゲーム制作イベント)
  • 特定の領域に絞れば、Siv3D で開発するのが最も生産性が高いと考えている。その領域を広げていくことに現在取り組んでいる。

共有できる知見

  • C++ の最新技術を使ったプログラミング
  • ユーザが成果を出せるライブラリやフレームワークを作ること
  • オープンソースソフトウェアのユーザコミュニティを運営すること
    • 60 人以上のコミッタ。全国 30 か所以上で Siv3D 勉強会を開催
  • オープンソースソフトウェアでお金を集めること
    • IPA 未踏、文科省や学内の研究費、助成金、寄付等で 3,000 万円以上を集める

最近 Siv3D で作ったもの

AI 絵しりとり

コード
# include <Siv3D.hpp>

void Main()
{
	// ウィンドウを 1280x720 にリサイズする
	Window::Resize(1280, 720);

	// 背景色を設定する
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// フォントを用意する
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
	const Font font2 = Font{ FontMethod::MSDF, 40, Typeface::Heavy, FontStyle::Italic }.setBufferThickness(4);

	// 環境変数から API キーを取得する
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// キャンバスの位置とサイズ
	constexpr Point CanvasOffset{ 100, 80 };
	constexpr Size CanvasSize{ 512, 512 };

	// ペイント用の画像
	Image image{ CanvasSize, Palette::White };

	// ペイント用の画像からテクスチャを作成する
	DynamicTexture texture{ image };

	// 非同期タスク
	AsyncHTTPTask task;

	// しりとりの履歴
	Array<String> answers;

	// 現在のしりとりの文字
	char32 ch = Random(U'A', U'Z');

	// 履歴のスクロール関連
	int32 scrollState = 0;
	Stopwatch scrollStopwatch;
	Stopwatch scoreStopwatch{ 1.0s };

	// スコア
	int32 score = 0;

	while (System::Update())
	{
		// 背景の市松模様を描画する
		for (int32 y = 0; y < (720 / 40); ++y)
		{
			for (int32 x = 0; x < (1280 / 40); ++x)
			{
				if (IsEven(x + y))
				{
					Rect{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.6, 0.8, 0.7 } * 0.965);
				}
			}
		}

		// お絵描き
		if (MouseL.pressed())
		{
			const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
			const Point to = Cursor::Pos();
			Line{ from, to }.movedBy(-CanvasOffset).overwrite(image, 6, Color{ 0 });
			texture.fill(image);
		}

		// 送信ボタンが押されたら
		if (SimpleGUI::Button(U"判定", Vec2{ (CanvasOffset.x + 100), 620 }, 120,
			(not task.isDownloading()))) // タスクの実行中でないときだけボタンを有効にする
		{
			// 画像に関するリクエストを作成する
			OpenAI::Vision::Request request;
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
			request.questions = U"What is depicted in this image? The answer starts with the letter {}."_fmt(ch);
			request.questions += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// タスクを作成する
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// クリアボタンが押されたら
		if (SimpleGUI::Button(U"クリア", Vec2{ (CanvasOffset.x + CanvasSize.x - 120 - 100), 620 }, 120))
		{
			// 画像をクリアする
			image.fill(Palette::White);
			texture.fill(image);
		}

		// 画像を描画する
		RoundRect{ CanvasOffset, CanvasSize, 10 }(texture).draw(ColorF{ 0.99, 0.98, 0.97 })
			.drawFrame(3, 0, Arg::top(0.5, 0.5), Arg::bottom(0.5, 0.0))
			.drawFrame(1, 10, ColorF{ 0.6, 0.4, 0.2 });

		// しりとりの文字を表示する
		Circle{ CanvasOffset.movedBy(30, 30), 70 }
			.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
			.draw(ColorF{ 0.8, 0.9, 1.0 })
			.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });
		font(ch).drawAt(70, CanvasOffset.movedBy(30, 30), ColorF{ 0.11 });

		// 非同期処理が完了し、正常なレスポンスである場合
		if (task.isReady() && task.getResponse().isOK())
		{
			// 非同期処理の結果を取得する
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// 結果の配列に追加する
			answers << answer;

			// 正解したら
			if (answer != U"?")
			{
				// しりとりの文字を更新する
				ch = answer.back();
				++score;
				scoreStopwatch.restart();
			}
		}

		if ((not scrollStopwatch.isRunning())
			&& (((scrollState == 0) ? 7 : 8) <= answers.size()))
		{
			scrollStopwatch.start();
			scrollState = Min(scrollState + 1, 2);
		}

		// しりとりのスクロールアニメーション
		if (scrollState == 1)
		{
			if (1.0s <= scrollStopwatch)
			{
				scrollStopwatch.pause();
				scrollStopwatch.set(1s);
			}
		}
		else if (scrollState == 2)
		{
			if (2.0s <= scrollStopwatch)
			{
				answers.pop_front();
				scrollStopwatch.pause();
				scrollStopwatch.set(1s);
			}
		}

		// しりとりの履歴の表示
		{
			const double t = Min(scrollStopwatch.sF(), 2.0);
			const double t1 = static_cast<int32>(t);
			const double t2 = t - t1;
			const double e = EaseInOutExpo(t2);
			const double yOffset = ((t1 + e) * 80.0);

			for (const auto& [i, answer] : Indexed(answers))
			{
				// 1 文字目
				const Vec2 pos{ 700, (80 + i * 80 - yOffset) };
				Circle{ pos, 32 }.draw(ColorF{ 0.8, 0.9, 1.0 });
				font(answer.front()).drawAt(46, pos, ColorF{ 0.11 });

				// 1 文字目以降
				font(answer.substr(1)).draw(46, Vec2{ 736, (47.5 + i * 80 - yOffset) }, ColorF{ 0.11 });
			}

			// 次の文字
			{
				const Vec2 pos{ 700, (80 + answers.size() * 80 - yOffset) };
				Circle{ pos, 32 }.draw(ColorF{ 0.8, 0.9, 1.0 });
				font(ch).drawAt(46, pos, ColorF{ 0.11 });

				// AI の応答を待つ間は回転する円を表示する
				if (task.isDownloading())
				{
					Circle{ pos, 42 }.drawArc((Scene::Time() * 240_deg), 300_deg, 5, 2, ColorF{ 0.8, 0.9, 1.0 });
				}
			}
		}

		// スコアの表示
		{
			const double s = ((0.5 - Min(scoreStopwatch.sF(), 0.5)) * 2.0);
			const double e = EaseInOutExpo(s);
			const Vec2 center = font2(score).region(140, Arg::topRight(1185, 15)).center();
			const Transformer2D transformer{ Mat3x2::Scale((1.0 + e * 0.4), center) };
			font2(score).draw(TextStyle::OutlineShadow(0.2, ColorF{ 1.0 }, Vec2{ 2, 2 }, ColorF{ 0.0, 0.5 }), 140,
				Arg::topRight(1185, 15), ColorF{ 1.0, 0.6, 0.1 });
		}
	}
}

漢字 Wordle

ゲーム UI の研究

日本地図を原神のマップ風に変換するプログラム

ゼンレスゾーンゼロの UI の分解と再現

崩壊:スターレイルの UI の分解と再現