ゲーム開発

Web ゲーム用の 2D 描画技法とライブラリ

Michael Oneppo

これまで長い間、対話型 Web ゲームを作成するには実質的に Flash 以外の方法がありませんでした。好き嫌いはさておき、Flash には高速な描画システムがありました。アニメーション、ポイント アンド クリック方式のアドベンチャー ゲームなど、あらゆる種類のゲーム要素には Flash が使われていました。

HTML5 の登場に伴い各種ブラウザーが揃って Web 標準に準拠するようになると、プラグインが不要で高速かつ高品質なグラフィックを開発するための選択肢が、文字どおり爆発的に増えました。この記事では、描画手法の簡単なサンプルを解説し、サンプルの基盤になっているテクノロジと、テクノロジを使いやすくするライブラリをいくつか紹介します。今回は、ゲーム専用ライブラリは取り上げません。ゲーム用ライブラリは多数あるので、また別の機会に紹介しましょう。

描画標準

HTML5 の登場によって、主に 3 とおりの 2D 描画方法が現れました。具体的には、ドキュメント オブジェクト モデル (DOM)、canvas、およびスケーラブル ベクター グラフィックス (SVG) 形式の 3 つです。3 つのテクノロジを使ったライブラリの概説に進む前に、各手法のトレードオフについて理解を深めるために、3 つのテクノロジのしくみを再確認しましょう。

当然ながら、HTML にグラフィックを描画する最も基本的な方法は、まさに HTML です。大量の画像や背景要素を作成し、jQuery などのライブラリを使うと、シーンを再描画せずに動かせるスプライトを手軽に作成できます。実際の処理はブラウザーによって実行されます。この種類の構造は、シーン グラフとよく呼ばれます。HTML の場合は、DOM がシーン グラフに当たります。スプライトのスタイル指定には CSS を使うので、CSS の切り替えやアニメーションを使って、シーンに滑らかな動きを追加することもできます。

この手法の主な問題は、DOM レンダラーに依存している点です。シーンが複雑だと、処理に時間がかかることがあります。要素は数百個以上使用しないことをお勧めします。そのため、マッチ 3 ゲームやプラットフォーム ゲームよりも複雑なゲームでは、パフォーマンスの問題が発生する可能性があります。また、パーティクル システムなどで要素の数が急増すると、アニメーションが一時的に停止することがあります。

この手法のもう 1 つの問題は、CSS を使用して要素のスタイルを指定する必要があることです。CSS の記述方法によって、処理が十分に高速になるか、非常に低速になるかが決まります。最後の問題として、HTML を対象にしたコードを作成すると、ネイティブな C++ などの別のシステムに移植するのが難しいことがあります。コンソールなどにゲームを移植しようと考えている場合、この問題は重大です。メリットのまとめは、以下のとおりです。

  • Web ページの基本構造に基づいている。
  • jQuery などのライブラリを使うと簡単に要素を動かせる。
  • スプライトを比較的作成しやすい。
  • CSS の切り替えやアニメーションを使った組み込みのアニメーション システムがある。

デメリットのまとめは、以下のとおりです。

  • 細かい要素が大量にあると処理速度が低下することがある。
  • CSS を使用して要素のスタイルを指定する必要がある。
  • ベクター画像を使用できない。
  • 他のプラットフォームへの移植が難しいことがある。

HTML5 canvas

canvas 要素を使用すると、DOM のデメリットの多くが解消します。canvas 要素には、イミディエイト モードのレンダリング環境 (ピクセルで構成された帯状の平面) が用意されています。描画する内容を JavaScript で canvas に指定すると、ただちに (イミディエイトに) 描画されます。canvas によって描画コマンドがピクセルに変換されるので、システムのパフォーマンス低下を招くことなく、大量の描画コマンドをすばやく構築できます。描画できる対象には、幾何学図形、テキスト、画像、およびグラデーションなどの要素があります。canvas をゲームに使用する方法の詳細については、David Catuhe の記事を参照してください (bit.ly/1fquBuo、英語)。

では、デメリットは何でしょうか。canvas では、描画の完了後は描画内容が保存されないため、シーンを変更するときは毎回そのシーンを手動で再描画する必要があります。さらに複雑な方法 (折り曲げ、アニメーション化など) で図形を変更する場合は、計算を行って項目を再描画する必要があります。つまり、シーンに関する大量のデータをデータ構造内に保持しなければなりません。ただし、この作業が簡単になるライブラリがあることを考えれば、それほど大きな問題ではありません。カスタマイズが必要な場合は、canvas では独自の情報が保持されないことに注意してください。最後の問題として、canvas にはアニメーションがありません。滑らかなアニメーションを作るには、連続した手順でシーンを描画する必要があります。メリットのまとめは、以下のとおりです。

  • 直接描画するため、シーンをさらに複雑にできる。
  • 多種多様なビジュアル要素に対応している。

デメリットのまとめは、以下のとおりです。

  • シーンの情報が保持されないため、手動でシーンを構築する必要がある。
  • 複雑な変換やアニメーションは手動で設定する必要がある。
  • アニメーション システムがない。

SVG: スケーラブル ベクター グラフィックス

SVG は、2D 表現を記述するための XML ベースのマークアップ言語であり、HTML に似ています。SVG と HTML の主な違いは、SVG は描画を目的としているのに対し、HTML は主にテキストとレイアウトを目的としていることです。このため、SVG には滑らかな図形、複雑なアニメーション、変形、さらにはブラーをはじめとした画像フィルターなど、強力な描画機能が備わっています。SVG では HTML と同様にシーン グラフ構造を採用しているので、SVG 要素を調査し、図形を追加して、図形のプロパティを変更できるうえに、全体を再描画する必要はありません。実際の処理はブラウザーによって実行されます。詳しい説明については、Channel 9 の動画「HTML5 で SVG を使う」(bit.ly/1DEAWmh、英語) をご覧ください。

HTML と同様に、シーンが複雑になると SVG もパフォーマンスが低下しやすくなります。SVG ではある程度までの複雑さに対処できますが、canvas を使用する場合の複雑さと肩を並べるほどではありません。さらに、処理を簡略化するためのツールが他に存在しているとは言え、SVG を操作するためのツールは難しくなりがちです。メリットのまとめは、以下のとおりです。

  • 曲面表現、複雑な図形など、多くの描画オプションが備わっている。
  • 再描画不要な構造を採用している。

デメリットのまとめは、以下のとおりです。

  • 描画内容が複雑だとパフォーマンスが低下するおそれがある。
  • 操作が難しい。

2D 描画ライブラリ

Web での描画に利用できる標準について解説したので、今度は描画やアニメーションが簡単になるライブラリをいくつか紹介しましょう。たいていの場合は、描画と同時に他の処理も行うことに注意してください。たとえば、グラフィックが入力に反応するような処理が必要なことはよくあります。ライブラリを利用すると、描画に付随している一般的なタスクを簡略化できます。

KineticJS: canvas 用のシーン グラフが必要なら、このライブラリが役に立ちます。KineticJS は非常に強力な canvas ライブラリで、シーン グラフや追加機能が用意されています。基本的には、KineticJS を使うと、描画対象の形状が含まれた canvas 内の層を定義できます。たとえば、図 1 は、KineticJS を使ってシンプルな赤い円を描画する方法を示しています。

図 1 KineticJS による円の描画

// Points to a canvas element in your HTML with id "myCanvas"
var myCanvas = $('#myCanvas'); 
var stage = new Kinetic.Stage({
  // get(0) returns the first element found by jQuery,
  // which should be the only canvas element
  container: myCanvas.get(0),
    width: 800,
    height: 500
  });
   
var myLayer = new Kinetic.Layer({id: “myLayer”});
stage.add(myLayer);
var circle = new Kinetic.Ellipse({
  // Set the position of the circle
  x: 100,                                            
  y: 100,
   
  // Set the size of the circle
  radius: {x: 200, y: 200},
   
  // Set the color to red
  fill: '#FF0000'  
});
 
myLayer.add(circle);
stage.draw();

図のシーンを再描画する際は、必ず図 1 の最後の行を呼び出す必要があります。残りの処理は KineticJS によって実行されます。つまり、シーンのレイアウトが保存され、正しく描画されるように処理されます。

KineticJS には、その強力さの要因となっている興味深い特徴があります。たとえば、オブジェクトの fill プロパティには、次のグラデーションなど、さまざまな塗りつぶし方を指定できます。

fill: {
  start: {x: 0, y: 0},
  end: {x: 0, y: 200},
  colorStops: [0, '#FF0000', 1, '#00FF00']
},

また、画像で塗りつぶすこともできます。

// The "Image" object is built into JavaScript and
// Kinetic knows how to use it
fillPatternImage: new Image('path/to/an/awesome/image.png'),

KineticJS にはアニメーション システムもあります。この機能を使用すると、Animation オブジェクトを作成するか、Tween オブジェクトを使ってシーン内の図形のプロパティを徐々に変更して、要素を動かすことができます。図 2 は、両方のタイプのアニメーションを示しています。

図 2 KineticJS を使ったアニメーション

// Slowly move the circle to the right forever
var myAnimation = new Kinetic.Animation(
  function(frame) {
    circle.setX(myCircle.getX() + 1);
  },
  myLayer);
 
// The animation can be started and stopped whenever
myAnimation.start();
// Increase the size of the circle by 3x over 3 seconds
var myTween = new Kinetic.Tween({
  node: circle,
  duration: 3,
  scaleX: 3.0,
  scaleY: 3.0
});
 
// You also have to initiate tweens
myTween.play();

KineticJS は強力なうえに、ゲームを中心とした幅広い分野で使えます。コード、サンプル、およびマニュアルについては、kineticjs.com (英語) で参照してください。

Paper.js: Paper.js には、canvas への描画が簡単になるという点では単なるライブラリ以上の機能があります。このライブラリには、一般的な描画タスクを簡略化するために、PaperScript という JavaScript を若干変更した言語が用意されています。プロジェクトに PaperScript を含めるときは、コードの種類以外は通常のスクリプトと同じ方法で、プロジェクトをスクリプトにリンクします。

<script type=“text/paperscript" src=“mypaperscript.js”>

このように指定すると、Paper.js によってコードの解釈方法がわずかに変更されます。この変更では、実は 2 種類の解釈が変わるだけです。まず、PaperScript には、Point と Size という 2 つの組み込みオブジェクトがあります。PaperScript には、関数で共通して使用するためにこれらの 2 つのオブジェクトが用意されていて、直接これらの型を加算、減算、および乗算する機能があります。たとえば、PaperScript でオブジェクトを動かすには、次のように記述します。

var offset = new Point(10, 10);
 
var myCircle = new Path.Circle({
  center: new Point(300, 300),
  radius: 60
});
 
// Direct addition of Point objects!
myCircle.position += offset;

Paper.js によって解釈が変更されるもう 1 つの処理は、イベントへの応答です。JavaScript で以下のコードを記述することを考えてみましょう。

function onMouseDown(event) {
  alert("Hello!");
}

このコードでは、関数をいずれの要素のイベントにもバインドしていないので、何も実行されません。しかし、同じコードを PaperScript で記述すると、Paper.js によって自動的にこの関数が検出され、マウスダウン イベントにバインドされます。この処理の詳細については、paperjs.org (英語) を参照してください。

Fabric.js: Fabric.js は、多くのコードを使わなくても高度な効果や図形を Web ページに統合できる、機能満載の canvas ライブラリです。注目すべき機能には、背景の削除などの画像フィルター、独自の複合オブジェクトを作成できるカスタム クラス、さまざまなスタイルで canvas に描画できる "自由描画" のサポートが挙げられます。Fabric.js は、シーン グラフを採用している点で KineticJS と似ていますが、簡潔な構造になっているため、一部の開発者に人気があります。たとえば、シーンを再描画する必要はありません。

var canvas = new fabric.Canvas('myCanvas');
var circle = new fabric.Circle({
  radius: 200,
    fill: '#FF0000',
    left: 100,
    top: 100
});
 
// The circle will become immediately visible
canvas.add(circle);

これは大きな違いではありませんが、Fabric.js には、自動再描画と手動再描画を組み合わせたきめ細かいレンダリング制御があります。たとえば、Fabric.js で円を拡大するには次のように記述します。

circle.animate(
  // Property to animate
  'scale',
  // Amount to change it to
  3,
  {
    // Time to animate in milliseconds
    duration: 3000,
    // What's this?
    onChange: canvas.renderAll.bind(canvas)
  });

Fabric.js で何かをアニメーション化するには、値を変更したときに実行する処理を指定する必要があります。たいていの場合は、シーンの再描画を指定します。これが、先ほどのコードの canvas.renderAll.bind(canvas) で参照している内容です。このコードを実行すると、シーン全体をレンダリングする関数が返されます。ただし、この方法で大量のオブジェクトをアニメーション化する場合は、オブジェクトごとに 1 回ずつシーンを不必要に再描画することになります。代わりに、シーン全体が再描画されないようにして、手動でアニメーションを再描画することもできます。図 3 は、このアプローチを示しています。

図 3 Fabric.js での厳密な再描画制御

var needRedraw = true;
 
// Do things like this a lot, say hundreds of times
circle.animate(
  'scale',
  3,
  {
    duration: 3000,
       
    // This function will be called when the animation is complete
    onComplete: function() {
      needRedraw = false;
    }
  });
 
// This function will redraw the whole scene, and schedule the
// next redraw only if there are animations going
function drawAnimations() {
  canvas.renderAll();
  if (needRedraw) {
    requestAnimationFrame(drawAnimations);
  }
}
 
// Now draw the scene to show the animations
requestAnimationFrame(drawAnimations);

Fabric.js ではさまざまなカスタマイズが可能なので、必要なときだけに再描画を最適化できます。カスタマイズは、制御が難しい場合もあります。ただし、多くの複雑なゲームでは、この機能は不可欠と言えます。詳細については、fabricjs.com (英語) を参照してください。

Raphaël: Raphaël は、SVG の操作にかかわる複雑さの大半を解消できる、便利な SVG ライブラリです。Raphaël では、SVG を使用できる場合は SVG を使います。SVG を使用できない場合は、ブラウザーで利用可能なあらゆるテクノロジを使用して JavaScript で SVG を実装します。Raphaël で作成されるすべてのグラフィカル オブジェクトは DOM オブジェクトでもあり、イベント ハンドラーや jQuery アクセスのバインドなど、DOM オブジェクトの全機能も備わっています。Raphaël にはアニメーション システムもあります。このシステムを使用すると、描画対象のオブジェクトから独立したアニメーションを定義できるので、何度も再利用できます。

var raphael = Raphael(0, 0, 800, 600);
 
var circle = raphael.circle(100, 100, 200);
circle.attr("fill", "red");
circle.animate({r: 600}, 3000);
 
// Or make a custom animation
var myAnimation = Raphael.animation(
  {r: 600},
  3000);
circle.animate(myAnimation);

このコードでは、円を描画する代わりに、cricle 要素が存在しているページに SVG ドキュメントを配置します。奇妙なことに、Raphaël は SVG ファイルの読み込みをネイティブにはサポートしていません。Raphaël には活発なコミュニティがあるので、SVG ファイルの読み込み用プラグインを bit.ly/1AX9n7q (英語) から入手できます。

Snap.svg: Snap.svg は Raphaël とうり二つのライブラリです。

var snap = Snap("#myCanvas"); // Add an SVG area to the myCanvas element
var circle = snap.circle(100, 100, 200);
circle.attr("fill", "#FF0000");
circle.animate({r: 600}, 1000);

大きな違いの 1 つは、Snap.svg にはシームレスな SVG インポート機能があることです。

Snap.load("myAwesomeSVG.svg");

もう 1 つの違いは、処理対象の SVG の構造がわかっていれば SVG の構造をその場で検索して編集できる、強力な組み込みツールがあることです。たとえば、SVG に含まれているすべてのグループ (g タグ) を非表示にするとしましょう。SVG を読み込んだら、以下の機能を、load メソッドのコールバックに追加する必要があります。

Snap.load("myAwesomeSVG.svg", function(mySVG) {
  mySVG.select("g").attr("opacity", 0);
});

select メソッドの機能は jQuery の $ セレクターによく似ていて、非常に強力です。Snap.svg の詳細については、snapsvg.io (英語) を参照してください。

補足: p5.js

ここまでに紹介したライブラリの多くには、一般的なタスク用のちょっとした追加機能があります。そのため、単純な描画から対話型メディアや複雑なゲームまで、幅広いアプリケーションに対処できるさまざまなテクノロジが生まれています。では、このようなテクノロジのうち中程度のテクノロジ、つまり単純な描画ソリューションよりも高度であっても完全なゲーム エンジンと言えるほどではないテクノロジには、他にどのようなものがあるでしょうか。

注目に値するプロジェクトの 1 つは、p5.js です。p5.js は、人気のプログラミング言語である Processing を基盤としています (processing.org (英語) を参照してください)。この JavaScript ライブラリでは、ブラウザーに Processing を実装することで対話型メディア環境を実現しています。p5.js では、特に一般的なタスクを集約して、システムのイベント (シーンの再描画、マウス入力など) に応答するために定義が必要な一連の関数にしています。このライブラリは Paper.js によく似ていますが、マルチメディア ライブラリを備えている点が異なります。以下に、このアプローチによってグラフィック コードが簡潔になるようすの例を示します。

float size = 20;
function setup() {
  createCanvas(600, 600);
}
 
function draw() {
  ellipse(300, 300, size, size);
  size = size + .1;
}

このプログラムでは、画面いっぱいまでサイズが拡大する円を作成しています。p5.js の詳細については、p5js.org (英語) を参照してください。

ライブラリを選ぶ

canvas にも SVG にも、明確なメリットとデメリットがあります。また、どちらのアプローチにも、デメリットの多くを大幅に軽減するライブラリがあります。では、どのライブラリを使用すればよいでしょうか。概して、単純な HTML の使用はお勧めしません。最新ゲームのグラフィックは、おそらく HTML ではサポートできないほど複雑です。したがって SVG と canvas のどちらかを選ぶことになりますが、難しい選択です。

際立った特徴があるゲーム ジャンルの場合は、選択が少し簡単になります。数十万ものパーティクルがあるゲームをビルドするなら、canvas をお勧めします。一方、漫画本形式でポイント アンド クリック方式のアドベンチャー ゲームをビルドするなら、SVG の検討をお勧めします。

多くのゲームではパフォーマンスの問題が発生するように思えますが、大半のゲームでは、実際にパフォーマンスの問題に至ることはありません。使用するライブラリについて、長時間かけて議論する場合もあるでしょう。しかし結論を言えば、それはゲームの作成に使えるはずの時間を費やす行為です。

個人的には、アート アセットに基づいてライブラリを選択することをお勧めします。Adobe Illustrator または Inkscape でキャラクターのアニメーションを作成しているなら、そのアニメーションの各フレームをピクセルに変換してみてはどうでしょうか。また、ベクター アートはネイティブに使います。canvas にアートを詰め込んで労力を台なしにすることは避けましょう。

逆に、もしアートの大半がピクセル ベースの場合、またはピクセル単位の複雑な効果を作成するつもりの場合は、canvas が最適な選択肢です。

もう 1 つの選択肢

できる限り最高のパフォーマンスを追求していて、ゲームをもう少し複雑にしようと考えている場合は、Pixi.js の検討を強くお勧めします。この記事で紹介した他のライブラリとは異なり、Pixi.js は 2D レンダリングに WebGL を使っています。そのため、パフォーマンスが大幅に向上します。

この API は今回紹介した他のライブラリほど簡単ではありませんが、そのために大きな違いが生まれるわけでもありません。また、WebGL は他のライブラリほど多くのブラウザーではサポートされていません。そのため、従来のシステムでは Pixi.js を使用してもパフォーマンスが向上しません。何を選ぶにしても、自分で選び、その過程を楽しんでください。


Michael Oneppo* はクリエイティブな技術者で、マイクロソフトの Direct3D チームで前プログラム マネージャーを務めていました。近年では、非営利技術団体 Library For All の CTO としての業務、さらにニューヨーク大学の Interactive Telecommunications Program で修士号の取得などに努めました。*

この記事のレビューに協力してくれた技術スタッフの Shai Hinitz に心より感謝いたします。