見出し画像

完成する前に作り直したくなる

Everquest熱がちょっと落ち着いてきたので、またWizardryにとりかかろうと思う。前も書いた気がするが、Wizardryをつくっているのはプログラミングの素人である私のPythonの勉強のためで、Pythonにかかわらない部分はあまり重要ではない。とは言え、自分にとっては思い入れのあるゲームであるためシステム部分に関してはちゃんとつくりたいとは思っている。逆に言えば、性能をあげたり、消費メモリ量をなるべく小さくするようなチューンにはこだわりがない。さすがにただの8bitのゲームに数GBもメモリを食うようなことはしたくないが。
今日は今回ゲームを作ってみて悩んでいるところを書いてみる。ちなみに細かいし、逆に詳しい人が見れば鼻で笑ってしまう内容だ。

Pyxelはアクションゲームがちゃんと遊べる作りになっているため、ゲームパッドやキーボード、マウスなどの入力の処理と、描画の処理が別れている。シューティングゲームみたいに、オープニング画面とゲーム中とエンディング画面くらいしかないと、これで問題ないのだが、Wizardryのような細かく場面がわかれてその度に操作が変わるゲームだと処理をわけるとすごく煩雑になる。なにを言っているかわからないと思うので、ちょっと実例をあげようと思う。
以下はApple版のWizardryのソースコードの抜粋だ。オリジナルのWizardryはPascalという言語で書かれている。ただ、これはたぶんオリジナルのソースコードではなくて、バイナリを逆アセンブルしてPascalに変換したか、直でPascalに変換するソフトを使ったものを人間が手を使って見やすくしたものだと思う。で、これはキャンプ時でビショップがアイテムを鑑定する部分のルーチンだ。

私はPascalは大学のときにちょっと勉強したくらいなのでよくわからんが、さすがに教育用の言語だけあってコードの内容はわかりやすい。流れを書くと、

  1. 最初のREPEATからUNTILの部分で、どのスロットにあるアイテムを鑑定する?と表示しキーボードの入力を取得する。入力が0ならキャラクター画面に戻り、他の数字であれば次にうつる

  2. UNTILの次のIF文で、得られたスロット番号のアイテムがすでに鑑定されているならキャラクター画面に戻る

  3. 次のIF文で、1d100のサイコロを振り、自分のレベルx5+10より下になれば鑑定成功となり、そのスロット番号のアイテムに鑑定済みフラグをたてる

  4. 鑑定成功したなら、成功した!と表示する

  5. 鑑定失敗したなら、失敗した!と表示する

  6. 1d100のサイコロを振り、35-自分のレベルx3より下になった場合、そのアイテムを装備してしまう。そのアイテムが呪われている場合、呪いも有効となる

  7. キャラクター画面に戻る

といった感じだ。この1の部分で、所持アイテム数の最大の8以下を押すことを暗に求められているが、ここで9を指定すると、3の部分で、全然関係ない(たぶん自身の経験値)変数に1が書き込まれ、すさまじい経験値が設定されてしまうバグ(スーパービショップ)がある。これは、

UNTIL (ITEMX > 0) OR (ITEMX <= CHARACTR[ CHARX].POSS.POSSCNT);

この部分が、

UNTIL (ITEMX >= 0) AND (ITEMX <= CHARACTR[ CHARX].POSS.POSSCNT);

だったら起きなかったと思う。

話が脱線してしまった。40年以上前のロバート・ウッドヘッドのコーディングの間違いを指摘したいわけではないのだ。言いたいのは、オリジナルのWizardryは、CUIのアプリのようにキー入力とその結果の表示を同じ部分で書ける。これはアクションゲームでは非常に不便だが、シングルタスクなアドベンチャーゲームや古典的なRPGのような、リアルタイム性を求められないインタラクティブなインタフェースではコーディングがしやすい。

オリジナルのWizardryと同様、Pyxelを使って、この時代を逆行するやり方ができないかを模索したい。これができれば、この手のアプリはすごく簡単につくれる気がする。Pyxelで上記の鑑定ルーチンを書くと以下のようになる。もしかしたらもっとスマートな書き方になるかもしれないが、今のところ思いつかない

# キー入力(イベント)が発生されると呼び出される関数
def update(key):
    if stat == 1:
        if key == '0':
            return char_sheet
        elif key >= ord('1') and key <= ord('1')+player.itemcount():
            if player.try_identify(key - ord('1')):
                stat = 4      # success
            else:
                if player.try_equipcurse(key - ord('1')):
                    stat = 5      # failure
                else:
                    stat = 6      # cursed!
    elif stat == 4:
        if key == 'A' or key == 'B':
            return char_sheet
    elif stat == 5:
        if key == 'A' or key == 'B':
            return char_sheet
    elif stat == 6:
        if key == 'A' or key == 'B':
            return char_sheet

# 描画関数 
def draw():
    if stat == 1:
        text('どれを鑑定しますか?(0でもどります)')
    elif stat == 4:
        text('鑑定できました!')
    elif stat == 5:
        text('運がない!')
    elif stat == 6:
        text('呪われた!')

すごくざっくり書いたので、本物とは違うが、簡単に言えば、キー入力と描画が別れているために、毎回現在のステートを確認して、それごとに処理を書かなければならない。これは別にいいのだが、ステートが増えてくるとこのステート管理がupdateとdrawの両方にのしかかっていきどんどん煩雑になっていく。一方オリジナルのコードはシングルタスクで現在処理している以外システムがどうなってもしったことではないので、ステートの管理はいらない。今実行しているところがそのステートを表している。
何をいってるのかわからないと思うので、オリジナルのコードをpythonで書くと以下のような感じになる。

# 鑑定する関数
def identify_item():
    text('どれを鑑定しますか?(0でもどります)')
    key = getkey()
    while key < ord('0') or key > ord('1')+player.itemcount():
        key == getkey()
    if key == '0':
        return char_sheet
    if player.try_identify(key - ord('1')):
        text('鑑定できました!')
    else:
        if player.try_equipcurse(key - ord('1')):
            text('運がない!')      # failure
        else:
            text('呪われた!')      # cursed!
    getkey()
    return char_sheet

キー入力イベント処理と描画処理が別れているものと比べて非常にシンプルでわかりやすい。ただ、今のOSは一つのアプリケーションがCPUをずうっと専有するわけにはいかなく、オリジナルのコードのようにそれのためにすべての資源をつぎ込むような書き方はできない。この書き方をそのままの意味でとると、getkey()を呼び出している間、待ち続けるので世界は止まるし、text()などの描画は実際にどのタイミングで画面に出力されるのかがはっきりしないので、わけがわからなくなる。ただ、getkey()でうまいことやればなんとかなるのではないかとちょっと思う。描画周りの関数も、上記で言えばtext()は呼び出された時点では描画をせずにディスプレイリストみたいなものに登録するだけにして裏で描画すればいいのではないか。getkey()はキーを取得するだけでなく、キー入力がない間のスリープを行うようにすれば擬似的に同じようなことができるのではないかと思う。
というわけでPyxelでサンプルを書いてみた。たぶんここまで読んでいる人はもういない気がする。

だらだら書いていたら長くなってしまった。コードが汚いのはご容赦願いたい。まず、今回やりたいことを実現するために、スレッドを使うことにした。pyxelの処理が走るスレッド(この場合Gfx)とゲーム本体のスレッド(この場合App)だ。この2つは仮想的には並列で走っていることになっている。ふたつのスレッドは、キー入力のやりとりと、描画の指示のふたつのデータをやりとりする。キー入力はGfx側でキーイベントをうけとるたびに保存し、App側でgetkey()関数が呼び出されると保存したデータを取得、なければイベントがくるまで待ちつつけるという処理をする。描画指示は、App側で描画したいものがあればそれを一旦リストに保持して、Gfx側が描画するタイミングでリストに記述されているものを淡々と描画していく。2つのスレッド間のデータのやりとりはロックでアトミックにしている。
このやり方によって、お互いのスレッドは自由に動くことができ、オリジナルのWizardryにあったやり方でも問題なくpyxelの書き方でなくとも動かすことができる。

ちゃんと動いた

思ったよりも簡単にかけてしまった。pythonすごいなあ。こんなことできないかなと思ったことが結構簡単にできてしまう。上で書いたサンプルはあくまで動作チェック用なので、もう少し使いやすい形で修正して、今のコードを書き直そうと思う。とくにディスプレイリストの実行がeval()を使っていて、なんか美しくないんだよなあ。


この記事が気に入ったらサポートをしてみませんか?