Box2dを使ったゲームの授業で発表したものです。
豚がトンネルの上から矢を放ちます。今までは、矢の動きは直線的に同じ速度にしか動きませんでしたが、Box2dを使えば、下に重力を加えて、矢はリアルな放物線を描くことができます。
先生のサンプルコードの中に、前回説明したパワーメーターがあったのですが、これが出る前に、息子は角度メーターを作りたいと作り始めていたので、そちらを付けることにしました。
角度は、クリックしたところの角度を取ることができるので、どちらかと言うとこちらのほうが簡単です。何週間か前に角度メータと矢の角度を連動させるものを考えたので、どうやって作ったのかもう忘れてしまいましたが、今見返すと思ったほど全然計算してなかったです^^;
サンプルに加えてオリジナルで付け加えた工夫は、
・目標物(だるま)までの距離を長くして、透明の石を動かして画面をスクロールさせて見せる機能をつける
・角度メーターをつけてクリックした角度で矢を飛ばす
・modeのパラメータをつけて、スクロールしている間は撃てないように制御する
・最後だるまが落ちるときに、だるまにスクロールレンジを設定して、落ちるだるまが見えるようにする
などです。
画面のスクロール
画面より外のドカンに目標物があるので、まず、それを見せます。そのために、透明の箱を作って飛ばしてから戻すという方法でやってみました。(息子のアイデア)
透明な箱には、setScrollRangeというプログラムを設定します。これは、設定したパディングを常に保ってくれるというもので、スプライトを移動させても画面から出てしまうことがなく、画面のほうがスクロールしてくれるというものです。これは今井先生がプログラムを作ってくださいました。
箱にこれをつけて移動させれば、画面がスクロールするというわけです。
箱の色はテスト用にredにしておき、あとで消します。
そのままではずっと向こうに移動し続けてしまうので、壁にぶつかって止まるように、壁も配置します。
//スクロールさせる用オブジェクト var scro = new PhyBoxSprite(32, 32, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0, 0.3 ); //scro.backgroundColor = "red"; // テスト用・透明にする scro.x = 160; scro.y = 55; scene.addChild(scro); //壁 var wall = new PhyBoxSprite(45, 480, enchant.box2d.STATIC_SPRITE); //wall.backgroundColor = "red";// テスト用・透明にする wall.x = daruma.x+ 140; wall.y = 0; scene.addChild(wall);
これで、scroはだるまより向こうに飛んで行って、壁にぶつかって落ちます。
さらに、スクロールしている間は矢が発射できないように、幕を作って、それをタッチできないようにします。ここではbackiというスプライトを画面一杯に大きさで上にかぶせています。
// scroが移動している間はタッチ無効化すべし scro.tl.then(function(){ backi.touchEnabled = false; scro.addImpulse(10, -2); });
適度なタイミングで豚の方に戻します。帰ってくるときにだるまにぶつからないように調節します。(addImpulse(-9, -2))
戻ってから、scroと壁は消します。
// だるまにぶつからないように修正
scro.tl.delay(25);
scro.tl.then(function(){
scro.setAwake(false);
scro.addImpulse(-9, -2);
});
scro.tl.delay(45);
scro.tl.then(function(){
scro.remove();
wall.remove();
backi.touchEnabled = true;// 帰ってきたらタッチ可能
});
scene.setScrollRange(scro, 55);
角度メーターをつくる
上下に振れるメーターをつくります。幅1、長さ32ピクセルのスプライトを作り、ワイパーのように動かしたいので、基準をx=0、y=32とします(下のbar.originX = 0; bar.originY = 32;)。
これを、90度まで動かしてから、0度まで戻すという動きをloopさせれば、ワイパーのようなメーターができます。yの値が複雑なのは、豚の矢の位置に合わせるために微調整したからです。
ただ、ここで、あとでrotationと対応させるときに、ExSprite(1、32)とするより、(32、1)としたほうがやりやすかったかもしれません。途中で気づきましたが、90度ずらすだけなのでそのままやってしまいました。
// メーター var bar = new ExSprite(1, 32); bar.backgroundColor = "red"; bar.x = 52; bar.y = 480 - 64 - 32 - 13 - 170;// 豚の位置に合わせる scene.addChild(bar); bar.originX = 0; // 回転の基準 bar.originY = 32; // 〃 bar.tl.rotateBy(90, 32); bar.tl.rotateTo(0, 32); bar.tl.loop();
矢を飛ばす
タッチして矢を飛ばします
// 矢を飛ばす backi.addEventListener(Event.TOUCH_START, function(e) { // やを飛ばせるのはmode=0のときのみ if(mode == 0){ var arrow = new PhyBoxSprite( 22, // 幅 9, // 高さ enchant.box2d.DYNAMIC_SPRITE, // 動作モード 1 // 密度 //2 // 実験 ); arrow.image = core.assets["images/bal_arrow.png"]; arrow.x = pig.x + pig.width + 1; arrow.y = pig.y + 18; scene.addChild(arrow); arrow.tag = "矢"; scene.setScrollRange(arrow, 50); var sounds2 = core.assets["sounds/cf307/muti.mp3"].clone(); sounds2.play();
角度メーターと矢の角度を連動させる
ここが角度メーターの肝ですが、簡単です。なぜなら、bar.rotationというプロパティが、タッチしたときのbarの角度を取ってくれるからです。
そして、この2.5・・ってなんでしょう・・自分でやったのに忘れました・・多分、bar.rotationというのは、動いているbarをタッチしたときの角度になるので、0〜90の値そのままでは、addImpulseに入れる値としては大き過ぎるので、それで、一旦100で割って、すると今度は小さくなりすぎるので、調節して2.5倍くらいにすると、矢の飛ぶ距離としてはちょうどよくなったのだと思います。
つまり、メーターのスプライトは(1、32)なので、垂直に立った状態から倒す方向で動かしています。ですから、x方向は、bar.rotationの値に比例させればいいので、その比例定数は的との距離で適当に調整して、2.5/100となりました。
逆にy方向は、傾きが大きくなるほど、y方向への力は小さくなるので、(傾きの最大値90度のとき0となる)(90-bar.rotation)に比例させればよく、これも調整して、2.5/90となり、上方向にとばすので-(マイナス)となります。
そのあとまた微調整して、arrow.addImpulse(ax * 1.2, ay * 1.2)でいい具合に飛ばすことができました。
// バーの角度と矢の飛ぶ方向を対応させる var ax; ax = 2.5 * bar.rotation / 100; var ay; ay = - 2.5 * (90 - bar.rotation) / 90; console.log(ax); console.log(ay); arrow.addImpulse(ax * 1.2, ay * 1.2);
矢の出し方
上で、矢を飛ばす力については決めました。しかし画面上の画像としては、そのままでは矢が真横に出て、真横のまま放物線を描くことになるので不自然です。(Box2dはそこまではやってくれない)
そこで矢を出す時の角度もメーターの角度と同じになるようにして、飛ばしたあとも角度に合わせてゆるやかに回転させるようにしました。
また、矢と同時に一旦消したスクロール用の透明の箱を、矢をフォローして一緒に動くようにします。これで、矢が遠くに飛んでも画面がスクロールして見ることができます。
// 矢の角度 arrow.angle = -(90 - bar.rotation);// 矢の角度はメーターの角度と同じ arrow.addTorque(0.007);// 飛ばしているときに回転させる arrow.originX = 0; arrow.originY = 9;// 回転するときにちょうどよくなるように backi.tl.then(function(){ backi.touchEnabled = false; scene.addChild(scro); scro.x = pig.x + 32; scro.y = pig.y - 32; scro.follow(arrow);// scroが矢についていくことで画面スクロール });
矢が外れたら画面を豚に戻す
矢に衝突判定をつけ、地面に落ちたら矢をフォローしていたscroは消して、別のscro2という箱を出し、それを豚のところまで戻すことで画面をスクロールさせます。
矢が地面に落ちた時点で箱を出します。ただし、だるまが地面に落ちたとき(ゲームクリアのとき)にだるまだけでなく矢も落ちてしまったときには出したくないので、だるまが落ちていないときだけ出るようにif(daruma.y <= 219)という条件分岐をつけます。
矢がだるまを超えたときは、戻るときに高さが足りなくてだるまや土管にぶつかって戻ってこれなかったので、scro2の高さを高くしました。(scro2.y = daruma.y – 32;)
外れた矢がそのままだと、次に外れた矢がその上に乗ってしまったとき、地面との衝突判定が効かなくなるので、外れた矢は消します。( arrow.remove();)
// 矢が地面に落ちたら青いscro2を出して画面をスクロールしてぶたに戻す arrow.addCollision(ground); daruma.addCollision(arrow); arrow.addEventListener(Event.COLLISION, function(e){ if (e.collision.target.tag == "地面"){ if(daruma.y <= 219){// だるまが落ちていないとき console.log("矢を打ってから戻る"); scro.remove(); arrow.tl.delay(8).then(function(){ if(mode == 1){ mode = 2; var scro2 = new PhyBoxSprite(32, 32, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.1,//摩擦 0.3 ); //scro2.backgroundColor = "blue"; // 矢ががだるまを超えちゃった場合 if(scro.x > daruma.x){ scro2.x = scro.x; scro2.y = daruma.y - 32; }else{ scro2.x = scro.x; scro2.y = scro.y; } scene.addChild(scro2); console.log(daruma.y); console.log("scro2でます"); scro2.addImpulse(-8, -5); console.log("戻ります"); scene.setScrollRange(scro2, 55); // scro2がトンネルまで戻ってぶつかったら消える、モードを戻す scro2.addCollision(tunnel1); scro2.addEventListener(Event.COLLISION, function(){ scro2.remove(); mode = 0; // 矢も消す arrow.remove(); }); } }); } } });
modeを使った制御
画面をスクロールさせている間に、矢が撃ててしまうと、スクロールがおかしくなってしまうので、その間は撃てないように、modeのパラメーターを使って制御します。
だるまについても、地面に落ちたときにゲームクリアにすると、だるまが跳ね返って地面に衝突する度に何度もゲームクリアが発動してしまうので、地面に衝突した最初の一度目だけゲームクリアとなるようにします。
// modeで制御(scro2を一個だけにするのと、連射できないようにするため、 // だるまの衝突判定のため)) /* 矢を飛ばす前 0 :mode = 0; modec = 0; 飛ばしたら 1 :mode = 1; scro2が発射できるのは1のときのみにして2個以上出ないようにする scro2が発射されたらすぐ 2: mode = 2; scro2が帰ってきたら 0に戻す mode = 0; 矢を飛ばせるのは 0の時のみにする if(mode == 0){} だるまが地面におちたら modec = 1; */
だるまにスクロールレンジをセットする
だるまに矢が当たって、土管から落ちる時(ゲームクリアのとき)、だるまが見えなくなってしまうとつまらないので、だるまにsetScrollRangeをつけます。
だるまのy座標を見ると、218.79968127001965なので、(box2dだと重力によって、ほんの少し下がるようです)落ち始めたらセットとなるようにします。
ただし、だるまが地面に落ちてからもスクロールし続けると、ゲームクリアの文字やお殿様の画像がずれてしまうので、地面に落ちたら外します。(cancelScrollRangeも今井先生が作ってくださいました)
console.log(daruma.y); // だるまが消えないようにだるまが落ち始めたらスクロールレンジ設定 scene.addEventListener(Event.ENTER_FRAME, function(){ if(daruma.y > 220){ // 218.7 scene.setScrollRange(daruma, 55); } if(daruma.y > 300){ // scene.cancelScrollRange(daruma); } });
感想
角度メーターと矢の飛び方を連動させる調整のところ以外は、アイデアも含めて息子が頑張りました。先生もおっしゃっていたけれど、一人で全部できるものなんてないのだから、(そもそも基のゲームのつくりかただって全部先生から教えてもらったものなのだし)、手伝ってもらったことを負い目に感じる必要はないと思うのですが、息子は手伝ってもらったにしても自分でコードを打ったという実感がないと嫌みたいなので、その辺さりげなく手伝うようにしています。息子も頼ってくれるので、それも嫌でないうちは、協力したいと思います。
だいたい以上の流れで作りましたが、一つ機能を増やすと、その影響でそれまでのところに不具合がでて調整するために何かを付け足さないといけない、また一つ増やすとまた調整、この繰り返しです。おそろしく何度もプレイしなければなりません。
今までもそうでしたが、今この記事を書いていて、改めてそれを実感しました。
ゲーム作りって大変ですね。でもパズルを解いていくみたいで本当に面白いです。私にとってはゲーム作り自体が、今までで一番面白いパズルゲームです。
完成したソースコード
以上を組み合わせて完成です
var assets = [ // 背景 "images/do_background.png", "images/do_ground.png", // 箱 "images/do_box.png", // ダルマ "images/do_daruma.png", "images/gameclear.png", // ブタ "images/bal_pig.png", // 土管 "images/ft_tunnel.png", // 矢 "images/bal_arrow.png", // その他 "images/title.png",// タイトル "images/bk_body.png", "images/bk_mage.png", "sounds/cf307/hyoushigi2.mp3", "sounds/cf307/muti.mp3", "sounds/cf307/hit04.mp3", "sounds/cf307/drum-japanese1.mp3", ]; function gameStart(){// ゲーム画面 scene = new Scene(); core.replaceScene(scene); core.resume(); //========== // ここから //========== // モードと背景色 var modec = 0; scene.backgroundColor = "#76d4fe"; var mode = 1; // 背景 var back = new Sprite(320 * 100, 480); back.image = core.assets["images/do_background.png"]; scene.addChild(back); back.y = -32; // 重力のある世界 var world = new PhysicsWorld( 0, 6 ); scene.addEventListener(Event.ENTER_FRAME, function() { world.step(core.fps); }); // 土管1 var tunnel1 = new PhyBoxSprite(45, 200, enchant.box2d.STATIC_SPRITE); tunnel1.image = core.assets["images/ft_tunnel.png"]; tunnel1.x = 10; tunnel1.y = 250; //tunnel1.y = 100; scene.addChild(tunnel1); // 土管2 var tunnel2 = new PhyBoxSprite(45, 480, enchant.box2d.STATIC_SPRITE); tunnel2.image = core.assets["images/ft_tunnel.png"]; tunnel2.x = 220+320; tunnel2.y = 250; scene.addChild(tunnel2); // 地面 var ground = new PhyBoxSprite(320 * 100, 64, enchant.box2d.STATIC_SPRITE); ground.image = core.assets["images/do_ground.png"]; ground.x = 0; ground.y = 480-64; scene.addChild(ground); ground.tag = "地面"; // ダルマ落とし 第四引数:density(密度)・・・省略時:1.0 // 第五引数:friction(摩擦)・・・省略時:0.5←難易度を決めるのはこれ // 第六引数:restitution(反発)・・・省略時:0.3 // だるま var daruma = new PhyBoxSprite( 31, 31, enchant.box2d.DYNAMIC_SPRITE, 0.35,// 初めの設定は0.5 0.5, 0.3, ); daruma.image = core.assets["images/do_daruma.png"]; daruma.x = tunnel2.x + tunnel2.width / 2 - daruma.width / 2; daruma.y = tunnel2.y - daruma.height; scene.addChild(daruma); daruma.tag = "だるま"; // 衝突判定 daruma.addCollision(ground); daruma.addEventListener(Event.COLLISION, function(e){ // 矢が当たったら if(e.collision.target.tag == "矢"){ console.log("当たった"); var hit = core.assets["sounds/cf307/hit04.mp3"].clone(); hit.play(); } // 地面に落ちたら if(modec != 1 && e.collision.target.tag == "地面"){ scro.remove(); var sounds = core.assets["sounds/cf307/drum-japanese1.mp3"].clone(); sounds.play(); // だるまが消えてしまうので //scene.setScrollRange(daruma, 55); // だるまが地面に落ちたらゲームクリア! var gameclear = new ExSprite(230, 32); gameclear.image = core.assets["images/gameclear.png"]; gameclear.x = daruma.x - 230; gameclear.y = daruma.y - 200 + 13; scene.addChild(gameclear); console.log("クリア"); modec = 1; scene.setScrollRange(gameclear, 45); gameclear.tl.delay(32).then(function(){ scene.setScrollRange(gameclear, 45); var tono = new ExSprite(190, 140); tono.image = core.assets["images/bk_body.png"]; tono.x = gameclear.x + gameclear.width / 2 - tono.width / 2; tono.y = gameclear.y - 140; scene.addChild(tono); // あっぱれ var label = new Label(" あっ ぱれ"); label.color = 'white'; label.font = "30px 'PixelMplus10'";// 28 label.x = tono.x - 32; label.y = tono.y + tono.height / 2; scene.addChild(label); // まげ var mage = new ExSprite(45, 60); mage.image = core.assets["images/bk_mage.png"]; mage.x = tono.x + tono.width / 2 - mage.width / 2; mage.y = tono.y - 60; scene.addChild(mage); var sounds = core.assets["sounds/cf307/hyoushigi2.mp3"]; sounds.play(); }); } }); // 豚さん var pig = new PhyBoxSprite(32, 32, enchant.box2d.DYNAMIC_SPRITE); pig.image = core.assets["images/bal_pig.png"]; pig.x = tunnel1.x + tunnel1.width - pig.width; pig.y = tunnel1.y - pig.height; pig.frame = [0,0,1,1,2,2,3,3]; scene.addChild(pig); // 撃てなくするための背景 var backi = new Sprite(320 * 100, 480); //backi.image = core.assets["images/do_background.png"]; scene.addChild(backi); backi.y = -32; // メーター var bar = new ExSprite(1, 32); bar.backgroundColor = "red"; bar.x = 52; bar.y = 480 - 64 - 32 - 13 - 170; scene.addChild(bar); bar.originX = 0; bar.originY = 32; bar.tl.rotateBy(90, 32); bar.tl.rotateTo(0, 32); bar.tl.loop(); // modeで制御(scro2を一個だけにするのと、連射できないようにするため) /* 矢を飛ばす前 0 飛ばしたら 1 scro2が発射できるのは1のときのみにして2個以上出ないようにする scro2が発射されたらすぐ 2 scro2が帰ってきたら 0に戻す 矢を飛ばせるのは 0の時のみにする だるまが地面におちたらmodec = 1; */ mode = 0; // モードの初期値 // 矢を飛ばす backi.addEventListener(Event.TOUCH_START, function(e) { // やを飛ばせるのはmode=0のときのみ if(mode == 0){ var arrow = new PhyBoxSprite( 22, // 幅 9, // 高さ enchant.box2d.DYNAMIC_SPRITE, // 動作モード 1 // 密度 //2 // 実験 ); arrow.image = core.assets["images/bal_arrow.png"]; arrow.x = pig.x + pig.width + 1; arrow.y = pig.y + 18; scene.addChild(arrow); arrow.tag = "矢"; scene.setScrollRange(arrow, 50); var sounds2 = core.assets["sounds/cf307/muti.mp3"].clone(); sounds2.play(); // バーの角度と矢の飛ぶ方向を対応させる var ax; ax = 2.5 * bar.rotation / 100; var ay; ay = - 2.5 * (90 - bar.rotation) / 90; console.log(ax); console.log(ay); arrow.addImpulse(ax * 1.2, ay * 1.2); // 矢の角度 arrow.angle = -(90 - bar.rotation); arrow.addTorque(0.007); arrow.originX = 0; arrow.originY = 9; backi.tl.then(function(){ backi.touchEnabled = false; scene.addChild(scro); scro.x = pig.x + 32; scro.y = pig.y - 32; scro.follow(arrow);// followの解除の仕方がわからない }); // 矢を飛ばしたらモード1に mode = 1; // 矢が地面に落ちたら青いscroを出して画面をスクロールしてぶたに戻す arrow.addCollision(ground); daruma.addCollision(arrow); arrow.addEventListener(Event.COLLISION, function(e){ if (e.collision.target.tag == "地面"){ if(daruma.y <= 219){ // だるまのyが218と少しなので console.log("矢を打ってから戻る"); scro.remove(); arrow.tl.delay(8).then(function(){ if(mode == 1){ mode = 2; var scro2 = new PhyBoxSprite(32, 32, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.1,//摩擦 0.3 ); //scro2.backgroundColor = "blue"; // スクロがだるまを超えちゃった場合 if(scro.x > daruma.x){ //scro2.x = daruma.x; scro2.x = scro.x; scro2.y = daruma.y - 32; }else{ scro2.x = scro.x; scro2.y = scro.y; } scene.addChild(scro2); console.log(daruma.y); console.log("scro2でます"); scro2.addImpulse(-8, -5); console.log("戻ります"); scene.setScrollRange(scro2, 55); // scro2がトンネルまで戻ってぶつかったら消える、モードを戻す scro2.addCollision(tunnel1); scro2.addEventListener(Event.COLLISION, function(){ scro2.remove(); mode = 0; // 矢も消す arrow.remove(); }); } }); } } }); // 戻ってから撃てるように backi.tl.then(function(){ backi.touchEnabled = true; }); console.log(daruma.y); } }); //スクロールさせる用オブジェクト var scro = new PhyBoxSprite(32, 32, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0, 0.3 ); //scro.backgroundColor = "red"; scro.x = 160; scro.y = 55; scene.addChild(scro); // scroが移動している間はタッチ無効化すべし scro.tl.then(function(){ backi.touchEnabled = false; scro.addImpulse(10, -2); }); // だるまにぶつからないように修正 scro.tl.delay(25); scro.tl.then(function(){ scro.setAwake(false); scro.addImpulse(-9, -2); }); scro.tl.delay(45); scro.tl.then(function(){ scro.remove(); wall.remove(); backi.touchEnabled = true; }); scene.setScrollRange(scro, 55); //壁 var wall = new PhyBoxSprite(45, 480, enchant.box2d.STATIC_SPRITE); //wall.backgroundColor = "red"; wall.x = daruma.x+ 140; wall.y = 0; scene.addChild(wall); console.log(daruma.y); // だるまが消えないようにだるまが落ち始めたらスクロールレンジ設定 scene.addEventListener(Event.ENTER_FRAME, function(){ if(daruma.y > 220){ // 218.7 scene.setScrollRange(daruma, 55); //scro.remove(); // ここで消すとはやすぎるのでラベルのところで消す } if(daruma.y > 300){ // scene.cancelScrollRange(daruma); } }); //========== // ここまで //========== } function titleStart(){// タイトル画面 var scene = gameManager.createTitleScene(); core.replaceScene(scene); core.pause(); scene.on(enchant.Event.TOUCH_START, function(){gameStart();}); } //========== // EnchantJS enchant(); var gameManager; var core; var scene; window.onload = function(){ gameManager = new common.GameManager(); core = gameManager.createCore(320, 480); core.preload(assets); core.onload = function(){titleStart();}; core.start(); } /* enchant.Scene.prototype.setScrollRange = function(child, padding) { if (!child) return; var _paddingLeft = padding || 0; var _paddingRight = padding || 0; var _paddingTop = padding || 0; var _paddingBottom = padding || 0; // EnterFrameを解除 if (this._scrollRangeArg) { this.removeEventListener(Event.ENTER_FRAME, this._scrollRangeArg); this._scrollRangeArg = null; } // 対象が画面の表示領域からはみ出ようとした時にスクロールさせる this.addEventListener(Event.ENTER_FRAME, function(){ this._scrollRangeArg = arguments.callee; // Left if (child.x < _paddingLeft - this.x) { this.x += (_paddingLeft - this.x) - child.x; } // Right if (child.x > this.width - _paddingRight - this.x) { this.x -= child.x - (this.width - _paddingRight - this.x); } // Top if (child.y < _paddingTop - this.y) { this.y += (_paddingTop - this.y) - child.y; } // Bottom if (child.y > this.height - _paddingBottom - this.y) { this.y -= child.y - (this.height - _paddingBottom - this.y); } }); }; */