【帶著canvas去流浪(8)】碰撞

来源:https://www.cnblogs.com/dashnowords/archive/2019/04/22/10753005.html
-Advertisement-
Play Games

示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 經過前面章節相對枯燥的練習,相信你已經能夠上手 的原生API了,那麼從這一節開始,我們 ...


目錄

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

博客園地址:《大史住在大前端》原創博文目錄

華為雲社區地址:【你要的前端打怪升級指南】

經過前面章節相對枯燥的練習,相信你已經能夠上手canvas的原生API了,那麼從這一節開始,我們就開始接觸點好玩的東西——動畫。

一. canvas的能力

如果你以為canvas只能繪製圖表那真的就圖樣圖森破了,且不談webgl的繪圖上下文,單就2d空間的畫筆就可以做很多有意思的事情,比如實現一些酷炫的動畫效果,比如做一些物理模擬,圖片濾鏡,直播彈幕,甚至做游戲開發等等,畫面的變化大多依賴於canvas提供的像素操作能力,而動效幾乎都是靠canvas在短時間內逐幀繪製而形成的,和電影的原理是一樣的。

我們知道javascript中和時間控制有關的函數setTimeout( ) 以及setInterval( )最終執行時的時間點並不准確,因為在事件隊列中會被其他非同步任務影響甚至直接阻塞,那麼在不斷重覆的繪製中,就有可能會出現卡頓或者忽快忽慢;另一方面,假設我們使用的電腦顯示屏刷新率為60幀/秒,也就是大約16.7ms重繪一次,那麼即時我們在16.7ms時間內執行了很多次計算和繪製命令,實際上最終呈現出的也只是最後一次結果,就好比對一段很密集的數據進行了隔點採樣,輕則浪費性能,重則會在畫面呈現時出現跳幀。為了配合顯示器刷新,我們可以使用另一個方法——requestAnimationFrame(fn),這是javascript中專門用來繪製逐幀動畫的,它會配合顯示器的刷新頻率進行必要的圖像更新,節省不必要的性能浪費。

二. 動畫框架

canvas上實現基本的動畫,可以遵循一個基本的編程框架:

function step(){
    /**
    *在每一幀中要執行的邏輯
    *......
    */
    requestAnimationFrame(step);
}

step();//啟動執行

你沒看錯,這就是canvas動畫最核心的一段代碼,step()函數會在每個繪圖周期內重覆執行。那麼每一幀中需要做哪些工作呢?

我們將canvas想象成一個舞臺stage,每一個需要繪製在畫布上的元素被稱為精靈,無論它們擁有怎樣的屬性,它們都具備update( )paint( )兩個基本方法,前者用於在每一幀中計算更新精靈的參數屬性,後者用於將這個精靈對象繪製在畫布上。那麼step函數在每一幀中所執行的邏輯就變得明朗了,對畫布進行必要的擦除,接著更新每一個精靈的狀態(可能是位置,顏色等等),然後將其繪製在畫布上。

比如現在要在畫布上表現一段太陽東升西落得動畫,對應的偽代碼就是下麵這個樣子的:

let stage = [];
stage.push(background, tree, cloud, sun);

function step(){
    cleanStage();//對畫布進行必要擦除
    background.update();//更新土地的屬性
    tree.update();//更新樹的屬性
    cloud.update();//更新雲的屬性
    sun.update();//更新太陽的屬性(屬性中必然包含著太陽的位置數據)
    background.paint();//繪製土地
    tree.paint();//繪製樹
    cloud.paint();//繪製雲
    sun.paint();//繪製太陽
    requestAnimationFrame(step);
}

如果你理解了上面的過程,那麼接下來我們對上述代碼進行一些抽象和改寫:

//建立舞臺及添加元素的代碼
let stage = [];
stage.push(background, tree, cloud, sun....);

//逐幀動畫代碼
function step(){
    cleanStage();
    stage.map(sprite=>{
        sprite,update();
        sprite.paint(ctx);
    });
    requestAnimationFrame(step);
}

每一個精靈對象都需要實現自己的update( )paint( )方法來描述自己的參數如何變化,以及如何在每一幀中被繪製,被添加進stage數組的都是精靈的實例,一般會將canvas繪圖上下文傳入paint(context)方法,這樣就可以將精靈繪製在指定的畫布上。上面的範式只是一個簡陋的核心模型,但是已經足夠說明canvas動畫的本質。

三. 在canvas中模擬碰撞

現在我們就通過一個碰撞模擬的例子來學習canvas動畫以及基本的物理模擬分析,示例雖然精簡,但包含了canvas動效最核心的精靈動畫和碰撞檢測主題。為了方便二維向量操作並隱藏各種數學計算的細節,我們直接使用一個已經定義好的Vector2類,其中封裝了很多向量的基本操作,都是初高中數學的知識,如果你已經記不太清楚,可以找一些有關的資料複習一下。

3.1定義小球的屬性

將每一個小球視為一個精靈,我們需要為它增加一些基本屬性以便在每一幀中能夠將其繪製出來。通過位置,半徑和顏色信息,就能夠繪製出小球;通過速度信息,就可以計算小球的位置變化,以便在繪製下一幀時使用。

class Ball{
    constructor(x,y,id){
        this.pos = new Vector2(x,y);//初始化小球的位置
        this.id = id;
        this.color = '';//繪製的顏色
        this.r = 20;//小球半徑,為方便演示,此處使用給定值
        this.velocity = null;//小球的速度
    }
}

3.2 生成新的小球

為了增加演示效果,我們使用一個定時函數來隨機生成小球,每次生成時為其賦予一個顏色,並給定一個隨機的初始速度。

//為全局balls數組增加一個新的小球,初始位置為(50,30),
function addBall() {
   let ball = new Ball(50,30,balls.length);
       ball.color = colorPalette[parseInt(steps / 100,10) % 10];
       ball.velocity = new Vector2(5*Math.random(), 5 * Math.random());
       balls.push(ball);
}

為了方便起見,我們使用一個全局自增的數值變數,在step中根據條件來執行addBall()方法:

if (steps % 100 === 0 && steps < 1500) {
  addBall();
}

step每迴圈100次(大約1.5秒)就會多生成一個向隨機方向發射的小球,且小球的數量不能超過15個。

3.3 幀動畫繪製函數step

step函數是動畫的核心,我們需要在其中完成重繪背景,添加小球,更新每個小球,繪製小球這些邏輯(由於背景是靜態的,示例中並沒有將其抽象為精靈動畫)。

function step() {
    steps++;
    //重繪背景
    paintBg();
    //每隔一定時間增加一個小球
    if (steps % 100 === 0 && steps < 1500) {
      addBall();
    }
    //更新每個小球的狀態
    balls = balls.map((ball,index,originArr)=>{
      ball.update(index,originArr);
      ball.paint();//描線但不在畫布上繪製
      return ball;
    });
    //繪製每個小球位置
    requestAnimationFrame(step);
}

3.4 定義小球的update方法

精靈的繪製方法paint一般都只涉及canvas的基本繪圖API,並不複雜,例如本例中,只需要在小球的pos屬性記錄的位置處繪製一個封閉弧線並填充它就可以了。精靈的update( )方法往往才是最難編寫的部分。在這個方法中,需要完成的基本邏輯包括狀態更新和碰撞檢測。

  • 狀態更新

    狀態更新一般包括自身狀態更新和相對狀態更新。自身狀態的更新,比如你希望小球在運動過程中顏色會有變化,就屬於自身狀態的變化,相對狀態變化一般指小球相對公共坐標系或某個參照對象而發生的巨集觀位置變化,比如本例中的小球位置變化。

  • 碰撞檢測

    碰撞檢測一般包括精靈是否與其他精靈發生碰撞,並需要對碰撞後造成的影響進行模擬。

參考代碼:

/*更新狀態
由於檢測碰撞需要知道其他小球的位置,故此處將小球數組的引用傳入
也可以直接以面向對象的方式來定義*/
update(index,balls){

    let nextPos;//模擬下一次落點

    //1.計算下一次落點
    nextPos = this.pos.add(this.velocity.multiply(dt)); 

    //2.判斷新位置是否碰觸邊界,如果是則邊界法向的速度反向,假設碰撞過程是無能量損失
    if (nextPos.x + this.r > rightBorder || nextPos.x < this.r) {
        this.velocity.x = -1 * this.velocity.x;//速度分量反向
        nextPos = this.pos;//取消當前幀的位置更新
    } 
    if (nextPos.y + this.r > bottomBorder || nextPos.y < this.r) {
        this.velocity.y = -1 * this.velocity.y;
        nextPos = this.pos;
    }

    //3.判斷是否與其他小球產生碰撞,為避免重覆,每個小球只和比自己id更大的小球做檢測
    balls.map(ball=>{
       if (ball.id > index && this.checkCollision(ball)) {
           this.handleCollision(ball);
       }
       return ball;
    });

    //4.確認更新位置
    this.pos = nextPos;      
}

3.5 碰撞檢測

規則形狀的碰撞檢測一般有某些特殊方法,例如平面內的小球,其實只需要判斷圓心的距離和兩球半徑和的大小,就可以知道兩球是否碰撞。而當檢測物體的外觀並不規則時,碰撞檢測是成了一個非常複雜的問題,最常用的方法包括外接盒檢測,光線投射法和分離軸定理檢測,感興趣的小伙伴可以自行查資料進行學習。本例中的檢測方法實際上是外接盒檢測法的一種基本情況。

//碰撞檢測
checkCollision(ball){
   return this.pos.subtract(ball.pos).length() < this.r + ball.r;
}

3.6 碰撞模擬

碰撞模擬就是利用物理知識來計算碰撞對於物體造成的影響並修改其對應參數。本例中的碰撞可以抽象為兩個質量相等的運動小球的非對心碰撞,且不計能量損失,一般情況下需要使用能量守恆定理和動量守恆定理聯立方程進行求解。本例的模擬中,我們先將小球的非對心碰撞簡化為對心碰撞,方法是將小球的速度向量分解為沿球心連線方向Vr以及沿圓心連線法向Vn兩個分量,然後使用兩個小球的Vr來進行對心碰撞的模擬(質量相等的剛體對心碰撞後會互換速度),接著再將碰撞後的速度與小球自己的法向速度Vn進行向量合成即可。

本例的代碼中使用了簡化的方案,只計算了沿球心連線方向的分量併進行了碰撞模擬,沒有對碰撞後的速度進行合成,但對碰撞模擬的效果影響不大。參考代碼如下:

//處理碰撞
handleCollision(ball){
    let ballToThis = this.pos.subtract(ball.pos).normalize();
    let thisToBall = ballToThis.negate();
    this.velocity = ballToThis.multiply(Math.abs(ball.velocity.length()*(ball.velocity.dot(ballToThis) / ball.velocity.length())));
    ball.velocity = thisToBall.multiply(Math.abs(this.velocity.length()*(this.velocity.dot(ballToThis) / this.velocity.length())));
}

碰撞後兩個小球的速度都發生了變化,在下一幀更新位置時就會表現出來,效果已經在本節開頭展示出了。

完整的示例代碼可以參見附件的demo,或訪問開頭處我的github倉庫地址。

四. 下一步

有了這樣一個撞球的基本模型和示例,你能做出一個乒乓球小游戲或是撞球小游戲嗎?


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 版權聲明:本文為HaiyuKing原創文章,轉載請註明出處! 前言 在較新版本的Android Studio中新建項目預設使用 ConstraintLayout進行佈局的。 ConstraintLayout是一個允許您以靈活的方式定位和調整小部件的ViewGroup。 註意: ConstraintL ...
  • 我的csv文件: 使用d3.csv()輸出: 可以看到並不是csv數組。 解決方法1: 查看官方API文檔(https://github.com/d3/d3-fetch/blob/master/README.md#dsv): 修改代碼如下: 輸出: 解決方法2: 使用d3.dsv(),代碼並格式化數 ...
  • 微信小程式計算器BUG版本 無APPID的測試號登錄,先在app.json中更改路徑,以及修改頭部信息。 首先一個輸入框欄位用{{screenData}} 功能可以退格,清屏,正負號,正常操作加減乘除以及計算曆史。這是計算器1.0,以後會優化添加更多功能。 每個按鈕添加點擊事件bintap,給每一個 ...
  • babel 7 對於 babel 7, "babel 的官網" 已經介紹得非常詳細了,但有時感覺文檔和實際使用總是差那麼一點東西。 主要包 先來看一下主要的包,babel 7 對於包進行了一些簡化。 "@babel/cli" : 用於執行相應命令 "@babel/core" : 核心包,將 js 代 ...
  • 1、jQuery.ajax(url[, settings]) 通過HTTP請求載入遠程數據。 註意:所有的settings選擇都可以通過$.ajaxSetup()函數來全局指定。 回調函數 在實際開發中,當我們需要處理$.ajax() 得到的數據,就需要使用到回調函數。 (1) beforeSend ...
  • 這兩天弄一個mui的底部菜單,有點費時了,嘗試了用vue寫,純js寫,還有根據mui的寫,還是有些問題和麻煩。直到看了網上的一些例子,才想明白,之前一直是一種點擊觸發事件才高亮的思維去做,這個雖然可以了,但是頁面跳轉了就又都沒了。網上看明白的例子是:讓當前頁面地址與導航里的地址做對比,相同就高亮,之 ...
  • 上一篇,介紹了 range 對象的一些屬性和方法,瞭解了一些基本操作,現在來介紹另外一個重要的對象:selection 對象; MDN 的解釋是:Selection 對象表示用戶選擇的文本範圍或插入符號的當前位置。它代表頁面中的文本選區,可能橫跨多個元素。文本選區由用戶拖拽滑鼠經過文字而產生; 先來 ...
  • 概要 狹義的 DOM API 僅僅包含 DOM 樹形結構相關的內容。 DOM 中的所有的屬性都是用來表現語義的屬性,CSSOM 的則都是表現的屬性。 CSSOM 是 CSS 的對象模型,在 W3C 標準中,它包含兩個部分: 描述樣式表和規則等 CSS 的模型部分(CSSOM) 跟元素視圖相關的 Vi ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...