変愚蛮怒をXft対応する話 Part1 X11における変愚蛮怒の描画コード

変愚蛮怒というRoguelikeゲームはLinuxでも動作するけれど, TrueTypeフォントが扱えなくてフォント指定がかなりだるいし, レンダリングがびみょう. 変愚蛮怒の描画をXftで行うようにした. フォントがきれいになって, お好きなフォントを使えて超ハッピー. この経験をふまえて, libX11のものをXft対応ってどうやったらいいのかを書く.

リポジトリはここ

github.com

変愚蛮怒ってなに

変愚蛮怒はMoria/Angbandから始まる*band系ローグライクゲームのバリアント(変種)の一種です。直接にはZangbandから派生しています。
鉄獄100Fに潜むラストボス『混沌のサーペント』を撃破して『*勝利*』を遂げるためには、キャラクターのレベルや装備だけでなく、*あなた*自身の習熟が求められます。

変愚蛮怒 公式WEB

まあ日本で派生したこんな感じなローグライクゲームで, 日本で派生しているので斬鉄剣持ったサムライっぽい人とか, 光の剣をはなつ不老不死にして史上最強のエスパーとか, 石仮面を持った吸血鬼とか出てくる. 主人公の方も剣術家になって天翔龍閃したり, 鏡使いになってラフノールの鏡したりできる.

https://pbs.twimg.com/media/DBjx6C6VoAA231H.jpg:large

最近だと東方成分の入ったバリアントもあるみたい. 当方東方ネタはあんまわからんのだけど, わかる人はよさそう.

www65.atwiki.jp

Linuxにおける変愚蛮怒

変愚蛮怒Linuxでも動いてとてもサイコー. ただコードが昔に書かれたものであるため, 現状とあわない点が様々ある

  • 内部コードがEUC (これは簡単に文字幅とか計算するのにはしかたなかったり)
    • 表示もこの前までEUCでしようとしていたがpatchが当たった: チケット #25917: UTF-8サポート - 変愚蛮怒 - OSDN
      • これもまだ, UTF-8読んでEUCにして, またUTF-8に戻して表示しててなんかアレなんだけどまあ既存ルーチンいじらないといけないしな〜
      • あと, UTF-8のCファイルをgcc-wrapというスクリプトで, EUCにconvertしてからcompileしていてまたむずかしい. atomicではないからCtrl-cするとめっちゃdiffが出たりする
  • TrueTypeフォントがうまくあつかえない
    • フォント指定には昔ながらの, XLFD (-*-*-medium-r-normal--24-*-*-*-*-*-iso8859-1ってやつ)を使う
      • めちゃ複雑わからん
    • mkfontdir, mkfontscaleなど使うとTTFも使えるらしい?んだけどうまく動かんわ
      • 昔動いてたんだけど…もうきえた?
    • bitmapフォントは…見た目やっぱアレ

現在のX11用描画コード

ということで, 変愚蛮怒をXftというTrueTypeフォントを扱えるライブラリで描画するように書き変えよう, という話.

今回は現在の描画コードについて整理する.

変愚蛮怒WindowsやらX11やらCursesやら様々な環境に対応できるように, 描画対象をTermと抽象化している. 各環境では以下のhookを実装して, 描画を行う.

いろいろhookはあるけれど, 主に仕事をするのはtext_hookになる. X11での実装は以下の通り:

(x, y)の位置に, 属性番号aで, nバイト長の文字列sを描画します, ということですね. Infoclr_set(clr[a]);で描画色を設定して, Infofnt_text_std(x, y, s, n);でテキスト描画している感じ. ここの(x, y)は文字単位での座標. ピクセル単位での座標へはInfofnt_text_std()内で変換する.

Infofnt_text_std()は以下のあたりから. ifdefとかが多いので抜粋する.

https://github.com/naota/hengband/blob/master/src/main-x11.c#L1605

static errr Infofnt_text_std(int x, int y, cptr str, int len)
{
    /*** Decide where to place the string, vertically ***/

    /* Ignore Vertical Justifications */
    y = (y * Infofnt->hgt) + Infofnt->asc + Infowin->oy;

    /*** Decide where to place the string, horizontally ***/

    /* Line up with x at left edge of column 'x' */
    x = (x * Infofnt->wid) + Infowin->ox;

    /*** Actually draw 'str' onto the infowin ***/
...
    /*** Handle the fake mono we can enforce on fonts ***/

    /* Monotize the font */
    if (Infofnt->mono)
...
    /* Assume monoospaced font */
    else
    {
...
        XmbDrawImageString(Metadpy->dpy, Infowin->win, Infofnt->info, Infoclr->gc, x, y, kanji, kp-kanji);
        free(kanji);
    }

    /* Success */
    return (0);
}

まず, 引数の文字単位の座標である(x, y)ピクセル単位に変換して, 描画位置を決める. フォントの高さ, 幅はどのフォントでも一定(monospaceフォント)と仮定しているので, 基本的にはx, yにそれぞれフォントの幅とフォントの高さを掛ければよい. これに描画の開始座標の(Infowin->ox, Infowin->oy)を足す. また, Y座標にはフォントのアセント(フォントのベースラインから上端までの高さ)Infofnt->ascを足す. これで, (x * フォントの幅 + ox, y * フォントの高さ + oy)ピクセルに文字の左上が当たるように描画されることになる.

あとはkanjiUTF-8が入っているので, そいつをXmbDrawImageString()で描画する. XサーバがMetadpy->dpy(XのDISPLAYのやつ)で, 描画先がInfowin->winになる. フォントがInfofnt->infoで指定され, 色がInfoclr->gcで指定される.

Xft化することを考えると, Infowin, Infofnt, Infoclrの初期化とかを見ておくとべんりそう.

(しかし, #define DPY (Metadpy->dpy)とかしてるのに全然使ってなくてあんまりコードの治安はよくない)

ウィンドウの管理: Infowin

struct infowin Infowinは以下のような変数. ウィンドウの管理をする.

struct infowin
{
    Window win;
#ifdef USE_XIM
    XIC xic;
    long xic_mask;
#endif
...
};

InfowinのデータはInfowin_prepare()で初期化されている.

Xft化する時に, ウィンドウ関係でなにか追加でほしい時は, 上の構造体に変数増やして, Infowin_prepare()で初期化したらいいかな, という感じ.

フォントの管理: Infofnt

struct infofnt Infofntは以下のような変数. フォントの管理をする.

struct infofnt
{
    XFontSet info;

    cptr name;

    s16b wid;
    s16b twid;
    s16b hgt;
    s16b asc;
...
};

ここに, フォント, その幅・高さ・アセントなどが保持されている. 初期化はInfofnt_prepare()で行う.

static errr Infofnt_prepare(XFontSet info)
{
    infofnt *ifnt = Infofnt;

    XCharStruct *cs;
    XFontStruct **fontinfo;
    char **fontname;
    int n_fonts;
    int ascent, descent, width;

    /* Assign the struct */
    ifnt->info = info;

    n_fonts = XFontsOfFontSet(info, &fontinfo, &fontname);

    ascent = descent = width = 0;
    while(n_fonts-- > 0){
        cs = &((*fontinfo)->max_bounds);
        if(ascent < (*fontinfo)->ascent) ascent = (*fontinfo)->ascent;
        if(descent < (*fontinfo)->descent) descent = (*fontinfo)->descent;
        if(((*fontinfo)->max_byte1) > 0){
            /* 多バイト文字の場合は幅半分(端数切り上げ)で評価する */
            if(width < (cs->width+1)/2) width = (cs->width+1)/2;
        }else{
            if(width < cs->width) width = cs->width;
        }
        fontinfo++;
        fontname++;
    }
    ifnt->asc = ascent;
    ifnt->hgt = ascent + descent;
    ifnt->wid = width;
...
    /* Success */
    return (0);
}

XFontsOfFontSet()でフォント一覧をとってきて, そのリストをなめて, 最大のアセント・デセントを求める. 幅は多バイト文字なら, フォント幅の半分と計算していて, 多バイト文字の幅が単一バイト文字の幅の2倍と仮定しているとわかる.

Xftでも, このへんでアセントとかを計算したらいいね, ということになる.

色の管理: Infoclr

Infoclrは以下の通り. GCがXlibのグラフィックコンテキストで, 描画色・背景色などを保持してくれる. ついでに, fg, bgでも描画色・背景色を保持しているけど, べつに使っていない.

struct infoclr
{
    GC gc;

    Pixell fg;
    Pixell bg
...
};

これまでの予想に反して, Infoclrのデータの初期化はinit_x11()の中で行われている.

errr init_x11(int argc, char *argv[])
{
...
    /* Prepare normal colors */
    for (i = 0; i < 256; ++i)
    {
        Pixell pixel;

        MAKE(clr[i], infoclr);

        Infoclr_set(clr[i]);

        /* Acquire Angband colors */
        color_table[i][0] = angband_color_table[i][0];
        color_table[i][1] = angband_color_table[i][1];
        color_table[i][2] = angband_color_table[i][2];
        color_table[i][3] = angband_color_table[i][3];

        /* Default to monochrome */
        pixel = ((i == 0) ? Metadpy->bg : Metadpy->fg);

        /* Handle color */
        if (Metadpy->color)
        {
            /* Create pixel */
            pixel = create_pixel(Metadpy->dpy,
                         color_table[i][1],
                         color_table[i][2],
                         color_table[i][3]);
        }

        /* Initialize the color */
        Infoclr_init_ppn(pixel, Metadpy->bg, "cpy", 0);
    }

angband_color_table[]にARGBが入っている. create_pixelPixell(X11の場合, XColorのtypedef)を作り, その色を描画色としてGCを設定する. Infoclr_set(clr[i])Infoclr = clr[i]となって, Infoclr_init_ppn()で色が設定される. 結局,のところ配列clr[]に各色でGCが格納される.

(ここで256個のGC作っているけど, 実際には8色しかない. alpha == 0xFFあたりでbreakしちゃってもいい気もする)

まとめ

Infowin, Infofnt, Infoclrがそれぞれ現在のウィンドウ・フォント・描画色を管理する.

InfowinInfowin_prepare()で, InfofntInfofnt_prepare()で, Infoclr(clr[])はinit_x11()で初期化される.

Term_text_x11()が指定位置にテキストを描画する. Infoclr_set(clr[a]);で色を変えて, Infofnt_text_std(x, y, s, n);でテキスト描画する. 実際に描画しているのは, XmbDrawImageString().

次回はXmbDrawImageString()をXftのテキスト描画関数に置き換える. それにあたって, 描画先・フォント・色の構造体が変わるので必要な処理を初期化関数に追加するなどについて書く.