前回, C++の開発環境を構築しました. これにより標準ライブラリが使え, ターミナルに文字を出力することができました.
ですが, これはゲームとは程遠いですね. 根本的にグラフィカルでありません. 太古の昔にはCUIゲームという文字ベースのゲームもあったそうですが, そんなものを作りたいと思ってこれを読んでる人はいないと思います.
我々が作りたいGUIゲームを作るには, まずウィンドウを立ち上げる必要がありますね. そしてそこに画像や文字を配置する必要があります. これらの機能をC++で実装するにはSDLというライブラリがデファクトスタンダードになっています. まずはこれをインストールしましょう.
SDLの最新版はSDL3です. 現在のインターネットの情報(とそれらを参考にしているAIの回答)は多くがSDL2がベースになっていますが, 2から3で仕様が大きく変わったので注意してください.
外部ライブラリのインストール
vcpkgを使えば一瞬です. 今,
-
sdl3
SDLの本体. -
sdl3-image[core,png]
画像のロードを行う. (SDL3ではこう書かないとpngが読み込めないっぽい(?)) -
sdl3-ttf
フォントのロードを行う.
のwindows(x64)版をインストールしたいとします. ターミナルで以下を実行してください.
vcpkg install sdl3:x64-windows sdl3-image[core,png]:x64-windows sdl3-ttf:x64-windows
ウィンドウを立ち上げる
ソースコード
// 01_HelloSDL/main1.cpp
#include <iostream>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
int main(int argc, char* argv[])
{
// SDL初期化
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cerr << "SDL初期化失敗: " << SDL_GetError() << "\n";
return 1;
}
// ウィンドウ生成
SDL_Window* window = SDL_CreateWindow(
"Hello SDL", // タイトルテキスト
800, 600, // 横幅、縦幅
SDL_WINDOW_RESIZABLE // リサイズ設定(ウィンドウの端をつかんでサイズを変えられるかどうか. 今はできるようになってる)
);
// ウィンドウ出現位置
SDL_SetWindowPosition(
window, // ウィンドウの変数名
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED // x座標, y座標 (画面の左上からウィンドウの左上までの距離). こう書くと中央に出現する.
);
// アイコンの設定 (.pngだと失敗したので.svgにするとよい)
SDL_Surface* iconSurf = IMG_Load("../assets/icons/T-Rex.svg"); // 実行するときにターミナル上で今いる場所からの相対パス
if (!iconSurf) {
std::cerr << "アイコンロード失敗: " << SDL_GetError() << "\n";
}
SDL_SetWindowIcon(window, iconSurf); // windowのアイコンをiconSurfで上書き
SDL_DestroySurface(iconSurf); // もうiconSurfは要らないのでメモリ解放
// メインループ
bool running = true;
while(running) {
SDL_Event ev;
while(SDL_PollEvent(&ev)) {
if (ev.type == SDL_EVENT_QUIT) {
running = false;
}
}
}
// 後処理(しなくても動くけど推奨される)
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
CMakeビルドと実行
# 01_HelloSDL/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloSDL LANGUAGES CXX)
# SDL3, SDL3_imageの探索
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_image CONFIG REQUIRED)
# 実行ファイル生成
add_executable(hello_sdl1 main1.cpp)
# ライブラリリンク
target_link_libraries(hello_sdl1
PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
)
ビルドして実行しましょう. キャッシュファイル生成時にvcpkg内の外部ライブラリを読み込む必要があります.
# buildフォルダを作成・移動
mkdir build && cd build
# キャッシュファイル作成
# パスは自分がvcpkgを入れた場所に従って変更してください.
cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/Users/**/vcpkg/scripts/buildsystems/vcpkg.cmake
# Debugフォルダの中に実行ファイルを生成
# 以降, コードを変更したらここからやり直す
cmake --build . --config Debug
# 実行
./Debug/hello_sdl1.exe
ウィンドウが立ち上がりました! ゲーム制作への大きな第一歩ですね! ターミナルへの出力しかしてこなかった人にとっては感動の瞬間かもしれません.
ここでメインループがどのように機能しているかについては各自で考えてみてください. 試しにメインループを消して実行するとわかると思います. 加えて, メインループの中のイベントループだけを消してみるのも面白いですが, ウィンドウを閉じる手段がなくなるのでおすすめしません. 損害が起きても責任はとれません. (強制終了すればいいだけだけど)
画像を配置する
次に画像を配置してみましょう. 使う画像はChromeの恐竜ゲームの恐竜にします. 各自で用意してください. 別にいらすとやとかでもいいです.
画像はウィンドウに配置するのではなく, ウィンドウの中にレンダラーというキャンバスを作成してそこに配置します.
ソースコード
// 01_HelloSDL/main2.cpp
#include <iostream>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
int main(int argc, char* argv[])
{
// SDL初期化
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cerr << "SDL初期化失敗: " << SDL_GetError() << "\n";
return 1;
}
// ウィンドウ生成
SDL_Window* window = SDL_CreateWindow(
"Hello SDL", // タイトルテキスト
800, 600, // 横幅、縦幅
SDL_WINDOW_RESIZABLE // リサイズ設定(ウィンドウの端をつかんでサイズを変えられるかどうか. 今はできるようになってる)
);
// ウィンドウ出現位置
SDL_SetWindowPosition(
window, // ウィンドウの変数名
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED // x座標, y座標 (画面の左上からウィンドウの左上までの距離). こう書くと中央に出現する.
);
// アイコンの設定 (.pngだと失敗したので.svgにするとよい)
SDL_Surface* iconSurf = IMG_Load("../assets/icons/T-Rex.svg"); // 実行するときにターミナル上で今いる場所からの相対パス
if (!iconSurf) {
std::cerr << "アイコンロード失敗: " << SDL_GetError() << "\n";
}
SDL_SetWindowIcon(window, iconSurf); // windowのアイコンをiconSurfで上書き
SDL_DestroySurface(iconSurf); // もうiconSurfは要らないのでメモリ解放
// レンダラー生成(ウィンドウの中に描画用のキャンバスを作る. 以降この中にテクスチャを配置していく. レンダラーは複数作れるけど, 普通1つで十分)
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
// 画像ロード
SDL_Texture* dino_tex = IMG_LoadTexture(renderer, "../assets/images/dino.png");
if (!dino_tex) {
std::cerr << "恐竜画像ロード失敗: " << SDL_GetError() << "\n";
}
// 画像を描画するための領域
// F: float(引数がintに制限されるSDL_Rectもある. ピクセルパーフェクトのためにそっちを使うこともある)
// Rect: rectangular(長方形)
SDL_FRect dino_frect = {
50.0f, 50.0f, // 位置
100.0f, 100.0f // サイズ
};
// メインループ
bool running = true;
while(running) {
SDL_Event ev;
while(SDL_PollEvent(&ev)) {
if (ev.type == SDL_EVENT_QUIT) {
running = false;
}
}
// レンダリング
// 1. レンダラーをクリア
SDL_RenderClear(renderer);
// 2. 画像を配置
SDL_RenderTexture(
renderer, // レンダラー「renderer」に以下の要領で
dino_tex, // テクスチャ「dino_tex」を配置せよ:
nullptr, // 画像をトリミングせずに(i.e. 元の画像を拡大/縮小して調整しながら) ← 毎回これではなく, スプライトシートを使う時にはトリミングして配置する
&dino_frect // 描画領域「dino_frect」の中に配置(読み取りしかしないのでポインタ渡し)
);
// 3. レンダラーの現在の状況を画面に表示(実はここまでは内部でレンダラーに配置しただけで, 画面に表示する命令ではなかった)
SDL_RenderPresent(renderer);
// 遅延させてリフレッシュレートを調整する. 引数の単位はミリ秒. これでおよそ60fps
SDL_Delay(16);
}
// 後処理(しなくても動くけど推奨される)
SDL_DestroyTexture(dino_tex);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
CMakeビルドと実行
ゲームというよりCMakeの話ですが, 複数の実行ファイルを同時に作ることができます. もうmain1.cppはビルドする必要ないし, なんなら多くの人がmain1,cppに上書きしてるかもしれませんが, CMakeの勉強もかねて複数のファイルを同時にビルドする書き方を見てみましょう.
# 01_HelloSDL/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloSDL LANGUAGES CXX)
# SDL3, SDL3_imageの探索
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_image CONFIG REQUIRED)
# 実行ファイル生成(main1)
add_executable(hello_sdl1 main1.cpp)
# ライブラリリンク
target_link_libraries(hello_sdl1
PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
)
# 実行ファイル生成(main2)
add_executable(hello_sdl2 main2.cpp)
# ライブラリリンク
target_link_libraries(hello_sdl2
PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
)
cmake --build . --config Debug
./Debug/hello_sdl2.exe
画像が表示されました! 嬉しいですね.
ちなみに背景が黒いですが, これはレンダラーの背景色を設定することで変えることができます. レンダラー定義後に以下を追加します. メインループの外に書けばずっとこの色ですし, メインループの中に書いてゲーム状況に応じて変化させることもできます.
SDL_SetRenderDrawColor(renderer, 255, 192, 203, 255);
引数は左からr, g, b, alpha(不透明度)です.
ところで, 画像の描画はメインループの中で行っているのに, 画像のロードは外で行っています. これは非常に重要なポイントです. 試しにメインループの中でロードを行うように書き換えて実行してみると面白いです. 実行中にタスクマネージャーのVS Codeの中にあるHello SDLのメモリ使用量を見ると徐々に増加していくのが分かります. これはフレームごとにディスクから画像をロードし, 新しいメモリ領域に格納しているせいです. 放っておくとそのうちゲームが落ちます.
文字を配置する
次に文字を配置しましょう. まずフォント(.ttf)を用意する必要があります. Google Fontsなどから調達してきてください.
ソースコード
文字の描画ですが, 画像よりも一手間多いです. まず文字列+フォント+大きさ+色の条件からCPU上で扱う「サーフェイス」を生成します. これをGPUが扱う「テクスチャ」に変換して描画します. フォントをロードしサーフェイスを作成するのにSDL_ttfを使います.
// 01_HelloSDL/main3.cpp
#include <iostream>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
int main(int argc, char* argv[])
{
// SDL初期化
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cerr << "SDL初期化失敗: " << SDL_GetError() << "\n";
return 1;
}
// SDL_ttf初期化
if (!TTF_Init()) {
std::cerr << "SDL_ttf初期化失敗: " << SDL_GetError() << "\n";
SDL_Quit();
return 1;
}
// ウィンドウ生成
SDL_Window* window = SDL_CreateWindow(
"Hello SDL", // タイトルテキスト
800, 600, // 横幅、縦幅
SDL_WINDOW_RESIZABLE // リサイズ設定(ウィンドウの端をつかんでサイズを変えられるかどうか. 今はできるようになってる)
);
// ウィンドウ出現位置
SDL_SetWindowPosition(
window, // ウィンドウの変数名
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED // x座標, y座標 (画面の左上からウィンドウの左上までの距離). こう書くと中央に出現する.
);
// アイコンの設定 (.pngだと失敗したので.svgにするとよい)
SDL_Surface* iconSurf = IMG_Load("../assets/icons/T-Rex.svg"); // 実行するときにターミナル上で今いる場所からの相対パス
if (!iconSurf) {
std::cerr << "アイコンロード失敗: " << SDL_GetError() << "\n";
}
SDL_SetWindowIcon(window, iconSurf); // windowのアイコンをiconSurfで上書き
SDL_DestroySurface(iconSurf); // もうiconSurfは要らないのでメモリ解放
// レンダラー生成(ウィンドウの中に描画用のキャンバスを作る. 以降この中にテクスチャを配置していく. レンダラーは複数作れるけど, 普通1つで十分)
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer) {
std::cerr << "レンダラー生成失敗: " << SDL_GetError() << "\n";
}
// 背景色を変更
SDL_SetRenderDrawColor(renderer, 35, 35, 35, 255);
// 画像ロード
SDL_Texture* dino_tex = IMG_LoadTexture(renderer, "../assets/images/dino.png");
if (!dino_tex) {
std::cerr << "恐竜画像ロード失敗: " << SDL_GetError() << "\n";
}
// 画像を描画するための領域
// F: float(引数がintに制限されるSDL_Rectもある. ピクセルパーフェクトのためにそっちを使うこともある)
// Rect: rectangular(長方形)
SDL_FRect dino_frect = {
50.0f, 50.0f, // 位置
100.0f, 100.0f // サイズ
};
// フォントロード
TTF_Font* NotoSansJP_R_32 = TTF_OpenFont("../assets/fonts/NotoSansJP-Regular.ttf", 32); // 第二引数はサイズ
// 色を定義
SDL_Color white = {255,255,255, 255}; // RGB+不透明度
// 表示する文字列
const char* text = "インターネットに接続されていません。";
// テキストサーフェス生成 (文字列 → ピクセル画像(CPU))
SDL_Surface* text_surf = TTF_RenderText_Blended(NotoSansJP_R_32, text, 0, white);
// サーフェイスをテクスチャに変換 (ピクセル画像(CPU) → ピクセル画像(GPU))
SDL_Texture* text_tex = SDL_CreateTextureFromSurface(renderer, text_surf);
// テクスチャに変換したらもうサーフェイスは要らないので破棄する
SDL_DestroySurface(text_surf);
// テクスチャのサイズ取得
float texW, texH;
SDL_GetTextureSize(text_tex, &texW, &texH); // text_texの横と縦がそれぞれ第二, 第三引数に格納される
// 描画スペースを確保
SDL_FRect text_frect = {
200.0f, 75.0f, // 座標
texW, texH // サイズ
};
// メインループ
bool running = true;
while(running) {
SDL_Event ev;
while(SDL_PollEvent(&ev)) {
if (ev.type == SDL_EVENT_QUIT) {
running = false;
}
}
// レンダリング
SDL_RenderClear(renderer);
SDL_RenderTexture(renderer, dino_tex, nullptr, &dino_frect);
SDL_RenderTexture(renderer, text_tex, nullptr, &text_frect); // レンダリング時の扱いは画像と同じ
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
// 後処理(しなくても動くけど推奨される)
SDL_DestroyTexture(dino_tex);
SDL_DestroyTexture(text_tex);
TTF_CloseFont(NotoSansJP_R_32);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
return 0;
}
CMakeビルドと実行
CMakeListsにSDL_ttfを追加する必要があります.
cmake_minimum_required(VERSION 3.15)
project(HelloSDL LANGUAGES CXX)
# SDL3, SDL3_image, SDL3_ttfの探索
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_image CONFIG REQUIRED)
find_package(SDL3_ttf CONFIG REQUIRED)
# 実行ファイル生成(main3)
add_executable(hello_sdl3 main3.cpp)
# ライブラリリンク
target_link_libraries(hello_sdl3
PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
SDL3_ttf::SDL3_ttf
)
無事文字も表示できました!
画像と同様, メインループの外でロードしています. ですがこの方法はスケールするのでしょうか? ある程度の規模のゲームで, ゲーム内に登場する文字列をすべて事前にロードするのは現実的とは思えません. どうしてもメインループの中でロードする必要がありそうです. そのための仕組みを導入するのは次回にしましょう.
予習: ゲームというアルゴリズム
あるプログラムがゲームであるとは, どういうことを言うのでしょうか? それは, (そのサービスとしての要件はひとまず無視するとして,) そのプログラムが次のアルゴリズムに従うことと言えます:
- 以下を1フレームごとに実行せよ:
- イベントハンドリング
- アップデート
- レンダリング
まず, ゲームは時間経過やプレイヤーからの入力などのイベントを受け取ります. 次に, そのイベントに従ってゲーム内容を更新します. 最後に, 更新した内容に従って画面を表示します. この一連の処理を16ミリ秒とかの時間ごとに行うループを, プレイヤーが満足するまでひたすら繰り返します.
単純なwhileループに見えますか? そうですね, 確かにこれだけだと単純です. このアルゴリズムの中に同じ構造のアルゴリズムを埋め込むようなことをしない限り, 単純ですね.