以前 C#でドラゴンクエストのようなRPGをつくりましたが、今回はJavaScriptでつくります。
HTML部分
HTML部分をさきに示します。JavaScriptでcanvasに描画するのですが、ボタンは絶対配置にします。スマホでも遊べるようにボタンで操作できるようにします。シューティングゲームのようなものはスマホには向きませんが、RPGならスマホでも遊べるはずなので対応させます。表示サイズは320×480ピクセル固定です。
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 |
<!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"> <link rel='stylesheet' href='./style.css' type='text/css' media='all' /> </head> <body> <div id = "main"> <canvas id = "canvas"></canvas> <button id = "fight" onclick="NormalAttack()">通常攻撃</button> <button id = "magic1" onclick="Magic1()">攻撃魔法</button> <button id = "magic2" onclick="Magic2()">回復魔法</button> <button id = "up" >上</button> <button id = "down" >下</button> <button id = "left" >左</button> <button id = "right" >右</button> <button id = "start" onclick="Start()">プレイする</button> <button id = "retry" onclick="Retry()">もう一度プレイする</button> </div> <script src='./app.js'></script> </body> </html> |
CSS部分
ボタンを適切な位置に表示させます。
style.css
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 |
#main { position: relative; } #fight { position: absolute; top: 300px; left:20px; width: 280px; height:50px; } #magic1 { position: absolute; left:20px; top: 360px; width: 280px; height:50px; } #magic2 { position: absolute; left:20px; top: 420px; width: 280px; height:50px; } #up { position: absolute; left:100px; top: 300px; width: 120px; height:50px; } #down { position: absolute; left:100px; top: 420px; width: 120px; height:50px; } #left { position: absolute; left:20px; top: 360px; width: 120px; height:50px; } #right { position: absolute; left:180px; top: 360px; width: 120px; height:50px; } #retry { position: absolute; left:80px; top: 420px; width: 160px; height:50px; } #start { position: absolute; left:80px; top: 420px; width: 160px; height:50px; } |
JavaScript部分
JavaScript部分を示します。以下は主なグローバル変数です。他にもありますが、その都度示します。
app.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 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
const displayWidth = 320; // canvasの幅と高さ const displayHeight = 480; let started = false; // ゲームは開始されたか? let cleared = false; // ゲームをクリアしたか? let message = ''; // 城で会話しているときなどに表示されるメッセージ let messageOnBattle = ''; // 戦闘中に表示されるメッセージ let showCommandButtons = false; // 戦闘コマンドを表示させるかどうか? const SCENE_BLACK_OUT = -1; // シーンとシーンの切り替わりの背景が黒一色の状態 const SCENE_MY_CASTLE = 1; // 自分の城 const SCENE_FIELD = 2; // フィールド上 const SCENE_ENEMY_CASTLE = 3; // 魔王の城 const SCENE_ZAKO_BATTLE = 4; // ザコ敵との戦闘シーン const SCENE_BOSS_BATTLE = 5; // 魔王との戦闘シーン let scene = SCENE_MY_CASTLE; // 最初のシーンは自分の城 let appearanceRate = 0.01; // ザコ敵出現率 // 描画処理用のイメージ const blockSize = 32; // プレイヤー let playerImage = new Image(blockSize, blockSize); playerImage.src = './image/player.png'; // 王 let kingImage = new Image(blockSize, blockSize); kingImage.src = './image/king.png'; // 城の床 let floorImage = new Image(blockSize, blockSize); floorImage.src = './image/floor.png'; // 城壁 let wallImage = new Image(blockSize, blockSize); wallImage.src = './image/wall.png'; // 階段 let stairsImage = new Image(blockSize, blockSize); stairsImage.src = './image/stairs.png'; // フィールドに描画される自分の城 let castle1Image = new Image(blockSize * 3, blockSize * 3); castle1Image.src = './image/castle1.png'; // フィールドに描画される魔王の城 let castle2Image = new Image(blockSize * 3, blockSize * 3); castle2Image.src = './image/castle2.png'; // フィールドに描画される草原 let flatImage = new Image(blockSize, blockSize); flatImage.src = './image/flat.png'; // フィールドに描画される橋 let bridgeImage = new Image(blockSize, blockSize); bridgeImage.src = './image/bridge.png'; // フィールドに描画される山 let mountainImage = new Image(blockSize * 2, blockSize * 2); mountainImage.src = './image/mountain.png'; // 魔王の城シーンで描画される魔王 let devilImage = new Image(blockSize, blockSize); devilImage.src = './image/devil.png'; // 戦闘シーンで描画されるザコ敵 let enemyImage = new Image(blockSize, blockSize); enemyImage.src = './image/enemy1.png'; // 戦闘シーンで描画される魔王 let bossWidth = 240; let bossHeight = 120; let bossImage = new Image(bossWidth, bossHeight); bossImage.src = './image/boss.png'; /** @type {HTMLCanvasElement} */ // @ts-ignore let can = document.getElementById('canvas'); can.width = displayWidth; can.height = displayHeight; /** @type {CanvasRenderingContext2D | null | undefined} */ let ctx = can.getContext('2d'); // プレイヤーと敵の状態 const playerName = '勇者'; const magicName = '攻撃魔法'; const initPlayerMaxHP = 30; let playerMaxHP = initPlayerMaxHP; let playerHP = playerMaxHP; const initPlayerMaxMP = 30; let playerMaxMP = initPlayerMaxMP; let playerMP = playerMaxMP; let enemyMaxHP = 10; let enemyHP = enemyMaxHP; let bossMaxHP = 100; let bossHP = bossMaxHP; // ボタン let $up = document.getElementById('up'); let $down = document.getElementById('down'); let $left = document.getElementById('left'); let $right = document.getElementById('right'); let $fight = document.getElementById('fight'); let $magic1 = document.getElementById('magic1'); let $magic2 = document.getElementById('magic2'); let $start = document.getElementById('start'); let $retry = document.getElementById('retry'); if($retry != null) $retry.style.display = 'none'; // プレイヤーの移動速度 let speed = 4; // 移動用のボタンが押され続けているかどうかのフラグ let moveLeft = false; let moveRight = false; let moveUp = false; let moveDown = false; // BGM、効果音関連 const bgm = new Audio('./sound/bgm.mp3'); const battleBgm = new Audio('./sound/battle-bgm.mp3'); const hitSound = new Audio('./sound/attack.mp3'); const damageSound = new Audio('./sound/damage.mp3'); const decisionSound = new Audio('./sound/decision.mp3'); const battleEndSound = new Audio('./sound/battle-end.mp3'); const magicSound = new Audio('./sound/magic.mp3'); |
マップをつくる
マップの元になる文字列を用意しておき、それを利用して山や草原などを描画します。画像は以下を用います。
(圧縮)
~は海、Mと・は山、全角スペースは草原、橋は橋、城は自分の城、魔は魔王がいる城です。
app.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 |
let mapFieldText = "" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~MMMMMM~~~~~~~~~~\n" + "~~~~~~MMMMMM・~~~~~~~~MMM・・・・・MMMM~~~~~~~\n" + "~~~~~MMMM・・・・~~~~~~~~・・・・ MMMM~~~~~~~\n" + "~~~~MMMMM・ 橋橋橋橋橋橋橋橋 MMMMM~~~~~~\n" + "~~~MMMMM・・ M~~~~~~~~MMMM・ MMMMMM~~~~~\n" + "~~~MMMM・・ M~~~~~~~~MMMM・ ・MMMMMM~~~~\n" + "~~~MMMM・ M~~~~~~~MMMMMM・ MMMMMMM~~~\n" + "~~~MM・・・ MMMMMMMMMMMMMMM・ MMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMMMMMM・ MMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMMMMM・・・ MMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMMM・・・・ MMMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMM・ MMMMMMMM~~~\n" + "~~~MM・ MMMMMMM・・・・M・ MMMMMMMM~~~~\n" + "~~~MM・ MMMMMMM・魔○○M・ MMMMMMMMMMMM~~~~\n" + "~~~MM・ ・M・・・・M・○○○M・ ・・MMMMMMMMM~~~~~\n" + "~~~MM・ M・城○○M・○○○M・ MMMMMMMMM~~~~~\n" + "~~~MM・ M・○○○MM・ M・ MMMMMMMM~~~~~~\n" + "~~~MM・ M・○○○MM・ MMM・ MMMMMMMM~~~~~~~\n" + "~~~MM・ M・ MM・ ・・・・ MMM山MMM~~~~~~~~\n" + "~~~MM・ M・ MMMMM~~~~~~~~~~\n" + "~~~MM・ ・・ MMMM~~~~~~~~~~~\n" + "~~~MMM・ MMMMMMMMMMMMMM~~~~~~~~~~~~~\n" + "~~~MMM・ MMMMMMMMMMMM~~~~~~~~~~~~~~~\n" + "~~~MMMMMMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~\n" + "~~~~MMMMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~~\n" + "~~~~~MMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~~~\n" + "~~~~~~・・・・・・・・・・・・・・・・~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; |
城のなかにいるときの描画に使います。床は床、壁は城壁、階は階段です。
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 |
let mapCastleText = "" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " 壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床壁壁壁床床床床床床壁 \n" + " 壁床床床床床床床壁王壁床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁床床床床床床床床床床床床床床床床壁 \n" + " 壁壁壁壁壁壁壁壁床床床壁壁壁壁壁壁壁 \n" + " 壁壁壁壁壁壁壁壁段段段壁壁壁壁壁壁壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 \n" + " 壁段段段壁 "; |
BaseMapクラスの定義
これらの描画やこのなかにいるプレイヤーの座標などはクラスを使っておこないます。フィールドはMapFieldクラス、自分の城はMyCastleクラス、魔王がいる城はEnemyCastleクラスにしますが、これらの基底クラスであるBaseMapクラスを先に示します。
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class BaseMap { constructor(mapText){ this.PlayerX = 0; this.PlayerY = 0; this.ArrMap = []; let arr = mapText.split('\n'); for(let i = 0; i < arr.length; i++){ const arr2 = [...arr[i]]; this.ArrMap.push(arr2); } this.RowMax = this.ArrMap.length; this.ColMax = this.ArrMap[0].length; } } |
プレイヤーが移動するときにおこなわれる処理を示します。CanMove関数でその方向に移動することができるかどうか調べて、移動できるときだけ移動処理をおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 |
class BaseMap { Move(direct){ if(direct == 'left' && this.CanMove('left')) this.PlayerX -= speed; if(direct == 'right' && this.CanMove('right')) this.PlayerX += speed; if(direct == 'up' && this.CanMove('up')) this.PlayerY -= speed; if(direct == 'down' && this.CanMove('down')) this.PlayerY += speed; } } |
CanMove関数は移動できるかどうかを調べてtrueまたはfalseを返します。移動できるのは移動先が床、階段、草原、橋のときだけです(橋は左右にしか移動できないので引数が’up’と’down’のときは調べていない)。
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 |
class BaseMap { CanMove(direct){ if(direct == 'up'){ let playerRow = Math.floor((this.PlayerY - 4) / blockSize); let char1 = this.ArrMap[playerRow][Math.floor(this.PlayerX / blockSize)]; let char2 = this.ArrMap[playerRow][Math.ceil(this.PlayerX / blockSize)]; if(char1 != ' ' && char1 != '段' && char1 != '床') return false; if(char2 != ' ' && char2 != '段' && char2 != '床') return false; return true; } if(direct == 'down'){ let playerRow = Math.ceil((this.PlayerY + 4) / blockSize); let char1 = this.ArrMap[playerRow][Math.floor(this.PlayerX / blockSize)]; let char2 = this.ArrMap[playerRow][Math.ceil(this.PlayerX / blockSize)]; if(char1 != ' ' && char1 != '段' && char1 != '床') return false; if(char2 != ' ' && char2 != '段' && char2 != '床') return false; return true; } if(direct == 'left'){ let playerCol = Math.floor((this.PlayerX - 4) / blockSize); let char1 = this.ArrMap[Math.floor(this.PlayerY / blockSize)][playerCol]; let char2 = this.ArrMap[Math.ceil(this.PlayerY / blockSize)][playerCol]; console.log(char2); if(char1 != ' ' && char1 != '段' && char1 != '床' && char1 != '橋') return false; if(char2 != ' ' && char2 != '段' && char2 != '床' && char2 != '橋') return false; return true; } if(direct == 'right'){ let playerCol = Math.ceil((this.PlayerX + 4) / blockSize); let char1 = this.ArrMap[Math.floor(this.PlayerY / blockSize)][playerCol]; let char2 = this.ArrMap[Math.ceil(this.PlayerY / blockSize)][playerCol]; if(char1 != ' ' && char1 != '段' && char1 != '床' && char1 != '橋') return false; if(char2 != ' ' && char2 != '段' && char2 != '床' && char2 != '橋') return false; return true; } } } |
Draw関数は描画処理をおこないます。メッセージが存在するときはこれを表示します。
山と橋、城はサイズが他のイメージと違ううえにその下に描画しなければならないものがあるので最後に描画します。
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 |
class BaseMap { Draw(){ if(ctx == null) return; ctx.fillStyle = '#ff0'; ctx.fillRect(0,0,can.width,can.height); let shiftX = (displayWidth - blockSize) / 2 - this.PlayerX; let shiftY = (displayHeight - blockSize) / 2 - this.PlayerY; ctx.fillStyle = '#00f'; for(let row = 0; row < this.RowMax; row++){ for(let col = 0; col < this.ColMax; col++){ if(this.ArrMap[row][col] == '壁'){ ctx.fillStyle = '#000'; ctx.fillRect(col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); } if(this.ArrMap[row][col] == ' ' || this.ArrMap[row][col] == '○') ctx.drawImage(flatImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); if(this.ArrMap[row][col] == '段') ctx.drawImage(stairsImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); if(this.ArrMap[row][col] == '壁') ctx.drawImage(wallImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); if(this.ArrMap[row][col] == '王'){ ctx.drawImage(floorImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); ctx.drawImage(kingImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); } if(this.ArrMap[row][col] == '敵'){ ctx.drawImage(floorImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); ctx.drawImage(devilImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); } if(this.ArrMap[row][col] == '床') ctx.drawImage(floorImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); if(this.ArrMap[row][col] == 'M' || this.ArrMap[row][col] == '・'){ if(this.ArrMap[row - 1][col] == '~') ctx.fillRect(col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); else ctx.drawImage(flatImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); } if(this.ArrMap[row][col] == '~') ctx.fillRect(col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); if(this.ArrMap[row][col] == '橋') ctx.fillRect(col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); } } for(let row = 0; row < this.RowMax; row++){ for(let col = 0; col < this.ColMax; col++){ if(this.ArrMap[row][col] == 'M') ctx.drawImage(mountainImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize * 2, blockSize * 2); if(this.ArrMap[row][col] == '城'){ ctx.drawImage(flatImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); ctx.drawImage(castle1Image, col * blockSize + shiftX, row * blockSize + shiftY, blockSize * 3, blockSize * 3); } if(this.ArrMap[row][col] == '魔'){ ctx.drawImage(flatImage, col * blockSize + shiftX, row * blockSize + shiftY, blockSize, blockSize); ctx.drawImage(castle2Image, col * blockSize + shiftX, row * blockSize + shiftY, blockSize * 3, blockSize * 3); } if(this.ArrMap[row][col] == '橋') ctx.drawImage(bridgeImage, col * blockSize + shiftX, row * blockSize + shiftY + 8, blockSize, blockSize); } } ctx.drawImage(playerImage, (displayWidth - blockSize) / 2, (displayHeight - blockSize) / 2); if(message != ''){ ctx.font = "14px MS ゴシック"; ctx.fillStyle = '#000'; ctx.fillRect(20, 340, 280, 80); ctx.fillStyle = '#fff'; let texts = message.split('\n'); ctx.fillText(texts[0], 40, 370); if(texts.length > 1) ctx.fillText(texts[1], 40, 390); } ShowMoveButtons(message == '' && !cleared && started); ShowBattleButtons(false); DrawHPMP(); } } |
最後にShowMoveButtons関数、ShowBattleButtons関数、DrawHPMP関数を呼び出していますが、前二者は移動用のボタン、戦闘時のコマンド選択用のボタンを表示させたりさせないためのものです。DrawHPMP関数は画面の右上にプレイヤーのHPとMPを表示させるためのものです。
ShowMoveButtons関数、ShowBattleButtons関数、DrawHPMP関数を示します。これらはメンバ関数ではありません。
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 |
function ShowMoveButtons(visible){ let display = visible ? 'block' : 'none'; if($up != null) $up.style.display = display; if($down != null) $down.style.display = display; if($left != null) $left.style.display = display; if($right != null) $right.style.display = display; } function ShowBattleButtons(visible){ let display = visible ? 'block' : 'none'; if($fight != null) $fight.style.display = display; if($magic1 != null) $magic1.style.display = display; if($magic2 != null) $magic2.style.display = display; } function DrawHPMP(){ if(ctx == null) return; ctx.fillStyle = '#000'; ctx.fillRect(170, 2, 140, 40); ctx.fillStyle = '#fff'; ctx.font = "18px MS ゴシック"; ctx.fillText(`HP ${playerHP} / ${playerMaxHP}`, 180, 20); ctx.fillText(`MP ${playerMP} / ${playerMaxMP}`, 180, 38); } |
続きは次回。
DQ1 javascript で検索して見つけちゃいました。
楽しいですね。まさかBGMも付いているとは。
確かな技術力に感服しました。
レスポンス御無用です。