JavaScriptでゲームを作る!
それでは処理を見ていきます。
先ずはファイル構成です。
ファイルとそれを入れるフォルダーを以下のような構成します。
コード全体で行おうとしていることは以下の通りです。
これにより、スプライトクラス の描画処理が繰り返し実行され、移動し続ける画像を表示できるようになります。
次は各ファイル毎に解説していきます。
尚、「imageManager.js」については、前回の「画像を表示する方法」から変更がないので割愛します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=660,user-scalable=no,shrink-to-fit=yes">
<title>画像を移動する方法</title>
</head>
<body>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
canvas {
display: block;
margin: 20px auto;
border: 1px solid #ccc;
}
p {
font-size: 18px;
color: #333;
}
</style>
<p>
リフレッシュレート:<span class="リフレッシュレート">0</span>Hz
/フレームレート:<span class="フレームレート">0</span>fps
</p>
<canvas class="レイヤー1" width="640" height="640"></canvas>
<script type="module" src="js/sample.js"></script>
</body>
</html>
このファイルでは、以下のことを行っています。
初心者向けのポイント:
export class レイヤークラス {
constructor(セレクター) {
this.canvas = document.querySelector(セレクター);
this.ctx = this.canvas.getContext('2d');
this.処理リスト = [];
}
描画する() {
this.クリアする();
// ループ中に処理リストの処理が削除されてもいいように逆順でループします
for (let i = this.処理リスト.length - 1; i >= 0; i--) {
this.処理リスト[i].処理();
}
}
クリアする() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
処理を追加する(id, 処理) {
const 既存処理 = this.処理リスト.find(x => x.id == id);
if (既存処理) {
既存処理.処理 = 処理; // 既にある場合は差し替える
return;
}
this.処理リスト.unshift({ id, 処理 });
}
処理を削除する(id) {
this.処理リスト = this.処理リスト.filter(x => x.id != id);
}
}
レイヤークラスは、Canvas要素(キャンバス)で画像や図形を描画するための管理クラスです。
主な機能:
const レイヤー = new レイヤークラス('.レイヤー1');
// 上記はキャンバス(.レイヤー1)を管理する新しいレイヤーを作成します。
export class スプライトクラス {
constructor(レイヤー, 画像要素, x1, y1, x2, y2) {
this.レイヤー = レイヤー;
this.ctx = レイヤー.ctx;
this.画像要素 = 画像要素;
this.レイヤー.処理を追加する(this, this.描画する.bind(this));
this.座標を設定する(x1, y1, x2, y2);
}
座標を設定する(x1, y1, x2, y2) {
const 画像要素 = this.画像要素;
const 表示幅 = x2 - x1;
const 表示高さ = y2 - y1;
const 画像縦横比 = 画像要素.width / 画像要素.height;
const 表示縦横比 = 表示幅 / 表示高さ;
let 描画幅, 描画高さ, 位置調整X, 位置調整Y;
if (画像縦横比 > 表示縦横比) {
描画幅 = 表示幅;
描画高さ = 表示幅 / 画像縦横比;
位置調整X = 0;
位置調整Y = (表示高さ - 描画高さ) / 2; // 垂直中央揃え
} else {
描画高さ = 表示高さ;
描画幅 = 表示高さ * 画像縦横比;
位置調整X = (表示幅 - 描画幅) / 2; // 水平中央揃え
位置調整Y = 0;
}
this.x = x1 + 位置調整X;
this.y = y1 + 位置調整Y;
this.幅 = 描画幅;
this.高さ = 描画高さ;
}
描画する() {
this.ctx.drawImage(this.画像要素, this.x, this.y, this.幅, this.高さ); // 画像を描画
}
移動して描画する(移動幅x, 移動幅y, 停止位置x, 停止位置y) {
return new Promise(resolve => {
this.レイヤー.処理を追加する(this, () => {
if (移動幅x) this.x += 移動幅x;
if (移動幅y) this.y += 移動幅y;
this.描画する();
let 停止フラグ = false;
if (移動幅x) {
if (移動幅x > 0) {
if (this.x >= 停止位置x) 停止フラグ = true;
} else if (移動幅x < 0) {
if (this.x <= 停止位置x) 停止フラグ = true;
}
}
if (移動幅y) {
if (移動幅y > 0) {
if (this.y >= 停止位置y) 停止フラグ = true;
} else if (移動幅y < 0) {
if (this.y <= 停止位置y) 停止フラグ = true;
}
}
if (!停止フラグ) return;
// 停止位置に来たら描画だけの処理に切替える
this.レイヤー.処理を追加する(this, this.描画する.bind(this));
resolve();
});
})
}
}
スプライトクラスは、画面上の画像(スプライト)を管理するためのクラスです。たとえば、キャラクターや背景画像を表示したり動かしたりする処理を提供します。
主な機能:
const 人物01 = new スプライトクラス(レイヤー, 画像, x1, y1, x2, y2);
// 人物01というスプライトを作成し、レイヤー上に表示します。
export class ループ {
static 初期化する(フレームレート) {
ループ.処理リスト = [];
ループ.リフレッシュ時刻;
ループ.処理時刻;
ループ.リフレッシュカウンタ = 0;
ループ.処理カウンタ = 0;
ループ.フレームレート = フレームレート | 1000 / 60;
ループ.リフレッシュレート要素 = document.querySelector(".リフレッシュレート");
ループ.フレームレート要素 = document.querySelector(".フレームレート");
}
static 開始する() {
ループ.処理時刻 = ループ.リフレッシュ時刻 = Date.now();
ループ.する();
}
static する() {
ループ.処理を実行する();
requestAnimationFrame(ループ.する);
}
static 処理を実行する() {
const 現時刻 = Date.now();
if (現時刻 - ループ.リフレッシュ時刻 < 1000) {
ループ.リフレッシュカウンタ++;
} else {
ループ.リフレッシュ時刻 = 現時刻;
ループ.リフレッシュレート要素.innerHTML = ループ.リフレッシュカウンタ;
ループ.フレームレート要素.innerHTML = ループ.処理カウンタ;
ループ.リフレッシュカウンタ = 0;
ループ.処理カウンタ = 0;
}
if (現時刻 - ループ.処理時刻 < ループ.フレームレート) return;
ループ.処理時刻 += ループ.フレームレート;
ループ.処理カウンタ++;
// ループ中に処理リストの処理が削除されてもいいように逆順でループします
for (let i = ループ.処理リスト.length - 1; i >= 0; i--) {
ループ.処理リスト[i].処理();
}
}
static 処理を追加する(id, 処理) {
const 既存処理 = ループ.処理リスト.find(x => x.id == id);
if (既存処理) {
既存処理.処理 = 処理; // 既にある場合は差し替える
return;
}
ループ.処理リスト.push({ id, 処理 });
}
static 処理を削除する(id) {
ループ.処理リスト = ループ.処理リスト.filter(x => x.id != id);
}
}
ループクラスは、アニメーションを管理するためのメインループを提供します。アニメーションとは、画像を動かしたり、複数の処理を一定の時間間隔で繰り返すことを指します。
主な機能:
ループ.初期化する();
ループ.開始する();
ループ.処理を追加する(レイヤー, レイヤー.描画する.bind(レイヤー));
// アニメーションを開始し、レイヤーの描画をループ内で実行します。
import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { スプライトクラス } from './sprite.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート
class サンプル {
static async main() {
const レイヤー = new レイヤークラス('canvas.レイヤー1');
const 画像管理 = new 画像管理クラス();
ループ.初期化する(1000/60);
ループ.開始する();
// 事前に読み込む画像の配列
const 画像情報リスト = [
{ id: '草原', src: 'img/bg01.jpg' },
{ id: '人物01', src: 'img/chara01.png' },
{ id: '炎竜', src: 'img/mon01.png' },
{ id: 'デビル', src: 'img/mon02.png' }
];
// ページ初期化時に画像をプリロード
if (!await 画像管理.画像を読み込む(画像情報リスト)) return;
// 画像の描画位置を設定
const 背景01 = new スプライトクラス(レイヤー, 画像管理.取得する('草原'), 0, 0, 640, 640);
const 人物01 = new スプライトクラス(レイヤー, 画像管理.取得する('人物01'), 0, 100, 640, 640);
const 人物01正位置 = { x: 人物01.x, y: 人物01.y };
人物01.座標を設定する(640, 100, 1280, 640); //初期表示位置を設定する
const モンスター01 = new スプライトクラス(レイヤー, 画像管理.取得する('炎竜'), 0, 0, 640, 540);
const モンスター01正位置 = { x: モンスター01.x, y: モンスター01.y };
モンスター01.座標を設定する(-640, 0, 0, 540); //初期表示位置を設定する
const モンスター02 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 0, 320, 320, 640);
const モンスター02正位置 = { x: モンスター02.x, y: モンスター02.y };
モンスター02.座標を設定する(640, -640, 960, 320); //初期表示位置を設定する
const モンスター03 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 320, 320, 640, 640);
const モンスター03正位置 = { x: モンスター03.x, y: モンスター03.y };
モンスター03.座標を設定する(-320, -640, 0, 320); //初期表示位置を設定する
ループ.処理を追加する(レイヤー, レイヤー.描画する.bind(レイヤー));
let プロミスリスト;
while (1) {
await 人物01.移動して描画する(-40, 0, 人物01正位置.x, 0);
await サンプル.一時停止(2000);
プロミスリスト = [];
プロミスリスト.push(人物01.移動して描画する(40, 0, 640, 0));
プロミスリスト.push(モンスター01.移動して描画する(5, 0, モンスター01正位置.x, 0));
プロミスリスト.push(モンスター02.移動して描画する(-20, 20, モンスター02正位置.x, モンスター02正位置.y));
プロミスリスト.push(モンスター03.移動して描画する(10, 10, モンスター03正位置.x, モンスター03正位置.y));
await Promise.all(プロミスリスト);
await サンプル.一時停止(2000);
プロミスリスト = [];
プロミスリスト.push(モンスター01.移動して描画する(0, 10, 0, 640));
プロミスリスト.push(モンスター02.移動して描画する(0, 5, 0, 640));
プロミスリスト.push(モンスター03.移動して描画する(0, 20, 0, 640));
await Promise.all(プロミスリスト);
await サンプル.一時停止(2000);
モンスター01.座標を設定する(-640, 0, 0, 540); //初期表示位置を設定する
モンスター02.座標を設定する(640, -640, 960, 320); //初期表示位置を設定する
モンスター03.座標を設定する(-320, -640, 0, 320); //初期表示位置を設定する
}
}
static 一時停止(時間) {
return new Promise(resolve => setTimeout(resolve, 時間));
}
}
addEventListener('load', サンプル.main);
サンプルクラスは、これらのクラスを組み合わせて具体的な処理(ゲームやアニメーション)を実現する部分です。
主な処理:
await サンプル.一時停止(2000);
// 2000ミリ秒(2秒)待ってから次の処理を実行します。