概要
SDLを用いて画面を作成できるという状態から,フォントを読み込んで画面に表示できるようにする.また,実行中は常に存在してゲームの状態を管理するGameクラスを作成し,データ構造を整理する.
なお,ローグライクの話にはたどり着かないのでまだタイトル詐欺.
はじめに
前回
前回は画面を表示するところまで行きました.まだSDL wikiのサンプルそのままです.
今回の話
文字を表示します.前回のサンプルの実行画面でも文字は表示されましたが,これはデバッグ用のレンダリングコマンド(参照:wiki)を用いており,汎用性はありません.SDLは True Type font を扱うためのライブラリSDL_ttfを提供しており,まずはこれを導入します.
開発環境
Windows11 Home 25H2
Visual Studio Community 2022
使用するライブラリは以下の通り
SDL3 (version: 3.2.26)
SDL3_ttf (version: 3.2.2)
フォントの取り扱い
SDL_ttfの導入
さて,SDL_ttfですが,これを導入するためのサンプルと手順がすでにSDL wikiに用意されています(リンク).
使用するソースコードはこちら
初めからこちらに従ってプロジェクトを構築すればSDL本体もSDL_ttfも使える状態になるので,前回の内容はあまり意味が無いという……まあ良いでしょう.
SDLだけ(前回の手順に従って)使える状態にあるという場合は,次のことを行えば問題なく利用できるはずです.
- hello.cを上記サイトのリンク先ものに更新する
- SDL_ttfのソースコードをダウンロードして好きな場所に展開する
- 依存関係を入手
- SDL_ttfを自分のプロジェクトのサブプロジェクトとして追加する
- SDL_ttfを自分のプロジェクトの参照に加え,SDL_ttfのフォルダ内のincludeディレクトリを(メインプロジェクトの)プロパティの「追加のインクルードディレクトリ」に追加する
- 同様にSDLをSDL_ttfの参照に加え,SDLのincludeディレクトリを(SDL_ttfプロジェクトの)プロパティの「追加のインクルードディレクトリ」に追加する
プロジェクト同士の依存関係に注意するのと,もしかしたらリンカの設定をいじる必要があるかもしれません.
以上,問題なくできていれば,実行すると画面中央に"Hello World!"と表示されるはずです.

SDL_ttfライブラリで使用できる関数(一部)の紹介
bool TTF_Init(); // Initialize成功でtrueを返す
void TTF_Quit();
兎にも角にも,最初(このライブラリの他の関数を使う前)にTTF_Init()を実行,そして最後にTTF_Quit()を実行します.
TTF_Font * TTF_OpenFont(const char *file, float ptsize);
TTF_Font * TTF_OpenFontIO(SDL_IOStream *src, bool closeio, float ptsize);
void TTF_CloseFont(TTF_Font *font);
名前の通り,TTF_OpenFont()/TTF_OpenFontIO()はフォントをロードし,TTF_CloseFont()はフォントを開放します(TTF_Quit()では行われないので書く必要あり).
TTF_OpenFont()は外部のファイルから,TTF_OpenFontIO()はSDL_IOStream型構造体からフォントをロードします.ptsizeはフォントのサイズです.今回のサンプルではSDL_IOFromConstMem()に直接フォントの中身を直接入れて読み取り専用のストリームを作り,そこから読んでいるようです.
(TTF_OpenFontIO()がなんの用途で使われるのかあまりよくわかっていません……)
SDL_Surface * TTF_RenderText_Blended(TTF_Font *font, const char *text, size_t length, SDL_Color fg);
SDL_Surface * TTF_RenderText_Blended_Wrapped(TTF_Font *font, const char *text, size_t length, SDL_Color fg, int wrap_width);
fontでフォント,fgで色を指定して,textの文字列に対応するSDL_Surfaceへのポインタを生成します.lengthは文字列の長さを指定しますが,0を指定すればnull文字まで読み込むので,基本は0で問題ありません.
_Wrappedとついている方の関数は,wrap_widthで指定した幅で折り返しを行います.
SDL_Surface * TTF_RenderText_LCD(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg);
SDL_Surface * TTF_RenderText_Shaded(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg);
SDL_Surface * TTF_RenderText_Solid(TTF_Font *font, const char *text, size_t length, SDL_Color fg);
どれもTTF_RenderText_Blended()同じ文字列からSDL_Surface*を生成する関数ですが,それぞれ特徴が異なります.
_LCD:サブピクセルレンダリングを行う.横方向がより鮮明に
_Shaded:フォアグラウンドと背景色を指定して描画する.
_Solid:最も高速で軽量.高頻度で更新する要素に向く
また,それぞれに対して後ろに_Wrappedがついた関数も存在します.
// TextEngine の作成
TTF_TextEngine * TTF_CreateSurfaceTextEngine();
TTF_TextEngine * TTF_CreateRendererTextEngine(SDL_Renderer *renderer);
TTF_TextEngine * TTF_CreateGPUTextEngine(SDL_GPUDevice *device);
// 破棄
void TTF_DestroySurfaceTextEngine(TTF_TextEngine *engine);
void TTF_DestroyRendererTextEngine(TTF_TextEngine *engine);
void TTF_DestroyGPUTextEngine(TTF_TextEngine *engine);
// 描画用のオブジェクトを生成・破棄
TTF_Text * TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length);
void TTF_DestroyText(TTF_Text *text);
// 描画(エンジンに応じた描画関数)
bool TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface);
bool TTF_DrawRendererText(TTF_Text *text, float x, float y);
TTF_GPUAtlasDrawSequence * TTF_GetGPUTextDrawData(TTF_Text *text); // たぶんこれ
まとめて行きます.これらはSDL_ttfのversion 3から新たに使えるようになった方法のようです.
まずTTF_CreatehogeTextEngine()で,TTF_TextEngineという構造体を作成します.
次に,TTF_TextEngineとフォント,生成したい文字列からTTF_TextをTTF_CreateText()で作成します.このTTF_Textは,TTF_SetTexthuga()という関数で要素huga(ColorやDirectionなど)を変更できます.
そして,TextEngineに応じて別の関数を用いて描画します.
最後に,不要になったTTF_TextやTTF_TextEngineは破棄します.
この機能は文字列の描画を,描画先によらず統一的に扱うためのものだと思われます.また,一度作成したテキスト(TTF_Text)に対して属性の変更が行えるようになったことで,動的なテキスト表示の改善が期待されます.
状態管理クラスの作成
さて,ようやくといったところですが,SDLのサンプルから脱却しましょう.
まず,グローバル変数の利用を止めて,状態管理クラスGameの中に納めます.また同時に,文字列の描画を先ほど紹介したversion 3から使えるようになった方法へと変更します.
まず,Gameクラスの定義は以下の通りです.
#pragma once
class Game
{
public:
Game();
~Game();
bool Initialize();
void Shutdown();
void Update();
void Output();
private:
struct SDL_Window* mWindow;
struct SDL_Renderer* mRenderer;
struct TTF_TextEngine* mEngine;
struct TTF_Font* mFont;
struct TTF_Text* mText;
};
サンプルではグローバル変数として管理していたSDL_Window*,SDL_Renderer*,TTF_Font*がメンバ変数に含まれています.また,SDL3_ttfの方式による描画の為のTTF_TextEngine*と,画面に表示する文字を保持するTTF_Text*もメンバ変数です.
また,このクラスではコンストラクタとは別にメンバ関数Initialize()を用意していますが,これは初期化の成功・失敗を戻り値として返すためです.それに対応してShutdown()も用意しています.
次に,Gameクラスの実装を一部省略して以下に示します.
bool Game::Initialize()
{
if (!SDL_CreateWindowAndRenderer("Hello World", 800, 600, SDL_WINDOW_FULLSCREEN, &mWindow, &mRenderer)) {
SDL_Log("Couldn't create window and renderer: %s\n", SDL_GetError());
return false;
}
if (!TTF_Init()) {
SDL_Log("Couldn't initialize SDL_ttf: %s\n", SDL_GetError());
return false;
}
mEngine = TTF_CreateRendererTextEngine(mRenderer);
if (!mEngine) {
SDL_Log("Couldn't create text engine: %s\n", SDL_GetError());
return false;
}
mFont = TTF_OpenFont("your/font/path", 24.0);
if(!mFont) {
return false;
}
mText = TTF_CreateText(mEngine, mFont, u8"Hello World!", 0);
return true;
}
void Game::Shutdown()
{
TTF_DestroyText(mText);
TTF_DestroyRendererTextEngine(mEngine);
TTF_CloseFont(mFont);
TTF_Quit();
}
void Game::Output()
{
int w = 0, h = 0;
int w2, h2;
const float scale = 1.0f;
SDL_GetRenderOutputSize(mRenderer, &w, &h);
SDL_SetRenderScale(mRenderer, scale, scale);
TTF_GetTextSize(mText, &w2, &h2);
w = ((w / scale) - w2) / 2;
h = ((h / scale) - h2) / 2;
SDL_SetRenderDrawColor(mRenderer, 0, 0, 0, 255);
SDL_RenderClear(mRenderer);
TTF_DrawRendererText(mText, w, h);
SDL_RenderPresent(mRenderer);
}
基本的にはこれまでSDL_App**()関数で行っていた処理を移してきた形になります.ちなみに,フォントは外部から読み込む形式に変えています.
なお,Update()は現状何も行っていません.
これを利用する側,つまりメインのソースコードは次のようになります.
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Game.h"
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
Game* game = new Game();
if (!game->Initialize()) {
return SDL_APP_FAILURE;
}
*appstate = game;
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
Game* game = (Game*)appstate;
game->Update();
if (event->type == SDL_EVENT_KEY_DOWN ||
event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void* appstate)
{
Game* game = (Game*)appstate;
game->Output();
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
Game* game = (Game*)appstate;
game->Shutdown();
delete game;
}
見ての通りほとんどの処理をGameクラスに奪われてしまいました.一つの関数内で完結しない変数に関わる処理はGameクラスに任せるのが妥当だと考えられるので,現状仕方なくはあります.
ともあれ,これがデータ構造の一番基盤の部分になります!
終わりに
SDLのAPIリファレンスを少しづつ読んでいますが,なかなか進みません.今回は本文中に「ローグライク」と一度も書くことなくこの節まで来てしまいました.次回はせめてもう少しゲームらしき見た目にすることを目指します.
ここまで読んでいただきありがとうございました.
参考文献
- Sanjay Madhav. 吉川邦夫訳. ゲームプログラミング C++. 翔泳社, 2018.
- SDL Wiki https://wiki.libsdl.org/SDL3/FrontPage