JavaScriptでつくるレトロなゲーム 万引少年。このページではオープニングと店内における処理を実装します。
⇒ 動作確認はこちら
Openingクラス
では最初にオープニングの処理をするOpeningクラスを示します。といっても動きがないので簡単な処理しかしていません。
opening.js
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 |
class Opening{ constructor(fieldElement){ this.FieldElement = fieldElement; this.OpeningFieldString = "" + " \n" + " \n" + " \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ セ ○ ン イ レ ブ ン ■■■ \n" + " ■■■■■■■■■■■■■■■■■■■■ \n" + " ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " ■ ┏━━┓ ┏━━┓ ┏━━┓ ■■■ \n" + " アイテテ ヨカッタ! ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " ○ ○ ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " (□\/□ ■ ┃ ┃ ┃ ┃ ┃ ┃ ■■■ \n" + " || || ■ ┗━━┛ ┗━━┛ ┗━━┛ ■■■ \n" + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n" + " \n" + " PUSH S KEY GAME START! \n" + " \n"; } Show(){ this.FieldElement.innerHTML = this.OpeningFieldString; } } |
Storeクラス
次にメインのStoreクラスを示します。が、その前に座標を管理するためのPositionクラスを示します。
store.js
1 2 3 4 5 6 |
class Position{ constructor(x, y){ this.X = x; this.Y = y; } } |
初期化
ではStoreクラスのコンストラクタを示します。
store.js
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 |
const ColumMax = 41; // StoreFieldの一行の幅(改行含む) class Store{ constructor(fieldElement){ this.StoreFieldString = "" + "┳┳━━━━┳━━┳━━━━┳━━┳━━━━┳━━┳━━━━┳━━┳━━━━┳┳\n" + "┗┛ ┗━━┛ ┗━━┛ ┗━━┛ ┗━━┛ ┗┛\n" + " \n" + " \n" + " \n" + "━┓ ┏━\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + "┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫┣■ ■┫\n" + " \n"; this.FieldElement = fieldElement; // 店内の商品(少年にとって左側か右側か?) this.LeftItems = []; this.RightItems = []; // 制限時間 0になったらどこにいても警備員につかまる this.RemainingTime = 300; // スコア this.Score = 0; // スコアと制限時間を表示するための文字列 this.ScoreInfo = ''; // 警備員は右に移動するかどうか this.MoveRights = []; // 警備員が左右に移動するときのX座標(ピクセル単位ではなく文字ベース) this.PositionXK = 20; this.LastPositionXK = 14; // 警備員を右向きで表示するか左向きで表示するかの判断で前回のX座標が必要 // 警備員が少年を捕まえにくるときの初期のY座標 this.PositionYK = 2; // 少年のXY座標 this.PositionBoyX = 14; this.PositionBoyY = 12; // 少年が捕まってからの経過時間をカウントする(その後、警察署に移動) this.KoraUpdateCount = 0; // 店内の状態を初期化 this.InitStore(); } ] |
ゲームを開始するときにInitGame関数が実行されます。スコアを0に戻して店内を初期化します。
1 2 3 4 5 6 |
class Store{ InitGame(){ this.Score = 0; this.InitStore(); } } |
InitStore関数は店内を初期化するための処理をおこないます。
制限時間を300秒にセット、警備員と少年の座標を初期位置に戻します。そして商品を並べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Store{ InitStore(){ this.KoraUpdateCount = 0; this.RemainingTime = 300; this.PositionXK = 20; this.PositionYK = 2; this.LastPositionXK = 20; this.MoveRights = []; this.PositionBoyX = 14; this.PositionBoyY = 12; this.InitItems(); } } |
InitItems関数は商品を初期の位置に並べます。
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 |
class Store{ InitItems() { // 前回ぶんのデータが入っているので最初にクリアする this.LeftItems = []; this.RightItems = []; for (let y=6; y <= 21; y++) this.LeftItems.push(new Position(1, y)); for (let y = 6; y <= 11; y++) this.LeftItems.push(new Position(9, y)); for (let y = 15; y <= 21; y++) this.LeftItems.push(new Position(9, y)); for (let y = 6; y <= 11; y++) this.LeftItems.push(new Position(17, y)); for (let y = 15; y <= 21; y++) this.LeftItems.push(new Position(17, y)); for (let y = 6; y <= 11; y++) this.LeftItems.push(new Position(25, y)); for (let y = 15; y <= 21; y++) this.LeftItems.push(new Position(25, y)); for (let y = 6; y <= 11; y++) this.LeftItems.push(new Position(33, y)); for (let y = 15; y <= 21; y++) this.LeftItems.push(new Position(33, y)); for (let y = 6; y <= 11; y++) this.RightItems.push(new Position(6, y)); for (let y = 15; y <= 21; y++) this.RightItems.push(new Position(6, y)); for (let y = 6; y <= 11; y++) this.RightItems.push(new Position(14, y)); for (let y = 15; y <= 21; y++) this.RightItems.push(new Position(14, y)); for (let y = 6; y <= 11; y++) this.RightItems.push(new Position(22, y)); for (let y = 15; y <= 21; y++) this.RightItems.push(new Position(22, y)); for (let y = 6; y <= 11; y++) this.RightItems.push(new Position(30, y)); for (let y = 15; y <= 21; y++) this.RightItems.push(new Position(30, y)); for (let y = 6; y <= 21; y++) this.RightItems.push(new Position(38, y)); } } |
警備員を移動させる
タイマーイベントが発生したら警備員を移動させます。どちらもMoveK関数が呼び出されます。GameStatusを調べて左右に移動するのか下に移動するのかを切り分けます。
1 2 3 4 5 6 7 8 9 10 11 |
class Store{ // 少年が万引き中、左右に移動させる Timer500ms(){ this.MoveK(); } // 少年を見つけて下に移動させる Timer150ms(){ this.MoveK(); } } |
MoveK関数は警備員を移動させます。
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 |
class Store{ MoveK() { // リストのなかが空であれば乱数でリストに格納するbool変数を生成する if (this.MoveRights.length == 0) { let r1 = Math.floor(Math.random() * 2); let r2 = Math.floor(Math.random() * 5) + 1; for (let i = 0; i < r2; i++) { let isRight = r1 == 1 ? true : false; this.MoveRights.push(isRight); if (!isRight && this.PositionXK - i < 2) break; if (isRight && this.PositionXK + i >= ColumMax - 4) break; } } // リストのなかから最初の値をひとつだけ取り出す let moveRight = this.MoveRights[0]; this.MoveRights.splice(0, 1); // 取り出した値によって警備員を左右に移動させるが、端の場合は移動できる方向に移動させる if (!(this.PositionXK < ColumMax - 4)) // 右端に来ているので左にしか移動できない this.KMoveLeft(); else if (this.PositionXK <= 0) // 左端に来ているので右にしか移動できない this.KMoveRight(); else if (moveRight) // 右に移動できるので移動する this.KMoveRight(); else // moveRightがfalseであれば左に移動する this.KMoveLeft(); // 少年のX座標がdangerXのどれかの場合、見つかる可能性があるし、 // 警備員のX座標がdangerXのどれかの場合、万引きを摘発できる可能性がある。 let dangerX = [ 1, 2, 3, 4, 9, 10, 11, 12, 17, 18, 19, 20, 25, 26, 27, 28, 33, 34, 35, 36 ]; let danger = dangerX.filter(x => x == this.PositionBoyX); let find = dangerX.filter(x => x == this.PositionXK); // 両方の条件をみたしている場合で両者のX座標の差の絶対値が4未満のときは // 万引きが摘発されたことになる。この場合はGAME_STATUS_CATCH(警備員が少年を捕まえる)に移行する if (danger != null && danger.length != 0 && find != null && find.length != 0 && Math.abs(this.PositionXK -this. PositionBoyX) < 4){ // GameStatusがGAME_STATUS_STEALからGAME_STATUS_CATCHに変更されるとき // 警備員が少年に歩み寄る効果音を鳴らす if(GameStatus == GAME_STATUS_STEAL){ let music = new Audio('catch.mp3'); music.play(); } GameStatus = GAME_STATUS_CATCH; } // GAME_STATUS_STEALのときは0.5秒おきにイベントが発生するので残り時間を0.5秒減らす if(GameStatus == GAME_STATUS_STEAL) this.RemainingTime -= 0.5; if(this.RemainingTime <= 0){ // 時間切れ 少年を見つかる位置に移動させる this.PositionBoyX = 18; this.PositionBoyY = 16; if(GameStatus == GAME_STATUS_STEAL){ let music = new Audio('catch.mp3'); music.play(); // 再生 } GameStatus = GAME_STATUS_CATCH; } this.ReDraw(); } } |
警備員を左右に移動させる処理です。端にいるときは画面外に移動しないようにチェックしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Store{ KMoveRight() { if (this.PositionXK < ColumMax - 4){ // 移動前のX座標を保存しておく this.LastPositionXK = this.PositionXK; this.PositionXK++; } } KMoveLeft() { if (this.PositionXK > 0){ this.LastPositionXK = this.PositionXK; this.PositionXK--; } } } |
少年を移動させる処理
少年を移動させる処理を示します。キーが押されたらその方向に移動可能であれば移動させます。キーを押しっぱなしで連続移動できないようにメンバ変数keyDownを使って制御しています。またGameStatusがGAME_STATUS_STEALでない場合は処理はおこわれません。
左右に移動できない場合はそこに商品が置かれているかもしれません。その場合は盗めるかを確認します。
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 |
class Store{ OnKeyUp(e){ if(GameStatus != GAME_STATUS_STEAL) return; this.keyDown = false; } OnKeyDown(e) { if(GameStatus != GAME_STATUS_STEAL) return; if(this.keyDown == true) return; this.keyDown = true; // 左 if(e.keyCode == 37){ if(this.CanBoyMove(this.PositionBoyX - 1, this.PositionBoyY)){ this.PositionBoyX--; this.ReDraw(); // 少年の位置が移動した場合は全体を再描画する(後述) } else this.SteelLeftItemIfCan(); // 少年の左側に盗める商品があったら盗む } // 右 if(e.keyCode == 39){ if(this.CanBoyMove(this.PositionBoyX + 1, this.PositionBoyY)){ this.PositionBoyX++; this.ReDraw(); } else this.SteelRightItemIfCan(); } // 上 if(e.keyCode == 38){ if (this.CanBoyMove(this.PositionBoyX, this.PositionBoyY - 1)){ this.PositionBoyY--; this.ReDraw(); } } // 下 if(e.keyCode == 40){ if (this.CanBoyMove(this.PositionBoyX, this.PositionBoyY + 1)){ this.PositionBoyY++; this.ReDraw(); } } } } |
CanBoyMove関数は少年が引数として渡された座標に移動できるかを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Store{ CanBoyMove(newPositionBoyX, newPositionBoyY) { if ( newPositionBoyX == 2 || newPositionBoyX == 3 || newPositionBoyX == 10 || newPositionBoyX == 11 || newPositionBoyX == 18 || newPositionBoyX == 19 || newPositionBoyX == 26 || newPositionBoyX == 27 || newPositionBoyX == 34 || newPositionBoyX == 35) { if (newPositionBoyY >= 5 && newPositionBoyY <= 20) return true; else return false; } if (newPositionBoyY == 12) { if (newPositionBoyX >= 2 && newPositionBoyX <= 35) return true; else return false; } return false; } } |
SteelRightItemIfCan関数とSteelLeftItemIfCan関数は、商品が左右にあるかどうかを調べて存在する場合は盗む処理をおこないます。少年の手の動きを表現するため、盗んだときは0.1秒のあいだメンバ変数のStealRightSucceededとStealLeftSucceededをtrueにしています。そのあと再描画をおこない、0.1秒後に手を元の位置に戻して再描画しています。
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 |
class Store{ SteelRightItemIfCan(){ // 盗める商品が存在するか調べる for(let i = 0; i < this.RightItems.length; i++){ if(this.RightItems[i].X == this.PositionBoyX +3 && this.RightItems[i].Y == this.PositionBoyY+1){ this.RightItems.splice(i, 1); // 盗んだ商品を配列から取り除く this.StealRightSucceeded = true; this.StealSucceeded(); // 盗んだあとの処理 this.ReDraw(); setTimeout(function(obj){ obj.StealRightSucceeded = false; obj.ReDraw(); }, 100, this); break; // 盗める商品はひとつしかないのでループを抜ける } } } SteelLeftItemIfCan(){ for(let i = 0; i < this.LeftItems.length; i++){ if(this.LeftItems[i].X == this.PositionBoyX -1 && this.LeftItems[i].Y == this.PositionBoyY+1){ this.LeftItems.splice(i, 1); this.StealLeftSucceeded = true; this.StealSucceeded(); this.ReDraw(); setTimeout(function(obj){ obj.StealLeftSucceeded = false; obj.ReDraw(); }, 100, this); break; } } } } |
StealSucceeded関数は盗んだあとの処理をおこないます。スコア加算と効果音の再生、ステージクリアの判定をおこないます。
1 2 3 4 5 6 7 8 9 10 11 |
class Store{ StealSucceeded(){ this.Score += 10; let music = new Audio('./get.mp3'); music.play(); if(this.LeftItems.length == 0 && this.RightItems.length == 0) this.StageClear(); } } |
盗んだ結果、すべての商品がなくなったらステージクリアです。GameStatusをGAME_STATUS_CLEAREDに変更して残り時間に50を乗じた値をボーナスポイントとして追加します。
1 2 3 4 5 6 |
class Store{ StageClear(){ GameStatus = GAME_STATUS_CLEARED; this.Score += Math.floor(this.RemainingTime) * 50; } } |
描画処理
キャラクタの座標を変更しても描画処理をしなければ意味がありません。ここからは描画処理を解説します。
最初にReplaceString関数を示します。これは第一引数で渡された文字列から第二引数を先頭に第三引数分の文字を削除して第四引数の文字列を挿入します。
1 2 3 4 5 6 7 |
class Store{ ReplaceString(source, begin, deleteCount, replace_str){ var before = source.slice(0, begin); var after = source.slice(begin + deleteCount); return before + replace_str + after; } } |
再描画に必要な文字列を取得して再描画をする処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Store{ ReDraw(){ let str1 = this.GetStringItems(); // 商品が入った文字列を取得 let str2 = this.GetStringDrawK(str1); // 警備員が入った文字列を取得 // 少年が入った文字列を取得 let str3 = this.GetStringDrawBoy(str2, this.StealLeftSucceeded, this.StealRightSucceeded); let str = this.GetScoreString() + str3; if(GameStatus == GAME_STATUS_STEAL || GameStatus == GAME_STATUS_CATCH) this.FieldElement.innerHTML = str; } } |
GetStringItems関数はメンバ変数の文字列 StoreFieldStringに店内に存在する商品を入れた文字列を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Store{ GetStringItems() { let arr = this.StoreFieldString.split('\n'); this.LeftItems.forEach(position => { arr[position.Y] = this.ReplaceString(arr[position.Y], position.X, 1, '$'); }); this.RightItems.forEach(position => { arr[position.Y] = this.ReplaceString(arr[position.Y], position.X, 1, '$'); }); return arr.join('\n'); } } |
GetStringDrawK関数はGetStringItems関数が返した文字列を引数とし、警備員の姿が入った文字列を返します。
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 |
class Store{ GetStringDrawK(source) { let arr = source.split('\n'); if(GameStatus == GAME_STATUS_STEAL){ // this.LastPositionXK - this.PositionXKが正か負かで警備員の方向を決める if (this.LastPositionXK - this.PositionXK > 0){ arr[2] = this.ReplaceString(arr[2], this.PositionXK, 3, '\●〉'); arr[3] = this.ReplaceString(arr[3], this.PositionXK, 3, ' ■ '); arr[4] = this.ReplaceString(arr[4], this.PositionXK, 3, '/〈 '); } else { arr[2] = this.ReplaceString(arr[2], this.PositionXK, 3, '〈●/'); arr[3] = this.ReplaceString(arr[3], this.PositionXK, 3, ' ■ '); arr[4] = this.ReplaceString(arr[4], this.PositionXK, 3, ' 〉\'); } } else if(GameStatus == GAME_STATUS_CATCH){ // 少年をみつけた警備員を下に移動させる let x = this.PositionXK = this.PositionBoyX; let y = this.PositionYK; if(this.KoraUpdateCount == 0){ this.PositionYK++; // 少年の真上まで移動させる。移動後、KoraUpdateCountを0から1に変更 if(y + 5 > this.PositionBoyY){ this.KoraUpdateCount++; } } else { // KoraUpdateCountが0でない場合は12までインクリメント // そのあいだ「コラ!」を表示させる // それ以降はゲームオーバーのシーンへ if(this.KoraUpdateCount < 12){ this.KoraUpdateCount++; arr[y-1] = this.ReplaceString(arr[y], this.PositionXK, 3, 'コラ!'); } else GameStatus = GAME_STATUS_GAMEOVER; } arr[y] = this.ReplaceString(arr[y], x, 3, ' ● '); arr[y+1] = this.ReplaceString(arr[y+1], x, 3, '(■)'); arr[y+2] = this.ReplaceString(arr[y+2], x, 3, ' || '); } return arr.join('\n'); } } |
GetStringDrawBoy関数はGetStringDrawK関数が返した文字列を引数とし、少年の姿が入った文字列を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Store{ GetStringDrawBoy(source, isLeft, isRight) { let arr = source.split('\n'); arr[this.PositionBoyY] = this.ReplaceString(arr[this.PositionBoyY], this.PositionBoyX, 3, ' ○ '); arr[this.PositionBoyY + 2] = this.ReplaceString(arr[this.PositionBoyY + 2], this.PositionBoyX, 3, ' ||? '); let hand = '(■)'; if(isLeft) hand = '/■)'; // 左の商品を盗む動作 else if(isRight) hand = '(■\'; // 右の商品を盗む動作 arr[this.PositionBoyY + 1] = this.ReplaceString(arr[this.PositionBoyY + 1], this.PositionBoyX, 3, hand); return arr.join('\n'); } } |
GetScoreString関数はスコアと残り時間を表示するために必要な文字列を返します。
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 |
class Store{ GetScoreString(){ // スコアは6桁0埋めとする let s1 = '000000' + this.Score; s1 = s1.slice(-6); // レトロ感を出すために全角で表示 s1 = s1.replace(/0/g, '0'); s1 = s1.replace(/1/g, '1'); s1 = s1.replace(/2/g, '2'); s1 = s1.replace(/3/g, '3'); s1 = s1.replace(/4/g, '4'); s1 = s1.replace(/5/g, '5'); s1 = s1.replace(/6/g, '6'); s1 = s1.replace(/7/g, '7'); s1 = s1.replace(/8/g, '8'); s1 = s1.replace(/9/g, '9'); // 残り時間は3桁0埋めとする let s2 = '000' + Math.round(this.RemainingTime); s2 = s2.slice(-3); s2 = s2.replace(/0/g, '0'); s2 = s2.replace(/1/g, '1'); s2 = s2.replace(/2/g, '2'); s2 = s2.replace(/3/g, '3'); s2 = s2.replace(/4/g, '4'); s2 = s2.replace(/5/g, '5'); s2 = s2.replace(/6/g, '6'); s2 = s2.replace(/7/g, '7'); s2 = s2.replace(/8/g, '8'); s2 = s2.replace(/9/g, '9'); return 'SCORE ' + s1 + ' 残り時間 ' + s2 + '\n\n'; } } |