從零開始開發一款H5小遊戲(四) 撞擊吧粒子,炫酷技能的實現

本系列文章對應遊戲代碼已開源 Sinuous gamejavascript

本遊戲有五種技能粒子,分別是 "護盾","重力場","時間變慢","使敵人變小","增長生命"。Player粒子吃了技能粒子後就能表現各類特殊效果。java

碰撞檢測

遊戲中Player粒子可能會撞擊到Enemy粒子,也可能吃到Skill粒子。咱們怎麼來判斷呢?畫布中兩個粒子的碰撞檢測其實很簡單,若是是圓形粒子,只須要判斷兩個粒子圓心的距離是否小於兩個圓半徑之和就好了。git

//index.js
function collision(enemy, player) {
    const disX = player.x - enemy.x;
    const disY = player.y - enemy.y;
    
    return Math.hypot(disX, disY) < (player.radius + enemy.radius);
}

撞擊敵人

roadmap.path

撞擊後Enemy粒子尾巴上的生命點會減一,而且Player身體出現閃爍,接着會有藍色粒子爆炸的效果。github

前面咱們已經講過尾巴上的生命點如何實現,這時候只須要將生命點值livesPoint減一就能夠了。segmentfault

Player的閃爍怎麼實現呢?若是將這個過程拆解一下,其實閃爍效果就是在一段時間內,Player的顏色不斷隨機地作藍白變化。這裏只要控制兩個變量,閃爍時間和閃爍顏色。數組

collision檢測到碰撞的時候,會調用一個flash方法。這個方法有兩個做用,一是控制閃爍的時間,經過flashing, 判斷是否渲染閃爍效果。二是當時間結束後,咱們須要重置Player的顏色爲默認的藍色。dom

//Player.js
flash() {
    let self = this;
    
    self.flashing = true;
    let timeout = setTimeout(function() {
        self.flashing = false;
        self.color = BODYCOLOR;
        clearTimeout(timeout);
    }, 500);
}

在整個Player的render方法中, 若是flashing標記爲true,則控制Player的顏色在兩個隨機值間切換。這樣每次render調用所產生的顏色就有所不一樣,實現隨機閃爍的效果。this

render() {
    //閃爍效果
    if (this.flashing) {
        this.color = ["#fff", BODYCOLOR][Math.round(Math.random())];
    }
}

爆炸的實現其實也很簡單。一樣的方法,咱們將這個過程分解一下:多個粒子以撞擊點爲原點,向隨機方向作速度不一樣的運動,到達某個邊界距離時,粒子消失。
這裏咱們要肯定哪些變量呢?粒子的數量和顏色大小、爆炸原點位置、粒子的運動方向和速度,粒子消失的邊界值。因爲這些屬性比較多,因此仍是獨立出來一個爆炸粒子的類Particle.jsspa

//Particle.js
/**
 * 爆炸粒子
 */

import map from './Map';

const rand = Math.random;

export default class Particle {

    constructor(options) {
        this.x = options.x;
        this.y = options.y;
        this.vx = -2 + 4 * rand();   //速度隨機
        this.vy = -2 + 4 * rand();   //速度隨機
        this.destroy = false;
        this.speed = 0.04;           //粒子消失的速度
        this.size = options.size || 2;
        this.color = options.color || "rgb(30,136,168)";
        this.width = this.size + rand() * 2; //粒子大小
        this.height = this.size + rand() * 2; //粒子大小
    }

    update() {
        //向x軸和y軸的運動
        this.x += this.vx;
        this.y += this.vy;
        
        //粒子不斷變小
        this.width -= this.speed;
        this.height -= this.speed;
        
        //粒子消失時,將狀態至爲destroy,再也不渲染
        if (this.width < 0) {
            this.destroy = true;
        }
    }

    render() {
        map.ctx.fillStyle = this.color;
        map.ctx.fillRect(this.x, this.y, this.width, this.height);
    }
}

一樣,在檢測到碰撞時,會調用boom方法, 該方法初始化全部爆炸粒子,因爲爆炸須要一個過渡的過程,因此不能像閃爍同樣用簡單的時間控制,這樣會照成爆炸到一半忽然全部粒子消失的狀況。設計

//Player.js
boom(x, y, color, size) {
    let self = this;
    let eachPartical = [];
    for (let i = 0; i < self.particleCount; i++) {
        eachPartical.push(new Particle({x, y, color, size}));
    }
    self.particles.push(eachPartical);
}

在整個大render方法中,調用renderBoom方法,當某個爆炸粒子達到邊界值時,就將其從數組中剔除。達到粒子漸漸消失,不斷變少的效果。

//Player.js
renderBoom() {
    for (let i = 0; i < this.particles.length; i++) {
        let eachPartical = this.particles[i];
        for (let j = 0; j < eachPartical.length; j++) {
            //爆炸粒子消失時,從數組中排除
            if (eachPartical[j].destroy) {
                eachPartical.splice(j, 1);
            } else {
                eachPartical[j].render();
                eachPartical[j].update();
            }
        }
    }    
}

render() {
    //爆炸
    if (self.particles.length) self.renderBoom();
}

最後還要作一件事,就是將撞擊的Enemy粒子從數組中除去,並從新隨機生成一個。

護盾

roadmap.path

知道了Enemy撞擊效果的實現,護盾效果實現起來就簡單不少了。試着分解一下護盾撞擊的整個動做,就能清晰地用代碼描述出來,這裏就不細講了。
有所不一樣的就是護盾撞擊的判斷,他的撞擊點變成了外圈,而不是粒子自己。因此須要對collosion作點修改。

function collision(enemy, player) {
    const disX = player.x - enemy.x;
    const disY = player.y - enemy.y;
    if (player.hasShield) {
        return Math.hypot(disX, disY) < (player.shieldRadius + enemy.radius);
    }
    return Math.hypot(disX, disY) < (player.radius + enemy.radius);
}

細心的話會注意到護盾撞擊粒子後右上角有分數增長,這些數字會出現並漸隱。他的實現原理跟爆炸粒子類似,咱們用一個數組來存儲撞擊位置,並在render將數組渲染出來,每一個粒子達到邊界值時將其刪除,same thing。

重力場

roadmap.path

重力場這個效果實際上是最難的,它須要找到一條公式來完美描述粒子的運動軌跡。嘗試了不少種方法仍是沒能達到很好的效果。這裏主要講一下個人實現思路。

首先重力場的渲染原理跟護盾差很少,都是畫圓,不過這裏用到了顏色過渡的API createRadialGradient

renderGravity() {
    map.ctx.beginPath();
    map.ctx.globalCompositeOperation="source-over";

    var gradient = map.ctx.createRadialGradient(this.x, this.y, this.radius, this.x, this.y, this.gravityRadius);
    gradient.addColorStop(0, "rgba(30,136,168,0.8)");
    gradient.addColorStop(1, "rgba(30,136,168,0)");
        
    map.ctx.fillStyle = gradient;
    map.ctx.arc(this.x, this.y, this.gravityRadius, 0, Math.PI*2, false);
    map.ctx.fill();
}

重力技能有別於其餘技能的點在於,他會影響Enemy粒子的運動軌跡,因此還要在Enemy中作點手腳。

index.js中,發動機animate方法經過一個循環來渲染Enemy粒子。

//index.js
function animate() {
    for (let i = 0; i < enemys.length; i++) {
        enemys[i].render();
        enemys[i].update();
        if (!player.dead && collision(enemys[i], player)) {
            if (player.hasGravity) {
                enemys[i].escape(player);
            }
        }
    }
}

這裏加入了一個判斷,當粒子撞擊的時候,判斷Player是否有重力技能,若是有的話調用Enemy的escape方法,傳入player爲引用。爲何要傳入player?由於Enemy粒子要根據Player的位置實時作出反饋。來看escape方法怎麼實現的,這裏講兩種思路:

第一種,計算Enemy粒子和Player粒子之間的角度,並經過Player重力場的半徑算出在x軸方向和y軸方向的運動速度,主要是想獲得兩個方向運動速度的比例,從而也就肯定運動的方向。再將兩個速度乘以某個比率ratio,從而達到想要的速度。這個效果會致使Enemy粒子朝Player相反的方向運動,有種排斥的效果。

//Enemy.js
escape(player) {
    let ratio = 1/30;
    let angle = Math.atan2(this.y - player.y, this.x - player.x);
    let ax = Math.abs(player.gravityRadius * Math.cos(angle));    
    ax = this.x > player.x ? ax : -ax;    

    let ay = Math.abs(player.gravityRadius * Math.sin(angle));    
    ay = this.y > player.y ? ay : -ay;

    this.vx += ax * ratio;
    this.vy += ay * ratio;
    this.x += this.vx * ratio;
    this.y += this.vy * ratio;
}

第二種,一樣計算出兩個撞擊粒子之間的角度,並計算出x軸和y軸的投射距離。當兩個粒子碰撞時,粒子還會繼續前進,而後Enemy粒子就會進入Player粒子的重力場,這時候立刻改變各軸上的位置。使Enemy粒子運動到重力場外,這樣達到的效果就是Enemy粒子會沿着重力場的邊界運動,直到逃離重力場。

escape(player) {
    let angle = Math.atan(Math.abs(player.y - this.y) / Math.abs(player.x - this.x));
    let addX = (player.gravityRadius) * Math.cos(angle);
    let addY = (player.gravityRadius) * Math.sin(angle);

    if (this.x > player.x && this.x < player.x + addX) {
        this.x += this.speed * 2;
    } else if (this.x < player.x && this.x > player.x - addX) {
        this.x -= this.speed * 2;    
    }

    if (this.y > player.y && this.y < player.y + addY) {
        this.y += this.speed;
    } else if (this.y < player.y && this.y > player.y - addY) {
        this.y -= this.speed;    
    }
}

這兩種方法都還不夠完美,無法表現出順滑的逃逸效果。自認功力尚淺,須要繼續研究一些物理運動的方法才行。

粒子變小&時間變慢

粒子變小的操做就很簡單了。只需改變Enemy粒子的半徑就能夠了。而時間變慢也僅僅是改變Enemy粒子的運動速度,這兩個就不拿出來說了。
roadmap.path

增長生命

還有一個功能是增長生命,沒錯,上面提到了減小生命直接改變livesPoint的值,而增長生命咱們還須要改變尾巴的長度。尾巴的長度怎麼變長?讀了上一篇文章你應該知道了吧。
roadmap.path

關於粒子撞擊和技能的實現就講到這了,這部分是遊戲的精華,也是遊戲能不能吸引人的根本。然而一個遊戲要完整,確定少不了一些遊戲的策略還有一些附屬場景,下一節要講的是《從零開始開發一款H5小遊戲(五) 必要的包裝,遊戲規則和場景設計》

相關文章
相關標籤/搜索