#はじめに
こんにちは。僕はゲームライブラリphina.jsではじめてJavaScriptに触れた入門者です。今まで触ってきた言語にC/C++/Java/Matlabがあります。phina.jsを使って工夫した点と、僕と同じようなJavaScript入門者に対して、僕がよくしたミスの提示をこの記事でしたいと思います。後半については、phina.jsに対してというよりも、どちらかと言うとJavaScriptという言語そのものに対してのつまづきポイントを紹介する形になります。
#今回作ったゲーム概要
(画像クリックorタップでゲームが遊べます)(※firefoxの場合少し画像が歪んでしまいます)
- 横画面の2D対戦アクションゲーム。
- MMDから得たアニメーション画像をドット絵ライクな解像度まで縮小し、PCだけでなくスマホでも快適に動作。
- Voiceroidという製品のキャラクターを用いた二次創作品です。
phina.jsは上のような幅・高さの揃った画像が敷き詰められたスプライトシートを用いることで、アニメーションさせることが出来ます。ただ、今回のような大量(1キャラあたり300枚程度)の画像を並べると、画像がかさばってしまいます。そこで、各画像をトリミングし、敷き詰めました。
この際、各画像の左上原点から(x,y)離れた点から(w,h)の矩形をトリミングし、スプライトシート画像に載せるわけですが、このx,y,w,h、そして載せる先の座標(srcx,srcy)を各画像について記録し、すべてjsonファイルとして一つにまとめておきます。
{
"frames": [
{
"name": "frame000.png",
"x": 20,
"y": 11,
"srcx": 100,
"srcy": 299,
"w": 39,
"h": 70,
},
{
"name": "frame001.png",
...省略...
"h": 72,
}
],
"animations":
}
ついでに上のように各画像の名前もnameプロパティに記録し、名前順に並び替えておくことで、アニメーションの設定がやりやすくなります。
このjsonファイルを、phina.jsのspritesheetファイルとして扱わせるようにphina.jsを拡張します。phina.jsのSpriteSheetクラスを書き換えて、各フレームの幅と高さをspritesheetファイルのframeのプロパティwidth,height,rows,colsから計算して得させるのではなく、上のjsonファイルのframes配列の各srcx,srcy,w,hから得るようにします。こうすることで、幅・高さの異なる画像によるスプライトシートに対応しました。
あとは、FrameAnimationクラスとSpriteクラスを書き換えて、各画像がトリミングした分左上原点からずれたx,yの値だけ位置修正して画像を描くようにすれば、左上原点に設定したSprite要素が正しくアニメーションするようになります。
##スプライトに自身の名前を登録
今回のような対戦アクションゲームの場合、キャラクターの色変えを行い、対戦相手のキャラクターと区別することが必要になってきます。
Sprite要素を一旦親要素から取り除き、再度別の画像を持ったSprite要素を親要素に紐付けることでこれを実現させましたが、今どの画像をSprite要素が持っているのかがわからないと、対戦相手との色かぶりが起きてしまいます。そこで、Spriteクラスのコンストラクタに、代入した文字列(AssetManagerに与える文字列)をnameプロパティとして記録するように書き加えしました。
また、phina.jsのspritesheetファイルには、アニメーションのパターンを記述できるanimationsプロパティがありますが、これについてもアニメーション自身の名前を上のnameプロパティのように記録させ、今キャラクターがどのアニメーションパターンを描写しているのかをFrameAnimationクラスのcurrentAnimationプロパティから得られるようにしてみました。今回のような様々なアニメーションパターンを駆使するアクションゲームでは重宝しました。
##アンチエイリアスのないドット絵を目指して
今回はドット絵風なビジュアルなので、Canvasが自動的に発生させるアンチエイリアスはなんとしても防ぎたいところです。簡潔に描くと、
- 描画する画像の座標は必ず整数にする
- CanvasのimageSmoothingEnabledをfalseにする
- キャンバス要素の image-rendering CSSプロパティに対してpixelated(Chrome)やcrisp-edges(firefox)を定めておく。
この三点を抑えておけば、とりあえず2016/11/30においてはアンチエイリアスのない完璧なドット絵が拡大して描写されます。imageSmoothingEnabledについては、phina.jsがブラウザ間の差異を吸収しているようです。僕はphina.jsのSpriteクラスのdraw(canvas)メソッド内にcanvas.imageSmoothingEnabled = false; と挿入しました。
##requestAnimationFrameでメインループを回す
つい最近twitter上で見かけたツイートに、setTimeoutではなくrequestAnimationFrameでメインループを回すことで、Android版chromeのタップ時の画面停止を回避できるというものがありました。
そこで単純に、phina.jsのTickerクラスのstartメソッド内に定義されている関数fn内からfn自身を一定間隔ごとに再帰呼び出ししているsetTimeoutをrequestAnimationFrameに差し替えてみました。すると、特にエラーもなく、タップ時に硬直しないのでかなり快適に動作しました。ただし、requestAnimationFrameはsetTimeoutのように引数にdelayを指定できず、60fps固定となっているため、requestAnimationFrameで呼び出している関数内でupdateやdrawを実行するかどうか分岐させて、60以下のfpsを実現させました。こちらのサイトを参考に実装してみました。
#つまづきかけたところ
##あれ?thisって・・・
JavaとJavaScriptは全くの別物。そういう話は聞いていたのですが、いざ触ってみると様々な相違点に少し戸惑いました。thisの違いもその一つです。詳しくは他の投稿者様の記事に譲るとして、ここでは実際にゲーム制作において出てきたthisとその用法を説明します。
/*
* メインシーン
*/
phina.define("MyClass", {
// 継承
superClass: 'DisplayElement',
//メンバ変数(省略可能)
player: null,
// コンストラクタ
init: function(options,player) {
// super init
this.superInit(options);
//MyClassのplayerメンバ変数を初期化
this.player = player;
}
}
これはあまりJavaと変わらない用法だなと感じた例です。こういったクラスの書き方はphina.js固有のものですが、オブジェクト指向言語を触った事があればすぐに分かる要素で構成されていると感じました。ちなみにこうして記述されたクラスをインスタンス化する際、newは不要です。JavaScriptにおいてもnewは基本的に必要なようですが、~~phina.jsはphina.globalize()とグローバルに展開することでnewを付けずにインスタンス化しているようです。~~phina.globalize()はc++のusing namespaceのように長いクラス名を省略する役割をしているようです。コメントくださったaxion014様、ありがとうございます。
/*
* メインシーン
*/
phina.define("MyClass", {
// 継承
superClass: 'DisplayScene',
//メンバ変数(省略可能)
player: null,
// コンストラクタ
init: function(options,player) {
// super init
this.superInit(options);
//MyClassのplayerメンバ変数を初期化
this.player = player;
},
update: function(app){
this.player.x += 10;//プレイヤーが右へ動く
player.x += 10;//これではダメ
}
}
Javaと違い、updateメソッド内でthisが省略されずに書かれています。Javaではメソッドの引数等とメンバ変数の違いを区別する際にthisが用いられますが、特に区別する必要がない場合、thisは省略可能です。JavaScriptでthisを省略すれば、それはメソッドの引数やメソッド内で定義された変数を指すことになり、そういった変数が定義されていなければエラーとなります。はじめはこのthisの書き忘れによってエラーを頻発しました。
var myObject = {
value: "member_of_Object",
test:function(){
var array = ["shujinkou","tekiA","tekiB","tekiC"];
array.forEach(function(element){
console.log(element); //arrayの中身を出力
console.log(this.value) //このthisはグローバルオブジェクトを指してしまい、エラー
});
},
test2:function(){
var array = ["shujinkou","tekiA","tekiB","tekiC"];
array.forEach(function(element){
console.log(element); //arrayの中身を出力
console.log(this.value) //member_of_Object と出る
},this);//ここ(forEach関数の第二引数)にthisと書く
},
}
上のforEach(これはphina.jsに限らずJavaScript共通)関数のように、時たまコールバック関数を引数に持つメソッドを使うことがあります。その際、関数内でthisを使ったにも関わらず、thisが何を指すか明示しないと基本的にエラーになってしまいます。この場合はforEach関数の第二引数にthisを入れてやることで、コールバック関数内のthisはmyObject内のthis(myObject自身を指すthis)と一致します。これもよく忘れてしまいます。
var self = this;
var elm = DisplayElement().addChildTo(scene);//シーンに紐付けられたDisplayElement
elm.setInteractive(true); //タッチ判定を付与
// タッチ開始時
elm.onpointstart = function() {
this //このthisはelmを指す
this.getParent() //これはsceneを指す
self //これで外側のthisを利用できる
}
phina.jsは画面に写す要素(DisplayElement)にタッチ判定を与えて、タッチされたときに呼ばれる関数を定義出来ます。このonpointstartはphina.js内でcall関数を拡張されて定義されているようで、このcall関数内でのthisはタッチ判定を付与したelmを指します。ひとつ上の例でもそうですが、外のスコープのthisを指したいときは、一旦var self = this;などとして変数に代入しておき、関数内でselfを用いて参照することが出来ます。あるいは、function(){関数の中身}.bind(this)としても良いようですね。
##変数はすべてvar で定義する
JavaScriptでは定義した変数に何を代入したかによって型が変わりますね。これは始めこそ戸惑いましたが、Matlabと似ていたので、わりとすんなり馴染みました。
Math.floor()は数値を整数にならしたいときに便利ですね。上記のように今回はドット絵をそのまま画面に出したかったので、そのときによく使いました。
また、この特徴、逆を返せばクラスの記述をせずに、オブジェクトを定義することが出来るため、ちょっとしたデータ構造を持つオブジェクトを作るときに非常に便利だなと感じました。配列の要素数が可変な点も扱いやすいと思いました。
#まとめ・感想
##phina.jsはJavaScript初心者でも使いやすかった!
phina.jsはオブジェクト指向な言語を一つでも触っていれば、僕のようなJavaScript初心者でも、制作者のphiさんの公開されているチュートリアルとGitHubに公開されているphina.jsのソースコードを照らし合わせることで理解、拡張が簡単に行えました。わからないところ、気になったところもtwitterやgitter上で気軽に質問できました。また、同じ製作者様による開発用のrunstantというサービスもとても使いやすかったです。
JavaScriptを触ったことがないけれどjsでゲーム作りたい、という方はぜひphina.jsからJavaScriptの世界に入門してみてはいかがでしょうか。