2Dゲームを作ってみたというお話

作ったもの

下記のように実行すると、ゲームマップが現れ、ゲーム開始。

$./so_long_bonus map.ber

f:id:rakiyama0229:20210805173614g:plain
player(勇者)を操作して、enemy(ドラゴン)から逃げながら、
collectible(光)を集めてexit(ダークサイドっぽいやつ)まで移動させてる。
一応enemyからはギリギリぶつからず逃げ切った。笑
wall(木)を通り抜けることはできない。
移動はW,A,D,S keyで。

  • so_long_bonus : 今回作った実行ファイル 。
  • map.ber : マップのソース。ゲームスタートの際の各マスの配置(enemyの配置はプログラム側で決める)。
    • 0 : empty
    • 1 : wall
    • C : collectible
    • E : exit
    • P : player
$cat map
1111111111111
10010000000C1
1000011111001
1P0011E000001
1111111111111

github.com

ざっくりとした流れ

まず、今回マップの表示はXサーバを利用している。
そんでもってXサーバを利用する上で便利なライブラリ(今後使うmlx系関数はここから)が事前に用意されている。
1. マップのソースを引数で受け取り、読み込む、(Xサーバの)windowで表示するためのマップ情報を用意する。
2. mlx系関数を使ってwindowに表示するための情報を用意する(各マスの画像の用意など)
3. マップを表示させる。

主に使った関数

  • mlx_init
    Xサーバを利用するために必要な情報を持った構造体のポインタを返してくれる。
  • mlx_xpm_file_to_image
    xpmファイル(今回だと事前に用意した各マスの画像)を元にimage情報を持った構造体のポインタを返してくれる。
  • mlx_new_window
    使用するwindowの情報を持った構造体のポインタを返してくれる。
  • mlx_put_image_to_window
    window内の指定した位置にimageを表示してくれる。
  • mlx_loop
    事前に指定した関数(例えば、マップを表示する関数)を実行するなどの、ループ処理をしてくれる。
    = プログラムが終了処理をするまで、マップを表示し続けてゲームを続行する。
  • mlx_loop_hook
    mlx_loop内で、実行する関数を指定できる。
  • mlx_key_hook
    mlx_loop実行中に、keyを押された際に実行する関数を指定できる。
  • mlx_hook
    mlx_loop実行中に、red cross(xボタン)を押された際など特定のイベント発生時に実行する関数を指定できる。

プログラムの流れ

各情報の用意

  1. 今回あらゆる情報を構造体(t_data *data)にまとめるので、構造体の初期化
  2. 引数で受け取ったmap.berの読み込み、中身のチェック
    map.berの内容をint型の二次元配列data->mapに落とし込む(そっちの方が扱いやすく思えた) f:id:rakiyama0229:20210805194516p:plain
  3. imageやwindowを用意やら、enemyの位置をmapに追加やらの準備
  4. loopに入る前に各関数をセット
    • keyが押された際にplayerの移動などマップの情報を書き換える関数をmlx_key_hookに渡す
    • red crossが押された際にプログラムを終了させる関数へ繋ぐ関数をmlx_hookに渡す
    • loop内で繰り返す関数(マップの表示、enemyを動かす、アニメーションを動かす)をmlx_loop_hookに渡す

f:id:rakiyama0229:20210806013308p:plain

loop実行

基本的に、マップ情報を元にimageを表示する処理(put_map)が常に一箇所で動いていて、
表示内容を変える場合はマップ情報の書き換えのみ他の関数で行っている。 f:id:rakiyama0229:20210809134023p:plain

imageの管理と表示について

windowに表示するimageは三次元配列で管理していて、
以下の情報を元に配列内のimageにアクセスして、それを表示させている。
- data->map(マップ情報、どのマスなのか、playerのマスなのか、emptyのマスなのか)
- data->square_side(どの向きなのか、playerは右向きなのか上向きなのか)
- data->square_act(どの動きなのか、これは向きに加え4種類の動きを用意している)

下の図は表示したいマップの位置のマスがENEMYで、向きは右向き、動きは2番の時の例
f:id:rakiyama0229:20210809133747p:plain マップの情報を書き換えたり、向きの情報を変えたりすれば、マップ表示の際のimage配列へのアクセスが代わり別のimageを表示するので画面に変化が出てくるという仕組み。 さらに動き(data->square_act)の情報を定期的に変更するように関数を動かしておけば、アニメーションのように動きが生まれる。

課題

  • 今回各マスに向きやアニメーションの動きを入れるコードを書いたが、結局全画像を用意することに疲れて全ての動きができていない。playerは正面のみだし、enemyは右向きのみ。
  • メモリの使用量が200MG近くなったり多いのと、実行速度が環境によってかなり遅い様子。解決方法がないか考え中。
  • 同じような文字列多用していたので、defineなど使ってコードをわかりやすくする。

パイプを実装してみたというお話

やりたいこと

下記のように、bashのパイプの動きを再現する。
言語 : c

infile : 入力元ファイル
outfile : 出力先ファイル
pipex : 今回作るプログラムの実行ファイル名

$ cat infile
Hello World
$ < infile cat | cat -e | cat -n > outfile
$ cat outfile
     1  Hello World$
  • 今回作るプログラムの動き(引数で材料を渡す)
$ cat infile
Hello World
$ ./pipex infile "cat" "cat -e" "cat -n" outfile
$ cat outfile
     1  Hello World$

github.com

使った関数

  • pipe int pipe(int pipefd[2]);
    pipe(一時的に生成されるファイルみたいなもの)を生成して、pipefd[]にそのファイルディスクリプタを格納する。
    ※書き込み用pipefd[1]と読み込み用pipefd[0]のファイルディスクリプタがあって、pipefd[1]を閉じずにpipefd[0]から読み込みすると書き込みを待って処理が停止してしまう。 f:id:rakiyama0229:20210901000523p:plain

  • dup2 int dup2(int oldfd, int newfd);
    oldfd(fd=ファイルディスクリプタ)を複製してnewfdに割り当てる。つまり、newfdはoldfdと同じファイルを指す。f:id:rakiyama0229:20210831235631p:plain

  • execve int execve(const char *filename, char *const argv[], char *const envp[]);
    実行形式のファイルを実行する。
    通常標準入力fdから入力を受け取り、標準出力fdに実行結果を出力する。 f:id:rakiyama0229:20210901000158p:plain

  • fork pid_t fork(void);
    子プロセスの生成。
    execveを実行するとそのプログラムは終了してしまうので、
    今回はコマンド(catなど)を実行したい度に子プロセスを作成して、子プロセスでexecveを実行させた。

  • wait pid_t wait(int *status);
    親プロセスが子プロセスの終了を待つときに使った。

  • その他
    access, open, close, read, write, malloc, free, exit, perror

プログラムの流れ

f:id:rakiyama0229:20210804224252p:plain

上の図だとわかりづらいので、さらに図式化

f:id:rakiyama0229:20210901000957p:plain

課題

実は上述したプログラムでは、大きいサイズ(50Mバイトとかやってみた)のinfileを渡されるとプログラムが停止してしまうという、、。
おそらく下記の流れでこの現象が起きている。
pipeに書き込むサイズがpipeの容量を超える
→ 子プロセスでのpipeへの書き込み作業が完了しない
親プロセスは子プロセスの終了を待っているため進まない

他の人のプログラムや、linuxのmanページのpipeの容量に関する記事を参考にした。 man7.org

  • 解決方法の流れ
    親プロセスでは、子プロセスを次々生成させる(逐一子プロセスの終了を待たない)
    → 子プロセスのstdinfdには、前の子プロセスが書き込みに使うpipeのfdを繋げておく
    子プロセスを生成し終わった親プロセスは、最後の子プロセスの実行のみ終了を待つ
    小プロセスはゾンビを作らないためにも最後にすべてwait系で対処した方がいいらしい。
//全ての小プロセスを生成後
while(child_process_cnt--)
    wait(NULL);

f:id:rakiyama0229:20211005094006p:plain
子プロセスを各々走らせ、最後に全てwaitする
このようにすれば、各子プロセス間でpipeがつながっているので、パイプがフルになって書き込み作業が止まるという現象がなくなる。
これが本来shellが行っているようなマルチプロセスの処理っぽいですね。
というよりせっかくのマルチプロセスだから逐一waitしてちゃ処理速度的にもったいなかったのかもしれない。

追記

親プロセスで標準入出力fdの指す先をいじる(今回だとstdinfdをpipeに繋いでいる)場合、その後に親プロセスで標準入出力先が変わっていることに注意。
解決法の例

  • stdinfdをpipeに繋げるのではなく、pipeのfdを変数で持っておいて、次の子プロセスではそこから読み込むようにする。
  • stdinfdを指す他の変数(仮にoriginal_stdinfd)をdup()で取っておいて、全ての作業が終わったらstdinfdがoriginal_stdinfdの指す先を向くようdup2()で戻す。

参考にした記事

[https://www.c-lang.net/pipe/index.html:title] linuxjm.osdn.jp stackoverflow.com