Flash でパーティクルシステムの開発に挑戦しよう
パーティクル・システムはグラフィック表現のひとつ。炎、爆発、煙とか光跡や呪文の視覚効果の制作に役立ちます。私の管理しているサイトの一つ「Beautifl」を見て頂ければわかりますが、昔は「これでもか!」というぐらい皆パーティクルを作っていました。
After Effectsでパーティクルの表現力に魅せられて
私がペーペーの見習いFlasherだった頃の話。Adobe After Effectsにもパーティクルを実現する機能が搭載されていまして、「パーティクル・プレイ・グラウンド」や「Trapcode Particular」とかのエフェクトを触る機会がありました。ayato@webのデモを見れば、パーティクル表現の可能性に触れることができますが、表現力の高さに随分興奮したものです。それと同時に、このエフェクトを開発した人は人類の叡智を超えた神々だと崇拝していました。
wonderfl で知った意外と簡単だったパーティクルの実装
それから数年、wonderfl build flash onlineの登場でパーティクルの実装コードが次々とパブリックに投稿されるようになりました。コード神と呼ばれる海外の凄腕クリエイターが投稿しているかとおもいきや、20代後半から学生の方たちがパーティクルを投稿しているんですね。
「えっ、自分と同じぐらいの年の人が?」
コードを読んでみると、神々でもなくても普通の社会人エンジニアが読める程度の難易度だったのです。
皆さんも挑戦しましょう
パーティクルの開発自体はそんな難しいものではないーということを多くの方に知ってもらいたく解説を書くことにしました。段階を踏んで、一つ一つ学んでいきましょう。解説は、約30分程度で学べる内容となっています。
1つだけのパーティクル制御を実装
パーティクルはMovieClip
インスタンスとして作成します(Shape
クラスでもいいのですが、この後の解説のためMovieClip
で解説を続けます)。このパーティクルに対して、速度の計算を行います。
private const size:int = 10;
private var particle:Shape;
private var vx:Number;
private var vy:Number;
public function particle_step1()
{
// 赤い丸を作成
particle = new Shape();
// (省略)
// 速度情報
vx = 0;
vy = 0;
this.addChild(particle);
// 時間経過
this.addEventListener(Event.ENTER_FRAME, handleTick);
}
private function handleTick(event):void
{
// 重力
vy += 1;
// 摩擦
vx *= 0.95;
vy *= 0.95;
// 反映
particle.x += vx;
particle.y += vy;
// 地面の作成
// (省略)
}
vx
とvy
あたりの制御は速度情報をX軸とY軸で管理している基本的な実装です。詳しく知っておきたいテクニックですが、このことは「神の書」と呼ばれかつて全てのFlasherのバイブルだった「ActionScript 3.0 アニメーション: Keith Peters」を参考にするといいでしょう。
配列で複数のパーティクルを制御
まずは複数のパーティクルを管理する配列を作成しましょう。
private var particleList:Array = [];
※ []
と記述してもnew Array()
と記述しても同じです。多くのコーディング規約で[]
と記述することが推奨されているので、本サイトでは配列は[]
として作成します。
for
文を使って、複数のパーティクルを作成し、この配列に保存します。パーティクルの発生座標は視覚的にわかりやすくするために、Math.random()
メソッドを使ってランダムな座標にしています。
// for文を作成 (上限数はパーティクルの個数)
for (var i:int = 0; i < 20; i++)
{
// 赤い丸を作成
var particle:MovieClip = new MovieClip();
// (省略)
particle.vx = 0;
particle.vy = 0;
// 配列に保存
particleList[i] = particle;
}
速度情報のvx
とvy
はMovieClip
インスタンスのプロパティーとして設定しています(particle.vx = 0;
)。ActionScript 3.0のMovieClipクラスはダイナミッククラスといい、自由にオブジェクトのプロパティーを追加できます。本来はMovieClip
クラスにvx
とvy
プロパティーは存在しませんが、速度情報はパーティクル自身それぞれで管理するべきものなので、パーティクル自身に速度情報をもたせたほうがプログラムの見通しがよくなります。
particle.vx = 0;
particle.vy = 0;
次にEvent.ENTER_FRAME
イベントでは配列に保存した複数のパーティクルを制御します。for
文では配列の長さparticleList.length
に応じてループさせます。配列の制御では要素を変数に取り出し(var particle = particleList[i];
)、一個のパーティクルを制御したときと同じ処理を実装します。
for (var i:int = 0; i < particleList.length; i++)
{
// i番目の要素を変数に代入
var particle:MovieClip = particleList[i];
// 重力
particle.vy += 1;
// 摩擦
particle.vx *= 0.95;
particle.vy *= 0.95;
// 反映
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.y > stage.stageHeight - size)// 地面
{
particle.y = stage.stageHeight - size; // 行き過ぎ補正
particle.vy *= -1; // Y軸の速度を反転
}
}
パーティクルを時間経過で発生させ、寿命を設ける
プログラムの構造 : エンターフレームイベントで2つの関数を呼び出して制御
時間経過でパーティクルを発生させたいので、パーティクルの作成をEvent.ENTER_FRAME
イベントで行います。emitParticles()
関数でパーティクルを発生させ、updateParticles()
関数でパーティクルを動かします。
private var particleList:Array = [];
/** コンストラクター */
public function particle_step3()
{
// 時間経過
this.addEventListener(Event.ENTER_FRAME, handleTick);
}
private function handleTick(event):void
{
// パーティクルを発生
emitParticles();
// パーティクルを更新
updateParticles();
}
これらの関数の役割を掘り下げてみましょう。
パーティクルの発生を担当するemitParticles()
関数
パーティクルは時間経過で次々と発生します。配列の大きさに制限はなく、いくらでも要素を追加することができます。今回は新しく発生したパーティクルは配列の最後に追加するため、push()
メソッドを使ってパーティクルを保存しています。
/** パーティクルを発生させます */
private function emitParticles():void {
// for文を作成 (上限数はパーティクルの個数)
for (var i:int = 0; i < 4; i++)
{
// 赤い丸を作成
var particle:MovieClip = new MovieClip();
// (省略)
// 寿命
particle.life = 50;
// 配列に保存
particleList.push(particle);
}
}
パーティクルには座標x
、y
や速度情報vx
、vy
を設定しますが、今回はパーティクルに寿命life
を設定します。MovieClip
クラスにはlife
というプロパティーは本来存在しませんが、勝手に追加しておきます。
// 寿命
particle.life = 50;
パーティクルの更新を担当するupdateParticles()
関数
パーティクルの更新処理では寿命の更新をおこないます。寿命というと難しいように思いますが、処理としてはプロパティーlife
の値を-1
するだけです。1回のtick
イベントでlife
が1ずつ減っていきます。
/** パーティクルを更新します */
private function updateParticles():void {
for (var i:int = 0; i < particleList.length; i++)
{
// i番目の要素を変数に代入
var particle:MovieClip = particleList[i];
//(省略)
// 寿命を減らす
particle.life -= 1;
// 寿命の判定
if (particle.life <= 0) {
// ステージから削除
this.removeChild(particle);
// 配列からも削除
particleList.splice(i, 1);
}
}
}
寿命がつきたとき、つまりlife
が0
になったときにパーティクルを消去しましょう。if
文での判定はlife
が0
になったかどうかを調べればわかります。削除処理には2つの実装が必要となり、画面から消すremoveChild()
と、保存されている配列からの消去particles.splice()
を行います。
removeChild()
はステージから消去する命令で画面からパーティクルを消すことができます。配列のsplice(a, b)
は配列のa
番目からb
個の要素を消すという命令になります。画面から消すだけでは不十分で、配列からもパーティクルを消しておかなければ、無駄に座標更新や物理演算の計算が行われ続けます。
// 寿命の判定
if (particle.life <= 0) {
// ステージから削除
stage.removeChild(particle);
// 配列からも削除
particles.splice(i, 1);
}
カラフルなパーティクルシステムの実装
最後に表現をブラッシュしていきましょう。
パーティクルの色を時間経過で変える
beginFill()
メソッドでパーティクルの色を設定しますが、これを時間経過で色をかえていきます。今回は時間経過で色相(赤→緑→青)をかえるためにHSVカラーを使います。HSVカラーはActionScript 3.0で簡単に使うために、AS3ライブラリのFrocessing ( nutsu/Frocessing - Spark project )を利用します。
// HSLカラーを作成 (Frocessingを利用)
hsl = new ColorHSV(0, 0.7, 0); // 彩度は70%で指定
値(0〜360で色相環の一周になる)をかえるだけで色相が変わります。カラフルな表現をつくるときに便利な命令です。Event.ENTER_FRAME
イベントが発生するたびに1.5ずつ色相をずらします。
hsl.h += 1.5; // 色相は時間経過で変化
hsl.v = 0.4 + 0.2 * Math.random(); // 明度は40%〜60%でランダム
var color:uint = hsl.value;
// 赤い丸を作成
var particle:MovieClip = new MovieClip();
particle.graphics.beginFill(color);
particle.graphics.drawCircle(0, 0, size);
パーティクルの合成方法を変える
表示オブジェクトの重なり方を指示できるblendMode
プロパティーを使って、レイヤーの合成方法を変更させておきます。パーティクルとパーティクルが重ねると、色が加算されて(BlendMode.ADD
の指定によって)、明るくみえるようになります。
particle.blendMode = BlendMode.ADD;
パーティクルのサイズを時間経過で小さくする
life
プロパティーの数値に比例して、パーティクルのサイズを小さくさせてみましょう。大きさを変化させるにはscaleX
とscaleY
プロパティーを使います。現在のライフが寿命の何%に相当するかを計算し(particle.life / MAX_LIFE
)、その値が0.0〜1.0
の範囲になるのでscaleX
とscaleY
プロパティーに適用します。
private const MAX_LIFE:int = 40; // 寿命の最大値
/** パーティクルを発生させます */
private function emitParticles():void
{
// for文を作成 (上限数はパーティクルの個数)
for (var i:int = 0; i < 2; i++)
{
// 赤い丸を作成
var particle:MovieClip = new MovieClip();
//(省略)
// 寿命
particle.life = MAX_LIFE;
//(省略)
}
}
/** パーティクルを更新します */
private function updateParticles():void
{
for (var i:int = 0; i < particleList.length; i++)
{
// i番目の要素を変数に代入
var particle:MovieClip = particleList[i];
//(省略)
// パーティクルのサイズをライフ依存にする
var scale:Number = particle.life / MAX_LIFE;
particle.scaleX = particle.scaleY = scale;
//(省略)
}
}
まとめ
いかがでしたでしょうか。パーティクルは難しそうなものに見えますが、一つ一つ分解してみるとシンプルな実装の組み合わせに過ぎません。パーティクルの移動方向や、パーティクル自体の形状を変更すると様々な表現に転換できます。ぜひ、いろいろパラメーターを調整してみてください。
応用技として、パーティクルのガベージコレクション対策に、「ゲームにおけるガベージコレクションとの付き合い方」で紹介されているオブジェクトプールや、「Stalingフレームワークの歩み」で紹介されているStarlingフレームワークでGPUをフル活用すると、パーティクル表現が極まっていくでしょう。
またHTML5 Canvas (CreateJS)でのパーティクルシステムの作り方をCreateJS でパーティクルシステムの開発に挑戦しよう - CreateJS入門 - ICS MEDIAで解説しています。この記事と内容はほぼ同じなのですが、Flashと異なる言語でも考え方は通用するということがおわかり頂けると思います。
それではみなさんも良きパーティクルライフを!