【レスポンスプログラミングの思考芸術】(4)飛行機ゲームからの同時と流れの融合を理解する


本文はRxjs応答式プログラミング-第三章:コンカレントプログラムを構築するこの文章の学習ノートである.
サンプル・コード管理:http://www.github.com/dashnowords/blogs
もっと多くの博文:《大史は大先端に住んでいます》オリジナルの博文目録
一.重点を置く
  • 外部状態をできるだけ避ける基本的な関数式プログラミングでは、純粋な関数は構築されたデータパイプが正確な予測可能な結果を得ることを保障することができ、応答式プログラミングでは同様の要求があり、博文の例では、外部状態に依存すると、複数の購読者が同じストリームを観察する際に互いに影響し合い、混乱を引き起こすことが明らかになった.異なるストリームの間に共有の外部依存が現れる場合、一般的な実装の考え方は2つあります.
  • は、この外部状態を独立して観察可能なオブジェクトを生成し、実際の論理的要件に基づいて正しいストリームマージ方法を使用してマージする.
  • は、この外部状態を独立して観察可能なオブジェクトを生成し、Subjectを使用して他の論理ストリームと関連付ける.

  • 配管の実行効率前節ではcompose演算子を用いて純関数を組み合わせることで、コンテナ関連の方法のほとんどが高次関数であることが分かる.これにより、配管は構築中に有効にならず、キャッシュが組み合わされる(前編のIO容器の例から実行を遅らせる形が見られる).サブスクリプションされると、本当に起動します.
  • SubjectクラスSubjectは同時にObservableobserverの機能を備えており、メッセージを購読したり、データを生成したりすることができ、一般的にストリームと観察者のエージェントとして使用され、ストリームのデカップリングを実現するために使用することができる.より詳細なサブスクリプション制御を実現するために、Subjectはまた、以下のいくつかの方法を提供する.
  • AsyncSubject AsyncSubject観察のシーケンスが完了すると、最後の値が発行され、この値が永遠にキャッシュされ、その後、このAsyncSubjectを購読した観察者はすぐにこの値を得ることができます.
  • BehaviorSubject Observerは、BehaviorSubjectを購読する際に、最後に発行された値を受信し、その後に発行された値を受信する.一般に、観察者が受信したメッセージは、購読時間に最も近いデータおよびストリームの後に生成されたデータである.
  • ReplaySubject ReplaySubjectは、傍受されたストリームからの値をキャッシュし、その後、構造関数にパラメータを入力することによってバッファ時間の設定を実現する任意の遅いObserverに送信する.


  • 二.理論から実践まで
    原文の中で1つのとても详しい飞行机を打つゲームのコードを提供して、しかし私は依然としてあなたがその基本的な原理と考え方を熟知した后に自分でそれを実现することを提案して、それから原文の中のコードと対比して、どれらのものが本当に理解したことをはっきりさせて、どれらはただあなたが自分が理解したと思って、それからいくつかのとても明らかな最适化点を探して、応答プログラミングの思考パターンを引き続き使用して実現してみましょう.最初はどこから手をつけるか分からないのが普通でしたが(もちろん筆者の自己慰めかもしれませんが)、応答プログラミングの思考習慣を育成するのに役立ちました.筆者は自分の実現に右ボタンで宇宙船のタイプを切り替える機能を加えており、ビジネスロジックを書くよりもゲームの開発が面白いと言わなければならない.
    スプライト図の座標を正確に計算していないため、衝突検出時にいくつかのばらつきがあります.
    三.問題と反省
  • canvasのサイズに関する問題は、
    
    
    //    2
    canvas = document.getElementById('canvas');
    canvas.height = 300;
    canvas.width = 300;
    回避する必要があるいくつかの方法(いずれも画板のサイズだけを変更し、キャンバスのサイズを変更しないと、図面が引き伸ばされることになる):
    //1.CSS  
    #mycanvas{
       height:300px;
       width:300px;
    }
    //2.DOM  API  
    canvas = document.getElementById('canvas');
    canvas.style.height = 300;
    canvas.style.width= 300;
    //3.Jquery  
    $('#mycanvas').width(300);
    canvasの幅がパーセンテージ設定をサポートしていないことに注意する必要があります.
  • Rx.Observable.combineLatest以降の全体的なストリームは、combineLatestという演算子を自動的にトリガーしない.この演算子は、統合された各ストリームの最新値を維持する必要があるため、すべてのストリームがemitの1回のデータを待ってからemitデータを開始する必要がある.したがって、自動起動の方法も簡単で、初回データをトリガしにくいストリームに初期値を追加すればよい.筆者が上述した右ボタンを実現して宇宙船の外観を交換する際に実現したように、startWith演算子を用いて初期値を提供すると、マウスカーソル移動時にcombineLatestで生成された宇宙船ストリームがデータを生産し始める.さらに、combineLatestが結合すると、いずれのストリーム生成データも合成後のストリーム生成データをもたらし、グラフィックデータの座標は描画関数で実現されるため、受動的なトリガは、元のストリームの予想周波数を乱し、いくつかのステージ要素の位置や形状の変化をより速くする可能性がある.この場合、sample( )演算子を使用して、マージされたストリームをサンプリングし、データトリガ周波数を制限することができる.
  • ますます速くなる流れ筆者自身が敵機を生成する時、初めてこのようなコードを書きました:
    let enemyShipStream = Rx.Observable.interval(1500)
    .scan((prev)=>{//             ,    scan               
         prev.push({
            shape:[238,178,120,76],
            x:parseInt(Math.random() * canvas.width,10),
            y:50
         });
         return prev
    },[])
    .flatMap((enemies)=>{
       return Rx.Observable.interval(40).map(()=>{
           enemies.forEach(function (enemy) {
               enemy.y = enemy.y + 2;
           });
           return enemies;
       })
    });
    が運行する時敵機の速度がますます速くなることを発見して、とても奇妙で、もしあなたが問題がどこにあるかが見えないならば、大理石図を描いて、flatMapが集まった全体のデータストリームがどのように構成されているかを見てみることをお勧めします.時間が経つにつれて、複数のストリームが最初のソースデータを操作しているため、座標の自己増加の周波数はますます速くなるのが容易に見えます.
  • scanオペレータの集約結果の大きさを制限自分でコードを書く場合、scanオペレータを用いて生成されたデータを集約することが多く、集約の形式が集合形式であれば、その占有空間は時間が経つにつれてますます大きくなり、解決策は,scanオペレータが受信したコールバック関数において配列のfilter法を用いて集約結果をフィルタリングし,新しい配列を生成して返すことで,集約結果の大きさを制御することである.
  • 衝突検出の実現構想衝突検出は即時に有効であるため、フレームごとに行う必要があり、最終的にまとめられたストリームはデータを送信するたびに描画すべき要素の座標情報をすべて取得することができ、この場合は衝突検出を実現するタイミングであり、衝突を検出すると座標データにマークを付けるだけで、次に、最初のscanの集約方法では、タグに合致するデータを消去すればよいが、衝突を検出する論理と衝突発生後のデータ消去および描画判断は、異なる場所で記述されており、筆者が提供した例で見ることができる.

  • 四.参照コード及びDemo説明
    demoのindex.htmlは原文学習時にコピーされたコードであり、mygameのコードは筆者が書いたものであり、必要な読者が自分で使えばよい.myspace.js-星空のバックグラウンドストリーム
    /**
     *   
     *     :      resize         
     */
    //           
    let canvas = document.getElementById('canvas');
        canvas.height = window.innerHeight;
        canvas.width = window.innerWidth;
        canvas.style.backgroundColor = 'black';
    let ctx = canvas.getContext('2d');
        ctx.fillStyle = '#FFFFFF';
    let spaceShipImg = new Image();
        spaceShipImg.src = 'plane2.png';
    
    //    
    //               [{x:1,y:1,size:1},{}]
    let starStream = Rx.Observable.range(1,250)
    .map(function(data){
       return {
          x:Math.ceil(Math.random()*canvas.width),
          y:Math.ceil(Math.random()*canvas.height),
          size: Math.ceil((Math.random()*4))
       }
    })
    .toArray()
    .flatMap(function(stars){
        /*         ,              
        *                
        */
        return Rx.Observable.interval(40).map(function () {
            stars.forEach(function (star) {
                star.y = (star.y+2)  % canvas.height; 
            });
            return stars;
        })
    })
    
    //    
    function paintStar(stars){
        //    ,                
        ctx.fillStyle = '#000000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = '#FFFFFF';
        //    
        stars.forEach(function (star) {  
            ctx.fillRect(star.x, star.y, star.size, star.size);  
        });
    }
    myship.js-味方の宇宙船の流れ
    /**
     *      
     *     :               ?
     */
    
    //     
    let mouseMoveStream = Rx.Observable.fromEvent(window, 'mousemove')
    .distinct() //         
    .map(function (data) {
        return {
            x:data.clientX,
            y:canvas.height - 100
        }
    });
    
    //       
    let shipTypeStream = Rx.Observable.from([
           [0,0,130,90],
           [135,0,130,100],
           [265,0,126,100],
           [0,170,110,100]
        ]).toArray();
    
    //     -      ,        ,               
    let mouseRightStream = Rx.Observable.fromEvent(window, 'contextmenu')
    .map(function (event) {
        event.preventDefault();//        
    })
    .scan(count=>count+1,0)//      
    .map(count=>count % 4).startWith(0);//            
    
    
    //     -      
    let mouseClickStream = Rx.Observable.fromEvent(canvas, 'click')
    .sample(200)
    .scan((prev,cur)=>{
       prev.push({
           x:cur.clientX,
           y:canvas.height - 50,
           used:false //            
       });
       return prev.filter((bullet)=>{return bullet.y || !bullet.used});
    },[])
    .startWith([{x:0,y:0}]);
    
    //     
    let myShipStream = Rx.Observable.combineLatest(mouseMoveStream,
                                                   shipTypeStream,
                                                   mouseRightStream,
                                                   mouseClickStream,
                                                   function(pos,typeArr,typeIndex,bullets){
                                                      return {
                                                        x:pos.x,
                                                        y:pos.y,
                                                        shape:typeArr[typeIndex],
                                                        bullets:bullets
                                                      }
                                                   });
    
    //    
    function paintMyShip(ship) {
        //    
        ctx.drawImage(spaceShipImg,ship.shape[0],ship.shape[1],ship.shape[2],ship.shape[3], ship.x - 50, ship.y, ship.shape[2],ship.shape[3]);
        //      
        ship.bullets.forEach(function (bullet) {  
             bullet.y = bullet.y - 10;
             ctx.drawImage(spaceShipImg, ship.shape[0],ship.shape[1],ship.shape[2],ship.shape[3], bullet.x , bullet.y, ship.shape[2] / 4 ,ship.shape[3] / 4);
        });
    }
    enemy.js-敵機流
    /**
     *     
     */
    
    //    -          
    function isVisible(obj) {
       return obj.x > -60 && obj.x < canvas.width + 60 && obj.y > -60 && obj.y < canvas.height + 60;
    }
    
    // 2              
    let enemyShipStream = Rx.Observable.interval(2000)
    .scan((prev)=>{//             ,    scan                  
         let newEnemy = {
            shape:[238,178,120,76],
            x:parseInt(Math.random() * canvas.width,10),
            y:50,
            isDead:false,//         
            bullets:[]
         }
    
         //      
         Rx.Observable.interval(1500).subscribe(()=>{
             if (!newEnemy.isDead) {//            
                newEnemy.bullets.push({ x: newEnemy.x, y: newEnemy.y });
             }
             newEnemy.bullets = newEnemy.bullets.filter(isVisible);
         });
          
         prev.push(newEnemy);
         return prev.filter(isVisible);
    },[]);
    
    //    
    function paintEnemy(enemies) {
       enemies.forEach(function (enemy) {
           //           
           enemy.y = enemy.y + 3;
           enemy.x = enemy.x + parseInt(Math.random()*8 - 4,10);
           //             
           enemy.bullets.forEach(function(bullet){bullet.y = bullet.y + 16;});
           //           
           if (!enemy.isDead) {
             ctx.save();
             ctx.translate(enemy.x, enemy.y);
             ctx.rotate(Math.PI);
             //    
             ctx.drawImage(spaceShipImg,enemy.shape[0],enemy.shape[1],enemy.shape[2],enemy.shape[3], 0, 0, enemy.shape[2] * 0.8 ,enemy.shape[3] * 0.8);
             ctx.restore();
           }
           //    
           enemy.bullets.forEach(function (bullet) {
              ctx.save();
              ctx.translate(bullet.x, bullet.y);
              ctx.rotate(Math.PI);
              ctx.drawImage(spaceShipImg,enemy.shape[0],enemy.shape[1],enemy.shape[2],enemy.shape[3], 0, 0, enemy.shape[2] / 4,enemy.shape[3] / 4);
              ctx.restore();
           });
           ctx.restore();
       });
    }
    
    collision.js-衝突検出
    //     
    function isCollision(target1, target2) {
     return (target1.x > target2.x - 50 && target1.x < target2.x + 50) && (target1.y > target2.y - 20 && target1.y < target2.y + 20);
    }
    
    //      
    function checkCollision(myship, enemies) {
        let gameOver = false;
        myship.bullets.forEach(function(bullet) {
            enemies.forEach(function (enemy) { 
                //         
                if (isCollision(bullet, enemy)) {
                     bullet.used = true;
                     enemy.isDead = true;
                };
                //       ,        
                enemy.bullets.forEach(function (enemyBullet) {
                    if (isCollision(myship, enemyBullet)) {
                        gameOver = true;
                    }
                })
            })
        });
        return gameOver;
    }
    combineAll.js-最終的なゲームストリームを融合
    /**
     *      
     */
    
    let gameStream = Rx.Observable.combineLatest(starStream,
                                                myShipStream,
                                                enemyShipStream,
                                                function (stars,myship,enemies) {
                                                 return {
                                                    stars,myship,enemies
                                                 }
    })
    .sample(40);//sample                         
    
    //      
    function paintAll(data) {
         let isGameOver;
         isGameOver = checkCollision(data.myship, data.enemies);//          
         if (!isGameOver) {
             paintStar(data.stars);
             paintMyShip(data.myship);
             paintEnemy(data.enemies);
         }else{
            gameSubscription.dispose();
            alert('    ');
         }
    }
    
    //             
    let gameSubscription = gameStream.subscribe(paintAll);