(10/11更)私下有一些朋友反應手機不太能玩,我才想起這個問題,所以有對此demo在長版進行微調,原文主要講解橫版(電腦端)的處理
先給大家看看成果吧!
https://jerry-the-potato.github.io/Chapter5-demo2/
昨天做到這就中斷了,說實話後面也比較複雜一點,目前的框架似乎有一點難hold住,講解起來可能會有一點混亂,總之就講重點,把實現的方法講清楚就好!
function GamingScreen(){
try{
Resize("#game-box", canvas, context, '#000');
Redraw(); // 主動畫
// 遊戲結束時(音樂播放完畢)
if(audio.ended){
// 一樣可以設置轉場動畫,等轉場結束回到初始畫面
if(true){ // 這邊設置true直接跳轉
header.style.pointerEvents = "auto"; // 重新啟用選單事件(主要是點擊)
startScreen.style.display = "block"; // 重新顯示起始畫面
myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/6, 90, maxTimes);
loadingAnime = requestAnimationFrame(LoadingScreen);
return;
}
}
}catch(e){
console.log(e);
return;
}
if(audio.paused) return;
gamingAnime = requestAnimationFrame(GamingScreen);
}
還沒時間做結算畫面,等遊戲結束時就直接回到起始畫面
話說loadingAnime和gamingAnime其實都表示當下的編號,不需要用不同變數存,這邊只是我想留下每個場景最後的紀錄,才分別用不同變數儲存。
分為兩部分,一個是遊戲的主畫面,會受運鏡影響視野範圍,包含樹、樹葉、落葉等等;另一個是UI視窗,不受運鏡影響,包含滑鼠、分數等等資訊:
function Redraw(){
clear(context);
AudioProcess();
// 1. 運鏡會改變整體座標
let x = camera.pointX * WIDTH;
let y = camera.pointY * HEIGHT - (myMouse.pointY - 0.5 * HEIGHT) * 0.1;
context.translate(x, y);
myTree.Transform();
myTree.Draw(); // 畫樹
AnimeProcess(); // 樹葉掉落
context.translate(-x, -y);
// 2. 以下為UI介面不受運鏡影響
MouseAnime(); // 滑鼠追蹤
context.font = WIDTH * 0.02 + 'px IBM Plex Sans Arabic';
context.fillStyle = 'rgba(179, 198, 213, 1)';
context.fillText("分數: " + Math.floor(gameScores/RATIO),
WIDTH * 0.55, HEIGHT * 0.90);
context.fillText("生命值: " + leafNodes.length + " / " + lifePoint,
WIDTH * 0.55, HEIGHT * 0.95);
}
在運鏡的地方除了camera也增加了滑鼠可以上下移動,來改變視野的效果,可以把整棵樹看得更清楚,不添加水平座標,避免玩家覺得頭暈。
其他都是沿用以前文章的內容,就不特別說明了。
今天修了蠻多地方,有點難描述,一個一個來吧!另外因為程式碼一次貼上,會太多造成閱讀困難,因此只要是之前講過的「相同內容」,都會以略過的方式,只有針對我們要修改的地方給大家看,如果想看得更完整可以去github唷!
在樹葉節點誕生之初新增了幾個屬性:
let Tree = function(x, y, r, theta, times, min = 15){
treeNodes = [this];
leafNodes = [];
fallingNodes = []; // 新增正在等待掉落的落葉隊列
// ...以下略
}
let Stick = function(father, shrink_diff, angleOffset, times){
// ...以上略
if(this.r < this.min || times < 0){
leafNodes.push(this);
this.growth = 0; // 葉子的成長值(最大為1)
this.growing = true; // 葉子是否成長中
this.falling = false; // 葉子是否正在掉落
this.img = pngImg['1'][2 + Math.floor(random(2))];
return this;
}
// ...以下略
並且在繪製樹葉(樹上的)的地方做了結構的修改,
for(let N = 0; N < leafNodes.length; N++){
let node = leafNodes[N];
if(node.growing == true){ // 如果還在成長中
node.growth = Math.min(1, node.growth * 1.005 + 0.002); // 成長
if(node.growth == 1){ // 如果已經長到最大
fallingNodes.push(node); // 在掉落的前置陣列中放入該節點
node.growing = false; // 停止成長
}
}
if(node.falling == false){ // 如果葉子尚未掉落
// ...略
// 畫樹葉
}
}
增加這些設定是為了後面「隨著音樂使葉子掉落的功能」
此時this.falling還是false,因為還沒開始掉落,要等到音樂播放的時候,還記得有一段代碼:
if(v1 > 0 && v2 > 0 && v3 <= 0){
let times = 100 * Math.max(v1, v2) / maxVolume; // 100乘上一個0~1之間的數
new animeObject(times * 0.3, 'Falling');
}
這次我把動畫的物件建立簡化成以上的方式,沒有立刻push到動畫清單animeList裡面,原因是我們要先判斷「樹上是否有樹葉」等待掉落,也就是:
function animeObject(){
// ...基礎設定
// 以上略
// 樹上如果沒有樹葉等待掉落,該陣列的長度則為0,則不會把物件送到動畫清單
if(fallingNodes.length){
// 隨機取用一個樹葉
let ranIndex = Math.floor(Math.random() * fallingNodes.length);
this.node = fallingNodes[ranIndex];
// 檢查圖片來源是否有問題
if(this.node.img == undefined) return;
// 把我們剛剛設置的falling狀態改成true,靜態樹葉就不會再被繪製
this.node.falling = true;
// 以公轉角度去代入落葉的動畫公式
this.beginX = this.node.father.father.endX - this.period * WIDTH * 0.02 * Math.sin(this.revolveTheta);
this.beginY = this.node.father.father.endY - this.period * HEIGHT * 0.01 * Math.sin(this.revolveTheta * this.period);
// 繼承原本樹葉的屬性
this.rotateTheta = -Math.PI / 4 - this.node.theta / 180 * Math.PI;
this.size = this.node.r * 1.5 / WIDTH;
this.img = this.node.img;
// 前面都沒問題,則做最後處理:
// 1. 把該樹葉從等待掉落的陣列中移除
fallingNodes.splice(ranIndex, 1);
// 2. 把該樹葉送到動畫清單中
animeList.push(this);
// 3. 根據音樂脈衝決定是否掉落更多樹葉
if(times > 5) new animeObject(Math.pow(times, 0.9), animeName);
}
}
座標公式可對照下面的Falling方法
除此之外,我們還要修改一下落葉的大小變化,當初第四章設計從0-1-0變大再變小,這邊因為是從樹上掉落,因此是原始大小1-0,持續變小,我們可以簡單的寫:
animeObject.prototype.Falling = function(dT){
let revolveNow = this.revolveTheta + this.revolveOmega * dT;
let A = Math.sin(revolveNow);
let C = Math.sin(revolveNow * this.period);
this.pointX = this.beginX + this.scaleX * (this.period * WIDTH * 0.02 * A + WIDTH * 0 * dT);
this.pointY = this.beginY + this.scaleY * (this.period * HEIGHT * 0.01 * C + HEIGHT * 0.04 * dT);
this.sizeNow = WIDTH * this.size * (1 - 1 * dT / this.lifeTime);
}
最一開始dT為0,因此
接下來這邊要注意,因為剛才有運鏡設定,所以樹葉在空間中的座標,和滑鼠的座標,是一個相對值,也就是:
let x = camera.pointX * WIDTH - 0 * (myMouse.pointX - 0.5 * WIDTH) * 0.1;
let y = camera.pointY * HEIGHT - (myMouse.pointY - 0.5 * HEIGHT) * 0.1;
在做碰撞檢測時,要把它考慮進去:
let distance2p = Math.pow(this.pointX + x - myMouse.pointX, 2) +
Math.pow(this.pointY + y - myMouse.pointY, 2);
let mouseWidth = WIDTH * 0.05;
let thisWidth = this.sizeNow;
if(distance2p < Math.pow((mouseWidth * 0.4 + thisWidth * 0.5), 2)){
// 加分,落葉救得越早分數越多
gameScores+= this.sizeNow;
// 用另一個物件取代該物件
let newObject = new animeObject2(this.node, this.rotateTheta,
this.img, this.sizeNow,
this.pointX, this.pointY,
this.beginX, this.beginY, 60);
let index = animeList.indexOf(this);
delete animeList[index];
animeList[index] = newObject;
}
在觸碰到樹葉的當下進行加分,並且用一個新的動畫物件取代,只保留部分屬性,this.node是今天才設計的,也要一起被保留下來
把音樂播放和處理的函式AudioProcess放入開場畫面,在進遊戲前,就用一段放鬆的音樂,先讓玩家看到,有一棵樹會隨著音樂掉落,也算是賞心悅目,然後準備若干個按鈕元件:
<ul id="game-menu">
<li><button id="Play">Play</button></li>
<li><button id="How">How to Play</button></li>
<!-- <li><button id="Pause">Pause</button></li> -->
<li>
<select id="Select">
<option>- Select -</option>
<option value="Advertime.mp3">Advertime</option>
<option value="Brothers Unite.mp3">Brothers Unite</option>
<option value="Horizon Flare.mp3">Horizon Flare</option>
<option value="Lovely Piano Song.mp3">Lovely Piano Song</option>
<option value="Motions.mp3">Motions</option>
</select>
</li>
<li><button id="Learn">Learn more</button></li>
</ul>
有基本的遊玩、遊戲方法、選擇曲子、了解更多四個選項
接著設計一個對話框,是我從第二章的demo一直用到現在,中間都有用,概念就很簡單:
<div id="dialog-box">
<h3 id="dialog">預設曲目: Lovely Piano Song.mp3</h3>
</div>
然後在事件監聽中做不同處理:
let How = document.querySelector("#How");
let Learn = document.querySelector("#Learn");
let dialog = document.querySelector("#dialog");
How.addEventListener("click", function(){
dialog.textContent = "樹葉會隨著音樂,不斷的掉落,玩家需要移動滑鼠,接住落葉,讓樹避免枯萎的命運";
});
Learn.addEventListener("click", function(){
dialog.textContent = "本遊戲出自「從零打造網頁遊戲,造輪子你也辦的到!」教學文--2021年度鐵人賽--by Jerry, the Potato";
});
其他兩個按鈕是之前就做過的功能,這邊就不佔篇幅了
終於,完賽了嗚嗚,還好最後有把遊戲做出來^u^
然後才發現完全不是休閒遊戲,玩的時候要拼命接樹葉,還要頂著樹木枯萎的壓力www