響應式編程思維藝術】 (4)從打飛機游戲理解并發與流的融合

      網友投稿 787 2022-05-30

      本文是Rxjs 響應式編程-第三章: 構建并發程序這篇文章的學習筆記。

      示例代碼托管在:http://www.github.com/dashnowords/blogs

      更多博文:“大史不說話”前端知識地圖

      【響應式編程的思維藝術】 (4)從打飛機游戲理解并發與流的融合一. 劃重點二. 從理論到實踐三. 問題及反思四. 參考代碼及Demo說明

      一. 劃重點

      盡量避免外部狀態

      在基本的函數式編程中,純函數可以保障構建出的數據管道得到確切的可預測的結果,響應式編程中有著同樣的要求,博文中的示例可以很清楚地看到,當依賴于外部狀態時,多個訂閱者在觀察同一個流時就容易互相影響而引發混亂。

      當不同的流之間出現共享的外部依賴時,一般的實現思路有兩種:

      將這個外部狀態獨立生成一個可觀察對象,然后根據實際邏輯需求使用正確的流合并方法將其合并。

      將這個外部狀態獨立生成一個可觀察對象,然后使用Subject來將其和其他邏輯流聯系起來。

      管道的執行效率

      在上一節中通過compose運算符組合純函數就可以看到,容器相關的方法幾乎全都是高階函數,這樣的做法就使得管道在構建過程中并不不會被啟用,而是緩存組合在了一起(從上一篇的IO容器的示例中就可以看到延緩執行的形式),當它被訂閱時才會真正啟動。

      Subject類

      Subject同時具備Observable和observer的功能,可訂閱消息,也可產生數據,一般作為流和觀察者的代理來使用,可以用來實現流的解耦。為了實現更精細的訂閱控制,Subject還提供了以下幾種方法。

      AsyncSubject

      AsyncSubject觀察的序列完成后它才會發出最后一個值,并永遠緩存這個值,之后訂閱這個AsyncSubject的觀察者都會立刻得到這個值。

      BehaviorSubject

      Observer在訂閱BehaviorSubject時,它接收最后發出的值,然后接收后續發出的值,一般要求提供一個初始值,觀察者接收到的消息就是距離訂閱時間最近的那個數據以及流后續產生的數據。

      ReplaySubject

      ReplaySubject會緩存它監聽的流發出的值,然后將其發送給任何較晚的Observer,它可以通過在構造函數中傳入參數來實現緩沖時間長度的設定。

      二. 從理論到實踐

      原文中提供了一個非常詳細的打飛機游戲的代碼,但我仍然建議你在熟悉了其基本原理和思路后自己將它實現出來,然后去和原文中的代碼作對比,好搞清楚哪些東西是真的理解了,哪些只是你以為自己理解了,接著找一些很明顯的優化點,繼續使用響應式編程的思維模式來試著實現它們,起初不知道從何下手是非常正常的(當然也可能是筆者的自我安慰),但這對于培養響應式編程思維習慣大有裨益。筆者在自己的實現中又加入了右鍵切換飛船類型的功能,必須得說開發游戲的確比寫業務邏輯要有意思。

      由于沒有精確計算雪碧圖的坐標,所以在碰撞檢測時會有一些偏差。

      三. 問題及反思

      關于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一次數據以后才會開始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){

      /*此處是默寫時的難點,靜態生成的數組流需要一直保持

      *后續的結果都是在此之上不斷累加的

      */

      【響應式編程的思維藝術】 (4)從打飛機游戲理解并發與流的融合

      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);

      附件: demo.rar 329.94KB 下載次數:3次

      附件: md原文.rar 3.3M 下載次數:3次

      面向對象編程

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel表格退出受保護視圖的方法
      下一篇:深入 Python 解釋器源碼,我終于搞明白了字符串駐留的原理!
      相關文章
      亚洲影院在线观看| 亚洲黄黄黄网站在线观看| 国产亚洲sss在线播放| 亚洲视频在线一区| 亚洲一区二区三区偷拍女厕| 妇女自拍偷自拍亚洲精品| 亚洲大片免费观看| 亚洲天堂久久精品| 久久亚洲国产精品五月天| 国产亚洲综合久久系列| 亚洲一区二区三区无码中文字幕| 久久久久无码专区亚洲av| 亚洲女人被黑人巨大进入| 亚洲精品无码久久久久A片苍井空| 亚洲中文字幕日本无线码| 亚洲最大天堂无码精品区| 亚洲综合国产成人丁香五月激情| 亚洲一线产品二线产品| 亚洲日韩精品无码专区加勒比| 亚洲最大福利视频| 亚洲精品中文字幕无码A片老| 亚洲国产高清国产拍精品| 亚洲av午夜电影在线观看| 国产午夜亚洲精品不卡免下载| 亚洲第一永久AV网站久久精品男人的天堂AV | 亚洲综合久久精品无码色欲| 亚洲一本之道高清乱码| 国产成人亚洲综合网站不卡| 亚洲18在线天美| 亚洲精品无码专区在线播放| 亚洲精品又粗又大又爽A片| 亚洲日本va一区二区三区| 亚洲精品456人成在线| 亚洲色精品VR一区区三区| 亚洲精品成a人在线观看夫| 亚洲丰满熟女一区二区哦| 亚洲色大成网站www久久九| 亚洲va中文字幕| 国产成人亚洲综合a∨| 亚洲色偷偷狠狠综合网| 亚洲裸男gv网站|