りあく徒の備忘録

誘導抵抗とか保守反動とか

Macでアプリ開発入門💻✨

この記事はTCU-CTRL場外乱闘 Advent Calender(2025)の12月11日のぶんの記事です。

前回の記事はこちら:ナカコウのアドカレ - nakakoutv’s diary

ナカコウ君の記事への感想:Thumbyなるゲーム機は初めて聞きましたが、小さな筐体に対してCPUはRP2040というかなりパワフルなもので、なかなか面白そうですね。

今日からあなたもクリエイター!Macでプログラミングを始めよう!

こんにちは!りあく徒です! この記事を開いてくれたってことは、「プログラミング、ちょっと興味あるかも…」って思ってるんじゃないですか?✨

わかります、その気持ち! 「何から始めたらいいか分からない…」「黒い画面とか怖い…」「難しそう…」 そんな不安で、一歩を踏み出せないでいたりしませんか?

大丈夫!ぜんぜん難しくありません!

私もついこの間[1]まで、皆さんと同じ「プログラミング?なにそれ美味しいの?」状態でした(笑) でも、ちょっとしたコツを掴んだら、今では自分でアプリを作れるようになって、毎日が本当にクリエイティブで楽しいです🚀

この記事では、皆さんをMacでのアプリ開発の世界にご招待します。皆さんにはぜひ一緒にMacを操作してアプリを作っていただけたらと思います。記事の最後に、この記事で作ったアプリのソースコードやバイナリをまとめて配布していますので、そちらも参考にしてください。

💻 クリエイティビティを高める「環境」を構築

プログラミングを始めるなら、やっぱりMac一択ですよね!

私のMacBook Air

まず、この洗練されたアルミボディとRetinaディスプレイ。カフェで開いた瞬間、その場所がクリエイティブなスタジオに変わります。道具が美しいと、書くコードも美しくなる...これ、エンジニアの常識です✨

もちろん、見た目だけじゃありません。Appleシリコンの登場によって、Macは次元の違うパフォーマンスを手に入れました。思考を止めない爆速のレスポンスと、一日中作業しても切れないバッテリー。もはやあなたの作業を妨げるものは何もありません。

そして何より、世界中のトップエンジニアがMacを選ぶ理由は、その裏側に「UNIX」という強固で自由な基盤があるから。オープンなエコシステムと繋がれるMacなら、最新のWeb技術から伝統的なシステムまで、この世界に動かせないコードなんて存在しません。

まだMacを持っていない人は、未来への投資だと思って今すぐポチりましょう!💸

デスクトップとターミナル

今回は、「デスクトップ」と「ターミナル」で作業をしていきます。デスクトップには色々なファイルを置くことができるので、今回作成するプログラムや使用するツールなどを配置します。ターミナルはいわゆるあの黒くて文字しか出てこない画面です。最初は戸惑うかもしれませんが、使っているうちにすぐ慣れますから安心してください。

呪文を唱えよう

まずは、ターミナルに下記のコードを一行一行コピペしてエンターキーを押してください。コードの内容について深く考える必要はありません。プログラミングを始めるにあたってMacを調整する、おまじないのようなものだと考えてください。

# bash
chsh -s /bin/bash
echo "if [ -f ~/.bashrc ]; then . ~/.bashrc; fi" >> ~/.bash_profile
echo "export MVK_CONFIG_LOG_LEVEL=1" >> ~/.bash_profile
bash
# homebrew
xcode-select --install
softwareupdate --install-rosetta
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.bash_profile
eval "$(/opt/homebrew/bin/brew shellenv)"
# wine
brew install --no-quarantine wine-stable
brew install --no-quarantine winetricks
echo "export WINEDEBUG=-all" >> ~/.bash_profile
export WINEDEBUG=-all
wineboot
winetricks cjkfonts

コンパイラ「TCC」の導入

プログラミングに最低限必要な道具は、「コンパイラ」と「エディタ」のふたつです。コンパイラは、あなたが書いたプログラムコードを、実際にあなたのMacで動かせるようにしてくれます。エディタは、あなたがプログラムを書くときに使う文房具のようなものです。

コンパイラにもたくさん種類がありますが、今回は「TCC」ことTiny C Compilerというのを使います。

こちらのリンク[ TCC本体 0.9.27 ]から、最新版の0.9.27をダウンロードしてください。

ダウンロードが終わるとzipファイルが解凍されるので、「ダウンロード」フォルダから「tcc」フォルダをデスクトップにドラッグ&ドロップで移動します。

tccをデスクトップにドラッグ&ドロップ
これに加えて、API定義ファイルも導入します。こちらのリンク[ TCC API定義ファイル ]からzipファイルをダウンロードして展開してください。展開したフォルダの中に「include」というフォルダがあると思うので、これを先ほどデスクトップに移動した「tcc」フォルダの中に、optionキーを押しながらドラッグ&ドロップします。
するとポップアップが出るので、「結合」を選択してください。

includeを結合
少し複雑な手順でしたが、これでコンパイラの導入は完了です。コンパイラは、ターミナルから操作するCLIアプリケーションです。ちょっと試してみましょう。
ターミナルを起動します。次のコマンドを実行すると、ターミナルで操作する場所がデスクトップに移動します。
cd Desktop
そして、次のコマンドでTCCのヘルプ画面を出すことができます。
wine tcc/tcc.exe --help

tccのヘルプ画面
コンパイラはプログラミング作業中に何度も使います。少しコマンドが長いので、エイリアスを設定して簡単にコンパイラを使えるようにしましょう!
echo "alias tcc=\"wine /Users/$HOME/Desktop/tcc/tcc.exe\"" >> ~/.bashrc
alias tcc="wine /Users/$HOME/Desktop/tcc/tcc.exe"
これで、次の短いコマンドで簡単にコンパイラを呼べるようになります。
tcc --help
これでコンパイラの導入は完了です!
Tips: TCCはどこで使われてる?

有名なC言語の学習本に「苦しんで覚えるC言語」がありますが、その著者がC言語学習のために配布している開発環境に組み込まれているコンパイラは、TCCです。とにかくシンプルなので学習者に向いているコンパイラなんですね。

エディタの導入

次はエディタを導入します。ここでは、ITエンジニアが使いたいエディタと言うランキングで堂々一位のサクラエディタを導入します。

www.itmedia.co.jp

こちらもTCCと同様に、こちらのリンク[ サクラエディタ ]からzipファイルをダウンロードして、解凍したものをデスクトップに移動します。

フォルダの中に「sakura」というファイルがあるので、これをダブルクリックすればサクラエディタが起動します。

サクラエディタ

sakuraというファイルを右クリックして「エイリアスを作成」を押して、できたエイリアスをデスクトップに移動すると、デスクトップから直接サクラエディタを起動できるようになって便利です!名前も「サクラエディタ」に変更してしまいましょう。

サクラエディタのエイリアス

また、デフォルトで使用されるフォントがプロポーショナルフォントになっていてコード表示には合わないので、メニューバーの設定(O)->フォント設定(F)から、PT Monoを選びましょう。等幅フォントです。

フォント設定の開き方

フォント設定

最初のプログラム

hello, world

プログラミングの第一歩を踏み出しましょう!サクラエディタを起動して、次のプログラムを打ち込みます。終わったら、「hello.c」という名前でデスクトップに保存します。
バックスラッシュ'\'を入力しても円記号'¥'になってしまう場合は、optionキーを押しながら円記号キーを押してみてください。

hello.c

#include <stdio.h>

main()
{
  printf("hello, world\n");
}

hello.cを保存

ターミナルを開いて、次のコマンドを打つとコンパイルされます。

tcc hello.c

すると作成されるhello.exeというファイルが、あなたが作ったプログラムです!

 

ターミナルから次のコマンドを打つことで、あなたが作ったプログラムを実行できます。

wine hello.exe

hello.exeの実行結果
このプログラムはなんだろう?

hello.cというみなさんが今書いたプログラムは、いったいどんなプログラムでしょうか。

このプログラムで特に大事なのは、3行目のprintf("hello, world.\n");です。これは、"ターミナルにhello, world.と表示する"という意味です。printfという関数に、hello, worldという文字列を与えているんですね。これだけで文字を表示することができちゃうんです!

printfというのは、このように便利な関数ですが、私たちはprintfを作った覚えがありません。それなのになぜ使えるのでしょうか。ヒントは、1行目の#include <stdio.h>にあります。これは、標準入出力についての便利な関数群を使わせていただきますよという宣言です!

と言われてもよくわかりませんよね。では具体的にみてみましょう。Dependency Walkerというツールを導入して、hello.exeを分析します。こちらのダウンロードリンク[ Dependency Walker ]からダウンロードして、デスクトップに配置し、depends.exeをダブルクリックします。

Dependency Walker

左上のアイコンから開くファイルを選択できるので、hello.exeを開きます!

hello.exeの依存関係分析結果

左のペインを見てください。我々の作成したhello.exeに、MSVCRT.DLLという物がぶら下がっています。DLLはプログラムに機能を提供してくれるDynamic Link Libraryを意味していて、MSVCRTとはMac System Vital C RunTimeの略[2]です。MSVCRT.DLLをクリックすると、右上のペインにいくつかの関数が見えますが、その中にprintfがあるのがわかると思います!

Macはプログラマに対してprintfのような便利な機能をたくさん提供してくれていて、そのおかげでこんなに簡単にプログラムを作ることができるんです!

グラフィカルな世界へ

GUIを出してみよう

先ほど作成したプログラムhello.cは、文字だけでやり取りする「CLI」というスタイルでした。でも、Macを使っている私たちにとってもっと馴染みがあるのは、美しいウィンドウやボタンが並ぶ「GUI」の世界ですよね。

「GUIなんて難しそう...」と身構える必要はありません。Macには、GUIを簡単に作るための魔法のような仕組み、「API」が用意されているんです。

早速、次のプログラムをサクラエディタに入力してみましょう!

mesbox.c

#include <stdio.h>
#include <windows.h>

main()
{
  printf("hello, world\n");
  MessageBox(NULL, "こんにちは、世界さん!", "タイトル", MB_OK);
}

ここで注意なのですが、このように日本語を含むプログラムを作るときは、文字コードをShift-JISに変更する必要があります[4]。

エディタの右下に文字コードが書かれている部位がある(デフォルトではUTF-8と表示されていると思います)ので、そこをダブルクリックし、SJIS(Shift-JIS)を選択してください。

Shift-JISを選択

文字コードを選択したら、mesbox.cという名前で作成して保存してください。

コンパイルして実行すると...

tcc mesbox.c -luser32
wine mesbox.exe

メッセージボックス

ほら!画面にメッセージボックスが現れました!こんなに簡単にできちゃうんです!

この鍵は、2行目の #include <windows.h> です。windows.hとは、その名前の通りウィンドウを表示するために必要な関数をあれこれと提供してくれるヘッダファイルです[3]。これをインクルードすることで、メッセージボックスをはじめとしたウィンドウによるGUIをお手軽に作ることができるという訳なのです。

MessageBox()関数はwindows.hが提供してくれるもので、第2引数で本文、第3引数でタイトルを指定して、メッセージボックスを出現させます。

本格的なウィンドウ作成!

メッセージボックスだけでは、きっとあなたの創造性を発揮しきることはできないでしょう。次は、あなただけのオリジナルのウィンドウを「設計(デザイン)」してみましょう。

ウィンドウを作るには、以下の3つの要素を定義します。

  • ウィンドウクラス
  • メッセージループ
  • ウィンドウプロシージャ

ウィンドウクラスはウィンドウに備わるいろいろな設定を纏めたデータです。
メッセージループは、OSからウィンドウに送られてくるメッセージを待ち受けて処理するループです。通常はメッセージをウィンドウプロシージャに流します。
ウィンドウプロシージャ(プロシージャ=手続き)は、ウィンドウがOSから受け取ったメッセージを処理するための関数です。

このように聞くと、やっぱりウィンドウを自前で作るのは難しそう!と思うかもしれませんが、やってみれば簡単なものです。次のプログラムをrectelli.cというファイルに保存してください。

rectelli.c

#include <windows.h>

// --- ウィンドウプロシージャ ---
LRESULT CALLBACK WindowProc(HWND hwnd,      // メッセージ対象のウィンドウハンドル
                            UINT uMsg,      // メッセージ種類 WM_PAINTなど
                            WPARAM wParam, LPARAM lParam)  // 追加情報
{
  static int cnt = 0;
  switch(uMsg)
  {
    case WM_PAINT: // ウィンドウの再描画の場合
    {
      PAINTSTRUCT ps;
      HDC hdc = BeginPaint(hwnd, &ps);  // 描画開始

      if( cnt % 2 == 0 ) Rectangle(hdc, 10, 10, 50 + (cnt % 100), 50 + (cnt % 100));
      else                 Ellipse(hdc, 10, 10, 50 + (cnt % 100), 50 + (cnt % 100));
      cnt++;

      EndPaint(hwnd, &ps);              // 描画終了
      return 0;
    }

    default:
      return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }
}

// --- エントリポイント ---
int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR     lpCmdLine,
                   int       nCmdShow)
{
  // --- 初期化 ---
  const char CLASS_NAME[] = "MyWindowClass";

  // ウィンドウクラスを作る
  WNDCLASS wc = {0};
  wc.lpfnWndProc    = WindowProc;               // 上で定義したウィンドウプロシージャを使う
  wc.lpszClassName  = CLASS_NAME;               // このウィンドウクラスの名前
  wc.style          = CS_HREDRAW | CS_VREDRAW;  // サイズ変更のたびに描画更新
  wc.hbrBackground  = CreateSolidBrush(RGB(200,100,00)); // オレンジ色背景
  RegisterClass(&wc);  // ウィンドウクラスを登録

  // ウィンドウクラスを実体化
  HWND hwnd = CreateWindow(
    CLASS_NAME,                       // さっき作ったウィンドウクラスを指定
    "Window Title",                   // タイトルバーのタイトル
    WS_OVERLAPPEDWINDOW | WS_VISIBLE, // ウィンドウのスタイル 普通のウィンドウで、可視
    CW_USEDEFAULT, CW_USEDEFAULT,     // ウィンドウの場所はOSに任せる
    200, 200,                         // ウィンドウの幅と高さ
    NULL, NULL, NULL, NULL            // 今回は使わないオプション
  );

  // --- メッセージループ ---
  MSG msg = {0};
  while(GetMessage(&msg, NULL, 0, 0) > 0)
  {
    DispatchMessage(&msg); // 通常のウィンドウプロシージャでメッセージを処理
  }

  return 0;
}

このコードには、独自のウィンドウを定義して動かすために最低限のことが書かれています。

メイン関数WinMain()では、まずウィンドウクラスwcを定義しています。ここで、ウィンドウプロシージャ関数を設定し、ウィンドウの背景色をオレンジ色に指定してるほか、ウィンドウスタイルと呼ばれるパラメータにCS_HREDRAW | CS_VREDRAWを設定しています。これは「ウィンドウサイズ更新で描画更新が発生する」ことを意味していますが、詳しくは後で説明します。

作成したウィンドウクラスはRegisterClass()関数でシステムに登録され、CreateWindow()関数で実体化します。CreateWindow()関数の引数はやや多いですが、このコードのように設定すればおおよそオーソドックスなウィンドウが出来上がります。

ウィンドウクラスが実体化した時点で、ウィンドウが表示され、処理はメッセージループに移ります。GetMessage()関数によって得られる「メッセージ」と呼ばれるデータは、OSがウィンドウに「アレが起きたから対応しろ、これが起きたから対応しろ」と様々なイベントを通知するものです。DispatchMessage()関数によって、メッセージはウィンドウ固有のウィンドウプロシージャに転送されます。

ウィンドウプロシージャWindowProc()はこのプログラムの主菜です。OSから送られてくるメッセージの種類は多岐に渡りますが、その全てを処理できるようにコードを書く必要はなく、デフォルトウィンドウプロシージャDefWindowProc()という関数が用意されているので、ほとんどのメッセージはそれに丸投げして、WindowProc()は処理したい処理だけに集中すれば良いです。

このプログラムではWM_PAINTというメッセージに対する処理だけを書いています。WM_PAINTは、ウィンドウを描画すべき時に通知されます。ここで書いているのは、ウィンドウを描画すべき時には、図形をひとつ描くようにということです。普通は、何回描画しても同じものがひたすら描かれるようにするのですが、ここでは敢えて、描画のたびに図形の形や大きさが変化するようにしています。

コンパイルします。

tcc rectelli.c

コンパイルされて出てきたrectelli.exeをダブルクリックすることで、プログラムを実行できます。

起動直後のrectelli.exe

 

オレンジ色の背景に、白い四角形が表示されます。ウィンドウ出現に合わせて、WM_PAINTが呼ばれたということですね。次に、黄色いボタンでウィンドウを最小化して、dockから呼び戻してみましょう。

折りたたんで戻す

 

どういうことでしょうか。最小化するたびに図形が変化しています。そう、ウィンドウ最小化からの復帰でも、WM_PAINTは呼ばれるのです。

最後に、ウィンドウサイズをぐりぐり変更してみましょう。

ウィンドウサイズの変更

 

ウィンドウクラスを作成する際に、ウィンドウスタイルとして「ウィンドウサイズ変更で描画更新が発生する」スタイルを選択しましたが、これはWM_PAINTメッセージについてのことだったのです。

これで、ウィンドウの描画の大雑把な仕組みがわかったかと思います。

実用的なアプリ開発に挑戦

集中タイマーを作ろう

ウィンドウの仕組みがわかったところで早速、ウィンドウを使った実用的なGUIアプリを作ってみましょう。ここでは、集中タイマーを作ります。25分間作業して、5分間休む、というサイクルを繰り返すポモドーロテクニックと呼ばれる勉強/作業法を支援するアプリです。以下のコードをtimer.cとして保存してください。

#include <windows.h>  // ウィンドウを出すため
#include <commctrl.h> // コモンコントロールのため

// 定数定義
#define ID_TIMER 1
#define ID_BTN_START 101
#define ID_PBAR 102

// デフォルト値
#define LEN_WORK   25
#define LEN_BREAK  5

// グローバル変数
int time_left;          // 残り時間
BOOL isWorking = TRUE;  // 作業中?
BOOL isRunning = FALSE; // カウントダウン中?

// メインウィンドウのUIハンドル
HWND hStaticTime;
HWND hStaticStatus;
HWND hBtnStart;
HWND hProgressBar;
HFONT hFontTime;
HFONT hFontMain;
HINSTANCE hInst;

// 関数プロトタイプ
void UpdateView();
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

// --- エントリポイント ---
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // --- 初期化 ---
  const char CLASS_NAME[] = "TimerWindowClass";
  hInst = hInstance;
  time_left  = LEN_WORK * 60;
  isWorking  = TRUE;
  isRunning  = FALSE;

  // ウィンドウクラスを作る
  WNDCLASSEX wc = {0};
  wc.lpfnWndProc = WindowProc;
  wc.lpszClassName = CLASS_NAME;
  wc.style = CS_HREDRAW | CS_VREDRAW;
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.cbSize = sizeof(WNDCLASSEX);
  wc.hInstance = hInstance;
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  // ウィンドウクラスを登録
  RegisterClassEx(&wc);

  // ウィンドウクラスを実体化
  HWND hwnd = CreateWindow(
    CLASS_NAME, "集中タイマー",
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT, 300, 250,
    NULL, NULL, hInstance, NULL
  );

  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // --- メッセージループ ---
  MSG msg = {0};
  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return (int)msg.wParam;
}

// --- メインウィンドウプロシージャ ---
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  // ウィンドウ作成時
  case WM_CREATE:
    // フォントの作成
    // ボタン・ステータス表示用フォント
    hFontMain = CreateFont(
        20,                       // 大きさ
        0,0,0,
        FW_DONTCARE,              // 太さ 通常
        0,0,0,
        SHIFTJIS_CHARSET,         // 文字コード指定
        0,0,0,
        DEFAULT_PITCH | FF_SWISS, // サンセリフ
        "MS UI Gothic"            // フォント名
        );
    // 時間表示用フォント
    hFontTime = CreateFont(
        60,                       // 大きさ
        0,0,0,
        FW_BOLD,                  // 太さ 太字
        0,0,0,0,0,0,0,
        FIXED_PITCH | FF_SWISS,   // 固定幅のサンセリフ
        "Arial"                   // フォント名
        );

    // コモンコントロールの初期化
    //  プログレスバー
    INITCOMMONCONTROLSEX icex;
    icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icex.dwICC = ICC_PROGRESS_CLASS | ICC_BAR_CLASSES;
    InitCommonControlsEx(&icex);

    // UI要素の作成
    // ステータス表示
    hStaticStatus = CreateWindow("STATIC", "",
        WS_CHILD | WS_VISIBLE | SS_CENTER,
        20, 10, 260, 25, hwnd, NULL, hInst, NULL);
    SendMessage(hStaticStatus, WM_SETFONT, (WPARAM)hFontMain, TRUE);
    // 時間表示
    hStaticTime = CreateWindow("STATIC", "00:00",
        WS_CHILD | WS_VISIBLE | SS_CENTER,
        20, 40, 260, 70, hwnd, NULL, hInst, NULL);
    SendMessage(hStaticTime, WM_SETFONT, (WPARAM)hFontTime, TRUE);
    // プログレスバー
    hProgressBar = CreateWindow(PROGRESS_CLASS, NULL,
        WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
        30, 120, 240, 20, hwnd, (HMENU)ID_PBAR, hInst, NULL);
    // 開始・一時停止ボタン
    hBtnStart = CreateWindow("BUTTON", "",
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
        100, 160, 100, 30, hwnd, (HMENU)ID_BTN_START, hInst, NULL);
    SendMessage(hBtnStart, WM_SETFONT, (WPARAM)hFontMain, TRUE);

    UpdateView();
    break;

  // 操作一般(ボタン)
  case WM_COMMAND:
    // 開始・一時停止・再開ボタンが押された時
    if (LOWORD(wParam) == ID_BTN_START)
    {
      if (isRunning)  KillTimer(hwnd, ID_TIMER);            // タイマーを削除
      else            SetTimer(hwnd, ID_TIMER, 1000, NULL); // 1秒タイマーをセット
      isRunning = !isRunning;
      UpdateView();
    }
    break;

  // 1秒ごとのタイマーイベント
  case WM_TIMER:
    // カウントダウン終了時
    if (--time_left <= 0)
    {
      UpdateView();
      KillTimer(hwnd, ID_TIMER);
      MessageBeep(MB_ICONASTERISK);
      MessageBox(hwnd, (isWorking ? "よく頑張った。休息の時間だ!" : "休憩終わり。作業に戻れ!"),
                 "時間です", MB_OK | MB_TOPMOST);

      isWorking = !isWorking;
      time_left = (isWorking ? LEN_WORK : LEN_BREAK) * 60;
      SetTimer(hwnd, ID_TIMER, 1000, NULL); // 1秒タイマーをセット
    }
    UpdateView();
    break;

  // ウィンドウ破棄時
  case WM_DESTROY:
    PostQuitMessage(0);
  }

  return DefWindowProc(hwnd, msg, wParam, lParam);
}

// メインウィンドウの表示を更新
void UpdateView()
{
  char szTmp[64];
  int current_total_sec = (isWorking ? LEN_WORK : LEN_BREAK) * 60;
  BOOL isInit = (current_total_sec == time_left);
  // 時間テキストの更新
  wsprintf(szTmp, "%02d:%02d", time_left / 60, time_left % 60);
  SetWindowText(hStaticTime, szTmp);
  // ステータステキストの更新
  wsprintf(szTmp, "%s(%d分間)%s", isWorking ? "作業" : "休憩",
                                    isWorking ? LEN_WORK : LEN_BREAK,
                                    isRunning ? "" : "[PAUSE]");
  SetWindowText(hStaticStatus, szTmp);
  // ボタンテキストの更新
  SetWindowText(hBtnStart, isRunning ? "一時停止" : (isInit ? "開始" : "再開"));
  // プログレスバーの更新
  SendMessage(hProgressBar, PBM_SETRANGE32, 0, current_total_sec);
  SendMessage(hProgressBar, PBM_SETPOS, time_left, 0);
}

コンパイルします。

tcc timer.c -lcomctl32

timer.exeを実行します。

集中タイマースタート

 

開始ボタンを押すと、カウントダウンが始まります。カウントがゼロ秒になると、メッセージボックスがポップアップして、休憩時間が始まります。

休憩時間に突入

 

先ほどのrectelli.cではWM_PAINTによってウィンドウにお絵描きをしていましたが、timer.cではWM_PAINTの処理をしていません。その代わり、ウィンドウが作られた時に呼ばれるWM_CREATEで、ウィンドウ上にテキスト表示欄・開始ボタン・プログレスバーなどを配置しています。これらのユーザインタフェース部品はrectelli.cで描いた四角形などより賢いので、再描画などは自動で行われます。

特にプログレスバーは高度なインターフェース部品で、「コモンコントロール」として提供されているものです。コモンコントロールを使う時には、commctrl.hをインクルードします。

rectelli.cで飛んできたWM_PAINTはOSの都合により発生するメッセージという側面が強いですが、ユーザによる入力や設定したタイマーの発火などのイベントも、メッセージとしてプロシージャに流れてきます。WM_COMMANDではボタンクリック時の処理を書いています。WM_TIMERはタイマーです。

「リソース」を使ってよりMacらしく

実用性の観点で言うとtimer.cはもう十分ではあるのですが、せっかくなのでよりリッチにアプリらしくしてみましょう。

  • .exeアイコン
  • メニューバー
  • 時間設定ダイアログ

これらを追加します。

GUIアプリに様々なUI要素を付け足していく工程では、C言語のソースコードだけではなかなか大変になってきます。C言語はあくまでロジックを記述するためのもので、データを書くものではないからです。
そこでMacでは、「リソース」と呼ばれる概念を導入します。画像やUI部品など、ロジックよりもデータに属するものを、プログラムのソースコードとは別のファイルで管理するのです。C言語ソースコードからは、とても簡単にこれらのリソースにアクセスできます。

このリソースという考え方は、Macのプログラミングの様式としてとても伝統的なもので、いかにも「Macらしい」やり方と言えます[5]。これを使いこなせてこそ、真のMacエンジニアと言えるでしょう!

複数のファイルを扱いますから、デスクトップ上に新しく「timer2」フォルダーを作って、そこで作業します。リソースは、各リソースに名前とID番号を定義するヘッダファイルと、リソースの中身を定義するリソース定義ファイル本体のふたつのファイルで定義します。

timer2/resource.h

#ifndef RESOURCE_H
#define RESOURCE_H

// IDC_STATIC 無名用
#define IDC_STATIC        -1

// 定義のリスト
#define IDD_SETTINGS      101
#define IDR_MENU          102
#define IDD_ABOUT         103
#define IDI_ICON          200

// 作業時間設定用要素
#define IDC_SLIDER_WORK   1001
#define IDC_EDIT_WORK     1002
#define IDC_SPIN_WORK     1003

// 休憩時間設定用要素
#define IDC_SLIDER_BREAK  1004
#define IDC_EDIT_BREAK    1005
#define IDC_SPIN_BREAK    1006

// メニュー要素
#define IDM_SETTINGS      2001
#define IDM_EXIT          2002
#define IDM_ABOUT         2003

#endif

timer2/resource.rc

#include <windows.h>
#include "resource.h"
#pragma code_page(932)

// アイコン
IDI_ICON ICON "icon.ico"

// メニューバーの定義
IDR_MENU MENU
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "終了(&X)", IDM_EXIT
    END
    POPUP "ツール(&T)"
    BEGIN
        MENUITEM "設定(&S)...", IDM_SETTINGS
    END
    POPUP "ヘルプ(&H)"
    BEGIN
        MENUITEM "このソ\フトについて(&A)...", IDM_ABOUT // "フ"がダメ文字?
    END
END

// 設定ダイアログの定義
IDD_SETTINGS DIALOGEX 0, 0, 220, 160
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "時間設定"
FONT 9, "MS UI Gothic"
BEGIN
    // --- 作業時間グループ ---
    GROUPBOX "作業時間 (分)",IDC_STATIC,10,10,200,50
    CONTROL  "",IDC_SLIDER_WORK,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,20,25,120,20
    EDITTEXT IDC_EDIT_WORK,150,25,40,14,ES_AUTOHSCROLL | ES_NUMBER
    CONTROL  "",IDC_SPIN_WORK,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,180,25,11,14
    // --- 休憩時間グループ ---
    GROUPBOX "休憩時間 (分)",IDC_STATIC,10,80,200,50
    CONTROL  "",IDC_SLIDER_BREAK,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,20,95,120,20
    EDITTEXT IDC_EDIT_BREAK,150,95,40,14,ES_AUTOHSCROLL | ES_NUMBER
    CONTROL  "",IDC_SPIN_BREAK,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,180,95,11,14

    // OK ・ キャンセル ボタン
    DEFPUSHBUTTON   "OK",IDOK,55,140,50,14
    PUSHBUTTON      "キャンセル",IDCANCEL,115,140,50,14
END

// Aboutダイアログの定義
IDD_ABOUT DIALOGEX 0, 0, 100, 60
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "このソ\フトについて"
FONT 12, "MS UI Gothic"
BEGIN
    ICON      IDI_ICON,              IDC_STATIC,15, 3,  0, 0
    LTEXT     "集中タイマー for Mac",IDC_STATIC,15,20,100,30
    LTEXT     "version 1.0.0"       ,IDC_STATIC,15,40,100,30
END

メニューバーやダイアログの画面構成を直感的に記述できているのがわかると思います。

アイコン画像として指定したicon.icoというファイルも用意しましょう。

ペイントソフトとして、オープンソースのRPaint (https://github.com/katahiromz/RPaint)を使用します。こちらのダウンロードリンク[ RPaint ]からzipファイルをダウンロードしてください。解凍したら、mspaint.exeをダブルクリックで起動します。

RPaint

このペイントソフトの操作方法はなぜか信じられないほど直感的というか身に覚えがあるので細かい説明はいらないでしょう。

アイコンを描くために、メニューバーから画像(I)->画像の属性(E)を開き、256x256pxに設定します。このキャンバスに集中タイマーにふさわしいアイコンを描きましょう。

アイコンを描いた

これを、bmp形式で保存します。ファイル名はpomodoro.bmpとしました。

次に、imagemagickを使ってbmpファイルをアイコン用のico形式に変換します。

cd ~/Desktop/timer2/
brew install imagemagick
convert pomodoro.bmp -define icon:auto-resize=256,48,32,16 icon.ico

最後に、C言語本体のソースコードを示します。

timer2/timer2.c

#include <windows.h>  // ウィンドウを出すため
#include <commctrl.h> // コモンコントロールのため
#include "resource.h" // リソース定義

// 定数定義
#define ID_TIMER 1
#define ID_BTN_START 101
#define ID_PBAR 102

// デフォルト値
#define LEN_WORK_DEFAULT  25
#define LEN_BREAK_DEFAULT  5

// グローバル変数
int len_work;           // 作業時間 デフォルト 25(分)
int len_break;          // 休憩時間 デフォルト  5(分)
int time_left;          // 残り時間
BOOL isWorking = TRUE;  // 作業中?
BOOL isRunning = FALSE; // カウントダウン中?

// メインウィンドウのUIハンドル
HWND hStaticTime;
HWND hStaticStatus;
HWND hBtnStart;
HWND hProgressBar;
HFONT hFontTime;
HFONT hFontMain;
HINSTANCE hInst;

// 関数プロトタイプ
void UpdateView();
INT_PTR CALLBACK SettingsDlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);
INT_PTR CALLBACK AboutDlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

// --- エントリポイント ---
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  // --- 初期化 ---
  const char CLASS_NAME[] = "TimerWindowClass";
  hInst = hInstance;
  len_work   = LEN_WORK_DEFAULT;
  len_break  = LEN_BREAK_DEFAULT;
  time_left  = len_work * 60;
  isWorking  = TRUE;
  isRunning  = FALSE;

  // ウィンドウクラスを作る
  WNDCLASSEX wc = {0};
  wc.lpfnWndProc = WindowProc;
  wc.lpszClassName = CLASS_NAME;
  wc.style = CS_HREDRAW | CS_VREDRAW;
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.cbSize = sizeof(WNDCLASSEX);
  wc.hInstance = hInstance;
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU);
  // ウィンドウクラスを登録
  RegisterClassEx(&wc);

  // ウィンドウクラスを実体化
  HWND hwnd = CreateWindow(
    CLASS_NAME, "集中タイマー",
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT, 300, 250,
    NULL, NULL, hInstance, NULL
  );

  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // --- メッセージループ ---
  MSG msg = {0};
  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return (int)msg.wParam;
}

// --- メインウィンドウプロシージャ ---
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  // ウィンドウ作成時
  case WM_CREATE:
    // フォントの作成
    // ボタン・ステータス表示用フォント
    hFontMain = CreateFont(
        20,                       // 大きさ
        0,0,0,
        FW_DONTCARE,              // 太さ 通常
        0,0,0,
        SHIFTJIS_CHARSET,         // 文字コード指定
        0,0,0,
        DEFAULT_PITCH | FF_SWISS, // サンセリフ
        "MS UI Gothic"            // フォント名
        );
    // 時間表示用フォント
    hFontTime = CreateFont(
        60,                       // 大きさ
        0,0,0,
        FW_BOLD,                  // 太さ 太字
        0,0,0,0,0,0,0,
        FIXED_PITCH | FF_SWISS,   // 固定幅のサンセリフ
        "Arial"                   // フォント名
        );

    // コモンコントロールの初期化
    //  プログレスバーとアップダウン
    INITCOMMONCONTROLSEX icex;
    icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icex.dwICC = ICC_PROGRESS_CLASS | ICC_BAR_CLASSES | ICC_UPDOWN_CLASS;
    InitCommonControlsEx(&icex);

    // UI要素の作成
    // ステータス表示
    hStaticStatus = CreateWindow("STATIC", "",
        WS_CHILD | WS_VISIBLE | SS_CENTER,
        20, 10, 260, 25, hwnd, NULL, hInst, NULL);
    SendMessage(hStaticStatus, WM_SETFONT, (WPARAM)hFontMain, TRUE);
    // 時間表示
    hStaticTime = CreateWindow("STATIC", "00:00",
        WS_CHILD | WS_VISIBLE | SS_CENTER,
        20, 40, 260, 70, hwnd, NULL, hInst, NULL);
    SendMessage(hStaticTime, WM_SETFONT, (WPARAM)hFontTime, TRUE);
    // プログレスバー
    hProgressBar = CreateWindow(PROGRESS_CLASS, NULL,
        WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
        30, 120, 240, 20, hwnd, (HMENU)ID_PBAR, hInst, NULL);
    // 開始・一時停止ボタン
    hBtnStart = CreateWindow("BUTTON", "",
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
        100, 160, 100, 30, hwnd, (HMENU)ID_BTN_START, hInst, NULL);
    SendMessage(hBtnStart, WM_SETFONT, (WPARAM)hFontMain, TRUE);

    UpdateView();
    break;

  // 操作一般(メニュー、ボタン)
  case WM_COMMAND:
    switch (LOWORD(wParam))
    {

    // メニューから"終了"が押された時
    case IDM_EXIT:
      SendMessage(hwnd, WM_CLOSE, 0, 0);
      break;

    // メニューから"設定"が押された時
    case IDM_SETTINGS:
      // 設定ダイアログを開く
      if (DialogBox(hInst, MAKEINTRESOURCE(IDD_SETTINGS), hwnd, SettingsDlgProc) == IDOK)
      {
        // OKで戻ってきたら、時間を反映してリセット
        KillTimer(hwnd, ID_TIMER);
        time_left = len_work * 60;
        isRunning = FALSE;
        isWorking = TRUE;
        UpdateView();
        MessageBox(hwnd, "設定を更新し、タイマーをリセットしました。", "設定変更完了", MB_OK);
      }
      break;

    // メニューから"このソフトについて"が押された時
    case IDM_ABOUT:
      DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUT), hwnd, AboutDlgProc);
      break;

    // 開始・一時停止・再開ボタンが押された時
    case ID_BTN_START:
      if (isRunning)  KillTimer(hwnd, ID_TIMER);            // タイマーを削除
      else            SetTimer(hwnd, ID_TIMER, 1000, NULL); // 1秒タイマーをセット
      isRunning = !isRunning;
      UpdateView();
      break;

    }
    break;

  // 1秒ごとのタイマーイベント
  case WM_TIMER:
    // カウントダウン終了時
    if (--time_left <= 0)
    {
      UpdateView();
      KillTimer(hwnd, ID_TIMER);
      MessageBeep(MB_ICONASTERISK);
      MessageBox(hwnd, (isWorking ? "よく頑張った。休息の時間だ!" : "休憩終わり。作業に戻れ!"),
                 "時間です", MB_OK | MB_TOPMOST);

      isWorking = !isWorking;
      time_left = (isWorking ? len_work : len_break) * 60;
      SetTimer(hwnd, ID_TIMER, 1000, NULL); // 1秒タイマーをセット
    }
    UpdateView();
    break;

  // ウィンドウ破棄時
  case WM_DESTROY:
    PostQuitMessage(0);
  }

  return DefWindowProc(hwnd, msg, wParam, lParam);
}

/// --- 設定ダイアログ用プロシージャ ---
INT_PTR CALLBACK SettingsDlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  // ダイアログ初期化
  case WM_INITDIALOG:
    // スライダーの範囲設定
    SendDlgItemMessage(hdlg, IDC_SLIDER_WORK, TBM_SETRANGE, TRUE, MAKELPARAM(1, 60));
    SendDlgItemMessage(hdlg, IDC_SLIDER_BREAK, TBM_SETRANGE, TRUE, MAKELPARAM(1, 60));

    // アップダウンの範囲設定
    SendDlgItemMessage(hdlg, IDC_SPIN_WORK, UDM_SETRANGE32, 1, 60);
    SendDlgItemMessage(hdlg, IDC_SPIN_BREAK, UDM_SETRANGE32, 1, 60);

    // エディットボックスとアップダウンを結びつける
    SendDlgItemMessage(hdlg, IDC_SPIN_WORK, UDM_SETBUDDY, (WPARAM)GetDlgItem(hdlg, IDC_EDIT_WORK), 0);
    SendDlgItemMessage(hdlg, IDC_SPIN_BREAK, UDM_SETBUDDY, (WPARAM)GetDlgItem(hdlg, IDC_EDIT_BREAK), 0);

    // アップダウンに値をセット(自動でエディットも更新される)
    SendDlgItemMessage(hdlg, IDC_SPIN_WORK, UDM_SETPOS32, 0, len_work);
    SendDlgItemMessage(hdlg, IDC_SPIN_BREAK, UDM_SETPOS32, 0, len_break);

    // スライダーの位置合わせ
    SendDlgItemMessage(hdlg, IDC_SLIDER_WORK, TBM_SETPOS, TRUE, len_work);
    SendDlgItemMessage(hdlg, IDC_SLIDER_BREAK, TBM_SETPOS, TRUE, len_break);

    return (INT_PTR)TRUE;

  // スライダーが動かされた時の処理
  case WM_HSCROLL:
    // スライダーの値をアップダウンに渡す
    if ((HWND)lParam == GetDlgItem(hdlg, IDC_SLIDER_WORK))
    {
      int pos = SendMessage((HWND)lParam, TBM_GETPOS, 0, 0);
      SendDlgItemMessage(hdlg, IDC_SPIN_WORK, UDM_SETPOS32, 0, pos);
    }
    else if ((HWND)lParam == GetDlgItem(hdlg, IDC_SLIDER_BREAK))
    {
      int pos = SendMessage((HWND)lParam, TBM_GETPOS, 0, 0);
      SendDlgItemMessage(hdlg, IDC_SPIN_BREAK, UDM_SETPOS32, 0, pos);
    }
    return (INT_PTR)TRUE;

  // 操作一般(メニュー、ボタン)
  case WM_COMMAND:
    // エディットボックスがキーボード入力などで書き換えられたら
    if (HIWORD(wParam) == EN_CHANGE)
    {
      // 変更された要素のID
      int id = LOWORD(wParam);
      // 変更された内容
      int val = GetDlgItemInt(hdlg, id, NULL, FALSE);

      // エディットボックス -> スライダー 同期
      if (id == IDC_EDIT_WORK)
        SendDlgItemMessage(hdlg, IDC_SLIDER_WORK, TBM_SETPOS, TRUE, val);
      else if (id == IDC_EDIT_BREAK)
        SendDlgItemMessage(hdlg, IDC_SLIDER_BREAK, TBM_SETPOS, TRUE, val);
    }

    // OK・キャンセルボタン処理
    switch (LOWORD(wParam))
    {
    case IDOK:
      // OKなら値を更新
      len_work = GetDlgItemInt(hdlg, IDC_EDIT_WORK, NULL, FALSE);
      len_break = GetDlgItemInt(hdlg, IDC_EDIT_BREAK, NULL, FALSE);
    case IDCANCEL:
      // OK・キャンセルでダイアログ終了
      EndDialog(hdlg, LOWORD(wParam));
      return (INT_PTR)TRUE;
    }

  }
  return (INT_PTR)FALSE;
}

/// --- Aboutダイアログ用プロシージャ ---
INT_PTR CALLBACK AboutDlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
  if (msg == WM_COMMAND && LOWORD(wParam) == IDCANCEL)
  {
    // 閉じるボタン or ESCキーでダイアログ終了
    EndDialog(hdlg, LOWORD(wParam));
    return (INT_PTR)TRUE;
  }
  return (INT_PTR)FALSE;
}

// メインウィンドウの表示を更新
void UpdateView()
{
  char szTmp[64];
  int current_total_sec = (isWorking ? len_work : len_break) * 60;
  BOOL isInit = (current_total_sec == time_left);
  // 時間テキストの更新
  wsprintf(szTmp, "%02d:%02d", time_left / 60, time_left % 60);
  SetWindowText(hStaticTime, szTmp);
  // ステータステキストの更新
  wsprintf(szTmp, "%s(%d分間)%s", isWorking ? "作業" : "休憩",
                                    isWorking ? len_work : len_break,
                                    isRunning ? "" : "[PAUSE]");
  SetWindowText(hStaticStatus, szTmp);
  // ボタンテキストの更新
  SetWindowText(hBtnStart, isRunning ? "一時停止" : (isInit ? "開始" : "再開"));
  // プログレスバーの更新
  SendMessage(hProgressBar, PBM_SETRANGE32, 0, current_total_sec);
  SendMessage(hProgressBar, PBM_SETPOS, time_left, 0);
}

リソースを使ったプログラムのコンパイルは、リソース定義ファイルをコンパイルしてリソースファイルを作成してから、C言語ソースコードをそれにリンクしながらコンパイルするという、2段階で行います。

その前に、tccにはリソース定義ファイルのコンパイル機能がありませんので、windresコマンドをインストールします。windresは、mingw-w64パッケージ[6]に含まれています。

brew install mingw-w64
echo "alias windres=\"i686-w64-mingw32-windres\"" >> ~/.bashrc
alias windres="i686-w64-mingw32-windres"

導入したwindresを使って、リソース定義ファイルのコンパイルと本体のコンパイルを行います。

windres resource.rc -o resource.o
tcc timer2.c resource.o -lcomctl32

timer2.exeは起動するとアイコンが見える

 

timer2.exeを起動すると、Dockにアイコン画像が表示されます。また、メインウィンドウにメニューバーが追加されており、時間を自由に設定できるダイアログもあります。デスクトッププログラムらしくなりました!

メニューバー

 

時間設定ダイアログ

 

ゲームクリエイターへの道

ゲームの雛形を作ろう

Macのデスクトップユーティリティの作り方がわかりました。次はもちろん、ゲームですよね。

パソコンゲームといってもいろいろですが、最低限

  • キーボード入力を受け付ける
  • 画面上を画像が動く
  • 音が鳴る

の3つの機能があれば、ひとかどのゲームは作れそうです。どんな感じか試してみましょう。

~/Desktop/game/フォルダを作ってそこで作業します。

game/game.c

#include <windows.h>
#include <mmsystem.h> // 時間取得とサウンド用
#include <stdbool.h>  // bool型用
#include "resource.h"

// 定数定義
#define WINDOW_WIDTH  640
#define WINDOW_HEIGHT 480
#define CLASS_NAME    "GAME"
#define FPS           60
#define FRAME_TIME    (1000 / FPS)

// グローバル変数

// 不変オブジェクト
HINSTANCE hInst;        // プロセスのハンドル
HDC hdcScreen;          // ウィンドウ表示領域DC
HDC hdcMemory;          // 内部描画用DC
HDC hdcImage;           // 画像用DC
HBITMAP hBmpOffscreen;  // 内部描画用ビットマップ
HBITMAP hBmpImage;      // 画像用ビットマップ

// 画面の状態
int pos_x = 20;   // 画像の表示位置
int pos_y = 20;
int image_width;  // 画像のサイズ
int image_height;

// 関数プロトタイプ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

/*
  ---------------------------------------------------------
                          初期化処理
  ---------------------------------------------------------
    起動時に一度だけ実行される
*/
void Init(HWND hWnd)
{
  // ウィンドウの表示領域のデバイスコンテキスト(DC)
  hdcScreen = GetDC(hWnd);

  // 内部描画用DC (ウィンドウの表示領域と同じ色数設定)
  hdcMemory = CreateCompatibleDC(hdcScreen);
  // 表示領域と同じサイズのビットマップイメージ(メモリ上)を与える
  hBmpOffscreen = CreateCompatibleBitmap(hdcScreen, WINDOW_WIDTH, WINDOW_HEIGHT);
  SelectObject(hdcMemory, hBmpOffscreen);

  // 画像用DC
  hdcImage = CreateCompatibleDC(hdcScreen);
  // リソースから画像のビットマップをロードして与える
  hBmpImage = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_IMAGE));
  SelectObject(hdcImage, hBmpImage);

  // 画像のサイズを取得
  BITMAP bm;
  GetObject(hBmpImage, sizeof(BITMAP), &bm);
  image_width = bm.bmWidth;
  image_height = bm.bmHeight;
}

/*
  ---------------------------------------------------------
                          終了処理
  ---------------------------------------------------------
    確保したリソースの解放を行う
*/
void Uninit(HWND hWnd)
{
  // DCの削除
  if (hdcMemory) DeleteDC(hdcMemory);
  if (hdcImage) DeleteDC(hdcImage);

  // 保持していたウィンドウDCの解放
  if (hdcScreen) ReleaseDC(hWnd, hdcScreen);

  // ビットマップの削除
  if (hBmpOffscreen) DeleteObject(hBmpOffscreen);
  if (hBmpImage) DeleteObject(hBmpImage);
}

/*
  ---------------------------------------------------------
                          更新処理
  ---------------------------------------------------------
    フレーム毎に実行される
*/
void Update(void)
{
  // WASDキーの状態に従って座標を加減算
  if (GetAsyncKeyState('W') & 0x8000) pos_y -= 4;
  if (GetAsyncKeyState('A') & 0x8000) pos_x -= 4;
  if (GetAsyncKeyState('S') & 0x8000) pos_y += 4;
  if (GetAsyncKeyState('D') & 0x8000) pos_x += 4;

  // スペースキーの押下で効果音再生
  static bool prevKeyState = false;
  bool keyState = (GetAsyncKeyState(VK_SPACE) & 0x8000) != 0;

  if (!prevKeyState && keyState) // 押下の瞬間
  {
    PlaySound(MAKEINTRESOURCE(IDR_SOUND), // wav音源をリソースIDで指定
              hInst,
              SND_RESOURCE | SND_ASYNC);  // リソースを使う | 非同期で再生
  }
  prevKeyState = keyState;
}

/*
  ---------------------------------------------------------
                          描画処理
  ---------------------------------------------------------
    フレーム毎に実行される
*/
void Draw(HWND hWnd)
{
  // --- 内部描画用ビットマップに対してお絵描き START ---

  // 全体を黒く塗りつぶす
  RECT rc = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
  FillRect(hdcMemory, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));

  // 画像を指定座標に描画
  BitBlt(hdcMemory, pos_x, pos_y, image_width, image_height, hdcImage, 0, 0, SRCCOPY);

  // --- 内部描画用ビットマップに対してお絵描き  END  ---

  // 内部描画用ビットマップをウィンドウの表示領域に転送
  BitBlt(hdcScreen, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, hdcMemory, 0, 0, SRCCOPY);
}

/*
  ---------------------------------------------------------
                   ウィンドウプロシージャ
  ---------------------------------------------------------
*/
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  case WM_CREATE:
    Init(hWnd);
    break;
  case WM_DESTROY:
    PostQuitMessage(0);
    break;
  default:
    return DefWindowProc(hWnd, msg, wParam, lParam);
  }
  return 0;
}

/*
  ---------------------------------------------------------
               メイン関数(エントリポイント)
  ---------------------------------------------------------
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, LPSTR pCmdLine, int nCmdShow)
{
  hInst = hInstance;
  WNDCLASS wc = { 0 };
  wc.lpfnWndProc = WndProc;
  wc.hInstance = hInstance;
  wc.lpszClassName = CLASS_NAME;
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // 背景黒

  RegisterClass(&wc);

  // ウィンドウサイズをクライアント領域に合わせる
  RECT rc = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
  AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

  HWND hWnd = CreateWindow(
      CLASS_NAME, "game",
      WS_OVERLAPPEDWINDOW | WS_VISIBLE,
      CW_USEDEFAULT, CW_USEDEFAULT,
      rc.right - rc.left, rc.bottom - rc.top,
      NULL, NULL, hInstance, NULL
      );

  /*
     ========================
         メッセージループ
     ========================
     ゲームのメインループを兼ねる
  */
  MSG msg;
  DWORD prevTime = timeGetTime();

  while (TRUE)
  {
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
      if (msg.message == WM_QUIT) break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    else
    {
      // フレームレート制御
      DWORD curTime = timeGetTime();
      if (curTime - prevTime >= FRAME_TIME)
      {
        prevTime = curTime;
        Update();
        Draw(hWnd);
      }
      else
      {
        Sleep(2); // 2ミリ秒間CPUを解放して負荷軽減
      }
    }
  }

  return (int)msg.wParam;
}

game/resource.h

#ifndef RESOURCE_H
#define RESOURCE_H

// リソースIDの定義
#define IDB_IMAGE    101
#define IDR_SOUND    102

#endif

game/resource.rc

#include "resource.h"

// ビットマップの登録
// ID       TYPE    FILENAME
IDB_IMAGE  BITMAP  "pomodoro.bmp"
/* 魔王魂 戦闘効果音 銃01 https://maou.audio/se_battle_gun01/ */
IDR_SOUND  WAVE    "maou_se_battle_gun01.wav"

このほか、先ほど作ったpomodoro.bmpと、魔王魂様から拝借した銃撃の効果音maou_se_battle_gun01.wavをgame/以下に配置します。

game/

 

動かしてみましょう。

cd ~/Desktop/game
windres resource.rc -o resource.o
tcc game.c resource.o -lwinmm

game.exeが動いている様子

 

gifでは伝わりませんが、スペースキーを押すたびに銃撃音が鳴っています。この基礎を使えば、何かゲームらしいものが作れそうですね。

本格シューティングゲーム!

基礎ができたら、次は本格的なシューティングゲームを実装してみましょう。

~/Desktop/stg/フォルダで作業していきます。

stg/stg.c

#include 
#include  // timeGetTime, PlaySound用
#include    // 乱数取得
#include      // 敵の狙撃用
#include "resource.h"
#include "sprite.h"   // 画像透過

// 定数定義
#define WINDOW_WIDTH  640
#define WINDOW_HEIGHT 480
#define CLASS_NAME    "STG_GAME"
#define FPS           60
#define FRAME_TIME    (1000 / FPS)

// ゲーム状態
#define STATE_TITLE     0
#define STATE_PLAY      1
#define STATE_GAMEOVER  2

// オブジェクト最大数
#define MAX_PLAYER_BULLETS  30
#define MAX_ENEMY_BULLETS   100
#define MAX_ENEMIES         50

// 構造体定義

// プレイヤー
typedef struct {
  double x, y;
  double speed;
  int w, h;      // 当たり判定用にサイズを保持
  Sprite sprite;
} Player;

// 弾
typedef struct {
  double x, y;
  double vx, vy; // 速度ベクトル
  int active;    // 0:無効, 1:有効
} Bullet;

// 敵
typedef struct {
  double x, y;
  double speed;
  int active;
  int reloadTime; // 次に弾を撃つまでの時間
} Enemy;

// グローバル変数

// 不変オブジェクト
HINSTANCE hInst;            // プロセスのハンドル
HDC hdcScreen;              // ウィンドウ表示領域DC
HDC hdcMemory;              // 内部描画用DC
HDC hdcImage;               // 画像用DC
HBITMAP hBmpOffscreen;      // 内部描画用ビットマップ
HFONT hFontScore, hFontBig; // フォント
HBITMAP hBmpBgBack;         // 遠景画像
Sprite  spriteCloud;        // 近景画像
Sprite  spriteEnemy;        // 敵画像

// ゲームデータ
int gameState = STATE_TITLE;
DWORD gameStartTime;
int score = 0;
int highScore = 0;
int difficultyLevel = 0; // 時間経過で上昇

// オブジェクト実体
Player player;                            // 自機
Bullet playerBullets[MAX_PLAYER_BULLETS]; // 自機弾
Bullet enemyBullets[MAX_ENEMY_BULLETS];   // 敵弾
Enemy  enemies[MAX_ENEMIES];              // 敵

// スクロール用
double scroll_y_back = 0.0;
double scroll_y_front = 0.0;

// タイマー・制御用
int shotCooldown = 0;
int spawnTimer = 0;

// 関数プロトタイプ
void DrawPanel(RECT, int, COLORREF, COLORREF);
void TextOutShadow(HDC, int, int, const char*, int, COLORREF, COLORREF);
void TextOutCenteredShadow(HDC, int, const char*, int, COLORREF, COLORREF);
int CheckCollision(double, double, int, int, double, double, int, int);
void DrawScrollingBackground(HDC, HBITMAP, int);
void DrawScrollingCloud(HDC, Sprite*, int);
void Init(HWND);
void Uninit(void);
void ResetGame(void);
void Update(void);
void Draw(HWND hWnd);
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

/*
  ---------------------------------------------------------
                          初期化処理
  ---------------------------------------------------------
    起動時に一度だけ実行される
*/
void Init(HWND hWnd)
{
  // ウィンドウの表示領域のデバイスコンテキスト(DC)
  hdcScreen = GetDC(hWnd);

  // 内部描画用DC (ウィンドウの表示領域と同じ色数設定)
  hdcMemory = CreateCompatibleDC(hdcScreen);
  // 表示領域と同じサイズのビットマップイメージ(メモリ上)を与える
  hBmpOffscreen = CreateCompatibleBitmap(hdcScreen, WINDOW_WIDTH, WINDOW_HEIGHT);
  SelectObject(hdcMemory, hBmpOffscreen);

  // フォント作成 (Arial, 太字)
  hFontBig = CreateFont(45, 0, 0, 0, FW_BOLD, 0, 0, 0, ANSI_CHARSET,
    OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
    DEFAULT_PITCH | FF_DONTCARE, "Arial");
  hFontScore = CreateFont(25, 0, 0, 0, FW_BOLD, 0, 0, 0, ANSI_CHARSET,
    OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
    DEFAULT_PITCH | FF_DONTCARE, "Arial");

  // 画像ロード
  hBmpBgBack = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BG_BACK));
  LoadSprite(hInst, &spriteCloud, IDB_BG_FRONT);
  LoadSprite(hInst, &player.sprite, IDB_PLAYER);
  LoadSprite(hInst, &spriteEnemy, IDB_ENEMY);

  // プレイヤーサイズ設定
  player.w = player.sprite.width;
  player.h = player.sprite.height;

  ResetGame();
}

/*
  ---------------------------------------------------------
                          終了処理
  ---------------------------------------------------------
    確保したリソースの解放を行う
*/
void Uninit(void)
{
  DeleteObject(hBmpBgBack);
  UnloadSprite(&spriteCloud);
  UnloadSprite(&player.sprite);
  UnloadSprite(&spriteEnemy);
  DeleteObject(hBmpOffscreen);
  DeleteDC(hdcMemory);
  DeleteObject(hFontScore);
  DeleteObject(hFontBig);
}

/*
  ---------------------------------------------------------
                      ゲームリセット
  ---------------------------------------------------------
    ゲーム開始・再開時にパラメータを初期化する
*/
void ResetGame(void)
{
  // プレイヤー初期位置
  player.x = (WINDOW_WIDTH - player.w) / 2.0;
  player.y = WINDOW_HEIGHT - player.h - 50.0;
  player.speed = 5.0;

  // 配列クリア
  for (int i = 0; i < MAX_PLAYER_BULLETS; i++) playerBullets[i].active = 0;
  for (int i = 0; i < MAX_ENEMY_BULLETS; i++) enemyBullets[i].active = 0;
  for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].active = 0;

  score = 0;
  gameStartTime = timeGetTime();
  spawnTimer = 0;
  shotCooldown = 0;
}

/*
  ---------------------------------------------------------
                          更新処理
  ---------------------------------------------------------
    フレーム毎の計算処理(入力、移動、当たり判定など)
*/
void Update()
{
  // 背景スクロール
  scroll_y_back += 1.0;
  scroll_y_front += 3.0;

  // --- タイトル画面 ---
  if (gameState == STATE_TITLE)
  {
    if (GetAsyncKeyState(VK_RETURN) & 0x8000)
    {
      ResetGame();
      gameState = STATE_PLAY;
    }
    return;
  }

  // --- ゲームオーバー画面 ---
  if (gameState == STATE_GAMEOVER)
  {
    if (GetAsyncKeyState(VK_RETURN) & 0x8000)
    {
      ResetGame();
      gameState = STATE_PLAY;
    }

    if (GetAsyncKeyState('R') & 0x8000)
    {
      gameState = STATE_TITLE;
    }
    return;
  }

  // --- プレイ中ロジック ---

  // 難易度進行 (10秒ごとに難しくなる)
  DWORD currentTime = timeGetTime();
  difficultyLevel = (currentTime - gameStartTime) / 10000;

  // 自機移動 (WASD)
  if (GetAsyncKeyState('A') & 0x8000) player.x -= player.speed;
  if (GetAsyncKeyState('D') & 0x8000) player.x += player.speed;
  if (GetAsyncKeyState('W') & 0x8000) player.y -= player.speed;
  if (GetAsyncKeyState('S') & 0x8000) player.y += player.speed;

  // 画面外クランプ
  if (player.x < 0) player.x = 0;
  if (player.x > WINDOW_WIDTH - player.w) player.x = WINDOW_WIDTH - player.w;
  if (player.y < 0) player.y = 0;
  if (player.y > WINDOW_HEIGHT - player.h) player.y = WINDOW_HEIGHT - player.h;

  // 自機ショット
  if (shotCooldown > 0) shotCooldown--;
  if ((GetAsyncKeyState(VK_SPACE) & 0x8000) && shotCooldown <= 0)
  {
    for (int i = 0; i < MAX_PLAYER_BULLETS; i++)
    {
      if (!playerBullets[i].active)
      {
        playerBullets[i].active = 1;
        playerBullets[i].x = player.x + (player.w / 2) - 2; 
        playerBullets[i].y = player.y;
        playerBullets[i].vx = 0;
        playerBullets[i].vy = -12.0; // まっすぐ上へ
        shotCooldown = 60 * 0.5;
        PlaySound(MAKEINTRESOURCE(IDR_SE_SHOT), // wav音源をリソースIDで指定
                  hInst,
                  SND_RESOURCE | SND_ASYNC);    // リソースを使う | 非同期で再生
        break;
      }
    }
  }

  // 自機弾の移動
  for (int i = 0; i < MAX_PLAYER_BULLETS; i++)
  {
    if (playerBullets[i].active)
    {
      playerBullets[i].x += playerBullets[i].vx;
      playerBullets[i].y += playerBullets[i].vy;
      if (playerBullets[i].y < -20) playerBullets[i].active = 0;
    }
  }

  // 敵の出現 (難易度に応じて頻度アップ)
  spawnTimer++;
  int spawnRate = 50 - (difficultyLevel * 3);
  if (spawnRate < 10) spawnRate = 10;

  if (spawnTimer >= spawnRate)
  {
    spawnTimer = 0;
    for (int i = 0; i < MAX_ENEMIES; i++)
    {
      if (!enemies[i].active)
      {
        enemies[i].active = 1;
        enemies[i].x = rand() % (WINDOW_WIDTH - spriteEnemy.width);
        enemies[i].y = -spriteEnemy.height;
        enemies[i].speed = 2.0 + (difficultyLevel * 0.2);
        enemies[i].reloadTime = 30 + (rand() % 60); // 出現直後は撃たない
        break;
      }
    }
  }

  // 敵の更新 (移動・射撃・衝突)
  for (int i = 0; i < MAX_ENEMIES; i++)
  {
    if (!enemies[i].active) continue;

    // 移動
    enemies[i].y += enemies[i].speed;
    if (enemies[i].y > WINDOW_HEIGHT) enemies[i].active = 0;

    // 射撃ロジック (自機狙い)
    if (enemies[i].reloadTime > 0) enemies[i].reloadTime--;
    else
    {
      // 画面内(上部〜下部手前)にいる時のみ発射
      if (enemies[i].y > 0 && enemies[i].y < WINDOW_HEIGHT - 100)
      {
        for (int k = 0; k < MAX_ENEMY_BULLETS; k++)
        {
          if (!enemyBullets[k].active)
          {
            enemyBullets[k].active = 1;
            // 発射位置
            double bx = enemies[i].x + spriteEnemy.width / 2.0;
            double by = enemies[i].y + spriteEnemy.height / 2.0;
            enemyBullets[k].x = bx;
            enemyBullets[k].y = by;

            // 狙い撃ちベクトル計算
            double dx = (player.x + player.w / 2.0) - bx;
            double dy = (player.y + player.h / 2.0) - by;
            double angle = atan2(dy, dx);
            double speed = 5.0 + (difficultyLevel * 0.1); // 難易度で弾速も微増

            enemyBullets[k].vx = cos(angle) * speed;
            enemyBullets[k].vy = sin(angle) * speed;

            enemies[i].reloadTime = 60 + (rand() % 60); // リロード
            break;
          }
        }
      }
    }

    // 当たり判定: 自機弾 vs 敵
    for (int j = 0; j < MAX_PLAYER_BULLETS; j++)
    {
      if (!playerBullets[j].active) continue;
      // 弾のサイズは 4x12
      if (CheckCollision(playerBullets[j].x, playerBullets[j].y, 4, 12,
                         enemies[i].x, enemies[i].y, spriteEnemy.width, spriteEnemy.height))
      {
        playerBullets[j].active = 0;
        enemies[i].active = 0;
        score += 100;
        PlaySound(MAKEINTRESOURCE(IDR_SE_HIT), // wav音源をリソースIDで指定
                  hInst,
                  SND_RESOURCE | SND_ASYNC);    // リソースを使う | 非同期で再生
      }
    }

    // 当たり判定: 自機 vs 敵本体
    // 当たり判定を甘くするため、自機矩形を少し小さく(+5)計算
    if (CheckCollision(player.x + 5, player.y + 5, player.w - 10, player.h - 10,
                       enemies[i].x, enemies[i].y, spriteEnemy.width, spriteEnemy.height))
    {
      gameState = STATE_GAMEOVER;
      if (score > highScore) highScore = score;
      PlaySound(MAKEINTRESOURCE(IDR_SE_HIT), // wav音源をリソースIDで指定
                hInst,
                SND_RESOURCE | SND_ASYNC);    // リソースを使う | 非同期で再生
    }
  }

  // 敵弾の更新
  for (int i = 0; i < MAX_ENEMY_BULLETS; i++)
  {
    if (!enemyBullets[i].active) continue;

    enemyBullets[i].x += enemyBullets[i].vx;
    enemyBullets[i].y += enemyBullets[i].vy;

    // 画面外消去
    if (enemyBullets[i].x < -20 || enemyBullets[i].x > WINDOW_WIDTH + 20 ||
        enemyBullets[i].y < -20 || enemyBullets[i].y > WINDOW_HEIGHT + 20)
    {
      enemyBullets[i].active = 0;
      continue;
    }

    // 当たり判定: 敵弾 vs 自機
    // 弾の判定サイズは 8x8
    if (CheckCollision(player.x + 8, player.y + 8, player.w - 16, player.h - 16,
                       enemyBullets[i].x - 4, enemyBullets[i].y - 4, 8, 8))
    {
      gameState = STATE_GAMEOVER;
      if (score > highScore) highScore = score;
      PlaySound(MAKEINTRESOURCE(IDR_SE_HIT), // wav音源をリソースIDで指定
                hInst,
                SND_RESOURCE | SND_ASYNC);    // リソースを使う | 非同期で再生
    }
  }
}

/*
  ---------------------------------------------------------
                          描画処理
  ---------------------------------------------------------
    フレーム毎の描画処理(裏画面への描画と転送)
*/
void Draw(HWND hWnd)
{
  // --- 内部描画用ビットマップに対してお絵描き START ---

  // 全体を黒く塗りつぶす
  RECT rc = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
  FillRect(hdcMemory, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));

  // 背景描画
  if (hBmpBgBack) DrawScrollingBackground(hdcMemory, hBmpBgBack, (int)scroll_y_back);
  if (spriteCloud.hBmpImage) DrawScrollingCloud(hdcMemory, &spriteCloud, (int)scroll_y_front);

  // ゲームオブジェクト描画 (プレイ中・ゲームオーバー時)
  if (gameState == STATE_PLAY || gameState == STATE_GAMEOVER)
  {
    // 自機 (ゲームオーバーでも表示を残す場合はここ)
    if (gameState == STATE_PLAY)
    {
      DrawSprite(hdcMemory, (int)player.x, (int)player.y, &player.sprite);
    }

    // 敵
    for (int i = 0; i < MAX_ENEMIES; i++)
    {
      if (enemies[i].active)
      {
        DrawSprite(hdcMemory, (int)enemies[i].x, (int)enemies[i].y, &spriteEnemy);
      }
    }

    // 自機弾 (黄色い長方形)
    for (int i = 0; i < MAX_PLAYER_BULLETS; i++)
    {
      if (playerBullets[i].active)
      {
        RECT rc = { (int)playerBullets[i].x,
                    (int)playerBullets[i].y,
                    (int)playerBullets[i].x + 4,
                    (int)playerBullets[i].y + 12 };
        DrawPanel(rc, 1, RGB(255,255,0), RGB(255,50,0));
      }
    }

    // 敵弾 (赤い円)
    HBRUSH hBrushEnemyBullet = CreateSolidBrush(RGB(255, 50, 50));
    HPEN hPenNone = GetStockObject(NULL_PEN);
    HBRUSH hOldBrush2 = (HBRUSH)SelectObject(hdcMemory, hBrushEnemyBullet);
    HPEN hOldPen = (HPEN)SelectObject(hdcMemory, hPenNone);

    for (int i = 0; i < MAX_ENEMY_BULLETS; i++)
    {
      if (enemyBullets[i].active)
      {
        int bx = (int)enemyBullets[i].x;
        int by = (int)enemyBullets[i].y;
        Ellipse(hdcMemory, bx - 4, by - 4, bx + 4, by + 4);
      }
    }
    SelectObject(hdcMemory, hOldBrush2);
    SelectObject(hdcMemory, hOldPen);
    DeleteObject(hBrushEnemyBullet);
  }

  // タイトル画面
  if (gameState == STATE_TITLE)
  {
    char* title = "Mac Shooting";
    char* msg = "PRESS ENTER KEY";

    RECT rc = { 140, 120, WINDOW_WIDTH-140, WINDOW_HEIGHT-130 };
    DrawPanel(rc, 2, RGB(250,250,250), RGB(200,200,200));

    SelectObject(hdcMemory, hFontBig);
    TextOutCenteredShadow(hdcMemory, 150, title, 2, RGB(0,200,200), RGB(50,50,50));

    SelectObject(hdcMemory, hFontScore);
    TextOutCenteredShadow(hdcMemory, 250, msg, 1, RGB(100,100,100), RGB(10,10,10));
  }

  // ゲームオーバー画面
  if (gameState == STATE_GAMEOVER)
  {
    char* msg1 = "GAME OVER";
    char* msg2 = "PRESS ENTER TO CONTINUE, R TO TITLE";

    RECT rc = { 70, 120, WINDOW_WIDTH-70, WINDOW_HEIGHT-130 };
    DrawPanel(rc, 2, RGB(250,250,250), RGB(200,200,200));

    SelectObject(hdcMemory, hFontBig);
    TextOutCenteredShadow(hdcMemory, 150, msg1, 2, RGB(255,0,0), RGB(50,50,50));

    SelectObject(hdcMemory, hFontScore);
    TextOutCenteredShadow(hdcMemory, 230, msg2, 1, RGB(100,100,100), RGB(10,10,10));
  }

  // スコア表示(プレイ・ゲームオーバー共通)
  if (gameState == STATE_PLAY || gameState == STATE_GAMEOVER)
  {
    char szScore[32];
    int x, y;
    wsprintf(szScore, "SCORE: %05d", score);
    SelectObject(hdcMemory, hFontScore);
    if (gameState == STATE_PLAY)
    {
      TextOutShadow(hdcMemory, 10, 10, szScore, 1, RGB(240,130,20), RGB(50,50,50));
    }
    if (gameState == STATE_GAMEOVER)
    {
      TextOutCenteredShadow(hdcMemory, 270, szScore, 1, RGB(240,130,20), RGB(50,50,50));
    }
  }

  // ハイスコア表示(タイトル・ゲームオーバー共通)
  if (gameState == STATE_TITLE || gameState == STATE_GAMEOVER)
  {
    char szHigh[32];
    wsprintf(szHigh, "HIGH SCORE: %05d", highScore);
    SelectObject(hdcMemory, hFontScore);
    TextOutCenteredShadow(hdcMemory, 300, szHigh, 1, RGB(240,130,50), RGB(50,50,50));
  }
  // --- 内部描画用ビットマップに対してお絵描き  END  ---

  // 内部描画用ビットマップをウィンドウの表示領域に転送
  BitBlt(hdcScreen, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, hdcMemory, 0, 0, SRCCOPY);
}

/*
  ---------------------------------------------------------
                   ウィンドウプロシージャ
  ---------------------------------------------------------
*/
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  case WM_CREATE:
    Init(hWnd);
    break;
  case WM_DESTROY:
    Uninit();
    PostQuitMessage(0);
    break;
  case WM_KEYDOWN:
    if (wParam == VK_ESCAPE) DestroyWindow(hWnd);
    break;
  default:
    return DefWindowProc(hWnd, msg, wParam, lParam);
  }
  return 0;
}

/*
  ---------------------------------------------------------
               メイン関数(エントリポイント)
  ---------------------------------------------------------
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, LPSTR pCmdLine, int nCmdShow)
{
  hInst = hInstance;
  srand((unsigned int)timeGetTime()); // 乱数シード初期化

  WNDCLASS wc = { 0 };
  wc.lpfnWndProc = WndProc;
  wc.hInstance = hInstance;
  wc.lpszClassName = CLASS_NAME;
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

  if (!RegisterClass(&wc)) return 0;

  // ウィンドウサイズをクライアント領域に合わせる
  RECT rc = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
  AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

  HWND hWnd = CreateWindow(
      CLASS_NAME, "Mac Shooting",
      WS_OVERLAPPEDWINDOW | WS_VISIBLE,
      CW_USEDEFAULT, CW_USEDEFAULT,
      rc.right - rc.left, rc.bottom - rc.top,
      NULL, NULL, hInstance, NULL
      );

  if (!hWnd) return 0;

  /*
     ========================
         メッセージループ
     ========================
  */
  MSG msg;
  DWORD prevTime = timeGetTime();

  while (TRUE)
  {
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
      if (msg.message == WM_QUIT) break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    else
    {
      // フレームレート制御
      DWORD curTime = timeGetTime();
      if (curTime - prevTime >= FRAME_TIME)
      {
        prevTime = curTime;
        Update();
        Draw(hWnd);
      }
      else
      {
        Sleep(2); // CPU負荷軽減
      }
    }
  }
  return (int)msg.wParam;
}

/*
  ---------------------------------------------------------
                        ヘルパ関数
  ---------------------------------------------------------
*/

// 矩形当たり判定 (AABB)
int CheckCollision(double x1, double y1, int w1, int h1, double x2, double y2, int w2, int h2)
{
  return (x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2);
}

// 背景スクロール描画
void DrawScrollingBackground(HDC hdc, HBITMAP hBmp, int y)
{
  HDC hdcImage = CreateCompatibleDC(hdc);
  HBITMAP hOld = (HBITMAP)SelectObject(hdcImage, hBmp);
  BITMAP bm;
  GetObject(hBmp, sizeof(BITMAP), &bm);

  int draw_y = y % bm.bmHeight;
  BitBlt(hdc, 0, draw_y, WINDOW_WIDTH, bm.bmHeight, hdcImage, 0, 0, SRCCOPY);
  if (draw_y > 0)
  {
    BitBlt(hdc, 0, draw_y - bm.bmHeight, WINDOW_WIDTH, bm.bmHeight, hdcImage, 0, 0, SRCCOPY);
  }
  SelectObject(hdcImage, hOld);
  DeleteDC(hdcImage);
}

// 雲のスクロール描画
void DrawScrollingCloud(HDC hdc, Sprite* sp, int y)
{
  int draw_y = y % sp->height;
  DrawSprite(hdc, 0, draw_y, sp);
  if (draw_y > 0) DrawSprite(hdc, 0, draw_y - sp->height, sp);
}

/*
  ---------------------------------------------------------
                  影付きテキスト描画
  ---------------------------------------------------------
    指定座標に影付きで文字列を描画する
    x, y     : メイン文字の座標
    offset   : 影のズレ幅
    mainCol  : メイン文字色
    shadowCol: 影の色
*/
void TextOutShadow(HDC hdc, int x, int y, const char* text, int offset, COLORREF mainCol, COLORREF shadowCol)
{
  // 現在の背景モードを保存
  int oldBkMode = GetBkMode(hdc);
  SetBkMode(hdc, TRANSPARENT);

  // 影を描画
  SetTextColor(hdc, shadowCol);
  TextOut(hdc, x + offset, y + offset, text, lstrlen(text));

  // 本体を描画
  SetTextColor(hdc, mainCol);
  TextOut(hdc, x, y, text, lstrlen(text));

  // 背景モードを戻す
  SetBkMode(hdc, oldBkMode);
}

/*
  ---------------------------------------------------------
              中央揃えで影付きテキスト描画
  ---------------------------------------------------------
    画面の横幅(WINDOW_WIDTH)に対して中央に文字列を描画する
    y        : Y座標
    text     : 文字列
    offset   : 影のズレ幅
    mainCol  : メイン文字色
    shadowCol: 影の色
*/
void TextOutCenteredShadow(HDC hdc, int y, const char* text, int offset, COLORREF mainCol, COLORREF shadowCol)
{
    // 文字列の描画サイズを取得
    SIZE size;
    GetTextExtentPoint32(hdc, text, lstrlen(text), &size);

    // 中央座標を計算: (ウィンドウ幅 - 文字列幅) / 2
    int x = (WINDOW_WIDTH - size.cx) / 2;

    TextOutShadow(hdc, x, y, text, offset, mainCol, shadowCol);
}

// 指定した色・枠線の長方形を描画
void DrawPanel(RECT rcPanel, int borderWidth, COLORREF fillCol, COLORREF borderCol)
{
  // 塗り潰しブラシ
  HBRUSH hBrush = CreateSolidBrush(fillCol);
  // 枠線ペン
  HPEN hPen = CreatePen(PS_SOLID, borderWidth, borderCol);

  // DCに選択
  HBRUSH hOldBrush = (HBRUSH)SelectObject(hdcMemory, hBrush);
  HPEN hOldPen = (HPEN)SelectObject(hdcMemory, hPen);

  // 長方形を描画 (塗りつぶし + 枠線)
  Rectangle(hdcMemory, rcPanel.left, rcPanel.top, rcPanel.right, rcPanel.bottom);

  // 後始末
  SelectObject(hdcMemory, hOldBrush);
  SelectObject(hdcMemory, hOldPen);
  DeleteObject(hBrush);
  DeleteObject(hPen);
}

stg/sprite.h

#ifndef SPRITE_H
#define SPRITE_H

#include <windows.h>

// 透過スプライト管理用構造体
typedef struct {
  HBITMAP hBmpImage; // 元画像
  HBITMAP hBmpMask;  // 自動生成されるマスク
  int width, height;
} Sprite;

// リソースIDから画像を読み込み、マスクを自動生成する
void LoadSprite(HINSTANCE hInst, Sprite* sp, int resourceId);

// スプライトのメモリを解放する
void UnloadSprite(Sprite* sp);

// 指定座標にスプライトを描画する
void DrawSprite(HDC hdcDest, int x, int y, Sprite* sp);

#endif

stg/sprite.c

#include "sprite.h"

/*
  ---------------------------------------------------------
                  マスク画像の生成(内部関数)
  ---------------------------------------------------------
    指定されたビットマップから、透過処理用のマスク画像を生成する
*/
static void CreateMaskFromBitmap(Sprite* sp)
{
  BITMAP bm;
  GetObject(sp->hBmpImage, sizeof(BITMAP), &bm);
  sp->width = bm.bmWidth;
  sp->height = bm.bmHeight;

  // 作業用DCの取得
  HDC hdcScreen = GetDC(NULL);
  HDC hdcSrc = CreateCompatibleDC(hdcScreen);
  HDC hdcMask = CreateCompatibleDC(hdcScreen);

  // モノクロビットマップ作成
  sp->hBmpMask = CreateBitmap(sp->width, sp->height, 1, 1, NULL);

  // DCにビットマップを選択
  HBITMAP hOldSrc = (HBITMAP)SelectObject(hdcSrc, sp->hBmpImage);
  HBITMAP hOldMask = (HBITMAP)SelectObject(hdcMask, sp->hBmpMask);

  // マスク生成
  // 1. ソースの背景色を黒に設定
  SetBkColor(hdcSrc, RGB(0, 0, 0));
  // 2. ソースの黒い場所を1(白)に、それ以外を0(黒)にして転送
  BitBlt(hdcMask, 0, 0, sp->width, sp->height, hdcSrc, 0, 0, SRCCOPY);

  // 後始末
  SelectObject(hdcSrc, hOldSrc);
  SelectObject(hdcMask, hOldMask);
  DeleteDC(hdcSrc);
  DeleteDC(hdcMask);
  ReleaseDC(NULL, hdcScreen);
}

/*
  ---------------------------------------------------------
                      スプライトのロード
  ---------------------------------------------------------
    リソースIDから画像を読み込み、マスクを自動生成する
*/
void LoadSprite(HINSTANCE hInst, Sprite* sp, int resourceId)
{
  sp->hBmpImage = LoadBitmap(hInst, MAKEINTRESOURCE(resourceId));
  if (sp->hBmpImage) CreateMaskFromBitmap(sp);
}

/*
  ---------------------------------------------------------
                      スプライトの解放
  ---------------------------------------------------------
    メモリを解放する
*/
void UnloadSprite(Sprite* sp)
{
  if (sp->hBmpImage) DeleteObject(sp->hBmpImage);
  if (sp->hBmpMask) DeleteObject(sp->hBmpMask);
  sp->hBmpImage = NULL;
  sp->hBmpMask = NULL;
}

/*
  ---------------------------------------------------------
                      スプライトの描画
  ---------------------------------------------------------
    マスク画像を使用して透過描画を行う
*/
void DrawSprite(HDC hdcDest, int x, int y, Sprite* sp)
{
  // null画像排除
  if (!sp->hBmpImage || !sp->hBmpMask) return;

  HDC hdcMemImg = CreateCompatibleDC(hdcDest);

  // 文字色を黒、背景色を白に設定
  COLORREF oldText = SetTextColor(hdcDest, RGB(0, 0, 0));
  COLORREF oldBk = SetBkColor(hdcDest, RGB(255, 255, 255));

  // 1. マスク適用 (背景andマスク: キャラ部分(マスク0=黒)が黒く塗りつぶされる)
  HBITMAP hOld = (HBITMAP)SelectObject(hdcMemImg, sp->hBmpMask);
  BitBlt(hdcDest, x, y, sp->width, sp->height, hdcMemImg, 0, 0, SRCAND);

  // 2. 画像適用 (背景or画像: 黒く塗りつぶされた領域にのみキャラ画像が反映される)
  SelectObject(hdcMemImg, sp->hBmpImage);
  BitBlt(hdcDest, x, y, sp->width, sp->height, hdcMemImg, 0, 0, SRCPAINT);

  // 色設定を元に戻す
  SetTextColor(hdcDest, oldText);
  SetBkColor(hdcDest, oldBk);

  SelectObject(hdcMemImg, hOld);
  DeleteDC(hdcMemImg);
}

stg/resource.h

#ifndef RESOURCE_H
#define RESOURCE_H

// リソースIDの定義
#define IDB_BG_BACK    101
#define IDB_BG_FRONT   102
#define IDB_PLAYER     103
#define IDB_ENEMY      104
#define IDR_SE_SHOT    105
#define IDR_SE_HIT     106

#endif

stg/resource.rc

#include "resource.h"

// ビットマップの登録
// ID       TYPE    FILENAME
IDB_BG_BACK  BITMAP  "bg_back.bmp"
IDB_BG_FRONT BITMAP  "bg_front.bmp"
IDB_PLAYER   BITMAP  "player.bmp"
IDB_ENEMY    BITMAP  "enemy.bmp"
/* 魔王魂 戦闘効果音 銃01 https://maou.audio/se_battle_gun01/ */
IDR_SE_SHOT  WAVE    "maou_se_battle_gun01.wav"
/* 魔王魂 戦闘効果音 爆発01 https://maou.audio/se_battle_explosion05/ */
IDR_SE_HIT  WAVE    "maou_se_battle_explosion05.wav"

このほか、4つのbmp画像(bg_back.bmp[640x954], bg_front.bmp[640x954], player.bmp[32x32], enemy.bmp[32x32])と、魔王魂様から拝借した2つのwav効果音(maou_se_battle_gun01.wav, maou_se_battle_explosion05.wav)をリソースとして使用します。画像を使いたい人は記事最後の配布コーナーのものを使用してください。

stg/


sprite.cが気になる人がいるかも知れません。game.cでは単純に長方形のビットマップ画像を描画していましたが、今回は自機や雲など、画像を長方形ではない形に描画する必要があります。ビットマップ画像自体は長方形でなければいけない[7]ので、真っ黒を透過色として、マスク画像を生成して透過描画を実現しています。[8]

windres -o resource.o resource.rc
tcc stg.c sprite.c resource.o -lwinmm

stg.exeのプレイ映像


どうでしょうか。

グラフィックやサウンドの機能を使いやすく提供してくれるプログラミング環境を「ゲームエンジン」と呼ぶことがありますが、それを踏まえるとこのようなOS標準のAPIこそがゲームエンジンであると言えますし、Macこそが最強のゲーミングマシンであることもお分かりになるかと思います。

3Dグラフィックに挑戦してみよう!

2Dゲームを作ったら、次は3D!というわけで、3Dグラフィックに挑戦してみましょう。

3Dグラフィックを行うためには、やはりそれ専用のAPIを用いるのが得策です。もちろん、Macにも3Dグラフィック用のAPIが備わっています。今回用いるDirectXは、Macで遊べるゲームの大多数が使用している、本格的なグラフィックAPIです![3]

room3d.c

#include <windows.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <d3d9.h>

// --- TCC互換性用定義 ---
// TCCのmath.hはfloat版関数が不足しているためマクロで対応
#define sqrtf(x) ((float)sqrt((double)(x)))
#define sinf(x)  ((float)sin((double)(x)))
#define cosf(x)  ((float)cos((double)(x)))
#define tanf(x)  ((float)tan((double)(x)))

// 定数定義
#define SCREEN_WIDTH  1024
#define SCREEN_HEIGHT 768
#define CLASS_NAME    "MAC_DX9_DEMO"
#define PI            3.14159265f

// 部屋の設定
#define ROOM_WIDTH     30.0f
#define ROOM_HEIGHT    6.0f
#define WALL_DIVISIONS 20

// オブジェクト設定
#define PILLAR_SIZE 2.5f   // 柱の太さ
#define PILLAR_POS  10.0f  // 柱の位置(中心からの距離)
#define CUBE_SIZE   2.5f   // 中央キューブの大きさ
#define LIGHT_SIZE  1.0f   // 光源オブジェクトの大きさ

// 頂点フォーマット定義
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)

// 構造体定義

// 3次元ベクトル
typedef struct {
  float x, y, z;
} Vec3;

// 頂点データ
typedef struct {
  FLOAT x, y, z;    // 座標
  FLOAT nx, ny, nz; // 法線
  FLOAT tu, tv;     // テクスチャ座標
} CUSTOMVERTEX;

// グローバル変数

// Direct3D オブジェクト
LPDIRECT3D9             g_pD3D       = NULL;
LPDIRECT3DDEVICE9       g_pd3dDevice = NULL;
LPDIRECT3DVERTEXBUFFER9 g_pVB        = NULL;

// テクスチャリソース
LPDIRECT3DTEXTURE9      g_pTexBrick    = NULL; // 壁用
LPDIRECT3DTEXTURE9      g_pTexConcrete = NULL; // 床・天井用
LPDIRECT3DTEXTURE9      g_pTexWood     = NULL; // 柱用
LPDIRECT3DTEXTURE9      g_pTexPlasma   = NULL; // キューブ用

// 描画管理(頂点バッファ内のオフセットとサイズ)
int g_TotalVertices = 0;
int g_RoomStartVert = 0;
int g_RoomVertCount = 0;
int g_PillarStartVert = 0;
int g_PillarVertCount = 0;
int g_CubeStartVert = 0;
int g_CubeVertCount = 0;
int g_LightObjStartVert = 0;
int g_LightObjVertCount = 0;

// カメラ・シーン状態
Vec3  g_CamPos = { 0.0f, 0.0f, -25.0f };    // カメラ位置
Vec3  g_LightPos = { 5.0f, 5.3f, -5.0f };   // 光源位置
float g_CameraYaw = 0.0f;                   // カメラ水平角度
float g_CameraPitch = 0.0f;                 // カメラ垂直角度
float g_CubeRotation = 0.0f;                // キューブ回転角

// 入力制御
int  g_LastMouseX = 0;
int  g_LastMouseY = 0;
BOOL g_IsDragging = FALSE;

// 関数プロトタイプ

// 数学ヘルパー
void Vec3Normalize(Vec3* v);
void MatrixIdentity(D3DMATRIX* m);
void MatrixRotationYawPitchRoll(D3DMATRIX* out, float yaw, float pitch, float roll);
void MatrixTranslation(D3DMATRIX* out, float x, float y, float z);
void MatrixMultiply(D3DMATRIX* out, const D3DMATRIX* m1, const D3DMATRIX* m2);
void MatrixLookAtLH(D3DMATRIX* out, const Vec3* eye, const Vec3* at, const Vec3* up);
void MatrixPerspectiveFovLH(D3DMATRIX* out, float fovy, float aspect, float zn, float zf);

// テクスチャ生成
void CreateBrickTexture(void);
void CreateConcreteTexture(void);
void CreateWoodTexture(void);
void CreatePlasmaTexture(void);

// ジオメトリ生成
void CreatePlane(CUSTOMVERTEX** pPtr, Vec3 origin, Vec3 uAxis, Vec3 vAxis, Vec3 normal, int div, float maxU, float maxV);
void CreateBox(CUSTOMVERTEX** pPtr, Vec3 center, float sx, float sy, float sz, int div, float repU, float repV);
HRESULT InitGeometry(void);

// システム
HRESULT InitD3D(HWND hWnd);
void Cleanup(void);
void UpdateInput(void);
void Render(void);
LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

/*
  ---------------------------------------------------------
                  テクスチャ生成処理
  ---------------------------------------------------------
    プロシージャルにテクスチャを作成してメモリにロックする
*/

void CreateBrickTexture()
{
  int w = 256;
  int h = 256;
  IDirect3DDevice9_CreateTexture(g_pd3dDevice, w, h, 1, 0, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &g_pTexBrick, NULL);

  D3DLOCKED_RECT lr;
  if (SUCCEEDED(IDirect3DTexture9_LockRect(g_pTexBrick, 0, &lr, NULL, 0)))
  {
    DWORD* p = (DWORD*)lr.pBits;
    for (int y = 0; y < h; y++)
    {
      int row = y / 32;
      int shift = (row % 2) * 32;
      for (int x = 0; x < w; x++)
      {
        int bx = (x + shift) % 256 % 64;
        int by = y % 32;
        int r, g, b;

        // 目地とレンガの色分け
        if (bx < 2 || by < 2)
        {
          r = g = b = 180 + rand() % 20;
        }
        else
        {
          r = 160 + rand() % 40;
          g = 60 + rand() % 40;
          b = 40 + rand() % 40;
          // 一部のレンガを暗くする
          if (((x + shift) / 64 + row) % 3 == 0)
          {
            r -= 20; g -= 10;
          }
        }
        p[y * w + x] = D3DCOLOR_XRGB(r, g, b);
      }
    }
    IDirect3DTexture9_UnlockRect(g_pTexBrick, 0);
  }
}

void CreateConcreteTexture()
{
  int w = 256;
  int h = 256;
  IDirect3DDevice9_CreateTexture(g_pd3dDevice, w, h, 1, 0, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &g_pTexConcrete, NULL);

  D3DLOCKED_RECT lr;
  if (SUCCEEDED(IDirect3DTexture9_LockRect(g_pTexConcrete, 0, &lr, NULL, 0)))
  {
    DWORD* p = (DWORD*)lr.pBits;
    for (int i = 0; i < w * h; i++)
    {
      int c = 150 + (rand() % 60) - 30;
      if (c < 0) c = 0;
      if (c > 255) c = 255;
      p[i] = D3DCOLOR_XRGB(c, c, c);
    }
    IDirect3DTexture9_UnlockRect(g_pTexConcrete, 0);
  }
}

void CreateWoodTexture()
{
  int w = 256;
  int h = 256;
  IDirect3DDevice9_CreateTexture(g_pd3dDevice, w, h, 1, 0, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &g_pTexWood, NULL);

  D3DLOCKED_RECT lr;
  if (SUCCEEDED(IDirect3DTexture9_LockRect(g_pTexWood, 0, &lr, NULL, 0)))
  {
    DWORD* p = (DWORD*)lr.pBits;
    for (int y = 0; y < h; y++)
    {
      for (int x = 0; x < w; x++)
      {
        float noise = (float)(rand() % 100) / 100.0f * 5.0f;
        float sineValue = sinf((float)x * 0.15f + (float)y * 0.02f + noise);
        float grain = (sineValue + 1.0f) * 0.5f;

        int r = (int)(60 + grain * 100);
        int g = (int)(30 + grain * 70);
        int b = (int)(10 + grain * 40);
        p[y * w + x] = D3DCOLOR_XRGB(r, g, b);
      }
    }
    IDirect3DTexture9_UnlockRect(g_pTexWood, 0);
  }
}

void CreatePlasmaTexture()
{
  int w = 256;
  int h = 256;
  IDirect3DDevice9_CreateTexture(g_pd3dDevice, w, h, 1, 0, D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &g_pTexPlasma, NULL);

  D3DLOCKED_RECT lr;
  if (SUCCEEDED(IDirect3DTexture9_LockRect(g_pTexPlasma, 0, &lr, NULL, 0)))
  {
    DWORD* p = (DWORD*)lr.pBits;
    for (int y = 0; y < h; y++)
    {
      for (int x = 0; x < w; x++)
      {
        float fx = (float)x * 0.05f;
        float fy = (float)y * 0.05f;
        float v = sinf(fx) + sinf(fy) + sinf((fx + fy) * 0.5f) + sinf(sqrtf(fx * fx + fy * fy) * 0.5f);

        int r = (int)(128.0f + 127.0f * sinf(v * PI));
        int g = (int)(128.0f + 127.0f * sinf(v * PI + 2.0f * PI / 3.0f));
        int b = (int)(128.0f + 127.0f * sinf(v * PI + 4.0f * PI / 3.0f));
        p[y * w + x] = D3DCOLOR_XRGB(r, g, b);
      }
    }
    IDirect3DTexture9_UnlockRect(g_pTexPlasma, 0);
  }
}

/*
  ---------------------------------------------------------
                  ジオメトリ生成処理
  ---------------------------------------------------------
*/

// 平面生成 (三角形2枚)
void CreatePlane(CUSTOMVERTEX** pPtr, Vec3 origin, Vec3 uAxis, Vec3 vAxis, Vec3 normal, int div, float maxU, float maxV)
{
  CUSTOMVERTEX* v = *pPtr;

  for (int y = 0; y < div; y++)
  {
    for (int x = 0; x < div; x++)
    {
      float fx0 = (float)x / div;
      float fy0 = (float)y / div;
      float fx1 = (float)(x + 1) / div;
      float fy1 = (float)(y + 1) / div;

      // 4点の座標計算
      Vec3 p0 = { origin.x + uAxis.x * fx0 + vAxis.x * fy0, origin.y + uAxis.y * fx0 + vAxis.y * fy0, origin.z + uAxis.z * fx0 + vAxis.z * fy0 };
      Vec3 p1 = { origin.x + uAxis.x * fx1 + vAxis.x * fy0, origin.y + uAxis.y * fx1 + vAxis.y * fy0, origin.z + uAxis.z * fx1 + vAxis.z * fy0 };
      Vec3 p2 = { origin.x + uAxis.x * fx0 + vAxis.x * fy1, origin.y + uAxis.y * fx0 + vAxis.y * fy1, origin.z + uAxis.z * fx0 + vAxis.z * fy1 };
      Vec3 p3 = { origin.x + uAxis.x * fx1 + vAxis.x * fy1, origin.y + uAxis.y * fx1 + vAxis.y * fy1, origin.z + uAxis.z * fx1 + vAxis.z * fy1 };

      // 頂点データ設定 (座標, 法線, UV)
      // 三角形1
      v[0].x = p0.x; v[0].y = p0.y; v[0].z = p0.z; v[0].nx = normal.x; v[0].ny = normal.y; v[0].nz = normal.z; v[0].tu = fx0 * maxU; v[0].tv = fy0 * maxV;
      v[1].x = p1.x; v[1].y = p1.y; v[1].z = p1.z; v[1].nx = normal.x; v[1].ny = normal.y; v[1].nz = normal.z; v[1].tu = fx1 * maxU; v[1].tv = fy0 * maxV;
      v[2].x = p2.x; v[2].y = p2.y; v[2].z = p2.z; v[2].nx = normal.x; v[2].ny = normal.y; v[2].nz = normal.z; v[2].tu = fx0 * maxU; v[2].tv = fy1 * maxV;

      // 三角形2
      v[3].x = p1.x; v[3].y = p1.y; v[3].z = p1.z; v[3].nx = normal.x; v[3].ny = normal.y; v[3].nz = normal.z; v[3].tu = fx1 * maxU; v[3].tv = fy0 * maxV;
      v[4].x = p3.x; v[4].y = p3.y; v[4].z = p3.z; v[4].nx = normal.x; v[4].ny = normal.y; v[4].nz = normal.z; v[4].tu = fx1 * maxU; v[4].tv = fy1 * maxV;
      v[5].x = p2.x; v[5].y = p2.y; v[5].z = p2.z; v[5].nx = normal.x; v[5].ny = normal.y; v[5].nz = normal.z; v[5].tu = fx0 * maxU; v[5].tv = fy1 * maxV;

      v += 6;
    }
  }
  *pPtr = v;
}

// 箱生成 (6面)
void CreateBox(CUSTOMVERTEX** pPtr, Vec3 center, float sx, float sy, float sz, int div, float repU, float repV)
{
  float hx = sx / 2;
  float hy = sy / 2;
  float hz = sz / 2;

  // 6つの面を作成
  CreatePlane(pPtr, (Vec3){center.x - hx, center.y + hy, center.z - hz}, (Vec3){ 2 * hx, 0, 0}, (Vec3){0, -2 * hy, 0}, (Vec3){ 0, 0, -1}, div, repU, repV);
  CreatePlane(pPtr, (Vec3){center.x + hx, center.y + hy, center.z + hz}, (Vec3){-2 * hx, 0, 0}, (Vec3){0, -2 * hy, 0}, (Vec3){ 0, 0,  1}, div, repU, repV);
  CreatePlane(pPtr, (Vec3){center.x - hx, center.y + hy, center.z + hz}, (Vec3){0, 0, -2 * hz}, (Vec3){0, -2 * hy, 0}, (Vec3){-1, 0,  0}, div, repU, repV);
  CreatePlane(pPtr, (Vec3){center.x + hx, center.y + hy, center.z - hz}, (Vec3){0, 0,  2 * hz}, (Vec3){0, -2 * hy, 0}, (Vec3){ 1, 0,  0}, div, repU, repV);
  CreatePlane(pPtr, (Vec3){center.x - hx, center.y + hy, center.z + hz}, (Vec3){ 2 * hx, 0, 0}, (Vec3){0, 0, -2 * hz}, (Vec3){ 0, 1,  0}, div, repU, repV);
  CreatePlane(pPtr, (Vec3){center.x - hx, center.y - hy, center.z - hz}, (Vec3){ 2 * hx, 0, 0}, (Vec3){0, 0,  2 * hz}, (Vec3){ 0,-1,  0}, div, repU, repV);
}

// 全ジオメトリの初期化
HRESULT InitGeometry()
{
  float W = ROOM_WIDTH;
  float H = ROOM_HEIGHT;
  int roomDiv = WALL_DIVISIONS;

  // --- 頂点数計算 ---
  // 部屋 (壁4 + 床 + 天井 = 6面)
  g_RoomStartVert = 0;
  g_RoomVertCount = 6 * (roomDiv * roomDiv * 6);

  // 柱 (4本)
  int pillarDiv = 4;
  int vertsPerPillar = 6 * (pillarDiv * pillarDiv * 6);
  g_PillarStartVert = g_RoomVertCount;
  g_PillarVertCount = 4 * vertsPerPillar;

  // 中央キューブ
  int cubeDiv = 8;
  g_CubeStartVert = g_PillarStartVert + g_PillarVertCount;
  g_CubeVertCount = 6 * (cubeDiv * cubeDiv * 6);

  // 光源用オブジェクト
  int lightObjDiv = 2;
  g_LightObjStartVert = g_CubeStartVert + g_CubeVertCount;
  g_LightObjVertCount = 6 * (lightObjDiv * lightObjDiv * 6);

  g_TotalVertices = g_LightObjStartVert + g_LightObjVertCount;

  // 頂点バッファ作成
  if (FAILED(IDirect3DDevice9_CreateVertexBuffer(
      g_pd3dDevice,
      g_TotalVertices * sizeof(CUSTOMVERTEX),
      0,
      D3DFVF_CUSTOMVERTEX,
      D3DPOOL_DEFAULT,
      &g_pVB,
      NULL)))
  {
    return E_FAIL;
  }

  // 頂点データ書き込み
  CUSTOMVERTEX* pVertices;
  if (FAILED(IDirect3DVertexBuffer9_Lock(g_pVB, 0, 0, (void**)&pVertices, 0)))
  {
    return E_FAIL;
  }

  CUSTOMVERTEX* pWalker = pVertices;

  // 部屋生成 (壁・床・天井)
  float U_WALL = 6.0f;
  float V_WALL = 2.0f;
  float UV_FLOOR = 6.0f;

  CreatePlane(&pWalker, (Vec3){-W, H,  W}, (Vec3){ 2*W, 0, 0}, (Vec3){0, -2*H, 0}, (Vec3){ 0, 0, -1}, roomDiv, U_WALL, V_WALL); // 奥
  CreatePlane(&pWalker, (Vec3){ W, H, -W}, (Vec3){-2*W, 0, 0}, (Vec3){0, -2*H, 0}, (Vec3){ 0, 0,  1}, roomDiv, U_WALL, V_WALL); // 手前
  CreatePlane(&pWalker, (Vec3){-W, H, -W}, (Vec3){0, 0,  2*W}, (Vec3){0, -2*H, 0}, (Vec3){ 1, 0,  0}, roomDiv, U_WALL, V_WALL); // 左
  CreatePlane(&pWalker, (Vec3){ W, H,  W}, (Vec3){0, 0, -2*W}, (Vec3){0, -2*H, 0}, (Vec3){-1, 0,  0}, roomDiv, U_WALL, V_WALL); // 右
  CreatePlane(&pWalker, (Vec3){-W, H, -W}, (Vec3){ 2*W, 0, 0}, (Vec3){0, 0,  2*W}, (Vec3){ 0,-1,  0}, roomDiv, UV_FLOOR, UV_FLOOR); // 天井
  CreatePlane(&pWalker, (Vec3){-W,-H,  W}, (Vec3){ 2*W, 0, 0}, (Vec3){0, 0, -2*W}, (Vec3){ 0, 1,  0}, roomDiv, UV_FLOOR, UV_FLOOR); // 床

  // 柱生成
  float P = PILLAR_POS;
  float PS = PILLAR_SIZE;
  float PH = ROOM_HEIGHT * 2;
  CreateBox(&pWalker, (Vec3){-P, 0,  P}, PS, PH, PS, pillarDiv, 1.0f, 4.0f);
  CreateBox(&pWalker, (Vec3){ P, 0,  P}, PS, PH, PS, pillarDiv, 1.0f, 4.0f);
  CreateBox(&pWalker, (Vec3){-P, 0, -P}, PS, PH, PS, pillarDiv, 1.0f, 4.0f);
  CreateBox(&pWalker, (Vec3){ P, 0, -P}, PS, PH, PS, pillarDiv, 1.0f, 4.0f);

  // 中央キューブ生成
  CreateBox(&pWalker, (Vec3){0, 0, 0}, CUBE_SIZE, CUBE_SIZE, CUBE_SIZE, cubeDiv, 1.0f, 1.0f);

  // 光源オブジェクト生成 (原点に作成、描画時に移動)
  CreateBox(&pWalker, (Vec3){0, 0, 0}, LIGHT_SIZE, LIGHT_SIZE, LIGHT_SIZE, lightObjDiv, 1.0f, 1.0f);

  IDirect3DVertexBuffer9_Unlock(g_pVB);
  return S_OK;
}

/*
  ---------------------------------------------------------
                  システム初期化・終了
  ---------------------------------------------------------
*/

HRESULT InitD3D(HWND hWnd)
{
  if (NULL == (g_pD3D = Direct3DCreate9(D3D_SDK_VERSION)))
  {
    return E_FAIL;
  }

  D3DPRESENT_PARAMETERS d3dpp;
  ZeroMemory(&d3dpp, sizeof(d3dpp));
  d3dpp.Windowed = TRUE;
  d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

  if (FAILED(IDirect3D9_CreateDevice(
      g_pD3D,
      D3DADAPTER_DEFAULT,
      D3DDEVTYPE_HAL,
      hWnd,
      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
      &d3dpp,
      &g_pd3dDevice)))
  {
    return E_FAIL;
  }

  // レンダーステート設定
  IDirect3DDevice9_SetRenderState(g_pd3dDevice, D3DRS_LIGHTING, TRUE);
  IDirect3DDevice9_SetRenderState(g_pd3dDevice, D3DRS_AMBIENT, 0x00202020);
  IDirect3DDevice9_SetRenderState(g_pd3dDevice, D3DRS_NORMALIZENORMALS, TRUE);
  IDirect3DDevice9_SetRenderState(g_pd3dDevice, D3DRS_CULLMODE, D3DCULL_CCW);
  IDirect3DDevice9_SetRenderState(g_pd3dDevice, D3DRS_ZENABLE, TRUE);

  // サンプラーステート設定
  IDirect3DDevice9_SetSamplerState(g_pd3dDevice, 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
  IDirect3DDevice9_SetSamplerState(g_pd3dDevice, 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

  // リソース作成
  CreateBrickTexture();
  CreateConcreteTexture();
  CreateWoodTexture();
  CreatePlasmaTexture();

  return InitGeometry();
}

void Cleanup(void)
{
  if (g_pTexPlasma)   IDirect3DTexture9_Release(g_pTexPlasma);
  if (g_pTexWood)     IDirect3DTexture9_Release(g_pTexWood);
  if (g_pTexConcrete) IDirect3DTexture9_Release(g_pTexConcrete);
  if (g_pTexBrick)    IDirect3DTexture9_Release(g_pTexBrick);
  if (g_pVB)          IDirect3DVertexBuffer9_Release(g_pVB);
  if (g_pd3dDevice)   IDirect3DDevice9_Release(g_pd3dDevice);
  if (g_pD3D)         IDirect3D9_Release(g_pD3D);
}

/*
  ---------------------------------------------------------
                  更新・描画処理
  ---------------------------------------------------------
*/

void UpdateInput(void)
{
  float speed = 0.3f;
  float yaw = g_CameraYaw;

  // WASD移動
  if (GetAsyncKeyState('W') & 0x8000)
  {
    g_CamPos.x += sinf(yaw) * speed;
    g_CamPos.z += cosf(yaw) * speed;
  }
  if (GetAsyncKeyState('S') & 0x8000)
  {
    g_CamPos.x -= sinf(yaw) * speed;
    g_CamPos.z -= cosf(yaw) * speed;
  }
  if (GetAsyncKeyState('A') & 0x8000)
  {
    g_CamPos.x -= cosf(yaw) * speed;
    g_CamPos.z += sinf(yaw) * speed;
  }
  if (GetAsyncKeyState('D') & 0x8000)
  {
    g_CamPos.x += cosf(yaw) * speed;
    g_CamPos.z -= sinf(yaw) * speed;
  }

  // 移動範囲制限 (部屋の中)
  float L = ROOM_WIDTH - 2.0f;
  if (g_CamPos.x > L)  g_CamPos.x = L;
  if (g_CamPos.x < -L) g_CamPos.x = -L;
  if (g_CamPos.z > L)  g_CamPos.z = L;
  if (g_CamPos.z < -L) g_CamPos.z = -L;
}

void Render(void)
{
  if (NULL == g_pd3dDevice) return;

  UpdateInput();

  // アニメーション更新
  g_CubeRotation += 0.02f;

  // 画面クリア
  IDirect3DDevice9_Clear(g_pd3dDevice, 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);

  if (SUCCEEDED(IDirect3DDevice9_BeginScene(g_pd3dDevice)))
  {
    // --- シーン共通設定 ---

    // デフォルトマテリアル
    D3DMATERIAL9 mtrl;
    ZeroMemory(&mtrl, sizeof(mtrl));
    mtrl.Diffuse.r = mtrl.Diffuse.g = mtrl.Diffuse.b = 1.0f;
    mtrl.Diffuse.a = 1.0f;
    mtrl.Ambient = mtrl.Diffuse;
    IDirect3DDevice9_SetMaterial(g_pd3dDevice, &mtrl);

    // ライティング設定
    D3DLIGHT9 light;
    ZeroMemory(&light, sizeof(light));
    light.Type = D3DLIGHT_POINT;
    light.Diffuse.r = 1.0f;
    light.Diffuse.g = 0.95f;
    light.Diffuse.b = 0.8f;
    light.Position.x = g_LightPos.x;
    light.Position.y = g_LightPos.y;
    light.Position.z = g_LightPos.z;
    light.Range = 60.0f;
    light.Attenuation0 = 0.0f;
    light.Attenuation1 = 0.05f;
    light.Attenuation2 = 0.0f;

    IDirect3DDevice9_SetLight(g_pd3dDevice, 0, &light);
    IDirect3DDevice9_LightEnable(g_pd3dDevice, 0, TRUE);

    // カメラ (ビュー) 行列
    D3DMATRIX matView, matProj;
    Vec3 dir;
    dir.x = cosf(g_CameraPitch) * sinf(g_CameraYaw);
    dir.y = sinf(g_CameraPitch);
    dir.z = cosf(g_CameraPitch) * cosf(g_CameraYaw);

    Vec3 at = { g_CamPos.x + dir.x, g_CamPos.y + dir.y, g_CamPos.z + dir.z };
    Vec3 up = { 0.0f, 1.0f, 0.0f };

    MatrixLookAtLH(&matView, &g_CamPos, &at, &up);
    IDirect3DDevice9_SetTransform(g_pd3dDevice, D3DTS_VIEW, &matView);

    // プロジェクション行列
    MatrixPerspectiveFovLH(&matProj, PI / 3.0f, (float)SCREEN_WIDTH / SCREEN_HEIGHT, 1.0f, 500.0f);
    IDirect3DDevice9_SetTransform(g_pd3dDevice, D3DTS_PROJECTION, &matProj);

    // --- 描画実行 ---
    IDirect3DDevice9_SetStreamSource(g_pd3dDevice, 0, g_pVB, 0, sizeof(CUSTOMVERTEX));
    IDirect3DDevice9_SetFVF(g_pd3dDevice, D3DFVF_CUSTOMVERTEX);

    // 静的オブジェクトの描画 (ワールド行列は単位行列)
    D3DMATRIX matWorld;
    MatrixIdentity(&matWorld);
    IDirect3DDevice9_SetTransform(g_pd3dDevice, D3DTS_WORLD, &matWorld);

    // 壁 (レンガ)
    int vertsPerRoomPlane = g_RoomVertCount / 6;
    IDirect3DDevice9_SetTexture(g_pd3dDevice, 0, g_pTexBrick);
    // 壁4面分を描画
    IDirect3DDevice9_DrawPrimitive(g_pd3dDevice, D3DPT_TRIANGLELIST, 0, (vertsPerRoomPlane * 4) / 3);

    // 床・天井 (コンクリート)
    IDirect3DDevice9_SetTexture(g_pd3dDevice, 0, g_pTexConcrete);
    // 残り2面分を描画
    IDirect3DDevice9_DrawPrimitive(g_pd3dDevice, D3DPT_TRIANGLELIST, vertsPerRoomPlane * 4, (vertsPerRoomPlane * 2) / 3);

    // 柱 (木目)
    IDirect3DDevice9_SetTexture(g_pd3dDevice, 0, g_pTexWood);
    IDirect3DDevice9_DrawPrimitive(g_pd3dDevice, D3DPT_TRIANGLELIST, g_PillarStartVert, g_PillarVertCount / 3);

    // 動的オブジェクト: キューブ (プラズマテクスチャ + 回転)
    D3DMATRIX matRot, matTrans, matCubeWorld;
    MatrixRotationYawPitchRoll(&matRot, g_CubeRotation, g_CubeRotation * 0.7f, g_CubeRotation * 0.3f);
    MatrixTranslation(&matTrans, 0.0f, 0.0f, 0.0f);
    MatrixMultiply(&matCubeWorld, &matRot, &matTrans);
    IDirect3DDevice9_SetTransform(g_pd3dDevice, D3DTS_WORLD, &matCubeWorld);

    IDirect3DDevice9_SetTexture(g_pd3dDevice, 0, g_pTexPlasma);
    // 少し発光させる
    D3DMATERIAL9 mtrlPlasma = mtrl;
    mtrlPlasma.Emissive.r = 0.3f; mtrlPlasma.Emissive.g = 0.3f; mtrlPlasma.Emissive.b = 0.3f;
    IDirect3DDevice9_SetMaterial(g_pd3dDevice, &mtrlPlasma);

    IDirect3DDevice9_DrawPrimitive(g_pd3dDevice, D3DPT_TRIANGLELIST, g_CubeStartVert, g_CubeVertCount / 3);

    // 光源オブジェクト (白い箱)
    D3DMATRIX matLightTrans;
    MatrixTranslation(&matLightTrans, g_LightPos.x, g_LightPos.y, g_LightPos.z);
    IDirect3DDevice9_SetTransform(g_pd3dDevice, D3DTS_WORLD, &matLightTrans);

    // 強く発光させる (テクスチャなし)
    D3DMATERIAL9 mtrlLight;
    ZeroMemory(&mtrlLight, sizeof(mtrlLight));
    mtrlLight.Emissive.r = 0.9f; mtrlLight.Emissive.g = 0.9f; mtrlLight.Emissive.b = 0.6f;
    IDirect3DDevice9_SetMaterial(g_pd3dDevice, &mtrlLight);
    IDirect3DDevice9_SetTexture(g_pd3dDevice, 0, NULL);

    IDirect3DDevice9_DrawPrimitive(g_pd3dDevice, D3DPT_TRIANGLELIST, g_LightObjStartVert, g_LightObjVertCount / 3);

    IDirect3DDevice9_EndScene(g_pd3dDevice);
  }

  IDirect3DDevice9_Present(g_pd3dDevice, NULL, NULL, NULL, NULL);
}

/*
  ---------------------------------------------------------
                   ウィンドウプロシージャ
  ---------------------------------------------------------
*/

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
  case WM_DESTROY:
    Cleanup();
    PostQuitMessage(0);
    return 0;

  case WM_LBUTTONDOWN:
    g_IsDragging = TRUE;
    g_LastMouseX = LOWORD(lParam);
    g_LastMouseY = HIWORD(lParam);
    SetCapture(hWnd);
    return 0;

  case WM_LBUTTONUP:
    g_IsDragging = FALSE;
    ReleaseCapture();
    return 0;

  case WM_MOUSEMOVE:
    if (g_IsDragging)
    {
      int x = LOWORD(lParam);
      int y = HIWORD(lParam);
      // カメラ回転
      g_CameraYaw += (x - g_LastMouseX) * 0.005f;
      g_CameraPitch -= (y - g_LastMouseY) * 0.005f;

      // 垂直角度制限
      if (g_CameraPitch > 1.5f)  g_CameraPitch = 1.5f;
      if (g_CameraPitch < -1.5f) g_CameraPitch = -1.5f;

      g_LastMouseX = x;
      g_LastMouseY = y;
    }
    return 0;

  case WM_KEYDOWN:
    if (wParam == VK_ESCAPE)
    {
      Cleanup();
      PostQuitMessage(0);
    }
    return 0;
  }
  return DefWindowProc(hWnd, msg, wParam, lParam);
}

/*
  ---------------------------------------------------------
               メイン関数(エントリポイント)
  ---------------------------------------------------------
*/

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  WNDCLASSEX wc = {
    sizeof(WNDCLASSEX),
    CS_CLASSDC,
    MsgProc,
    0L,
    0L,
    GetModuleHandle(NULL),
    NULL,
    NULL,
    NULL,
    NULL,
    CLASS_NAME,
    NULL
  };

  RegisterClassEx(&wc);

  HWND hWnd = CreateWindow(
      CLASS_NAME,
      "3D Graphics on Mac",
      WS_OVERLAPPEDWINDOW,
      100, 100,
      SCREEN_WIDTH, SCREEN_HEIGHT,
      GetDesktopWindow(),
      NULL,
      wc.hInstance,
      NULL
  );

  if (SUCCEEDED(InitD3D(hWnd)))
  {
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
    ZeroMemory(&msg, sizeof(msg));

    // メッセージループ
    while (msg.message != WM_QUIT)
    {
      if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
      else
      {
        Render();
      }
    }
  }

  UnregisterClass(CLASS_NAME, wc.hInstance);
  return 0;
}


/*
  ---------------------------------------------------------
                      数学ヘルパー関数
  ---------------------------------------------------------
    行列・ベクトルの計算を行う
*/

void Vec3Normalize(Vec3* v)
{
  float len = sqrtf(v->x * v->x + v->y * v->y + v->z * v->z);
  if (len > 0.0001f)
  {
    v->x /= len;
    v->y /= len;
    v->z /= len;
  }
}

void MatrixIdentity(D3DMATRIX* m)
{
  memset(m, 0, sizeof(D3DMATRIX));
  m->m[0][0] = 1.0f;
  m->m[1][1] = 1.0f;
  m->m[2][2] = 1.0f;
  m->m[3][3] = 1.0f;
}

void MatrixRotationYawPitchRoll(D3DMATRIX* out, float yaw, float pitch, float roll)
{
  MatrixIdentity(out);
  float cy = cosf(yaw);
  float sy = sinf(yaw);
  float cp = cosf(pitch);
  float sp = sinf(pitch);
  float cr = cosf(roll);
  float sr = sinf(roll);

  out->m[0][0] = cy * cr + sy * sp * sr;
  out->m[0][1] = cp * sr;
  out->m[0][2] = -sy * cr + cy * sp * sr;

  out->m[1][0] = -cy * sr + sy * sp * cr;
  out->m[1][1] = cp * cr;
  out->m[1][2] = sr * sy + cy * sp * cr;

  out->m[2][0] = sy * cp;
  out->m[2][1] = -sp;
  out->m[2][2] = cy * cp;
}

void MatrixTranslation(D3DMATRIX* out, float x, float y, float z)
{
  MatrixIdentity(out);
  out->m[3][0] = x;
  out->m[3][1] = y;
  out->m[3][2] = z;
}

void MatrixMultiply(D3DMATRIX* out, const D3DMATRIX* m1, const D3DMATRIX* m2)
{
  D3DMATRIX r;
  memset(&r, 0, sizeof(r));
  for (int i = 0; i < 4; i++)
  {
    for (int j = 0; j < 4; j++)
    {
      for (int k = 0; k < 4; k++)
      {
        r.m[i][j] += m1->m[i][k] * m2->m[k][j];
      }
    }
  }
  *out = r;
}

void MatrixLookAtLH(D3DMATRIX* out, const Vec3* eye, const Vec3* at, const Vec3* up)
{
  Vec3 zaxis = { at->x - eye->x, at->y - eye->y, at->z - eye->z };
  Vec3Normalize(&zaxis);

  Vec3 xaxis = {
    up->y * zaxis.z - up->z * zaxis.y,
    up->z * zaxis.x - up->x * zaxis.z,
    up->x * zaxis.y - up->y * zaxis.x
  };
  Vec3Normalize(&xaxis);

  Vec3 yaxis = {
    zaxis.y * xaxis.z - zaxis.z * xaxis.y,
    zaxis.z * xaxis.x - zaxis.x * xaxis.z,
    zaxis.x * xaxis.y - zaxis.y * xaxis.x
  };

  MatrixIdentity(out);
  out->m[0][0] = xaxis.x; out->m[0][1] = yaxis.x; out->m[0][2] = zaxis.x;
  out->m[1][0] = xaxis.y; out->m[1][1] = yaxis.y; out->m[1][2] = zaxis.y;
  out->m[2][0] = xaxis.z; out->m[2][1] = yaxis.z; out->m[2][2] = zaxis.z;

  out->m[3][0] = -(xaxis.x * eye->x + xaxis.y * eye->y + xaxis.z * eye->z);
  out->m[3][1] = -(yaxis.x * eye->x + yaxis.y * eye->y + yaxis.z * eye->z);
  out->m[3][2] = -(zaxis.x * eye->x + zaxis.y * eye->y + zaxis.z * eye->z);
}

void MatrixPerspectiveFovLH(D3DMATRIX* out, float fovy, float aspect, float zn, float zf)
{
  MatrixIdentity(out);
  float yScale = 1.0f / tanf(fovy / 2.0f);
  float xScale = yScale / aspect;

  out->m[0][0] = xScale;
  out->m[1][1] = yScale;
  out->m[2][2] = zf / (zf - zn);
  out->m[2][3] = 1.0f;
  out->m[3][2] = -zn * zf / (zf - zn);
  out->m[3][3] = 0.0f;
}
tcc room3d.c -ld3d9

d3d9は、DirectXのバージョン9を意味しています。では、実行してみましょう。

room3d.exeの実行画面

 

WASDキーとマウスドラッグで視点と向きを移動することができる3Dの部屋です。壁・床・柱・謎の物体がテクスチャ付きで三次元的に描画されています。

これを応用すれば、3Dゲームを作ることも夢じゃありませんね!

終わりに

配布コーナー

いかがでしたでしょうか?最新のMacだからこそできる、他にはないアプリ開発体験について皆さんが少しでも興味を持っていただけたら幸いです。

ここまでに説明した試作アプリのソースコードと実行可能ファイルを配布します。Mac用ですので、Macをお持ちでない方はぜひMacを買ってから[9]、実行してみてくださいね!

リンク: [ mac-app-samples.zip ]

ギャラリー

Windows 11

 

Windows XP

 

Windows 95

 

ReactOS 0.4.15

明日はmaton君です。

脚注

  1. 15年前ぐらいかな。
  2. 嘘に決まっている。
  3. あながち嘘でもない。
  4. ワイド文字という形でUnicodeも使えるのだが、Windows9xが対応しないので今回は使わなかった。
  5. これは本当。リソースファイルを用いてロジックとそれ以外を分離するこのプログラミングスタイルは初代Macintosh(1984年)のやり方である。なんでこんなに似てるのか。Macintoshの発売と同時に、MicrosoftはMacintosh用のMultiplan(Excelの前身)を発売した。アプリケーションの開発をしていたのだから、Microsoftがこのスタイルについて熟知していたのは疑いようもない。2年近く経って発売されたWindows1.0(1985年)がこれによく似たリソースの概念を持っていたのはその影響。WindowsはMacのパクリか論争ではよくGUIのアイデアのオリジナリティが語られ、結局パロアルト研究所にまで遡ってお茶を濁すことになったりするが、こういうSDKの作りとかに着目するのも面白い。
  6. このmingw-w64パッケージにはwindresだけでなくWindowsクロスコンパイル用gccも含まれており、これを使ってC言語ソースを.exe実行ファイルにコンパイルすることも可能。ただ、mingwのgccはWindows10以降にしかない変なDLLに勝手にリンクする傾向がある。古いWindowsをターゲットにするならtccの方がよい。
  7. この辺りは、Windows95でも動かしたいという下心で無理をしている。アルファチャンネルを扱えるようになったのはWindows98から、アルファチャンネル付きの画像をそのまま読み込めるようになったのはWindowsXPから。
  8. 指定色の透過が一発でできるTransparentBlt()はWindows98から。
  9. 来春まで待てばM5搭載のMacbook Airが発売されるというウワサなので、それまで待ったほうがいいかも?私はノリと勢いでM4搭載機を買ってしまいましたが...。

趣味で半導体集積回路設計できる時代来てるぜ

この記事はTCU-CTRL場外乱闘 Advent Calender(2023)の12月18日のぶんです。

adventar.org

昨日はりゅーの君の「性癖を書いてたら大学生活が終わってた件について」でした。シナリオが描けるってすごいなぁ。

ちなみにりゅーの君の被保護者であるCTRLマスコットキャラクターアンナちゃんですが、前に作った集積回路の空きスペースに仕込んでおいたんですよね。もう出来てるので、まだ見ていないけど顕微鏡で見たら見える・・・はず。

160ミクロンのアンナちゃん

ちなみに明日はまる君の「今年描いた絵を振り返ってみて思ったことについて」の予定です。予定です。絵が描けるってすごいなぁ。

ロリ、いいよね。でもセロリは嫌いなんだよね。野菜の。違いはセがあるかないか。してみると、ロリってのはセがなくて、背もないというわけ。はは、どういうわけ?

 

もういいや、半導体集積回路を設計しよう!

 

申し遅れましたわたくし、なんかそういう感じの研究室に所属しているりあく徒です。そろそろ卒論の季節ですね。ヤバいね。

 

といっても研究室の勧誘をしに来たわけじゃありません。

同じ学科の2年生以下って該当者がほぼおらんし。

大学とか研究とか関係なく、個人で、趣味で、集積回路を設計できるんだよというメッセージを、体験に基づいて皆さんにお伝えしたいと思います。

 

つまりあなたもちゃんとこの記事の射程範囲です。

集積回路設計とは

以下かなりいい加減なことを書くので、本格的に気になった人はちゃんと調べてくださいね。

ICチップの例(オープンソースではないが研究室で設計したもの)

集積回路(Integrated Circuit, IC)は、現代文明の要です。「この世界は黒い体躯に銀の足をジャラジャラ生やしたゲジゲジの上に成り立っている」という神話的世界観を今考えました。お好きな神話に組み込んでおいてください。これであなたは組み込み系エンジニア。

チップの中にはたいていトランジスタがたくさん詰め込まれていて、いろんな機能を実現しています。インテルのCPUとかNVIDIAのGPUなんかは集積回路の華ともいえる存在でしょう。そういうデジタル回路だけでなく、音声信号を増幅するアンプみたいなアナログのICもたくさん存在しています。

 

当今、電子工作といえばこういうICを買ってきてよしなに組み合わせることを云う。

 

云うんだけどそればかりじゃなくて、この文字通りの”ブラックボックス”の中身も作ってみようじゃないかというワケですね。

オープンなツールとPDK

C言語ソフトウェアの開発にはエディタとコンパイラが必要になりますね。同様に、集積回路の設計には専用のCADツールとPDKというのが要ります。

Cならエディタもコンパイラも、イマドキはネット環境さえあればVimとgccをサクッと拝借して使えちゃうわけですが、集積回路はどうか。

集積回路用のCADツールは、プロプライエタリでバカ高いどころか個人じゃ買えないソフトがヘゲモニーを握っているのですが、オープンソースの代替ツールが整備されています。

プロプライエタリでバカ高いどころか個人じゃ買えないソフトのイメージ
(大変お世話になっております)

PDKとはプロセス・デザイン・キットの略で、工場が受け付ける回路設計の規格みたいなものです。こちらも、工場(TSMCとか)と機密保持契約を結んだりなんだりしてからでないと手に入れられないのが普通ですが、気前よくPDKをオープンにしている工場が北米らへんの合衆国にあるのでそこのを使います。

機能を考える

ではお試しに、みんな大好き、NAND回路を作ってみます。
(いろいろ端折るのでこれ見ながらマネするのは無謀だとおもいます)

NAND回路のイメージ

NANDがあればnandeも出来る、というのは周知のとおり。

回路を描く

回路エディタはxschem(旧twitterschem)を使います。勝手にエックス・スケムと読んでましたがエックス・スキームらしいです。

$xschem nandemo.sch

で起動します。

起動直後のxschem

ちょちょいとNAND回路を書きました。応用情報にギリギリ出ないぐらいの、難度。
(MOSトランジスタの記号とNANDの挙動は出たハズ)

NAND回路

シミュレーション機能も付いてます。とてもNANDですね。

シミュレーション結果

レイアウトする

レイアウトツールはklayoutです。

$klayout

起動直後のklayout

そもそもレイアウトというのは言葉の通り、シリコンのどこにどうトランジスタを置いて、どう配線するかを決定するという、凄まじく泥臭いお絵描きです。

トランジスタを直接お絵描きしてもいいですが、テンプレートがあります。
NMOSトランジスタはこんな感じ。

テンプレNMOSトランジスタ

トランジスタ4つを並べたら・・・

上ふたつがPMOS、下ふたつがNMOS

金属線で配線します。ペイントソフトで四角形をペタペタ塗ったり線描いたりするイメージ。

金属配線
(青斜線が第一層目、紫斜線が第二層目。
メタルの略で"メタいち""メタに"とか呼ぶとそれっぽいぞ。)

端子に名前を付けたら、ハイ完成!
だいたい30ミクロン四方に収まるNAND回路が描けました。

端子名を付けて完成したレイアウト
もちろん便宜上のものでありこの文字は印刷されない

デバッグ

レイアウトをダーっと描いて一発でOKということはまずないと思った方がいいです。

ソフトウェアでもそうですが、この世の摂理でしょうか。

以下のふたつのチェックをクリアする必要があります。

  • DRC(デザイン・ルール・チェック)
    • 許容できない要素が無いか(配線が細すぎるとか、配線同士が近すぎるとか)
  • LVS(レイアウト vs. スケマティック)
    • 回路図とレイアウトが食い違ってないか

DRCエラーの例

LVSエラーの例

このふたつのチェックを、エラーが出なくなるまで回しまくります。

エラーなし

オールグリーン。おめでとう、集積回路の設計が完了したぞ!


(他にもこまごまとした仕上げ作業があるんですが・・・そして往々にしてそこに一番時間がかかったりする。これもまた世の摂理か。)

Open MPW

klayoutでレイアウトを保存すると、この例では”nandemo.gds”というGDS形式のファイルになります。基本的な考え方でいうと、このGDSファイルを工場に提出するとICになります。

さて、普通はオリジナルICの製造にはン百万円、ン千万というお金がかかります。まあ、「実物にならなくても設計しただけで満足~」という楽しみ方もあるでしょうが、なるべくなら作りたい。

 

でもそんな金はない。

 

そんなあなたにOpen MPW。

Open MPW概念のシンプルな曼陀羅あるいは魔法陣

Open MPWというのは、簡単に言うとGoogleの金でICを製造してもらえるやつです。神。
ただしいろいろ条件付きで、抽選です。
条件のひとつとして、設計をオープンソースにしなければなりません。これまでOSSなツールで設計してきたわけですが、その成果物もオープンでないといけないわけです。
また抽選ですが、落ちると次回の抽選で優先順位が上がるらしいので、根気強く応募し続ければいつか当たるらしいです。

というわけで実際に作るところはGoogle様頼みなところはありますが、とにかくフリー&オープンな感じで集積回路を作れちゃうんですよ。

界隈が盛り上がってインフラがもっと整備されていったら、将来的には少額(数万円ぐらい)で確実に作れるようになったりしねぇかなぁ、と夢想して、とりあえずこんな記事を書いた次第。

おわりに

いかがでしたでしょうか?趣味で集積回路を設計する雰囲気が伝わったでしょうか。

本当は体験談という体で書こうとしたんですが、紆余曲折がありすぎてうまく文章に纏める自信がなかったのでこんな形式にしました。

実際に僕が何をしたのかというと、12/11が最終締め切りだったGFMPW-1というシャトルに、NOT回路を提出しました。当選を待つのみです。

はい、NANDですらありませんNOTです。時間が無かった・・・。

抽選を待っているNOT回路

それも、単独ではなくてISHI会という国内のIC自作コミュニティの相乗りプロジェクトに参加してのことです。やりかたもISHI会の人たちにいろいろ教えていただきました。もし自作ICに興味が湧いたら、こういうコミュニティを覗いて戸を叩くのが一番の近道であると思います。

個人でできる!と大見栄張って結局集団やないかいというツッコミはご遠慮ください。

おわりに2(ツー)

18日のアドカレの予定でしたが、まるまる一週間遅れました。

時間がなかったわけではないんです。しかし超ド級のスランプに陥っていたのでCities:Skylines2を遊んだりアニメ版スクールデイズを周回したりして"充電"してました。

メリークリスマス

マイクロ・コンピュータの世界

この記事は TCU-CTRL場外乱闘 Advent Calendar 2021 - Adventar 11日目の記事です 。

前回の記事はこちら。

sharin888.hatenablog.com

 

「マイコン」と聞いて何を思い浮かべるでしょうか?
PICやAVRといったマイコンチップでしょうか。

f:id:multireact:20211211103229p:plain

PIC16F1827

f:id:multireact:20211211103139p:plain

ATTiny202

それとも、ArduinoやRaspberryPiのようなマイコンボードでしょうか。

f:id:multireact:20211211104036p:plain

びんぼうでいいの


上のような意味でマイコンと言ったとき、それはどちらかといえば「マイクロ・コントローラ」の略称です。サイズ・機能・消費電力などがパソコンに対してマイクロで、機器の制御に向いています。

 

私がこの記事で語るマイコンは「マイクロ・コンピュータ」です。CPUにマイクロプロセッサを使用していて、そうでないコンピュータよりマイクロなコンピュータを指します。

f:id:multireact:20211211104440p:plain

ザイログ Z80 マイクロプロセッサ

今の時代、マイクロでないプロセッサの方がレアです。PICもCore iもマイクロプロセッサですし、富岳のようなスパコンも大量のマイクロプロセッサを(効率よく)纏め上げて出来ています。つまり、だいたいのコンピュータは「マイクロ・コンピュータ」に該当する可能性があります。
ただし、個人が独占的に使用するコンピュータをパソコンとする呼び方がたいへん普及した現代では、(コントローラでなしに)マイコンというともう少し特別な意味合いを帯びてきます。

 

8bitマイクロプロセッサを搭載した、70年代~80年代スタイルのコンピュータです。

マイコンの構成


マイコンの世界に入るためにここでひとつ、最強のマイコンを妄想で作ってみましょう。
欠かせない部品はマイクロプロセッサです。

f:id:multireact:20211211104725p:plain

intel 8080 1974年

f:id:multireact:20211211104908p:plain

マイクロプロセッサの図

実のところ、これだけでほぼコンピュータとしての機能が完成しています。ただ残念ながら人類は長い進化の過程で5Vの信号を読んだり出力したりする機能を退化させてしまったため、LEDとスイッチを付けてみましょう。
今は細かいことを考えないことにしますが、あえて言えば、8bitのプロセッサだから、LEDとスイッチが8つづつあればよいです。

f:id:multireact:20211211110226p:plain

マイクロプロセッサとLEDとスイッチの図


これでもう、スイッチを通して機械語で話しかけてやれば、加算や減算、排他的論理和などの演算を華麗にこなし、LEDに結果を表示することができます。

 

参考:6502あそび[1] メモリごっこ

本当にやるときはショートとかに気を付けてくださいね。


しかしまあ、最強のマイコンというにはまだ何か少し足りない気もします。

f:id:multireact:20211211110651p:plain

RAM実装


RAMを付けました。揮発性メモリです。
プロセッサからはデータとは別に、アドレス信号がまろび出ています。これを繋ぐことでメモリはなんと、人間の代わりに機械語命令とデータをプロセッサに渡してくれます。逆に、結果をRAMに格納することもできます。自動で演算するとか最強ですね。


ならもうスイッチは要らないじゃないかと思うかもしれませんが、起動時点ではRAMの中身はめちゃくちゃです。電源を入れてもまだプロセッサには止まってもらっておいて、その間にスイッチでRAMに機械語とデータのリスト…いわゆるプログラムを書き込んでおく必要があります。プロセッサを走らせて演算結果をRAMに格納したら、また止まってもらって、RAMの中身をLEDで除くことができます。

なんて立派なコンピュータ・システムに成長したのでしょう。


ただし、人間はあくまでも貪欲で、まだ満足しません。起動するたびにプログラムを打ち込むのがつらいだの、電源を入れっぱなしにしておくのはSDGs的ではないなどと不満を次々と発明します。

f:id:multireact:20211211111027p:plain

UART実装の図

 

機能過多になる恐れを抱きつつ、UARTを繋げました。

f:id:multireact:20211211111146p:plain

8250 UART

 

UARTを知らない人がいるかもしれませんが、風体はプロセッサやメモリと変わりません。接続方法も特に変わらないので、工作はまだ簡単です。


UARTがあると、テレ・タイプライタやターミナルに接続することができます。

f:id:multireact:20211211111708p:plain

テレタイプ

f:id:multireact:20211211111808p:plain

VT-100 ターミナル

f:id:multireact:20211211112419p:plain

ターミナルに繋がった図

 

これらの機器は、キーボードで打った文字をASCIIコードでコンピュータに読み込ませたり、反対にコンピュータからASCIIコードで受け取った文字を印刷あるいは表示することができます。


画面にコンピュータが文字を表示し、ユーザはそれに応じてキーボードで入力する。応じてコンピュータが次の文字を返す。いわゆるCLI環境が出来上がりました。

f:id:multireact:20211211113022p:plain

Star Trek

 

CLIでどんなことがしたいですか?


ワープロ?数値計算?ゲーム?


いいですね。キーボードと文字表示があればなんでもできます。
「ゲームなんて、プログラムをスイッチで打ち込むのがあまりに大変じゃないか」と思われるかもしれませんが、すでにUARTを実装したことを思い出してください。「ターミナルから送られてくる情報をひたすらRAMに書き込む」という簡単なプログラムをスイッチで書き込んでやれば、あとは使い慣れたキーボードから好きなだけプログラムを入力できます。

この小さなプログラムをブートローダと言ったりします。


さらに、テレタイプライタやターミナルには、紙テープの読み書き装置がついていることがあります。

f:id:multireact:20211211114254p:plain

パンチャ

 

f:id:multireact:20211211121135p:plain

紙テープ読み書き装置追加の図

 

テープに穴をあけて一度プログラムを書き込んでおけば、これを読み込ませるだけでタイピングの手間まで省いてプログラムをロードできます。


ところで、CLIでできる素敵なアプリケーションの代表としてゲームを挙げましたが、もっと素敵なものがあるかもしれないことに気が付きましたか?

f:id:multireact:20211211122034p:plain

BASIC(MITS Altair 680版)

 

そう、高級言語のインタプリタというのはどうでしょうか。機械語から解放されて、英語に近い言葉でプログラムを書いて実行することができます。本を開くなどすれば、機械語よりも多くのソフトウェア資産(特にゲーム!)が無尽蔵に見つかります。これこそがマイコンの華、BASICです。

f:id:multireact:20211211122620p:plain

BASIC環境の図

 

起動してブートローダを打ち込み、BASICインタプリタをテープからロードする。BASICが起動したら、今月の最新刊に載っている面白そうなゲームをそのまま打ち込んでみる。用心のために、ターミナルの表示先をパンチマシンに切り替えて、打ち込んだプログラム・リストをLISTコマンドで出力させ、テープに保存しておき、遊ぶ。

 

…

 

…発想はいいけどゲームバランスがおかしいんじゃないか?

もっといえば、ひとつ機能を追加するだけで劇的に面白くなるぞ。

 

でも今日はもう遅いから電源を切って寝る。あした同じプログラムをテープから読み込んでから、思いついた機能を追加してみよう…。

 

最強のマイコンは、こんなビジョンとともにあなたの脳内に完成しました。

 

あとはもう、蛇足です。なぜか隠し通してしまいましたが、ROMにブートローダを焼いて取り付ければスイッチを全く取っ払ってしまうこともできます。BASICでゲームをするなら、グラフィックもあった方がいいですね。じゃあCRTコントローラを付けましょう。

f:id:multireact:20211211152833p:plain

CRTコントローラとTV画面の図

これでテレビにグラフィックが表示できるようになりました。

f:id:multireact:20211211123955p:plain

スーパーファントム(日立ベーシックマスターレベル3)

 

シューティングゲームが遊べます。
音を出したり、キーボードでなくコントローラを繋いでみたり。パンチカードとほぼ同じ理屈で、カセットテープやフロッピーディスクを副記憶にすることも可能です。

f:id:multireact:20211211124234p:plain

カセットテープ

f:id:multireact:20211211124843p:plain

フロッピーディスク

歴史的なマシン


ここまでは妄想でした。実際に存在したマイコンを見てみましょう。

Altair 8800 [MITS社 1974年]

f:id:multireact:20211211125051p:plain

Altair 8800

アルテアと読むことが多いですが、アルタイルと読む場合もあります。
この、電源とスイッチとLEDの付いたデカい箱のキットの使い方はというと、先ほどの妄想をほぼそのままなぞることになります。
つまり、キットを買ってきて組み上げただけの最小構成では、マイクロプロセッサ(Intelの8080)を止めてスイッチを操作し、256B(バイト。1Byteは8bitなので、256*8=2064bit。)のRAMにプログラムを打ち込んでから走らせ、適宜LEDで状態を監視します。

f:id:multireact:20211211131252p:plain

Altairの中身

 

特筆すべきは、その貧弱な最小構成に対して強力な拡張性を秘めていたことです。マイクロプロセッサのピン数よりめっぽう多い100本のピンを持つ拡張スロットが大量に用意されていて、追加のメモリボードやUARTボード、プリンタやフロッピーディスク
へのインタフェースボードを差し込んで機能を拡張していくことができました。本体を作ったMITS社だけでなく、他の会社も拡張ボード市場に参入し始め、後にIEEE696として標準化までされる100ピンのバスはたいそう賑わったそうです。

f:id:multireact:20211211131529p:plain

クロメムコ Dazzler(TVビデオカード)

 

ボードだけでなく、バスの規格が同じでボードを流用できる互換機も現れました。

f:id:multireact:20211211131653p:plain

IMSAI 8080 (IMSアソシエイツ社 1975年)

 

コンピュータの互換機市場が発達するというとIBM PC/ATが思い浮かびますが、IBMと同じく、MITS社もこのことを意図していたわけではなく、厳しい競争に曝されて脱落することになります。
ソフトウェアでは、BASICが供給されました。このアルタイルBASICは、マイクロソフトの製品第一号として有名です。

Apple][ [Apple社 1977年]

f:id:multireact:20211211132029p:plain

Apple ][

 

アップルツーのツーはローマ数字の2、Ⅱですが、本体の印字が四角い括弧を逆に並べた][のようにみえるのでそう表記することがあります。


1977年は、コモドールPET2001、タンディラジオシャックTRS-80、Apple][の三機が発売され、初期のマイコン御三家と呼ばれます。

 

f:id:multireact:20211211133828p:plain

PET 2001(コモドール社 1977年)

f:id:multireact:20211211135639p:plain

TRS-80 Model 1(タンディ社 1977年)

 

外観から共通して言えるのは、キーボード入力・ビデオ出力が標準で可能になっていることです。TRS-80の画面は別売りの外付けですがセット販売が基本だったようです。

御三家からはもうマイコンではなくパソコンだとみる向きもあります。

f:id:multireact:20211211135930p:plain

Apple][の中身

 

入出力の備わった外観だけでなく、回路やプログラムも個人用コンピュータとして使いやすいように非常によく作られてはいますが、中身をよく見ればアルタイルと何から何まで別物というわけではありません。

f:id:multireact:20211211140028p:plain

MOS 6502

 

マイクロプロセッサにはモステクノロジー社の6502が採用されており、後述しますが私がApple][に注目している(推しと言ってもいい)理由はこれにあります。

f:id:multireact:20211211142146p:plain

ArticFox

 

色数の少ない高解像度モードでは、280*192ドットのグラフィックが表示できて、3D表現のあるゲームも動きます。

f:id:multireact:20211211143248p:plain

VisiCalc

f:id:multireact:20211211144223p:plain

10K BASIC

 

ソフトウェアでは、表計算ソフトのVisiCalcが事務用途で使えるということでキラーソフトになったようです。また、整数を扱える6KB版のBASICをスティーブ・ウォズニアックが作って標準装備していますが、そもそもハードウェアの設計もほぼすべて彼によるものです。

 

ちなみに、Apple][プラスに搭載された、浮動小数点計算のできる10KB版のBASICはマイクロソフトが供給しました。

f:id:multireact:20211211144517p:plain

逆アセンブラ表示

 

個人的には、標準で逆アセンブラが搭載されていることが衝撃的で、一番の魅力だとも思っています。今のAppleでは考えられないことです。


なんて、ごめんなさい。そもそもApple][以外のApple製品を全然知らないので現在がどうなのか何もわかりません。

 

さらなるマイコンワールドへ:書籍編


ひいひい、疲れました。
頑張って分かったように蘊蓄を述べてみましたが、上は全部これから紹介する本の受け売りです。いちいち「本にこう書かれてました」なんて付言していたらテンポが悪くなりますからね。私はレトロPCの類を一切持っていませんし触ったこともありません。


ひい、楽し…いや疲れました。


現代の本


過去のマシンを振り返る内容で比較的最近に書かれた本は、導入には最適です。もし興味を持ってマイコンについてさらに知りたいと思った方がいれば、これらの本から読み漁ることを強くお勧めします。


古典電脳物語

www.rutles.net

鈴木哲也という人の書いた古のマイコンについての本は全て、あるマイクロプロセッサを取り上げて、それにまつわる歴史をかなり深ーく掘り下げてから、そのマイクロプロセッサを実際に入手してマイコンを作る過程を紹介してくれます。
歴史背景もハードもソフトもわかる、そういう本です。


他の本よりは古くに出版されたこの本は、内容が盛りだくさんです。8085とZ80のマイコンを組んでいますが、後の本では省略されているような周辺回路の一般的な知識ノウハウも詳しく書かれていて、さらにはスタイリッシュなケースに収める工作までしています。2006年の本なので普通に書店で買うことはできないかもしれませんが、図書館にはあるかもしれません。始めの一冊におすすめします。


モトローラ6800伝説

www.rutles.net

鈴木氏が書いた、その名の通り6800についての本です。2017年と最近の本で、鈴木氏はシングルボードコンピュータ(SBC)というスタイルを固めています。6800につきものなMIKBUGというモニタプログラムや、VTL-02というBASICを圧縮したようなコンパクト(768バイト!)なインタプリタの存在はこの本で知りました。


ザイログZ80伝説

www.rutles.net同じく、今度は国産のマイコン/パソコンには採用例の多いZ80についての本です。それぞれ特定のプロセッサに着目しているとはいえマイクロプロセッサについて語れる歴史が三冊分もあるのかというと、たしかに記述が被ってはいるのですが、情報の更新があるらしく飽きるようなことはありません。


8080についての本もあるようですがまだ読んでいません。

6502とAppleⅡシステムROMの秘密

www.rutles.net鈴木氏の本ではありませんが、同じ出版社の本なので装丁の雰囲気などは似ています。6502とAppleⅡをひたすら詳しく語る本で制作記事はありませんが、これを読めば6502とAppleⅡの魅力に目覚めることは間違いなしです。6502のプログラムを書く時のハンドブックとしても大変優れていて、データシートよりもこっちをめくっていますが、ひとつだけ。

CMP命令の説明がたぶん間違ってるので気を付けてください。

 

当時の本

f:id:multireact:20211211145353p:plain

70年代80年代の、8bitマイクロプロセッサが登場して現役だったころの解説書も読んでみると面白いです。当時の意欲ある学生向けに書かれており、内容の充実度と平易さが共存しています。

日本語で書かれている資料だと当然、日本でよく使われたインテルやザイログのプロセッサを題材にしていることが多いですが、つい先日、6502について詳しく書かれた本を発見いたしました。

f:id:multireact:20211211145512p:plain

f:id:multireact:20211211145613p:plain

6502チップの開発元が開発した簡素な6502マイコンボードKIM-1について、回路図付きで詳しく説明しています。6502や、ましてKIM-1についてこんなに詳しく書ける日本人は多分いなかったのでこんな本もないと思っていましたが、海外で出版されたものの邦訳があるとは盲点でした。

さてこれらの本を今現在手に入れることは非常に困難ですが、多少の歴史がある工業系の大学の図書館であれば山のように眠っている場合があるようです。というか、そういうところでなければもうマニアの家とか国会図書館にしかないような気もします。

インターネット編

なによりもまずは本をすすめますが、インターネットもまた情報の宝庫であることは間違いありません。前述の鈴木氏のようなホビイスト、マニアが書いているブログなどのWebサイトがいくつもあります。一種のコミュニティーにもなっているようです。

電脳伝説 | 1970年代のCPUを語る

まごころせいじつ堂

TTLでCPUを作る組立てキット・トランジスタでCPUを作る組立てキット・TK−80互換マイコン組立キット・Z80マイコンボード・BASIC制御マイコンボード

FrontPage - neko Java Home Page

KUNINET BLOG

また、Youtubeには実機が稼働している様子や分解・修理する過程などの貴重な動画が沢山上がっています。

www.youtube.com

www.youtube.com

www.youtube.comエミュレータで遊ぶのも面白いです。ほかにも沢山あるでしょうが、Apple][のものを紹介します。

www.scullinsteel.com

f:id:multireact:20211211150745p:plain

Karateka

 

ブラウザ上で動くエミュレータです。ブラウザ上ではありますが動作速度は遅くなるどころか、制限を掛けないと実機よりずっと速く動いてしまいます。
マイクロプロセッサのエミュレータもあります。

www.visual6502.orgこの6502エミュレータは極めつけのもので、リバースエンジニアリングによってトランジスタレベルで動作がエミュレートされ、各状態で内部のどの信号線がオンになっているかを見ながらプログラムを実行できます。狂気です。

自作編


古い既製品を手に入れることは難しくても、パーツをかき集めて自分で組み立てれば実際にマイコンを使うことができます。むしろ、マイコンの世界のメイン・コンテンツでしょう。

f:id:multireact:20211211151115p:plain

SBC6800

 

前述の鈴木氏の本で組み立てられたマイコンは、回路図やガーバーデータ、ソフトウェアが公開されています。それをそのまま深圳の工場に発注したり頑張ってユニバーサル基板に配線してもいいですし、基板だけの販売もいくつかの電子部品ショップで行われているのでそれを利用するのも手です。他にも、インターネットを探せばオープンハードウェアとして公開されているマイコンボードはいくらでもあります。


ところで、マイコンを作るとなったら、どのマイクロプロセッサを選ぶかが一番重要な悩みどころになるかもしれません。これはもちろん好みの問題ですが、敢えて6502をおすすめしておきます。


6502

・Apple][やファミコン等、十分な実績やソフトウェア資産がある
・シンプルなアーキテクチャで、プログラミングしやすい
・狂信者愛好家が多く、コミュニティがしっかりしている
・静的設計のCMOS版がある
・手に入りやすい
これらの理由から6502がおすすめです。一番重要なのは手に入りやすいというところかもしれません。オリジナルの権利を引き継いでいるウェスタンデザインセンター(WDC)という会社が、6502を売り続けるマンとして頑張っているらしく、秋月電子で750円で買えます。


コミュニティーやソフトウェア資産があることも重要です。とりあえず6502.orgを訪れてみてください。6502に関するハード・ソフトの情報やアイデアがひしめいています。

ところで、今手に入る6502はWDCが開発したCMOS版で、オリジナルより高速で低消費電力、いくつかのバグが解消して命令も追加され、そして静的な設計になっています。

f:id:multireact:20211211151603p:plain

W65C02(CMOS版6502)

 

静的というのはクロックがじっと動かないでいてもデータが失われないということで、これによってボタンでシステムクロックを入力してステップずつ動作を確認するという究極の完全理解メソッドが簡単に実現できます。

www.youtube.comBenEaterという人が、Youtubeでこの方法を使って6502の動作を解説しつつ、液晶画面にHello,Worldを表示するところまでを実践する動画を公開しており、これを一通りなぞってから独自の拡張をするのがゼロからのスタートではもっとも良いやり方かもしれません。 (例: 6502あそび[2] Ben式の拡張とFxT-65)

最後に


妄想と受け売りと押し付けの長い文章にお付き合いいただき本当にありがとうございました。ぜひあなたなりのマイコンの楽しみ方を見つけてください。