Mask+Graphics貝塞爾曲線繪製波動的水面

摘要

我們都知道水面的運動形式是極其複雜的。但是在一些二維的小遊戲中,我們只是想模擬其波動特性,做一些出水入水的效果。如何簡單的實現波動的水面呢?

正文

看看效果

準備工作

一張背景圖片即可

層級結構

Main Camera 攝像機組件的 Background Color 設置白色(不是節點顏色)。
Mask 節點掛載 cc.Mask 組件,無需其他處理。
water 節點掛載精靈組件,就是背景圖。正常情況下,應該是全被遮住。
wave 節點掛載 wave.js 腳本即可。

點的結構

現實中,水面是由一個個水分子組成。
那麼我們代碼裏就可以抽象成一個個對象

水:{
    x: 0,
    y: 0
}

好了,我們現在有了水面寬度(設計分辨率寬度 720),有了點的對象。
那麼我們放多少個點合適呢?要知道水分子的直徑約爲 0.3 納米,我們用這麼多對象電腦不炸了。
那這個數量就由你決定了,數量越多效果越真實,但性能就會下降。
這裏我設置了

this.n = 20;                 // 細分數

能量傳遞

現在,我們規定了在這 720 寬的水面上有 20 個點。

this.nodeArray = [];         // 裝載水面上的點

那麼點與點之間是有能量傳遞的呀,比如最右側的點(this.nodeArray[19])向上運動了,那麼其旁邊的點(this.nodeArray[18])必然受到影響呀。
所以我們應該用一個數組來表徵能量

this.nodeEnergy = [];        // 每個點的能量

用循環的方式代表能量傳遞,循環次數越多就相當於傳遞的越遠。

// 左右點互相影響 2 次, 決定波的傳播快慢
for (let k = 0; k < 2; k++) {
    for (let i = 0; i < this.n; i++) {
        if (i > 0) {
            // 0.02 的傳播損失
            // 向左傳
            this.nodeEnergy[i-1] += 0.98 * (this.nodeArray[i].y - this.nodeArray[i-1].y);
        }
        if (i < this.n - 1) {
            // 向右傳
            this.nodeEnergy[i+1] += 0.98 * (this.nodeArray[i].y - this.nodeArray[i+1].y);
        }
    }
}  

速度衰減

能量傳遞寫好了,那麼我們自身也要有能量損失的。(主要是表面張力與重力)

// 最右側的跳過
for (let i = 0; i < this.n - 1; i++) {
    // 0.02 速度損失
    this.nodeEnergy[i] *= 0.98;
    // 改變位置
    this.nodeArray[i].y += this.nodeEnergy[i] * dt;
}

遮罩顯示

不知小夥伴們有沒有這樣用過 cc.Mask 組件。

let draw = this.mask._graphics;

遮罩組件是含有 _graphics 這個 cc.Graphics 對象的,我們用其畫圖就是擦除遮罩的效果。
那麼我們只要根據水面上的點與水底封閉圖形進行擦除就能模擬出水面形狀了。
因爲是 720 x 1280 找到最下的兩個點(-360,-640)和(360, -640),與水面點連接進行圖形封閉。
注意不要用 lineTo 要用貝塞爾進行曲線。(因爲會出現尖尖的角)

// 利用遮罩原理,把下方顯示
showWater () {
    let draw = this.mask._graphics;
    draw.clear();
    draw.lineWidth = 1;
    // 畫線顏色與填充顏色不要一致
    draw.strokeColor = cc.color(255,0,0);
    draw.fillColor = cc.color(0,255,0);
    // 移動到屏幕最左邊,this.h = 200 是我自定義的水面高度。
    draw.moveTo(-360, this.h);
    // 貝塞爾曲線是隔一個點,作爲控制點。
    for (let i = 0; i < this.n; i+=2) {
        // 貝塞爾
        draw.quadraticCurveTo(this.nodeArray[i].x, this.nodeArray[i].y, this.nodeArray[i+1].x, this.nodeArray[i+1].y);
    }
    // 封閉區域
    draw.lineTo(360, -640);
    draw.lineTo(-360, -640);
    draw.lineTo(-360, this.h);
    draw.fill();
    draw.stroke();
}

賦予動力

在 start 方法下讓最右側的點呈 sin 緩動。(沒看懂的一定要去官方文檔裏看 cc.tween)

// 最右側點緩動
let obj = this.nodeArray[this.n-1];
let time = 0.5;
cc.tween(obj)
    .repeatForever(
        cc.tween()
        .to(time, { y: 40 + this.h}, { easing: 'sineOut'})
        .to(time, { y: 0 + this.h}, { easing: 'sineIn'})
        .to(time, { y: -40 + this.h}, { easing: 'sineOut'})
        .to(time, { y: 0 + this.h}, { easing: 'sineIn'})
    )

完整代碼

wave.js

cc.Class({
    extends: cc.Component,

    properties: {
        mask: cc.Mask
    },

    onLoad () {
        // 水面高度
        this.h = 200;
        this.n = 20;                 // 細分數
        this.nodeArray = [];         // 裝載水面上的點
        this.nodeEnergy = [];        // 每個點的能量
        // 賦予初始值
        for (let i = 0; i < this.n; i++) {
            this.nodeEnergy[i] = 0;
        }
    },

    start () {
        // 創建水面上點
        for (let i = 0; i < this.n; i++) {
            let node = {x: 0, y: 0};
            node.y = this.h;
            node.x = -360 + (i + 1) * 720 / this.n;
            this.nodeArray[i] = node;
        }
        // 最右側點緩動
        let obj = this.nodeArray[this.n-1];
        let time = 0.5;
        cc.tween(obj)
            .repeatForever(
                cc.tween()
                .to(time, { y: 40 + this.h}, { easing: 'sineOut'})
                .to(time, { y: 0 + this.h}, { easing: 'sineIn'})
                .to(time, { y: -40 + this.h}, { easing: 'sineOut'})
                .to(time, { y: 0 + this.h}, { easing: 'sineIn'})
            )
        .start();
    },

    // 利用遮罩原理,把下方顯示
    showWater () {
        let draw = this.mask._graphics;
        draw.clear();
        draw.lineWidth = 1;
        draw.strokeColor = cc.color(255,0,0);
        draw.fillColor = cc.color(0,255,0);
        draw.moveTo(-360, this.h);
        for (let i = 0; i < this.n; i+=2) {
            // 貝塞爾
            draw.quadraticCurveTo(this.nodeArray[i].x, this.nodeArray[i].y, this.nodeArray[i+1].x, this.nodeArray[i+1].y);
        }
        // 封閉區域
        draw.lineTo(360, -640);
        draw.lineTo(-360, -640);
        draw.lineTo(-360, this.h);
        draw.fill();
        draw.stroke();
    },

    update (dt) {
        // 左右點互相影響 2 次, 決定波的傳播快慢
        for (let k = 0; k < 2; k++) {
            for (let i = 0; i < this.n; i++) {
                if (i > 0) {
                    // 0.02 的傳播損失
                    // 向左傳
                    this.nodeEnergy[i-1] += 0.98 * (this.nodeArray[i].y - this.nodeArray[i-1].y);
                }
                if (i < this.n - 1) {
                    // 向右傳
                    this.nodeEnergy[i+1] += 0.98 * (this.nodeArray[i].y - this.nodeArray[i+1].y);
                }
            }
        }  
        // 最右側的跳過
        for (let i = 0; i < this.n - 1; i++) {
            // 0.02 速度損失
            this.nodeEnergy[i] *= 0.98;
            // 改變位置
            this.nodeArray[i].y += this.nodeEnergy[i] * dt;
        }
        this.showWater();
    },
});

結語

想改變水面上某一點的能量或者位置時

// 改變這兩個數組中對應點數據即可
this.nodeArray
this.nodeEnergy

工程源碼在我的微信公衆號回覆關鍵詞【水波】即可獲得

O(∩_∩)O~~

微信公衆號


this.showWater();
},
});


### 結語
想改變水面上某一點的能量或者位置時

// 改變這兩個數組中對應點數據即可
this.nodeArray
this.nodeEnergy

**工程源碼在我的微信公衆號回覆關鍵詞【水波】即可獲得**

O(∩_∩)O~~

### 微信公衆號
[外鏈圖片轉存中...(img-e4HZKtYK-1569064554942)]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章