とりあえず前回の記事でチクタクバンバンのようなゲームは完成したことになりますが、さらにもうひと工夫加えてみたくなるのが人情です。チクタクバンバンには反則技が存在します。
動作確認は こちらからどうぞ。
Contents
意図的にループを作る事は禁止
意図的にループを作る事は禁止であり、ベルが鳴り終わるまでにループを崩さなければ反則負けとなります。
そこで今回はループ判定について考えます。ループができてしまった場合は警告文を表示させ、一定時間内にループを崩さなければゲームオーバーとします。
ループが形成されているかどうか
ではループが形成されているかどうかはどのようにして考えればいいのでしょうか? 現在目覚まし時計が乗っている線路プレートを調べれば目覚まし時計の移動方向とプレートから外へでる出口の方角がわかります。その方角に次のプレートは存在するか、存在する場合は内部に移動することができるかを調べます。内部に移動することができる場合はまた出口の方向を調べて、次のプレートが存在するかを調べます。
この処理を永久に続けることができるのであればループが形成されています。ループが形成された場合は「あと○秒以内にループを崩してください」という警告文を表示させ、警告に従わない場合はゲームオーバーの処理をおこないます。
ではループが形成されているかどうか判定する関数をつくります。こんなのはどうでしょうか?
最初に目覚まし時計がどの方角に移動しているかを示す列挙体をつくります。
1 2 3 4 5 6 7 |
enum Direct { North, South, East, West, None, } |
LoopCheck関数のなかでは、以下の処理がおこなわれます。
現在いる線路プレートとAlarmClock1.DirectOfMoveからそのときにどの方角に移動しているかを調べる。
次にその方向に線路プレートがあり、内部に進入できるかどうか調べる。
新しい線路プレートが見つかったらそのプレートの出口がある方角を調べる。
出口がある方向にさらに別の線路プレートがあり、内部に進入できるかどうか調べる。
この処理をループさせ、処理が永久に続くのであれば線路プレートはループ状態になっていると判定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
function LoopCheck() { if (isGameOver) return false; let curDirect: Direct; let curTrackPlate: TrackPlate = alarmClock1.TrackPlate; // 目覚まし時計がどの方向に移動 if (alarmClock1.DirectOfMove == DirectOfMove.EastToNorth || alarmClock1.DirectOfMove == DirectOfMove.SouthToNorth || alarmClock1.DirectOfMove == DirectOfMove.WestToNorth) curDirect = Direct.North; else if (alarmClock1.DirectOfMove == DirectOfMove.EastToWest || alarmClock1.DirectOfMove == DirectOfMove.NorthToWest || alarmClock1.DirectOfMove == DirectOfMove.SouthToWest) curDirect = Direct.West; else if (alarmClock1.DirectOfMove == DirectOfMove.NorthToEast || alarmClock1.DirectOfMove == DirectOfMove.SouthToEast || alarmClock1.DirectOfMove == DirectOfMove.WestToEast) curDirect = Direct.East; else if (alarmClock1.DirectOfMove == DirectOfMove.EastToSouth || alarmClock1.DirectOfMove == DirectOfMove.WestToSouth || alarmClock1.DirectOfMove == DirectOfMove.NorthToSouth) curDirect = Direct.South; else return false; let nextDirect = curDirect; let nextTrackPlate: TrackPlate = curTrackPlate; let i = 0; while (true) { // 北側から出る場合、そのプレートの北側に新しい線路プレートはあるか? if (nextDirect == Direct.North) { nextTrackPlate = TrackPlate.GetNorthPlate(nextTrackPlate); // 北側から出る場合、そのプレートの北側に新しい線路プレートはあるか? // ない場合は線路はループになっていない if (nextTrackPlate == null) return false; // 北側に新しい線路プレートはある場合、その線路プレートのPlateTypeを調べて // 目覚まし時計が通ることができるプレートが調べる // 目覚まし時計が通ることができるプレートであればそのプレートの出口の方角を記憶する if (nextTrackPlate.PlateType == PlateType.SE || nextTrackPlate.PlateType == PlateType.NWSE) nextDirect = Direct.East; else if (nextTrackPlate.PlateType == PlateType.SW || nextTrackPlate.PlateType == PlateType.NESW) nextDirect = Direct.West; else if (nextTrackPlate.PlateType == PlateType.NS || nextTrackPlate.PlateType == PlateType.NSWE) nextDirect = Direct.North; else return false; // 北側に新しい線路プレートがあっても // 目覚まし時計が通ることができないプレートであればfalseを返す } else if (nextDirect == Direct.South) { // 北側以外から出る場合も同様に調べる nextTrackPlate = TrackPlate.GetSouthPlate(nextTrackPlate); if (nextTrackPlate == null) return false; if (nextTrackPlate.PlateType == PlateType.NE || nextTrackPlate.PlateType == PlateType.NESW) nextDirect = Direct.East; else if (nextTrackPlate.PlateType == PlateType.NW || nextTrackPlate.PlateType == PlateType.NWSE) nextDirect = Direct.West; else if (nextTrackPlate.PlateType == PlateType.NS || nextTrackPlate.PlateType == PlateType.NSWE) nextDirect = Direct.South; else return false; } else if (nextDirect == Direct.East) { nextTrackPlate = TrackPlate.GetEastPlate(nextTrackPlate); if (nextTrackPlate == null) return false; if (nextTrackPlate.PlateType == PlateType.NW || nextTrackPlate.PlateType == PlateType.NWSE) nextDirect = Direct.North; else if (nextTrackPlate.PlateType == PlateType.SW || nextTrackPlate.PlateType == PlateType.NESW) nextDirect = Direct.South; else if (nextTrackPlate.PlateType == PlateType.WE || nextTrackPlate.PlateType == PlateType.NSWE) nextDirect = Direct.East; else return false; } else if (nextDirect == Direct.West) { nextTrackPlate = TrackPlate.GetWestPlate(nextTrackPlate); if (nextTrackPlate == null) return false; if (nextTrackPlate.PlateType == PlateType.NE || nextTrackPlate.PlateType == PlateType.NESW) nextDirect = Direct.North; else if (nextTrackPlate.PlateType == PlateType.SE || nextTrackPlate.PlateType == PlateType.NWSE) nextDirect = Direct.South; else if (nextTrackPlate.PlateType == PlateType.WE || nextTrackPlate.PlateType == PlateType.NSWE) nextDirect = Direct.East; else return false; } // ループ処理が何回続くかをカウントする // ループができている場合、中央の線路プレートは最大2回、周囲のプレートであれば最大1回 // 目覚まし時計が通過することになる。 // そのため以下の条件式を満たすのであれば明らかにループ状態になっていることになる。 // 単純計算なので、実際にはもっと少ない値であってもループ状態と判定できるはずなのだが・・・ if (i > rowMax * columMax + (rowMax - 2) * (columMax - 2)) return true; i++; } } |
ループが形成されているときの処理
Update関数が呼び出されたときにLoopCheck関数を実行してループになっていないか調べます。ループになっている場合は5秒以内にループを崩さないとゲームオーバーとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
let roopStart = 0; // ループが発生した時間 let roopTime = 0; // ループ状態が継続している時間 let roopTimeMax = 5 * 1000; function Update() { if (!isGameOver) { alarmClock1.Move(); let now = new Date(); let nowTime = now.getTime(); passedTime = nowTime - startTime; ProcessingAtLoop(nowTime); // 追加(後述) } ShowTextIfGameOver(); ShowTime(); ShowAlertIfRoop(); // 追加(後述) // レンダリング renderer.render(scene, camera); } function ProcessingAtLoop(nowTime: number) { if (LoopCheck()) { roopTime = nowTime - roopStart; if (roopTime > roopTimeMax) { // ループ継続時間が最大許容量を超えたらゲームオーバー alarmClock1.GameOver(); // roopTimeとroopStartは2回目以降のゲームでも使うので、ここで初期化してしまう。 roopTime = 0; roopStart = 0; } } else { roopStart = nowTime; roopTime = 0; } } |
警告文を表示させる機能
次にループ発生の警告と○秒以内にループを崩すように警告文を表示させる処理を示します。
HTMLには”roop-alert”を追加しておいてください。
1 2 3 4 5 |
<canvas id="can"></canvas> <div id="time" style="position: absolute; top: 0; left: 0;"></div> <div id="roop-alert" style="position: absolute; top: 0; left: 0;"></div> <div id="gameover" style="position: absolute; top: 0; left: 0;"></div> <div id="retry" style="position: absolute; top: 0; left: 0;"></div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function ShowAlertIfRoop() { const tf1 = document.getElementById("roop-alert"); let graceTime = ((roopTimeMax - roopTime) / 1000).toFixed(1); let text = ""; if (roopTime > 100) text = "ループが発生しています! " + graceTime + " 秒以内にループを崩してください!"; // 赤字で警告文を表示する tf1.innerHTML = text; tf1.style.transform = "translate(40px, 50px)"; tf1.style.color = "#ff0000"; tf1.style.fontSize = "18px"; tf1.style.fontWeight = "bold"; } |
これで線路プレートでループができてしまった場合の処理は終わりです。
最高スコアを表示する機能
それから最高スコアを表示する機能も追加しておきます。
ハンドルネームとスコアを登録する機能
HTMLに加筆して以下のように変更します。
HTMLでユーザーの名前を登録できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<!DOCTYPE html> <html> <head> <title>Three.jsでチクタクバンバンのようなゲームをつくってみた</title> <meta charset="UTF-8" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script> <script src="./ColladaLoader.js"></script> </head> <body> <canvas id="can"></canvas> <div id="time" style="position: absolute; top: 0; left: 0;"></div> <div id="roop-alert" style="position: absolute; top: 0; left: 0;"></div> <div id="gameover" style="position: absolute; top: 0; left: 0;"></div> <div id="retry" style="position: absolute; top: 0; left: 0;"></div> <div><label>ハンドルネーム</label> <input type="text" id="name" maxlength='16' /><br /> <a href="./high-score.html" target="_blank" rel="noopener">トップ10を見る</a></div> <script src="TrackPlate.js"></script> <script src="AlarmClock.js"></script> <script src="functions.js"></script> <script src="app.js"></script> </body> </html> |
ゲームオーバーになったらSendData関数でスコアをサーバーに送ります。
1 2 3 4 5 6 7 |
function GameOver() { isGameOver = true; soundGameover.currentTime = 0; soundGameover.play(); SendData(); // スコアをサーバーに送る } |
ハンドルネームの欄になにも入力されていない場合は「名無しさん」になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function SendData() { let name = ""; const textbox1 = document.getElementById("name"); let phpurl = "./save-data.php"; let url = location.href; if (textbox1 != null) //@ts-ignore name = textbox1.value; if (name == "") name = "名無しさん"; //@ts-ignore $.post(phpurl, { url: url, name: name, score: passedTime, }); } |
データが送られてきたらsave-data.phpで処理をします。送られてきたスコア(ミリ秒単位のプレイ時間)が上位10位以内であれば新しく登録します。save-data.phpの内容は ゲームのハイスコアを保存するで示したものと同じです。
ハイスコア閲覧機能
次にハイスコアを見ることができるようにする方法ですが、これもゲームのハイスコアを保存するで示したものとほとんど同じです。ただし今回はスコアをそのまま表示させるのではなく、○分○秒に変換する必要があります。
get-data.phpはゲームのハイスコアを保存するで示したものと完全に同じです。HTML部分だけ示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ハイスコア</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> td { padding:3px 10px 3px 10px; } </style> </head> <body> <div id = "result" border="1"></div> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script type="text/javascript"> let urldata = "./get-data.php"; let request = createXMLHttpRequest(); request.open("GET", urldata, true); request.send(""); request.onreadystatechange = function() { if (request.readyState == 4 && request.status == 200) { //受信完了時の処理 var result = document.getElementById("result"); var text = document.createTextNode(decodeURI(request.responseText)); let str = text.textContent; const lines = str.split("\n"); $(result).append("<table border=\"1\" id=\"table\">\n</table>"); var table = document.getElementById("table"); let len = lines.length; let num = 0; for(let i=0; i<len; i++) { if(lines[i] == "") break; let trid = "tr-id" + i.toString(); $(table).append("<tr id = " + trid + "></tr>\n"); const words = lines[i].split(","); let len2 = words.length; var tr = document.getElementById(trid); $(tr).append("<td>" + (i + 1).toString() + " 位</td>"); for(let j=0; j<len2; j++) { if(j != 1) $(tr).append("<td>" + words[j] + "</td>"); else { // words[1]にゲームのプレイ時間(単位ミリ秒)が格納されている let passedTime = Number(words[j]); // passedTimeを○分○秒に変換する let minutes = Math.floor(passedTime / 1000 / 60); let str = minutes + " 分 " + Math.floor((passedTime - 60000 * minutes) / 1000) + " 秒"; $(tr).append("<td>" + str + "</td>"); } } $(table).append("</tr>"); } } } function createXMLHttpRequest() { if (window.XMLHttpRequest) { return new XMLHttpRequest(); } else if (window.ActiveXObject) { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e2) { return null; } } } else { return null; } } </script> </body> </html> |
動作確認は こちらからどうぞ。