js+canvas仿微信《彈一彈》小遊戲

前言

半年前用js和canvas仿了熱血傳奇網遊(地址),基本功能寫完以後,剩下的都是堆數據、堆時間才能完成的任務了,沒什麼新鮮感,所以進度極慢。此次看到微信《彈一彈》比較火,由於涉及到物理引擎(爲了真實),因而動手試了一下。一共用了10個小時,不只完成了這個demo,<刪除線>而且打上了彈一彈好友排行榜的第一頁</刪除線>。css

demo

資料彙總

準備工做

微信這個小遊戲的遊戲規則很簡單,看圖就能看明白,這裏再也不贅述。涉及到的幾個開發難度:chrome

1.物理引擎

固然不用也能夠,無非就是改改圖片的位置,能夠本身模擬掉落和碰撞效果。不過因爲我追(wu)求(li)體(hen)驗(cha),所以開始尋找第三方的物理引擎。canvas

最後我使用的是chipmunk的js版(這個庫是底層計算庫,所以star很少,可是比較有名氣的hilo和cocos2d的物理引擎用到了這個庫)。主要緣由之一是,這個庫的功能只是進行了物理運算,而且支持重力、彈性、摩擦、浮力等功能。固然體積也比較小。畢竟咱們只是寫一個小demo,引入一個遊戲框架的話極可能徒增成本。瀏覽器

不過我使用的時候遇到了幾個罕見的bug,應該是做者的疏漏,在issue中也有人反饋。看做者更新頻率很低,我拿來用的時候有一些修改。若是其它人使用的時候遇到js報錯,能夠試試這裏bash

2.UI渲染

我選擇的是canvas,由於涉及到頻繁的樣式更新,每幀都去改寫style的話太佔性能。並且用canvas寫的話,之後還能夠迭代一些碰撞產生的畫效。微信

以前封裝了一個easycanvas庫,能夠將樹形數據結構「翻譯」成「canvas畫布中的一個個對象」。此次又順手補充了一個支持chipmunk的插件,這樣整個「彈一彈」的開發就徹底只須要管理數據便可,渲染工做不多。數據結構

開始開發

html及背景

因爲項目較小,我把html、css、js堆在了一個文件(最後寫完以後,發現一共連同註釋才400行)。

首先建立一個空html,爲了看起來高大上,我搜了一張天空主題的背景圖。

<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>
複製代碼

可能用到的變量

接下來,準備一些咱們須要用到的數據。例如遊戲的寬高、小球的大小、當前遊戲狀態(是否能夠射擊)、每次能夠射出的小球數、玩家的分數,blabla。

因爲是直接在html裏寫碼,爲了兼容老瀏覽器,只能var來var去。

// 在html直接寫代碼,不編譯、不構建,否則應該用const的
var width = 400, height = 600, ballSize = 20;

// 遊戲狀態
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');

// 給每一個東西起一個type,後面會用來作碰撞檢測
var BALL_TYPE = 1, BLOCK_TYPE = 2, BORDER_TYPE = 3, BOTTOM_TYPE = 4, BONUS_TYPE = 5;
複製代碼

頂部文本

接下來先將分數和小球個數寫到canvas中。首先建立一個easycanvas實例,寬400,高600。而後add2個對象。一個以左上角(5,5)爲頂點,向右下方寫分數。一個以右上角(395, 5)爲頂點,向左下角寫當前小球個數。

step1

// 初始化easycanvas實例
var $Painter = new Easycanvas.painter();
$Painter.register(el, {
    width: width,
    height: height,
});
$Painter.start();

$Painter.add({
    content: {
        text: function () {
            return '得分:' + score;
        }
    },
    style: {
        tx: 5, ty: 5,
        textAlign: 'left', textVerticalAlign: 'top',
        color: 'black'
    }
});
$Painter.add({
    content: {
        text: function () {
            return '小球個數:' + ballCount;
        }
    },
    style: {
        tx: 395, ty: 5,
        textAlign: 'right', textVerticalAlign: 'top',
        color: 'black'
    }
});
複製代碼

添加方塊

接下來,設置整個場景的重力,而且添加一些方塊進去。每一個方塊對象含有一個child,用來展現數字(還能夠撞幾下)。爲了不方塊重疊,咱們讓方塊的x座標在50、100、150、……、300、350循環。同時,爲了不看起來「太整齊」,每次添加一個小的隨機數,讓這些方塊們錯落有致。(「錯落」指良莠不齊,「致」指情趣。形容事物的佈局雖然良莠不齊,但卻極有情趣,令人看了有好感。——某度)

每一個方塊的大小是30x30,所以shapes包括4條邊,例如(0,0)到(30,0)是一條邊。這些方塊是失重的(不會掉下去),所以static設置爲true。爲了更加錯落有致,咱們給他一個隨機的角度rotate。

每一個方塊含有一個child,寫着一個數字。不須要給數字設置rotate,不然6和9可能就分不清了。

step2

// 初始化easycanvas物理引擎,添加一個有物理樹形的空容器
var $space = new Easycanvas.class.sprite({
    physics: {
        gravity: 2, // 重力默認爲1,可是遊戲進程有點慢,看着不夠爽
        accuracy: 2,
    },
});
$Painter.add($space);

var space = $space.launch();

// 防止方塊重疊,記錄上一次方塊的X座標
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;
    }
}
複製代碼

接下來,咱們作瞄準部分。大體功能是,有一排小圓點,會隨着鼠標運動,而且有彈簧的感受。

step3

首先要記錄鼠標的軌跡,咱們給easycanvas實例$Painter加上事件監聽。在「彈一彈」遊戲中,小球不能向上發射。所以記錄鼠標的Y座標值的時候,咱們讓他至少爲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,
    }
});
複製代碼

小球瞄準

接下來,咱們添加7個小球,讓他們排列在一條線上,從遊戲正上方的(300, 20)點到鼠標位置均勻鋪開。具體邏輯就是,咱們將鼠標位置和(300, 20)的座標差進行6等分,第一個球的座標向鼠標位置偏移0/六、第二個球偏移1/6……,最後一個球偏移6/6(正好落在了鼠標位置)。這幾個球咱們給他們一個透明度,而且不啓用物理規則(由於這個階段小球不能掉下來)。咱們在每一個小球上設置一個shoot鉤子,當玩家射出真實的小球時,刪除這個瞄準用的小球。

// 顯示瞄準軌跡
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 () {
                    return 20 + (mouse.y - 20) * this.data.gap;
                },
                tw: 20, th: 20,
                opacity: 0.4,
            },
            hooks: {
                shoot: function () {
                    this.remove();
                }
            }
        });
    }
};
startAim();
複製代碼

發射小球

接下來,咱們添加真實的小球(受到物理規則影響的小球)。

step4

當射擊時,咱們廣播shoot事件,移除剛纔瞄準用的小球。

以後,咱們間隔100毫秒,連續調用addBall方法來建立小球。addBall方法中,咱們爲每一個小球設置物理規則。包括形狀、彈性、摩擦等。

這裏有一個坑,就是一旦開始射擊,無論鼠標怎麼移動,射擊的方向都不能變化。所以咱們要先記錄下當前的mouse值,這裏用的是JSON.parse(JSON.stringify(mouse))來copy一個簡單對象。

這裏又有一個坑:「彈一彈」遊戲中,剛射擊出去的小球是不受重力影響的(否則瞄準還有什麼意義)。所以,咱們在每一個小球上增長一個和重力相反的做用力,抵消重力。(在其它部分的代碼中,有着「當小球發生一次碰撞後,取消這個做用力」的實現,這裏爲了清晰沒有一塊兒貼出來)。

同時,咱們給小球加上初速度。

這裏又又又又又有一個坑(好煩啊):無論怎麼射擊,小球初始得到的速度是相同的。哪怕小球的瞄準位置距離射出位置很近,速度也不能慢。這裏須要修正一下初始速度,這裏用到了著名的Pythagoras theorem定理:直角三角形的兩條直角邊的平方和等於斜邊的平方。

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: [
                // 形狀是一個以(ballSize / 2, ballSize / 2)爲圓心的,半徑也是ballSize / 2的圓
                // 改爲位運算符吧,看着能高大上一點(其實在這裏卵用沒有)
                [ballSize >> 1, ballSize >> 1, ballSize >> 1]
            ],
            mass: 1, // 質量
            friction: 0.1, // 摩擦(摩擦太大了會損失能量)
            elasticity: 0.8, // 彈性
            collisionType: BALL_TYPE,
        },
        style: {
            tw: ballSize, th: ballSize,
            sx: 0, sy: 0,
            tx: 200,
            ty: 20,
            zIndex: 1,
        },
    });
    $space.add($ball);

    $ball.physicsOn();

    // 抵消重力
    $ball.$physics.body.applyForce({x: 0, y: 1000}, {x: 0, y: 0});

    // 初速度
    var speed = {
        x: (mouse.x - 200) / (20 - mouse.y),
        y: 1
    };

    // 修正速度,確保從各個角度射出小球的速度差很少
    // 這裏用到的著名的高等數學知識:勾股定理
    var muti = Math.sqrt(Math.pow(speed.x, 2) + Math.pow(speed.y, 2)) / 700;

    $ball.$physics.body.setVel({
        x: -speed.x / muti,
        y: -speed.y / muti,
    });
}
複製代碼

其它

輪廓已經有了,後面的部分再也不是難點。不過作到最後,坑仍是比較多的:

例如小球可能會停在方塊上(就是這麼巧),這是須要人爲給予小球一個速度(「彈一彈」遊戲裏也是這樣作的)。

例如小球撞到方塊上,可能會觸發2次碰撞,由於影響不大,我先擱置了。這個是由於時間精度沒有太細,小球在上一幀沒有發生碰撞,由於速度較快,下一幀同時撞到了2個邊界。

相關文章
相關標籤/搜索