◆◆ ユーザー定義のストリーム ◆◆
非公式日本語翻訳
はじめに
SFML にはリソースを扱うクラス群があります。リソースというのは画像、フォント、サウンド、などのことです。
こういったリソースは通常、ファイルから読み込んでプログラム内で使いますよね(つまり、loadFromFile()関数を使う)。
あるいはリソースは実行ファイルに埋め込まれていたり、アーカイブ化されていることもあるかもしれません
(つまり、loadFromMemory()関数でメモリから取得)。
大体は、この2つの方法で事足りると思います。が、足りないこともある。
たとえば、圧縮ファイルからリソースを読み込みたいかもしれない。暗号化されているファイルから読み込みたいかもしれない。
ネットワークを通して、他のコンピューターから読み込みたいかもしれない。
そういう特殊なケースのために、SFML には第3の関数があります。loadFromStream()関数です。
この関数は、抽象クラス sf::InputStream からデータを読み込みます。
抽象クラスなので、中身は好きなように実装できます。
以下、このページでは、独自のストリームクラスを実装して使う方法を紹介します。
標準のストリーム機能を使えばいいのではなくて?
ご多分に漏れず、C++には入力ストリーム用のクラスが用意されています。
用意されているクラスは2つ。std::istream と std::streambuf。
前者はフロントエンド専用。独自のデータを扱うための抽象インターフェイスを提供しているのは後者です。
残念ながら、どちらもあんまりユーザーフレンドリーではありません。
しかも、細かい制御をしようとすると、非常に複雑になってしまいます。
Boost の Iostreams ライブラリはシンプルなインターフェイスを実現しようとしているようです、が、
Boost は非常に巨大なライブラリなので、SFMLとしては「使うといいよ」とは言いにくい。
そんなわけで、SFML は、独自のストリーム用のインターフェイスを用意しています。
よりシンプル&高速、で、ありますように……。
実装すべきインターフェイス
sf::InputStream という抽象クラス(インターフェイス)があります。
内部で4つの仮想関数が宣言されています。
以下は、各関数の仕様です。
このインターフェイスを継承し、仕様に沿って中身を実行することで、独自のストリームクラスとなります。
class InputStream
{
public :
virtual ~InputStream() {}
virtual Int64 read(void* data, Int64 size) = 0;
virtual Int64 seek(Int64 position) = 0;
virtual Int64 tell() = 0;
virtual Int64 getSize() = 0;
};
read()関数は、"size" バイトのデータをストリームから取り出し、"data" にコピーします。
戻り値は実際に取り出すことができたデータのバイト数です。エラーのときは -1 となります。
seek()関数は、ストリーム内での現在の読み込み位置を変更します。
引数 "position" が表しているのは、絶対位置です。データの開始位置からの相対座標とも言えます。
tell()関数は、ストリーム内での現在の読み込み位置を返します(単位は byte)。エラーのときは -1 になります。
getSize()関数は、ストリーム内のデータの総バイト数を返します。エラーのときには -1 となります。
実装例
ユーザー定義のインプットストリームの実装例です。
あくまで「例」なので、単にファイルからデータを読み込んでいるだけです。
実際に動く実装の例として、参考にしてください。
まずは、宣言(ヘッダー)から。
#include <SFML/System.hpp>
#include <string>
#include <cstdio>
class FileStream : public sf::InputStream
{
public :
FileStream();
~FileStream();
bool open(const std::string& filename);
virtual sf::Int64 read(void* data, sf::Int64 size);
virtual sf::Int64 seek(sf::Int64 position);
virtual sf::Int64 tell();
virtual sf::Int64 getSize();
private :
std::FILE* m_file;
};
このサンプルでは、C言語でおなじみの ファイルAPIを使っています。
privateメンバー変数として、std::FILE を持ちます。
そのほか、デフォルトコンストラクタ、デストラクタ、ファイルをオープンする open() 関数を追加しています。
では、次は実装です。
FileStream::FileStream() :
m_file(NULL)
{
}
FileStream::~FileStream()
{
if (m_file) {
std::fclose(m_file);
}
}
bool FileStream::open(const std::string& filename)
{
if (m_file) {
std::fclose(m_file);
}
m_file = std::fopen(filename.c_str(), "rb");
return m_file != NULL;
}
sf::Int64 FileStream::read(void* data, sf::Int64 size)
{
if (m_file)
{
return std::fread(data, 1, static_cast<std::size_t>(size), m_file);
}
else
{
return -1;
}
}
sf::Int64 FileStream::seek(sf::Int64 position)
{
if (m_file)
{
std::fseek(m_file, static_cast<std::size_t>(position), SEEK_SET);
return tell();
}
else
{
return -1;
}
}
sf::Int64 FileStream::tell()
{
if (m_file)
{
return std::ftell(m_file);
}
else
{
return -1;
}
}
sf::Int64 FileStream::getSize()
{
if (m_file)
{
sf::Int64 position = tell();
std::fseek(m_file, 0, SEEK_END);
sf::Int64 size = tell();
seek(position);
return size;
}
else
{
return -1;
}
}
どの関数も、エラーのときには -1 を返している点に注目。前の項で述べた仕様に沿ってます。
ところで……ネット上にはすでに、アナタのニーズに合うような sf::InputStream を作って公開している人がいらっしゃるかもしれません。
検索してみましょう!
そして、アナタが作ったクラスも誰かの役に立つかもしれません。ぜひぜひシェアしましょう!
自家製ストリームクラスを使う
まずは自家製のストリームクラスのインスタンスを生成。
データを読み込んだ後、そのデータを使いたいオブジェクトの loadFromStream()関数(または openFromStream()関数)
に、その自家製ストリームを渡します。
// 自家製ストリームで 画像を読み込んで、
FileStream stream;
stream.open("image.png");
// テクスチャとして使う
sf::Texture texture;
texture.loadFromStream(stream);
よくあるミス
リソースクラスの種類によっては、 一度 loadFromStream() を実行しただけでは読み込みが完了しないケースがあります。
そのリソースを使い続けている間、読み込みも続く。
たとえば、サウンドデータ(sf::Music)では、音声を再生している間、ストリーミングが続行します。
フォント(sf::Font)では、テキストの内容に応じて文字の形状(グリフ)を取得し続けます。
つまり、そういうストリームクラスのインスタンス(と、元データ)は、
そのリソースが使われる間は存在し続けている必要があります。
まだ使われているのにインスタンスが破棄されてしまったら、何が起きるのかはパルプンテです
(何も表示されない or 崩れたデータ or クラッシュ or とてつもなく……)。
もう1つのよくあるミスは、内部の実装に使った関数の戻り値をそのまま使ってしまうことです。
そのせいで、SFMLのストリームの仕様通りではなくなってしまう。
たとえば、上記のサンプルのようなファイル読み込みで、
うっかりな誰かさんは次のような実装をしたくなってしまうかもしれません。
sf::Int64 FileStream::seek(sf::Int64 position)
{
return std::fseek(m_file, position, SEEK_SET);
}
これの何がダメなのか?
std::fseek は成功時に ゼロを返します。一方、SFMLの仕様では、新たな読み込み位置を返す、ということになってます。
(訳註:上の実装サンプルの
seek()関数内では、戻り値として tell() の値を返していることに注目)