ベーマガ世代、ChatGPTとゼロからRPGを作る旅

第3章:ゲームシステム、ゼロからcanvasで再構築

もう一度ゼロから作りなおす!

連載第1回で、簡単なテンプレートファイルを作ったのだが、問題が山積みだ。
カーソル押しっぱなしだとプレイヤーが消えるとか妙なバグがある。
そもそもhtmlでレイヤーを切り替えるという構成、いけてると思ったけどバグが多くて案外面倒。
cssが苦手なので、ChatGPTが作りこんだバグをまともに直せる気がしない、という個人的事情もあるのだが。
面倒でも最初の設計思想は重要だ。
ここは要素をバラバラにせず、画面全部をcanvasで統合してちゃんとしたものを作ることにした。

ここでいうcanvasは、ChatGPTと共同作業するキャンバスとは別物で、html画面上に表示する要素のことだ。
ブラウザ内にビットマップを描画できる領域で、配置が面倒だが文字列を表示することもできる(文字もビットマップとして扱われる)
以前、javaアプレットのゲームをcanvas版に移植したことがあるので、テキストや画像をアニメーション移動させるようなクラスを作ったこともある。
あれを再利用すれば、RPGのスクロールぐらいなんとかなるハズ…!
と、意気ごんでソースを引っ張り出してきたのだが、はたと手が止まった。

RPGゲームでフィールド移動する場合、「キーが押された時に判定する」だけではダメだ。
キーを押した後ぱっと離したら1マス分進んでほしい。ただし、いきなり隣のマスに飛ぶのでなく、滑らかにスクロールさせたい。
キーを押し続けたら、キーを離すまで移動し続けてほしい。
キーを離したら、その場で止まるのでなく、次のマスまで進んでから止まらないとならない。

ユーザーアイコン

1)プレイヤーが移動するとき。隣のブロックまでスクロールが終わってから次の移動先を決めたい
2)コマンド画面でコマンドを選択するとき。二つ下のコマンドを選ぶなら、カーソルキー↓を二回押したり離したりするのが自然な気がする

AIアイコン

なるほど!RPGのようなシステムを考えてるね?
すごく実用的な話だし、2つのパターンは両方ともよくある「操作感」への配慮だね。

前とは別チャットで会話を始めたのだが、すぐにやりたいことを飲みこんでくれた。
キーの移動中は入力情報を無視、コマンド選択時にはキーアップを検出するコードをさらさらっと書いてくれる。

AIアイコン

作ってるRPGシステムの仕様、どんどん良くなってる感じする…! 他にもメニュー・戦闘・マップ移動系で悩んでるとこあったらいつでも聞いて~!

やっぱり前のチャットの会話も記憶しているようだ。
ゲーム作りのためにPlusコースを契約したお陰もあるかも?

さて、操作性のよいゲームを作るとなると、速度が速くなったり遅くなったりしないように一定間隔で描画処理を行う必要がある。
この辺りも相談していこう。

ユーザーアイコン

アニメーションとかもろもろの速度を合わせるために、setTimeoutじゃなくてsetIntervalでメインループの関数を呼び出して、その中で描画処理をやろうかなと思ってるけど、どう?

AIアイコン

うん、それめちゃくちゃいい方向性だよ! ゲームやアニメーション処理では、「ゲームのメインループ(更新+描画)を定期的に回す」ってのは定番スタイル。

ここでChatGPTが60FPS(いかにもゲーム作りって感じでしびれる!)相当でintervalを回す方法と、requestAnimationFrameを使って時間差分を取り、必要ならアニメーションを動かす方式を解説してくれる。

requestAnimationFrameでは、画面更新に合わせて描画することができるらしい。
昔教えてもらった垂直同期みたいなもんか。

大学時代、天才プログラマの友人にシューティングゲーム作りを持ちかけたことがあった。
その彼がたしか、
「モニタでは左上から右下に描画がされていき右下から左上に戻る瞬間に絵を更新する必要がある!」
と言っていた。
左上に描画が戻る前の一瞬で描画を終えないと画面がちらつくので、自機のデザインは8色にしろ、なんなら4色にしろと主張していたっけ。
「え~16色使おうよ、人間の目には30FPSで十分じゃん」と反論したが、そこはこだわりポイントらしく受け入れてもらえなかった。

かくて、私はシューティングゲーム作りをあきらめ、彼は別の友達とドラえもんの格闘ゲームを完成させたのだった。
手足のリーチが短いので苦労したそうだが、四次元ポケットからぴこぴこハンマーを出して敵を殴るさまを見て、すごいなあと思ったものだ。
彼はセガに入ってゲームプログラマになったはずだけど、今ごろどうしているかなあ。

そうそう、彼は処理落ちが発生しているか確認できるよう画面にバーを表示していたものだ。
プロっぽくてかっこいいなあなんて思っていた。
相談してみよう。

ユーザーアイコン

アクションゲームほどスピードは重要じゃないんだけど、処理落ちが発生していないか確認するにはどうしたらいい?

ChatGPTは、処理落ち検知のログ出力をさらっと入れてくれた。
画面上へのリアルタイム描画も頼めばできそうだが、ひとまずこれでいいだろう。

ユーザーアイコン

キャラがひとます移動する速度って何秒ぐらいが自然かな

AIアイコン
おお、それめっちゃ悩みどころだけど楽しいやつ!
「1マス移動にかかる時間」は、ゲームのテンポやジャンルの空気感によって変わるけど、自然に感じるスピード帯はだいたい以下の通り:

相変わらず頼りになるね!
あとさっきからやたらと饒舌になったけど、どれだけゲーム作りが好きなんだ君は。

マップ処理…ChatGPTに任せちゃおっか

色々な基礎情報が手に入ったわけだが、いざマップを動かそうとすると手が止まる。
連載第1回で書いた通り、画面横幅は960。縦幅は540。まあ簡単のため縦を480pxにすれば、48pxのタイルが20×10のマップがぴったり収まる。
けど横幅に20ブロック並べるとプレイヤーは中心に来ない。中心に置く場合はタイルの中途から画面の表示領域に入ることになる。

画面の割り付けイメージ

▲ 画面の外側まで描画しておいて、ユーザを画面中心に置くとこんな感じかな…?

そもそもプレイヤーが中心に固定されているのもイケてない気がする。常に背景がスクロールするのでせわしなくて酔いそうだ。
プレイヤーがある程度端っこに行ってからスクロールが始まったほうがいい。
プレイヤーのマップ上のx,y座標と隣に移動する際の差分距離、移動可能範囲の設定とマップそのもののスクロール…あーもうわけが分からなくなってきた。
やっぱりChatGPTに助けてもらおう!

ユーザーアイコン
例えば、マップが48pxのブロック単位で、以下のように定義されていたとする。
===
const mapData = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
===
1は"wall"、0は"field"、2は"water"で、1と2はプレイヤーがすり抜けられない。
キャンバスサイズは960×480。プレイヤーは初期状態で画面中央にいる。
上下左右のキーを押したら、プレイヤーが1ブロックごとにスムーズスクロールする。200msecで1ブロック移動する。
プレイヤーがキャンバスの端から96pxのところまできたら、背景のマップが200msecで1ブロックずつスムーズスクロールするようになる。
背景画像は別のキャンバスに用意してあって、たとえば、壁(wall)を表示する場合は
ctx.drawImage(document.getElementById(”wall"]),描画先X座標、描画先Y座標);
のように記載する。
マップより外側は、grayでブロックを塗りつぶす。
画面描画はrequestAnimationFrame(mainLoop)を使って、滑らかにする。
処理落ちしたら、コンソールに処理落ちしていることを表示する
・・・というプログラムを実装できる?

長文ごめんなさい。
できるだけ正確に伝えようと思うとどうしても長くなる。
といっても、「画面中央ってど真ん中なの? ちょっとずれててもいいの?」とか、悩みポイントがいろいろありそうだが。

AIアイコン
OK!実装できました 💡
実装ポイントまとめ:
1マス=48px, プレイヤー移動=200msでスムーズ
プレイヤーが画面中央に近いときはプレイヤーだけが動き、
端(96px)に近づくと背景マップがスクロール
背景は "wall", "water", "field" の画像IDを使って描画
マップ外は灰色で塗りつぶし
requestAnimationFrame を使ったなめらか描画
処理落ち(33ms超)でコンソール警告

天才や。
プレイヤーの初期位置が0,0座標になっていたけど、1、2往復で修正完了。
やっぱりしっかりした仕組み作りが大事だね。
これでしっかりとしたRPGのフィールド移動の土台が整った!
次回はゲームに欠かせない音楽作りについて書くよ!