【enchant.js】Hello Physical World vol.4【box2d】
4月は更新できずに申し訳ありません。
「Hello Physical World」第4回目は、インスタンス生成時の最後の引数、isSleeping についてです。
isSleeping について、まずリファレンスを確認してみましょう。
{Boolean} isSleeping Optional Spriteが初めから物理演算を行うか.
う~ん、ちょっとイメージしづらいですね。
それなら実際にコードを書いて実験してみましょう。
まずは isSleeping を true にしたパターン。
はい、すーっと落ちてゆきますね。
今度は isSleeping を false に変えて実行してみましょう。
var box = new PhyBoxSprite(96, 16, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.2, 0.0, false);
……どうなりましたか?
さっきは落ちていったブロックが動かなくなりましたね。
はい、isSleeping はゲーム開始時から物理演算を適用するかどうか指定する引数なんですね。*1
それでは、この Sprite に物理演算を適用させるにはどうすればいいのでしょうか。
ちょっと下のコードを書き足した後、Sprite をタップしてみて下さい。
bar.ontouchstart = function() { this.setAwake(true); };
タップすると Sprite が落ちるようになりましたね。
ここで利用した setAwake 関数は、引数で物理演算の適用を ON/OFF するものなんです。
物理シミュレーションされていない時、物理シミュレーションを行う(sleep時は動かなくなるので) Parameters: {Boolean} flag 物理シミュレーションを行うかどうか
Sprite に物理演算を適用させる方法はもうひとつあります。
今度は下のコードを書き足して実行してみて下さい。
var box = new PhyBoxSprite(16, 16, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.2, 0.0, true); box.image = core.assets[MAP2]; box.x = (320 - 16) / 2; box.y = 16; core.rootScene.addChild(box);
はい、他の Sprite がぶつかると自動的に物理演算が適用されるようになるんですね。
こうなってくると、Sprite の物理演算適用状態を調べる方法も知りたくなりますよね。
リファレンスを見てみると、その名も sleep というプロパティが紹介されています。
sleep 物理シミュレーションされているか
ちょっと見てみましょう。
var sleep = new Label(String(bar.sleep)); core.rootScene.addChild(sleep); // onenterframeの中に記述 sleep.text = String(bar.sleep);
……変わらないですね。ずっと true のまま。
ざっとソースコードを確認したところ、原因はbox2dwebの側にあるです。
ちょっと手が出ない(というか出したくない)領域ですね。
他の方法を模索してみましょう。
まずぱっと浮かぶのが、Sprite の速度が格納されている vx 及び vy を調べる方法です。
vx == 0 && vy == 0
ふたつの加速が0であれば Sprite は静止状態にある――つまり物理演算の影響を受けていないと見なすことができます。
そしてもうひとつ―― setAwake があるなら isAwake もあるのではと思った方、鋭い!
isAwake は私も『チョコレート・ドロップ』で使用した関数で、sleep とは異なり Sprite は静止状態をきちんと確認できます。
ただ、残念なことに isAwake にアクセスするには、長い回廊を下らなければなりません。こんな感じです。
Sprite.body.m_body.IsAwake();
今回でインスタンス生成時の引数については終りです。
次回は Sprite に動きを加える関数について説明したいと思います。
*1:isSleeping(寝ているか?)なんだから、true と false が逆でもいいような気がします。
【enchant.js】Hello Physical World vol.3【box2d】
「Hello Physical World」3回め。
今回のお題は摩擦係数です。
摩擦とは何か、例によってWikipediaを引いてみましょう。
二つの物体が接触している際に、その接触面に平行な方向に働く力。
ちょっとわかりにくいですね。もうすこし読み進めてみるとこんな箇所があります。
質量をもった物体が動いているとき、その物体の進行方向逆向きに働く力を摩擦力(Frictional Force)という。
「進行方向逆向きに働く力」!
うん、わかりやすいフレーズです。学校でもそんな感じで習ったような気がします。
ではさっそく例を見てみましょう。
画面をタッチして下さい。すべて右にブロックが進みますが、その移動量は異なっていますね。
これは床の摩擦係数がそれぞれ異なっているからです。
ブロックの摩擦係数と画面をタッチした時にブロックに加えられる瞬発力はすべて同じですが、
床の摩擦係数が上から0.5、1.0、2.0となっています。
それでは、このソースをforkして、ブロックの摩擦係数を0.0にしてみて下さい。
……どうでしょうか。すべて同じ動きになりましたね。
摩擦力は、接触するオブジェクト同士の摩擦係数をかけた値となります。
従って、今の例ではブロックの摩擦係数を0.0としたため摩擦力も0.0となり、すべて同じ動きをするようになったのです。
壁にぶつかるたびに速度が遅くなるのは、壁の反発係数に由来するものです。
もうひとつ例を見てもらいましょう。これは摩擦力がすべて同じになるパターンの例です。
というわけでおさらい。
- 摩擦力は、接触するオブジェクト同士の摩擦係数をかけた値である
以上でした!
【enchant.js】チョコレート・ドロップ【box2d】
新作、「チョコレート・ドロップ」を投稿しました。
今回の特徴は多角形(三角形とか台形)を利用しているところでしょうか。enchant.jsの公式pluginのbox2d.enchant.jsではなく、ベースとなったPhySprite.enchant.jsを使用しています。PhySprite.enchant.jsでは多角形の他、Box2dの目玉のひとつ(だと思っている)ジョイントも利用可能です。box2d.enchant.jsでは物足りなくなった人は使ってみてはいかがでしょう。*1
*1:ただしv.0.6以降では利用できません。
【enchant.js】Hello Physical World vol.2【box2d】
さて。
お待たせしました。「Hello Physical World」2回めです。
今回はちょっとやっかいな反発係数を取り上げようと思います。
反発係数とは! 2物体の衝突において、衝突前の互いに近づく速さに対する、衝突後の互いに遠ざかる速さの比のことである!
ようするに、物体が跳ね返ってきた時の、元のスピードとの比率ってわけですね。
例えば10mm/sで飛んでいった物体が、壁にぶつかって8mm/sで戻ってきたとする。その時の反発係数は10:8、つまり0.8となります。式にするとこんな感じですね。
戻ってきた時のスピード = 元のスピード * 反発係数
反発係数は、通常0.0~1.0の値を取ります。プログラム上、2.0とか設定出来ますが、それは10mm/sで飛んでいった物体が20mm/sで戻ってくるとかステキな結果が待ってます。
まず前回のコードを引っ張り出してきましょう。そんでもって床の反発係数を0.0にしてみて下さい。
floor = new PhyBoxSprite(320, 16, enchant.box2d.STATIC_SPRITE, 1.0, 0.5, 1.0, true);
これを
floor = new PhyBoxSprite(320, 16, enchant.box2d.STATIC_SPRITE, 1.0, 0.5, 0.0, true);
こんな感じですね。動きに変化は感じられないですね。
続いて床の反発係数を1.0に戻し、リンゴの反発係数を0.0にしてみて下さい。
var apple = new PhyCircleSprite(8, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.5, 0.0, true);
これも動きが変わりませんね。
はい、重要なポイント。
反発係数は値の大きな方が採用され、小さな方は無視されます。
これはBox2Dの基本的な動きなので覚えておいて下さい。
……さて。
みなさまのお手元で実行中のプログラム、リンゴはどこへ行ったでしょうか? そろそろ画面の外に飛び出している頃じゃないでしょうか。
私、冒頭でこう書きましたよね?
戻ってきた時のスピード = 元のスピード * 反発係数
しかしこの動きはどう見てもこうです。
戻ってきた時のスピード = 元のスピード * 反発係数 + α
「ウソつき!」って声が聞こえてきそうです。
ウソなんですよ! 私もびっくりしましたよ! 「運動量保存の法則」無視ですよ! 永久機関の完成ですよ!
果たしてこのαはどこから来るのか? box2d.enchant.js*1の製作者、@kassy708さんに面白いお話をうかがうことができました。
@v416 戻す"力"というよりは、例えば時刻t=0の時高さy=10の球が重力加速度g=3で落下するとき地面の高さが0だとすると、球はt=3の時に速度v=-9でy=-8となり地面にめり込みます。その時、反射としてv*=-1の処理を行います。 ...続きます。
2013-01-17 20:20:11 via web to @v416
@kassy708 @v416 この時、めり込んだ球を地面表面に戻す処理を行うと、y=0の時にv=9となります。するとt=6にv=0となり、y=18の高さまで上昇することになります。戻す処理をしなければt=6の時はy=10となります。 とこんな感じで予想してます。
2013-01-17 20:20:51 via web to @kassy708
めり込んだ分、yの座標を補正する。その補正分がαとなっているようなのです。
いやはや、ややこしい話であります。
さて。まとめです。
- 反発係数はぶつかった時に戻ってくるスピードを決定するための値
- 範囲は0.0~1.0
- 係数が異なる同士がぶつかったら、大きい方の係数が採用される
- めり込んだ分の補正が加わる
以上です。
*1:正しくはPhySprite.enchant.js
Sublime Text 2のsnippetを試してみた
Sublime Text 2にはsnippetという、まぁ一種の自動補完のような機能があります。
snippetの詳しいことはテキトーにググって下さい。
で、こちらを参考にひとつ試しに作ってみました。
<snippet> <content> <![CDATA[ enchant(); window.onload = function(){ var game = new Game(320, 320); game.fps = 30; game.preload(); game.onload = function(){ ${0} }; game.start(); }; ]]> </content> <tabTrigger>enGame</tabTrigger> <scopre>source.javascript</scopre> <description>enchant Game</description> </snippet>
はい。これで何ができるかというと、enGameと入力することで""でくくられた部分が入力され、あまつさえ"${0}"のところにカーソルが移るのです!
非常に便利です!
9minits Battleもこれで万全です!
「第肆回天下一カウボーイ大会」で生き恥をさらしてきましたよ
2月3日に行われた「第肆回天下一カウボーイ大会」に行って来ました。すごかったですね! どれもこれも魅力的な講演で、もう、本当に聞き入ってしまいましたよ。
実はですね、本当は行く気なかったんです。
「数学とかわかんねーし、どーせ聞いてもワケワカメ」とナハから無関心装ってたんです。でももしかしたらenchantMOONには触れるかもと二次会には申し込んでましたが。
そんな私が何故カウボーイ大会に行くどころか登板までしてしまったのか。
それは大会の1周間前に、UEIさんから届いたメールから始まりました。
9 Minutes Coding Battleやんね?
挑戦状キタ━━━━(゚∀゚)━━━━!!*1
やらないかと問われれば、応と答える以外にありましょうや、私はささっと了承の旨を返信しました。
「9 Minutes Coding Battle」とは9分間でゲームを作ろうというお馬鹿な企画です。無理難題な企画ではありますが、ライブラリだとかforkだとかを駆使すれば何とかなります。
というわけで、その週は準備に明け暮れました。中でも自信作(?)がこちらです。
これは文字列をSpriteとして生成するクラスです。
var enemy = new TChar("敵", 32);
とすると、どまんなかに「敵」と書かれた幅32(くらい)x縦32のSpriteを生成します。ダミーの画像ファイルを置いたりする時便利です。今にして思うとLabelで事足りたなとは思いますが。*2
こうして迎えた「第肆回天下一カウボーイ大会」。詳しい内容はリンク先の詳細レポートに譲るとして……すごかったです。詳しいことはやっぱりよくわからないのですが「こういう技術が研究されており、こういう未来が考えられる」と見せられるのは心が踊ります。センス・オブ・ワンダー、それを体感した1日でした。
さて。
「9 Minutes Coding Battle」の結果はどうだったかというと……もう散々でしたw 私は今回、jsdo.itを利用するつもりだったのですが、何と壇上でWiFiにつながらない! 回してくれた有線ケーブルもダメ! 結局デスクトップにあったフォルダをコピってガガガッとコーディングしましたが、完成には至りませんでした。面白いかどうかはさておき、完成させる気ではいましたから残念です。――が、日高さんが「よいちさん!」と名前呼んでくれたのですべてチャラです。私的に。
二次会も面白かったです。中でも、大会内で「時間切れ」のため発表を断念せざるを得なくなった安達真さんのtumblrに関する発表の続きを見られたのはよかったです。
そうそう、enchantMOONのデザインを手がけた@abfly先生にサインを頂いてしまいました!
本当にすばらしい大会でした。
参加する機会を与えてくれたUEIのみなさま、本当にありがとうございました。
次回はハナからチケット押さえるよ!
【enchant.js】アトラスXの例えばこんなハック【GGJ】
先日のGGJ、皆様お疲れ様でした。
同じチームとなった@sparrow_Pさんと@Kadesh_Laurantさん、本当にお疲れ様でした。一緒にゲームを作ることが出来て、しかも完成にまでこぎつけることが出来てほんとうによかったです。
GGJ内のサイト。
Game: Earthly Star | Global Game Jam
jsdo.it内のコード。コードの中身はこちらから。
Earthly Star 〜地上の星〜 - jsdo.it - Share JavaScript, HTML5 and CSS
プレイはこちらから。
さて。
「Earthly Star」はGPSを利用した一種のARGです。
私たちが目指したのは、登録された方向に登録された距離を移動したらイベントが発生するという、相対座標を使ったシステムでした。
一方、今回NII会場主催の@mnagakuが用意してくれた「ARG用アトラスX改」は事前に目的地となる場所の座標を登録しておき、プレイヤーがその座標についたらイベントが発生するという、絶対座標を使ったシステムです。
今にして思えば「ARG用アトラスX改」をいじらなくてもよかったのですが(今、その方法思い付いた orz)、その時は「こりゃ、中のイベント発生のところをいぢらないとどうにもならんなぁ」ということでハックすることにしました。
ではどのように改造したのか。
それをこれから書いてゆこうと思います。
「ARG用アトラスX改」には、位置情報を取得してマップや現在座標を表示する「プロローグ2nd」という関数がCSSに存在します。該当箇所をまるっと書き出してみましょう。
プロローグ2nd = function(){ セーブ("プロローグ"); if(現在位置が取得できた == NO){ クリア(); 台詞("現在地が取得できませんでした"); 台詞("再試行します"); 一時停止(); 現在位置を取得して次へ(プロローグ); return; } if(typeof debug == "undefined" || debug != "nogps"){ 画像("http://maps.google.com/maps/api/staticmap?center="+現在の緯度+","+現在の経度+"&size=320x320&sensor=false&zoom=18"); 現在地 = {経度:現在の経度, 緯度:現在の緯度}; 舞台 = "なし"; 遊ぶ範囲から外れている = YES; 最も近いエリアまでの距離 = 40000000; 最も近いエリアまでの方位 = ""; 遊ぶ範囲.forEach(function(エリア){ 現在の距離 = 距離(エリア, 現在地); if(現在の距離 < エリア.半径){ 遊ぶ範囲から外れている = NO; } if(現在の距離 < 最も近いエリアまでの距離){ 最も近いエリアまでの距離 = Math.floor(現在の距離 - エリア.半径); 最も近いエリアまでの方位 = 方位(現在地, エリア); } }); イベント場所.forEach(function(エリア){ 現在の距離 = 距離(エリア, 現在地); if(現在の距離 < エリア.半径){ 舞台 = エリア.場所; } }); }else{ 舞台 = place; 現在の緯度 = 0; 現在の経度 = 0; 位置情報の正確さ = 0; 画像("http://maps.google.com/maps/api/staticmap?center="+現在の緯度+","+現在の経度+"&size=320x320&sensor=false&zoom=18"); } クリア(); セリフ("現在地"); セリフ("緯度:"+Math.round(現在の緯度*1000000)/1000000); セリフ("経度:"+Math.round(現在の経度*1000000)/1000000); accstr = "m"; if(位置情報の正確さ >= 100)accstr = "m(遊ぶのには不安な精度です)"; セリフ("誤差:±"+位置情報の正確さ+accstr); if(typeof debug != "undefined"){ セリフ("舞台:"+舞台); for(f in フラグ) セリフ(f+":"+eval("フラグ."+f)); console.log("プレイヤーの情報"); str = ""; for(f in game.memory.player) if(eval("typeof game.memory.player."+f) != "function") str += f+":"+eval("game.memory.player."+f)+", "; console.log(str); console.log("友達の情報 x " + 友達の数); for(i = 0; i < 友達の数; i++){ str = ""; for(f in game.memories.friends[i]) if(eval("typeof game.memories.friends[i]."+f) != "function") str += f+":"+eval("game.memories.friends[i]."+f)+", "; console.log(str); } } if(eval("typeof "+フラグ.進行+"_"+舞台+" != 'undefined'")){ eval("次へ("+フラグ.進行+"_"+舞台+");"); return; }else if(eval("typeof "+フラグ.進行+" != 'undefined'")){ eval("次へ("+フラグ.進行+");"); return; } if(舞台 != "なし"){ セリフ("何かの気配は感じるのだが..."); }else if(遊ぶ範囲から外れている==YES){ セリフ("現在地は、このゲームを遊ぶ地域から外れています"); セリフ(最も近いエリアまでの方位+"に "+最も近いエリアまでの距離+"m 移動すると"); セリフ("ゲームを遊ぶ地域に戻れます"); }else{ セリフ("ここでは何も起こらないようだ..."); } 選択肢("ゲームをリセット",リセット); 選択肢("次の場所へ",プロローグ); };
ポイントとなるのは「イベント場所」にforEachかましてるところです。抜粋。
イベント場所.forEach(function(エリア){ 現在の距離 = 距離(エリア, 現在地); if(現在の距離 < エリア.半径){ 舞台 = エリア.場所; } });
「イベント場所」とは「ARG用アトラスX改」ではなく、「一ツ橋クエスト」や「Earthly Star」など、ゲームの方で定義されたグローバル変数です。
ちょっと「一ツ橋クエスト」の中身をのぞいてみましょう。
イベント場所 = [ {場所:"NII入口", 経度:139.757736, 緯度:35.692760, 半径:50}, {場所:"コンビニ", 経度:139.759445, 緯度:35.692841, 半径:30}, ];
と、このようなオブジェクトの配列です。
forEachは配列のすべての要素に対し、指定された関数を実行するというモノです。この部分で現在地と登録されている絶対座標とを比較してるんですね。つまり、ここでチェックする内容を変更すれば、目的を達成できるわけです。
では、まず「イベント場所」の内容を検討してみましょう。
相対座標を得るにはどうすればいいのか。経度、緯度、それぞれにどれだけ動いたかを持たせる。これを言い換えると、とある方位にxメートル移動ってことになります。
というわけでこんなんなりました。
イベント場所 = [ {場所:"北", 経度:0, 緯度:0, 半径:20, 距離:50, 方位:"北"}, {場所:"北西", 経度:0, 緯度:0, 半径:50, 距離:100, 方位:"北西"}, {場所:"西", 経度:0, 緯度:0, 半径:50, 距離:150, 方位:"西"}, ];
経度と緯度は不要だったのですが、知らない所で参照されても嫌なのであえて残しました。
次はforEachの中身ですね。
まずはスタート地点から見て、現在どの方位にいるのか、どれだけ移動したのかを知りたい。
移動距離については問題ないですね。2つの座標の距離を求める、「距離」なんていう身も蓋もない名前の関数が用意されています。
次は方角ですが、実はこちらも「方角」という関数が「ARG用アトラスX改」には用意されています。
そんなこんなでこんな感じ。
イベント場所.forEach(function(エリア){ 現在の距離 = 距離(フラグ.起点, 現在地); 現在の方位 = 方位(フラグ.起点, 現在地); if (現在の方位 == エリア.方位) { if (エリア.距離 - エリア.半径 < 現在の距離 && 現在の距離 < エリア.距離 + エリア.半径) { 舞台 = エリア.場所; } } });
「フラグ.起点」ってなんじゃー!
はい、そいつがスタート地点の座標です。
それではこのスタート地点の座標をいつ取得するのか、そこいらへんにいきましょう。
ゲームを起動すると、まず必ず位置情報を取得にいきます。つまり「プロローグ2nd」が呼ばれるわけです。だったらその時に現在座標をスタート地点として登録すればいい。なので「現在地」を取得している真上でこんなん書きました。
if(フラグ.起点 == undefined){ フラグ.起点 = {経度:現在の経度, 緯度:現在の緯度}; }
登録はこれでOKです。次は初期化ですね。
「プロローグ2nd」の最後を見ると「選択肢("ゲームをリセット",リセット);」という行がありますね。ここで呼び出される「リセット」関数の中で「フラグ.起点」を初期化してやればOKですね。「リセット」は「プロローグ2nd」の次に定義されています。
リセット = function(){ フラグ.進行 = "開始"; フラグ.履歴 = ""; 現在位置を取得して次へ(プロローグ); };
こいつの中に「フラグ.起点 = undefined;」という1文を足します。
大きな修正はこんなところですね。
あとは、遊ぶ範囲が限定されないので関連の処理をコメントアウトしたり、不要と思える情報を表示してるところもコメントアウト。これで出来上がりです。
と、まぁこのように。
わかってしまうと簡単なハックなんですが、やっぱり分かるまでが大変ですね。そこがハックの面白い所でもありますが。最終的はソースは下記の通りです。
そうそう。会場では@shi3zさんにお会いしました。
「おっ。知った顔がいる」wwww