iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 29

Chapter5 - 輕鬆用Canvas實現轉場動畫和運鏡處理

接下來時間真的很緊,也顧不上結構了,只能就目前想到的功能,先以直覺的方式編寫了,如果講不太清楚還多多包涵!昨天的一定要先看唷,今天是接著作下去的

轉場

玩家從進到網頁開始,一直到遊戲結束,總共會經歷四個場景:

  1. 讀取畫面+主畫面(LoadingScreen)
    1. 樹的生長動畫(treeGrowth)
    2. 讀取進度條(loading)
    3. 選單淡入(opacity)
  2. 遊戲開場畫面(OpeningScreen)
    1. 選單淡出(opacity)
    2. 運鏡(camera)
  3. 遊戲進行畫面(GamingScreen)
    1. 樹葉生長動畫
    2. 樹葉掉落動畫
    3. 樹葉收集動畫
  4. 遊戲結算畫面(EndingScreen)

1. 讀取畫面+主畫面 LoadingScreen

調整一下昨天設計的:「點擊畫面任一處,重新長出一顆新的樹」取名為MakeTree函式,方便我們在離開讀取畫面後,可以移除這個功能(事件):

startScreen.addEventListener('click', MakeTree);
function MakeTree(){
    treeGrowth.Restore();
    myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/6, 90, maxTimes);
}

樹的生長1-1和讀取進度條1-2昨天都完成了,這邊繼續添加選單淡入1-3的功能,是在按下開始鍵後始選單1.5秒(90幀)內淡入,覆蓋整個畫面,初始設定:

let opacity = new Trail(0, 0, false);

按下Start按鈕時,移除MakeTree函式,接著設定轉場效果:

Start.addEventListener("click", function(event){
    // 設定
    event.stopPropagation();

    // 執行
    startScreen.removeEventListener('click', MakeTree);
    Start.style.display = "none";
    opacity.NewTarget(1, 0, 90);
});

由於昨天有設置整個畫面的click作為讓樹重新長出來的互動,如果點了Start按鈕也會觸發則顯得奇怪,因此在點擊事件中做阻擋冒泡的設定,這樣點擊時,這樣就不算點擊到整個startScreen

然後在昨天設計的動畫框架中,添加1-3選單淡入,設定header的opacity:

function LoadingScreen(){
    try{
        Resize("#game-box", canvas, context, '#000');
        clear(context);
        
        // 1. 讓樹長出來
        treeGrowth.NextFrame(1, -1, 3);
        myTree.Transform();
        myTree.Draw();
        
        // 2. 讀取進度條
        if(loading.timer > 0){
            loading.NextFrame(1, 0, 2);
            let percent = Math.floor(loading.pointX * 100);
            Start.style.width = minWidth + percent + "px";
            Start.textContent = percent + "%";
        }
        else if(Start.disabled == true){
            Start.textContent = "Start";
            Start.disabled = false;
        }
         
        // 3. 選單淡入
        if(opacity.timer > 0){
            opacity.NextFrame(1, 0, 2);
            let header = document.getElementsByTagName("header")[0];
            header.style.opacity = opacity.pointX;
        }
    }catch(e){
        console.log(e);
        return;
    }
    loadingAnime = requestAnimationFrame(LoadingScreen);
}

NextFrame內部會自行判斷timer是否已歸零(動畫結束),這邊的if判斷式在於限制dom的操作在90次(幀數frames=90),為了應該會嘗試把它整合到NextFrame的原型方法中,今天沒這個時間啦~~

2. 遊戲開場畫面(OpeningScreen)

2-1是銜接1-3,讓選單淡入後,等待玩家點擊Play,就讓選單淡出,此過程從場景1換到場景2,因此在這邊:

Play.addEventListener("click", function(event){
    // 讓玩家發現,剛剛開場的那顆樹,已經默默地成長為參天大樹
    myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/2, 90, maxTimes);
    
    // 設定淡出和運鏡
    opacity.NewTarget(0, 0, 90);
    camera.NewTarget(0.1, 0.3, 120); // 第一次運鏡到左下角
    
    // 切換場景 1>2
    cancelAnimationFrame(loadingAnime);
    openingAnime = requestAnimationFrame(OpeningScreen);
});

直接創建了一顆新的樹,嚴謹一點的作法可以在使用原型方法Transform,並在其中修改樹根長度的參數,這樣可以保證是同一棵樹,且形狀相同,不過今天沒時間去修改了><

運鏡起始點:

let camera = new Trail(0, 0, false);

運鏡這邊用偷吃步的作法,把四個運鏡接起來,邏輯就是每次timer歸零,就會進入載入下一次的運鏡,然後把幀數當作編號,依序用120、121、122、123表示四種運鏡,因此當編號120的運鏡結束(timer歸零)時,就會設定新的路徑編號為121,以此類推,當然這樣寫有點佔空間,未來可以再多花時間去思考能怎麼樣包成更簡單的函式:

function OpeningScreen(){
    try{
        Resize("#game-box", canvas, context, undefined);
        clear(context);

        // 1. 選單淡出
        if(opacity.timer > 0){
            opacity.NextFrame(1, 0, 2);
            let header = document.getElementsByTagName("header")[0];
            header.style.opacity = opacity.pointX;
        }

        // 2. 運鏡
        let x = camera.pointX * WIDTH;
        let y = camera.pointY * HEIGHT * 1;
        context.translate(x, y);
        myTree.Transform();
        myTree.Draw(context);
        context.translate(-x, -y);
        
        if(camera.timer > 0){
            if(camera.period == 120){
                camera.NextFrame(1, 1.5, 0);
            }
            else if(camera.period == 121){
                camera.NextFrame(1, 0, 1);
            }
            else if(camera.period == 122){
                camera.NextFrame(1, 1.5, 0);
            }
            else if(camera.period == 123){
                camera.NextFrame(1, 0, 1.5);
            }
        }
        else if(camera.period == 120){
            camera.NewTarget(0, 1, 121); // 第二次運鏡到正上方
        }
        else if(camera.period == 121){
            camera.NewTarget(-0.3, 0.2, 122); // 第三次運鏡到右下角
        }
        else if(camera.period == 122){
            camera.NewTarget(0, 0.6, 123); // 第四次運鏡到中間
        }
        else{
            // 運鏡結束,直接進入第三個場景:遊戲進行畫面
            GamingAnime = requestAnimationFrame(GamingScreen);
            // 直接在此處中斷即可結束該開場動畫
            return;
        }
    }catch(e){
        console.log(e);
        return;
    }
    openingAnime = requestAnimationFrame(OpeningScreen);
}

以camera的座標去換算,運鏡分別設定四個點,讓鏡頭順時針繞一圈,呈螺旋狀(底部>左下>正上方>右下>中間)

3. 遊戲進行畫面(GamingScreen)

做到這邊我發現一個致命問題,接下來的第三個場景需要一個靜態畫布+動態畫布,而我原先設計的動畫框架,沒有考慮到使用兩個以上的畫布的情況,因此有點難以擴充,搞了一下子發現事情不太對勁,要重新編修整個框架,包含Resize動態調整寬高的方式,這部份明天繼續!

今日DEMO (上傳完畢)

https://jerry-the-potato.github.io/Chapter5/
運鏡的部分稍微有點粗糙,主要是因為這四個運鏡都是直線,如果之後有把第四章附錄的貝茲曲線繼續做完,就可以應用在這上面,便再也不用擔心運鏡看起來卡卡的囉!


上一篇
Chapter5 - 當一個勤勞的園丁,來修剪我們美麗的樹(III)Canvas動畫 讓樹隨著讀取畫面長大
下一篇
Chapter5 - 不介意的話,請玩玩看這個Canvas遊戲!試圖拾回一片片的落葉,拯救這顆樹吧
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言