要想給遊戲注入活力和互動性,粒子引擎是很一個很重要的元素。只需要採用比較簡單的系統,你就能建立出幾乎是無窮的效果,只需要改動幾個引數就能產生比如火焰、水滴、煙霧、火花、血濺、爆炸以及天氣等效果。本教程將帶領大家用Javascript建立一個簡單而又豐富的2D粒子系統。到本文的第一部分結束時,你就會擁有一個最基本但可以使用的粒子系統,它能建立廣泛的基本效果。
單個粒子
首先定義粒子物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var Particle = function(x, y, angle, speed, life, size) { /* the particle's position */ this.pos = { x: x || 0, y: y || 0 }; /* set specified or default values */ this.speed = speed || 5; this.life = life || 1; this.size = size || 2; this.lived = 0; /* the particle's velocity */ var radians = angle * Math.PI / 180; this.vel = { x: Math.cos(radians) * speed, y: -Math.sin(radians) * speed }; }; |
這一步很簡單,而且直到本教程結束也不會有大的改動。構造方法的引數分別是座標,以度數表示的角度,以畫素/秒為單位的速度,以秒為單位的生命長度以及以畫素為單位的大小。接下來建立一個座標向量,然後給其他屬性賦值並把速度和角度轉換成速度向量。這裡採用的角度和速度值是便於理解,但是轉換成一個簡單的向量表示更便於處理X和Y值。由於Y軸是從頂部遞增,這裡Y值要求是負數。
建立噴射器
粒子物件本身其實沒什麼用,所以我們需要一個東西來控制它, 那就是噴射器。噴射器根據設定的引數噴射粒子,當生命週期結束後消亡。首先定義一些噴射器需要的設定,先來一些簡單的吧。這裡我們把這些設定命名為“基本”並放入到settings物件中保持整潔。這樣我們就能通過”settings.fire”或者”settings.smoke”等方式來應用不同的設定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var settings = { 'basic': { 'emission_rate': 50, 'min_life': 3, 'life_range': 2, 'min_angle': 0, 'angle_range': 360, 'min_speed': 25, 'speed_range': 15, 'min_size': 3, 'size_range': 2, 'colour': '#82c4f5' } }; |
噴射速率表示每秒鐘噴射的粒子數量。其餘的設定可以顧名思義。粒子物件的構造方法中每一個引數都對應一個設定項,除了粒子的座標引數,它預設值為0,0(噴射器的座標)。以_range結尾的設定指定特定引數的範圍,比如速度的取值範圍是55到60.
現在我們需要對這些設定進行應用,下面開始定義噴射器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var Emitter = function(x, y, settings) { /* the emitter's position */ this.pos = { x: x, y: y }; /* set specified values */ this.settings = settings; /* How often the emitter needs to create a particle in milliseconds */ this.emission_delay = 1000 / settings.emission_rate; /* we'll get to these later */ this.last_update = 0; this.last_emission = 0; /* the emitter's particle objects */ this.particles = []; }; |
很簡單吧。假如我們想在100,100的位置建立噴射器就可以採用下面的程式碼:
1 |
var emitter = new Emitter(100, 100, settings.basic); |
開幹吧!
到目前為止噴射器還幹不了啥事,我們需要新增一個Update方法來管理粒子。更新噴射器的關鍵在於計算與上一次更新的時間差,從這個值上我們可以得出需要噴射多少數量的粒子,哪些粒子需要消亡,粒子的位置以及噴射器是否需要消亡等。完成這一功能我們只需要使用 Date.now(), 它會返回自從1970年1月1日 00:00:00 AM UTC到現在的毫秒數。我們可以減去上一次更新時的這個值就能得出距離上一次更新的毫秒數。
下面是update方法的基本結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
Emitter.prototype.update = function() { /* set the last_update variable to now if it's the first update */ if (!this.last_update) { this.last_update = Date.now(); return; } /* get the current time */ var time = Date.now(); /* work out the milliseconds since the last update */ var dt = time - this.last_update; /* add them to the milliseconds since the last particle emission */ this.last_emission += dt; /* set last_update to now */ this.last_update = time; /* check if we need to emit a new particle */ if (this.last_emission > this.emission_delay) { /* find out how many particles we need to emit */ var i = Math.floor(this.last_emission / this.emission_delay); /* subtract the appropriate amount of milliseconds from last_emission */ this.last_emission -= i * this.emission_delay; while (i--) { /* calculate the particle's properties based on the emitter's settings */ } } /* loop through the existing particles */ var i = this.particles.length; while (i--) { /* check the particle's life status */ /* calculate the particle's new position based on the seconds passed */ /* draw the particle */ } }; |
為了得到粒子物件某些按範圍取值的屬性,我們可以採用Math.random(), 它返回一個0到1的數,如果我們乘以範圍值再加上最小值就能得到在範圍內的某個值。所以我們可以採用下面的方法來建立粒子。
1 2 3 4 5 6 7 8 9 10 |
this.particles.push( new Particle( 0, 0, this.settings.min_angle + Math.random() * this.settings.angle_range, this.settings.min_speed + Math.random() * this.settings.speed_range, this.settings.min_life + Math.random() * this.settings.life_range, this.settings.min_size + Math.random() * this.settings.size_range ) ); |
目前我們把粒子的座標都設定為0,0,這是相對於噴射器的座標。所有粒子的生命都將從這裡開始。
計算粒子的新座標很簡單,只需要用速度向量乘以距上次更新的時間,然後加到當前座標就行了。要注意的是我們的速度單位是畫素/秒,但是這裡時間單位是毫秒,所以需要除以1000才能得到正確值。
1 2 3 4 |
dt /= 1000; particle.pos.x += particle.vel.x * dt; particle.pos.y += particle.vel.y * dt; |
接下來就該描繪粒子了。這裡我們只使用一種顏色,就是我們在設定裡指定的顏色。是有點枯燥,但好戲留到第二部分吧,到時候我們會加入閃亮的變色系統,可以讓粒子融入到另一種顏色或者漸隱,這樣會顯得更自然和真實。由於粒子的座標是相對於噴射器的座標的,所以我們需要把兩者加在一起才能正確的得出粒子應該被顯示的座標。
1 2 3 4 5 6 7 8 |
ctx.fillStyle = this.settings.colour; var x = this.pos.x + particle.pos.x; var y = this.pos.y + particle.pos.y; ctx.beginPath(); ctx.arc(x, y, particle.size, 0, Math.PI * 2); ctx.fill(); |
現在我們畫的是粒子,但你可以畫任何你想要的形狀。你可以用ctx.fillRect來畫矩形或用ctx.drawImage來使用圖片。
下面是完整的update方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
Emitter.prototype.update = function() { /* set the last_update variable to now if it's the first update */ if (!this.last_update) { this.last_update = Date.now(); return; } /* get the current time */ var time = Date.now(); /* work out the milliseconds since the last update */ var dt = time - this.last_update; /* add them to the milliseconds since the last particle emission */ this.last_emission += dt; /* set last_update to now */ this.last_update = time; /* check if we need to emit a new particle */ if (this.last_emission > this.emission_delay) { /* find out how many particles we need to emit */ var i = Math.floor(this.last_emission / this.emission_delay); /* subtract the appropriate amount of milliseconds from last_emission */ this.last_emission -= i * this.emission_delay; while (i--) { /* calculate the particle's properties based on the emitter's settings */ this.particles.push( new Particle( 0, 0, this.settings.min_angle + Math.random() * this.settings.angle_range, this.settings.min_speed + Math.random() * this.settings.speed_range, this.settings.min_life + Math.random() * this.settings.life_range, this.settings.min_size + Math.random() * this.settings.size_range ) ); } } /* convert dt to seconds */ dt /= 1000; /* loop through the existing particles */ var i = this.particles.length; while (i--) { var particle = this.particles[i]; /* skip if the particle is dead */ if (particle.dead) { /* remove the particle from the array */ this.particles.splice(i, 1); continue; } /* add the seconds passed to the particle's life */ particle.lived += dt; /* check if the particle should be dead */ if (particle.lived >= particle.life) { particle.dead = true; continue; } /* calculate the particle's new position based on the seconds passed */ particle.pos.x += particle.vel.x * dt; particle.pos.y += particle.vel.y * dt; /* draw the particle */ ctx.fillStyle = this.settings.colour; var x = this.pos.x + particle.pos.x; var y = this.pos.y + particle.pos.y; ctx.beginPath(); ctx.arc(x, y, particle.size, 0, Math.PI * 2); ctx.fill(); } }; |
終於等來高潮:
現在是萬事俱備只欠東風了,東風就是建立一個噴射器物件並在每一幀呼叫Update方法。別忘了清理畫布。下面就是最終效果。
See the Pen Particle engine tutorial Part 1 by suffick (@suffick) on CodePen.
有點無聊是不是,你可以修改一些設定來建立一些不同的效果。
接下來幹啥?
基本的粒子系統已經有了,第二部分就是對它進行優化。
第二季內容:
- 重力效果
- 色彩變換
- 紋理渲染
- 座標設定
- 還有你猜…
上面這些實現之後,我們的粒子引擎將會比現在要耀眼以及豐富得多得多。。。
先說到這兒吧,希望你喜歡。 敬請期待下一季……