Edited at
DxLibDay 25

数字だけ等幅で描画するライフハック

DxLib Advent Calendar 12月25日!

メリークリスマス!そしてハッピーバースデー私!Seiten Minagawaです。

はじめましての人向けに私が誰か簡単に説明しますと、Aquamarine Sky Projectという同人サークルの代表を務め、音ゲーとか作ってる変な青い生物です。

こういう記事を書く……というかQiita自体初投稿になりますが、私の誕生日にこの記事が公開されるようにしたくて、僭越ながら12月25日に割り込んでしまいました。すいません!

そしてDxLib Advent Calendarの最後を飾る記事がこんなのでいいのかという恐怖。


数字の文字幅、気にしてる?

ゲームで数字を描画する時、時間やスコアなど頻繁に動く数字があります。

この数字を描画する時、数字が等幅のフォントを使ってますか?

フォントによっては、この頻繁に動く数字がすごく見苦しくなってしまうんです。

検証動画 : https://www.youtube.com/watch?v=Q3FiCLu1N4Q

実はこの数字が等幅なフォントは少数派で、元から等幅なフォント以外で私が知る限りだとM+、Noto、Open Sans、Roboto(Androidでおなじみのフォントです)ぐらいしかありません。


数字だけ等幅で描画しよう!

フォントを改変するという手もあるんですが、フォントのライセンスによっては改変が禁じられていることがあります。

そこで! プログラム的に数字だけ疑似的に等幅化して描画してしまいます!

というわけで、サクッと書いてみました。

1文字の単位が可変(UTF-8とか)だと変なことになりますが、今回の目的は数字だけ等幅で書くことなのでこの辺りはあまり気にしてません。

※私が使う時はこの部分はクラス化しているんですが、通常の関数っぽく書き直しています。

void DrawStringToHandleNumberMonoSpaced(int x, int y, const std::string& str, unsigned int color, int fonthandle, int longwidth, int s)

{
std::vector<int> xlist = DrawPosListInt(x, str, s, longwidth);

//1文字ずつひたすら描画していく
for (size_t i = 0, is = str.length(); i < is; i++)
{
std::string character = str.substr(i, 1);

//xlistは中心座標になっているので文字幅の半分だけずらす
DrawStringToHandle(xlist[i] - GetDrawStringWidthToHandle(character.c_str(), 1, FontHandle) / 2, y, character.c_str(), color, fonthandle);
}
}

DrawPosListIntは、文字を描画するX座標のリストを取得します。longwidthで数字の幅を決めます。

JudgeNumber関数で1文字が数字かどうかを判定します。判定方法はお好みで。私はswitchで判定してます。

描画位置を示す値がマジックナンバーなのは本当はこの部分はenumにしてるけどenumを書いておく場所がなかったんだ!

std::vector<int> DrawPosListInt(int x, const std::string& str, int s, int fonthandle, int longwidth)

{
std::vector<int> res1;
int now = pos.x;
int firstcharlength = 0;

//1文字目を判定
if (JudgeNumber(str[0]))
{
//数字なら数字幅分足す
now += longwidth / 2;
firstcharlength = longwidth;
}
else
{
//それ以外の文字なら幅を足す
now += GetDrawStringWidthToHandle(str.substr(0, 1).c_str(), 1, fonthandle) / 2;
firstcharlength = GetDrawStringWidthToHandle(str.substr(0, 1).c_str(), 1, fonthandle);
}

//描画位置によって位置をずらす
switch (s)
{
case 1:
now -= firstcharlength / 2;
break;

case 2:
now -= firstcharlength;
break;
}

res1.push_back(now);

//LEFTだったとした時の1文字単位の描画中心位置を設定する
for (size_t i = 1, is = str.length(); i < is; i++)
{
int prevcharlength;

//前の文字の文字幅を取得
if (JudgeNumber(str[i - 1]))
{
prevcharlength = longwidth;
}
else
{
prevcharlength = GetDrawStringWidthToHandle(str.substr(i - 1, 1).c_str(), 1, fonthandle);
}

if (JudgeNumber(str[i]))
{
//数字なら数字幅分足す
now += longwidth;
}
else
{
//それ以外の文字なら幅を足す(微妙に左に寄るため前の文字の幅の半分を足して補正)
now += prevcharlength / 2 + GetDrawStringWidthToHandle(str.substr(i, 1).c_str(), 1, fonthandle);
}

//現在の文字を描画すべき位置を押し込む
res1.push_back(now);
}

float strlength = firstcharlength;

//文字列の幅を取得する
if (res1.size() > 1) strlength = res1[res1.size() - 1] - res1[0];
vector<int> res2;

//描画位置によって位置をずらす
switch (s)
{
case 0:
return res1;
break;

case 1:
transform(res1.begin(), res1.end(), back_inserter(res2), [&](int a) {return a - strlength / 2; });
break;

case 2:
transform(res1.begin(), res1.end(), back_inserter(res2), [&](int a) {return a - strlength; });
break;
}

return res2;
}


描画してみた。

https://www.youtube.com/watch?v=S3hVSrRmT3E

綺麗!!!!!!!!!!

「1」が若干右寄りなのは少し気になりますが、これで数字が頻繁に動いても見苦しくありません!


数字以外を含むと?

数字や数字によくついてくるような記号(「:」とか「.」とか「%」とか)を含むと妙なスペースが生まれてしまいますが、この辺りを気にするかは個人差があると思います。というかどうしたらいいのか何かアイデアあったらください(泣)

アルファベットを描画するとすごく汚いことになってしまいますが、数字だけ等幅で描画することが目的だからこまけぇこたぁいいんだよ!(よくない)


ここで書いたコードについて

Apache License 2.0にします。Apache License 2.0の範囲でお好きにどうぞ。

作者名は「Seiten Minagawa」でお願いします。


まとめ

数字を描画するフォントは慎重に選ぼう!!選択肢少ないけど

フォントを作る皆さん、数字だけは全部等幅にしてください!! 頻繁に動く数字を見苦しくないようにするのはこれだけ大変なんだ!!