『RPGツクールMZ』のJavaScriptで図形を描く
はじめに
『RPGツクールMZ』でJavaScriptを使い画像をベクター描画する方法を調べました。
ベクター描画ができると、二点間に線を引いたりできるのでシステムの幅が広がります。
リアルタイムに描画できるので、さまざまなインタフェースの形を状況に合わせて書き換えることもできます。
たとえばゲージの形も融通が効くようになります。
常に理屈の上でできることと、実際できることに隔たりはありますが(笑)
例によって適宜、次のリファレンスに、クラスなどリンクしていこうと思います。
なお、今回はラスター画像の扱いや SVG(ベクター画像フォーマット)については触れません。
使われるメソッド・プロパティについてはCanvasRenderingContext2D関連メソッド一覧に分けて書いたので、参考にどうぞ。
用語
コンテキスト…context(文脈)は、ここでは画像を扱う方法(2D、3Dなど)を指します。
パス…path(経路)は、画像を描くための線の集合です。線は直線・楕円・ベジェなど。
サブパス…パスを表現するためのデータ構造で「線で結ばれた点×n + 閉じているか」の情報です。
ベクター画像…パスを使って描く画像。拡大縮小・回転・変形で画像が荒れないのが特徴です。
JavaScript… Webブラウザを中心に使われるプログラミング言語です。Javaは別の言語の名前でJavaScriptの略称ではありません。
コアスクリプト…『RPGツクールMZ』で作ったゲームに必要なクラスを定義しているスクリプトです。事実上、これを操作するのが『RPGツクールMZ』での JavaScript です。逆に一般の JavaScript プログラマは知らないので、質問しても「なにそれ」って言われる確率が高いです。プラグインは含みません。
描画のためのクラス関係
実際に描画するコードを書く場合に、そこまで意識する必要はないとは思いますが、一応の作りは知っておいて損はないと思うので、ざっと解説します。
まず『RPGツクールMZ』での描画関連の JavaScript の作りについて調べました。
-
Sprite
クラスはWindow
など画像表示クラスのほとんどに含まれている -
Sprite
クラスのbitmap
プロパティにあるBitmap
オブジェクトが画像データを持っている -
Bitmap
クラスのcontext
プロパティがCanvasRenderingContext2D
(長いので以後ctx
)オブジェクト - 画像操作は
ctx
を通して行われるが、これは『RPGツクールMZ』の機能ではない -
Bitmap
がCanvas
クラスを持っていて、画像の主体はここにある -
Canvas
はHTMLの画像描画用の<canvas>要素(HTMLタグ)をJavaScriptから見たもの
詳細は非公式リファレンスのWindow、Sprite、Bitmapを見てもらうとしまして。
Canvas
クラスを使っているということはつまり『RPGツクールMZ』はWebブラウザと同じHTML上で動作しているのですね。
Canvasオブジェクト
canvas要素は、ざっくり言うとWebページの中に小さいディスプレイを用意するみたいな感じで、HTMLの<canvas>タグで作られます。
『RPGツクールMZ』のゲーム部分はすべてこの canvas要素に描画されています。
canvas要素を JavaScript で使うためのオブジェクトが、Canvasオブジェクトです。
『RPGツクールMZ』では多数のCanvasオブジェクトが必要に応じて作られています。
画像を表示するための便利なオブジェクトとそのプロパティ・メソッドが JavaScript WebAPI には用意されています。
Canvas API - MDN
JavaScript での画像の扱いについては、ぶっちゃけこのMDNのページに詳しく解説してあるので、それを読めばいいと思います。
ここでは主に僕が引っかかった部分についてと『RPGツクールMZ』特有の部分について説明することにします。
CanvasRenderingContext2D
canvas に描画するには、直接 canvas のメソッドを使うのではなく、コンテキストという描画のためのオブジェクトを使います。
今回使うのはCanvasRenderingContext2D
という2D描画用のコンテキストオブジェクトです。
CanvasRenderingContext2D - MDN
canvas からコンテキストCanvasRenderingContext2D
を得るには次のようなスクリプトを書きます。
const ctx = canvas.getContext("2d");
ただし『RPGツクールMZ』においては、画像を保持しているオブジェクト(主にBitmap
)にcontext
(あるいは類似の)プロパティが存在しているので、それを使います。
豆知識
Canvas
オブジェクトのコンテキストとして今回解説する "2d" のほかに "WebGL" などが用意されています。
今回 WebGL について詳しく解説しませんが、簡単に言うと「画面上の座標を元に、その位置を何色で塗るかを計算して導き出す」ということをひたすら行うことで描画するのが WebGL です。
グラフィックボードによる演算と相性が良い描画形式と言え、3D描画の基礎となっています(2Dも描けます)
描画手法ごとに必要な操作方法が全然違うので、直接画像データに対して操作を行うのではなく、コンテキストを指定して得られたオブジェクトが持っているメソッド・プロパティを使うことで、膨大な描画方法を知っておく必要がなくなります。
1種類の描画手法しか使わないなら間に挟むコンテキストがムダに感じますが…多機能化と引き換えですね。
『RPGツクールMZ』での描画
『RPGツクールMZ』上での使い方としては、ctx
を手に入れれば後はこっちのもの!いじり放題と考えられます。
-
リファレンスのクラス一覧から、それっぽい
Sprite_XXX
を探す - そのクラス内のメソッドでは、
this.bitmap.context
でctx
が得られる
クラスのどのメソッドでゴニョゴニョやるかというと、主に次のふたつ。
- サイズ変更などで適宜
refresh()
か類似メソッドで表示処理が行われる - アニメーションがある場合は
update()
メソッドが毎フレーム呼ばれる
なのでとりあえずrefresh()
を探して、そこでctx
に対してゴニョゴニョやるとよい!
(ちなみにrefresh()
とupdate()
両方から、同じメソッドが呼ばれていることもあります)
『RPGツクールMZ』のウィンドウ関係のクラスは、canvas
に図形を描くためのメソッドがあらかじめ用意してありますdraw〇〇()
って感じのメソッドです。
詳しくはリファレンスのWindow_Baseや実際のコードを読んでもらうとしまして。
この中でdrawRect()
、gradientFillRect()
がパスを使って描いています。他のものも『RPGツクールMZ』の画像処理である限り、canvas
を使って描いてますがパスではありません。
実は天候、影ペンの影などパスを使って描いている箇所は他にもあります。
ある特定のウィンドウに装飾をつけたいという場合、Sprite_XXX
ではなくWindow_XXX
を探して、そのrefresh()
、update()
のメソッドからそれっぽいものを探して、直接そこで this.sprite_XXX.bitmap.context
でctx
を得て、ゴニョゴニョやるといいでしょう。
Window_XXX
の場合、メインの内容はcontents
プロパティが持っているBitmap
クラスに描けば良いので、this.contents.context
でctx
が得られます。
先ほど紹介したメソッドもcontents
に描いています。
パス生成
canvas
にパスを生成する方法の詳細は、MDNのCanvas API、CanvasRenderingContext2Dを読むとわかります。
また、HakuhinさんのJavaScript プログラミング講座 2D描画について(Canvas 2D Context)がよくまとまっているので参考になります。
ちなみにcanvas
の仕様書はHTML Living Standard - canvasにあります。
細部の仕組みも書かれているので、突っ込んでいくと仕様書を読むことになるんですが…フツーはそこまで突っ込まないんで、これが大元だよというぐらいの情報です。
実際これらの資料を「読むとわかる」かというと、結構ハードルが高いので参考としてひとまず僕の理解で解説します。
人によって理解しやすい説明は異なりますが、いろんな人の説明を読めば、理解しやすい説明に当たる可能性が高まりますからね!
パス生成の基本
パスは一筆書きの要領で連続して引かれるので、基本的にパス生成メソッドはパスの最後の点からの続きとして動作します。
このためパス情報を生成することを「パスを引く」と言ったり、パスの座標を動かしていくことをペンに準えたりします。
「プロッターやベクタースキャンのような描き方」で通じると簡単で良いのですが、なるほどと思った人だけ思ってください。
描画せずに点を離したい時はmoveTo()
でペン持ち上げて移動し、新たなサブパスの開始点にできます。
パスはペンの通り道の指定のみでまだ描画はされません。
描画するにはfill()
やstroke()
といったメソッドを実行する必要があります。
「fill()
やstroke()
といったメソッド」と書くのも少々まどろっこしいので、以後は「描画メソッド」と書きます。
面の塗り方を指定するfillStyle
や、線の書き方を指定するstrokeStyle
のようなスタイルは、描画メソッドが実行されるまで適用されないので、パスを生成とスタイル指定の順番は入れ替わっても構いません。
ただfillRect()
のようにパスの生成+描画を合わせたメソッドもありますし、スタイルを最初に書く方が読みやすいと思います。
また後に記述したものが上に描画されます(下に描く指定もできます)
描画メソッドやmoveTo()
にはbeginPath()
の操作は入っていないません。次に描画メソッドを実行した場合、それまでに生成したパス全体が再描画されます。結果として同じパスにあるfill()
やstroke()
の描画が上書きされます。
大抵の場合それでは困るので、beginPath()
で区切って次のパスを開始します。
塗りや線のスタイルを変更する前に、beginPath()
で新たなサブパスを作れば、その前に引いたパスは描画メソッドの適用範囲外になり後でスタイルを変えても変更されないようになります。
すでにctx
にCanvasRenderingContext2D
が入っているとして、だいたい次のようなコードが基本です。
ctx.beginPath();// パスやスタイル情報を継続せず、新規パスを開始
ctx.strokeStyle = "red";// スタイルを設定
ctx.moveTo( 10, 10 );// 新規サブパスを開始
ctx.lineTo( 100, 10 );// 図形を引き
ctx.stroke();// 描画メソッドを実行
コンテキストが持っているメソッドには、いくつかのメソッドをまとめたものが存在しているので、描画メソッドが不要だったりする場合もあります。
ctx.beginPath();// パスやスタイル情報を継続せず、新規パスを開始
ctx.strokeStyle = "red";// スタイルを設定
ctx.strokeRect( 10, 10, 200, 100 );// moveTo() + lineTo()×4 + stroke()と同等なメソッド
とこのようにstrokeRect()
はいくつかのメソッドがまとまっているので、それらを別途書く必要がありません。
この辺、リファレンスを読んだだけではなかなかわからないので実際書いて確かめることをオススメします。
ゲージを書き換える
画像ファイルを使って表示している部分も、パスでのベクトル描画に置き換えることはできますが、そこそこ改造する必要があるので、パスで描かれているゲージ表示を書き換えてみます。
Sprite_Gauge
クラスの場合はupdate()
から辿っていくとupdateBitmap()
、updateGaugeAnimation()
、redraw()
、drawGauge
と辿ってdrawGaugeRect()
に辿り着きます。
…長い旅路です。
drawGaugeRect()
の最後に書いてあるふたつのメソッドがゲージを表示しているので、こいつをゴニョゴニョです。
Sprite_Gauge.prototype.drawGaugeRect = function(x, y, width, height) {
const rate = this.gaugeRate();
const fillW = Math.floor((width - 2) * rate);
const fillH = height - 2;
const color0 = this.gaugeBackColor();
const color1 = this.gaugeColor1();
const color2 = this.gaugeColor2();
this.bitmap.fillRect(x, y, width, height, color0);
this.bitmap.gradientFillRect(x + 1, y + 1, fillW, fillH, color1, color2);
};
幸いbitmap
まではコアスクリプトが取ってきているので、改造も楽です。
ゲージの幅を細くするとかはctx
を使わなくてもできるので、ゲージの形を矢印にしてみましょう。
矢印型にとくに意味はないですが、システム的に「この数値から上だけで使えるスキルがある」といった場合に目盛りを刻んだり、色々と応用が効くかと思います。
ゲージ書き換えタイプA
コードとしては結構な量になりますが、地道に点をつないでいくだけです。
矢印の形にゲージをくり抜くためのclip()
の使い方がちょっとわかりにくいかもしれません。
クリッピング領域を作る前にsave()
で状態を保存しrestore()
で元の状態に戻すという処理を行なっています。
これをやっておかないと、今まで引いたパス全体にマスクがかかってしまいます。
Sprite_Gauge.prototype.drawGaugeRect = function( x, y, width, height ) {
const fillW = Math.ceil( width * this.gaugeRate() );
const color0 = this.gaugeBackColor();
const color1 = this.gaugeColor1();
const color2 = this.gaugeColor2();
const ctx = this.bitmap.context;// bitmapから定数`ctx`にコンテキストを代入
ctx.save();// クリッピング(など)の状態を保存
// 矢印型(→)のクリッピングマスクを用意
ctx.moveTo( x, y + 4 );
ctx.lineTo( x + width - 20, y + 4 );
ctx.lineTo( x + width - 20, y - 4 );
ctx.lineTo( x + width, y + Math.floor( height / 2 ) );
ctx.lineTo( x + width - 20, y + height + 4 );
ctx.lineTo( x + width - 20, y + height - 4 );
ctx.lineTo( x, y + height - 4 );
ctx.clip();
// グラデーションの準備
const grad = ctx.createLinearGradient( 0, 0, width, 0 );
grad.addColorStop( 0, color1 );
grad.addColorStop( 1, color2 );
ctx.fillStyle = grad;
// ゲージを着色
ctx.beginPath();
ctx.fillRect( x, y - 4, fillW, height + 8 );
ctx.beginPath();
ctx.fillStyle = color0;
ctx.fillRect( x + fillW, y - 4, width - fillW, height + 8 );
ctx.restore();// 保存した状態から復帰
};
書き換えの前にbitmap.clear()
を実行して描画領域のクリアと再表示の予約をしておく必要がありますが、そこはコアスクリプトで書いてあるので、今回は気にしなくていいです。
もちろんBitmap
オブジェクトの準備とか、諸々のややこしいこともできるだけコアスクリプトに任せたいものです。
なお、画像の描画領域を広げたい場合はSprite_Gauge
のbitmapWidth()
、bitmapHeight()
を書き換えるなどする必要があります。
ゲージ書き換えタイプB
マスクをする方法には画像の合成方法を指定するglobalCompositeOperation
プロパティを使う方法もあり、こっちの方が使い勝手が良い気がします。
globalCompositeOperation - MDN
合成方法がたくさん用意されているんですが、正直どういう効果なのか分かってません(笑)
誰か分かりやすく解説してください。分かりやすく解説してあるページを紹介してくれるのでもいいです。
Sprite_Gauge.prototype.drawGaugeRect = function( x, y, width, height ) {
const fillW = Math.ceil( width * this.gaugeRate() );
const color0 = this.gaugeBackColor();
const color1 = this.gaugeColor1();
const color2 = this.gaugeColor2();
const ctx = this.bitmap.context;// bitmapから定数`ctx`にコンテキストを代入
// 矢印型(→)のクリッピングマスクを用意
ctx.beginPath();
ctx.fillStyle = "#000";
ctx.moveTo( x, y + 4 );
ctx.lineTo( x + width - 20, y + 4 );
ctx.lineTo( x + width - 20, y - 4 );
ctx.lineTo( x + width, y + Math.floor( height / 2 ) );
ctx.lineTo( x + width - 20, y + height + 4 );
ctx.lineTo( x + width - 20, y + height - 4 );
ctx.lineTo( x, y + height - 4 );
ctx.fill();
ctx.globalCompositeOperation = "source-atop";// マスク上に描画
// グラデーションの準備
const grad = ctx.createLinearGradient( 0, 0, width, 0 );
grad.addColorStop( 0, color1 );
grad.addColorStop( 1, color2 );
ctx.fillStyle = grad;
// ゲージを着色
ctx.beginPath();
ctx.fillRect( x, y - 4, fillW, height + 8 );
ctx.beginPath();
ctx.fillStyle = color0;
ctx.fillRect( x + fillW, y - 4, width - fillW, height + 8 );
ctx.globalCompositeOperation = "source-over";
};
最後に
ベクトルで画像が描けると、リアルタイムにUIやエフェクトその他を書き換えることができるので夢が広がります。
ただ、テキトーに線が出せるのと、物理演算したゴムの挙動を線で表現することの間には深くて広い谷があって、簡単に飛び越えることはできません。
実際僕もメッセージウィンドウのコードを読んだりして、フキダシ状のメッセージウィンドウを作ろうとしてますが、5年経ってまだ作れてませんからね。
参考:『RPGツクールMZ』のウィンドウ枠を調べてみた
まじめに取り組んでいないだけという話もありますが(笑)
応用編として、こちらの記事が参考になるかもしれません。
タイトル画面の演出として、気候(Weather)みたいにエフェクトを描くとかするといいのかも(テキトーなこと言ってます)
JavaScriptで取り組むクリエイティブコーディング
パーティクル表現入門
では、レッツ エンジョイ ツクールライフ!!
Discussion