phina.js とは
ゲームが簡単に作れるライブラリです!
ゲームに限らずアプリやWEBサイトのちょっとしたアクセントなんかにも使えます。
修正履歴
2016/3/16修正
- マウスカーソルを最新版に対応
- BounceSoundのサンプルをjsdoitに変更
- VideoSprite のサンプルを修正
- CanvasRecorderのサンプルを修正
- Particle10k のサンプルをjsdoitに変更
まえがき
この記事でphina.jsについて解説してる点で疑問に思ったことやわからないことがあればコメントか、
@simiraaaaにDMかリプライで質問してくれればわかる範囲で答えます。
また、誤字脱字や間違っている点があれば、編集リクエストをお願いします。
すべてのサンプルでhttps://rawgit.com/phi-jp/phina.js/develop/build/phina.js
をScriptタグで読み込んでいます。
今はまだphina.jsの情報は検索してもあまり出てこないので、代わりにtmlibで検索すると、似たようなサンプルが出てきたりします。ちょっと使い方が違ったりしますが、だいたい同じなのでtmlibのサンプルを参考にphina.jsを書けることもあるかと思います。
解説に使うサンプル
どのサンプルも紹介するためではなく気まぐれで作ってたものなので、ソースコードが分かりにくいかもしれません。
サンプルのrunstant, jsdoitを開きながら読んでください。
簡単な順番に並べてます。下に行くほど難しく説明が雑になっていきます。
- Tweener: runstant
- MouseCursor: runstant
- VideoSprite: runstant
- AnimationGIF: runstant
- BounceSound: jsdoit
- Particle: runstant
- VideoSprite300: runstant
- Particle10k: jsdoit
Tweener
難易度3ぐらい
runstant
複数のTweenerを同時に実行するサンプルです。
解説
詳しい説明とかいらない!っていう人は適当に読み飛ばしても良いです。
気が向いたら読んでみてください。
Tweenerで並列アニメーションをする解説自体はこれとこれです。
おまじない
phina.globalize();
phina.jsの主要クラスをグローバルオブジェクトに展開しています。
この関数を実行すると
phina.display.Sprite
などが Sprite
でよくなります。
また、GameApp
が使用可能になります。
クラスの定義
phina.define('クラス名',メンバにしたいオブジェクト)
phina.define
で定義したクラスはnew
ありでもなしでもインスタンスを生成できます。
継承
superClass: 'クラス名'
シーンはCanvasScene
を継承してください。
コンストラクタ
init: 関数
インスタンスを生成するときに実行されます。
親クラスのコンストラクタ実行
this.superInit()
継承したら、基本的に使用します。
継承していて、特に意図がない場合は使ってください。
というのも、phina.jsのクラスはほとんど、phina.util.EventDispatcher
というクラスを継承していて、このクラスは初期化しないと使えないようになってるからです。
星
var star = StarShape({radius:100}).addChildTo(this).setPosition(100,100);
StarShape
は星を描画するSpriteみたいなものです。
radius:100
は半径を100pxで描画するということで、widthとheightは200pxぐらいになります。(Shapeはpaddingがデフォルトで8あって216pxぐらいになります)
addChildTo(this)
はthis
はシーンです。シーンの子に追加します。
追加すると画面に表示されるようになります。
なんか表示したいときはぜひaddChildTo(this)
使ってください。
setPosition(100,100)
はx,yに100をセットしています。
わかりやすく書き変えるとこうなります。
var star = StarShape({radius:100});
star.addChildTo(this);
star.x = 100;
star.y = 100;
アニメーションを追加
Tweenerはオブジェクトのプロパティを特定のアニメーション関数で徐々に変化させるためのクラスです。
もともとShapeやSpriteなどのクラスは
.tweener
で自身をアニメーションのターゲットに設定したTweenerインスタンスを参照できます。
しかし、これだと、並列でアニメーションすることができないので、このサンプルでは使っていません。
代わりにTweenerを配列に入れて、最後にattachで追加しています。
attachでアニメーションの対象を設定できます。
Tweenerの使い方
to(prop, time, easing)
by(prop, time, easing)
第一引数は、変化させたいプロパティを指定します。
第二引数は、変化にかける時間をミリ秒で指定します。
第三匹数は、どういう風に変化させるかEASINGの関数名を指定します。
phina.jsに入ってるeasingはphina.util.Tween.EASING
を見るとわかります。
省略すると等速直線運動になります。
toは現在の値に関係なく第一引数の値になります。
byは現在の値に第一引数の値を加算した値になります。
オブジェクトにもともと付いてるtweenerを使う場合
star.tweener.to({
x: 100,
y: 200,
}, 500, 'easeOutCirc');
この場合は、
easeOutCirc
という最後に急に加速する動きで、
500ミリ秒時間をかけて、star.x
は100 、 star.y
は200になります。
wait(time)
timeミリ秒待機します。何もしません。
set(prop)
setを使った時の時間でpropの値に書き換えます。
call(func)
callを使った時の時間にfuncを実行します。
star.tweener
.wait(500)
.to({x:100},500)
.set({y:100})
.wait(500)
.call(function(){
star.remove();
});
こう書くと
500ミリ秒待機したあと、
500ミリ秒かけてxを100に変化させて、
それが終わったらyを100にして、
500ミリ秒待機したあとに、starを削除します。
最初はややこしいですが、使ってれば慣れていくと思います。
並列Tweener
Tweenerを並列で使う場合は、並列にしたいだけTweenerを生成する必要があります。
サンプルのコードにコメントをつけると
//配列にTweenerのインスタンスをいくつか入れてtweenersに代入
var tweeners = [
Tweener() //tweener生成
.to({x:500},1000,'easeInOutBounce')//1000ミリ秒でxを500にする
.to({scaleY:2},400), // 400ミリ秒でscaleY(縦の拡大)を2にする
Tweener()
.wait(400) // 400待機
.set({fill:'green'})//緑にする
.to({
rotation:360, // 360度回転 :a
sides:30 // 星のトゲトゲを30個にする :b
},800,'easeInBack') // a,bを800ミリ秒かけて行う
.set({fill:'red'}), // そのあと赤くする
Tweener().wait(500) // 500待機
.set({stroke:'yellow',lineWidth:1}) //枠を黄色、枠の太さを1
.to({
scaleX:2, // 500かけて幅を二倍
},500,'easeInCirc'),
// 200 待機、1000かけてyを800にする
Tweener().wait(200).to({y:800},1000 ,'easeInOutElastic')
];
//starをアニメーションのターゲットにしている
tweeners.forEach(function(tweener){
star.attach(tweener);
// tweener.attachTo(star);という書き方でもできる
});
メイン関数
phina.main(func)
func
がwindow.onload
のタイミングぐらいに実行されます。
複数書くと書いた順番に実行されます。
GameApp
GameApp(options)
optionsは
{
width: 640,
height: 960,
startLabel: 'title',//title, main, resultなどがある
}
などを渡してください。
何も渡さなくても割と大丈夫です。
var app = GameApp({
startLabel: 'main'
});
これで、mainというラベルのシーンから開始するappを生成しました。
GameAppインスタンスを生成したときにMainSceneがmainラベルになるようになっています。
必ずMainSceneを作ってください。
app.fps = 60;
app.backgroundColor ='silver';
app.run();
fpsは60fpsです。1秒間に60回更新します。
backgroundColor は見ての通り背景色です。
run()はapp実行です。まず使わないことはないので、書いてください。
書かないと動きません。たまに忘れてあれ?ってなります。
MainScene
このサンプルでは最終的にMainSceneのinitが実行されます。
プログラムの流れは
- phina.defineでMainSceneクラスの定義
- HTMLのロード完了時phina.mainで登録した関数の実行
- GameAppを実行してMainSceneに移動
- MainSceneが生成されたので、initが実行される
- starのTweenerサンプルが動く
一気に理解するのは難しいかもしれないので、サンプルの値などを書き換えて遊びながら覚えてみてください。
MouseCursor
難易度5ぐらい
runstant
2016/3/16修正
オリジナルのマウスカーソルに変えるサンプルです。
マウスカーソルを変えているわけではなく、マウスカーソルを非表示にして、
ポインターの位置にカーソル用のスプライトを表示しています。
基本的な流れはTweenerと同じなので、説明してない部分だけ解説します。
cursorを非表示にする
app.interactive.cursor.normal = 'none';
app.interactive.cursor.hover = 'none';
マウスカーソルを非表示にしています。
カーソルのスプライトを作る
コメント書きました。
phina.define('CursorSprite',{
superClass:Sprite,
init:function(){
// 青い三角形
var s=TriangleShape({
fill:'skyblue',
stroke:'blue'
});
//三角形の描画
s.prerender(s.canvas);
s.render(s.canvas);
// 描画した三角形をSpriteにセットする
this.superInit(s.canvas);
// -40度回転
this.rotation = -40;
// くるくるカーソルのアニメーション
this.tweener.to({
scaleX:-0.5
},500,'easeInOutCirc')
.to({
scaleX:0.5
},500,'easeInOutCirc').setLoop(true);// ループ
this.scaleX=0.5;
},
});
CursorクラスはCursorSpriteの位置調整をしてるだけです。
マウスの位置に表示
MainSceneのinitの中身です
- csにCursorインスタンスを代入、シーンに追加
- enterframeで毎フレームcsが一番上に表示されるようにaddChildしている
- e.app.pointersはマウスやタッチの座標が配列で入ってます
- csの座標(x,y)を最後のポインターの座標にしています
- Button()は真ん中に表示されているHelloというボタンです。特に表示している意味はありません
- this.gridX(Y).center()でシーンの座標の中心の値が取得できます
var cs=this.cursor=Cursor().addChildTo(this);
this.on('enterframe',function(e){
this.addChild(cs);
e.app.pointers.forEach(function(p){
cs.position.set(p.x,p.y);
});
});
var shape = Button().addChildTo(this);
shape.x = this.gridX.center();
shape.y = this.gridY.center();
大まかな流れ
- クラスの定義(MainScene, Cursor, CursorSprite)
- main関数の登録(phina.main(func))
- HTMLの読み込み後main関数が実行される
- main関数でGameAppが生成
- startLabel: 'main'にしているので↓
- このタイミングでMainSceneが生成され、MainSceneのinitが実行され、現在のシーンがmainになる
- MainSceneのinitの中身に関しては前項で説明したとおり
- appのポインタのマウスの設定を'none'にしてマウスを非表示にしてる
- app.run() app実行 シーンの更新が開始される
VideoSprite
難易度7ぐらい
runstant
2016/3/16サンプル修正
Videoタグの動画をSpriteに描画するサンプルです。
流れは今までとほとんど同じです。
このサンプルでは、どちらかというとphina.jsよりVIDEOタグの操作の仕方の説明に近くなると思います。
このサンプルでのVIDEOの使い方はかなり雑なので、あまり参考にしない方が良いかもしれません。
動画の読み込み
VIDEOタグを生成
var v = document.createElement('video');
動画の読み込みが完了した時に実行される関数の登録
v.onloadeddata=func
動画のURLを入れると読み込みを開始する
v.src = "https://i.gyazo.com/9e4befc57463638f0be7b8d53211d58e.mp4";
GameApp の引数について
GameAppに渡した引数はGameSceneによって管理されているシーンの引数にも渡されます。
このサンプルでは、MainSceneの引数opで
{
startLabel:'main',
video:v
}
と同じオブジェクトを受け取っています。
なので、op.video
でmain関数内の変数v(VIDEOタグ)を参照することができます。
動画をSpriteに設定して表示する
2016/3/16一旦Canvasに描画してからSpriteに描画するように変更しました。詳細はサンプルを見てください
var sprite = Sprite({domElement:video},video.videoWidth,video.videoHeight).addChildTo(this);
Spriteの第一引数に{domElement:video}
というオブジェクトを渡してください。
第二引数、第三引数は幅と高さです。
動画自体の実際の幅と高さは videoWidth
videoHeight
から参照することができます。
生成時の引数にこれを渡すと綺麗に動画全体が描画されます。
生成後にsprite.width
を変更することで、動画が見切れることなくサイズの変更ができます。
scaleX, scaleY
を変更してもサイズを変更できます。
文字列の表示
普通最初に紹介するようなものですが、何故かこのタイミングでようやく紹介します。
var label = Label({
text:'クリックで再生停止',
fontSize:60,
})
これでクリックで再生停止という文字列を表示するためにShapeが生成されます。
あとからlabel.text = "HELLO!"
と変更すれば表示される文字はHELLO!
になります。
label.fontSize = 30
とすれば文字のサイズが30pxになります。(フォントによってサイズはまちまちですが)
改行は'\n'
でできます。
label.text = 'Hello!\nphina.js';
と書くと
Hello!
phina.js
と表示されます。
あとはsetPosition, addChildTo
などShapeやSpriteなどと同じメソッドが使えます。
クリックでのアクションを関数で登録する
MainSceneのクラスの定義のところで
onclick: func
を定義しています。
これだけで、シーンをクリックした時実行される関数が定義されます。
ほかには
-
onpointstart
マウスダウン、タッチ開始時 -
onpointend
マウスアップ、タッチ終了時 -
onpointmove
マウスの移動、タッチしながら移動
などがあります。
あとのコードは解説しませんが、そこまで変わった処理はしてないので、気が向いたら、読んで理解してみてください。
AnimationGIF
難易度10ぐらい
runstant
2016/3/16サンプル修正
phina.jsのアプリ画面からGIFアニメを生成するサンプルです。
gif.jsというライブラリを使用するので、Scriptタグで読み込んでおく必要があります。
http://cdn.rawgit.com/jnordberg/gif.js/v0.1.6/dist/gif.js
Label, CircleShape, Tweener
を使用していますが、今までに出てきた通り使ってるので、何が書いてあるのかある程度は理解できると思います。
GIFアニメを使うための初期化処理
現在のサンプルではこの処理は必要なくなりました
var createScriptURL = function(script){
var url = URL.createObjectURL(new Blob([script],{type:"text/javascript"}));
return url;
};
var WORKER_URL = createScriptURL(/*省略*/);
GIFアニメを作るときに、わからないときはこのサンプルの最初の方のこれをコピペしてから使ってください
GIFアニメを使うためのWorkerスクリプトのURL
2016/3/16追記
http://lite.runstant.com/asset?url=http://cdn.rawgit.com/jnordberg/gif.js/v0.1.6/dist/gif.worker.js
このURLをCanvasRecorderのオプションに指定してください。
runstantはhttp://lite.runstant.com/asset?url=[url]
とすることで対象のURLのコンテンツを強制的にlite.runstant.comのドメインのものとして使うことができます。
CanvasRecorder
CanvasRecorder
このクラスを使うとGIFアニメが簡単に作れます。
フルパスはphina.graphics.CanvasRecorder
です。
phina.globalize()
を使ってるので、CanvasRecorder
だけで良いですが。
おまじないしてないときはphina.graphics.CanvasRecorder
と書いてください。
displayとよく間違えます。
レコーダの生成
var rec = CanvasRecorder(this.app.canvas, options)
第一引数はappのcanvasを渡してください。
第二引数はoptionsですが省略可能です。
あとからrec.setOptions(options)
で設定しても同じです。
optionsに渡せるもの
- workers マルチスレッドの数 わからないときはいじらないでください
- quality クオリティ 低いほどクオリティが高くなる点に注意してください
- repeat リピート ループ再生にするか -1 しない 0 する
- workerScript WebWorkerのURL わからない場合はこのサンプル通りにしてください
- transparent 透過する色です。
0xFFFFFF
とすれば、白く描画されている部分を透過します
GIFアニメの生成が終了した時に実行される関数
rec.onfinished = func
生成されたGIFアニメを開く
rec.open()
GIFアニメの録画を開始
rec.start(fps, time)
fpsは保存間隔、timeは何ミリ秒録画するか です。
GIFアニメの録画を強制停止
rec.stop()
startに渡したtimeの時間に関係なく停止してGIFアニメ生成を開始します。
シーンが遷移した時に実行される関数
MainSceneで
onenter: func
が書いてありますが、これがMainSceneが現在のシーンになった時に実行される関数です。
initと違うのは、initはMainSceneのインスタンスが生成された時に実行されるのに対して、
onenterはシーンが変更されてMainSceneになったときに実行されます。
initでappを使いたいとき、this.appでは参照できません。
そういう時に、onenterでthis.appからappを参照する場合によく使います。
あとは、initのタイミングではなくシーンが変わるタイミングで確実に実行したい処理などで使います。
このサンプルでは、GIFアニメの録画をする一連の処理をonenterでやって、
initでは、CircleShapeやLabelを作ってTweenerしたりしてます。
シーンの更新
MainScene
update:func(app)
updateを書いておくと毎フレーム関数を実行します。
引数appはmain関数のGameAppのappと同じです。this.appも同じです。
マウス、タッチ、キーボードなどデバイスの処理
マウス、タッチ、キーボードなどの処理は、appのメンバから行えます。
マウス
app.pointer
タッチ
app.pointer
マウス、タッチ(複数)
app.pointers
キーボード
app.keyboard
マウスとタッチは同じpointer, pointers
ですが、マウスやタッチを検出して自動的に変更されるので、PCやスマホでも同じように書くことができます。
pointer と pointers の使い方
毎回app.pointer
と書くのは面倒なので、ここでは、p
という変数に代入して説明します。
var p = app.pointer;
今マウスダウン、タッチスタートされたか
p.getPointingStart()
タッチした瞬間などにtrueになります。
今マウスが押されてる、タッチされているか
p.getPointing()
タッチしているとき、マウスを押しているときtrueになります。
今マウスアップ、タッチを離したとき
p.getPointingEnd()
離したときtrueになります。
現在の座標
p.x
p.y
前フレームと現在のフレームの移動距離
p.dx
5px左に移動していれば -5, 10px右に移動していれば 10 が入っています
p.dy
5px上に移動していれば -5, 10px下に移動していれば 10 が入っています
x, y, dx, dy
などの座標は全て、アプリケーション画面の拡大縮小を考慮した値が入っているので、そのあたりは気にしなくても大丈夫です。
pointersはこうやって書いてください
if(app.pointers.length){
app.pointers.forEach(function(p){
// pointerと同じように書く
});
}
else{
// 何もタッチされていないとき
}
keyboardの使い方
var k = app.keyboard
とします。
キーが押されたとき
k.getKeyDown(key)
押されたときtrue
keyはキーボードの文字です。
Aなら'a'
でも'A'
でも良いです。
ENTERキー, RETURNキーなら'enter'
でも'return'
でも良いです。
そんな感じです。
キーが押されているとき
k.getKey(key)
押しっぱなしの時にtrue
キーを離したとき
k.getKeyUp(key)
離したとき時にtrue
このサンプルでは、
毎フレーム
マウスダウンかタッチスタートか
ENTERキー(RETURNキー)が押された時に、
GIFアニメの録画を強制終了するように
updateの関数で書いています。
BounceSound
難易度16ぐらい
jsdoit
Tweener
のeaseOutBounce
のバウンドするタイミングで音を出すサンプルです。
応用すればeaseInBounce
やeaseOutElastic
でびょんびょんするタイミングで音を出すとかできると思います。
音の読み込み
ここでassetsとLoadingSceneを使った音の読み込みを説明できたら良かったんですが、サンプルがアレなので、音の読み込みに関しては説明を省きます。
音の読み込みが完了したら、app.run()してます。
iPhone Safari で音を出す
phina.jsでは音の再生にWebAudioAPIを使用しています。
このAPIで音を再生するときiPhoneではtouchend
イベント内で一度何かしら音を再生しないといけません。
その処理がmain関数内のこれになります
var locked = true;
var f = function(e){
if(locked){
var s = phina.asset.Sound();
s.loadFromBuffer();
s.play();
s.volume=0;
s.stop();
locked=false;
app.domElement.removeEventListener('touchend', f);
}
};
app.domElement.addEventListener('touchend',f);
何をしているかというと、touchend時にポーという音を音量0で再生してすぐ停止しています。
あと、このサンプルでは忘れていますが、locked=false;
の次の行にapp.domElement.removeEventListener('touchend', f);
と書いて、一度実行されたら、無駄にこのイベントが発生しないようにします。
GameApp の引数の追記
{
title:'タイトル'
}
でTitleSceneで表示されるタイトルを設定できます。
このサンプルではいきなりstartLabel:'main'
なのでいきなりMainSceneに飛びますが、
startLabel:'title'
とするか、startLabelを書かなかった場合は
easeOutBounce
の
タイミング
で
音を出す
のというタイトルが表示されたシーンから開始されます。
BoundBall クラス
コンストラクタのx, y
の位置に何かしらの図形を出して、落ちてバウンドさせるクラスです。
コードが汚すぎて読みにくいですが、それなりに解説します。
配列の要素をランダムに選択して返す
array.pickup()
です。
このサンプルでは
['Circle','Triangle','Polygon','Heart','Star'].pickup()
と書いて、これらの文字列をランダムにひとつ返すようにしています。
Shape について
返ってきた文字列に + 'Shape'
としてwindow['なんとかShape']
となるようにしています。
なんとかShapeはphina.jsで最初から定義されている図形描画クラスで、以下のものがあります。
- Shape
- CircleShape
- TriangleShape
- PolygonShape
- HeartShape
- StarShape
- RectangleShape
RectangleShape以外はradius(半径)を設定すれば、その半径で描画されます。
- fill 塗りつぶしの色 falseで塗りつぶさない
- stroke 枠の色 falseで枠なし
- shadow 影の色 falseで影なし 初期値 false
オブジェクトの描画モード
obj.blendMode = mode
objはShapeやSpriteなどの描画されるものです。
あまり変更することはありませんが、変更するとしたら、よく使うものは 'lighter'
です。
描画モードに何があるのかは、ここに書いてあります。
http://www.html5.jp/canvas/ref/property/globalCompositeOperation.html
範囲指定で乱数を返す関数
Math.randint(min, max)
min から max の範囲の整数をランダムに返します。
min と max は範囲に含みます。
Math.randint(0, 1)
なら0か1です
他には
Math.randbool()
true か false
Math.randfloat(min, max)
小数点ありで範囲内の乱数を返す
などがあります。
BounceSound クラス
BoundとBounceはその時の気分で決めたので、使い分けに理由はありません。
phina.accessory.Accessory
を継承しています。
オブジェクトにattachTo
できるクラスです。
update(app)
はattachされた時に毎フレーム実行される関数です。
update
内でバウンドするタイミングがtweener
の'easeOutBounce'
と一致した時に音が鳴るようにごちゃごちゃやっています。
イベントの発火
phina.util.EventDispatcher
を継承してるクラスは、次のメソッドが使えます。
this.flare(イベント名, prop)
で発火できます。
発火すると
this.on(イベント名, func)
this.onイベント名 = func
で登録してある関数を実行します。
第二引数にはイベントオブジェクトに引っ付けるプロパティを渡します。
例
this.ontest = function(e){console.log(e.text);};
// ontestが実行されて、コンソールに'イベントテスト'が出力される
this.flare('test', {text:'イベントテスト'});
第二引数は省略できます。
Particle
難易度20ぐらい
難易度20ぐらいですが、反発とか摩擦とかのアルゴリズムとかは覚えなくていいことが前提で、20です。
phina.jsを使うのと、こういった物理的な知識は切り分けて考えた難易度です。
なので、そのへんの説明は省きます。
というより、tmlibのサンプルを移植しただけなので、あまり詳しく説明できません。
というわけで ParticleのサンプルですがParticleクラスの説明はしません。
app.isMobile()
app.isMobile()
でモバイルかどうかわかります。
PCならfalseでモバイルならtrueです。
app.enableStats()
Statsライブラリを読み込んでページ上に表示します。
fpsや1フレーム何ミリ秒かかってるかなどを表示してくれますが、こいつ自体が結構重いです。
このサンプルではモバイルでStatsを表示しないようにしています。
app.isMobile() || app.enableStats()
は
if(!app.isMobile()){ app.enableStats();}
と同じです。
この書き方は、私はよく使いますが、読みにくいのであまりオススメしません。
number.times(func(i), self)
ループです。
第一引数はループ処理の関数
第二引数はループ関数内でthisになるオブジェクトです。省略可
(5).times(function(i){
//i は 0 から 4
});
5.times(func...
と書くとエラーになります。
5..times(func...
はOK(5.0.times
として解釈される)
var n = 5; n.times(func...
はOK
サンプルでは(PARTICLE_NUM).times
と()で囲んでいますが、囲まなくても大丈夫です。
obj.$extend(ex[,ex2[,ex3...)
obj
のプロパティにex
のプロパティを追加します。
引数は複数渡せます。
obj
はJavaScriptのObject
全て(null, undefined
以外)です。
例
var obj = {a:1};
obj.$extend({b:2});
この例ではobj
は{a:1, b:2}
となります。
Math.DEG_TO_RAD
角度の表現を度数法から弧度法に変換するために使います。
360度のやつに乗算するとラジアンに変換されます。
わからなかったらこことかここを読んでください。
VideoSprite300
難易度25ぐらい
これは人によっては簡単だと思うかもしれません。
やってること
- VIDEOのイメージを一回Shape(CANVAS)に描画(丸く切り抜いてる)
- そのCANVASをSpriteにセット
- Sprite300個表示
丸く切り抜く処理はシーンのonenterframe
でやってます。
その中の c.arc(r,r,r,6.48,false)
は何やってるのかと思ったら、引数間違えてて、これで丸くなったからOKってそのままにしてあったやつです。
円のパスを設定する正しい引数は
c.arc(r,r,r,0,2*Math.PI,false);
こうです。
これがやりたかっただけです。
サンプルが間違ってる可能性もあるので、そういうところも注意してみると良いかもしれません
ということを書くためにわざと間違えてありました(嘘)。
擬似乱数クラス
phina.util.Random
クラス
おまじないしてる時はRandom
です。
擬似乱数は予測可能、再現可能な乱数です。
基本的な使いかたは、Math
のランダムと同じです
random()
randint()
randbool()
randfloat()
同じ結果の乱数を得る方法
Random.seed
に任意の値をセットしてから、
Randomのメソッドを実行すると同じ結果が得られます。
メソッドを実行するたびにseedが変化します。
Random.seed = 12345;
var a = Random.random();
var b = Random.randint(0,100);
Random.seed = 12345;
console.log(Random.random() === a); //true
console.log(Random.randint(0, 100) === b); //true
現在のシーン
app.currentScene
Particle10k
難易度100ぐらい
ネタなので、覚えなくても良いです。
これ覚えるより、WebGLを勉強したほうが良いと思います。
phina.jsでも、WebGLやThree.jsが使えるので、そっちで頑張ったほうがもっと高速描画ができると思います。
パーティクルを10000個表示するサンプルです。
phina.jsの機能をあんまり使ってないので、全然説明することがないです。
Ticker
appの更新を行ってます。
app.ticker
にTickerのインスタンスがあります。
var t = app.ticker
とします。
t.fps
更新頻度です。app.fps
は実はこれです。
t.tick(func)
fpsの頻度で実行する関数の登録です。
t.start()
更新を開始します。
tickerの使い道は、例えばappの更新とは別で毎フレーム更新したい処理をapp.ticker.tick(func)
で登録しておくと、app更新のタイミングでfunc
が実行されます。
app.run()
の後に書くと、func
はapp
の更新後に実行されます。
app.run()
の前に書くと、app
の更新前に実行されます。
描画の更新のタイミング
描画の更新のタイミングは、描画以外の全ての更新処理が終わったあとです。
app 各更新処理のタイミング
1フレームごとの大まかな更新の流れ
- update系
- interactive系
- 描画系
update系の流れ
- enterframe
- update
update系は シーンから始まって、
シーンにaddChildTo(scene)
などで追加されているオブジェクトを辿って更新しています。
A.addChildTo(scene);
B.addChildTo(A);
C.addChildTo(D);
D.addChildTo(A);
と書いた場合、
この順番に実行されます
- scene.onenterframe
- scene.update
- A.onenterframe
- A.update
- B.onenterframe
- B.update
- D.onenterframe
- D.update
- C.onenterframe
- C.update
interactive系の流れ
interactive系が何をしてるかというと
point系のイベントを管理していて、毎フレームupdate系が終わったあとにマウス、タッチの情報から対象のオブジェクトのpoint系イベントを発火させています。
このイベント発火の順番もupdate系と同じように最初にシーンから行われて、順番に追加されているオブジェクトを辿っていきます。
point系イベントの種類
- pointstart マウスダウン、タッチスタート
- pointend マウスアップ、タッチエンド
- pointmove マウス移動、タッチ移動
- pointstay マウスを押している状態、タッチしている状態
- pointover マウスが要素の上にある状態、要素上をタッチしている状態
- pointout マウス、タッチが要素の上にある状態から、要素の外に出た
interactive系の管理対象(マウスやタッチイベントを発火させる対象)にする要素は
elm.interactive = true
か
elm.setInteractive(true)
としてください。
シーンやボタンは最初から管理対象になっています。
逆に管理から外したい場合はelm.interactive = false
にしてください。
updateとdrawを分けた理由
updateとdrawの速度を計測しやすくするために分けました。
あと、描画が60FPS間に合わなくても、updateは60FPSで更新しておけば、60FPSで動いてるように見えるかもしれないと思ったからです。
requestAnimationFrame
(以下RAF)を使用して、色々発見があったので、書いておきます。
- 変な処理落ちが減った
-
setTimeout
よりもなめらかに見える - drawが遅い時は結局次フレームのupdateするタイミングに間に合わないので、updateのFPSも一緒に落ちる
- 描画と描画以外の更新処理は完全に切り分けて非同期に実行してもほぼ問題がないことがわかった(描画にupdateの処理を持ち込まない、updateに描画の処理を持ち込まないという設計にすればの話)
変な処理落ちが減ったというのは、1万個パーティクル表示していると50FPSぐらい出てたのが急に何かに引っかかったように5FPSぐらいしか出なくなることがありました。
この現象が発生しなくなったということです。
多分、画面の更新のタイミングと運悪く処理のタイミングが噛み合わなかったりしてPCに負荷がかかっていたのかな?という超適当な憶測で勝手に納得しています。
setTimeout
より滑らかに見える というのは、そのままの意味です。
phina.jsはsetTimeout
で更新していますが、RAFにした方が、なめらかに見えました。
多分、描画しても画面が更新されていないタイミングなので、ちょこちょことフレームが飛んでるのがsetTimeout
で微妙になめらかに見えない原因だと思います。
描画と描画以外の更新処理の完全な切り分け、住み分けについては、不可をうまく分散してFPS稼げないかな?
という思惑があります。
Particleについての説明は一切してませんが、このサンプルの解説は終わりです。
あとがき
後半はかなり端折りました。
気になる点など質問がありましたら、書こうと思っています。
コメント、質問等は遠慮せずにしてもらって構いません。
サンプルから解説したので、脈絡とか一切ない感じになってわかりにくかったかもしれませんが、ここまで読んでくれてありがとうございました。