こんにちは,NTTドコモの伊藤です.
私は業務でレコメンドエンジンの開発を行っています.エンジン自体はバックエンドで動くシステムなので,フロントエンドに関する知識はほとんどないのですが,いずれはフロントエンドの開発もできるようにならなければいけないなぁ,と常日頃から思っております.
そこで(?)本記事では,私が昔に現実逃避のために作っていたスマホ用落ちゲーについて書きたいと思います.
「落ちゲー」って何?という方もいるとは思うので,まずは以下のURLから遊んでみてください.
http://javascapp.html.xdomain.jp/icedrop_silent_2019.html
(スマホからじゃないと動作しません)
上から落ちてくるアイスクリームを,コーンを動かしてキャッチするというゲームです.
至ってシンプルですが,最低限ゲームの体は成しているかと思います.
日々JavaScriptも進化しているので、あまりベーシックにコードを直書きすることも減ったかと思いますが、今回は自学のために基礎的な部分から書いています。
#落ちゲーに求められるもの
落ちゲーを作るにあたって何を考えなければいけないかというと
1.描画処理
2.タッチイベント処理
3.ループ処理
4.成功失敗などの判定処理
の4つがメインかと思います.
ゲームの場合,Webサイトなどとは違って常に動的に処理を走らせないといけません.
どこにレイヤーを配置して,それをどのタイミングで表示/非表示にするか,どの程度の間隔で動かすか,ということを考えます.
また,ユーザからの操作も常に入ってくるので,それをリアルタイムで処理し,描画処理に渡してあげる必要があります.
そして,ユーザが操作して,画面が変わって,それに応じてまたユーザが操作して,画面が変わって,……ということを繰り返すので,基本的にゲームはループをベースに処理を進行させていきます.
最後に,ゲームですから当然どこかで終了させなくてはなりません.ゴールするとか,失敗するとか,そういった契機でゲームオーバーになるように条件分岐を設定してあげます.
#描画処理
描画処理は,基本的にはhtmlで宣言したレイヤーを出したり消したりすることで行います.
たとえば,上のゲームの場合body部は以下のようになっています.
<body>
<h2 id ="start"><img src="logo/logo.JPG"></h2>
<div id="game-start" style="position:absolute; left:64px; top:412px;">
<a href="JavaScript:gameStart(1)">アイスドロップ(初級編)</a><br>
<a href="JavaScript:gameStart(2)">アイスドロップ(中級編)</a><br>
<a href="JavaScript:gameStart(3)">アイスドロップ(上級編)</a><br>
<a href="JavaScript:gameStart(4)">アイスドロップ(超級編)</a><br>
<a href="JavaScript:gameStart(5)">アイスドロップ(チャレンジ)</a><br>
</div>
<div id="game-countdown-3" style="position:absolute; left:325px; top:400px;">
3
</div>
<div id="game-countdown-2" style="position:absolute; left:325px; top:400px;">
2
</div>
<div id="game-countdown-1" style="position:absolute; left:325px; top:400px;">
1
</div>
<div id="game-end" style="position:absolute; left:64px; top:412px; color:#ffffff">
アイスクリーム落としちゃった<br>
<a href="JavaScript:gameStart(1)" style="color:white">もう一回(初級編)</a><br>
<a href="JavaScript:gameStart(2)" style="color:white">もう一回(中級編)</a><br>
<a href="JavaScript:gameStart(3)" style="color:white">もう一回(上級編)</a><br>
<a href="JavaScript:gameStart(4)" style="color:white">もう一回(超級編)</a><br>
<a href="JavaScript:gameStart(5)" style="color:white">もう一回(チャレンジ)</a><br>
</div>
<div id="dropice" style="position:absolute; left:50px; top:370px;">
<img src="./images/IMG_4142.JPG" width="900px" height="1000px">
</div>
<div id="rocation-corn" style="position:absolute; left:50px; top:1450px;"></div>
<div id="rocation-ball" style="position:absolute; left:120px; top:200px;"></div>
<div id="score" style="position:absolute; left:50px; top:200px;"></div>
<div id="lyrcorn" style="position:absolute; left:380px;"><img src="corn.jpg" width="200px" height="400px"></div>
<div id="ball" style="position:absolute;"><img src="iceball.gif" width="150px" height="150px"></div>
</body>
1つ1つの<div id=~>
から</div>
で囲まれた部分がレイヤーに対応しています.それぞれ,ゲームスタート時に表示するテキスト,ゲームオーバー時に表示するテキスト,落ちてくるアイス,アイスを受け止めるコーンなどを定義しています.
ただ,このままだと最初にページを読みこんだ時点ですべてのレイヤーが表示されてしまうので,初期表示するレイヤー以外は非表示にしておく必要があります.
<style>
#game-start{
font-size:60px;
}
#lyrcorn{
z-index:1;
visibility:hidden;
}
#ball{
z-index:2;
visibility:hidden;
}
#game-countdown-3{
font-size:300px;
visibility:hidden;
}
#game-countdown-2{
font-size:300px;
visibility:hidden;
}
#game-countdown-1{
font-size:300px;
visibility:hidden;
}
#game-end{
z-index:4;
font-size:60px;
visibility:hidden;
}
</style>
上のようにstyle部でレイヤーごとにvisibility:hidden;
を入れてあげることで,そのレイヤーは読み込み時には表示されなくなり,game-startレイヤーだけが表示されるようになります.
逆に,レイヤーを表示させたいときには,visibility
をvisible
に設定します.
document.getElementById( 表示させたいレイヤー ).style.visibility = "visible";
#タッチイベント処理
スマホゲームの場合,基本的にはユーザがスマホのスクリーンをタッチしてイベントが進行していきます.
JavaScriptでは,こうした操作が行われたかどうかを検知するメソッドとしてaddEventListener()
というものが用意されています.これで,たとえばキーボードの入力,クリックの有無,タッチスクリーンの有無を監視して,検知した場合には指定したメソッドに飛ぶ,という仕組みになっています.
使い方としては,addEventListener(種類,実行したいメソッド, false)
という指定をします.例えば本ゲームの場合,
function gameStart(level) {
gamelevel = level
cornx="800px";
corny="1000px";
ballx="600px";
bally="400px";
score=0;
//スコアリセット
disscore = document.getElementById("score");
disscore.innerHTML="落とさなかったアイス " + score;
change=0;
lyrSetVisibility("game-start",false);
lyrSetVisibility("game-end",false);
lyrSetVisibility("dropice",false);
lyrSetVisibility("lyrcorn",true);
lyrSetVisibility("ball",false);
lyrSetPos("ball", "600px" , "400px" );
corn = document.getElementById('lyrcorn');
corn.addEventListener("touchstart", touchHandlers, false);
corn.addEventListener("touchmove", touchHandlers, false);
corn.addEventListener("touchend", touchHandlers, false);
gameCountdown(gamelevel);
}
function touchHandlers(e){
e.preventDefault();
mousex = e.changedTouches[0].pageX;
mousey = e.changedTouches[0].pageY;
}
という記述をしています.最初にゲーム開始をタッチしたときにgameStart()
が実行され,その中でaddEventListener
を定義しています.ユーザがコーンをタッチして左右に動かすことで,それに応じてコーンの位置を更新して描画するわけです.corn
という変数を定義してdocument.getElementById('lyrcorn')
とすることで,body部で定義したコーンのレイヤーをcorn
変数に紐づけてあげます.そこにイベントリスナーを定義してあげることで,コーンのタッチオンオフを検知できる機構ができました.イベントリスナーがタッチイベントを検知するとtouchHandlers
関数が実行され,今現在ユーザがタッチしている画面のX座標とY座標を取得できるようになります.
ゲーム開始後の処理についてはgameBody()
内で記述しているのですが,そこで
function lyrSetPos( lyr , x , y ) {
document.getElementById(lyr).style.left = x;
document.getElementById(lyr).style.top = y;
}
//コーンをマウス位置に合わせる.表示.
cornx = mousex - 50 + "px";
corny = 1000 + "px";
lyrSetPos("lyrcorn", cornx , corny );
と記述すると,上で取得されたX座標とY座標をもとにレイヤーを再描画するという処理が走り,ユーザからは自分の操作に応じてコーンの位置が変更されたように感じられる,という仕組みです.
#ループ処理
上で書いたように,ゲーム中は基本的に処理をループで回していきます.今回の場合は落ちゲーですから,
1.ユーザのタッチイベントを検知して,コーンのX座標をタップされている場所まで動かす
2.アイスクリームのY座標を数px下に移動する
3.計算された座標をもとに,画面を再描画する
3.アイスクリームのY座標が,キャッチ判定を行う場所まで来ているかチェックする
4.(キャッチ判定が必要な場合)アイスクリームとコーンのX座標が一致していればキャッチ成功,スコアを+1し,ループに戻る.一致していなければキャッチ失敗,ゲームオーバーメソッドを呼び出す.
という感じになると思います.ループさせる方法はいくつかあると思いますが,一番単純なのは再帰的にメソッドを呼び出すことです.
//ゲーム本体(ループ)
function gameBody(speed,chal) {
...
(中略)
...
timerID = setTimeout( "gameBody(speed,chal)" , 10 ); // タイマーで自分を呼んでみる
}
ループ処理を回したいメソッドを,タイマーを使って再帰的に呼び出すことでループさせることが出来ます.タイマーの間隔をどう設定するかで画面の更新感や捜査感が変わってくるので,自分でテストプレイしながら調整することになります.
#終了処理
永遠にループを回していたらゲームが終わらないので,ループの途中で判定を行い,ループから抜け出すための文を追加しなければなりません.さっきのループ処理フローでいうと,3と4の部分ですね.
//ボールy位置の決定.前回よりも下へ動く.
bally = parseInt(bally) + speed + "px";
//ボールの表示.
lyrSetPos("ball", ballx , bally );
//ボール位置とコーン位置の表示.
rocc = document.getElementById("rocation-corn");
//rocc.innerHTML = "スピード: " + speed;
rocb = document.getElementById("rocation-ball");
//rocb.innerHTML = "ball: " + parseInt(bally);
//ゲームオーバー条件
if(parseInt(bally) > 1000){
gameEnd();
return;
}
//キャッチ判定.ボールyが950~1000のときに行う.触れていればスコア加算+ボールもとへ戻す.触れていなければ素通り.
if((parseInt(bally) > 950 && parseInt(bally) < 1000)&&(parseInt(ballx)+75 > parseInt(cornx)+100-90 && parseInt(ballx)+75 < parseInt(cornx)+100+90)){
bally = "400px";
score = score + 1;
change = 1;
//チャレンジコース
if(chale==1){
if(score > 0 && score%3==0){
speed ++;
}
}
}
//スコア更新.
disscore = document.getElementById("score");
disscore.innerHTML="落とさなかったアイス " + score;
今回のケースで,どうやって「キャッチ成功or失敗」を判定しているかを説明します.アイスのy座標bally
が1ループごとに大きくなっていき,コーンのy座標corny
と重なっているときにキャッチ判定をしています.このキャッチ判定時にコーンとアイスが触れていればキャッチ成功とみなせるので,スコアを加算し,アイスの位置を一番上まで戻します.触れていなければ特に何もせずにループを続けていきます.こうしてループを続けていき,アイスのy座標がコーンのy座標の下端に来た時に,まだキャッチされていなければ強制的にgameEnd()
へつなげるという仕組みになっています.
また,今回チャレンジコースでは1ループごとにスピードを増加させているので,キャッチ判定時にその処理も追加しています.
#終わりに
業務で直接ゲームを開発することはないですが,JavaScriptの動的処理を理解するのにゲームという教材は非常に有用だと思っています.Pythonでの機械学習を組み合わせた,頭使う系のゲームも近々作りたいと思っています!