Tutorial: Creating a Wave effect, Mask + Graphics Bezier drawing in Cocos Creator

Creating a Wave effect, Mask + Graphics Bezier drawing with Cocos Creator

Introduction and getting help

You are welcome to post to the Cocos Creator category on the Cocos forums to talk about any issues you encounter during the development process. This tutorial does not teach how to code.

Please prepare your development environment before starting this tutorial. If you are having trouble with our tutorial, please take a look at the following documentation:

Preparing the development environment:


Summary

Simulating the movement and behaviors of water is extremely complex. When thinking about water in two-dimensional games, you just want to simulate its wave characteristics and make some effects that mimic the water flowing. Example:

Let’s get started

First, we need just a background image. The Background Color of the Main Camera camera component is set to white (not the node color). The Mask node holds the cc.Mask component and requires no additional setup. The water node holds the sprite component, which is the background image. Under normal circumstances, it should be completely covered. The wave node can hold the wave.js script.

1

Point structure

In reality, water surfaces are made up of water molecules. This means our code can be abstracted out into individual modules:

water:{
    x:0,
    y:0
}

We now have the water surface width (design resolution of width 720), and point objects. How many points do we fit? The diameter of water molecules is about 0.3 nanometers, it is up to you to decide how many water molecules you want to appear on the screen. The greater the number, the more realistic the effect, but performance may decrease. Set it like this:

this.n=20; // # of molecules

Now, we have 20 points on this 720 wide surface. We heed to store these points for future access.

this.nodeArray = []; // storing points

Next, there is energy transfer between points, for example, the right-most point, this.nodeArray [19] moves upward, then the point next to it, this.nodeArray [18] must be affected. An array should be used to represent the energy:

this.nodeEnergy = []; // storing the energy

Third, we need to represent the energy transfer cyclically, the more cycles there is, the farther it is transferred.

// The left and right points affect each other twice, 
// which determines the speed of wave propagation
for (let k = 0; k < 2; k++)
{
    for (let i = 0; i < this.n; i++)
    {
        if (i > 0)
        {
            // 0.02 pagenation loss
            // pass to the left
            this.nodeEnergy[i-1] += 0.98 * 
            (this.nodeArray[i].y - 
            this.nodeArray[i-1].y);
        }
        
        if(i < this.n - 1)
        {
            // pass to right
            this.nodeEnergy[i + 1] += 0.98 *
            (this.nodeArray[i].y -
            this.nodeArray[i + 1].y);
        }
    }
}

Fourth, velocity decay when the energy transfer occurs, energy must also be lost, mainly to surface tension and gravity.

// right-most
for (let i = 0; i < this.n - 1; i++)
{
    // 0.02 speed loss
    this.nodeEnergy[i] *= 0.98;
    
    // change position
    this.nodeArray[i].y += this.nodeEnergy[i] * dt;
}

Fifth, change the position of the mask display.

let draw = this.mask._graphics;

Sixth, the mask component contains a cc.Graphics object named _graphics, and the drawing we use to erase the effect of the mask. We can then simulate the shape of the water surface by erasing it based on the points on the water surface and the bottom closed figure. Because it is 720 x 1280, find the bottom two points (-360, -640) and (360, -640), and connect them to the surface point for graphic closure. Be careful not to use lineTo to curve with Bezier, because of sharp corners.

// Using a mask
showWater()
{
    let draw = this.mask._graphics;
    draw.clear();
    draw.lineWidth = 1;
    
    // do not match line color with fill color
    draw.strokeColor = cc.color(255,0,0);
    draw.fillColor = cc.color(0,255,0);
    
    // Move to the far left of the screen, this.h = 200 is my 
    // custom water surface height.
    draw.moveTo(-360, this.h);
    
    // The Bezier curve is separated by a point as a control point.
    for (let i = 0; i < this.n; i+=2)
    {
        // bessel
        draw.quadraticCurveTo(this.nodeArray[i].x, this.nodeArray[i].y,
            this.nodeArray[i+1].x, this.nodeArray[i+1].y);
    }
    
    // closed area
    draw.lineTo(360,- 640);
    draw.lineTo(-360,-640);
    draw.lineTo(-360, this.h);
    draw.fill();
    draw.stroke();
}

Finally, let the right-most point be sin eased under the start method. Please refer to the cc.Tween documentation if necessary.

// right-most point easing
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'})
)

Finished code

Here is the complete code of wave.js:

cc.Class ({
    extends : cc.Component,

    properties: {
        mask: cc.Mask
    },

    onLoad()
    {
        // water surface height
        this.h = 200;
        this.n = 20; // number of points
        this.nodeArray = []; // loaded points
        this.nodeEnergy = []; // energy for each point
        // assign the values
        for(let i = 0; i < this.n;i++)
        {
            this.nodeEnergy[i] = 0;
        }
    },

    start()
    {
        // create points on the water
        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;
        }
        
        // right-most point easing
        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();     
    },

    // Using a mask
    showWater()
    {
        let draw = this.mask._graphics;
        draw.clear();
        draw.lineWidth = 1;
    
        // do not match line color with fill color
        draw.strokeColor = cc.color(255,0,0);
        draw.fillColor = cc.color(0,255,0);
    
        // Move to the far left of the screen, this.h = 200 is my 
        // custom water surface height.
        draw.moveTo(-360, this.h);
    
        // The Bezier curve is separated by a point as a control point.
        for (let i = 0; i < this.n; i+=2)
        {
            // bessel
            draw.quadraticCurveTo(this.nodeArray[i].x, this.nodeArray[i].y,
                this.nodeArray[i+1].x, this.nodeArray[i+1].y);
        }
    
        // closed area
        draw.lineTo(360,- 640);
        draw.lineTo(-360,-640);
        draw.lineTo(-360, this.h);
        draw.fill();
        draw.stroke();
    }
    
    update(dt)
    {
        // The left and right points affect each other twice, 
        // which determines the speed of wave propagation
        for (let k = 0; k < 2; k++)
        {
            for (let i = 0; i < this.n; i++)
            {
                if (i > 0)
                {
                    // 0.02 pagenation loss
                    // pass to the left
                    this.nodeEnergy[i-1] += 0.98 * 
                    (this.nodeArray[i].y - 
                    this.nodeArray[i-1].y);
                }
        
                if(i < this.n - 1)
                {
                    // pass to right
                    this.nodeEnergy[i + 1] += 0.98 *
                    (this.nodeArray[i].y -
                    this.nodeArray[i + 1].y);
                }
            }
        }
        
        this.showWater();
    },
});

Conclusion

This tutorial has been fun!

One last tip: When you want to change the energy or position of a point on the water use the following code:

// Change the corresponding point data in these two arrays
this.nodeArray
this.nodeEnergy
4 Likes