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)]
发布了120 篇原创文章 · 获赞 133 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/kuokuo666/article/details/101114908