JavaScriptでゲームを作る!
以下のようなメッセージウィンドウを作成する方法を記します。
ファイルとそれを入れるフォルダーを以下のような構成します。
※前回と同じファイル名でも内容が変わっている場合があります。
※背景と人物の画像はBing Image Creatorで生成しています(AI生成)。
<!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>
<link href="css/sample.css" rel="stylesheet" />
</head>
<body>
<script type="module" src="js/sample.js"></script>
<aside class="画面更新情報">
<p>
リフレッシュレート:<span class="リフレッシュレート">0</span>Hz
/フレームレート:<span class="フレームレート">0</span>fps
</p>
</aside>
<main class="スクリーン">
<canvas class="レイヤー1" width="640" height="640" aria-label="グラフィックコンテンツ"></canvas>
<article class="メッセージウィンドウ 消去 透明化">
<p class="メッセージエリア">
<span class="メッセージ"></span>
<span class="カーソル 点滅 不可視化">■</span>
</p>
</article>
</main>
</body>
</html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=660,user-scalable=no,shrink-to-fit=yes">
<title>メッセージウィンドウを作成する方法</title>
<link href="css/sample.css" rel="stylesheet" />
</head>
<script type="module" src="js/sample.js"></script>
<aside class="画面更新情報">
<p>
リフレッシュレート:<span class="リフレッシュレート">0</span>Hz
/フレームレート:<span class="フレームレート">0</span>fps
</p>
</aside>
<main class="スクリーン">
<canvas class="レイヤー1" width="640" height="640" aria-label="グラフィックコンテンツ"></canvas>
<article class="メッセージウィンドウ 消去 透明化">
<p class="メッセージエリア">
<span class="メッセージ"></span>
<span class="カーソル 点滅 不可視化">■</span>
</p>
</article>
</main>
<canvas class="レイヤー1" width="640" height="640" aria-label="グラフィックコンテンツ"></canvas>
<article class="メッセージウィンドウ 消去 透明化">
<p class="メッセージエリア">
<span class="メッセージ"></span>
<span class="カーソル 点滅 不可視化">■</span>
</p>
</article>
:root {
--通常文字サイズ: 26px;
--通常文字行高さ: 38px;
--小文字サイズ: 18px;
--小文字行高さ: 24px;
--黒い影: 1px 1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, 2px 0px 1px #000, -2px -0px 1px #000, 0px 2px 1px #000, 0px -2px 1px #000;
--トランジションスピード: 0.8s;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
overscroll-behavior-y: none;
}
.画面更新情報 {
text-align: center;
font-size: var(--小文字サイズ);
line-height: var(--小文字行高さ);
color: #333;
}
.スクリーン {
width: 640px;
height: 640px;
margin: 20px auto;
border: 2px solid #ccc;
position: relative;
background-color: black;
}
canvas {
position: absolute;
}
.不可視化 {
visibility: hidden;
}
.透明化 {
opacity: 0;
}
.消去 {
display: none;
}
.表示 {
display: block;
}
@keyframes 点滅 {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.点滅 {
animation: 点滅 1s ease-out infinite;
}
.メッセージウィンドウ {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
border: solid 3px white;
padding: 12px 17px;
margin: 5px;
transition: opacity var(--トランジションスピード) ease-out;
}
.メッセージウィンドウ .メッセージエリア {
padding: 3px;
margin: 0px;
font-size: var(--通常文字サイズ);
line-height: var(--通常文字行高さ);
color: white;
text-shadow: var(--黒い影);
width: 584px;
height: 154px;
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.メッセージウィンドウ .メッセージエリア::-webkit-scrollbar {
display: none;
}
.メッセージウィンドウ .カーソル {
transition: opacity var(--トランジションスピード) ease-in;
font-size: var(--通常文字サイズ);
display: inline-block;
transform: translateY(-2px);
}
:root {
--通常文字サイズ: 26px;
--通常文字行高さ: 38px;
--小文字サイズ: 18px;
--小文字行高さ: 24px;
--黒い影: 1px 1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, 2px 0px 1px #000, -2px -0px 1px #000, 0px 2px 1px #000, 0px -2px 1px #000;
--トランジションスピード: 0.8s;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
overscroll-behavior-y: none;
}
.画面更新情報 {
text-align: center;
font-size: var(--小文字サイズ);
line-height: var(--小文字行高さ);
color: #333;
}
.スクリーン {
width: 640px;
height: 640px;
margin: 20px auto;
border: 2px solid #ccc;
position: relative;
background-color: black;
}
canvas {
position: absolute;
}
.不可視化 {
visibility: hidden;
}
.透明化 {
opacity: 0;
}
.消去 {
display: none;
}
.表示 {
display: block;
}
@keyframes 点滅 {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.点滅 {
animation: 点滅 1s ease-out infinite;
}
.メッセージウィンドウ {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
border: solid 3px white;
padding: 12px 17px;
margin: 5px;
transition: opacity var(--トランジションスピード) ease-out;
}
.メッセージウィンドウ .メッセージエリア {
padding: 3px;
margin: 0px;
font-size: var(--通常文字サイズ);
line-height: var(--通常文字行高さ);
color: white;
text-shadow: var(--黒い影);
width: 584px;
height: 154px;
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.メッセージウィンドウ .メッセージエリア::-webkit-scrollbar {
display: none;
}
.メッセージウィンドウ .カーソル {
transition: opacity var(--トランジションスピード) ease-in;
font-size: var(--通常文字サイズ);
display: inline-block;
transform: translateY(-2px);
}
このCSSファイルでは、メッセージウィンドウや画面の見た目を細かく調整しています。特徴的なポイントは以下の通り:
Com クラス は共通で使用できる処理をまとめています。
一時停止と時間フォーマットの2つがあります。
export class Com {
static 一時停止(時間) {
return new Promise(resolve => setTimeout(resolve, 時間 * 1000));
}
static シリアル値を時分秒に変換する(シリアル値) {
// シリアル値をDateオブジェクトに変換
const date = new Date(シリアル値);
// 時間、分、秒を取得
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
// フォーマットして返す
return `${hours}時${minutes}分${seconds}秒`;
}
}
役割:指定した時間だけプログラムを停止する機能を提供します。例えば、ゲームやタイマー機能で使われることがあります。
仕組み:
コードの流れ:
役割:シリアル値(数値形式の時刻)を、「〇〇時〇〇分〇〇秒」という形に変換して表示します。
仕組み:
コードの流れ:
この Data クラスは、文字列に埋め込まれた <var>変数名</var> の形式で記述された変数を、事前に登録されたデータ(オブジェクト管理)から値を引き出して置き換えます。
イメージとしては、テンプレートに変数名を埋め込んでおき、実行時にそれを動的に値へ変える仕組みです。
export class Data {
static オブジェクト管理 = {};
static 文字列内の変数を値に変換する(文字列) {
let 変換結果 = 文字列;
const varList = 変換結果.match(/<var>[^<]*<\/var>/g);
if (!varList) return 変換結果; // 変数が無い場合は何もせずにそのまま返す
varList.forEach(varTag => {
const 変数名 = varTag.replace(/<var>([^<]*)<\/var>/, "$1");
const 値 = Data.変数の値を取得する(変数名) || ""; // 値がない場合は空文字を使用
変換結果 = 変換結果.replace(varTag, 値); // <var>・・・</var>を値に置き換える
});
return 変換結果;
}
static 変数の値を取得する(変数名) {
const 名称リスト = 変数名.split(".");
let 値 = Data.オブジェクト管理;
for (const 名称 of 名称リスト) {
値 = 値?.[名称];
if (値 === undefined || 値 === null) return ""; // 値が存在しない場合
}
return 値;
}
}
static オブジェクト管理 = {};
例: もし オブジェクト管理 = { "user": { "name": "プレイヤー", "level": 25 } } となっている場合、user.name の値は "プレイヤー" となります。
役割:
コードの流れ:
const varList = 変換結果.match(/<var>[^<]*<\/var>/g);
この正規表現を使って <var>変数名</var> の形を探し出します。例えば、<var>user.name</var> が該当します。const 変数名 = varTag.replace(/<var>([^<]*)<\/var>/, "$1");
この処理で <var> と </var> を取り除き、純粋な変数名(例: user.name)を取り出します。const 値 = Data.変数の値を取得する(変数名) || "";
変換結果 = 変換結果.replace(varTag, 値);
このメソッドは、「変数名の構造」を辿りながら オブジェクト管理 の中から適切な値を見つけ出します。
コードの流れ:
const 名称リスト = 変数名.split(".");
例えば、user.name なら ["user", "name"] に分解されます。値 = 値?.[名称];
名前リストを順番に参照しながらネストされた構造を探索します。
例:
if (値 === undefined || 値 === null) return "";
例えば、以下のように使われます:
Data.オブジェクト管理 = {
user: { name: "プレイヤー", level: 25 },
system: { time: "12:00" }
};
const result = Data.文字列内の変数を値に変換する(
こんにちは、<var>user.name</var>さん。現在の時刻は<var>system.time</var>です。
);
console.log(result);
// 結果: "こんにちは、プレイヤーさん。現在の時刻は12:00です。"
import { Data } from './data.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート
import { Com } from './com.js'; // クラスをインポート
class 制御情報クラス {
constructor(メッセージ, メッセージエリア要素) {
this.メッセージ = メッセージ;
this.表示位置 = 0;
this.表示メッセージ = "";
this.クリック待ちフラグ = false;
this.表示フレームカウンタ = 0;
this.クリック待ちフレームカウンタ = 0;
this.スクロール高さ = メッセージエリア要素.scrollHeight;
this.一括表示フラグ = false;
}
}
export class メッセージウィンドウクラス {
constructor(スクリーンセレクタ, メッセージウィンドウセレクタ, 出現時間 = 1.0
, 表示フレーム間隔 = 2, クリック待ち時間 = 30) {
this.スクリーン要素 = document.querySelector(スクリーンセレクタ);
this.メッセージウィンドウ要素 = this.スクリーン要素.querySelector(メッセージウィンドウセレクタ);
this.メッセージエリア要素 = this.メッセージウィンドウ要素.querySelector(".メッセージエリア");
this.メッセージ要素 = this.メッセージエリア要素.querySelector(".メッセージ");
this.カーソル要素 = this.メッセージエリア要素.querySelector(".カーソル");
this.出現時間 = 出現時間;
this.表示フレーム間隔 = 表示フレーム間隔;
this.クリック待ちフレーム数 = this.基本クリック待ちフレーム数 = クリック待ち時間 > 0 ? ループ.フレームレート * クリック待ち時間 : 0;
}
メッセージを表示する(メッセージ) {
const 制御情報 = new 制御情報クラス(Data.文字列内の変数を値に変換する(メッセージ), this.メッセージエリア要素);
this.スクリーン要素.onclick = this.メッセージ表示時にクリックする.bind(this, 制御情報);
return new Promise(resolve => {
ループ.処理を追加する(this, () => {
if (this.クリック待ち時の処理を行う(制御情報)) return;
// 表示フレーム間隔でメッセージを表示する
if (0 < 制御情報.表示フレームカウンタ--) return;
制御情報.表示フレームカウンタ = this.表示フレーム間隔;
// 指定されたメッセージを全て表示したら処理を終了する
if (制御情報.表示位置 >= 制御情報.メッセージ.length) {
ループ.処理を削除する(this);
this.スクリーン要素.onclick = null;
resolve();
return;
}
if (this.メッセージを一括で表示する(制御情報)) return;
if (this.制御タグを取得する(制御情報)) return;
this.指定した文字数のメッセージを表示する(制御情報, 1);
});
});
}
クリック待ち時の処理を行う(制御情報) {
if (!制御情報.クリック待ちフラグ) return false;
// クリック待ち時間(クリック待ちフレーム数)が0の場合は無限にクリックを待つ
if (!this.クリック待ちフレーム数) return true;
// クリック待ち時間までクリックを待つ
if (this.クリック待ちフレーム数 > 制御情報.クリック待ちフレームカウンタ++) return true;
制御情報.クリック待ちフレームカウンタ = 0;
this.クリック待ち時にクリックする(制御情報);
return true;
}
メッセージを一括で表示する(制御情報) {
if (!制御情報.一括表示フラグ) return false;
// 制御タグまでの文字を一括で表示する
let 表示文字数 = 制御情報.メッセージ.indexOf('<', 制御情報.表示位置) - 制御情報.表示位置;
if (表示文字数 == 0) return false; // 次の文字が制御タグだった場合
if (表示文字数 < 0) {
// 制御タグが無い場合は最後まで表示する
表示文字数 = 制御情報.メッセージ.length - 制御情報.表示位置;
}
this.指定した文字数のメッセージを表示する(制御情報, 表示文字数);
return true;
}
指定した文字数のメッセージを表示する(制御情報, 文字数) {
制御情報.表示メッセージ += 制御情報.メッセージ.substring(制御情報.表示位置, 制御情報.表示位置 + 文字数);
制御情報.表示位置 += 文字数;
this.メッセージ要素.innerHTML = 制御情報.表示メッセージ;
if (制御情報.スクロール高さ == this.メッセージエリア要素.scrollHeight) return;
this.メッセージエリア要素.scrollTop = 制御情報.スクロール高さ = this.メッセージエリア要素.scrollHeight;
return;
}
制御タグを取得する(制御情報) {
if (制御情報.メッセージ.charAt(制御情報.表示位置) != '<') return false;
if (this.制御タグの処理を行う(制御情報, '<br>', () => 制御情報.表示メッセージ += '<br>')) return true; // 改行する
if (this.制御タグの処理を行う(制御情報, '<c>', () => 制御情報.表示メッセージ = '')) return true; // クリアする
if (this.クリック待ちにする(制御情報)) return true;
return false;
}
制御タグの処理を行う(制御情報, タグ名, 処理) {
if (!制御情報.メッセージ.startsWith(タグ名, 制御情報.表示位置)) return false;
制御情報.表示位置 += タグ名.length;
処理();
this.メッセージ要素.innerHTML = 制御情報.表示メッセージ;
return true;
}
クリック待ちにする(制御情報) {
const タグ名 = '<w>';
if (!制御情報.メッセージ.startsWith(タグ名, 制御情報.表示位置)) return false;
const 取得メッセージ = 制御情報.メッセージ.substring(制御情報.表示位置);
const wList = 取得メッセージ.match(/^<w>\d*<\/w>/);
if (wList) {
const 待ち時間 = Number(wList[0].replace(/<w>(\d*)<\/w>/, "$1"));
制御情報.表示位置 += wList[0].length;
this.クリック待ちフレーム数 = 待ち時間 > 0 ? ループ.フレームレート * 待ち時間 : 0;
} else {
制御情報.表示位置 += タグ名.length;
}
制御情報.クリック待ちフラグ = true;
制御情報.一括表示フラグ = false;
this.スクリーン要素.onclick = this.クリック待ち時にクリックする.bind(this, 制御情報);
this.カーソル要素.classList.toggle('不可視化', false);
return true;
}
クリック待ち時にクリックする(制御情報) {
制御情報.クリック待ちフラグ = false;
this.クリック待ちフレーム数 = this.基本クリック待ちフレーム数;
this.カーソル要素.classList.toggle('不可視化' , true);
this.スクリーン要素.onclick = this.メッセージ表示時にクリックする.bind(this, 制御情報);
}
メッセージ表示時にクリックする(制御情報) {
制御情報.一括表示フラグ = true;
}
async 表示する() {
this.メッセージウィンドウ要素.classList.replace('消去', '表示');
await Com.一時停止(this.出現時間); // 少しずつ出現する時間
this.メッセージウィンドウ要素.classList.toggle('透明化', false);
await Com.一時停止(this.出現時間); // 出現した後に少し待つ
}
async 消去する() {
this.メッセージウィンドウ要素.classList.toggle('透明化', true);
this.メッセージウィンドウ要素.classList.replace('表示', '消去');
this.メッセージ要素.innerHTML = "";
}
}
このコードは、以下の3つの主要なクラスを利用しています:
さらに、以下の2つのクラスを新たに定義しています:
このクラスは、文字表示の進捗状況を管理します。
プロパティの内容:
■ コンストラクタ
このクラスのコンストラクタでは、引数としてセレクタ(例えば、スクリーンおよびメッセージウィンドウの要素を指定する文字列)が渡されます。また、出現時間、1文字ずつ表示する際のフレーム間隔、クリック待ち時間(フレーム換算)を受け取ります。 処理の流れは以下のとおり:
■ メッセージを表示する:メソッド
■ 制御タグの例
■ メッセージ表示時にクリックする:メソッド
■ 表示する:メソッド
■ 消去する:メソッド
このプログラムは次のような処理を行います:
import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { スプライトクラス } from './sprite.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート
import { メッセージウィンドウクラス } from './messageWindow.js'; // クラスをインポート
import { Data } from './data.js'; // クラスをインポート
import { Com } from './com.js'; // クラスをインポート
class サンプル {
static async main() {
サンプル.ループ処理を作成する();
サンプル.メッセージウィンドウを作成する();
サンプル.レイヤーを作成する();
if (!await サンプル.使用する画像を読み込む()) return;
サンプル.読み込んだ画像でスプライトを作成する();
サンプル.作成した物を使って画面に表示する();
}
static ループ処理を作成する() {
ループ.初期化する();
ループ.開始する();
}
static メッセージウィンドウを作成する() {
サンプル.メッセージウィンドウ = new メッセージウィンドウクラス(".スクリーン", ".メッセージウィンドウ");
Data.オブジェクト管理["サンプル"] = サンプル;
}
static レイヤーを作成する() {
サンプル.レイヤー = new レイヤークラス('canvas.レイヤー1');
ループ.処理を追加する(サンプル.レイヤー, サンプル.レイヤー.描画する.bind(サンプル.レイヤー));
}
static async 使用する画像を読み込む() {
const 画像情報リスト = [
{ id: '町01', src: 'img/bg02.jpg' },
{ id: '人物01', src: 'img/chara01.png' }
];
サンプル.画像管理 = new 画像管理クラス();
return await サンプル.画像管理.画像を読み込む(画像情報リスト);
}
static 読み込んだ画像でスプライトを作成する() {
サンプル.背景01 = new スプライトクラス(サンプル.レイヤー, サンプル.画像管理.取得する('町01'), 0, 0, 640, 640);
サンプル.レイヤー.処理を追加する(サンプル.背景01, サンプル.背景01.描画する.bind(サンプル.背景01));
サンプル.人物01 = new スプライトクラス(サンプル.レイヤー, サンプル.画像管理.取得する('人物01'), 0, 150, 640, 640);
サンプル.人物01正位置 = { x: サンプル.人物01.x, y: サンプル.人物01.y };
サンプル.人物01.座標を設定する(640, 150, 1280, 640); //初期表示位置を設定する
}
static async 作成した物を使って画面に表示する() {
サンプル.主人公名 = "プレイヤー";
サンプル.現時刻 = Com.シリアル値を時分秒に変換する(Date.now());
await サンプル.人物01.移動してフェードインする(-10, 0, サンプル.人物01正位置.x, 0, 1);
ループ.処理を削除する(サンプル.レイヤー);
await サンプル.メッセージウィンドウ.表示する();
await サンプル.メッセージウィンドウ.メッセージを表示する(
"<var>サンプル.主人公名</var>は町を訪れた<w>5</w><br><c><var>サンプル.主人公名</var>はとりあえず町の人に今何時か聞いてみた<w><br>すると「<var>サンプル.現時刻</var>」だと教えてくれた<w>"
);
await サンプル.メッセージウィンドウ.消去する();
while (1) {
サンプル.現時刻 = Com.シリアル値を時分秒に変換する(Date.now());
await サンプル.メッセージウィンドウ.表示する();
await サンプル.メッセージウィンドウ.メッセージを表示する(
"暫くして、<var>サンプル.主人公名</var>はまた何時なのか気になった<w>2</w><br>辺りの人に聞いてみると「<var>サンプル.現時刻</var>」だと教えてくれた<w>"
);
await サンプル.メッセージウィンドウ.消去する();
}
}
}
addEventListener('load', サンプル.main);
import { 画像管理クラス } from './imageManager.js'; // 画像管理に関する機能を提供
import { スプライトクラス } from './sprite.js'; // スプライト(画像)管理に関する機能
import { レイヤークラス } from './layer.js'; // レイヤー(描画画面)管理
import { ループ } from './loop.js'; // 繰り返し処理管理
import { メッセージウィンドウクラス } from './messageWindow.js'; // メッセージ表示管理
import { Data } from './data.js'; // データ管理
import { Com } from './com.js'; // 共通処理管理
これらのインポートにより、各機能を効率的に使うことができます。例えば、画像管理やスプライト表示、ループ処理などのモジュールが含まれています。
サンプル.main() は全体の流れを管理するメイン処理です。これにより以下の手順が実行されます:
const 画像情報リスト = [
{ id: '町01', src: 'img/bg02.jpg' }, // 背景画像
{ id: '人物01', src: 'img/chara01.png' } // キャラクター画像
];
サンプル.画像管理 = new 画像管理クラス();
画像情報リストを定義し、それらを読み込みます。サンプル.背景01 = new スプライトクラス(サンプル.レイヤー, サンプル.画像管理.取得する('町01'), 0, 0, 640, 640);
サンプル.人物01 = new スプライトクラス(サンプル.レイヤー, サンプル.画像管理.取得する('人物01'), 0, 150, 640, 640);
背景やキャラクターなどをスプライトとして設定し、画面上に表示できるようにします。await サンプル.メッセージウィンドウ.メッセージを表示する("プレイヤーは町を訪れた...");
このコードで、メッセージウィンドウにテキストを表示できます。while (1) {
サンプル.現時刻 = Com.シリアル値を時分秒に変換する(Date.now());
await サンプル.メッセージウィンドウ.メッセージを表示する("現在時刻は" + サンプル.現時刻);
}
ここでは永遠ループを使って、時間の変化を表示しています。