Bugly 技術乾貨系列內容主要涉及移動開發方向,是由 Bugly 邀請騰訊內部各位技術大咖,經過平常工做經驗的總結以及感悟撰寫而成,內容均屬原創,轉載請標明出處。vue
好吧,說是「粒子引擎」仍是大言不慚而標題黨了,離真正的粒子引擎還有點遠。廢話少說,先看[demo],掃描後點擊屏幕有驚喜哦…git
本文將教會你作一個簡單的canvas粒子製造器(下稱引擎)。github
這個簡單的引擎裏須要有三種元素:世界(World)、發射器(Launcher)、粒子(Grain)。總得來講就是:發射器存在於世界之中,發射器製造粒子,世界和發射器都會影響粒子的狀態,每一個粒子在通過世界和發射器的影響以後,計算出下一刻的位置,把本身畫出來。canvas
所謂「世界」,就是全局影響那些存在於這這個「世界」的粒子的環境。一個粒子若是選擇存在於這個「世界」裏,那麼這個粒子將會受到這個「世界」的影響。markdown
用來發射粒子的單位。他們能控制粒子生成的粒子的各類屬性。做爲粒子們的爹媽,發射器可以控制粒子的出生屬性:出生的位置、出生的大小、壽命、是否受到「World」的影響、是否受到」Launcher」自己的影響等等……app
除此以外,發射器自己還要把本身生出來的已經死去的粒子清掃掉。dom
最小基本單位,就是每個騷動的個體。每個個體都擁有本身的位置、大小、壽命、是否受到同名度的影響等屬性,這樣才能在canvas上每時每刻準確描繪出他們的形態。編輯器
上面就是粒子繪製的主要邏輯。函數
咱們先來看看世界須要什麼。工具
不知道爲何我理所固然得會想到世界應該有重力加速度。可是光有重力加速度不能表現出不少花樣,因而這裏我給他增長了另外兩種影響因素:熱氣和風。重力加速度和熱氣他們的方向是垂直的,風影響方向是水平的,有了這三個東西,咱們就能讓粒子動得很風騷了。
一些狀態(好比粒子的存亡)的維護須要有時間標誌,那麼咱們把時間也加入到世界裏吧,這樣方便後期作時間暫停、逆流的效果。
define(function(require, exports, module) { var Util = require('./Util'); var Launcher = require('./Launcher'); /** * 世界構造函數 * @param config * backgroundImage 背景圖片 * canvas canvas引用 * context canvas的context * * time 世界時間 * * gravity 重力加速度 * * heat 熱力 * heatEnable 熱力開關 * minHeat 隨機最小熱力 * maxHeat 隨機最大熱力 * * wind 風力 * windEnable 風力開關 * minWind 隨機最小風力 * maxWind 隨機最大風力 * * timeProgress 時間進步單位,用於控制時間速度 * launchers 屬於這個世界的發射器隊列 * @constructor */ function World(config){ //太長了,略去細節 } World.prototype.updateStatus = function(){}; World.prototype.timeTick = function(){}; World.prototype.createLauncher = function(config){}; World.prototype.drawBackground = function(){}; module.exports = World; });
你們都知道,畫動畫就是不斷得重畫,因此咱們須要暴露出一個方法,提供給外部循環調用:
/** * 循環觸發函數 * 在知足條件的時候觸發 * 好比RequestAnimationFrame回調,或者setTimeout回調以後循環觸發的 * 用於維持World的生命 */ World.prototype.timeTick = function(){ //更新世界各類狀態 this.updateStatus(); this.context.clearRect(0,0,this.canvas.width,this.canvas.height); this.drawBackground(); //觸發全部發射器的循環調用函數 for(var i = 0;i<this.launchers.length;i++){ this.launchers[i].updateLauncherStatus(); this.launchers[i].createGrain(1); this.launchers[i].paintGrain(); } };
這個 timeTick 方法在外部循環調用時,每次都作着這幾件事:
那麼,世界的狀態到底有哪些要更新?
顯然,每一次都要讓時間往前增長一點是容易想到的。其次,爲了讓粒子儘量動得風騷,咱們讓風和熱力的狀態都保持不穩定——每一陣風和每一陣熱浪,都是你意識不到的~
World.prototype.updateStatus = function(){ this.time+=this.timeProgress; this.wind = Util.randomFloat(this.minWind,this.maxWind); this.heat = Util.randomFloat(this.minHeat,this.maxHeat); };
世界造出來了,咱們還得讓世界能造粒子發射器呀,要否則怎麼造粒子呢~
World.prototype.createLauncher = function(config){ var _launcher = new Launcher(config); this.launchers.push(_launcher); };
好了,作爲上帝,咱們已經把世界打造得差很少了,接下來就是捏造各類各樣的生靈了。
發射器是世界上的第一種生物,依靠發射器才能繁衍出千奇百怪的粒子。那麼發射器須要具有什麼特徵呢?
首先,它是屬於哪一個世界的得搞清楚(由於這個世界可能不止一個世界)。
其次,就是發射器自己的狀態:位置、自身體系內的風力、熱力,能夠說:發射器就是一個世界裏的小世界。
最後就是描述一下他的「基因」了,發射器的基因會影響到他們的後代(粒子)。咱們賦予發射器越多的「基因」,那麼他們的後代就會有更多的生物特徵。具體看下面的良心註釋代碼吧~
define(function (require, exports, module) { var Util = require('./Util'); var Grain = require('./Grain'); /** * 發射器構造函數 * @param config * id 身份標識用於後續可視化編輯器的維護 * world 這個launcher的宿主 * * grainImage 粒子圖片 * grainList 粒子隊列 * grainLife 產生的粒子的生命 * grainLifeRange 粒子生命波動範圍 * maxAliveCount 最大存活粒子數量 * * x 發射器位置x * y 發射器位置y * rangeX 發射器位置x波動範圍 * rangeY 發射器位置y波動範圍 * * sizeX 粒子橫向大小 * sizeY 粒子縱向大小 * sizeRange 粒子大小波動範圍 * * mass 粒子質量(暫時沒什麼用) * massRange 粒子質量波動範圍 * * heat 發射器自身體系的熱氣 * heatEnable 發射器自身體系的熱氣生效開關 * minHeat 隨機熱氣最小值 * maxHeat 隨機熱氣最小值 * * wind 發射器自身體系的風力 * windEnable 發射器自身體系的風力生效開關 * minWind 隨機風力最小值 * maxWind 隨機風力最小值 * * grainInfluencedByWorldWind 粒子受到世界風力影響開關 * grainInfluencedByWorldHeat 粒子受到世界熱氣影響開關 * grainInfluencedByWorldGravity 粒子受到世界重力影響開關 * * grainInfluencedByLauncherWind 粒子受到發射器風力影響開關 * grainInfluencedByLauncherHeat 粒子受到發射器熱氣影響開關 * * @constructor */ function Launcher(config) { //太長了,略去細節 } Launcher.prototype.updateLauncherStatus = function () {}; Launcher.prototype.swipeDeadGrain = function (grain_id) {}; Launcher.prototype.createGrain = function (count) {}; Launcher.prototype.paintGrain = function () {}; module.exports = Launcher; });
發射器要負責生孩子啊,怎麼生呢:
Launcher.prototype.createGrain = function (count) { if (count + this.grainList.length <= this.maxAliveCount) { //新建了count個加上舊的還沒達到最大數額限制 } else if (this.grainList.length >= this.maxAliveCount && count + this.grainList.length > this.maxAliveCount) { //光是舊的粒子數量還沒能達到最大限制 //新建了count個加上舊的超過了最大數額限制 count = this.maxAliveCount - this.grainList.length; } else { count = 0; } for (var i = 0; i < count; i++) { var _rd = Util.randomFloat(0, Math.PI * 2); var _grain = new Grain({/*粒子配置*/}); this.grainList.push(_grain); } };
生完孩子,孩子死掉了還得打掃……(好悲傷,怪內存不夠用咯)
Launcher.prototype.swipeDeadGrain = function (grain_id) { for (var i = 0; i < this.grainList.length; i++) { if (grain_id == this.grainList[i].id) { this.grainList = this.grainList.remove(i);//remove是本身定義的一個Array方法 this.createGrain(1); break; } } };
生完孩子,還得把孩子放出來玩:
Launcher.prototype.paintGrain = function () { for (var i = 0; i < this.grainList.length; i++) { this.grainList[i].paint(); } };
本身的內部小世界也不要忘了維護呀~(跟外面的大世界差很少)
Launcher.prototype.updateLauncherStatus = function () { if (this.grainInfluencedByLauncherWind) { this.wind = Util.randomFloat(this.minWind, this.maxWind); } if(this.grainInfluencedByLauncherHeat){ this.heat = Util.randomFloat(this.minHeat, this.maxHeat); } };
好了,至此,咱們完成了世界上第一種生物的打造,接下來就是他們的後代了(呼呼,上帝好累)
出來吧,小的們,大家纔是世界的主角!
做爲世界的主角,粒子們擁有各類自身的狀態:位置、速度、大小、壽命長度、出生時間固然必不可少
define(function (require, exports, module) { var Util = require('./Util'); /** * 粒子構造函數 * @param config * id 惟一標識 * world 世界宿主 * launcher 發射器宿主 * * x 位置x * y 位置y * vx 水平速度 * vy 垂直速度 * * sizeX 橫向大小 * sizeY 縱向大小 * * mass 質量 * life 生命長度 * birthTime 出生時間 * * color_r * color_g * color_b * alpha 透明度 * initAlpha 初始化時的透明度 * * influencedByWorldWind * influencedByWorldHeat * influencedByWorldGravity * influencedByLauncherWind * influencedByLauncherHeat * * @constructor */ function Grain(config) { //太長了,略去細節 } Grain.prototype.isDead = function () {}; Grain.prototype.calculate = function () {}; Grain.prototype.paint = function () {}; module.exports = Grain; });
粒子們須要知道本身的下一刻是怎樣子的,這樣才能把本身在世界展示出來。對於運動狀態,固然都是初中物理的知識了:-)
Grain.prototype.calculate = function () { //計算位置 if (this.influencedByWorldGravity) { this.vy += this.world.gravity+Util.randomFloat(0,0.3*this.world.gravity); } if (this.influencedByWorldHeat && this.world.heatEnable) { this.vy -= this.world.heat+Util.randomFloat(0,0.3*this.world.heat); } if (this.influencedByLauncherHeat && this.launcher.heatEnable) { this.vy -= this.launcher.heat+Util.randomFloat(0,0.3*this.launcher.heat); } if (this.influencedByWorldWind && this.world.windEnable) { this.vx += this.world.wind+Util.randomFloat(0,0.3*this.world.wind); } if (this.influencedByLauncherWind && this.launcher.windEnable) { this.vx += this.launcher.wind+Util.randomFloat(0,0.3*this.launcher.wind); } this.y += this.vy; this.x += this.vx; this.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life); //TODO 計算顏色 和 其餘 };
粒子們怎麼知道本身死了沒?
Grain.prototype.isDead = function () { return Math.abs(this.world.time - this.birthTime)>this.life; };
粒子們又該以怎樣的姿態把本身展示出來?
Grain.prototype.paint = function () { if (this.isDead()) { this.launcher.swipeDeadGrain(this.id); } else { this.calculate(); this.world.context.save(); this.world.context.globalCompositeOperation = 'lighter'; this.world.context.globalAlpha = this.alpha; this.world.context.drawImage(this.launcher.grainImage, this.x-(this.sizeX)/2, this.y-(this.sizeY)/2, this.sizeX, this.sizeY); this.world.context.restore(); } };
嗟乎。
後續但願可以經過這個雛形,進行擴展,再造一個可視化編輯器供你們使用。
對了,代碼都在這:https://github.com/jation/CanvasGrain
若是你以爲內容意猶未盡,若是你想了解更多相關信息,請掃描如下二維碼,關注咱們的公衆帳號,能夠獲取更多技術類乾貨,還有精彩活動與你分享~
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!