媒介
半年前用js和canvas仿了熱血傳奇網游(地點),基本功用寫完今後,剩下的都是堆數據、堆時刻才完成的使命了,沒什麼新鮮感,因而進度極慢。此次看到微信《彈一彈》比較火,因為涉及到物理引擎(為了實在),因而着手試了一下。一共用了10個小時,不僅完成了這個demo,<刪除線>而且打上了彈一彈摯友排行榜的第一頁</刪除線>。
材料匯總
- 在線demo:點擊即玩
- 代碼:400行帶解釋
- canvas襯着庫:支撐物理引擎及chrome調試東西,這裏
預備事情
微信這個小遊戲的遊戲劃定規矩很簡樸,看圖就能看邃曉,這裏不再贅述。涉及到的幾個開闢難度:
1.物理引擎
固然不必也能夠,不過就是改改圖片的位置,能夠本身模仿掉落和碰撞結果。不過因為我追(wu)求(li)體(hen)驗(cha),因而最先尋覓第三方的物理引擎。
末了我運用的是chipmunk的js版(這個庫是底層盤算庫,因而star不多,然則比較有名氣的hilo和cocos2d的物理引擎用到了這個庫)。主要原因之一是,這個庫的功用只是舉行了物理運算,而且支撐重力、彈性、磨擦、浮力等功用。固然體積也比較小。畢竟我們只是寫一個小demo,引入一個遊戲框架的話極能夠徒增本錢。
不過我運用的時刻碰到了幾個稀有的bug,應該是作者的疏漏,在issue中也有人反應。看作者更新頻次很低,我拿來用的時刻有一些修改。假如別的人運用的時刻碰到js報錯,能夠嘗嘗這裏
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)為極點,向左下角寫當前小球個數。
// 初始化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輪迴。同時,為了防備看起來“太整潔”,每次增添一個小的隨機數,讓這些方塊們犬牙交錯。(“參差”指良莠不齊,“致”指情味。描述事物的規劃雖然良莠不齊,但卻極有情味,使人看了有好感。——某度)
每一個方塊的大小是30×30,因而shapes包含4條邊,比方(0,0)到(30,0)是一條邊。這些方塊是失重的(不會掉下去),因而static設置為true。為了越發犬牙交錯,我們給他一個隨機的角度rotate。
每一個方塊含有一個child,寫着一個数字。不須要給数字設置rotate,不然6和9能夠就分不清了。
// 初始化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;
}
}
接下來,我們做對準部份。大抵功用是,有一排小圓點,會跟着鼠標活動,而且有彈簧的覺得。
起首要紀錄鼠標的軌跡,我們給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/6、第二個球偏移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();
發射小球
接下來,我們增添實在的小球(遭到物理劃定規矩影響的小球)。
當射擊時,我們播送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個邊境。