SDL2の基本的な使い方

プログラマーの尾関です。

今回はちょっとした2Dゲームのプログラムを書く環境のに便利な “SDL2” の基本的な使い方を説明します。

SDL2とは

SDL2 とは “Simple DirectMedia Layer 2” の意味で、ゲームやマルチメディアアプリケーションで使用されるライブラリです。

環境を作る

SDL2 は C言語で書かれていて、様々なプラットフォームで動くのですが、今回は以下の環境でビルドできるようにしてみます。

  • Windows 11
  • Visual Studio 2022

ライブラリのダウンロード

ライブラリは SDL公式ページからダウンロードします。

右上にある “Get the current stable SDL_version” のリンクをクリック。

GitHubのページへ遷移します。”VC” という名前がついているZIPファイルをダウンロードしました。

プロジェクトの作成

Visual Studioを起動して “C++” の “コンソールアプリ” でプロジェクトを作成します。

先ほどダウンロードしたライブラリをプロジェクトがあるフォルダにコピーしておきます。

他のプロジェクトでもSDLを使う場合は、プロジェクトに含めずに「C:\SDL2」といった固定のパスにしたほうが良いかもしれませんが、今回はプロジェクトに含めるようにしました。

ライブラリにパスを通したいので、プロジェクトのプロパティを開いて「C/C++ > 全般」から「追加のインクルードディレクトリ」に “$(ProjectDir)sdl2\include” と指定します。

$(ProjectDir) はプロジェクトのフォルダを示す環境変数ですので、これでプロジェクトの場所を移動させてもパスが正しく設定されます。

次にリンクするライブラリの指定です。

リンカー > 全般」から「追加のライブラリディレクトリ」に “$(ProjectDir)sdl2\lib\$(PlatformShortName)” を指定します。

$(PlatformShortName) はビルド時に指定するプラットフォームです。x64 で動作すれば問題ないと思いますが、x86 のライブラリも提供されていたので念のため切り替えられるようにしました。

最後に「リンカー > 入力」の「追加の依存ファイル」に “SDL2.lib” “SDL2main.lib” を指定します。

画像では “SDL2_image.lib” の指定がありますが、画像の読み込みを行わないのであれば不要です。

もし画像ファイルを読み込みたい場合は、以下のページの「GitHub」のリンクから別途ダウンロードする必要があります。

main関数を書く

では SDL を使って main 関数を書いてみます。

#include "SDL.h"

// メイン関数.
int main( int argc, char* argv[] )
{
    // SDLの初期化.
    if ( SDL_Init( SDL_INIT_VIDEO ) != 0 ) {
        SDL_Log( u8"SDLの初期化処理に失敗しました。エラーメッセージ: %s", SDL_GetError() );
        return -1;
    }

    // SDLを使用した処理
    SDL_Delay(3000); // 3秒待つ.

    SDL_Quit();
    return 0;
}

SDLを初期化して、3秒待ってから終了する…というコードです。

さっそく実行してみると、以下のエラーとなります。

SDL2の実行には SDL2.dll が必要なのですが、それが見つからないため動かせない…というエラーです。

実行ファイルが作られるフォルダに手動でコピーしても良いですが、ビルド後に自動でコピーするとプロジェクトを配布した場合でも正常に動かせるので良いと思います。

自動でコピーするにはプロジェクト設定から「ビルドイベント > コマンドライン」に “xcopy” を使ったコマンドを指定します。

xcopy "$(ProjectDir)sdl2\lib\$(PlatformShortName)\SDL2.dll" "$(OutDir)" /i /s /y

“SDL2.dll” は libフォルダにあるので、これをコピーするコマンドです。

  • 補足:もし SDL2_image を使う場合には、”SDL2_image.dll” も同じように自動コピーすると良いです

この状態で実行すると、ウィンドウが3秒表示され、自動で閉じるようになりました。

ウィンドウの表示と描画を行う

ウィンドウの表示や描画を行うには以下の処理が必要となります。

  • SDL_CreateWindow() でウィンドウのインスタンスを生成する
  • SDL_CreateRenderer() でレンダラーのインスタンスを生成する
#include "SDL.h"

// ウィンドウのサイズ.
const int WINDOW_WIDTH = 1280;
const int WINDOW_HEIGHT = 720;

// 各種インスタンス.
static SDL_Window* s_pWindow = nullptr;
static SDL_Renderer* s_pRenderer = nullptr;

// 前方宣言.
extern void _Finalize(); // 終了処理.

// メイン関数.
int main( int argc, char* argv[] )
{
    // SDLの初期化.
    if ( SDL_Init( SDL_INIT_VIDEO ) != 0 ) {
        SDL_Log( u8"SDLの初期化処理に失敗しました。エラーメッセージ: %s", SDL_GetError() );
        return -1;
    }

    // ウィンドウの生成.
    s_pWindow = SDL_CreateWindow(u8"SDL2テスト!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, 0 );
    if ( s_pWindow == nullptr ) {
        SDL_Log( u8"ウィンドウの作成に失敗しました。エラーメッセージ: %s", SDL_GetError() );
        _Finalize();
        return -1;
    }

    // レンダラーの作成 (GPUに一部処理させる).
    s_pRenderer = SDL_CreateRenderer(s_pWindow, -1, SDL_RENDERER_ACCELERATED);
    if ( s_pRenderer == nullptr ) {
        SDL_Log( u8"レンダラの作成に失敗しました。エラーメッセージ: %s", SDL_GetError() );
        _Finalize();
        return -1;
    }

    // 画面を消去.
    SDL_SetRenderDrawColor( s_pRenderer, 0, 0, 0, 0xFF ); // 黒色で消す.
    SDL_RenderClear( s_pRenderer );
    // 図形を描画.
    SDL_SetRenderDrawColor( s_pRenderer, 0xFF, 0, 0, 0xFF );
    SDL_Rect rect = {32, 32, 256, 128}; // 矩形を作る.
    SDL_RenderFillRect( s_pRenderer, &rect);
    // 画面更新.
    SDL_RenderPresent(s_pRenderer);

    SDL_Delay(3000); // 3秒待つ.

    // 終了処理.
    _Finalize();
    return 0;
}

// 終了処理.
void _Finalize()
{
    if(s_pRenderer) {
        SDL_DestroyRenderer( s_pRenderer ); // レンダラーの破棄.
        s_pRenderer = nullptr;
    }

    if(s_pWindow) {
        SDL_DestroyWindow( s_pWindow ); // ウィンドウの破棄.
        s_pWindow = nullptr;
    }

    // SDLを終了.
    SDL_Quit();
}

コード量は増えましたが、ウィンドウと描画のインスタンスを生成して、描画処理を呼び出しているだけです。

生成した後は破棄処理が必要なので、それぞれのインスタンスをstatic変数に保持して破棄処理を共通化しました。

実行すると、黒い画面に赤い矩形が表示されました。

メインループと入力処理を作る

SDL2の入力判定は、対象のキーを「押し続けているかどうか」を取得することしかできません。そのため「そのフレームに押したかどうか」を判定するには「現在のフレームでの入力情報」と「1フレーム前の入力情報」の2つを保持する必要があります。

具体的には以下のstatic変数を定義します。

// 入力情報.
const int MAX_KEYBOARD = 1024; // キーボードの入力の最大.
static int s_KeyNums = 0;
static Uint8 s_pKeyState[MAX_KEYBOARD] = {}; // 現在のフレームの入力情報.
static Uint8 s_pKeyStatePrev[MAX_KEYBOARD] = {}; // 1フレーム前の入力情報.

キーボードの入力情報の更新は以下のように書きます。

// キーボード入力の更新.
static void UpdateKeyboardInput()
{
  // 前回の入力をコピーしておく.
  // 最大数を超えないように念のため.
  int num = std::min(MAX_KEYBOARD, s_KeyNums);
  for(int i = 0; i < s_KeyNums; i++) {
    s_pKeyStatePrev[i] = s_pKeyState[i];
  }

  // 入力情報を取得する.
  auto* pKeyState = SDL_GetKeyboardState(&s_KeyNums);
  for(int i = 0; i < s_KeyNums; i++) {
    // ポインタの中身をコピーしておく.
    // 実体をコピーしないと次回更新時に値が書き換わってしまうため.
    int num = std::min(MAX_KEYBOARD, s_KeyNums);
    for(int i = 0; i < s_KeyNums; i++) {
      s_pKeyState[i] = pKeyState[i];
    }
  }
}

注意点として、SDL_GetKeyboardState() で取得したポインタは実体をコピーしないと1フレーム前の情報も消えてしまいます。そのためポインタではなく配列にコピーしています。

これで入力情報が保持できたので、以下のような入力を判定する関数を作ることができます。

// キーボードの入力判定.
// このフレームで押したかどうか.
static bool KeyJustPressed(eKey Key)
{
  if(s_pKeyStatePrev && s_pKeyStatePrev[Key] == 1) {
    // 前フレームで押している.
    return false;
  }
  if(s_pKeyState[Key] == 1) {
    // 前フレームで押していなくて現在のフレームで押したかどうか.
    return true;
  }
  return false;
}

// 押し続けているかどうか.
static bool KeyPressed(eKey Key)
{
  return s_pKeyState[Key] == 1;
}

キーボード定数は以下のように定義してみました。

// キーボード定数.
enum eKey {
  eKey_Left  = SDL_SCANCODE_LEFT,
  eKey_Up    = SDL_SCANCODE_UP,
  eKey_Right = SDL_SCANCODE_RIGHT,
  eKey_Down  = SDL_SCANCODE_DOWN,
  eKey_Z     = SDL_SCANCODE_Z,
  eKey_X     = SDL_SCANCODE_X,
  eKey_C     = SDL_SCANCODE_C,
  eKey_V     = SDL_SCANCODE_V,
};

SDL1の時代とは入力処理が変わってしまったのか “SDL_SCANCODE_*” 定数を使う必要がある…というのがポイントです。

仕上げとしてメインループに組み入れてみました。

#include "SDL.h"
#include <iostream>

// キャプション.
const char* WINDOW_CAPTION = u8"SDL2テスト!";

// ウィンドウのサイズ.
const int WINDOW_WIDTH = 1280;
const int WINDOW_HEIGHT = 720;
// フレームレート.
const int FRAME_PER_SECOND = 60;

// 各種インスタンス.
static SDL_Window* s_pWindow = nullptr;
static SDL_Renderer* s_pRenderer = nullptr;

// 入力情報.
const int MAX_KEYBOARD = 1024; // キーボードの入力の最大.
static int s_KeyNums = 0;
static Uint8 s_pKeyState[MAX_KEYBOARD] = {}; // 現在のフレームの入力情報.
static Uint8 s_pKeyStatePrev[MAX_KEYBOARD] = {}; // 1フレーム前の入力情報.

// キーボード定数.
enum eKey {
  eKey_Left = SDL_SCANCODE_LEFT,
  eKey_Up = SDL_SCANCODE_UP,
  eKey_Right = SDL_SCANCODE_RIGHT,
  eKey_Down = SDL_SCANCODE_DOWN,
  eKey_Z = SDL_SCANCODE_Z,
  eKey_X = SDL_SCANCODE_X,
  eKey_C = SDL_SCANCODE_C,
  eKey_V = SDL_SCANCODE_V,
};

// 前方宣言.
extern bool _Initialize(); // 初期化.
extern void _Finalize(); // 終了処理.
extern void _UpdateKeyboardInput(); // 入力処理の更新.
// キーボードの入力判定.
static bool KeyJustPressed(eKey Key); // このフレームで押したかどうか.
static bool KeyPressed(eKey Key); // 押し続けているかどうか.

// テスト用変数.
static int s_PosX = 240;
static int s_PosY = 240;
static int s_Timer = 0;

// ====================================================
// メインループ.
// ====================================================
bool _MainLoop()
{
  // 入力イベントのポーリング.
  SDL_Event e = {};
  if (SDL_PollEvent(&e)) {
    if (e.type == SDL_QUIT) {
      // 閉じるボタン.
      return false; // おしまい.
    }
    else if (e.type == SDL_KEYUP && e.key.keysym.sym == SDLK_ESCAPE) {
      // ESCキー.
      return false; // おしまい.
    }
  }

  // 入力の更新.
  _UpdateKeyboardInput();
  // オブジェクトの移動.
  s_PosX += 8 * (KeyPressed(eKey_Right) - KeyPressed(eKey_Left));
  s_PosY += 8 * (KeyPressed(eKey_Down) - KeyPressed(eKey_Up));
  s_Timer++;

  // クリアカラーで消去.
  SDL_SetRenderDrawColor(s_pRenderer, 0, 0, 0, 0xFF); // 黒色.
  SDL_RenderClear(s_pRenderer);

 // オブジェクトの描画.
 const int DIV = 16;
 for(int i = 0; i < DIV; i++) {
  float d = ((float)i / DIV);
  int r = 0xFF;
  int gb = (int)(0xFF * (1 - d));
  SDL_SetRenderDrawColor(s_pRenderer, r, gb, gb, r); // 色を設定.
  float idx = (DIV * d);
  int px = s_PosX + 64 * cos((s_Timer * 0.1f) + idx * 3.14f * 2 / DIV);
  int py = s_PosY + 64 * sin((s_Timer * 0.1f) + idx * 3.14f * 2 / DIV);
  SDL_Rect rect = { px, py, 8, 8 }; // 矩形を作る.
  SDL_RenderFillRect(s_pRenderer, &rect);
 }

  // スクリーンに反映.
  SDL_RenderPresent(s_pRenderer);

  return true; // 続行.
}

// ====================================================
// メイン関数.
// ====================================================
int main(int argc, char* argv[])
{
  // 初期化.
  if (_Initialize() == false) {
    // 初期化失敗.
    _Finalize();
    return -1;
  }

  // メインループ.
  bool isLoop = true;
  while (isLoop) {
    isLoop = _MainLoop();

    // TODO: フレームレートの固定化は未実装.
    SDL_Delay(1000 / FRAME_PER_SECOND);
  }

  // おしまい.
  _Finalize();

  return 0;
}

// ====================================================
// 初期化.
// ====================================================
static bool _Initialize()
{
  // SDLの初期化.
  if (SDL_Init(SDL_INIT_VIDEO) != 0) {
    SDL_Log(u8"SDLの初期化処理に失敗しました。エラーメッセージ: %s", SDL_GetError());
    return false;
  }

  // ウィンドウを生成.
  s_pWindow = SDL_CreateWindow(WINDOW_CAPTION, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, 0);
  if (s_pWindow == nullptr) {
    SDL_Log(u8"ウィンドウの作成に失敗しました。エラーメッセージ: %s", SDL_GetError());
    return false;
  }

  // レンダラーの生成.
  s_pRenderer = SDL_CreateRenderer(s_pWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
  if (s_pWindow == nullptr) {
    SDL_Log(u8"レンダラーの作成に失敗しました。エラーメッセージ: %s", SDL_GetError());
    return false;
  }

  // 初期化成功.
  return true;
}

// 終了処理.
void _Finalize()
{
  if (s_pRenderer) {
    SDL_DestroyRenderer(s_pRenderer); // レンダラーの破棄.
    s_pRenderer = nullptr;
  }
  if (s_pWindow) {
    SDL_DestroyWindow(s_pWindow); // ウィンドウの破棄.
    s_pWindow = nullptr;
  }
  // SDLを終了.
  SDL_Quit();
}

// キーボード入力の更新.
static void _UpdateKeyboardInput()
{
  // 前回の入力をコピーしておく.
  // 最大数を超えないように念のため.
  int num = std::min(MAX_KEYBOARD, s_KeyNums);
  for (int i = 0; i < s_KeyNums; i++) {
    s_pKeyStatePrev[i] = s_pKeyState[i];
  }
  // 入力情報を取得する.
  auto* pKeyState = SDL_GetKeyboardState(&s_KeyNums);
  for (int i = 0; i < s_KeyNums; i++) {
    // ポインタの中身をコピーしておく.
    // 実体をコピーしないと次回更新時に値が書き換わってしまうため.
    int num = std::min(MAX_KEYBOARD, s_KeyNums);
    for (int i = 0; i < s_KeyNums; i++) {
      s_pKeyState[i] = pKeyState[i];
    }
  }
}

// キーボードの入力判定.
// このフレームで押したかどうか.
static bool KeyJustPressed(eKey Key)
{
  if (s_pKeyStatePrev && s_pKeyStatePrev[Key] == 1) {
    // 前フレームで押している.
    return false;
  }
  if (s_pKeyState[Key] == 1) {
    // 前フレームで押していなくて現在のフレームで押したかどうか.
    return true;
  }
  return false;
}
// 押し続けているかどうか.
static bool KeyPressed(eKey Key)
{
  return s_pKeyState[Key] == 1;
}

上下左右キーで回転する円を動かすコードとなります。

参考

今回の記事を書くにあたって以下のページを参考にしました。

SDL2の基本的な機能の説明とサンプルコードがまとまっていてオススメです。

以上、SDL2の基本的な使い方でした。

Follow me!