0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++でローグライクを作成してみる - 2. SDL_ttfで文字列表示

Last updated at Posted at 2025-11-10

概要

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だけ(前回の手順に従って)使える状態にあるという場合は,次のことを行えば問題なく利用できるはずです.

  1. hello.cを上記サイトのリンク先ものに更新する
  2. SDL_ttfのソースコードをダウンロードして好きな場所に展開する
  3. 依存関係を入手
  4. SDL_ttfを自分のプロジェクトのサブプロジェクトとして追加する
  5. SDL_ttfを自分のプロジェクトの参照に加え,SDL_ttfのフォルダ内のincludeディレクトリを(メインプロジェクトの)プロパティの「追加のインクルードディレクトリ」に追加する
  6. 同様にSDLをSDL_ttfの参照に加え,SDLのincludeディレクトリを(SDL_ttfプロジェクトの)プロパティの「追加のインクルードディレクトリ」に追加する

プロジェクト同士の依存関係に注意するのと,もしかしたらリンカの設定をいじる必要があるかもしれません.
以上,問題なくできていれば,実行すると画面中央に"Hello World!"と表示されるはずです.
image.png

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クラスの定義は以下の通りです.

Game.h
#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()は現状何も行っていません.

これを利用する側,つまりメインのソースコードは次のようになります.

main.cpp
#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リファレンスを少しづつ読んでいますが,なかなか進みません.今回は本文中に「ローグライク」と一度も書くことなくこの節まで来てしまいました.次回はせめてもう少しゲームらしき見た目にすることを目指します.

ここまで読んでいただきありがとうございました.

参考文献

  1. Sanjay Madhav. 吉川邦夫訳. ゲームプログラミング C++. 翔泳社, 2018.
  2. SDL Wiki https://wiki.libsdl.org/SDL3/FrontPage
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?