2

Flash Pro (AS3) で簡単な教育用アプリを開発していて、恥ずかしいほど行き詰まっています。

何かが発生したときの垂直方向のピークを示す単純なグラフを作成しようとしています (アイドル状態のときは平らな線)。グラフはプログラムから開始され、1 フレームおきに更新されるため、リアルタイムです。線は「グラフ」オブジェクトの一方の端から始まり、反対側に到達するまで続きます (たとえば、60 秒以上)。最後に到達すると、グラフ全体が左にスクロールして、最後に追加されるデータ用のスペースを作り始めます。データのスクロールと記録を永遠に (ユーザーが停止/一時停止するまで) 続けるように設計されています。

例を次に示します: http://img442.imageshack.us/img442/4784/runninggraph.png

そのようなグラフを実装する最良の方法は何ですか? ベースラインとティックで構成された単純なスクロール グラフ。

そんなに難しくないはず…と思っていました。私は 4 つの異なる方法を試しました (そのうちの 2 つは 100% 動作することが証明されましたが、遅すぎました)。もっといいものがあるに違いない!多分マトリックス変換+ bitmapFill?


質問の終わり; 単純化されたコードが続きます。


public class Graph extends Sprite
{
    include "constants.as";
    const pixelFactor:Number = (1 / GRAPH_TIMESCALE) * 1050; //60,000ms
    const amountToMove:Number = POLLING_RATE * pixelFactor, inverseTS:Number = 1.0/GRAPH_TIMESCALE;
    var graphHolder:GraphHolder; // A simple (large) sprite with many Graph instances
    var baseLine:Shape = new Shape(), stretched:Boolean = false;
    var apArray:Array = [], apRecycle:Vector.<Shape> = new <Shape>[];
    var apLength:int = 0, firstPeak:Shape;
    //var numberIndicator:TextField = new TextField();

    public function Graph(graphHolder:GraphHolder) 
    {
        //Set variables
        this.graphHolder = graphHolder; 
        mouseEnabled = false; mouseChildren = false;

        addChild(baseLine);
        with (baseLine)
        {
            x = 55; y = 10; // the base-line. Starts 55 pixels out so labels can be added
            graphics.lineStyle(2,0x000000); // Thickness 2, color black
            graphics.lineTo(1050,0);
            scaleX = 0.0005; // base-line starts as a little dot, expands with time
        }
    }

    // When not scrolling, draws idle baseline. When scrolling, drags peaks backwards.
    function drawIdle(timeElapsed:int):void
    {
        if (timeElapsed <= GRAPH_TIMESCALE) baseLine.scaleX = inverseTS * timeElapsed;
        else 
        {
            //if (graphNo < 1) graphHolder.scrollAxis(pixelFactor);
            if (!stretched) { baseLine.scaleX = 1.001; stretched = true; }

            // scroll all peaks
            if (apLength)
            {
                for (var i:int = 0; i < apLength; i++)
                {
                    apArray[i].x -= amountToMove;
                }
                firstPeak = apArray[0];
                if (firstPeak.x < 55) 
                { 
                    firstPeak.visible = false;
                    apRecycle[apRecycle.length] = firstPeak; // peaks are recycled instead of removed
                    apArray.shift(); // apArray is a linked-list Array; less shift cost

                    --apLength;
                }
            }
        }
        //lbd.copyPixels(graphHolder.baseCache,lbr,new Point(timeElapsed * pixelFactor,0), null, null, true);
        //line.graphics.lineTo(timeElapsed * pixelFactor,10);
    }
    function drawAP(timeElapsed:int):void
    {
        // Check for a peak in the recycle array
        if (apRecycle.length)
        {
            firstPeak = apRecycle.pop();
            firstPeak.x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
            firstPeak.visible = true;
            apArray[apArray.length] = firstPeak;
        }
        else
        {
            // Line drawn from baseline up 7 pixels.
            var peakShape:Shape = new Shape(); 
            //peakShape.cacheAsBitmap = true;
            addChild(peakShape);
            with (peakShape)
            {
                //cacheAsBitmap = true;
                graphics.lineStyle(2, 0x000000);
                graphics.moveTo(1, 10);
                graphics.lineTo(1, 3);
                x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
                apArray[apArray.length] = peakShape;
            }
        }
        ++apLength;
    }

    /* Reset baseline as tiny dot, remove peaks, reset peak array. 
       All peaks in recycle remain to draw from during next cycle. */
    function resetGraph():void
    {
        baseLine.scaleX = 0.0005; stretched = false;
        for each (var peak:Shape in apArray) removeChild(peak);
        apArray.length = 0; apLength = 0;
    }
}

繰り返しますが、本質的に、リアルタイムで発生する「アクション」を示す目盛りがグラフに表示されるはずです。グラフは、画面の最後 (N 秒) に達した後、左にスクロールを開始します。つまり、指定されたスペースの端に到達した後、無期限にスクロールし続けます。これを実装しようと試みた 3 つの方法を次に示します。どれも高速ではなく、他の問題を抱えているものもあります。

  1. グラフ上の対応する時点で新しい線分を作成して描画します -- ベースライン セグメント (小さな水平線) と目盛り (垂直線)。グラフは N 秒までしか進みませんが、時間 N+x のすべての目盛りは、グラフが右に永遠に続くかのように描画されます。次に、グラフの長方形の領域にマスクを使用し、N 秒に達したら、時間の経過とともにグラフを文字通り左に移動します。

    • これは最初は遅かったのですが、数分後には信じられないほど遅くなりました。これは、Flash がグラフ全体を左側の画面外でまだ処理しているように見えるためです。
    • 線分をグラフ オブジェクトに直接描画するのは良くありません。これは、各線分が明らかにオブジェクトであり、これらの無数のオブジェクトのそれぞれが描画後に明示的 (または暗黙的) な参照を持たないためです。(「見えない、頭から離れている」==遅くて間違っている。)
  2. 目盛り (小さな垂直線) とベースライン (小さな水平線) の形状を含む bitmapdata オブジェクトを作成し、すべてのフレームで次のビットマップ (線分) をグラフ オブジェクト (現在もビットマップ) にコピーします。ScrollRect で左にスクロールします。シンプルで、表示オブジェクトの代わりに bitmapData と copyPixel を使用する以外は #1 と非常によく似ています。

    • 2048 ピクセルを超えると、ビットマップにデータを描画できなくなるため、これは悪いニュースです。
    • アプリをスケーリングすると、すべてのビットマップが粗くギザギザに見え、ギャップが導入されました (各ビットマップに「スムージング オン」のフラグが立てられているにもかかわらず)。これはおそらく、グラフを構成する膨大な数の線分セグメントがすべてばらばらで他の近くに描画されているためです。ピクセル境界。
    • Scrollrect は GPU によって高速化できません (また、アプリはモバイル デバイスで動作する必要があります)。
    • 並行ビットマップを維持し、一方を他方にコピーして貼り付けてグラフを更新する (「ブリッティング」?) のは面倒ですが、ベクトル手法とは異なり、サブピクセル レベルでも非常に不正確です。また、フレームごとにゼロからすべてのピークを再計算するか、バックグラウンドで複数の幅 2048 ビットマップを作成、破棄、およびマージするために必要な計算は、長期的にはよりコストがかかることが判明する可能性があります。
  3. 1 つの形状、水平線 (ベースライン)、「スケールアップ」してグラフを塗りつぶし、停止します (N 秒)。したがって、時間の経過とともにグラフ全体に伸び、N 秒後に何もしないように見えるため、これが「ベースライン」です。ベースラインの上の各目盛りは Shape オブジェクトであり、ベースラインの上に描画されます。次に foreach ループは、グラフの左に到達するまですべての目盛りの x 座標を変更します。その時点で目盛りは非表示になり、リサイクル ベクトルに追加されます。右 (N で描画)。

    • まだ遅いですが、#1 よりもはるかに高速で、グラフがスクロールを開始したときに一定の速度を維持します。
    • プロセスを高速化する方法がわかりません。背景とグラフ、さらには一番上の行をビットマップとしてキャッシュしようとしましたが (cacheasbitmapmatrix で試しても)、デバッグ プレーヤーではプレーンなベクター レンダリングが最速のようです。
  4. トゥイーン。実際には、まだトゥイーンでグラフ自体を実装していませんが、これまでのところ、enterFrame 移動ハンドラー (フレームごとにオブジェクトの座標を手動で変更する) を Tweens に置き換えると、すべてのアニメーションがはるかに遅くなりました。図に行きます。ですから、グラフでは役に立たないと思います... TweenLite(ライセンス)も使用できません。


#3 については、正しい方向に進んでいると思いますが、AVM レンダリングの詳細について十分に理解していないのではないかと心配しています。ピークにシェイプの代わりにビットマップを使用する必要がありますか? #3 のオブジェクトで #1 と同じ手法を使用する必要がありますか (つまり、大きく動く「tickSprite」にピークを追加しますが、ピークが画面外に出たときにピークを「リサイクル」します)。この場合、含まれているスプライトは、画面外にティックがないにもかかわらず、左の画面外にプッシュされ続けます。画面の左側が完全に空の場合でも、リソースを直線的に消費しますか?

#3 で考えられる速度の原因: - 多数の移動する Shape と、それらを移動するために必要な for ループ。- ベクター、ビットマップ、および背景の機能。たとえば、Graph オブジェクト自体は、グラフの背後にあるアルファ 0.75 の灰色の四角形である背景形状 (ビットマップとしてキャッシュされる)、一連のラベルと軸 (これもビットマップとしてキャッシュされる)、およびグラフ形状 (ベースライン用の 1 つのスケーリング「線」形状と、ベクトルに格納された多数の「目盛り」形状)... - ユーザーは通常のベクトル オブジェクトをグラフの背後にドラッグできます。アルファ 0.75 の背景形状の背後にあるすべてのものを動かしてレンダリングします。

4

1 に答える 1

0

オプション 1 が唯一の実行可能な方法です。

あなたがする必要があるのは、データをどこかに保存し(配列/ベクトル)、線/目盛りが画面外になったら破棄することです。ユーザーが左にスクロールしたい場合は、保存したデータを使用して目盛りを再作成します (スクロール時に右にこぼれるものと同じ)。

最終的には理論的にはメモリが不足するため、データを保存するために、ある種のシード(日時?)を持つローカル共有オブジェクトのようなローカルストレージ技術を実装する必要があるかもしれません。

編集:

左にスクロールする必要がないので、とても簡単です。それぞれの「目盛り」を独自の表示オブジェクトにするだけです。(シェイプまたはスプライトである場合)、スクロールチェックとして、個々の目盛りが画面外にあるかどうかを確認します。そうであれば、それを破棄します。

サンプル グラフ クラス:

public class Graph extends Sprite

    private var container:Sprite;

    public function Graph():void {
        //create your baseline

        //create the container
        container = new Sprite();
        addChild(container);

        this.addEventListener(Event.ENTER_FRAME, drawIdle);
    }

    private function drawIdle(e:Event){
        //move your baseline?

        //scroll container
        container.x += scrollAmount;


        //iterate through all the ticks
        var i:int = container.numChildren;
        var tick:Shape;

        //iterate through the container children backwards (has to be backwards since your potentially removing items)
        while(i--){
            tick = container.getChildAt(i);
            //check if the tick is off screen to the left
            if(container.x + tick.x < 0){
                //if so, remove it so it can be garbage collected
                container.removeChild(tick);
            }
    }

    public function drawAp(timeElapsed):void {
        //create your tick
        var tick:Shape = new Shape(); 

        //draw your tick
        tick.graphics.beginFill(0);
        tick.graphics.drawRect(0,0,tickWidth,tickHeight);
        tick.graphics.endFill();

        //position your tick
        tick.x = whatever;
        tick.y = whatever;

        //add it to your tick container
        container.addChild(tick);
    }

}
于 2012-09-05T23:51:12.470 に答える