JavaScriptでドラゴンクエストのようなRPGをつくる(1)の続きです。
MapFieldクラスの定義
前回はフィールドを描画するためのクラスの基底クラスを定義しました。今回はこれを継承してフィールドを描画するためのMapFieldクラスを定義します。
コンストラクタを示します。引数の文字列が渡されたら二次元配列に分解してArrMapに格納します。そして配列の縦横のサイズをRowMaxとColMaxに格納します。
1 2 3 4 5 6 |
class MapField extends BaseMap { constructor(mapText){ super(mapText); this.Init(); } } |
Init関数はプレイヤーの初期位置をセットするためのものです。
1 2 3 4 5 6 |
class MapField extends BaseMap { Init(){ this.PlayerX = 12 * blockSize; this.PlayerY = 23 * blockSize; } } |
Move関数は基底クラスで定義したものを使うのですが、城の出入りの処理があるので、その部分は独自に定義したものを使います。
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 |
class MapField extends BaseMap { Move(direct){ let playerCol = this.PlayerX / blockSize; let playerRow = this.PlayerY / blockSize; // 自分の城に入る if(direct == 'up' && 11 <= playerCol && playerCol <= 13 && playerRow == 23){ // 自分の城に入るときはシーンをSCENE_BLACK_OUTに変更して、1.5秒後にSCENE_MY_CASTLEに変更する // BGMもいったん止めて再生しなおす scene = SCENE_BLACK_OUT; StopBgm(); setTimeout(()=>{ scene = SCENE_MY_CASTLE; PlayBgm(bgm); }, 1500); return; } // 魔王の城に入る if(direct == 'up' && 17 <= playerCol && playerCol <= 19 && playerRow == 21){ scene = SCENE_BLACK_OUT; StopBgm(); setTimeout(()=>{ scene = SCENE_ENEMY_CASTLE; PlayBgm(bgm); }, 1500); return; } super.Move(direct); // ある確率でザコ敵と遭遇して戦闘になる let r = Math.random(); if(r < appearanceRate) { scene = SCENE_ZAKO_BATTLE ; enemyHP = enemyMaxHP; messageOnBattle = '敵に襲われた!'; showCommandButtons = true; PlayBgm(bossBgm); StopPlayer(); } } } |
StopPlayer関数はプレイヤーの動きを止めるためのものです。これはメンバ関数ではありません。
1 2 3 4 5 6 |
function StopPlayer(){ moveLeft = false; moveRight = false; moveUp = false; moveDown = false; } |
PlayBgm関数はBGMを再生し、StopBgm関数はBGMを停止します。
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 |
let setIntervalId = null; function PlayBgm(music){ // 再生中のBGMがあるときは停止する StopBgm(); // 引数で渡されたBGMを再生する music.currentTime = 0; music.play(); // 0.1秒ごとにどこまで再生されているか調べる // 終わりのほうまで再生したら最初から再生する setIntervalId = setInterval(()=>{ if(music == bgm && music.currentTime >= 58){ // このmp3ファイルは58秒で1サイクルとする music.currentTime = 0; music.play(); } if(music == battleBgm && music.currentTime >= 93){ music.currentTime = 0; music.play(); } }, 100) } function StopBgm(){ if(setIntervalId != null) clearInterval(setIntervalId); setIntervalId = null; bgm.pause(); bgm.currentTime = 0; battleBgm.pause(); battleBgm.currentTime = 0; } |
MyCastleクラスの定義
プレイヤーが自分の城にいるときの移動と描画処理をおこなうためのMyCastleクラスを定義します。
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class MyCastle extends BaseMap { constructor(mapText){ super(mapText); this.talking = false; this.Init(); } Init(){ this.PlayerX = blockSize * 16; this.PlayerY = blockSize * 15; } } |
Move関数は基底クラスで定義したものを使うのですが、城の出入りと王に話しかけたときの処理があるので、その部分は独自に定義したものを使います。
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 |
class MyCastle extends BaseMap { Move(direct){ // ゲーム開始前とメッセージが表示されているときは移動処理をおこなわない if(!started || message != '') return; // 城からフィールドに出る if(direct == 'down' && this.PlayerY >= blockSize * 22){ this.PlayerX = blockSize * 16; this.PlayerY = blockSize * 22; scene = SCENE_BLACK_OUT; StopBgm(); setTimeout(()=>{ scene = SCENE_FIELD; PlayBgm(bgm); }, 1500); return; } let col = Math.round(this.PlayerX / blockSize); if(direct == 'up' && this.PlayerY == blockSize * 14 && col == 16){ this.QueenTalk(); return; } super.Move(direct); } } |
王女様に話しかけるとメッセージが表示されて、HPとMPが回復します。処理が二重に開始されないようにtalkingフラグで制御しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class MyCastle extends BaseMap { QueenTalk(){ StopPlayer(); if(this.talking) return; this.talking = true; let talkSpeed = 2000; message = `${playerName}さまの健闘をお祈りします。`; setTimeout(()=>{ message = `${playerName}の体力が全回復した。`; playerHP = playerMaxHP; playerMP = playerMaxMP; }, talkSpeed); setTimeout(()=>{ message = ''; this.talking = false; }, talkSpeed * 2); } } |
EnemyCastleクラスの定義
プレイヤーが魔王の城にいるときの移動と描画処理をおこなうためのEnemyCastleクラスを定義します。
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class EnemyCastle extends BaseMap { constructor(mapText){ super(mapText); this.Talking = false; this.Init(); } Init(){ this.PlayerX = blockSize * 16; this.PlayerY = blockSize * 22; } } |
Move関数は基底クラスで定義したものを使うのですが、城の出入りと魔王に話しかけたときの処理があるので、その部分は独自に定義したものを使います。
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 |
class EnemyCastle extends BaseMap { Move(direct){ if(message != '') return; // 城から出る if(direct == 'down' && this.PlayerY >= blockSize * 22){ this.PlayerX = blockSize * 16; this.PlayerY = blockSize * 22; scene = SCENE_BLACK_OUT; StopBgm(); setTimeout(()=>{ scene = SCENE_FIELD; PlayBgm(bgm); }, 1500); return; } // 魔王に話しかける let col = Math.round(this.PlayerX / blockSize); if(direct == 'up' && this.PlayerY == blockSize * 14 && col == 16){ this.BossTalk(); return; } super.Move(direct); } } |
魔王に話しかけるとメッセージが表示され、戦闘になります。処理が二重に開始されないようにtalkingフラグで制御しています。
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 |
class EnemyCastle extends BaseMap { BossTalk(){ StopPlayer(); if(this.Talking) return; this.Talking = true; StopBgm(); let talkSpeed = 1000; message = "よく来たな。\nここがおまえの墓場だ。4ね。"; setTimeout(()=>{ message = ''; bossHP = bossMaxHP; scene = SCENE_BOSS_BATTLE; PlayBgm(bossBgm); messageOnBattle = '魔王が出現した!'; showCommandButtons = true; this.Talking = false; }, talkSpeed); } } |
これらのクラスからインスタンスを生成します。自分の城と魔王の城は構造は同じで王座にいるキャラクタが違うだけなのでその部分だけ置換します。
1 2 3 4 5 |
let mapMyCastle = new MyCastle(mapCastleText); let mapField = new MapField(mapFieldText); // 自分の城と魔王の城は構造は同じ。王座にいるキャラクタが違うだけなので置換する let mapEnemyCastle = new EnemyCastle(mapCastleText.replace('王', '敵')); |
そしてプレイヤーを初期座標にセットします。
1 2 3 4 5 |
function InitPlayerPosition(){ mapMyCastle.Init(); mapField.Init(); mapEnemyCastle.Init(); } |
描画の処理
描画処理をするためのDraw関数を示します。プレイヤーがフィールド上や城にいるときは定義したクラスの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 |
function Draw(){ if(ctx == null) return; if(playerMP < 3){ $magic1.disabled = "disabled"; $magic2.disabled = "disabled"; } else { $magic1.disabled = null; $magic2.disabled = null; } ctx.clearRect(0,0,can.width,can.height); if(scene == SCENE_BLACK_OUT){ ctx.fillStyle = '#000'; ctx.fillRect(0,0,can.width,can.height); ShowMoveButtons(false); ShowBattleButtons(false); } if(scene == SCENE_FIELD) mapField.Draw(); if(scene == SCENE_ZAKO_BATTLE) DrawZakoBattleScene(); if(scene == SCENE_MY_CASTLE) mapMyCastle.Draw(); if(scene == SCENE_ENEMY_CASTLE) mapEnemyCastle.Draw(); if(scene == SCENE_BOSS_BATTLE) DrawBossBattleScene(); } |
ザコ敵とボスとの戦闘時の描画処理を示します。
戦闘中は移動用のボタンは非表示とします。コマンド選択用のボタンは必要であるとき(showCommandButtonsフラグがtrueのとき)のみ表示させます。
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 |
function DrawZakoBattleScene(){ if(ctx == null) return; ctx.fillStyle = '#000'; ctx.fillRect(0,0,can.width,can.height); ctx.drawImage(enemyImage, (displayWidth - blockSize) / 2, (displayHeight - blockSize) / 2 - 50); DrawBattleScene(); } function DrawBossBattleScene(){ if(ctx == null) return; ctx.fillStyle = '#000'; ctx.fillRect(0,0,can.width,can.height); ctx.drawImage(bossImage, (displayWidth - bossWidth) / 2, (displayHeight - bossHeight) / 2 - 50); DrawBattleScene(); } function DrawBattleScene(){ ctx.fillStyle = '#fff'; ctx.font = "16px MS ゴシック"; // 戦闘時に表示させるメッセージがあるときは表示する if(messageOnBattle != ''){ let texts = messageOnBattle.split('\n'); ctx.fillText(texts[0], 20, 280); if(texts.length > 1) ctx.fillText(texts[1], 20, 310); } ShowMoveButtons(false); ShowBattleButtons(showCommandButtons); DrawHPMP(); } |
移動処理
移動に関する処理を示します。キー操作だけでなくボタンが押されているときも移動処理をおこないます。
まず移動用のボタンに’touchstart’、’touchend’、’mousedown’、’mouseup’のイベントリスナを追加します。押されたらフラグをセットして離されたらクリアしているだけです。
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 |
$left?.addEventListener('touchstart', () => { moveLeft = true; }); $left?.addEventListener('touchend', () => { moveLeft = false; }); $left?.addEventListener('mousedown', () => { moveLeft = true; }); $left?.addEventListener('mouseup', () => { moveLeft = false; }); $right?.addEventListener('touchstart', () => { moveRight = true; }); $right?.addEventListener('touchend', () => { moveRight = false; }); $right?.addEventListener('mousedown', () => { moveRight = true; }); $right?.addEventListener('mouseup', () => { moveRight = false; }); $up?.addEventListener('touchstart', () => { moveUp = true; }); $up?.addEventListener('touchend', () => { moveUp = false; }); $up?.addEventListener('mousedown', () => { moveUp = true; }); $up?.addEventListener('mouseup', () => { moveUp = false; }); $down?.addEventListener('touchstart', () => { moveDown = true; }); $down?.addEventListener('touchend', () => { moveDown = false; }); $down?.addEventListener('mousedown', () => { moveDown = true; }); $down?.addEventListener('mouseup', () => { moveDown = false; }); let $main = document.getElementById('main'); $main?.addEventListener('mouseup', () => { moveLeft = false; moveRight = false; moveUp = false; moveDown = false; }); // PCのキー操作にも対応させる document.onkeydown = function(e) { if(e.keyCode == 37) Move('left'); if(e.keyCode == 38) Move('up'); if(e.keyCode == 39) Move('right'); if(e.keyCode == 40) Move('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 |
setInterval(() => { if(moveLeft) Move('left'); if(moveRight) Move('right'); if(moveUp) Move('up'); if(moveDown) Move('down'); Draw(); }, 33); function Move(direct){ if( cleared) // ゲームクリア時は移動処理はおこなわない return; if(scene == SCENE_FIELD) mapField.Move(direct); if(scene == SCENE_MY_CASTLE) mapMyCastle.Move(direct); if(scene == SCENE_ENEMY_CASTLE) mapEnemyCastle.Move(direct); } |