preface

Half a year ago, I used JS and Canvas to copy the online game (address) of Mir II. After the basic functions were written, the remaining tasks were only completed with piles of data and piles of time. There was nothing new, so the progress was very slow. This time I saw wechat “Play a bomb” is popular, because it involves the physics engine (for real), so I tried it. In 10 hours, not only did I finish the demo, < delete line > and hit the first page
of my friends’ leaderboard.

Data summary

  • Online Demo: Click to play

  • Code: 400 lines with comments

  • Canvas Rendering library: Supports physics engine and Chrome debugging tools, here

The preparatory work

Wechat this small game of the rules of the game is very simple, you can see the picture, here no longer repeat. Several development difficulties involved:

1. Physical engine

Of course, you can change the position of the image to simulate the effects of falling and collision. But because I was looking for a third party physics engine, I started looking for a third party physics engine.

I ended up using the JS version of Chipmunk (which is an underlying computing library, so it doesn’t have a lot of stars, but is used by hilo and Cocos2D physics engines). One of the main reasons is that the library functions only for physical computation, and supports gravity, elasticity, friction, buoyancy, and so on. Of course, it’s also smaller. After all, we’re just writing a small demo, and introducing a game framework can be costly.

However, I encountered several rare bugs when USING it, which should be the omissions of the author. Some people also gave feedback in the issue. Look at the author update frequency is very low, I use when there are some modifications. If other people encountered js error when using, can try here

2. The UI rendering

I chose Canvas, because it involves frequent style updates, and rewriting style every frame is too inefficient. In addition, if you use Canvas to write, you can iterate some drawing effects generated by collisions.

Previously, an EasyCanvas library was packaged, which can “translate” tree data structures into “objects in canvas”. This time a chipmunk plugin was added, so the whole “flick” development is all about managing data with very little rendering.

To start developing

HTML and background

Due to the small size of the project, I stacked my HTML, CSS, and JS in a single file (when I finally wrote it, it was only 400 lines with comments).

First, I created an empty HTML. To look good, I searched for a background image with a sky theme.

<style>
    body {
        margin: 0;
        text-align: center;
        background: black;
    }
    canvas {
        border: 1px solid grey;
        height: 100%;
        max-width: 100%;
        background-image: url(http://a3.topitme.com/2/d4/ff/1144306867e94ffd42o.jpg);
        background-size: auto 100%;
    }
</style>
<body>
    <canvas id="el"></canvas>
</body>
Copy the code

Variables that might be used

Next, prepare some data that we need to use. For example, the width and height of the game, the size of the ball, the current state of the game (whether it can be shot or not), the number of balls that can be shot each time, the player’s score, blabla.

Since the code is written directly in HTML, in order to be compatible with older browsers, only var to var.

// Const width = 400, height = 600, ballSize = 20; // Game state var canShoot =true; var score = 0, ballLeft = 0, ballCount = 5; var blockArray = []; Var BALL = Easycanvas. ImgLoader ('./ball.png');
var BLOCK = Easycanvas.imgLoader('./block.jpg');
var TRIANGLE = Easycanvas.imgLoader('./triangle.png'); // Create one for each itemtypeVar BALL_TYPE = 1, BLOCK_TYPE = 2, BORDER_TYPE = 3, BOTTOM_TYPE = 4, BONUS_TYPE = 5;Copy the code

At the top of the text

Next, write the score and number of balls to the canvas. Start by creating an easyCanvas instance that is 400 wide and 600 high. And then add2 objects. One takes the top left corner (5,5) and writes the fraction to the bottom right. One vertices the top right corner (395, 5) and writes the current number of balls to the bottom left corner.

// Initialize easyCanvas instance var$Painter = new Easycanvas.painter();
$Painter.register(el, {
    width: width,
    height: height,
});
$Painter.start();

$Painter.add({
    content: {
        text: function () {
            return 'score: + score;
        }
    },
    style: {
        tx: 5, ty: 5,
        textAlign: 'left', textVerticalAlign: 'top',
        color: 'black'}});$Painter.add({
    content: {
        text: function () {
            return 'Number of balls :' + ballCount;
        }
    },
    style: {
        tx: 395, ty: 5,
        textAlign: 'right', textVerticalAlign: 'top',
        color: 'black'}});Copy the code

Add the square

Next, set the gravity of the entire scene and add some squares to it. Each square object contains a child that displays the number (and can be bumped a few times). To avoid overlapping squares, let’s make the x coordinates of the squares 50, 100, 150… , 300, 350 cycle. Also, to avoid looking “too neat,” add a small random number at a time so that the squares fit together. (” scattered “means uneven, and” zhi “means interesting. Although the layout of things is uneven, it is very interesting and makes people feel good. – a degree)

Each square is 30×30 in size, so the shapes include four edges, for example (0,0) to (30,0) being one edge. These squares are weightless (they will not fall), so static is set to true. And just to make it even more random, we gave it a random Angle rotate.

Each square contains a child that contains a number. You don’t need to set rotate to the number, otherwise 6 and 9 might be indistinguishable.

// Initialize the EasyCanvas physics engine and add an empty container var with a physical tree$space= new easyCanis.class. Sprite ({physics: {gravity: 2, // gravity: 1,},});$Painter.add($space);

var space = $space.launch(); Var lastBlockPositionX = 50;function addBlock (max, boolAddToBottom) {
    var deg = Math.floor(Math.random() * 360);
    var sprite = $space.add(new Easycanvas.class.sprite({
        name: 'block', content: { img: BLOCK, }, physics: { shape: [[[0, 0], [0, 30]], [[0, 30], [30, 30]], [[30, 30], [30, 0]], [[30, 0], [0, 0]]], mass: 1, friction: 0.1, elasticity: 0.9, collisionType: BLOCK_TYPE, static:true,
        },
        style: {
            tw: 30, th: 30,
            tx: lastBlockPositionX + Math.floor(Math.random() * 20 - 10),
            ty: boolAddToBottom ? 500 : height - 100 - Math.floor(Math.random() * 100),
            locate: 'lt',
            rotate: deg,
        },
        children: [{
            content: {
                text: Math.floor(Math.random() * max) + 1,
            },
            style: {
                color: 'yellow',
                textAlign: 'center',
                textVerticalAlign: 'middle',
                textFont: '28px Arial',
                tx: 15, ty: 10
            }
        }]
    }));
    sprite.physicsOn();
    blockArray.push(sprite);

    lastBlockPositionX += 50;
    if(lastBlockPositionX > 350) { lastBlockPositionX = 50; }}Copy the code

Next, we do the aiming part. Basically, it has a row of dots that move with the mouse and feel like a spring.

To record the mouse’s trajectory, we add an event listener to easyCanvas instance $Painter. The ball cannot be fired upwards in “flick”. So when we record the Y position of the mouse, we want it to be at least 30.

Var mouse = {x: 300, y: 50}; var mouseRecord =function ($e) {
    mouse.x = $e.canvasX;
    mouse.y = Math.max(30, $e.canvasY);
};

$Painter.register(el, {
    width: width,
    height: height,
    events: {
        mousemove: mouseRecord,
        touchmove: mouseRecord,
        mouseup: shoot,
        touchend: shoot,
    }
});
Copy the code

Balls aimed at

Next, we added seven balls and arranged them in a line from the (300, 20) point directly above the game to the mouse position. The specific logic is that we divide the coordinate difference between the mouse position and (300, 20) into 6 equal parts. The coordinate of the first ball is shifted to the mouse position by 0/6, and the coordinate of the second ball is shifted by 1/6… The last ball is offset by 6/6. For these balls we give them transparency and do not enable physics rules (because balls cannot fall at this stage). We put a shoot hook on each ball, and when the player shoots a real ball, the aim ball is removed.

Var startAim =function () {
    for (var i = 0; i < 7; i ++) {
        $Painter.add({
            content: {
                img: BALL,
            },
            data: {
                gap: i / 6,
            },
            style: {
                tx: function () {
                    return 200 + (mouse.x - 200) * this.data.gap;
                },
                ty: function () {
                    return20 + (mouse.y - 20) * this.data.gap; Opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0function() { this.remove(); }}}); }}; startAim();Copy the code

Launch a ball

Next, we add real balls (balls affected by the rules of physics).

While shooting, we broadcast the shoot event to remove the ball we were aiming at.

We then create the ball by calling the addBall method every 100 milliseconds. In the addBall method, we set the physics rules for each ball. Including shape, elasticity, friction and so on.

There is a pit, is once the shooting, no matter how the mouse moves, the direction of the shooting can not change. Parse (json.stringify (mouse)) is used to copy a simple object.

Here’s another pothole: the ball you just shot is not affected by gravity (otherwise there’s no point in aiming). So, we add an opposite force to each ball, counteracting gravity. (In other parts of the code, there is an implementation of “when the ball hits once, cancel the force”, which is not posted here for clarity).

At the same time, we add the initial velocity to the ball.

Here’s another pitfall: no matter how you shoot it, the ball initially gains the same speed. Even if the ball’s aim position is very close to the shot position, the speed should not be slow. I need to correct the initial velocity here, using the famous Pythagoras Theorem: the sum of the squares of the two sides of a right triangle is equal to the hypotenuse squared.

function shoot () {
    if(! canShoot)return;

    $Painter.broadcast('shoot');
    canShoot = false;

    var currentMouse = JSON.parse(JSON.stringify(mouse));
    for (var i = 0; i < ballCount; i++) {
        setTimeout(function() { addBall(currentMouse); }, i * 100); }};function addBall (mouse) {
    ballLeft++;
    var $ball = new Easycanvas.class.sprite({
        name: 'ball', content: { img: BALL, }, physics: { shape: [// shape is a circle with (ballSize / 2, ballSize / 2) center and radius ballSize / 2. [ballSize >> 1, ballSize >> 1, ballSize >> 1]], mass: 1, // mass: 707mm }, style: {tw: ballSize, th: ballSize, sx: 0} 0, sy: 0, tx: 200, ty: 20, zIndex: 1, }, });$space.add($ball);

    $ball.physicsOn(); // Counteracts gravity$ball.$physics.body.applyForce({x: 0, y: 1000}, {x: 0, y: 0}); Var speed = {x: (mouse.x-200)/(20-mouse.y), y: 1}; // Adjust the speed to make sure the ball shoots at the same speed from all angles // Famous advanced mathematics used here: Var muti = math.sqrt (math.pow (spee.x, 2) + math.pow (spee.y, 2)) / 700;$ball.$physics.body.setVel({
        x: -speed.x / muti,
        y: -speed.y / muti,
    });
}
Copy the code

other

The outline is there. The next part is no longer the hard part. But in the end, there are more pits:

For example, a ball might land on a square (as it happens), which requires the ball to be artificially given a speed (this is also done in “Bounce”).

For example, when a ball hits a cube, it might trigger two collisions, but I’ll leave that aside because it doesn’t matter much. This is because the time accuracy is not too fine, the ball did not collide in the last frame, because the speed is faster, and the next frame hit two boundaries at the same time.