執筆:EugeneAmnis
JavaScriptでアクションゲーム
前回、ゲームの最低限の条件を以下の3つと仮定しました。
- キャラクター、背景と移動できる地形で構成されたマップ、敵などの障害物の3つが画面に表示されていること
- キャラクターはユーザーの操作に対して視覚的にわかる挙動をすること
- キャラクターと障害物は接触・非接触が感知できるようになっていること(当たり判定)
この3つだけを満たすゲームはアクションゲームであり、それを学習効果が高く、難易度も高くないJavaScriptで実装したコードを見ながら、具体的な説明を行うと話をしました。今回はまず、どんなアクションゲームなのかから話していきます。
FlappyBird
スマホ黎明期に爆発的にインストール・プレイされたアクションゲームがあります。その名をFlappyBirdといいます。ご存じの方も多いと思いますが、このゲームは小太りの小鳥が障害物である土管を避けながらどこまで長く飛び続けることができるかをプレイヤーが小鳥を操作(羽ばたき)する有料ゲームアプリです。
ゲームの操作は画面の左端に小鳥がいて自動横スクロールされて近づいてくる上下に伸びた土管の間を画面クリックで羽ばいて上昇し、羽ばたき無しで自由落下する動きで避けていくだけです。操作はこれだけです。
個人的にはインストール・プレイもしたこと無いのですが当時、出始めで新しい仕事を開拓できると謳っていたスマホアプリ業界のインパクトを与えるには十分な話題性を振りまきました。全くプログラミングに興味のなかった筆者自身が覚えているくらいで、酒の席ではよく「夢のある世界でいいね。」とおじさんがクダ巻いていたのを思い出します。
うろ覚えですがこのアプリのデベロッパーの方は学生さんで、毎日の売上に驚いて、アプリを非公開にしてしまったエピソードをテレビで見た覚えがあります。そして本家が居なくなったのをいいことに類似アプリが大量にアプリストアに溢れることになります。
このゲームは3つの条件のみで構成され、コードもそんなに複雑ではなかったのでネットでたくさんのソースコードを見ることができました。開発した学生さんも授業か何かで実装したものをアプリストアに何の気なしに上げていたとネットニュースで読んだ気がします。
JavaScriptの簡単な説明
サンプルコードを見る前にJavaScriptの簡単な説明をしておきます。どんなOS上でもブラウザとテキストエディタでコーディングする場合はHtmlファイルを通してJavaScriptのコーディングをしていく形が一般的になります。つまり、基本としてブラウザはJavaScriptを直接レンダリングするようにはできていません。
ブラウザが読み込むHtmlファイルに直接記入するか、又は読み込み先のJavaScriptを指定して、そのJavaScriptを編集する形を取ることになります。元々、JavaScriptはHtmlとCSSで記述されたWEBページに動きを持たせるためにつくられた言語なので基本中の基本的なスタンスでコーディングする訳です。
詳しい説明は省きますが、Html、CSS、JavaScriptの各関係性は建築で言うHtmlは構造、CSSはデザイン、JavaScriptは家具や家電に例えられます。何階建てか何部屋あるか、窓や扉・電気水道などの基本構造をHtmlで記述し、CSSで壁紙や扉の色を決め、TVや電気を点けるなどアクションに由来する動きをJavaScriptで決定するというイメージになります。
JavaScriptはプログラミング言語の中でも非宣言型の関数・変数を使用、オブジェクト指向型のモダンなコーディング、ブラウザ内で機能が完結されるサンドボックスタイプなのでコーディングのミスが致命的なダメージを与えることが少ないという初学者向けの特性を持っています。
Javaなどのプログラミング言語では宣言型と言って使用する関数・変数の型を文字列や整数、バイト型などを設定する必要があり、一度設定するとその関数・変数はその型しか使えなくなる特性がありますが、Javascriptは宣言した関数・変数に型の概念が無いため、文字列や整数を気軽に代入することができます。ただし、代入回数や生存期間についての制約はあります。
オブジェクト指向型というのは構造のある安全で効率的なプログラムが書けるというイメージを持ってもらえれば初学者は十分です。無論、それを意識しないコードも書けます。徐々にステップアップしたコードが書けると思ってもらえれば結構です。
C言語やJavaなどはコンピュータのメモリなどの記憶領域に直接作用する命令を持ち合わせているのでもし、ミスをした場合は最悪コンピュータを壊しかねないリスクがありますが、JavaScriptは安全に隔離されたブラウザ内に作用する命令しか無いため、ミスを犯しても最悪ブラウザがクラッシュする程度で済みます。因みに安全に隔離された環境をサンドボックスと呼びます。
コードの出所と開発環境の準備
今回提供するサンプルコードTurtleFlappingは筆者が全てコーディングし、画像データ類を含めてMITライセンスと呼べれる一般的なフリーライセンスになっています。ここからダウンロードできます。
元々はForMeで提供しているミニアプリ作成機能であるBottleWebのサンプルアプリとしてFlappyBirdをオマージュして作成したものです。
PCでの開発
Windows、macOS・Linuxで開発される方はダウンロードしたZipファイルを解凍してindex.htmlをダブルクリックすればブラウザで実行結果を見ることができます。ただし、スマホ向けに最適化されているので後で説明する手直しが必要となります。
index.jsをメモ帳などのテキストエディタで開き、編集・保存した後にブラウザで開いているindex.htmlを再読み込みすることで編集内容を確認できます。
スマホでの開発
Androidでは筆者が提供するForMeのBottleWebエディタを使用することが一番簡単ですが、他のアプリやiOSで開発をする場合ではブラウザプレビュー機能のあるテキストエディタアプリを使用する必要があります。
PCと違い、スマホのブラウザアプリはセキュリティ上の理由から複数のファイルにアクセスできない為、描写が不完全なものになってします。それではコードの編集結果が確認できないので手間にはなりますが、各ストアでアプリを見繕って下さい。
サンプルコード
それではコードを見ていきましょう。尚、Htmlファイルは載せませんがポイント表示用のp(テキスト)要素とゲーム本体を描写するCanvas要素、ジャンプ用ボタンが配置されているだけです。
ゲーム内容はウミガメが海に漂うビニール袋を避けていくだけのものです。ウミガメは海中内の重力で徐々に沈んでいき、ジャンプボタンでジャンプします。完全に沈むか、ビニール袋に当たったときにゲームオーバーになります。
ForMeで提供されているTurtleFlappingとは一部、コードが違う場合があります。
コードの流れと説明
このコードは以下の流れで処理をしていきます。
- 変更を行う各要素の取得と関数の初期化する。
- Htmlが描写されたと同時にゲーム描写部分であるCanvasのサイズを設定して背景等の各画像を読み込む。(ini()を実行)
- ゲームの描写を開始する。(メインループdraw()を繰り返す)
- 操作受付(jump()を実行)、当たり判定を行い、判定が真になったときにゲームを停止、各パラメータリセットを行う。(reset()を実行)
ここからはコードにコメントを挿入する形で説明していきます。各変数・定数や関数の名前、コーディングのセンスは目を瞑って下さい。
/* ライセンス等の情報を表記
Created on : 2021/07/08
Author : EugeneAmnis(https://eugeneamnis.com)
Home page : https://the-forme.net
Version :1.0
MIT LICENSE
*/
const PTT = document.getElementById('pt');//ポイント表示用のP要素取得。constは定数宣言で一回のみしか値を設定できない。
const CANVA = document.getElementById('cvs');//ゲーム描写用のCanvas要素を取得。Canvasは上左端が原点となり、右や下に進むほど数字(ピクセル)が増える。
var CTX;//Canvas要素を操作するためのオブジェクト用。varは変数宣言で何回も値を設定できるが、古い記法なので後述するletを使用したほうが無難。
var WIDTH = 0;//Canvas要素のサイズ設定用。
var TS = 0;//ウミガメのサイズ設定用。当たり判定に使用。TurtoleSize.
var PS = 0;//ビニール袋のサイズ設定用。当たり判定に使用。PlasticSize.
var back = new Image();//背景画像用。
var turtle = new Image();//ウミガメ画像用。
var pla = new Image();//ビニール袋画像用。
var jv = 0;//ウミガメ位置(縦軸)設定用。これを操作すると重力落下やジャンプを表現できる。JumpVertical.
var gs = [];//障害物位置情報と障害物数設定用。GavegeS.
var point = 0;//ポイント設定用。
window.addEventListener('DOMContentLoaded',function(){
ini();
},false);//ブラウザがHtmlを描写し終わったら、初期化処理ini()を実行する。
function ini(){//初期化処理。
let w = window.innerWidth;//画面の横幅を取得。letは変数宣言で値は何度も設定できる。宣言されたブロック内でのみ利用可能となるので名前の重複が可能となる利点がある。
let h = window.innerHeight;//画面の高さを取得。
WIDTH = w > h?h:w;横幅と高さの内、短い方をCanvasのサイズにする。ここでは三項演算子と呼ばれる1行判定文を利用。(判定式)?(真の返り値):(偽の返り値);
CANVA.width = WIDTH;//Canvasの横幅を設定。
CANVA.height = WIDTH;//Canvasの高さを設定。正方形にすることで画面回転時の処理を無くしている。
CTX = CANVA.getContext('2d');Canvas操作用オブジェクトを2次元で設定。
back.src = 'img/back.svg';//imgフォルダにある背景画像を設定。
back.onload = function(){
CTX.drawImage(back,0,0,WIDTH,WIDTH);
}//画像読み込みが完了後、Canvasいっぱいに画像を表示。画像を表示する際に幅・高さの縦横比を合わせる必要がある。今回用意した画像類は全て正方形な為、その計算を省いている。
turtle.src = 'img/turtle.svg';//imgフォルダにあるウミガメ画像を設定。
TS = WIDTH / 5;//ウミガメのサイズを設定。
jv = WIDTH * 0.3;ウミガメの縦位置を設定。
turtle.onload = function(){
CTX.drawImage(turtle,10,jv,TS,TS);
}//画像読み込み完了後、ウミガメを初期位置で表示。
pla.src = 'img/plabag.svg';//imgフォルダにあるビニール袋画像を設定。
PS = WIDTH / 4;//ビニール袋のサイズを設定。
pla.onload = function(){
CTX.drawImage(pla,WIDTH,WIDTH - PS,PS,PS);
}//画像読み込み完了後、ビニール袋を初期位置で表示。
gs = [{
x : WIDTH,
y : WIDTH / 3
}];//ビニール袋の初期位置を設定。配列を増やすと一度に表示されるビニール袋の数が増えるが、メモリ負荷が大きくなる。
draw();//メインループ処理呼び出し。
}
function draw(){//メインループ処理。
CTX.drawImage(back,0,0,WIDTH,WIDTH);//Canvasを背景画像で全てを上書き表示。これをしないと残像が残る。
CTX.drawImage(turtle,10,jv,TS,TS);//ウミガメを左端から10ピクセル、現在の高さの値で表示。
for (let i = 0; i < gs.length; i++) {//ビニール袋の数だけ処理を繰り返す。
CTX.drawImage(pla,gs[i].x,gs[i].y,PS,PS);ビニール袋を表示。
gs[i].x = gs[i].x - 4;//ビニール袋を前進。
if(gs[i].x <= -10){//ビニール袋が左端を過ぎていた場合。
gs[i].x = WIDTH;//ビニール袋の横位置を画面幅に設定。
gs[i].y = Math.floor(Math.random()*(WIDTH - PS / 3));//ビニール袋の高さを乱数を使って設定。
}
jv += 2;//自由落下分をウミガメの高さに足す。
point++;//ポイントを1点加算。
PTT.innerText = Math.floor(point / 10);//ポイント表示P要素に桁調整して表示。
if(jv > WIDTH | jv < (-1*TS)){//ウミガメの高さの値がCanvasの高さを超えるか(沈み過ぎ)、ウミガメのサイズをマイナスにしたものより小さい(水面を超えた)場合。
reset();//リセット処理呼び出し。
}else if((TS - 50) > gs[i].x){//ウミガメの横幅引く50ピクセル(調整値、マジックナンバーと呼ばれ、行儀は良いとは言えない。変数を別に用意すべき。)よりビニール袋の横位置が小さい場合。
if((TS + jv) > gs[i].y & jv < (gs[i].y + PS)){//ウミガメがビニール袋の高さに入っている場合。
reset();//リセット処理呼び出し。
}else{
requestAnimationFrame(draw);//再帰処理。(自身を呼び出す。)
}
}else{
requestAnimationFrame(draw);//再帰処理。(自身を呼び出す。)
}
}
}
function reset(){//リセット処理。
jv = WIDTH * 0.3;//ウミガメの高さを初期位置に設定。
PTT.innerText = "Retry : " + Math.floor(point / 10);//ポイント表示用P要素にRetry :最終ポイントを表示。
point = 0;ポイントリセット。
for (var i = 0; i < gs.length; i++) {
gs[i].x = WIDTH;
gs[i].y = Math.floor(Math.random()*(WIDTH - PS / 3));
}//ビニール袋の数だけビニール袋の位置を再設定。
}
function jump(){//ジャンプ処理。こらはHtmlのジャンプボタンにonclickで発火するイベントとして設定。
jv -= 50;ウミガメ縦位置の値から50(マジックナンバー)を引く。
}
アラの多いコードですが約100行程度で、アクションゲームを作ることができました。
当時、複数のサイト様のコードを参考にしていたのですが記録に残し忘れた為、具体的にサイト名を謝辞として上げることができないのをここで陳謝します。これからコードの勉強をされるかる方はこのような無礼なことが無いように参考にしたサイト様の記録は取っておきましょう。
より本格的なゲームプログラミングについて知りたい方はプログラミングスクールに通うのがおすすめです。