MaDi's Blog

一個紀錄自己在轉職軟體工程師路上的學習小空間

0%

前端遊戲專案紀錄

最近寫了一個前端遊戲的專案,過程中踩了滿多雷,花了滿多時間,為了不想以後重複造輪子,趁目前記憶猶新紀錄一下整個過程,提供給未來遇到相似問題的自己參考。

按鍵觸發音效

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 播放按鈕音效的函式
function playSoundEffect(){
var playSoundCorrect = new Audio("./public/game1/click.mp3");
playSoundCorrect.play();
}
// 函式持續執行
$(function(){
// 按下元素1的按鈕
$(元素1).on("click",function(){
playSoundEffect();
})
...
...
});

網頁播放音樂

html

1
2
3
4
5
6
<!-- 加音樂 -->
<body>
<audio id="myAudio" autoplay="true" loop>
<source src="./路徑/檔名.mp3" type="audio/mpeg">
</audio>
</body>

需要從google網頁的設定去調整播放的控制選項

解決新版google chrome 影片或聲音無法自動播放的問題 (Fix google chrome Autoplay policy)

用D3.js讀檔

html

1
2
<!-- d3 -->
<script src="https://d3js.org/d3.v6.min.js"></script>

js

1
2
3
4
5
6
// 用D3載入csv
ques_list = []
d3.csv(file, function(data){
ques_list.push(data); //用table的方式在console呈現json
return ques_list;
});

禁止在網頁上按右鍵、鍵盤…

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 禁止進行任何動作
$(document).ready(function() {
// 禁止按右鍵
$(document).get(0).oncontextmenu = function() {
return false;
};
// 禁止按ctrl/alt/shift
$(document).get(0).onkeydown = function(){
if ( event.ctrlKey ){
return false;
}
if ( event.altKey ){
return false;
}
if ( event.shiftKey ){
return false;
}
}
// 禁止在網頁選取
$(document).get(0).onselectstart = function(){
return false;
}
});

自定義動畫keyframe

參考網路上的插件 jquery-keyframes

遇到的問題:

有一個轉盤,背後有很多題目,每一題對應到一個角度。想客製化轉盤旋轉停止的角度,也就是第五題就停止在第五題對應的角度。

解決方法:

都先轉N圈,最後一圈才決定要轉幾度(depend on 第幾題, i.e. iEnd),rotate('+(360*N+iEnd*45)+'deg)',整體動畫播放才會流暢

html

1
2
<!-- jQuery.Keyframes -->
<script type="text/javascript" src='./public/js/jQuery.Keyframes-master/dist/jquery.keyframes.min.js'></script>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判斷是否支援
var supportedFlag = $.keyframe.isSupported();

// 定義一個叫做go的動畫
$.keyframe.define([{
name: 'go',
'0%': {'transform': 'translate(-50%,-50%) rotate(0deg)'},
'100%': {'transform': 'translate(-50%,-50%) rotate('+(1440+iEnd*45)+'deg)'}
// 都先轉4圈,最後一圈才決定要轉幾度(depend on 第幾題, i.e. iEnd)
}]);

// 在加上go這個class的元素上播放go動畫
$('元素.go').playKeyframe({
name: 'go',
duration: "7s", // 動畫執行時間
timingFunction: 'ease-out', // 快到慢
iterationCount: '1', // 只執行一次動畫
});

設定倒數計時器

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stop_time = false;  // 初期設定尚未停止
sec = 60; // 設定倒數時間60秒
var setint; // 設定倒數計時器

clearInterval(setint); // 清空倒數計時器,否則會一直倒數

// 設定每8000毫秒(8秒)的倒數計時器
setint = setInterval( function(){
// 預設stop_time=true -> 直接return
if(stop_time){
return
}
// 當倒數結束
if(sec==0){
...
}
}, 8000);

jquery-mobile 跳頁

下載 jquery.mobile-1.4.5.min.css

html

1
2
3
4
5
6
7
8
9
10
11
12
<head>
<link rel="stylesheet" href="./public/jquery.mobile-1.4.5/jquery.mobile-1.4.5.min.css">
</head>

<script src='https://code.jquery.com/jquery-1.11.1.min.js'></script>
<script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>

<body>
<div data-role="page" id="page1"></div> // 第一頁
<div data-role="page" id="page2"></div> // 第二頁
...
</body>

js

1
2
$.mobile.changePage('#page1',{allowSamePageTransition:true,transition:"slidedown"}); // 跳回第一頁
...

bootstrap modal 跳視窗

1
2
3
// 避免點擊外部空白而消失modal(重要!!)
$("#myModal").modal({backdrop:'static',keyboard:false});
$('#myModal').modal('show')

隨機產生數字

1
2
3
4
// 隨機產生數字 
function randomNum(min, max) {
return Math.floor(Math.random() * ((max - min)+1) + min);
}

隨機sample出n個數量的陣列

1
2
3
4
5
6
7
8
9
10
11
function generateRandomItemsArray(n, originalArray) {
let res = [];
let clonedArray = [...originalArray]; // ... operator -> 把迭代物件展開
if(n > clonedArray.length) n=clonedArray.length;
for(let i=1; i<=n; i++) {
const randomIndex = Math.floor(Math.random()*clonedArray.length); // 1~n
res.push(clonedArray[randomIndex]);
clonedArray.splice(randomIndex, 1); //把cloneArray隨機抽到的index丟進res,接著刪除cloneArray隨機抽到的那個元素
}
return res;
}

Drag-Drop事件

html

1
2
3
4
5
<!-- drag的物件 -->
<div class='draggable'></div>

<!-- drop的物件 -->
<div class='droppable'></div>

js

定義 dragStart, dragEnter, dragOver, dragLeave 四個觸發的函式

重點:

  1. event.dataTransfer.setData("text", 設定drag傳送的資料): 可以設定drag元素要丟給drop物件的資訊
  2. event.dataTransfer.getData("text"): drop物件抓取drag物件傳送的資訊
  3. dragStart: 觸發事件(event)是drag元素
  4. dragEnter, dragOver, dragLeave: 觸發事件(event)是drop元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 被拖曳的物件觸發
function dragStart(event) {
// 設定event傳送id給text
event.dataTransfer.setData("text", event.target.id); // or "text/plain"
}

// 滑鼠拖曳狀態下,滑鼠進入drop物件,由drop物件觸發
function dragEnter(event) {
if(event.target.classList && event.target.classList.contains("droppable") && !event.target.classList.contains("dropped")) {
event.target.classList.add("droppable-hover");
}
}
// 滑鼠拖曳狀態下,滑鼠在drop物件上,由drop物件觸發
function dragOver(event) {
if(event.target.classList && event.target.classList.contains("droppable") && !event.target.classList.contains("dropped")) {
event.preventDefault();
}
}
// 滑鼠拖曳狀態下,滑鼠離開drop物件,由drop物件觸發
function dragLeave(event) {
if(event.target.classList && event.target.classList.contains("droppable") && !event.target.classList.contains("dropped")) {
event.target.classList.remove("droppable-hover");
}
}

定義 drop 函式,這個函式同時考慮了電腦滑鼠操作以及平板手機操作

重點:

  1. 區分觸發事件是mouse還是touch: 用event.type去區分,mouse是drop,touch是touchend
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
function drop(event) {
event.preventDefault();

// 電腦
if(event.type=="drop"){
event.target.classList.remove("droppable-hover"); // 移除hover的class
var draggableElementBrand = event.dataTransfer.getData("text"); // 用text接收drag夾帶的id
var droppableElementBrand = event.target.getAttribute("data-brand"); // 獲取drop下來的物件的屬性,作為判斷是否放對的依據
}
// 觸控板
else if(event.type=="touchend"){
$(".droppable-hover").removeClass("droppable-hover") // 每次移動都把有包含hover這個動畫的元素的class去除

event.target.style.zIndex = '-10'; // 先藏下去,因為elementFromPoint是找最上層
var draggableElementBrand = event.target.id; // pickup選取的icon
var drop_element = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY) // elementFromPoint從位置抓元素(drop位置最上方的元素)
if(drop_element!=undefined){
var droppableElementBrand = drop_element.getAttribute("data-brand");
}
event.target.style.zIndex = '1'; // 再放上層,不然會看不見
}

const isCorrectMatching = draggableElementBrand===droppableElementBrand; // 判斷drop/drag是否放對

// drop對
if(isCorrectMatching) {
...

// 答對的icon去掉touch的觸發
if(event.type=='touchend'){
draggableElement.removeAttribute("ontouchstart")
draggableElement.removeAttribute("ontouchmove")
draggableElement.removeAttribute("ontouchend")
}
}

// drop錯
else{
...
}

// (觸控板)重設回原訂位置
if (moving) {
// reset our element
moving.style.left = '';
moving.style.top = '';
moving.style.height = '';
moving.style.width = '';
moving.style.position = '';

// moving = null;
event.preventDefault()
}
}

加上事件

重點:

  1. drag物件的觸發事件是要加 dragstart
  2. drop物件的觸發事件是要加 dragenter, dragover, dragleave, drop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 選取可以拖拉的物件
const draggableElements = document.querySelectorAll(".draggable");

// 所有拖拉物件加上dragstart事件
draggableElements.forEach(elem => {
elem.addEventListener("dragstart", dragStart);
// elem.addEventListener("drag", drag);
// elem.addEventListener("dragend", dragEnd);
});

// 選取可以置放的物件
droppableElements = document.querySelectorAll(".droppable");

// 所有可置放的物件加上drop相關的事件
droppableElements.forEach(elem => {
elem.addEventListener("dragenter", dragEnter);
elem.addEventListener("dragover", dragOver);
elem.addEventListener("dragleave", dragLeave);
elem.addEventListener("drop", drop);
});

觸控板的touch事件

參考至 Drag and drop elements on touch devices

html

1
2
3
4
5
<!-- drag的物件 -->
<div class='draggable' ontouchstart="pickup(event)" ontouchmove="move(event)" ontouchend="drop(event)"></div>

<!-- drop的物件 -->
<div class='droppable' ontouchend="drop(event)"></div>

js

自己定義 pickup, move 兩個函式來做為觸控板的碰觸移動事件,至於 drop的函式則合併至Drag-Drop事件的 drop函式

重點:

  1. event.changedTouches[0]: 所有從上一次觸摸事件到這次觸發事件,狀態發生改變過的Touch對象,[0] 代表找最新的
  2. document.elementFromPoint(x,y): 從 x, y 座標去找該位置上最上方的物件 (因為此時有拖拉元素,所以最上方的物件其實是拖拉的元素,所以將這個拖拉元素先手動藏下去再浮上來)
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
let moving = null;

// 觸碰板觸碰
function pickup(event) {
moving = event.target;

moving.style.height = moving.clientHeight;
moving.style.width = moving.clientWidth;

// 才不會一按下去就下滑
moving.style.left = event.changedTouches[0].clientX - moving.clientWidth/2 + "px"
moving.style.top = event.changedTouches[0].clientY - moving.clientHeight/2 + "px";
}

// 觸碰板移動
function move(event) {
event.preventDefault()

if (moving) {

// 找到觸發元素的位置
moving.style.left = event.changedTouches[0].clientX - moving.clientWidth/2 + "px";
moving.style.top = event.changedTouches[0].clientY + moving.clientWidth/2 + "px";

// drop的物件加上droppable-hover這個class才能觸發hover的動畫
event.target.style.zIndex = '-10'; // 先藏下去
var drag_element = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY) // elementFromPoint從位置抓元素(每次移動位置最上方的元素)
event.target.style.zIndex = '1'; // 再浮上來

$(".droppable-hover").removeClass("droppable-hover") // 每次移動都把有包含hover這個動畫的元素的class去除
if(drag_element!=undefined && drag_element.className == "droppable"){
drag_element.classList.add("droppable-hover"); // 當最上方的元素是droppable的就增加droppable-hover這個class
}
event.preventDefault();
}
}

事件對象的位置,圖片節錄至 Touch & Drag 事件

畫布 SVG

參考至 SVG 研究之路 (23) - 理解 viewport 與 viewbox

遇到的問題:

輪盤背後要排燈,但輪盤是圓的

解決方法:

先畫一個svg,z-index=-1 代表下移一層,viewbox 設定SVG可視範圍,圖片標籤是<image><img>座標高寬設定好之後,只需要改變 transform: rotate(旋轉deg) 即可

1
2
3
4
5
6
7
<svg style="z-index:-1" viewbox="-50 -50 100 100" version="1.1">
<image href="./路徑/檔名1.gif" x="-7.5px" y="-49px" height="15px" width="15px"/>
<image href="./路徑/檔名2.gif" style="transform:rotate(20deg)" x="-7.5" y="-49" height="15px" width="15px"/>
<image href="./路徑/檔名1.gif" style="transform:rotate(40deg)" x="-7.5" y="-49" height="15px" width="15px"/>
<image href="./路徑/檔名2.gif" style="transform:rotate(60deg)" x="-7.5" y="-49" height="15px" width="15px"/>
...
</svg>

Chrome無法讀取local文件的問題

利用d3.js去讀取本地端的csv檔會出現以下錯誤訊息:

URL scheme must be “http” or “https” for CORS request.

簡單來說,是因為Google Chrome預設禁止這樣的行為

所以參考至網路 處理Chrome無法讀取本地端文件問題,內有三種解決方法

我選擇的解決方法是正規作法,利用npm下載http-server來架一個本地伺服器

1
2
$ npm install http-server -g   // 安裝
$ http-server ./YOUR_FOLDER // 執行

輸入 http://127.0.0.1:8080 就可以看到網站