aburi6800のブログ

コンピュータのプログラミング、ゲームに関するニッチな情報を書いていくブログです。

【Python】Pyxelでカナ文字を表示したい

 さて今回は、Pythonゲームエンジン「Pyxel」を使ったカナ文字表示処理を作った記事です。
 ちょっと長文になりますが、作成の過程を赤裸々に書いていますので、何かの参考になれば幸いです。
 
 なお、ここではPythonやPyxelのインストール等の細かい説明は割愛します。
 Pyxelについては、公開されているGitHubリポジトリにあるREADMEを参照してください。
(今後も更新される可能性があるため、他のサイトの記事よりも、こちらを参照することを推奨します)

github.com

事の経緯

 さて、Pyxelでゲームを作り始めたところ、標準のAPIでは英文字(大文字・小文字)と数字、記号の表示はできますが、カタカナ・ひらがなの表示ができませんでした。
 メッセージが重要でないゲームであればこのままで良いのですが、今作っているのはRPGで、メッセージを読ませるためにカナ文字を表示したいのです。
 そこで、自前でカナ文字の表示を実現しよう、というのが始まりでした。

まずは文字画像の素材から

 一言に「カナ文字を表示」と言っても、プログラムとしてやることは「文字の形に作った画像を表示する」です。
 普通のキャラクターの表示と変わりません。
 なので、まずは文字パターンの画像データを作りました。

f:id:aburi6800:20200613132801p:plain
ひらがな・カタカナの画像データ
 Pyxelの英数字は、3x6ドットと縮小されたものですので、これと合わせたときに違和感がないように、7x6ドットで書いています。
(さすがにこれ以上小さくするのは無理でした・・・(^^;)
 これを、一般的なドットエディタで256x256ドットのpngで保存して、Pyxelに同梱されているPyxelEditorに先ほど作った画像を取り込んでリソースデータにします。

最初の実装

 次に、プログラムで表示していきます。
 まずは、どのように実装するかですが、以下のnote記事を参考にさせていただきました。

note.com

 簡単に説明すると、Pythonの辞書型に、「あ」だったら「A」のようにキーとする文字と、文字パターン画像データの対応する座標を定義していくものです。
 具体的には、以下のようなコードになります。
 キーは、ひらがなは大文字(「A」、「SA」など)、カタカナは小文字(「a」、「sa」など)としました。
 長すぎるので一部抜粋していますが、これをすべての文字に対して定義します。

# -*- coding: utf-8 -*-
import pyxel

class PyxelUtil:

    KANA_DIC = {
        "A"  : [  0,   1],
        "I"  : [  8,   1],
        "U"  : [ 16,   1],
        "E"  : [ 24,   1],
        "O"  : [ 32,   1],
    :
    :
(以下、定義が続く)

 
 これを実際に表示する処理として、以下のようなコードを書きました。

    @staticmethod
    def text(x, y, txt, color=7):  

        for i in range(len(txt)):
            if txt[i][0] == "*":
                t = txt[i].replace("*", "")
                pyxel.text(x, y, t, color)
                x = x + 4 * len(t)
            
            else:
                font_xy = PyxelUtil.KANA_DIC[txt[i]]
                fontx = font_xy[0]
                fonty = font_xy[1]
                pyxel.pal(7, color)
                pyxel.blt(x, y - 1, 0, fontx, fonty, 7, 6, 0)

 
 Pyxelのtextメソッドと同じ名前にして、引数も同じ構成にしています。
 ただし、引数の「txt」はリストの形としており、カナ文字の場合は辞書のインデックスを指定します。
 英数字を含んだメッセージを表示したかったので、先頭に「*」を付けた要素はPyxelのtextメソッドでそのまま表示するようにしました。
 
 これを使って実際に文字表示をするコードは、次のようになります。

PyxelUtil.text( 16,  16, ["A", "I", "U", "E", "O", "*ABCDE12345"], 7)

問題その①

 さて、ここまでで想定した通りに文字が表示ができるようになりました。
 しかし、この処理構成には、大きな問題があります。
 「Pyxelのイメージバンクの一部を文字パターン画像データで消費している」という問題です。
 このため、他のプログラムでは流用しにくいものになっているのです。
 例えば、イメージバンクのすべてを使い切ったゲームの場合は、この文字パターン画像データをイメージバンクに入れることができないので、カナ文字を表示できないことになります。
 
 そこで、文字パターンデータをプログラムに直接持とう、と考えました。
 そこでまず、ドットのあるところは1、ないところは0として、次にように全てデータ化しました。(大変でした・・・)

    KANA_DIC = {
        "A"  : ["0010000", "0111100", "0010001", "1111110", "1010101", "0111001"],
        "I"  : ["0000000", "1000010", "1000001", "1010001", "0110001", "0010000"],
        "U"  : ["0011100", "0000000", "0111110", "1000001", "0000001", "0011110"],
        "E"  : ["0011110", "0000000", "1111111", "0000110", "0111000", "1100111"],
        "O"  : ["0001001", "0111101", "0001000", "0111110", "1001001", "0111001"],
    :
    :
(以下、定義が続く)

 
 次に表示ですが、システム用イメージバンクの表示スクリーン用に直接描いてはどうだろうか?、と考えました。
 そこで、以下のようにしました。

    @staticmethod
    def text(x, y, txt, color=7):  

        for idx in range(len(txt)):
            if txt[i][0] == "*":
                t = txt[i].replace("*", "")
                pyxel.text(x, y, t, color)
                x = x + 4 * len(t)
            
            else:
                data = copy.deepcopy(PyxelUtil.KANA_DIC[txt[idx]])
                for i in range(len(data)):
                    data[i] = data[i].replace("1", "{:1x}".format(color))                    
                pyxel.image(4, system = True).set(x, y, data)
                x = x + 8

 
 else以降が修正した部分です。
 ビットパターンに色を指定するために一時的に辞書のデータを書き換えるので、copyモジュールをimportしてdeepcopyしました。
(単純にdata = PyxelUtil.KANA_DIC[txt[idx]]とすると、元の辞書データも変更されてしまいました。Pythonのリストは参照渡しのようです。)  これをsetメソッドで、表示スクリーン用イメージに直接描画しています。
 
 これで試したところ、画面に直接、プログラムで定義したパターンが描画することができました。
(システムリソースにアクセスするための引数の書き方を、最初はimage(4, True)としていて動かず、しばらく悩みましたが・・・)
 
 ようやく汎用的な文字表示クラスが作れた!
 これでどんなゲームでもカナ文字が表示できる!
 と、喜んでいたのですが・・・

問題その②

 先の対応で、Pyxelのリソースデータを消費せずに、カナ文字を含めた文字表示ができるようになりました。
 しかし、色々試していたところ、Pyxelのtextメソッドとは描画される挙動がちょっと違っていたのです。
 
 たとえば、pyxel.textを使って文字を重ねて表示すると、次のような結果になります。

f:id:aburi6800:20200613122835p:plain
pyxelのtextメソッドで重ねて表示
 次に、今回作成したtextメソッドで文字を重ねて表示すると、次のような結果になりました。
f:id:aburi6800:20200613123145p:plain
今回作成したtextメソッドで重ねて表示
 
 このように、「背景色の透過がされていない」んですね。
 それもそのはず、データ上「0」とした部分は、透明ではなく黒ですからね・・・。
 この黒の部分も含めてすべてのデータを表示スクリーン用イメージに描いているのですから、透過されるわけがありません。
 Pyxelのbltメソッドで透過表示できるのは、恐らく内部的には、イメージバンクから表示スクリーン用イメージにコピーするときに指定された色を描画しないようにしているのだと考えます。
 
 であれば、データで「0」とした部分を描かず、「1」となっている部分に対して、指定された色でドットを打てば良いですね。
 Pyxelではpsetメソッドが用意されていますので、これを使います。
 psetメソッドは表示スクリーン用イメージに直接描画するようですので、何も考えずに以下のようなコードとしました。

    @staticmethod
    def text(x, y, txt, color=7):  

        for idx in range(len(txt)):
            if txt[idx][0] == "*":
                t = txt[idx][1:len(txt[idx])]
                pyxel.text(x, y + 1, t, color)
                x = x + 4 * len(t)
            
            else:
                try:
                    for row, data in enumerate(PyxelUtil.KANA_DIC[txt[idx]]):
                        for col in range(len(data)):
                            if data[col] == "1":
                                pyxel.pset(x + col, y + row, color)
                    x = x + 8
                except KeyError:
                    x = x + 8
                    continue

 
 for文を2重にし、外側は辞書の各文字に定義したビットパターンの配列に対してのループ、内側は各配列要素に対してのループになります。
 要するに、全部のビットパターンを精査して、「1」だったら指定された色でpsetする、という処理です。

 tryexceptで括ったのは、辞書に存在しないキーが指定されたときにエラーで止まらず空白を開けるようにするためです。(元から潜在していた不具合ですね・・・)
 また、単純に「*」をreplaceしていたため、文字としての「*」が表示できない不具合がありましたので、5~6行目も修正しています。
 
 では、実行してみます。

f:id:aburi6800:20200613130233p:plain
修正したtextメソッドで重ねて表示
 
 おおっ!無事に、カナ文字も重ねて表示できるようになりました!!
 処理速度的にも問題なさそうです。

最後に

 実際にわかってしまえば「なんでこんな処理に苦労したんだ?」となるような処理ですが、ここに辿り着くまでに苦労したんですよ・・・。
 おかげで、だいぶPythonもPyxelもわかってきました。
 
 なお、今回作ったモジュールは、以下のGithubリポジトリに公開しています。

github.com

 cloneしたらpyxelUtil.pyをコピーしてきて、importしたらそのまま使えます。
 使い方は、pyxelUtil.pyを実行するか、プログラムソースの後半を見ればすぐにわかるかと思います。
 また、必要最低限のパターンしか定義していませんが、記号などの追加も簡単にできるように、単純なソースにしています。
 ご自由にお使いいただければと思います。