cocos2d-x」タグアーカイブ

cocos2d-xでNSLocalizedStringのようなローカライズ

cocos2d-xで多言語対応させたい場合、iOSのNSLocalizedStringのようなクラスが欲しくなるのだけど、そうしたクラスは標準では用意されていないようだ。
そこで自前で用意する。

【cocos2dx】アプリをローカライズさせて海外にも売り込もう! | albatrus.com

こちらのブログを参考にさせて頂きました。
ありがとうございます。

ただ、ここに記述されたコードでは、ローカライズ対象の文章に ” を含めたい場合に、上手くいかない場合がある。
たとえば "ABC" のように、""で囲ったテキストを使いたい場合。
Localizable.stringsに "key" = ""ABC""; と記述したいところだけど、これだと取り出されるテキストは ABC となってしまう。
"key" = "value";のvalueの両端にある"はカットされてしまうのだ。

これでは都合が悪かったので、上記のコードを元に自前で作成してみたコードが以下。

CCLocalizedString.h

class CCLocalizedString
{
public:
    static void init();
    static const char *localizedString(const char* searchKey, const char* comment);
};


CCLocalizedString.cpp

#include "CCLocalizedString.h"
#include "cocos2d.h"
#include <map>

USING_NS_CC;
using namespace std;

static map<string, string> localizable;
static bool initialyzed = false;

void CCLocalizedString::init() {
    if(initialyzed) {
        return;
    }
    initialyzed = true;
    
    // 言語ディレクトリを指定する
    string file;
    ccLanguageType lang = CCApplication::sharedApplication()->getCurrentLanguage();
    switch (lang) {
        case kLanguageJapanese:
            file = "ja.lproj";
            break;
        default:
            file = "en.lproj";
            break;
    }
    file += "/Localizable.strings";

    // ファイルパス
    string fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename(file.c_str());
    
    // ファイルデータ取得
    unsigned long fileSize = 0;
    unsigned char * fileContents = CCFileUtils::sharedFileUtils()->getFileData( fullPath.c_str( ) , "rb", &fileSize );
    if (fileContents == NULL) {
        return;
    }
    
    string line, subStr;
    bool isComment = false;
    
    // 1行ずつ解析
    istringstream fileStringStream( (char*)fileContents );
    while ( getline( fileStringStream, line ) ) {
        // 先頭と末尾のスペースを除去
        line.erase(0, line.find_first_not_of(" \t"));
        line.erase(line.find_last_not_of(" \t") + 1);
        
        if (!isComment) {
            // //で始まる行はコメント
            subStr = line.substr(0, 2);
            if (subStr.compare("//") == 0) {
                continue;
            }
            
            // /* で始まる行は複数行コメント
            else if (subStr.compare("/*") == 0) {
                isComment = true;
            }
        }
        
        // 複数行コメント中
        string::size_type len = line.length();
        if (isComment) {
            // 末尾が */ で終わっているならコメント終了
            if (len >= 2) {
                subStr = line.substr(len - 2);
                if (subStr.compare("*/") == 0) {
                    isComment = false;
                }
            }
            continue;
        }
        
        // 最初の = でキーと値に分割する
        string::size_type pos = line.find_first_of('=', 0);
        if (pos != string::npos) {
            string keyStr = line.substr(0, pos);
            string subStr = line.substr(pos + 1, len - 1);
            
            // キーを囲む " を除去("の外側にあるスペース等も同時に除去)
            pos = keyStr.find_first_of("\"");
            if (pos != string::npos) {
                keyStr.erase(0, pos + 1);
            }
            pos = keyStr.find_last_of("\"");
            if (pos != string::npos) {
                keyStr.erase(pos);
            }
            
            // 値を囲む " を除去("の外側にあるスペースや;等も同時に除去)
            pos = subStr.find_first_of("\"");
            if (pos != string::npos) {
                subStr.erase(0, pos + 1);
            }
            pos = subStr.find_last_of("\"");
            if (pos != string::npos) {
                subStr.erase(pos);
            }
            
            // \nを改行コードに
            do {
                pos = subStr.find("\\n");
                if (pos != string::npos) {
                    subStr.replace(pos, 2, "\n");
                }
                else {
                    break;
                }
            }
            while (true);
            
            // \"を"に
            do {
                pos = subStr.find("\\\"");
                if (pos != string::npos) {
                    subStr.replace(pos, 2, "\"");
                }
                else {
                    break;
                }
            }
            while (true);
            
            localizable.insert(pair<string, string>(keyStr,subStr));
        }
    }
    
    delete [] fileContents;
    fileContents = NULL;
}

const char *CCLocalizedString::localizedString(const char* searchKey, const char* comment)
{
    init();
    
    map<string, string>::iterator itr = localizable.find(string(searchKey));
    if (itr != localizable.end()) {
        return (itr->second).c_str();
    }
    return comment;
}


使い方はalbatrus.comさんのコードと同じです。
仕様としては、

  1. Localizable.stringsを1行ずつパースします。
    1行ごとに"key" = "value";で記述してください。

  2. 各行の先頭と末尾のスペースはカットします。

  3. コメントアウトに対応しています。
    // で始まる行と、 /* で始まる行から */ で終わる行までを無視します。

  4. keyとvalueは、最初の = で分割します。
    なので、keyに=を含めることはできません。
    valueには = を含めても問題ありません。

  5. 末尾の ; は有っても無くても同じです。
    keyとvalueを""で囲む必要もありません。
    "key" = "value";(標準)も、"key" = "value"(末尾に;なし)も、key = value""で囲まない)もkey=value(=の前後にスペース無し)もすべて同じです。

  6. "はそのまま使えます。
    "key" = ""ABC"";"key" = "ABC"EFG";も問題ありません。

  7. "をそのまま使うのが気持ち悪い人は\"にも対応しています。
    "key" = "\"ABC\"";"key" = "ABC\"EFG";のように。

  8. 事前にパースしておきたいケースもあるだろうと、CCLocalizedString::init()を用意しました。
    明示的に使わない場合は、最初にCCLocalizedString::localizedStringを呼び出したときに自動で呼ばれます。

以上です。
見よう見まねで作ったので、なにか不備等あれば教えてください。

cocos2d-xのCCLabelTTFでテキストの自動改行

cocos2d-xのCCLabelTTFで、横幅を固定し、テキストをその横幅に応じて自動改行させ、なおかつ高さは明示的に指定せずに、テキスト量に応じて可変にしたい(自動的に最適な高さにしたい)場合。

CCLabelTTF::create(const char *string, const char *fontName, float fontSize, const CCSize& dimensions, CCTextAlignment hAlignment)

を使って、dimensionsに例えば CCSize(100.f, 0)といった具合に、高さ0を指定してやれば良い。

ただし、表示させるテキストが空の行(いわゆる”\n\n”)を含む場合、おそらく空行の分の高さが無視され、結果、テキストの末尾の1行分が表示されないという不具合がある(cocos2d-x 2.2.2)。
空行が2行分あれば、末尾2行分が切れるはずだ。

これを回避するには、”\n \n”のように、空行にスペースをひとつ入れてやると良い。
そうすれば、そこは空行ではなく「文字のある行」として、きちんと高さの計算が合うらしい。

ただし、iOS7の場合で、条件によってはこれでも末尾が切れる場合があるようだ。
原因を探ると、結局のところ、CCImageのdrawInRectに行き当たった。

ちなみに、CCLabelTTFで文字の縁取りを行うenableStrokeがiOS7で正常に機能しないという話があって、これもCCImageのdrawInRectが原因らしい。
というわけで、これを修正することで両方の不具合を解消できる。

CCLabelTTFのenableStrokeをiOS7に対応させる – Poppin Games Engineer Blog

こちらのブログにCCImageのdrawInRectをiOS7に対応させる方法が記載されているので、ありがたく使わせてもらいました。

CCImage.mm内に

// XXX: ios7 casting

というコメントがあるので、その下を書き換える。
OSの判別処理を含めて、こんな感じかな。

unsigned uHoriFlag = eAlign & 0x0f;
if ([[UIDevice currentDevice].systemVersion floatValue] < 7.f) {
    UITextAlignment align = 
        (UITextAlignment)((2 == uHoriFlag) ? UITextAlignmentRight : 
            (3 == uHoriFlag) ? UITextAlignmentCenter : UITextAlignmentLeft);
    [str drawInRect:CGRectMake(textOriginX, textOrigingY, textWidth, textHeight) withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:(NSTextAlignment)align];
}
else {
    NSMutableParagraphStyle *paragraphStyle = [[[NSMutableParagraphStyle alloc] init] autorelease];
    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
    paragraphStyle.alignment = 
        ((2 == uHoriFlag) ? NSTextAlignmentRight :
             (3 == uHoriFlag) ? NSTextAlignmentCenter : NSTextAlignmentLeft);
    UIColor *strokeColor = [UIColor colorWithRed:pInfo->strokeColorR green:pInfo->strokeColorG blue:pInfo->strokeColorB alpha:1.0];
    UIColor *fontColor = [UIColor colorWithRed:pInfo->tintColorR green:pInfo->tintColorG blue:pInfo->tintColorB alpha:1.0];
    NSDictionary *attribute = 
        @{NSFontAttributeName:font,
            NSParagraphStyleAttributeName:paragraphStyle,
            NSForegroundColorAttributeName:fontColor,
            NSStrokeColorAttributeName:strokeColor,
            NSStrokeWidthAttributeName:@(-pInfo->strokeSize * 4.f)
        };
    [str drawInRect:CGRectMake(textOriginX, textOrigingY, textWidth, textHeight) withAttributes:attribute];
}

alignはiOS6以前でしか使わないようなので、ここに持ってきてます。

あと、NSStrokeWidthAttributeName:@(-pInfo->strokeSize * 4.f)の部分ですが、Poppin Games Engineer Blogさんのコードでは *2 となっているところを *4にしています。
縁取りの太さなんですが、iOS6の場合と比べると2倍ではまだ太さが足らなくて、4倍でちょうどのように思えるので。
でも、なぜ4倍になるのかよく分からない。

2倍なら、たとえば縁取りが外側と内側で2倍になる、ということかな?と思えもするが。
retina解像度の2倍も掛かってくるのだろうか?

cocos2d-xでユーザーデータを暗号化して保存したい

cocos2d-xでプレイヤーのプレイデータや設定データなどを保存する際に、改竄されるとマズいデータを暗号化して保護したいというケースはよくあると思う。
一般的にcocos2d-xでユーザーデータを保存するならCCUserDefaultを使うと思うが、これにはデータを暗号化する機能はない。
なので自前で作っちゃおうというお話。

一番簡単なのは、CCUserDefaultに保存する値を暗号化する方法だろう。
ただし、CCUserDefaultはキーと値のペアでデータを保存するので、値だけ暗号化してもキーは丸見えになる。
暗号化してあるので値そのものを改竄することは容易ではないだろうが、値自体を削除してリセットすることは容易だろう。
内容を推測しにくいキーにするとか、削除されても困らないようなデータ設計にするとか、そうした工夫が必要になる。

今回は、そうした値だけを暗号化する方法ではなく、データ全体を暗号化する方法を試みた。
CCUserDefaultには当然データ全体を暗号化する術が無いので、ユーザーデータを保存するための独自のクラスを作成する必要が有る。

処理としては、
CCDictionary → 文字列化 → 暗号化 → ファイルに保存。
ファイルから文字列取得 → 復号化 → CCDictionaryに変換。
となる。
言わずもがな、このCCDictionaryがユーザーデータ。

CCDictionaryと文字列との変換には、JSONを使った。
以下のブログにそのものズバリなコードが載っているので、大いに参考にさせて頂きました。
ありがとうございます。

Cocos2d-xでJSONをCCDictionaryに変換する – 銀の人のメモ帳
cocos2d-x – CCObjectからJSONを作成する – Qiita [キータ]

暗号化については、AESなどの本格的なやつを使おうとも思ったのだけど、導入がなかなか面倒臭そうだったのでやめた。
要はデータの改竄を防止できれば良いので、それも100%完全に防止しなければならないほどのものでもないので、簡易的な暗号化でいいやということで、XORを使った暗号化で良しとした。

const std::string Encription::xorEncode(std::string text, std::string key) {
    std::string output = text;
    const unsigned int n = text.size();
    const unsigned int keyN = key.size();
    unsigned int keyIndex = 0;
    for (unsigned int i = 0; i < n; i++) {
        output[i] ^= key[keyIndex];
        keyIndex = (keyIndex + 1) % keyN;
    }
    return output;
}

こんな感じでいいのではないかと思う。

// 暗号化
std::string encrypted = xorEncode(jsonString, KEY);

// 復号化
std::string decrypted = xorEncode(encrypted, KEY);

のように使う。
KEYは暗号鍵となる任意の文字列で。

残るはファイルへの読み書きだ。

以下のコードでは、ファイルの読み書き時にデータが破損するケースがあるようだ。
おそらく、暗号化によって文字データが特殊な値に変換された場合、それを”文字”として読み書きしようとして破損するのだろうと推測する。
元のコードの下に、修正したコードを記述するのでそちらを参考にどうぞ。

ファイルからの読み込みはCCFileUtilsを使って、こんな感じ。

// ファイル読み込み
unsigned long nSize;
const unsigned char* buffer = 
CCFileUtils::sharedFileUtils()->getFileData(filepath.c_str(), "rb", &nSize);

// 成功したら復号化
if (buffer != NULL) {
    const std::string decrypted = xorEncode((const char*)buffer, KEY);
}

ファイルへの保存はfopenを使って

// 暗号化
const std::string encrypted = xorEncode(jsonString, KEY);

// ファイルに保存
FILE *outputfile = fopen(filepath.c_str(), "w");
if (outputfile != NULL) {
    fputs(encrypted.c_str(), outputfile);
    fclose(outputfile);
}

以下、修正したコード。

ファイルの読み込み。

FILE *fp = fopen(filepath.c_str(), "rb");
if (fp != NULL) {
    std::string str = "";
    do {
        const int c = getc(fp);
        if (c == EOF) {
            break;
        }
        else {
            str += (char)c;
        }
    }
    while (true);
 
    //  復号化           
    const std::string decrypted = xorEncode(str, KEY);
}

ファイルへの保存。

// 暗号化
const std::string encrypted = xorEncode(jsonString, KEY);

FILE *outputfile = fopen(filepath.c_str(), "wb");
if (outputfile != NULL) {
    const unsigned int n = encrypted.size();
    for (unsigned int i = 0; i < n; i++) {
        putc(encrypted[i], outputfile);
    }
    fclose(outputfile);
}

読み書きの際に、1文字ずつ明示的にバイトコードとして扱うように修正した。
これでたぶん大丈夫だと思うけど、どうかな。