Actionscript

> Sashamon

緩緩だなぁとSashamonを聞きながら、
wonderflを見ていたら、無性に手を動かしたくなり、
Processing > MetaballをFlashへ。

> Metaball

このMetaballのレンダリングテクニックは、
1980年頃、Each metaballという人によって紹介されたらしい。

> Metaball Code at wonderfl
以前再帰による幅優先探索を紹介したが、
迷路が巨大になると、
スタックオーバーフローの問題に突き当たる。

再帰が駄目なんじゃなく、お前の再帰関数が駄目なんだよ、
とか言うけど、無視して、
この場合、再帰を使用して幅優先探索を実装することは、
向いていないと言わせてください。

解決策として、再帰では無く、
do whileのループを使用する方法が考えられる。

ただ、ループの上限を設定しない場合、
その他のプログラムの実行速度や描画に影響を及ぼす。

下記のようにループに対して、
最大ループ回数を設定し、
タイマーを使うことで、解決できる。

private var currentCount:uint;

private var searchRetryTimer:Timer=new Timer(100,1);

public function search(maxLoopCount:uint=1000){
     do{
       currentCount++;
       if(currentCount>maxLoopCount){
        searchRetryTimer.start();
        return;
       }
       checkGoal();
     }while(!founded);
    
     callBack.apply(scope , [goalInfo]);
}

private function onSearchRetryTimer(evt:Timer):void{
      search();   
}

Math.randomは、毎回同じ確率でランダム値を
返却するとても単純な優しいメソッドです。

しかし、このMath.randomメソッドは、非常に、柔軟で、パワーがあります。

Math.randomのようなコントラストの強いランダム現象は、
私たちの周りでは、あまり存在しません。

天候などの自然現象、人の成長や感情、生活、会話、また、
サウンドスペクトラムのビジュアライズ表示など、
ほとんどすべての物事は、コントラストが強い現象ではありません。

var a:Number=Math.random()*10000;
を100回ループ実行した出力結果、

1,500,9299,4,10,15,700,600,2090,1090,99,8829,,,,,,,,,,,,,

これは、非常にコンストラストが強い結果と言えます。



この現象は、私たちにとって異常です。



ActionScriptには、より自然なランダムメソッド、
BitmapData.perlinNoiseメソッドは存在しますが
BitmapDataに限らずより自然なランダムを、取得することが必要な機会は多いです。

より自然なランダムの意味は、drunkメソッドを知ることで理解できると思います。
より自然なランダム発生装置、drunkメソッドを紹介します。


> drunk method sample


上下のプログラムの違いは、
ENTER_FRAMEイベントリスナーメソッド内の
x,y,rgbのランダム値の取得の3行を、
drunkメソッドかMath.randomメソッドかに変更しただけです。
その他、すべてのコードが全く同じとは思えないランダムメソッドの威力です。


※どちらかといえば、
私たちの周りで起きている出来事すべては、上の自然なランダムdrunkメソッドで
表現されているものに近いと思います。


drunkメソッドも内部で、Math.random()を使用していて、
実は、Math.random同様とても単純で、作成も簡単です。


第一引数 : 0から指定した範囲のランダムな値を返却します。
第二引数 : 前のランダム値のオフセットを指定します。

例えば、
Randomクラスが、drunkメソッドを実装しているとします。

var rnd:Random=new Random();
var value:Number=rnd.drunk(100,10);

trace(value);

//output : 80

value=rnd.drunk(100,10);

このときのvalueの出力結果は、

70から90の間の乱数です。

これが,drunkメソッドです。
非常に単純なこのdrunkメソッドを使用するだけで、
上記のサンプルの結果が得られます。

このdrunkメソッドは、Max/Mspのdrunkオブジェクトを参考にしました。

> Max/Msp drunk method

このオブジェクトのdrunkという名前は、
数値の範囲の中で「酔っぱらいの千鳥足」のように
変化する値を返却することからこのように名づけられたらしいです。

Actionscriptで、迷路の最短距離の解が必要になり、

幅優先探索アルゴリズムを使用してみることに。

幅優先探索

下記の仕様で迷路配列を定義する。

// 0 : 道ID
// 1 : 壁ID

private const maze_ary:Array=[
[1,0,1,1,1,1,1,1],
[1,0,0,0,0,0,0,1],
[1,1,1,0,1,1,0,1],
[1,1,1,1,1,1,0,1]
]

// 5 : 探索済みID
private static const PASSED_ID:uint=5;


1. 探索を開始する。

まず空のキュー配列を作成し、

探索を開始する最初のマスのMassインスタンスを追加する。

※すべてのMassインスタンスは、
観測者、リレー走者みたいなものと、考えるとわかりやすい。
最初のMassインスタンスは、スタート走者とイメージできる。

迷路探索と最適解

※すべてのMassインスタンスは、
自分のマス情報であるrow , colプロパティと、
探索開始から、自分のマスにたどり着いたまでの
経路massList_aryを保持している。
つまり最初のMassインスタンス(スタート走者)に渡すmassList_aryは、空である。

※Massインスタンスは、コンストラクタで、
与えられたmassList_aryをコピーし、自分のMass情報
{row : row , col : col}を、コピーしたmassList_aryに追加する。


2.再帰処理を行う。

キュー配列からqueue_ary[0]のMassインスタンスを取り出す。

このインスタンスをcurrentMassと呼ぶとする。

currentMassのrow,colを調べ、左右上下で
キュー配列に追加すべき、移動可能なマスを探す。

ここでもし、移動可能なマスが、

すでに経由済みのマスであれば追加しない。

上記2つの条件を満たす場合、その移動可能なマスのMassインスタンスを作成し、
キュー配列に追加する。
この時、作成されるnextMassインスタンスのコンストラクタで、
currentMassインスタンスが保持している経由配列massList_aryを渡す。
これでnextMassインスタンスは、今までの経路を知っている。

(次の走者にバトンを渡す。バトンを渡す際は必ず、
自分に辿りつくまでに経由した各走者の位置配列を渡すことで、
現在の走者はどの経路を通ってバトンが渡ってきたか
を常に知っていることになる。)

左右上下調べ、キュー配列に、有効なMassインスタンスをすべて追加後、
maze_ary[currentMass.col-1][currentMass.row-1]=PASSED_ID
とし、すでに経由済みMassであることを記憶する。

不要になったcurrentMassインスタンスをキュー配列から削除し、
キュー配列に追加されているnextMassインスタンスに、委ねる。

ここでキュー配列を調べ、空でなければ 2. を繰り返す。


最初にゴールを見つけたMassインスタンスが、
最短経路massList_aryを保持していることになる。

ゴールが見つかった時点で、再帰処理をストップする。

※ここで重要なことは、
左右上下調べ、queue_aryに追加後
再帰処理を行うことだ。

でなければ、迷路に広いスペースがあった場合、
必ずしも最短距離を導き出してくれるとは限らない。



以上を、Actionscriptでプログラミングする。



----------------------------------------------------------------


MazeConfigクラスは、迷路の定数などを保持する。
MazeDataクラスは、迷路状況(どのマスに今いるかなど)
とMazeConfigクラスの一部のデータを保持する。
Massクラスは、MazeUtilsの内部クラス。


package utils
{

    import data.MazeConfig;
    import data.MazeData;
  
    public class MazeUtils
    {
       
        public static var queue_ary:Array;
       
        public static var maze_ary:Array;
       
        public static var COL:uint=MazeData.COL;
       
        public static var ROW:uint=MazeData.ROW;
       
        public static var goalRow:uint=MazeData.goalRow;
       
        public static var goalCol:uint=MazeData.goalCol;
       
        public static var startRow:uint=MazeData.startRow;
       
        public static var startCol:uint=MazeData.startCol;
       
        private static var goalInfo:Array;
       
        public static var PASSED_ID:uint=5;
       
        /**
         * 現在の位置からゴールまでの最短距離の配列を返却します。
         *
         * @return
         *
         */       
        public static function get goalList():Array{
            goalInfo=null;
           
            startRow=MazeData.currentRow;
            startCol=MazeData.currentCol;
           
            maze_ary=MazeData.copyInfo;
           
            queue_ary=[];
            queue_ary[0]=new Mass(startRow , startCol , []);
           
            check();
           
            return goalInfo;
        }
           
        private static function check():void{
            var currentMass:Mass=queue_ary[0];
            queue_ary.shift();
          
           if(maze_ary[currentMass.col-1][currentMass.row-1]==PASSED_ID){
               return;
           }

            maze_ary[currentMass.col-1][currentMass.row-1]=PASSED_ID;
           if(goalInfo!=null){
                   //最短距離が見つかっているので、残りのmassでは、何もしない。
            }else if(currentMass.isGoal){
                   //ゴールの最短距離を見つけた。
                goalInfo=currentMass.massList;
            }else{
                var nextMass:Mass;   
               
                //前方
                if(needCheck(currentMass.row , currentMass.col+1)){           
                    nextMass=new Mass(currentMass.row , currentMass.col+1 , currentMass.massList);
                    queue_ary.push(nextMass);
                    //ここで、check関数を実行してしまうと最短距離でなくなる可能性がある。
                }
               
                //後方
                if(needCheck(currentMass.row , currentMass.col-1)){
                    nextMass=new Mass(currentMass.row , currentMass.col-1 , currentMass.massList);
                    queue_ary.push(nextMass);
                }
               
                //左方
                if(needCheck(currentMass.row-1 , currentMass.col)){
                    nextMass=new Mass(currentMass.row-1 , currentMass.col , currentMass.massList);
                    queue_ary.push(nextMass);
                }
               
                //右方
                if(needCheck(currentMass.row+1 , currentMass.col)){
                    nextMass=new Mass(currentMass.row+1 , currentMass.col , currentMass.massList);
                    queue_ary.push(nextMass);
                }

                currentMass=null;

                if(queue_ary.length==0){
                    trace("not founded");
                }else{
                    check(queue_ary);
                }
            }
           
           
        }
       
        /**
         * 一度も通っていない場合 &&
         * マスが存在する &&
         * 道である場合、trueを返却します。
         *
         * @param row
         * @param col
         * @return
         *
         */       
        private static function needCheck(row , col):Boolean{
            if(row>1&&row<=ROW&&col>1&&col<=COL){
                return maze_ary[col-1][row-1]==MazeConfig.ROAD_ID;
            }
            return false;
        }
    }
}

/**
 * マス情報と経由配列を保持する内部クラス(観測者)
 *
 * @author
 *
 */
import utils.MazeUtils;

class Mass{
    private var mass_ary:Array;
    private var maze_ary:Array;
   
    private var _row,_col:uint;
   
    public function Mass(row:uint,col:uint,list:Array ){
        this.mass_ary=[];

        this._row=row;
        this._col=col;
       
        var len=list.length;
        for(var i=0;i<len;i++){
            mass_ary.push(list[i]);
        }
       
        this.mass_ary.push({row:_row , col:_col});

    }
   
    public function get massList():Array{
        return mass_ary;
    }
   
    public function get isGoal():Boolean{
        return row==MazeUtils.goalRow&&col==MazeUtils.goalCol;
    }

    public function get row():uint{
        return _row;
    }
   
    public function get col():uint{
        return _col;
    }
}





高速化など考えると、多々改善の余地はありし。
知らんけど。
as2でもas3でも、
flvの再生で、NetStreamのonStatusイベントを、
監視して、プログラムすると、
Play.EmptyとかPlay.Fullとかいちいち律儀に
発生して、やりずらい。
アニメーションイベントを意識しないで
作る人は、そんなことはないかもしれない。

EmptyとかFullは、バッファ中かどうかを知りたいときとかに使うけど、
実際バッファ中かどうかというよりも再生が、
止まっているか正常に近いレベルで進んでいるかを知りたい。
ユーザーが気にならない程度のバッファなどはどうでもいいんだが、
Play.Buffer、Play.Emptyイベントはいちいちうるさく相手にしにくい。

private var playing:Boolean;

private var checkPlayheadProgressIntervalID:Number;

private var checkPlayheadProgressIntervalTime:Number=.5*1000;

private var prevPlayheadTime:Number;

private function checkPlayheadProgress():void{
  if(playing){
     if(ns.time==prevPlayheadTime){
        //止まっている。
        //dispatchEvent(new FLVEvent(*****));
     }else{
        再生されている。
     }
 }

 prevPlayheadTime=ns.time;
}

こんな感じで、インターバル前のplayHeadの時間を保持し、
インターバルで直前のplayHeadをチェックするし、
再生が正常なレベルかどうか確認する方が、
onStatusを調べるよりもずっと楽な気がする。

あと例えば、
time outエラーのような画面も強制的に表示できるかもしれないし、
ユーザーの回線環境なども簡易的に判断できるといえばできるかもしれないし、
イベント発生を自分で意図的に操作しやすい気がする。

あとonStatusのエラー系のイベントも
結局ユーザーが知りたいのはエラー内容じゃなくて、
エラーが発生したかどうかだと思うから、

エラー系のイベントはまとめて、
private function onError(errorMessage:String){
    dispatchEvent(new ErrorEvent(ErrorEvent.Error , false ,fasle , errorMessage));
}

としておくと楽。

ついでにNetStatusEventをすべてブロードキャストする

private function onNetStatusEvent(evt:NetStatusEvent):void{
    dispatchEvent(evt);
}

も実装すると安心。

でも、調子のって、あんまりクラス複雑にしすぎちゃうと、
僕の力では、FLVPlaybackが使いづらいのと同じように使いづらくなった。

細かい機能部分は、こんな感じのクラスに委譲するクラスを、
プロジェクトによって作った方がいいと思った。


> セントリーノ氏の優雅な週末


FLV約180個。。


as3では、nullは傷つきやすい存在だ。


nullのメソッドなどにアクセスしてしまうと、
TypeErrorがスローされtry,catchしていない場合プログラムは、
簡単に壊われる。

TypeErrorは、flash.errorsパッケージに存在せず、トップレベルのクラスだ。
flexbuilderは、トップレベルのクラスを作ろうとすると、
怒鳴り倒してくるくせに、人生は矛盾だらけだ。

私たちは、
アイツはnullかどうかを絶対に忘れてはいけない。

if(aitsu!=null){

}else{
    throw new Error("FUCK");
}

このコードを何回も書くことで、自作クラスが、
とてもブッ細工になり、ゴミ箱に捨てたくなる。


地獄だ。


何も行わないNullオブジェクトを導入する。
アイツがnullかどうかを忘れ、
安心して眠れるかもしれない。




本当のことを言うと実は使ったことはない。汗
使った印象として、
削除が重要だったりする。

yajinfusa.jpg















FLVPlaybackには、close()が無い。
FLVPlaybackクラスを調べると、バッファ中にstop()を呼ぶと、
一旦QueCommandにpushしてる。

が、、、バッファ完了後、再生がはじまってしまう。

しかも悲惨な事に、
アンロードしたloader.contentのFLVPackplayerですら
REMOVED_FROM_STAGE時で、バッファ中であった場合、
stop()を実行するも無視され再生される。

調べてみると、
getVideoPlayer(0)で、VideoPlayerを取得でき、
VideoPlayerは、close()が実装されている。

var videoPlayer=flvPlayback.getVideoPlayer(0);
vidoPlayer.close();


NetStreamを閉じることができた。


addEventListener(Event.REMOVED_FROM_STAGE , onRemovedFormStage);

function onRemovedFromStage(evt : Event):void{}
    var videoPlayer=flvPlayback.getVideoPlayer(0);
    vidoPlayer.close();
}
実際コンテンツ作成で検証していないので、
ベストプラクティスかどうかわからないけど、メモ。

fuuya.jpg













事前条件  ------------------------------------------------------------

main.swfがtest.swfをロードします。
またtest.swfは、rootの一フレーム目と最終フレームに
数百バイトのデータを持ち、
一フレーム目には初期化コードが記述れています。

--------------------------------------------------------------------------

var loader:Loader=new Loader();
loader.load(new URLRequest("test.swf"));