Help us understand the problem. What is going on with this article?

JavaScriptでフルスクラッチゲーム開発しよう 第2回 画像読み込み編

More than 5 years have passed since last update.

前回: JavaScriptでフルスクラッチゲーム開発しよう 第1回 準備編

前回の続きとして、画像の表示をしてみます。

画像を読み込む方法

画像の読み込みはImageElementを使って行います。

HTML上では<img>として定義するものを、JavaScriptから生成して行います。

JavaScriptからImageElementを生成するにはdocument.createElementメソッドに、第1引数として'img'を与えて生成するか、Imageオブジェクトのインスタンスを作るか、どちらかの方法が使えます。

今回はImageオブジェクトから生成してみます。

Imageオブジェクトのインスタンス生成の例
var image = new Image();
image.src = '画像ファイルへのパス';
image.onload = function() {
  // 画像ファイルが読み込み終わった時に呼ばれる関数
};

これだけでは、読み込む画像の数が多くなった場合、それを読み込む処理が多く羅列することになります。

ですので、それらを管理するためのオブジェクトを作ってみます。

画像読み込みを管理する

画像に限らず、音声やテーブルデータなどのファイルを「リソース」と呼ぶことがありますが、昨今では「アセット」と呼ぶのがモダンでトレンディです。

アセットを管理するオブジェクトを作ります。

main.js
var Asset = {};

単なるオブジェクトです。シンプルですね。

「シンプルにしとけこのバカチンが」の原則にのっとりましょう。

参考:KISSの原則 - Wikipedia

読み込む画像の定義として「アセット種類」「名前」「ファイルパス」を定義していきます。それぞれtypenamesrcとかのプロパティ名で参照できるようにしておきます。

main.js
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)

back.png

  • みかん箱(box.png)

box.png

なんということでしょう。味わい深いスタイリッシュな画像が用意されました。

これらのファイルを、わかりやすく「assets」というディレクトリを切って(フォルダを作成して)、そこへ格納することとします。

では、Assetsへ定義します。

main.js
Asset.assets = [
  { type: 'image', name: 'back', src: 'assets/back.png' },
  { type: 'image', name: 'box', src: 'assets/box.png' }
];

画像を読み込む準備が整いました。

画像を読み込む関数を用意する

AssetloadAssetsメソッドを作成して、そこに読み込む処理を書いてみます。

main.js
Asset.loadAssets = function() {
  // ここに読み込み処理を書いていく
};

すべてのアセットが読み込み終わったことを知る仕組みとして、コールバック関数と呼ばれるものを使います。

参考:コールバック関数とは 【 callback function 】 - 意味/解説/説明/定義 : IT用語辞典

「JavaScript コールバック」で検索すると「コールバック地獄」という話が出ますが、今は気にしないでください。一度コールバック地獄を経験しないと感動できない世界が、その先にはあります。

main.js
Asset.loadAssets = function(onComplete) {
                         // ↑第1引数にonCompleteが登場
  // ここに読み込み処理を書いていく
  // すべてのアセットが読み込み終わったら
  // onCompleteに渡された関数を呼ぶ
};

loadAssetsメソッドの第1引数にonCompleteというのが登場しました。

ここに「関数」が渡されることを想定しています。

そして、定義されたすべてのアセットが読み込み終わった時に、このonCompleteに渡された関数を呼ぶという仕様にします。

すべてのアセットの読み込み完了を知る方法

すべてのアセットの読み込み完了を知るために以下の情報を扱う方法を採用します。

  • 定義されたアセットの合計数
  • 読み込み完了したアセット数

アセット1つひとつの読み込みが終わった時に「読み込み完了したアセット数がアセットの合計数に達したら、すべてのアセットが読み込み終わったと見なす」という方法にします。

ここでPromiseを使いたいと思い立ったあなた。君たちやっぱりPromiseだな!

ここでは、とりあえずわかりやすい処理にするために、上記のような単純なアルゴリズムの仕様にしましたので、ご了承ください。

ここの妙な面倒臭さを味わうことで、Promiseのキレ味に感動することができます。

main.js
Asset.loadAssets = function(onComplete) {
  var total = Asset.assets.length; // アセットの合計数
  var loadCount = 0; // 読み込み完了したアセット数

  // 以下で、アセット種類ごとの読み込み処理を行う
};

画像読み込み用のメソッドを用意する

今はまだ画像しか用意していないですが、画像読み込み用のメソッドを用意します。

引数として「アセット情報」「アセット読み込み完了時に呼ばれるコールバック関数」を定義します。

main.js
Asset._loadImage = function(asset, onComplete) {
};

おや、メソッド名に接頭辞としてアンダーバー(_)がついています。

_loadImageは、外部から呼び出される想定ではなく、loadAssetsメソッドからのみ呼び出される想定のメソッドですので、いわゆるprivateな立ち位置、ということで、慣習として接頭辞としてアンダーバーをつけました。

JavaScriptには言語仕様としてprivateなメソッドというのがありませんので、あくまで慣習としてそれに倣います。

ちなみに「接頭辞」は「せっとうじ」と読み、"prefix"(プレフィックス)と呼ばれたりもします。明日学校で使おう。

参考:接頭辞 - Wikipedia

そして、実際に画像の読み込みを行います。

main.js
Asset._loadImage = function(asset, onLoad) {
  var image = new Image();
  image.src = asset.src;
  image.onload = onLoad;
};

image.onloadと、仮引数のonLoadは、"L"の大文字と小文字で違いがありますが、自分が書く部分での名前の表記法を統一しておきたい、というのがあります。

プログラミング自体が割と初めてという方には「仮引数って何…?」という方もいらっしゃると思いますので、そんな方は、こちらを参考にしてください。

参考:引数 - Wikipedia

JavaScriptでは"LowerCamelCase"という方法でネーミングするのがスタンダードになっており、それに従っています。なのでImageonloadがなぜすべて小文字なのか、という心境です。

キャメルケースの参考:キャメルケース - Wikipedia

スネークケースというスタイルもあります。

参考:「キャメルケース」と「スネークケース」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

表記法の名前を、その表記法で書くという覚えやすい書き方もあります。

  • lowerCamelCase
  • UpperCamelCase
  • UPPER_SNAKE_CASE
  • lower_snake_case

話がいっぱい逸れました。

読み込み終わった画像を格納しておく

読み込み終わった画像を使うときに参照できるように、Asset.imagesというオブジェクトを作って、そこに格納するようにしておきます。

main.js
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つのアセットの読み込みが終わったときに呼ばれるコールバック関数を用意します。

main.js
Asset.loadAssets = function(onComplete) {
  var total = Asset.assets.length; // アセットの合計数
  var loadCount = 0; // 読み込み完了したアセット数

  // アセットが読み込み終わった時に呼ばれるコールバック関数
  var onLoad = function() {
    loadCount++; // 読み込み完了数を1つ足す
    if (loadCount >= total) {
      // すべてのアセットの読み込みが終わった
      onComplete();
    }
  };
};

仮引数のonCompleteで与えられている値が、本当に関数なのかというチェックをすることで、より堅牢な処理となりますが、サンプルコードとしての複雑さをなくすために、あえてその処理を入れていませんので、ご了承ください。

「俺は堅牢なコードを学びたいのだ」という方は「防御的プログラミング」というキーワードで勉強を進めてみてください。

では、実際にアセットの読み込み処理に繋げてみます。

main.js
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を呼びます。

main.js
function init() {
   :

  Asset.loadAssets(function() {
    // アセットがすべて読み込み終わったら、
    // ゲームの更新処理を始めるようにする
    requestAnimationFrame(update);
  });
}

Assetの全体のコード

main.js
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に見せかけた野放しの被害に遭っている方の少しでもの助けとなるように、「チュートリアル」のあるべき姿というものを考え続けられずには居られません。

次回: JavaScriptでフルスクラッチゲーム開発しよう 第3回 画像表示編

mozukichi
プログラミングが好きです。
http://mozukichi.netlify.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away