98 行でシューティングゲームを作る (C++/Siv3D)

短いコードにゲームのエッセンスを詰め込むのは俳句に似た楽しさがあります。
98 行 (58 論理行) でシューティングゲームを作ってみました。
hamukun8686 さんの Qiita | STGを200行以内で@280行 へのアンサーです。

目標

  • 疾走感のある背景
  • 難易度アップ
  • ハイスコア
  • ゲームオーバーのエフェクト

条件

  • 98 行のプログラムのみ。画像ファイルは使わない
  • 詰め込みすぎて読みにくいコードにしない

背景の空はプロシージャル生成しています。
時間の経過に伴い難易度が上がり、敵の数が増えていきます。

Siv3D March 2014 がインストールされていれば、以下のコードのコピペで動きます。

ゲームとメディアアートのための C++ ライブラリ「Siv3D」のこれまでとこれから

C++ Advent Calendar 2013 および Siv3D Advent Calendar 2013, 13 日目の記事です。

「Siv3D」C++ で楽しく簡単にゲームやメディアアートを作ることを目的としたライブラリです。2008 年から開発を始め、2012 年に α 版を公開、今年 9 月には IPA 未踏事業に採択 され、開発を加速しています。

設計方針

C++ の先進的な機能を取り入れ、コードの読みやすさ・書きやすさと、パフォーマンス、機能性を重視しています。
最新版の Siv3D December 2013 は 10 月にリリースされたばかりの Visual Studio 2013 に対応し、ライブラリからチュートリアル、リファレンスのすべてが現代的な C++ で書かれています。(最新の C++ の機能について知りたい方は こちら

サンプル [1/6]

# include <Siv3D.hpp>

void Main()
{
	const Font font(30);

	while (System::Update())
	{
		Circle(Mouse::Pos(), 100).draw();

		font.draw(Format(Mouse::Pos()), 50, 200, Palette::Orange);
	}
}

サンプル [2/6]

# include <Siv3D.hpp>

void Main()
{
	const Texture texture(L"Example/Windmill.png");

	while (System::Update())
	{
		// 位置 (100,50) にテクスチャを描く
		texture.draw(100, 50);
	}
}

サンプル [3/6]

# include <Siv3D.hpp>

void Main()
{
	Image image{ 640, 480, Palette::White };

	DynamicTexture texture{ image };

	while (System::Update())
	{
		if (Input::MouseL.pressed)
		{
			const Point pos = Input::MouseL.clicked ?
				Mouse::Pos() : Mouse::PreviousPos();

			Line(pos, Mouse::Pos()).write(image, 8, Palette::Blue);

			texture.fill(image);
		}

		texture.draw();
	}
}

サンプル [4/6]


Point, Rect, Circle, Line, Triangle, Quad, Polygon 等さまざまな図形同士であたり判定ができます。

# include <Siv3D.hpp>

void Main()
{
	const Rect rect(20, 20, 200, 100);

	const Circle circle(150, 300, 100);

	const Polygon star
	{
		{ 430, 100 }, { 470, 240 },
		{ 610, 240 }, { 505, 325 },
		{ 545, 460 }, { 430, 380 },
		{ 315, 460 }, { 355, 325 },
		{ 250, 240 }, { 390, 240 }
	};

	while (System::Update())
	{
		const Circle player(Mouse::Pos(), 30);

		const bool r = player.intersects(rect);

		const bool c = player.intersects(circle);

		const bool s = player.intersects(star);

		rect.draw(r ? Palette::Red : Palette::Yellow);

		circle.draw(c ? Palette::Red : Palette::Yellow);

		star.draw(s ? Palette::Red : Palette::Yellow);

		player.draw();
	}
}

サンプル [5/6]

# include <Siv3D.hpp>

void Main()
{
	if (!Kinect::IsConnected())
	{
		return; // Kinect の接続チェック
	}

	if (!Kinect::Start())
	{
		return; // Kinect の起動
	}

	DynamicTexture texture;

	optional<Skelton> skelton;

	while (System::Update())
	{
		if (Kinect::HasNewDepth())
		{
			Kinect::GetDepthTexture(texture);
		}

		if (const auto s = Kinect::GetSkelton())
		{
			skelton = s.get();
		}

		texture.draw();

		if (skelton)
		{
			for (const Vec2& p : skelton.get().screenPositions)
			{
				Circle(p, 10).draw();
			}
		}
	}
}

サンプル [6/6]


83 行の C++ プログラムで音楽プレイヤーを作ることができます。
コードは Qiita | Siv3D で音楽プレイヤーを作る で紹介しています。

ダウンロードして 3 秒で始められる

Visual Studio 2013 をインストールしていれば、Siv3D のインストーラを実行するだけで、すぐに開発を始めることができます。
Visual Studio の「新しいプロジェクト」メニューに項目が追加される

使い方を学ぶ

入門サイト 「Play Siv3D!」 では Siv3D のチュートリアル、リファレンスなどのコードサンプルを 200 以上用意しています。
まだ準備中の部分もありますが、2014 年春までには一通りのドキュメントを完成させる予定です。

ゲーム開発で使われる Siv3D


Siv3D を使って制作したゲームが サークル WCE で公開 されています。
今年の冬コミでも 7 つの新作ゲームが販売される予定です。
そのほかデジゲー博や、ニコニコ動画 でも Siv3D を利用した作品が登場しています。

今後のアップデート

今月末のアップデートでは以下の機能を追加する予定です。
・3D 描画を正式機能に格上げ、サンプルを追加

・手書き文字認識

・GIF 画像保存時にディザリングの有無の指定
GUIラジオボタンを追加

2014 年には
・3D グラフィックスの強化

物理エンジンとの連動
スクリプト対応
マルチプラットフォーム対応(Mac OS X, Android
などのアップデートを予定しています。

Siv3D のこれから

国内では HSP や DX ライブラリの後継になり、海外の C++ プログラマやプログラミング入門者にも使ってもらえるライブラリを目指しています。
Siv3D を使って初めてプログラミングをする世界中の子どもたちに「プログラミングってこんなに面白いんだ!」「C++ ってすげー!」と思ってもらうのが夢です。

Siv3D のダウンロード

ダウンロードからサンプル実行までの手順を Play Siv3D! | Siv3D を使う準備 で紹介しています。Enjoy Siv3D!


明日の C++ Advent Calendar は @USAGI_WRP さんです。よろしくお願いします!
Siv3D Advent Calendar は引き続き 14 日連続で僕がお送りします。

Siv3D December 2013 リリース | おすすめ機能

昨日 Siv3D December 2013 をリリースしました。5 か月ぶりの安定版です。
たくさんの更新内容 の中で、僕が特に気に入ってる 8 つのトピックを紹介します。

インストール時間が短くなった

June 2013 で Visual Studio プロジェクトテンプレートを導入、そして今回簡易インストーラを導入することで、セットアップの手間を 3 秒に短縮しました。
ユーザーができるだけ早く Siv3D を遊べることを目標に、さらに短くできる可能性を追求していきます。

もっと C++11 に対応

Visual Studio 2013 が対応した C++11 の新機能を活用して、より書きやすく読みやすいコードのために API デザインを一新しました。
いくつか例を紹介します。

Uniform initialization によって Vec2 や Color を省略できるようになりました。

	Triangle triangle(Vec2(10, 10), Vec2(30, 30), Vec2(10, 30));

	triangle.draw(Color(0, 255, 0));
	Triangle triangle({ 10, 10 }, { 30, 30 }, { 10, 30 });

	triangle.draw({ 0, 255, 0 });

Initializer list を使って Polygon を直接構築できるようになりました。

	// 頂点の配列
	const Vec2 pts[] =
	{
		Vec2(0,-200),Vec2(40,-60),
		Vec2(180,-60),Vec2(75,25),
		Vec2(115,160),Vec2(0,80),
		Vec2(-115,160),Vec2(-75,25),
		Vec2(-180,-60),Vec2(-40,-60)
	};

	// 頂点の配列から多角形を作成
	const Polygon star(pts);
	const Polygon star
	{
		{ 0, -200 }, { 40, -60 },
		{ 180, -60 }, { 75, 25 },
		{ 115, 160 }, { 0, 80 },
		{ -115, 160 }, { -75, 25 },
		{ -180, -60 }, { -40, -60 }
	};

Variadic templates のおかげで Format がスタイリッシュになりました。

String text = Format() + L"aaa" + (1 + 1) + Vec2(20, 20) + L'b';
String text = Format(L"aaa", 1 + 1, Vec2(20, 20), L'b');

FFT

再生・録音している音声のスペクトルを解析できるようになりました。音楽に使えばオーディオビジュアライザーや曲調に合わせた演出、マイクに使えば音声に応じたインタラクティブなアプリケーションを開発できます。( サンプル | マイク入力のスペクトラム )
正確な音の高さの口笛を吹く音ゲーを作るのとか面白そうな気がします。

フルスクリーン・ウィンドウの切り替え

実行中にフルスクリーンモード・ウィンドウモードを切り替えられるようになりました。
フルスクリーンモードが対応する解像度は環境によって違うので

std::vector<Point> Graphics::GetFullScreenSize()

関数で、利用可能な解像度を調べておきましょう。

アセット管理

August 2013 beta まで「Resource」という名前の実験的機能でしたが、正式機能に格上げされました。( リファレンス | アセット管理 )
プログラムのどこからでも名前だけで Texture や Sound, Font などのアセットデータを取得できるので、ゲームのように大量のアセットを使うアプリケーションの開発が楽になります。
アプリの実行中に画像やサウンドファイルを変更すると即座に実行中のアプリに反映される機能も開発のスピードを加速させます。

プログラミング入門者向けの簡単な文法

Siv3D で入門者にプログラミングの楽しさを教えられることが目標の 1 つです。
今回の Siv3D には Print / Println / WaitKey という 3 つの強力な関数が追加されました。
これを使えば、メインループや Font のことを考えずに Hello world を書けます。

# include <Siv3D.hpp>

void Main()
{
	Println(L"Hello, world!");

	WaitKey();
}

Print / Println は画面の最前面にテキストを表示する、Siv3D 版の printf や std::cout です。通常の開発時にもデバッグ出力として利用できます。

# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		if (Input::MouseL.clicked)
		{
			Println(L"クリックした座標: ", Mouse::Pos());
		}
	}
}

WaitKey の内部では特殊なメインループを実行していて、直前に描画、サウンド再生を入れることもできます。

# include <Siv3D.hpp>

void Main()
{
	Circle(100, 100, 50).draw();

	const Sound sound = Dialog::OpenSound();

	sound.play();

	WaitKey();
}

図形描画の機能拡張

アーチ型、LineStrig の頂点の整形、スプライン曲線、グラデーション付き図形といった機能を追加しました。
図形描画はテクスチャ描画に比べて、メモリ消費が少なく描画速度も速いといった利点があるので、なるべく多くのことを図形描画でできるようにしていきたいです。

サウンドファイルの機能拡張

32bit float WAVE や、特別なヘッダの入った WAVE ファイルが読み込めなかったバグ、一部の MP3 のデコードで曲の最後にノイズが入るバグなど、サウンドファイル関連の問題を修正しました。
出力については新しく MP3(Windows 8 以降) と WMA(Windows 7 以降) の保存に対応し、WAVE, OggVorbis, AAC と合わせて主要なオーディオフォーマットをほぼカバーしました。

December 2013 の新機能についてはまだ紹介しきれていないことが多いので、今後ブログで解説記事を増やしていきます。

「ゲーム開発者のための C++11 / C++14」を公開しました

全日本学生ゲーム開発者連合(全ゲ連)第 14 回交流会 での講演「ゲーム開発者のための C++11 / C++14」を SlideShare にアップロードしました。

ゲーム開発者のための C++11/C++14

SlideShare のページから PDF 版をダウンロードできます。SlideShare アカウントを持っていない場合は 全ゲ連 wiki からダウンロードしてください。

当日、全ゲ連会場まで講演を聴きにきてくれたみなさんに感謝します。

全ゲ連で講演します

9/14(土)に 全日本学生ゲーム開発者連合(全ゲ連)第 14 回交流会 にて、「ゲーム開発者のための C++11 / C++14」というテーマで講演します。
Visual Studio 2010, 2012, 2013 で使えるようになった機能を中心に、最新の C++ でゲーム開発をするためのガイドラインを紹介する予定です。
制作中のスライドが 100 ページを超えているので、時間内に収まるかが少し心配です。

スライドは講演後に slideshare にアップロードします。

Perlin Noise を C++ で実装

プロシージャルテクスチャの作成に便利な、Ken Perlin の ImprovedNoiseC++11 で実装しました。https://github.com/Reputeless/PerlinNoise

リファレンス実装には無い、乱数シード設定、1D/2D ノイズ、オクターブノイズといった機能を追加しています。

C++11 は std::iota, std::begin, std::mt19937, std::shuffle を使用。
Visual Studio 2012 で動作を確認しました。

PerlinNoise::PerlinNoise( unsigned seed )
{
	// Precondition: 0 < seed
	if(seed==0)
	{
		seed = std::mt19937::default_seed;
	}

	// p[0]..p[255] contains all numbers in [0..255] in random order		
	std::iota(std::begin(p),std::begin(p)+256,0);

	std::shuffle(std::begin(p),std::begin(p)+256,std::mt19937(seed));

	for(int i=0; i<256; ++i)
	{
		p[256+i] = p[i];
	}
}

Sample.cpp は Photoshop の雲模様フィルタのような画像を作成します。

コンソールウィンドウでマンデルブロ集合

Windows のコンソールウィンドウにマンデルブロ集合を表示します。


マウスでぐりぐり動かして、[i] / [o] キーでズームイン / アウト。
暇があればマルチサンプリングで表示を改善したり、コードを整備したりする予定です。

Visual C++ 2010 / Visual Studio 2012 で動作します。実行ファイル (64kB)

//
//	Mandelbrot Console
//	Version : 1.0
//	Author : @Reputeless, @agehama_
//
# include <string>
# include <Windows.h>
# include <conio.h>

struct Vec2
{
	double x,y;
	Vec2(){}	
	Vec2( double _x, double _y ): x(_x),y(_y){}
};

int Mandelbrot( double a, double b )
{
	double x=0.0, y=0.0;

	for(int n=0; n<300; ++n)
	{
		const double x1 = x*x - y*y +a;
		const double y1= 2.0*x*y + b;
		
		if(x1*x1 + y1*y1 > 4.0)
		{
			return n; // 発散
		}

		x = x1;	
		y = y1;
	}

	return 0;
}

void CreateOutput( int width, int height, std::string& buffer, const Vec2& center, const Vec2& range )
{
	const Vec2 pixelSize(range.x/width,range.y/height);
	const Vec2 beginPos(center.x-(pixelSize.x*width/2),
						center.y-(pixelSize.y*height/2));

	for(int y=0; y<height; ++y)
	{
		const double py = beginPos.y + pixelSize.y*y;

		for(int x=0; x<width; ++x)
		{
			const double px = beginPos.x + pixelSize.x*x;
			buffer[y*width+x] = Mandelbrot(px,py) ? '*' : ' ';
		}
	}
}

// マウスカーソルの位置 [0.0,1.0] を返す
Vec2 GetMousePos()
{
	RECT rc;
	::GetWindowRect(::GetForegroundWindow(),&rc);

	POINT pos;
	::GetCursorPos(&pos);
	return Vec2(1.0*pos.x/(rc.right-rc.left),1.0*pos.y/(rc.bottom-rc.top));
}

class Console
{
public:

	Console()
		:	m_center(0.0,0.0),m_scale(4.0),m_aspect(1.0)
	{
		::SetConsoleTitleW(L"Mandelbrot Console / zoom [i]n / zoom [o]ut / [q]uit");

		m_console = ::GetStdHandle(STD_OUTPUT_HANDLE);

		COORD size = ::GetLargestConsoleWindowSize(m_console);
		::SetConsoleScreenBufferSize(m_console,size);

		SMALL_RECT dw = {0,0,size.X-2,size.Y-1};
		::SetConsoleWindowInfo(m_console,TRUE,&dw);
	
		m_width = size.X;
		m_height = size.Y-2;

		m_aspect = 1.0*m_width/m_height*0.4;

		::SetConsoleScreenBufferSize(m_console,size);

		m_output.resize(m_width*m_height,' ');
	}

	~Console()	{ ::CloseHandle(m_console); }

	void draw()
	{
		const Vec2 mousePos = GetMousePos();
		m_center.x += (mousePos.x-0.5)*m_scale*0.1*m_aspect;
		m_center.y += (mousePos.y-0.5)*m_scale*0.1;

		CreateOutput(m_width,m_height,m_output,m_center,Vec2(m_scale*m_aspect,m_scale));

		COORD cp = {0,0};
		DWORD n;		
		::WriteConsoleOutputCharacterA(m_console,m_output.c_str(),m_output.length(),cp,&n);
	}

	void zoomIn()	{ m_scale *= 0.75; }

	void zoomOut()	{ m_scale /= 0.75; }

private:

	HANDLE m_console;

	int m_width, m_height;

	std::string m_output;

	Vec2 m_center;

	double m_scale, m_aspect;
};

int main()
{
	Console console;

	for(;;)
	{
		console.draw();

		if(_kbhit())
		{
			switch(_getch())
			{
			case 'i': console.zoomIn(); break;
			case 'o': console.zoomOut(); break;
			case 'q': return 0;
			}
		}

		::Sleep(80);
	}
}