この記事は東京理科大学アドベントカレンダーの7日目の記事です。
#前書き
どうもこんにちは。東京理科大学2017年卒の「やぶからぼう」と申します。少人数のベンチャー企業でスマホゲームを開発する仕事をしています。月日がたつのは早いもので、大学を卒業してからもう2年が過ぎようとしています。このアドベントカレンダーももう3年目になるのでしょうか。すべて埋まっていて盛り上がっているようで嬉しいですね。若輩ながら今年も記事を書かせて頂きたいと思います。ちなみにトップの画像は私ではありません。全然知らない男性です。ネットから拾ってきたフリー素材ですね。
さて表題についてですが、私は普段業務でcocos2dxというC++のオープンソースのフレームワークでゲームを開発しています。そんななか、UIを作っているときに、文字の位置が想定とずれるという問題に遭遇しました。詳細を調査した結果、ヒラギノフォントを使用したときだけccLabelの_contentSizeの大きさが文字の見た目に比べて大きくなるという問題が存在することがわかりました。この記事ではこの問題を回避する方法を紹介します。
検証
まず実際にどのようになるのか適当にコードを書いて、_contentSizeを可視化してみます。
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
//////////////////////////////
// 1. super init first
if ( !Scene::init() )
{
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
LayerColor *bgLayer = LayerColor::create(Color4B::WHITE);
this->addChild(bgLayer,-1);
const Vec2 centerPoint { origin+visibleSize*.5f };
{
Label* testLabel = this->createLabel("SanFranciscoDisplay","SanFranciscoDisplay-Black");
testLabel->setPosition(centerPoint + Vec2(0,visibleSize.height*.3f));
this->addChild(testLabel);
}
{
Label* testLabel = this->createLabel("HiraginoSans","HiraginoSans-W6");
testLabel->setPosition(centerPoint+ Vec2(0,visibleSize.height*.1f));
this->addChild(testLabel);
}
{
Label* testLabel = this->createLabel("HiraginoSans","HiraginoSans-W3");
testLabel->setPosition(centerPoint + Vec2(0,-visibleSize.height*.1f));
this->addChild(testLabel);
}
{
Label* testLabel = this->createTTFLabel("DroidSans","fonts/DroidSans.ttf");
testLabel->setPosition(centerPoint + Vec2(0,-visibleSize.height*.3f));
this->addChild(testLabel);
}
return true;
}
Label* HelloWorld::createLabel(std::string text,string fontName) const
{
auto visibleSize = Director::getInstance()->getVisibleSize();
Label* testLabel = Label::createWithSystemFont(text,fontName,12);
testLabel->setTextColor(Color4B::BLACK);
{
Node* target { testLabel };
const string name { "debug" };
Color3B color { Color3B::RED };
Node* debug { target->getChildByName(name) };
if (!debug) debug = LayerColor::create(Color4B(color));
debug->setName(name);
debug->setOpacity(50);
debug->setContentSize(target->getContentSize());
debug->setPosition(Vec2(0,0));
DrawNode* rect { dynamic_cast<DrawNode*>(debug->getChildByName(name)) };
if (!rect) rect = DrawNode::create();
rect->setName(name);
rect->clear();
rect->drawRect(Vec2::ZERO, debug->getContentSize(), Color4F(color));
if (!rect->getParent()) debug->addChild(rect);
if (!debug->getParent()) target->addChild(debug, -1);
debug->setCameraMask(target->getCameraMask());
}
return testLabel;
}
Label* HelloWorld::createTTFLabel(std::string text,std::string fontName) const
{
auto visibleSize = Director::getInstance()->getVisibleSize();
Label* testLabel = Label::createWithTTF(text,fontName,12);
testLabel->setTextColor(Color4B::BLACK);
{
Node* target { testLabel };
const string name { "debug" };
Color3B color { Color3B::RED };
Node* debug { target->getChildByName(name) };
if (!debug) debug = LayerColor::create(Color4B(color));
debug->setName(name);
debug->setOpacity(50);
debug->setContentSize(target->getContentSize());
debug->setPosition(Vec2(0,0));
DrawNode* rect { dynamic_cast<DrawNode*>(debug->getChildByName(name)) };
if (!rect) rect = DrawNode::create();
rect->setName(name);
rect->clear();
rect->drawRect(Vec2::ZERO, debug->getContentSize(), Color4F(color));
if (!rect->getParent()) debug->addChild(rect);
if (!debug->getParent()) target->addChild(debug, -1);
debug->setCameraMask(target->getCameraMask());
}
return testLabel;
}
DroidSansはシステムフォントではないので、挙動が異なる様子ですが比較対象として表示してみました。ご覧の通り、ヒラギノフォントだけ_contentSizeの高さが下に長いのがわかります。なぜこのようなことになるのか確認できていませんが,どうやら文字のtextureのサイズがおかしいようです。これがバグなのか仕様なのかはわかりませんが、文字の位置を指定するときに不都合があるのでこれを回避したいと思います。
#方針
文字の行数を計算したあとに、行間の幅とフォントサイズから高さを導出します。
###文字の行数を計算する
ccLabelに行数を取得するっぽいgetStringNumLinesというメソッドが存在しますが、これは役に立ちません。検証した結果、このメソッドは単純に文字列に含まれる改行コード(\n)を返すようです。なので、ccLabelの表示領域をsetDimensionsで指定した場合、全然検討違いの値が返ってきます。なので、頑張って行数を計算するメソッドを作る必要があります。いろいろと実験した結果、_contentSize.height = fontSize * 1.5 * 行数
という関係性を発見しました。この1.5という数値がどのような理由でこの値になったのかは不明ですが、とにかくこの関係性から行数を計算できそうです。
###行数から文字の高さを計算する
fontSizeとは具体的には文字の高さを意味します(たぶん)。なので文字の高さ=fontSize*行数
で求めることができるはずです。
結論
ということで_contentSizeを補正するCorrectionSizeLabelクラスは以下のようになります。
CorrectionSizeLabel* CorrectionSizeLabel::create(const std::string& text, const std::string& font, float fontSize)
{
CorrectionSizeLabel * ret = new (std::nothrow) CorrectionSizeLabel();
if (ret && ret->init(text,font,fontSize))
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
bool CorrectionSizeLabel::init(const std::string& text, const std::string& font, float fontSize)
{
this->setSystemFontName(font);
this->setSystemFontSize(fontSize);
this->setString(text);
return true;
}
void CorrectionSizeLabel::updateContent()
{
Label::updateContent();
const bool isHiraginoFont = this->getSystemFontName()=="HiraginoSans-W3"||this->getSystemFontName()=="HiraginoSans-W6";
if (isHiraginoFont)
{
// 行数とfontSize(≒高さ)から_contentSizeを補正する
Size originSize = Node::getContentSize();
const int numberOfLine = this->calcLine();
const float fontSize = this->getSystemFontSize();
this->correctionSize = Size(originSize.width,numberOfLine*fontSize);
// サイズを補正した分、位置がずれるので調整する
_textSprite->setPosition(Size(0,this->correctionSize.height-originSize.height));
}
else
{
this->correctionSize = Node::getContentSize();
}
}
const Size& CorrectionSizeLabel::getContentSize() const
{
Label::getContentSize();
return this->correctionSize;
}
/// _contentSizeとfontSizeから行数を計算する
int CorrectionSizeLabel::calcLine() const
{
Size originSize = Node::getContentSize();
const float ratio =1.5f; // この1.5という数値は計測した結果の数値。なぜ1.5なのかは不明
const float fontSize = this->getSystemFontSize();
const float oneLineHeight = fontSize*ratio;
const int numberOfLine { static_cast<int>(ceil(originSize.height / oneLineHeight)) };
return numberOfLine;
}
実行してみると...
これでうまくいきました。ただしみてわかる通り、"g","y"といった文字の下部分が少しはみ出てしまうので完璧とは言えませんね。でもまあ許容範囲なのでないでしょうか(適当)。
#あとがき
はい。ということでかなりニッチな問題の回避方法を紹介しました。ただかなり無理やりな解決方法ですね。本来であれば問題の原因をちゃんと特定して修正するべきです。なのであまり推奨できるようなものではないと思います。今回記事を書くにあたって普段の業務で遭遇した問題について書いてみました。ですがかなりニッチな問題で需要が少ないように感じたので、よくネットの記事でみる人が喋ってるみたいな形式で記事を書いてみました。とってつけたような感じでちょっと微妙かもですね。でもまあ普通に記事を書くよりはちょっと面白みが増しているのではないでしょうか?そうでもないです?
#謝辞
今回記事を書くにあたって、ぱくたそ様の素材を利用させていただきました。ありがとうございました。