No.1 C++で円を動かす

2025年9月28日

記念すべき第一回はC++で円を動かすことです。基本的に今一番やりたいこと、記事に書きたいことを書いていくことになりますが、はじまりといったらC++です。C++はパソコンさえあれば誰でも扱え、楽しめるものです。勉強するのは苦行かもしれませんが、そのまま打ち込んでその通りになるというのは、達成感があって楽しいものです。あれを変えてみようと思って変えて遊ぶのも一興です。自分はなるべく短い時間で書いていきたいので、長くなる場合は端折ります。おそらくC++の使い方に関しては今回は書けないでしょう。その通りに打ち込んで円を動かすことを目的としてもらえれば幸いです。では、始めましょう。


ウィンドウ生成

環境は「Visual Studio Community 2022」を使います。起動させたら、[新しいプロジェクトの作成]→言語選択から[C++]→[Windows デスクトップウィザード]をクリックします。

画像

好きなプロジェクト名、保存場所を選び[作成]をクリックし、アプリケーションの種類を[デスクトップアプリケーション]、[空のプロジェクト]にチェックを入れて[OK]をクリックしましょう。

プロジェクトが作成されたら、ソリューションエクスプローラーのソースファイルを右クリックして、新しいファイルを追加します。自分はいつも複数ファイルを作るのですが、今回はわかりやすいように一つのファイルにすべて書きます。次の内容を一気に書きましょう。

main.cpp
#include <windows.h>

const int WIDTH = 640;
const int HEIGHT = 480;
const wchar_t TITLE[] = L"円を動かす";

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

HWND m_hwnd;

int WINAPI wWinMain(_In_ HINSTANCE hInst, _In_opt_ HINSTANCE hPrev, _In_ PWSTR pCmd, _In_ int nCmd)
{
	WNDCLASS wc = {};
	wc.lpfnWndProc = WindowProc;
	wc.hInstance = hInst;
	wc.lpszClassName = L"Window Class";
	RegisterClass(&wc);

	RECT rc = { 0, 0, WIDTH, HEIGHT };
	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
	m_hwnd = CreateWindowEx(0, wc.lpszClassName, TITLE,
		WS_OVERLAPPEDWINDOW ^ WS_SIZEBOX ^ WS_MAXIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
		NULL, NULL, hInst, NULL);
	if (m_hwnd == NULL) return 0;

	ShowWindow(m_hwnd, nCmd);

	MSG msg = {};
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessageW(&msg);
	}

	return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProcW(hwnd, msg, wParam, lParam);
}

[デバッグなしで実行]を押すと幅640px、高さ480pxのウィンドウが描かれるはずです。一見すると長いコードですが、ある程度入力した後に予測変換が出てくるので、それを選択すれば短縮できます。ここで大事なのは、WIDTHでウィンドウの幅、HEIGHTで高さ、TITLEでウィンドウのタイトルを指定していることです。プログラムの内容を知らなくても、WIDTHに幅、HEIGHTに高さ、TITLEにタイトルを入れていることを知っていれば、それを変更して遊ぶことができます。実際に使っているのはwWinMain関数内のCreateWindowEx関数の引数の中です。引数でタイトル、幅、高さを指定する部分があり、そこに変数を入れています。

ページの調整をしていたら時間が結構経ってしまったので、ほいほいいきます。


Direct2Dを使った円の描画

次が初めてDirect2Dを使う人の鬼門になるところです。Direct2DはGPUを使った高速描画ができるのが特徴です。スムーズに動くゲームを作る人にとっては避けて通れない道です。とりあえず、次の内容をバッと打ち込みましょう。

main.cpp
#include <windows.h>
#include <d2d1.h>

#include <atlbase.h>

#pragma comment(lib, "d2d1")

using D2D1::ColorF;

const int WIDTH = 640;
const int HEIGHT = 480;
const wchar_t TITLE[] = L"円を動かす";

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

HRESULT CreateResources();
void OnPaint();

HWND m_hwnd;

CComPtr<ID2D1Factory> factory;
CComPtr<ID2D1HwndRenderTarget> rt;
CComPtr<ID2D1SolidColorBrush> brush;

int WINAPI wWinMain(_In_ HINSTANCE hInst, _In_opt_ HINSTANCE hPrev, _In_ PWSTR pCmd, _In_ int nCmd)
{
	≀
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
	case WM_CREATE:
		if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory)))
		{
			return -1;
		}
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	case WM_PAINT:
		OnPaint();
		return 0;
	}
	return DefWindowProcW(hwnd, msg, wParam, lParam);
}

HRESULT CreateResources()
{
	HRESULT hr = S_OK;
	if (rt == NULL)
	{
		D2D1_SIZE_U size = D2D1::SizeU(WIDTH, HEIGHT);
		hr = factory->CreateHwndRenderTarget(
			D2D1::RenderTargetProperties(),
			D2D1::HwndRenderTargetProperties(m_hwnd, size), &rt);
		if (SUCCEEDED(hr))
		{
			hr = rt->CreateSolidColorBrush(ColorF(ColorF::Black), &brush);
		}
	}
	return hr;
}

void OnPaint()
{
	HRESULT hr = CreateResources();
	if (SUCCEEDED(hr))
	{
		PAINTSTRUCT ps;
		BeginPaint(m_hwnd, &ps);
		rt->BeginDraw();

		rt->Clear(ColorF(ColorF::AliceBlue));

		rt->EndDraw();
		EndPaint(m_hwnd, &ps);
		InvalidateRect(m_hwnd, NULL, FALSE);
	}
}

結果は次のようになります。

画像

ウィンドウが生成され、背景が薄い青で塗られているのがわかります。いきなり円を描くのではなく、まずは背景を塗って、ちゃんとDirect2Dが動作しているのかを確かめることが大事です。おそらく、初めてDirect2Dを使うときはウィンドウが生成されないかもしれません。一文字でも間違うと動作しないので、頑張って間違いを探しましょう。ちなみに、何回も書いて慣れれば何も見ずにウィンドウの生成と円を動かすまでのプログラムが書けるようになります(実体験)。

今のところ重要なプログラムは、OnPaint関数にあるrt->Clear()の部分です。引数に書いた色により、背景をその色で塗ることができるのです。今回はAliceBlueなので薄い青になります。色の指定には、定義済みの色を指定する他に、赤、緑、青それぞれの色を指定するやり方、RGB値でしてするやり方があります。それぞれの色を指定するやり方は面倒なので、定義済みの色かRGB値(0xffffffなど)で指定するのがおすすめです。定義済みの色はここのページ、自由な色の指定はここのページが便利です。

では、円を描画しましょう。OnPaint関数の背景を塗る関数の下に次のように書きます。

main.cpp
rt->Clear(ColorF(ColorF::AliceBlue));

D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(WIDTH / 2, HEIGHT / 2), 20, 20);
brush->SetColor(ColorF(ColorF::Blue));
rt->FillEllipse(ellipse, brush);

大事なのは、Ellipse関数のPoint2Fの引数でxの位置、yの位置を指定し、後ろの引数で半径を指定していることです。最後に、この円を方向キーで動かせるようにして終わりましょう。


円を方向キーで動かす

ここからはシンプルになります。次のコードを書きましょう。

main.cpp
    ≀
const int WIDTH = 640;
const int HEIGHT = 480;
const wchar_t TITLE[] = L"円を動かす";

bool keyRight = false, keyLeft = false, keyUp = false, keyDown = false;
    ≀
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
        ≀
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	case WM_KEYDOWN:
		if (wParam == VK_RIGHT) keyRight = true;
		if (wParam == VK_LEFT) keyLeft = true;
		if (wParam == VK_UP) keyUp = true;
		if (wParam == VK_DOWN) keyDown = true;
		return 0;

	case WM_KEYUP:
		if (wParam == VK_RIGHT) keyRight = false;
		if (wParam == VK_LEFT) keyLeft = false;
		if (wParam == VK_UP) keyUp = false;
		if (wParam == VK_DOWN) keyDown = false;
		return 0;
        ≀
}
    ≀
void OnPaint()
{
	HRESULT hr = CreateResources();
	if (SUCCEEDED(hr))
	{
		≀

		rt->Clear(ColorF(ColorF::AliceBlue));

		if (keyRight) x += 3;
		if (keyLeft) x -= 3;
		if (keyUp) y -= 3;
		if (keyDown) y += 3;

		D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), 20, 20);
		brush->SetColor(ColorF(ColorF::Blue));
		rt->FillEllipse(ellipse, brush);
            ≀
	}
}

やっていることは、キー入力のイベントを登録して、方向キーを押したときにキーがtrueになり、もしtrueなら位置を動かすという処理です。ここで変えて遊べる点としては、円の移動速度を変えるくらいですね。下のほうにある3の値を変更すると円のスピードが変わります。


おわりに

以上となります。一応時間制限を設けてやっていたので、これ以上は書けません。書くとしたら別の機会になります。C++のことを知っている前提みたいになってしまいました。変更できる箇所はわかっても、他のところはちんぷんかんぷんでしょう。それを説明すると何回かに分けることになり、円を動かすまでに断念することになり兼ねません。なので、変えられるところだけ知るというやり方はありです。ただ、それだと応用が利きづらいですね。まあ、C++で円を動かすという目的は達成できたので良しとしましょう。そうですね、毎回目的に沿って時間を決めてやっていくことにします。あくまで目的の達成だけを目指すとしましょう。そうでないと一回で終わらなくなってしまいます。とりあえずはそのまま受けとってほしいです。おそらく、C++で別のことをする際もだいぶ端折ることになります。まあ、目的の達成を第一目標に記事を書いていきます。