forked from: 「弾幕 - パーティクルの応用で弾幕」を初級者が解説してみる。
コードのリファクタリング & 最適化
Nicolasが最適化してくれました。
http://wonderfl.net/code/63f88f2189846bdc7275a01d5d228b1607344e51
BitmapData#draw() を BitmapData#coloyPixces() に変えるだけ!
めっちゃ早いよ!これ!
「弾幕 - パーティクルの応用で弾幕」を初級者が解説してみる。
*
* 皆さん、wonderflってますか?
* 今回は弾幕の仕組みを勉強したいと思います・・・が。
* 今回は非常に判りにくい上に冗長な説明となってます。
* というのも、イメージがとにかく掴みづらく、説明しづらいのです。
*
* まずはFork元のソースを見て、わからないところがあったら
* こちらを参照する手順のほうがいいと思います。
*
* あと、これが一番大事ですが、ForkでもDownloadしてでもいいので
* 数値を弄ったり、紙に書いたり、コードをコメントアウトしたりしてみてください。
* 特にこのプログラムは眺めてるだけだとわかりづらいと思います。
* 自分もそういうの苦手なので偉そうにいえませんが・・・。
*
* 一応別途サンプルも用意しました。
* ご活用いただければと思います。
*
* http://wonderfl.net/code/ea635e4cbbe2cb898077ec9408a591e702ab21f5/
* http://wonderfl.net/code/1ddb5976ba254b8507c42e493c020360b3a0e40c/
*
* Fork元を製作された方へ。
* ・Forkを拒否されたい場合は、何かしらのアクションでお伝えください。
* 削除いたします。(できれば生暖かく見守っていただけると嬉しいです)
*
* 中級者・上級者の方へ。
* ・何かアドバイスや校正箇所があれば、ご指摘お願いします。
*
* 初心者・初級者の方へ。
* ・一緒に勉強しましょう。何か質問があればどうぞ。
おおまかな流れ
* 1.弾のデザイン、弾幕の発射位置等初期設定
*
* 2.キャンバスの色を落とす(軌跡用設定)
* 3.弾幕の発射位置をマウスに近づける
*
/**
* Copyright LegacyCrono ( http://wonderfl.net/user/LegacyCrono )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/oSWx
*/
// forked from Hiiragi's 「弾幕 - パーティクルの応用で弾幕」を初級者が解説してみる。
// forked from coppieeee's 弾幕 - パーティクルの応用で弾幕
//コードのリファクタリング & 最適化
//
// Nicolasが最適化してくれました。
// http://wonderfl.net/code/63f88f2189846bdc7275a01d5d228b1607344e51
// BitmapData#draw() を BitmapData#coloyPixces() に変えるだけ!
// めっちゃ早いよ!これ!
/*
* 「弾幕 - パーティクルの応用で弾幕」を初級者が解説してみる。
*
* 皆さん、wonderflってますか?
* 今回は弾幕の仕組みを勉強したいと思います・・・が。
* 今回は非常に判りにくい上に冗長な説明となってます。
* というのも、イメージがとにかく掴みづらく、説明しづらいのです。
*
* まずはFork元のソースを見て、わからないところがあったら
* こちらを参照する手順のほうがいいと思います。
*
* あと、これが一番大事ですが、ForkでもDownloadしてでもいいので
* 数値を弄ったり、紙に書いたり、コードをコメントアウトしたりしてみてください。
* 特にこのプログラムは眺めてるだけだとわかりづらいと思います。
* 自分もそういうの苦手なので偉そうにいえませんが・・・。
*
* 一応別途サンプルも用意しました。
* ご活用いただければと思います。
*
* http://wonderfl.net/code/ea635e4cbbe2cb898077ec9408a591e702ab21f5/
* http://wonderfl.net/code/1ddb5976ba254b8507c42e493c020360b3a0e40c/
*
* Fork元を製作された方へ。
* ・Forkを拒否されたい場合は、何かしらのアクションでお伝えください。
* 削除いたします。(できれば生暖かく見守っていただけると嬉しいです)
*
* 中級者・上級者の方へ。
* ・何かアドバイスや校正箇所があれば、ご指摘お願いします。
*
* 初心者・初級者の方へ。
* ・一緒に勉強しましょう。何か質問があればどうぞ。
*/
/*
* おおまかな流れ
* 1.弾のデザイン、弾幕の発射位置等初期設定
*
* 2.キャンバスの色を落とす(軌跡用設定)
* 3.弾幕の発射位置をマウスに近づける
* 4.砲台の角度を大きくする(砲台をまわす)(重要)
* 5.弾幕の発射位置を中心・砲台を始点として、円状に等間隔に配置する。
* (多分ここが文脈ではわからないかもしれません。サンプルをご覧ください)
* 6.弾をそれぞれの速度にしたがって動かす
* 7.弾のそれぞれの速度に加速度をプラスして速度を更新する
* 8.弾のそれぞれの加速度を計算する(重要)
* 9.弾のそれぞれを描画する
*
* 10.2~9の繰り返し
*/
package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Graphics;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.ColorTransform;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import net.hires.debug.Stats;
[SWF(frameRate="60",width="465",height="465" )]
public class PShooting extends Sprite
{
//ステージの幅
public static const WIDTH:Number = 465;
public static const HEIGHT:Number = 465;
//キャンバス
private var _canvas:BitmapData;
//弾のビットマップキャッシュ(青色の丸い玉のBitmapDataがこれに当たります)
private var _bulletImg:BitmapData;
//パーティクルリスト(パーティクルの情報を配列で管理します)
private var _particles:Vector.<Particle>;
//敵。というか弾の再生位置。
private var _enemy:Particle;
public function PShooting()
{
//キャンバスの生成・配置(透明度無し・黒色で塗りつぶし)
_canvas = new BitmapData(WIDTH, HEIGHT,false,0x000000);
var cb:Bitmap = new Bitmap(_canvas);
addChild(cb);
//パーティクルリストを生成します。
//今まで勉強してきたものと違い、Array型ではなくVector型ですが、
//非常によく似たものと考えてもらってかまわないと思います。(扱い方もArrayにとても似ています)
//FP10より実装されたもので、Arrayより速く配列を扱うことができます。
//注意点は、Array型は配列の中にいろんな型を混在させても大丈夫でしたが、
//Vector型はひとつの型しか入れられません。そして、それは<>内のクラス名で判断します。
//つまり、下のコードはParticle型のオブジェクトのみ_particlesに入れることができるというわけです。
_particles = new Vector.<Particle>();
//弾の発生位置を設定します。場所・速度、共に0です。
_enemy = new Particle(0,0,0,0);
//弾のShapeの生成(青くて丸い玉の絵を生成します)
//違う大きさの円を二つ書くことで、なんかかえるの卵っぽい、若干キモイ弾になってます(例えが悪いですが)。
//glow効果っぽい感じを簡単に出したい場合はこれもありですね。
//SpriteではなくShapeなのが個人的にはツボでした。
//動作自体には影響しないでしょうけど、適材適所ですね。
//(グラフィックを描くだけならShapeの方が軽い。これはクラスの構造の話になってしまうので詳しくは書きませんが、
//Shapeに比べてSpriteは絵を描画できるだけでなく、多くの機能を持っているということです)
var shape:Shape = new Shape();
var g:Graphics = shape.graphics;
g.beginFill(0x00AAFF,0.5);
g.drawCircle(16, 16, 16);
g.beginFill(0x55FFFF);
g.drawCircle(16, 16, 8);
g.endFill();
//BitmapDataに弾のShapeをdraw()
//円の大きさに合わせて生成します。透明度ありなんで、塗りつぶしの色はなんでもOKです。
_bulletImg = new BitmapData(shape.width, shape.height, true, 0xFFFFFF);
_bulletImg.draw(shape);
/*
* おまけの機能的なものなので、ちょっとコメントアウトさせていただきました。
*
//Statsの生成
//これで左上にあるメモリ管理のウィンドウを出すことができます。
//これは他の人が作ったライブラリなので、FlashCS4やFlashDevelop等で出そうとすると、ライブラリを設定しなければなりません。
var stats:Stats = new Stats();
addChild(stats);
//particlsの個数表示用のTextField生成
var tf:TextField = new TextField();
tf.y = 100;
tf.textColor = 0xFFFFFF;
tf.background = true;
tf.backgroundColor = 0x000000;
tf.autoSize = TextFieldAutoSize.LEFT;
addChild(tf);
//第2引数には関数名を入れるのが大体の常だと思いますが、
//ちょっとした関数を扱いたいだけならば、このように直接第2引数に関数を書くという手法もあります。
//無名関数というらしいです。その名のとおり、名前がないですね。
//
//便利ですが、関数を使いまわしたりする場合は、この書き方は好ましくありません。
//あくまでも、今回のように「ちょっとした関数を扱いたいとき」のみがいいと思います。
addEventListener(Event.ENTER_FRAME, function(e:Event):void {
tf.text = "bullets:"+_particles.length;
});
*/
//onEnterFrame関数を毎フレーム実行するようにします
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
//弾の発射位置の角度です。とりあえずこれがぐるぐる回ると考えてください。
private var _radian:Number = 0;
private function onEnterFrame(e:Event):void
{
//描画の負担を軽くするためにロックします。
//以後unlockされるまで表面上は描画されません。
_canvas.lock();
//キャンバスの範囲の色を落とします。これによって過去の描画されているものを暗くしていきます。
//HANABIのときもLiquid10000の時も一緒でしたね。わからない人は過去の解説作品を参照してください。
//あるいは、3行目のcolorTransformをコメントアウトしてみると一目でわかります。
var cr:Rectangle = new Rectangle(0, 0, _canvas.width, _canvas.height);
var ct:ColorTransform = new ColorTransform (0.8, 0.8, 0.9);
_canvas.colorTransform(cr, ct);
//弾の生成場所をマウスのところへ移動。
//マウスの位置と生成場所の位置を速度に変換することによって、徐々にカーソルに近づくような効果を出しています。
//わからない方は、実際に数値を入れたりして計算してみることをお勧めします。
//一気にマウスの場所に移動させたい場合は、下の4行をコメントアウトして、その下の2行のコメントアウトをはずしてください。
_enemy.vx = (stage.mouseX - _enemy.x) * 0.2;
_enemy.vy = (stage.mouseY - _enemy.y) * 0.2;
_enemy.x += _enemy.vx;
_enemy.y += _enemy.vy;
//_enemy.x = stage.mouseX;
//_enemy.y = stage.mouseY;
//=========================================================
//ここからが個人的に一番勉強したかった場所なので、超重要ということにしておきます。
//あと、初心者・初級者が見ると一発で挫折しそうな気がしたので、処理を若干簡略化というか書き換えてます。
//ご了承ください。
//=========================================================
//これが弾が回転する第1の肝です。砲台の角度ともいえます。
//砲台、という書き方は若干混乱を招きかねないですが・・・。いい言葉が見つかりません。
//とりあえず、これがないとどうなるかはコメントアウトしてみてください。
//なお、一応書き換える前のやつもコメントアウトで残しておきます。
//とりあえずは毎フレームに2.16ラジアンずつ増えていくということだけ覚えてください。
//
//ちなみに2.16っていうのは度数表記だと何度でしょう?・・・そう、約123度です。この3度、あるのとないので雲泥の差があります。
//例えば、まずは下にある「一フレームあたりの弾の生成数」を1にしてください。弾が3方向に分かれて、しかも発生源が回ってますね。
//では次に、直下の_radian計算部分で、120度のコードを適用してください。3方向なのは変わらないですが、回らなくなったと思います。
//これがこの「3度」の差です。
//_radian += 2.0943951023931954923084289221863; //約120度
_radian += (2.16 * Math.tan(_radian)) + .108; //約123度
//_radian += (Math.PI * 2 / 180) * 62.1;
//一フレームあたりの弾の生成数
var bCount:int = 5;
var bRadian:Number = _radian;
//砲台の角度をはじめとして、_enemyを中心にbCountの個数分だけ円状に等間隔に配置します。
//別途サンプルを見てもらうとわかるかもしれません。以下のURLからどうぞ。
//http://wonderfl.net/code/ea635e4cbbe2cb898077ec9408a591e702ab21f5/
for (var i:int = 0; i <bCount; i++ )
{
//速度(この場合は3)をX軸Y軸に分解します。速度を10くらいにするととてもとても速くなります。
var vx:Number = Math.cos(bRadian) * 3;
var vy:Number = Math.sin(bRadian) * 3;
//弾を_enemyの場所に生成します。
var newP:Particle = new Particle(_enemy.x, _enemy.y, vx, vy);
_particles.push(newP);
//次の弾の角度を計算します。これをfor文でまわすことによって円状に配置します。
bRadian += Math.PI * 2 / bCount;
}
//ステージからはみ出て、消す必要があるパーティクルを収納する配列です
var removedParticles:Vector.<Particle> = new Vector.<Particle>();
//弾の移動。_particlesに入ってる弾全部を処理します。
for each(var p:Particle in _particles)
{
//速度を位置にプラスして、移動させます。
p.x += p.vx;
p.y += p.vy;
//加速度を速度にプラスします。
p.vx += p.ax;
p.vy += p.ay;
//ここからが弾が曲がった軌道を描くための処理になります。第2の肝となります。
//このサンプルは以下のURLからどうぞ。
//http://wonderfl.net/code/1ddb5976ba254b8507c42e493c020360b3a0e40c/
//加速度計算をします。
//Math.atan2で速度の角度を求めます。二つの線から直角三角形をつくり、角度を求めるっていう感じです。
var radian:Number = Math.atan2(p.vy, p.vx);
//X軸・Y軸の速度から現在のスピードを求めます。この式は「三平方の定理」です。
//平面幾何学において直角三角形の斜辺の長さを c とし、その他の辺の長さを a, b とした時
//a^2 + b^2 = c^2
//なる関係が成立するという幾何学の定理である。(from Wikipedia)
var speed:Number = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
//ちょっと判りにくいかもしれませんが・・・。
//Math.PI / 180 * 80は、80度分のラジアン値の角度です。約1.396です。
//それに現在の速度の角度を足して、「目標とする角度」を出しています。
//これを元に加速度を計算します。
radian = radian + Math.PI / 180 * 80;
//上で求めた「目標とする角度」に向かう加速度を計算します。
//0.08掛けてるのは、これはあくまで加速度であって速度ではないから・・・といったところでしょうか。
p.ay = Math.sin(radian) * 0.04;
p.ax = Math.cos(radian) * 0.24 + p.ay;
//画面外に出たらremovedParticlesに登録
//ただ、現状ではステージから出ていなくても、ギリギリのところで弾が消えてしまいます。
//どうすればいいか・・・というのは課題にしてみます。考えてみてください。
if (p.x < 0 || p.x > WIDTH || p.y < 0 || p.y > HEIGHT)
{
removedParticles.push(p);
continue;
}
////draw()じゃなくてcopyPixels()使った方がめっちゃ早いよ!!
// copyPixels () メソッド
// イメージ間のピクセル操作 (伸長、回転、カラー効果なし) を高速に実行するルーチンを提供します。
// このメソッドは、ソースイメージの矩形領域を、ターゲット BitmapData オブジェクトのターゲットポイントにある
// 同じサイズの矩形領域にコピーします。 (from Help)
//
//第1引数:sourceBitmapData 貼り付けたいBitmapData
//第2引数:sourceRect 貼り付けたいBitmapDataの範囲(今回は_bulletImg全部なので、rectプロパティをそのまま入れてます)
//第3引数:destPoint BitmapDataを貼り付けるPointです。弾の中心点は左上になってますので、縦横共に半分ずらして中心点が真ん中に見えるようにします)
var bulletPoint:Point = new Point(p.x - _bulletImg.width/2, p.y - _bulletImg.height/2);
_canvas.copyPixels(_bulletImg, _bulletImg.rect, bulletPoint);
}
//いらない弾を_particlesから消す。
for each(var removedP:Particle in removedParticles)
{
//_particlesから、removedPがあるインデックスを検索します。あったらインデックス番号が、なかったら-1が返ってきます。
var index:int = _particles.indexOf(removedP);
//あった場合、そのインデックス番号からパーティクル情報を削除します。
if (index != -1)
{
_particles.splice(index, 1);
}
}
//ここで描画を開始。
_canvas.unlock();
}
}
}
//一応わからない人のために・・・。
//このParticleクラスは「位置情報」を持つクラスなので、実際にステージ上に表示されるわけではないです。
//表示するわけではないですが、位置情報を持つので、生成したオブジェクトを参照することによって、
//その場所にあたかもオブジェクトがあるかのような「情報」を得ることができます。
//速度・加速度については、高校物理の序盤で習うと思いますが、忘れてる方もいると思います。
//詳しく知りたい人は
//http://contest2005.thinkquest.jp/tqj2005/80453/index.html
//がお勧めです。楽しく学べると思います。
//さらっと説明すると、加速度は毎フレームでどれだけ速度を速めるか(遅くするか)で、
//現在のフレームの加速度をひとつ前の速度にプラスすると、現在のフレームでの速度になります。
//例えば、現在のフレームの加速度が1で、前の速度が3だった場合、3+1で現在の速度になります。
//(わかりやすくするように、あえて単位は入れていません)
//これは車を運転する人は、車に置き換えるとよくわかると思います。
//そして、前のフレームの位置に現在の速度をプラスしたら、現在のフレームでの位置になります。
class Particle
{
//パーティクルの位置
public var x:Number;
public var y:Number;
//パーティクルの速度
public var vx:Number;
public var vy:Number;
//パーティクルの加速度
public var ax:Number = 0;
public var ay:Number = 0;
public function Particle(x:Number,y:Number,vx:Number,vy:Number)
{
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
}