---
title: Windowsのゲームの作り方
tags: 
author: [風岡秀明](https://convert.docswell.com/user/Kazaoka)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/YE9929W4E3.jpg?width=480
description: C++breaktime 2025/Winter で発表したものです Breakoutのソースコードは https://github.com/Kazaoka にあります
published: December 21, 25
canonical: https://convert.docswell.com/s/Kazaoka/5DWRJL-2025-12-21-205408
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/YE9929W4E3.jpg)

Windowsのゲームの作り方
初心に帰って作ってみました。


# Page. 2

![Page Image](https://bcdn.docswell.com/page/GE8959NV7D.jpg)

自己紹介
風岡秀明（かざおか ひであき）
● 大手ゲーム会社で C++ を中心に開発
● 週末はのんびりスケートボード
● たまに元ジャニの推し活に付き合う（嫌々）


# Page. 3

![Page Image](https://bcdn.docswell.com/page/LELWYWPRJR.jpg)

C++勉強会なのに・・・
内容がWindows SDK寄りです。
標準入出力だけでゲームを作るのは、
さすがにムズいっす。
※C++のコードはちゃんと出てきますのでご安心ください
################
#
#
# [] [] [] [] [] [] [] [] #
#
#
#
#
#
()
#
#
#
#
#
#
===
#
#
#
################


# Page. 4

![Page Image](https://bcdn.docswell.com/page/4JM9N94PEW.jpg)

グラフィックス表現について
ブロック崩しを作るにあたって、
見た目をどう表現するかが課題でした。
描画ライブラリを使う方法もありますが、
今回はちょっと変わったアプローチを試しました。


# Page. 5

![Page Image](https://bcdn.docswell.com/page/PJRGDGWYE9.jpg)

ウインドウは形を変えられます
SetWindowRgnを使えば、ウインドウの
形を自由に変えられます。


# Page. 6

![Page Image](https://bcdn.docswell.com/page/PEXX1XP4EX.jpg)

ウインドウは形を変えられます
Regionとは、ウインドウの表示領域を定義する図形情報
SetWindowRgn
ウインドウの表示領域を指定したリージョンに設定します。リー
ジョン外は描画されず、ウインドウの形を変えられます。
CreateRectRgn
矩形のリージョンを作成します。四角いウインドウ形状を作ると
きに利用。
CreateRoundRectRgn
角の丸い矩形リージョンを作成します。丸みを帯びたウインドウ
形状を作るときに便利。
CombineRgn
複数のリージョンを結合・差分・交差させて新しいリージョンを
作成します。複雑な形状を作るときに使用。
GetWindowRgn
現在設定されているウインドウリージョンを取得します。


# Page. 7

![Page Image](https://bcdn.docswell.com/page/3EKW2WDPED.jpg)

ブロック崩しのためのウインドウ
●
ボール
●
パドル
●
ブロック
●
得点表示
（一桁）


# Page. 8

![Page Image](https://bcdn.docswell.com/page/L73141R4J5.jpg)

ブロックの形状設定プログラム
class WindowBlocks : public Window {
std::array&lt;uint16_t, num_block.cy&gt; blocks_; uint16_t：1行分の残りブロック
void ApplyBlocks() {
auto rgn = CreateRectRgn(0, 0, 0, 0);
for(int y=0; y&lt;blocks_.size(); y++) {
auto bits = blocks_[y];
for (int x = 0; x &lt; 16; x++, bits&gt;&gt;=1U) {
if (!(bits &amp; 1)) continue;
auto rgn_add = RgnBlock(x, y);
rgn = RgnOr(rgn, rgn_add);
}
}
SetWindowRgn(Handle(), rgn, TRUE);
DeleteObject(rgn);
}
public:
int GetRemain() const {
int count = 0;
std::popcount(bits) は bitsのONになっている
for (auto bits : blocks_) {
count += std::popcount(bits);
ビットの数を数えて返します
}
C++20 で&lt;bit&gt;に含まれます
return count;
}


# Page. 9

![Page Image](https://bcdn.docswell.com/page/87DXQXY5EG.jpg)

数字の形状設定プログラム
class WindowNumber : public Window {
int number_ = 0;
void ApplyNumber() {
auto number = number_;
number %= 10;
static uint16_t s_arr_pattern[] = {
0b11111’10001’11111,
0b00000’11111’00000,
0b10111’10101’11101,
0b11111’10101’10101,
0b11111’00100’00111,
0b11101’10101’10111,
0b11101’10101’11111,
0b11111’00001’00001,
0b11111’10101’11111,
0b11111’10101’10111,
};
10111
10101
11111
auto rgn = CreateRectRgn(0, 0, 0, 0);
auto pattern = s_arr_pattern[number];
for (int x = 0; x &lt; 3; x++) {
for(int y=0; y&lt;5; y++) {
if (pattern &amp; 1) {
auto rgn_add = RgnDot(x,
y);
rgn = RgnOr(rgn, rgn_add);
}
pattern &gt;&gt;= 1;
}
}
SetWindowRgn(Handle(), rgn, TRUE);
DeleteObject(rgn);
}
public:


# Page. 10

![Page Image](https://bcdn.docswell.com/page/VJPPLP6DJ8.jpg)

マウス操作の実装
WM_MOUSEMOVEではなく
WM_INPUTを使ったマウス操作の入力


# Page. 11

![Page Image](https://bcdn.docswell.com/page/2EV2Q23GEQ.jpg)

RAWINPUTによるマウス操作の実装
RAWINPUTとは
従来のマウス入力の課題
●
●
●
WM_MOUSEMOVE メッセージ：OS側でマウスカーソル位置に変換
カーソルの加速度が適用される
ゲーム（特にパドル操作）には不向き
RAWINPUTの特徴
●
●
●
●
デバイスからの生データを直接取得
マウス移動の**相対変位（delta）**を獲得
カーソル加速度の影響を受けない
低遅延で高精度な入力が可能


# Page. 12

![Page Image](https://bcdn.docswell.com/page/57GRWRYDEL.jpg)

MouseOperateクラスの構造
class MouseOperate {
int delta_x_ = 0; // マウスの移動量を蓄積
public:
void WmInput(HRAWINPUT hRawInput); // WM_INPUTメッセージ処理
int DeltaXPop(); // 蓄積した移動量を取得してリセット
MouseOperate(HWND hwnd); // 初期化：RAWINPUTデバイス登録
};
MouseOperate(HWND hwnd) {
RAWINPUTDEVICE rid;
rid.usUsagePage = 0x01; // HID汎用ページ
rid.usUsage = 0x02; // マウス
rid.dwFlags = RIDEV_INPUTSINK; // フォーカス外でも受信
rid.hwndTarget = hwnd;
RegisterRawInputDevices(&amp;rid, 1, sizeof(rid));
}


# Page. 13

![Page Image](https://bcdn.docswell.com/page/4EQV3VMXJP.jpg)

入力処理の流れ
1. WM_INPUTメッセージ受信
(HRAWINPUT)lParamとキャストしてHRAWINPUTを取得する
2. GetRawInputData()でデータ取得
まずデータサイズを取得し、次に実データを取得
GetRawInputData(hRawInput, RID_INPUT, NULL, &amp;dwSize, sizeof(RAWINPUTHEADER));
3. lLastXから移動量を抽出し、delta_x_に累積
raw-&gt;data.mouse.lLastXにX軸の移動量（マウスカウント）が格納
RAWINPUT* raw = (RAWINPUT*)lpb;
if (raw-&gt;header.dwType == RIM_TYPEMOUSE) delta_x_ += raw-&gt;data.mouse.lLastX;
4. ゲームループで一括取得
int DeltaXPop() { auto delta_x = delta_x_; delta_x_ = 0; return delta_x; }


# Page. 14

![Page Image](https://bcdn.docswell.com/page/KJ4M1MV2E1.jpg)

メインフロー
C++コルーチンを使った順次処理


# Page. 15

![Page Image](https://bcdn.docswell.com/page/LE18G83K7G.jpg)

コルーチンによるゲームフロー実装
概要
このブロック崩しでは、C++20のコルーチン機能を活用して、ゲーム全体の流れを
自然な順次処理として記述しています。
主な特徴
● メッセージループからのコールバックを意識させない設計
● ゲーム要素をローカル変数として管理（new/delete不要）
● ステートマシンを使わずに直感的なフローを実現


# Page. 16

![Page Image](https://bcdn.docswell.com/page/GEWZKZPPE2.jpg)

従来の問題点
コールバック地獄
// 従来のWindowsプログラミング
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
static int state = STATE_TITLE;
static Ball* ball = nullptr;
static Paddle* paddle = nullptr;
switch(msg) {
case WM_TIMER:
switch(state) {
case STATE_TITLE:
if(/* 条件 */) state = STATE_PLAYING;
break;
case STATE_PLAYING:
// ゲームロジック散在
break;
case STATE_GAMEOVER:
// 後始末
break;
}
break;
}
}


# Page. 17

![Page Image](https://bcdn.docswell.com/page/47Z1Z1K6J3.jpg)

コルーチンによる解決
自然な記述
static Coroutine GameMain(Window* window) {
BeepControl beep_control;
// 初期化
WindowMouseOperate window_mouse_operate;
Window window_frame{ /* ... */ };
while(true) {
// タイトル待機
while((GetAsyncKeyState(VK_LBUTTON) &amp; 0x8000)==0) {
if (window_frame.ShouldClose()) co_return; // ゲーム終了
paddle_.Move(mouse_operate-&gt;DeltaXPop());
co_await std::suspend_always{}; // 1フレーム待機
}
// ゲームプレイ
for (int cnt_ball = 3; 0 &lt; cnt_ball; cnt_ball--) {
Ball ball{ /* ... */ }; // ローカル変数！
do {
// ゲームループ
ball.Move();
co_await std::suspend_always{};
} while (ball.Pos().y &lt; size_frame.cy * size_pixel.cy);
}
}
}


# Page. 18

![Page Image](https://bcdn.docswell.com/page/YJ6LZLQLEV.jpg)

Coroutine型の実装
struct Coroutine {
struct promise_type {
Coroutine get_return_object() {
return
Coroutine{ std::coroutine_handle&lt;promise_type&gt;::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle&lt;promise_type&gt; handle_;
bool resume() {
if (!handle_ || handle_.done()) return false;
handle_.resume();
return !handle_.done();
}
};


# Page. 19

![Page Image](https://bcdn.docswell.com/page/GJ51W1RM74.jpg)

WindowWithCoroutineクラス
template&lt;auto coroutine&gt;
class WindowWithCoroutine : public Window {
CoroutinePtr coroutine_;
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
auto window_with_coroutine = /* ... */;
switch (uMsg) {
case WM_TIMER:
if (window_with_coroutine-&gt;Step()) return 0; // まだ続く
KillTimer(hwnd, TIMER_FRAME); // 終了
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
public:
bool Step() const {
if (coroutine_) {
if (coroutine_-&gt;resume()) return true; // 継続
coroutine_.reset(); // 終了
}
return false;
}
};


# Page. 20

![Page Image](https://bcdn.docswell.com/page/LE3141P975.jpg)

ローカル変数管理の利点
GameMain内のオブジェクト
static Coroutine GameMain(Window* window) {
// すべてローカル変数として宣言
BeepControl beep_control;
//
スタック上
WindowMouseOperate window_mouse_operate; //
スタック上
Window window_frame{ /* ... */ };
//
スタック上
WindowBlocks window_blocks(&amp;window_frame); //
スタック上
for (int cnt_ball = 3; 0 &lt; cnt_ball; cnt_ball--) {
Ball ball{ /* ... */ };
//
ループごとに生成
Window window_ball{ /* ... */ };
//
自動破棄
do {
// ...
} while (/* ... */);
// スコープ終了で自動的にballとwindow_ballが破棄
}
}
項目
従来の方法
コルーチン方式
メモリ管理
new/delete必要
自動管理
生存期間
手動管理
スコープで自動
初期化
NULLチェック必要
常に有効
エラー処理
メモリリーク危険
RAII保証


# Page. 21

![Page Image](https://bcdn.docswell.com/page/8EDXQXL8JG.jpg)

可読性の比較
ステートマシン方式
enum GameState { TITLE, PLAYING, GAMEOVER };
static GameState state = TITLE;
static int ball_count = 0;
void OnTimer() {
switch(state) {
case TITLE:
if(input.clicked()) {
state = PLAYING;
ball_count = 3;
}
break;
case PLAYING:
if(ball.out()) {
ball_count--;
if(ball_count == 0) {
state = GAMEOVER;
}
}
break;
}
}
コルーチン方式
while(true) {
while(!input.clicked()) co_await frame();
for(int balls = 3; balls &gt; 0; balls--) {
do {
ball.update();
co_await frame();
} while(!ball.out());
}
}


# Page. 22

![Page Image](https://bcdn.docswell.com/page/V7PPLP2W78.jpg)

サウンド
音声ファイルを使わないサウンド出力


# Page. 23

![Page Image](https://bcdn.docswell.com/page/2JV2Q2D9JQ.jpg)

BeepControlクラス
BeepControlクラスは、ゲーム内の効果音をリアルタイムに生成・再生するシステム
です。
主な特徴
●
●
●
●
WAV音源不要：数式から音を生成
低レイテンシ：Wave Audio APIを直接使用
軽量：外部ファイル依存なし
単純明快：100行以下の実装


# Page. 24

![Page Image](https://bcdn.docswell.com/page/5EGRWRY4JL.jpg)

BeepControlクラス
class BeepControl {
static constexpr int sampleRate = 44000;
// 44kHz
static constexpr int durationMs = 100;
// 100ミリ秒
static constexpr int samples = sampleRate * durationMs / 1000;
static constexpr double frequency = 880.0;
// A5 (ラの音)
std::vector&lt;BYTE&gt; buffer_;
HWAVEOUT hWaveOut_;
WAVEHDR hdr_ = {};
public:
void Play();
BeepControl();
~BeepControl();
};
// 波形データ
// 出力デバイス
// 波形ヘッダ
要素
値
理由
サンプルレ
ート
44kHz
CD音質、標準的
持続時間
100ms
短い効果音に最適
周波数
880Hz (A5)
明瞭で心地よい音
程
ビット深度
8bit
シンプル、軽量


# Page. 25

![Page Image](https://bcdn.docswell.com/page/4JQV3VMR7P.jpg)

音声波形の生成
BeepControl() : buffer_(samples) {
// サイン波の生成
for (int i = 0; i &lt; samples; ++i) {
double t = static_cast&lt;double&gt;(i) / sampleRate;
// 基本式：sin(2πft)
buffer_[i] = static_cast&lt;BYTE&gt;(
127 * (sin(2 * 3.141592 * frequency * t) * 0.1 + 1)
);
}
// デバイスの初期化 ...
}


# Page. 26

![Page Image](https://bcdn.docswell.com/page/K74M1MVQJ1.jpg)

Wave Audio APIの初期化
// フォーマット設定
WAVEFORMATEX wfx = {};
wfx.wFormatTag = WAVE_FORMAT_PCM; // 非圧縮PCM
wfx.nChannels = 1;
// モノラル
wfx.nSamplesPerSec = sampleRate; // 44kHz
フィールド
wfx.wBitsPerSample = 8;
// 8bit
wfx.nBlockAlign = 1;
// 1サンプル = 1バイト
wFormatTag
wfx.nAvgBytesPerSec = sampleRate; // 44000バイト/秒
// デバイスを開く
waveOutOpen(&amp;hWaveOut_, WAVE_MAPPER,
&amp;wfx, 0, 0, CALLBACK_NULL);
// ヘッダ準備
hdr_.lpData =
reinterpret_cast&lt;LPSTR&gt;(buffer_.data());
hdr_.dwBufferLength =
static_cast&lt;DWORD&gt;(buffer_.size());
waveOutPrepareHeader(
hWaveOut_, &amp;hdr_, sizeof(hdr_));
値
意味
WAVE_FORMAT_PC
M
非圧縮PCM形式
nChannels
1
モノラル
（ステレオは2）
nSamplesPerSec
44000
サンプリング周波数
wBitsPerSample
8
1サンプルあたり8bit
nBlockAlign
1
1サンプル = 1バイト
nAvgBytesPerSe
c
44000
データレート


# Page. 27

![Page Image](https://bcdn.docswell.com/page/LJ18G83LEG.jpg)

再生処理
void Play() {
if (hdr_.dwFlags &amp; (WHDR_PREPARED|WHDR_DONE)) {
waveOutWrite(hWaveOut_, &amp;hdr_, sizeof(hdr_));
}
}
フラグの意味
●
●
●
WHDR_PREPARED: バッファが再生準備完了
WHDR_DONE: 前回の再生が完了
どちらかが立っていれば再生可能


# Page. 28

![Page Image](https://bcdn.docswell.com/page/GJWZKZPQJ2.jpg)

後片付け
~BeepControl() {
waveOutUnprepareHeader(hWaveOut_, &amp;hdr_,
sizeof(hdr_));
waveOutClose(hWaveOut_);
}
リソース解放の順序
1. UnprepareHeader: バッファの準備を解除
2. Close: デバイスを閉じる


# Page. 29

![Page Image](https://bcdn.docswell.com/page/4EZ1Z1K373.jpg)

ゲーム内での使用例
static Coroutine GameMain(Window* window) {
BeepControl beep_control; // ローカル変数として生成
// ... ゲームループ内 ...
// 衝突判定
bool bound = ball.BoundFrame();
if (window_blocks.Collision(&amp;ball)) {
bound = true;
}
if (/* パドルに当たった */) {
bound = true;
}
// 何かに当たったら音を鳴らす
if(bound) beep_control.Play(); // ← 効果音再生
}


# Page. 30

![Page Image](https://bcdn.docswell.com/page/Y76LZLQZJV.jpg)

以上です。ご清聴ありがとうございました


# Page. 31

![Page Image](https://bcdn.docswell.com/page/G751W1R9E4.jpg)

僕のスケボー紹介
ふつうの
32インチ
Penny
22インチ
フリースケート
6.85インチx2
Penny CASPER
36インチ
サーフスケート
Carver Greenroom
33インチ
LOADED
Bhangla
48.5インチ


