前回: JavaScriptでフルスクラッチゲーム開発しよう 第1回 準備編
前回の続きとして、画像の表示をしてみます。
画像を読み込む方法
画像の読み込みはImageElement
を使って行います。
HTML上では<img>
として定義するものを、JavaScriptから生成して行います。
JavaScriptからImageElement
を生成するにはdocument.createElement
メソッドに、第1引数として'img'
を与えて生成するか、Image
オブジェクトのインスタンスを作るか、どちらかの方法が使えます。
今回はImage
オブジェクトから生成してみます。
var image = new Image();
image.src = '画像ファイルへのパス';
image.onload = function() {
// 画像ファイルが読み込み終わった時に呼ばれる関数
};
これだけでは、読み込む画像の数が多くなった場合、それを読み込む処理が多く羅列することになります。
ですので、それらを管理するためのオブジェクトを作ってみます。
画像読み込みを管理する
画像に限らず、音声やテーブルデータなどのファイルを「リソース」と呼ぶことがありますが、昨今では「アセット」と呼ぶのがモダンでトレンディです。
アセットを管理するオブジェクトを作ります。
var Asset = {};
単なるオブジェクトです。シンプルですね。
「シンプルにしとけこのバカチンが」の原則にのっとりましょう。
読み込む画像の定義として「アセット種類」「名前」「ファイルパス」を定義していきます。それぞれtype
、name
、src
とかのプロパティ名で参照できるようにしておきます。
Asset.assets = [
{ type: 'アセット種類', name: 'アセット名', src: 'ファイルパス' },
:
// 定義が続く
];
おや?アセット種類が文字列で定義されていますね。
C++やJavaで育ってきた方には違和感を感じるかもしれませんが、これがモダンでトレンディなJavaScriptでのスタイルです。
アセット種類ごとに定数を定義して、それを指定するという方法もありますが、例えばアセットのリストをJSONとして外部ファイルで定義しようとしたときに、その定数としての変数を利用できないというデメリットが生まれます。
var AssetType = {
Image: 1,
Sound: 2,
:
};
Asset.assets = [
{ type: AssetType.Image, name: 'hoge', src: 'hoge.png' },
:
];
// JSONを外部ファイルで定義しようとした場合
// assets.json
[
// type: 1 って何?
{ type: 1, name: 'hoge', src: 'hoge.png' },
:
]
JSONの参考:JavaScript Object Notation - Wikipedia
また、AssetType.Image
とタイピングする量も増えます。
'image'
と文字列にしたほうがわかりやすく、タイピング量も少なく済みます。
というわけで、画像ファイルのアセット種類は'image'
という文字列として定義することとします。
また、name
プロパティは、画像を使いたいときに参照するための名前のために用意します。いちいち使うたびにファイルパスで指定するのは面倒ですので。また、名前は内容がわかりやすい名前にしましょう。
画像を用意してアセットリストを作る
とりあえず、適当に画像を用意します。お手元のペイントやGimpで画像を作りましょう。
- 背景画像(back.png)
- みかん箱(box.png)
なんということでしょう。味わい深いスタイリッシュな画像が用意されました。
これらのファイルを、わかりやすく「assets」というディレクトリを切って(フォルダを作成して)、そこへ格納することとします。
では、Assets
へ定義します。
Asset.assets = [
{ type: 'image', name: 'back', src: 'assets/back.png' },
{ type: 'image', name: 'box', src: 'assets/box.png' }
];
画像を読み込む準備が整いました。
画像を読み込む関数を用意する
Asset
にloadAssets
メソッドを作成して、そこに読み込む処理を書いてみます。
Asset.loadAssets = function() {
// ここに読み込み処理を書いていく
};
すべてのアセットが読み込み終わったことを知る仕組みとして、コールバック関数と呼ばれるものを使います。
参考:コールバック関数とは 【 callback function 】 - 意味/解説/説明/定義 : IT用語辞典
「JavaScript コールバック」で検索すると「コールバック地獄」という話が出ますが、今は気にしないでください。一度コールバック地獄を経験しないと感動できない世界が、その先にはあります。
Asset.loadAssets = function(onComplete) {
// ↑第1引数にonCompleteが登場
// ここに読み込み処理を書いていく
// すべてのアセットが読み込み終わったら
// onCompleteに渡された関数を呼ぶ
};
loadAssets
メソッドの第1引数にonComplete
というのが登場しました。
ここに「関数」が渡されることを想定しています。
そして、定義されたすべてのアセットが読み込み終わった時に、このonComplete
に渡された関数を呼ぶという仕様にします。
すべてのアセットの読み込み完了を知る方法
すべてのアセットの読み込み完了を知るために以下の情報を扱う方法を採用します。
- 定義されたアセットの合計数
- 読み込み完了したアセット数
アセット1つひとつの読み込みが終わった時に「読み込み完了したアセット数がアセットの合計数に達したら、すべてのアセットが読み込み終わったと見なす」という方法にします。
ここでPromise
を使いたいと思い立ったあなた。君たちやっぱりPromise
だな!
ここでは、とりあえずわかりやすい処理にするために、上記のような単純なアルゴリズムの仕様にしましたので、ご了承ください。
ここの妙な面倒臭さを味わうことで、Promise
のキレ味に感動することができます。
Asset.loadAssets = function(onComplete) {
var total = Asset.assets.length; // アセットの合計数
var loadCount = 0; // 読み込み完了したアセット数
// 以下で、アセット種類ごとの読み込み処理を行う
};
画像読み込み用のメソッドを用意する
今はまだ画像しか用意していないですが、画像読み込み用のメソッドを用意します。
引数として「アセット情報」「アセット読み込み完了時に呼ばれるコールバック関数」を定義します。
Asset._loadImage = function(asset, onComplete) {
};
おや、メソッド名に接頭辞としてアンダーバー(_
)がついています。
_loadImage
は、外部から呼び出される想定ではなく、loadAssets
メソッドからのみ呼び出される想定のメソッドですので、いわゆるprivateな立ち位置、ということで、慣習として接頭辞としてアンダーバーをつけました。
JavaScriptには言語仕様としてprivateなメソッドというのがありませんので、あくまで慣習としてそれに倣います。
ちなみに「接頭辞」は「せっとうじ」と読み、"prefix"(プレフィックス)と呼ばれたりもします。明日学校で使おう。
そして、実際に画像の読み込みを行います。
Asset._loadImage = function(asset, onLoad) {
var image = new Image();
image.src = asset.src;
image.onload = onLoad;
};
image.onload
と、仮引数のonLoad
は、"L"の大文字と小文字で違いがありますが、自分が書く部分での名前の表記法を統一しておきたい、というのがあります。
プログラミング自体が割と初めてという方には「仮引数って何…?」という方もいらっしゃると思いますので、そんな方は、こちらを参考にしてください。
JavaScriptでは"LowerCamelCase"という方法でネーミングするのがスタンダードになっており、それに従っています。なのでImage
のonload
がなぜすべて小文字なのか、という心境です。
キャメルケースの参考:キャメルケース - Wikipedia
スネークケースというスタイルもあります。
参考:「キャメルケース」と「スネークケース」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
表記法の名前を、その表記法で書くという覚えやすい書き方もあります。
- lowerCamelCase
- UpperCamelCase
- UPPER_SNAKE_CASE
- lower_snake_case
話がいっぱい逸れました。
読み込み終わった画像を格納しておく
読み込み終わった画像を使うときに参照できるように、Asset.images
というオブジェクトを作って、そこに格納するようにしておきます。
Asset.image = {};
:
Asset._loadImage = function(asset, onLoad) {
var image = new Image();
:
Asset.images[asset.name] = image;
};
アセット定義のname
で参照できるように、Asset.image
オブジェクトに対してasset.name
をキーとして、Image
のオブジェクトを格納しておきます。
アセット種類'image'のアセットを_loadImageを使って読み込む
Asset.loadAssets
から、アセット種類が'image'
のものをAsset._loadImage
を使って画像を読み込むように繋げてあげるのですが、その前に、1つのアセットの読み込みが終わったときに呼ばれるコールバック関数を用意します。
Asset.loadAssets = function(onComplete) {
var total = Asset.assets.length; // アセットの合計数
var loadCount = 0; // 読み込み完了したアセット数
// アセットが読み込み終わった時に呼ばれるコールバック関数
var onLoad = function() {
loadCount++; // 読み込み完了数を1つ足す
if (loadCount >= total) {
// すべてのアセットの読み込みが終わった
onComplete();
}
};
};
仮引数のonComplete
で与えられている値が、本当に関数なのかというチェックをすることで、より堅牢な処理となりますが、サンプルコードとしての複雑さをなくすために、あえてその処理を入れていませんので、ご了承ください。
「俺は堅牢なコードを学びたいのだ」という方は「防御的プログラミング」というキーワードで勉強を進めてみてください。
では、実際にアセットの読み込み処理に繋げてみます。
Asset.loadAssets = function(onComplete) {
:
Asset.assets.forEach(function(asset) {
switch (asset.type) {
case 'image':
Asset._loadImage(asset, onLoad);
break;
}
});
};
おっと、forEach
なんていうのが登場しました。「forEachって何?」という方は、ぜひJavaScriptのArray
というオブジェクトの仕様を舐めまわしてください。
参考:Array.forEach - JavaScript | MDN
_loadImage
を呼ぶときにthis
から呼ぶ方が行儀が良いですが、その場合Function.bind(this)
の説明が必要となり、全部説明したくなっちゃうので省きました。
また、default
句についても、例外の話をしたくなってしまうため、省きました。
必要な時に学ぼうの精神の参考:YAGNI - Wikipedia
実際にloadAssetsを呼ぶ
前回作成したinit
関数の中で、Asset.loadAssets
を呼びます。
function init() {
:
Asset.loadAssets(function() {
// アセットがすべて読み込み終わったら、
// ゲームの更新処理を始めるようにする
requestAnimationFrame(update);
});
}
Assetの全体のコード
var Asset = {}
// アセットの定義
Asset.assets = [
{ type: 'image', name: 'back', src: 'assets/back.png' },
{ type: 'image', name: 'box', src: 'assets/box.png' }
];
// 読み込んだ画像
Asset.images = {};
// アセットの読み込み
Asset.loadAssets = function(onComplete) {
var total = Asset.assets.length; // アセットの合計数
var loadCount = 0; // 読み込み完了したアセット数
// アセットが読み込み終わった時に呼ばれるコールバック関数
var onLoad = function() {
loadCount++; // 読み込み完了数を1つ足す
if (loadCount >= total) {
// すべてのアセットの読み込みが終わった
onComplete();
}
};
// すべてのアセットを読み込む
Asset.assets.forEach(function(asset) {
switch (asset.type) {
case 'image':
Asset._loadImage(asset, onLoad);
break;
}
});
};
// 画像の読み込み
Asset._loadImage = function(asset, onLoad) {
var image = new Image();
image.src = asset.src;
image.onload = onLoad;
Asset.images[asset.name] = image;
};
最初は「画像表示編」にしようと思っていたのですが、読み込みだけで長くなったので「画像読み込み編」にしました。
実際の画像の描画については、次回にします。
基礎的な用語の解説がくどい?
細かい用語や考え方について、ところどころ説明や参考リンクを行っていますが、これは「もし自分が初心者の頃だったら、ここの説明が放置されたら困惑するだろうな」と思うようなところで手を差し伸べています。
昨今、技術系解説記事が氾濫していますが、基礎的な概念についてすべて説明する必要は無くとも、キーワードや参考リンクがあるだけでも十分助けとなる方がたくさん居るはずです。
新入社員がたくさん増えたであろうこの時期、OJTに見せかけた野放しの被害に遭っている方の少しでもの助けとなるように、「チュートリアル」のあるべき姿というものを考え続けられずには居られません。