JavaScript CSS3

Javascript 動態網頁程式設計 - 上課教材

動畫互動網頁程式設計 > 課程內容 > 第 09 章 - 事件處理

第 09 章 - 事件處理

上次更新日期 2020/12/01

我們在前面的章節中,其實使用了不少的『事件』處理功能了,包括使用於滑鼠的 onClick 事件,以及鍵盤的 keydown 事件。 這些事件的觸發,可以帶動程式在網頁裡面進行某些任務,這就是 javascript 的事件囉。還有哪些事件可用? 就讓我們來瞧一瞧。

學習目標:

  1. 理解如何進行事件的監聽,除了 onclick 之外,學習 addEventListener 的用法
  2. 了解滑鼠事件的應用
  3. 使用 HTML5 的 drag and drop 事件處理拖曳行為
  4. 使用鍵盤事件來處理射擊遊戲等任務

9.1: JavaScript 的 event 相關說明

當我們在作業系統或者是瀏覽器上面進行某些舉動時,就會發生某些特定的影響效應,這是所謂的事件驅動 (event driven)。

  • 事件驅動 event driven

在 windows 作業系統當中,任何視窗都有一個唯一的代碼,這就好像程序一般,都有個 PID 的概念。而系統會有個程序持續不斷的監視這些視窗的活動, 當這些視窗有任何的事件發生時,該監控程序就會提供相對應的反應來處理這些活動。舉例來說,使用者進行視窗的拖曳、按下鍵盤或滑鼠、 開啟新的視窗等,監控程式就會根據該次事件給予相對的活動。這種根據用戶操作來的反應行為模式,稱作事件驅動模式 (event driven)。

所以,說穿了,就是你進行什麼動作,就需要有個相對的程式來提供反應,這樣讓你可以順利的操作該系統,那就是事件驅動 (event driven)。 簡單的說,我們前面的章節,當你點擊 (onclick) 某個元件時,該元件會提供某些程式碼,這樣的反應,就稱事件驅動。 所以,同學們已經做過了!^_^

瀏覽器其實都有一直在監控使用者的動作,JavasScript 則可以根據這個監控的行為,來進行額外的某些指令碼,一項一項分別持續處理這樣, 這就是事件驅動的模式。而我們也是透過這樣的事件來撰寫 javascript 的程式碼,讓這些程式可以完成我們的需求。而根據事件的觸發與事件後續的動作, 大致上又將事件的影響分為傳送者與接收者:

  • 事件發送者 (event sender, event generator, event source):觸發事件的物件
  • 事件接收者 (event receiver, event consumer):接收事件後的物件

這些都只是簡單的定義而已,大家只要知道這些事件的專有名詞而已,事實上,之前已經有碰過啊,這裡只是彙整文件說明。 接下來就得要了解,那麼有哪些常見的事件在瀏覽器與 javascript 裡面會發生呢?

  • 傳統的 HTML 瀏覽器事件

底下為網頁、瀏覽器上面常見的事件,裡面有幾項大家應該都已經撰寫過相關的程式了!先來談談與視窗 (window), 亦即跟瀏覽器本身及網頁整體比較有關的事件:

  • load (載入):瀏覽器完成載入網頁時,會觸發的事件
  • unload (卸載):瀏覽器移除網頁或是窗時,會觸發的事件
  • focus (聚焦):例如透過鍵盤 [tab] 按鈕,讓某個元件聚焦時,會觸發的事件
  • blur (模糊):跟 focus 相反,當物件由聚焦變成沒有聚焦時,會觸發的事件
  • error (錯誤):當發生錯誤的時候,會觸發的事件
  • scroll (滾動捲軸):當瀏覽器捲動畫面時,會觸發的事件
  • resize (變更大小):當瀏覽器的視窗大小改變時,會觸發的事件

再來看看跟鍵盤的動作比較有關的事件:

  • keydown (按下按鍵):某個元件被按下鍵盤按鍵時,會觸發的事件
  • keyup (放開按鍵):某個元件放開按鍵時,會觸發的事件
  • keypress (按下放開):某個元件被按下鍵盤按鈕然後放開時,會觸發的事件

再來就是跟滑鼠有關係的事件,用最多的就是我們經常使用的 click 事件了。

  • click (按下滑鼠):在某個元件上面按下滑鼠按鍵 (通常是左鍵) 時,會觸發的事件
  • dbclick (雙擊滑鼠):同 click,只是為雙擊滑鼠按鍵,會觸發的事件
  • mousedown (按下滑鼠):在某元件上按下滑鼠任意鍵,就會立刻觸發的事件 (不用放開)
  • mouseup (鬆開滑鼠按鍵):與 mousedown 相反,放開滑鼠任意鍵,就會立刻觸發的事件
  • mouseover (滑鼠游標):滑鼠游標移動到這個物件上面時,就會觸發的事件
  • mousemove (滑鼠游標):滑鼠游標在這個物件上面移動時,就會觸發的事件
  • mouseout (滑鼠游標):與 mouseover 相反,移開這個物件時,就會觸發的事件
  • mousewheel (滾輪):滾動滑鼠滾輪時,會觸發的事件

滑鼠事件裡面比較有趣的是 mousedown / mouseup / click 這三個事件,到底有什麼分別啊?一般來說,我們使用的 click 事件, 大部分指的是按下左鍵之後鬆開,才會去進行的事件任務,而 mousedown 則是按下按鍵的那一刻,就開始運作的事件,無論是左鍵、右鍵還是中鍵。 至於 mouseup 則是鬆開按鍵後要去進行的事件。

如果三個事件都有設定,那當你按下滑鼠右鍵後鬆開,會分別進行 mousedown, mouseup, click 事件!

另外,還有跟表單相關性比較高的事件!這些事件最好放置到 <form> 的元件裡面,比較適當!畢竟因為是表單啊!

  • submit (送出):送出表單的時候,會觸發的事件
  • reset (重置):清除表單時候,會觸發的事件
  • select (選擇):大致上與 select 元件有關,選擇某些 option 或文字欄位時,會觸發的事件
  • change (改變):當表單上面的選擇有改變時,會觸發的事件
  • focus / blur :跟視窗事件一樣,就是某個表單上的元件在聚焦與模糊的情境下,會觸發的事件

通常我們使用的事件就是這一些,當然還有許多有趣的事件,包括 HTML5 事件、DOM 事件與觸控事件等,那就未來慢慢用到再來處理。 這些事件從瀏覽器上面被觸發,我們可以撰寫事件處理程式來進行對應的工作 (event handler),我們也能撰寫監聽事件的腳本程式, 來持續監聽事件並回應該事件 (event listener)。

不對啊!我們之前的 javascript 程式碼,不是都用 onclick 之類的方式來呼叫嘛?為啥上面寫 click 事件? 是的, click 是事件,而要再某些情境下讓這個事件開始工作,就是要加上 on!因此就得到 onclick 這樣的關鍵字!

另外,除了在 HTML 原始碼裡面使用類似 onclick="myfunction()" 之類的方式來呼叫函數之外,也可以透過 javascript 裡面直接這樣撰寫:

# 方法一:
function myfunction() {
	process...
	...
}
<tag onclick="myfunction()" id="myobj" />

# 方法二:
myobj = document.getElementById('myobj');
myobj.onclick = myfunction;
function myfunction() {
	process...
	...
}
上面兩種方法都可以執行,不過函數的撰寫方式比較不一樣。如果沒有外帶參數的話,在 javascript 裡面的物件使用 obj.onclick 時, 後面接的函數不能加上括號 (),否則會失敗。而一般在 HTML 原始碼裡面,就得要加上括號才會有動作。在撰寫程式時,得要特別留意喔! 否則會經常發生無法執行的錯誤!
例題 9-1-1:了解不同的事件,使用 javascript 內部事件的處理方式,取消 HTML 原始碼內的事件觸發語法
  1. 將 unit08-3-4.php 照片淡出淡入另存為 unit09-1-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 將 HTML 原始碼裡面,在 body 的部份,將 onload 函數取消,改以進入 javascript 的程式碼內, 加上底下這行來處理網頁載入後立刻進行的函數 (透過視窗事件):
    window.onload = mymain;
    
  3. 增加一個名為 rendergo() 的函數,內容為進行 timer,直接將 mymain() 裡面的 setIntervla 複製過來即可。
  4. 增加一個名為 renderdown() 的函數,內容為 clearInterval(timer) 即可。
  5. 修改 mymain() 函數的內容:
    • 讓 myfig 元件增加 onmouseover 時,執行 renderdown 的函數,亦即滑鼠移動到圖片上,圖片就不會淡出了!
    • 讓 myfig 元件增加 onmouseout 時,執行 rendergo 的函數,亦即滑鼠移出圖片後,圖片開始淡出淡入變化
    • 取消 timer,但是增加執行 rendergo() 函數的運作 (將相同的指令程式碼集中管理)

上面的範例中,我們可以學會不用在 HTML 的原始標籤裡面放置 onclick, onmouseover 等指令,取而代之的是直接在取得元件控制權後, 以事件物件的方式來進行設計。此外,上面的範例也能了解,某些網頁的變動時,我們可以使用類似的功能將圖片暫時固定不動的原因與方式。

  • addEventListener 的事件處理模式

除了上述的事件處理方式之外,還有另一個方式,那就是透過 addEventListener 函數來處理。這個函數的處理方式如下:

myobj = document.getElementById('myobj');
myobj.addEventListener('click', myfunction, false|true);
function myfunction() {
	process...
	...
}

在 addEventListener 當中,那個事件名稱就沒有含有 on 這個關鍵字,因此就得要使用 click, mouseover, keydown 等事件名稱, 而不是使用 onclick 之類的事件名稱。至於第二個欄位,那就是函數名稱,一樣不能加上括號喔!否則會失敗。最後一個是事件的執行範圍, 表示當該事件發生時,外層元件是否需要同步處理之意。預設為 false,不寫也就是 false 的意思。

例題 9-1-2:使用 addEventListener 取代 onmouse 等事件監聽方式
  1. 將 unit09-1-1.php 另存為 unit09-1-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 只要修改 mymain() 函數內的資料即可,亦即將 onmouseover 與 onmouseout 那兩行修改成為使用 addEventListener 的方法即可。
  3. 最後整體呈現的方法會與原本的暫停方式相同。
  • removeEventListener 的事件處理模式

想像一個案例,如同上面的照片淡出淡入功能,如果想要讓該照片在經過大約 3 次的 mouseover, mouseout 之後, 就放棄事件處理,而讓照片自己一直輪換下去,不再暫停,那該如何是好?意思就是說,我想要『移除』事件監聽程式。 這時,可以透過 removeEventListener 來處理啊!該函數內容的設定則與 addEventListener 完全一樣!

例題 9-1-3:使用 removeEventListener 移除 onmouse 等事件監聽方式
  1. 將 unit09-1-2.php 另存為 unit09-1-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 在全域變數的地方,設定名為 stopnu 的變數,這個變數給予初始值為 0,代表滑鼠尚未經過圖片。
  3. 在 renderdown 函數內,加上入 stopnu 的變數 +1 的語句
  4. 在 rendergo 的函數內,加入判斷式,當發生 stopnu >= 3 時,就停止 mouseover 與 mouseout 的事件監聽。
  5. 測試一下,在經過三次的暫停後,滑鼠指向應該就會失敗了!測試看看。

9.2: 滑鼠事件與資料拖曳行為

滑鼠事件就如同前一小節談到的,有 mouseover, mouseout, mousemove 等。我們在寫一些遊戲時,很可能需要知道滑鼠的游標所在。 那如何知道滑鼠的游標所在呢?其實就與 JavaScript 對『座標』的定義有關。事實上,根據定義,我們會需要知道幾種位置, 包括 pageX/Y, clientX/Y, screenX/Y 。基本上, screenX, screenY 指的是與『螢幕』有關的定位,這與網頁本身比較無關, 所以,撰寫遊戲時,不太會用到 screenX, screenY。但是如果是用在開啟新視窗,或許就有可能會用到這個定位。若以新開視窗為例, 底下是常見的可能會用到的物件參數:

  • window.screen.width:螢幕的寬度
  • window.screen.height:螢幕的高度
  • document.body.clientWidth:瀏覽器視窗的寬度
  • document.body.clientHeight:瀏覽器視窗的高度
例題 9-2-1:根據螢幕的大小,自動規範新開視窗的大小
  1. 新增 unit09-2-1.php 的檔案,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 加入 unit09-2-1.php 當中,並先查看瀏覽器顯示的狀態。
  3. 因為可能會有不同的函數使用到螢幕的寬度與高度,因此宣告 mysx, mysy 變數,分別代表螢幕的寬與高。
  4. 使用 window.onload = mymain; 設計視窗載入之後主動執行 mymain 函數。
  5. 在 mymain() 函數內設計:
    • 取得 mysx 為螢幕的寬度,注意,需要是整數的數值
    • 取得 mysy 為螢幕的高度,注意,需要是整數的數值
    • 宣告 mycx 為視窗的寬度,注意,需要是整數的數值
    • 宣告 mycy 為視窗的高度,注意,需要是整數的數值
    • 取代 res 這個 id 元素的內部 HTML 資料,顯示出螢幕的 (寬, 高) 以及視窗的 (寬, 高) 數值。
    • 使用 addEventListener 的方式,讓 opennew 在滑鼠 click 時,可以啟用 opennew 函數。
  6. 在 opennew() 函數內設計:
    • 設計 features 變數,變數內容為類似『 left=800, top=30, width=800, height=300 』之類的資料, 其中 left, top 為新開視窗的左上角定位點,而 width, height 則是視窗的長與寬。
    • 以上數的方式,啟動一個名為 mywin 的新視窗,且不需要給予 url 的數值。關於 window.open 的方法, 可以參考 7.1 小節的介紹。
    • 透過 document.write 的方式,將 features 的內容寫入到新視窗上。
探索螢幕與視窗大小的相關性

因為螢幕的寬、高指的是跟螢幕有關,實際上,我們設計網頁內容,還是需要與瀏覽器視窗、網頁內容有關才對! 所以,比較經常會用到的『座標』,大概就是整體網頁的位置: pageX, pageY,以及相對於瀏覽器可視範圍內的 clientX, clientY 兩個座標才對。

如果想要讓你的瀏覽器內的網頁 (document) 能夠進行滑鼠移動事件的偵測,可以寫這樣:

document.onmousemove = mydetect;
function mydetect(mye) {
	var myevent = mye || window.mye;
	var x = myevent.clientX;
	....
}

在 mydetect 函數中,傳來的變數 mye 則是當時的滑鼠特性,透過 myevent 抓取成為視窗參數, 新的瀏覽器可以直接將上述的 mye 當作是滑鼠事件傳輸者,但是如果是舊版的 I.E. 瀏覽器,就得要透過 window.mye 來取得參數。 之後再透過 clientX, clientY 等,即可取得座標了。

例題 9-2-2:應用滑鼠移動事件,取得滑鼠游標的座標
  1. 新增 unit09-2-2.php 的檔案,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 加入 unit09-2-2.php 當中,並先查看瀏覽器顯示的狀態。
  3. 在 style 樣式內,增加幾個特殊的定義:
    • 針對 body:新增高度為 3000 像素
    • 針對 res:位置為固定 (fixed)、定位點左側 0、定位點底端 0、給予 1 像素的框線
  4. 增加整份文件 (document) 的滑鼠移動事件,指定處理 mymain 函數。
  5. 在 mymain() 函數內:
    • 定義一個名為 e 的傳輸變數名,亦即 function mymain(e) 的語法
    • 定義名為 myevent 的變數,取得 e 的參數
    • 設定 x, y 分別取得 myevent 的 clientX 與 clientY
    • 設定 xx, yy 分別取得 pageX 與 pageY
    • 讓 res 的 innerHTML 呈現兩種座標。
探索滑鼠座標
  • 拖曳行為 (drag and drop)

將資料在網頁上面拖曳,而不是單純的 keyin 資料,會讓使用者的界面看起來直覺很多!Javascript 搭配 HTML5 的 API, 有相當多的拖曳資料可供參考,不過,在這個小節裡面,我們先針對原生的 JavaScript code 來看看如何讓一個圖片或方塊, 可以在整個網頁上面進行拖曳的行為。

所謂的『拖曳 (drga and drop)』行為,其實就是『(1)當滑鼠點擊下去時,若點擊的就在圖片上,(2)然後沒有放開滑鼠按鍵並進行移動時, (3)就將圖片的座標根據與滑鼠的座標相對位置進行移動』,大致上就是這樣而已。而要達成這樣可以在網頁上面胡亂移動的規則,就得要 CSS 的 position 為 absolute (絕對位置) 才能提供這樣的功能。

因此,在實做全部都是 JavaScript code 的拖曳時,得要針對整個網頁 (document) 去監聽滑鼠事件,而不是針對圖片或方塊物件去監聽滑鼠事件, 這個行為得要注意到!那怎麼知道滑鼠點擊的時候是剛好在圖片上呢?其實可以透過底下的位置去討論:

探索滑鼠座標

所以,滑鼠座標在 (fx, fy) 以及 ((fx+fw),(fy+fh)) 之間時,就是按住該元件了!那滑鼠座標可以使用 .clientX 與 .clientY 來取得, 這樣就能夠了解座標位置系統,來得到我們所需要的環境!接下來,就是 mousemove 的事件,讓元件的 left 與 top 樣式進行數據的變更, 基本上,應該就能夠進行物件的拖曳行為了!來玩一下:

例題 9-2-3:應用滑鼠移動事件,設計物件的拖曳 (drag and drop) 行為
  1. 新增 unit09-2-3.php 的檔案,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 加入 unit09-2-3.php 當中,並先查看瀏覽器顯示的狀態。
  3. 設計讓整個網頁載入後,就立刻執行 mymain 函數 (不要在 body 裡面新增的用法)
  4. 宣告全域變數,包括 myfig, myfig2, res, mx, my, fx, fy, diffx, diffy 等
  5. 宣告全域變數,包括 mcoor, fcoor 兩個未來會寫座標的變數
  6. 宣告 figgo = 0 這個全域變數,用來設計『是否有按到目標圖案』的設計,預設值 0 為沒有按到。
  7. 增加 document 的 addEventListener ,當發生 mousemove 事件時,呼叫 mmove 函數 (移動控制)
  8. 增加 document 的 addEventListener ,當發生 mousedown 事件時,呼叫 mdown 函數 (判定是否按到圖案)
  9. 增加 document 的 addEventListener ,當發生 mouseup 事件時,呼叫 mup 函數 (判定不要再移動)
  10. 設計第一個函數, mymain() 函數,內容大致上有:
    • 取得 myfig 元素的控制權,指定為 myfig 這個全域變數
    • 取得 myfig2 元素的控制權,指定為 myfig2 這個全域變數
    • 取得 res 元素的控制權,指定為 res 這個全域變數
    • 額外指定 myfig.style.left 為 0 ,且 myfig.style.top 為 100px 的模樣
    • 增加 myfig 的 mouseover 事件監聽,會主動去呼叫 fover 的函數
  11. 處理 fover() 函數的設計,這個函數的目的在處理 (1)取得目前圖片的座標 (2)替換 res 元素的 innerHTML 內容,所以:
    • 因為圖片的座標會一直被呼叫,所以額外執行 figxy() 這個函數,此函數等等設計
    • 變更 res 的 innerHTML,內容為 fcoor 與 mcoor 這兩個座標資料 (等等加入)
  12. 設計 figxy() 函數,重點在取得圖片的座標位置:
    • 取得 myfig.style.left 的『整數』型態,然後將數值帶給 fx 變數
    • 取得 myfig.style.top 的『整數』型態,然後將數值帶給 fy 變數
    • 設計 fcoor 內容為: "(" + fx + ", " + fy + ")"
  13. 開始處理 mmove(e) 這個函數,這個函數的目的 (1)取得滑鼠座標 (2)若有按下圖片時,變更圖片位置。這個函數需要與 mdown 搭配才能執行。
    • 設計 mye 這個變數為 e 的內容
    • 透過 mye.clientX 來設計 mx 變數內容,同理設計出 my 變數內容
    • 設計 mcoor 為 "
      (" + mx + ", " + my + ")"
    • 判斷,當 figgo 為 1 時,進行如下行為:
      • 執行 fover 變更座標值
      • 修改 myfig 的 left,成為 (mx+diffx) + "px" 的位置,其中 diffx 為上面談到的滑鼠與圖片座標之間的相對位置
      • 修改 myfig 的 top,成為 (my+diffy) + "px" 的位置
  14. 開始處理 mdown() 函數:
    • fw 為 myfig2 的寬度取整數
    • fh 為 myfig2 的高度取整數
    • 依據本題目上方的說明,設計出 mx 與 my (滑鼠座標) 在圖片上面的定義,並指定:
      • diffx 為 fx - mx
      • diffy 為 fy - my
      • figgo 為 1
  15. 最後才處理 mup() 函數
    • figgo 設定為 0
進行滑鼠拖曳的行為

經過測試之後,總是覺得怪怪的!為什麼會這樣呢?這是因為 HTML5 以後,瀏覽器廣泛的加入了很多的 API 支援,因此瀏覽器本身也具有拖曳的支援! 因此,當我們使用純 JavaScript code 進行拖曳時,瀏覽器就會同時進行兩個拖曳的行為 (Javascript code 與瀏覽器自己支援的拖曳), 所以就產生上面的困擾!明明是可以拖曳,但是就是怪怪的。此時,只要在 mymain 函數內,加上這個段落取消瀏覽器自己的拖曳功能,就能成功了!

myfig.ondragstart = function() { return false;};

意思就是說,取消 dragstart 這個事件的監聽之意!單純使用 JavaScript 自己的拖曳程式碼!現在再次拖曳看看,應該就可以發現順暢的移動了!

9.3: HTML5 的拖曳事件處理

在上述的滑鼠拖曳事件中,可以簡單的進行拖曳的行為。但是,如果想要將某個元件直接丟進某個元件當中,舉例來說, 剪刀石頭布用拖曳的方法來處理,而不是用點擊的方式,這樣要如何處理?如果還是使用這些 mouse 事件,恐怕得要撰寫更多的程式碼才行! 真是傷腦筋!

事實上,HTML5 是有提供滑鼠拖曳功能的 API 的,這個稱為『 drag and drop 』事件,跟上頭一樣,這是瀏覽器的標準事件了! 所以,你可以透過『 ondragstart = function() {}; 』之類的方式來啟用事件的監聽與處理喔!

整個拖曳的行為,基本的項目其實是:拖曳來源物件、拖曳目標物件這兩個,然後你必需要設計:

  • 拖曳來源物件:拖曳來源物件需要可以被拖曳,因此,需要在 HTML 裡面增加『 draggable="true" 』這個屬性才行。
  • 拖曳目標物件:一般瀏覽器預設不准資料被拖曳到任何元件上,因此,你就必需要指定 dragover 事件,將該事件取消不准拖曳的預設參數, 亦即在物件上加上 .preventDefault() 這個方法即可。

上面兩個資料是必要條件!另外,與滑鼠拖曳事件不同,這個 HTML5 的 drag 事件其實是『將某個元件丟到某個元件去』的意思, 並不是讓你在瀏覽器上面亂丟東西!這個很重要喔!完全是不同的狀態!也因為這個東西是一個蘿蔔一個坑,因此很適合製作一些簡易的遊戲, 讓使用者直接拖曳資料或答案到正確的方塊裡面去!這是相當好用的工具!

  • 拖曳行為

拖曳行為的事件,如果針對拖曳來源物件與拖曳目標物件來說,會影響到的事件分別是:

事件拖曳來源拖曳目標
dragstart(開始拖曳事件)V
drag(拖曳中)V
dragenter(進入目標)V
dragover(經過目標)V
dragleave(離開目標)V
drop(滑鼠按鍵放開)V
dragend(結束拖曳)V

一定要設計的拖曳事件,必須要有 dragstart, dragover, drop 這三個事件,那麼如何取得被拖曳的物件呢?其實可以透過底下的方式來設計:

document.addEventListener('dragstart', mstart);
function mstart(mye) {
	mye.dataTransfer.setData("text", mye.target.id);
	return false;
}

當產生 dragstart 事件時,就呼叫 mstart 函數,在 mstart 函數內規範 mye 事件物件,這個物件的目標屬性 (target) 裡面的 id, 就可以將該 id 名稱寫入到該事件傳輸時的資料當中 (setData),使用的函數為 dataTransfer ,方法用 setData!注意大小寫。 事實上,幾乎所有的拖曳事件,通通是透過 dataTransfer 函數來處理的!而這個函數可以運用的方法主要有:

方法說明
setData(format,data)基本的資料格式為文件 (text),也可以是網址 (url),後面的 data 指的就是放入的資訊, 一般而言,大部分都用事件的 target.id 或 target.tagName 等等,分別取得 id 或 tagName(標籤名),也分別取得單一物件或陣列物件。 一般使用上,大多數還是使用 id 比較多!比較好整理。
.setData("text",event.target.id);
getData(format)與 setData 相呼應,主要用在 drop 事件當中。就是將 setData 傳來的資訊抓下來, 以上面的範例來說,當使用 getData("text") 時,就會回傳該物件的 id 名稱,之後我們可以透過 document.getElementById 來取得這個元素的控制權, 最終可以將該元件直接以 event.target.appendChild(這裡) 來新增物件,這樣就有拖曳的效果了!
.getData("text")

不過我們還是得要注意,一般一開始,瀏覽器除了圖片 (img, image) 之外,基本上不允許其他元件在瀏覽器的其他元件上面進行拖曳的! 而同時,瀏覽器可能也會進行一些額外的重導向 (redirect) 的行為,這些行為可能都會導致整個拖曳的事件變得怪怪的。 因此,底下兩個函數,很多時候也需要注意一下,用在拖曳的事件裡面才能讓拖曳順利進行下去:

  • event.preventDefault():取消瀏覽器預設不能進行『拖曳放置』的行為,這才能讓原本不被支援的元件,也能進行拖曳!
  • event.stopPropagation():取消瀏覽器的 redirect 行為,可以避免瀏覽器過多的協助,反而造成拖曳事件的問題。

接下來,讓我們先來測試一下簡單的拖曳,亦即單一容器放置與單一可拖曳元件:

例題 9-3-1:單一物件拖曳與單一拖曳目標
  1. 新增 unit09-3-1.php 的檔案,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 加入 unit09-3-1.php 當中,並先查看瀏覽器顯示的狀態。
  3. 設計讓整個網頁載入後,就立刻執行 mymain 函數 (不要在 body 裡面新增的用法)
  4. 設計 mymain() 函數:
    • 使用 paper 變數取得 paper 元素的控制權
    • 使用 mytarget 變數取得 mytarget 元素的控制權
    • 增加 paper 的監聽事件,事件名稱為 dragstart,會處理 mstart 函數
    • 增加 mytarget 的監聽事件,事件名稱為 drop,會處理 mdrop 函數
    • 增加 mytarget 的監聽事件,事件名稱為 dragover,會處理 mover 函數
  5. 設計 mstart(mye) 函數,開始處理拖曳事件的資料提供
    • 使用 mye.dataTransfer.setData ,格式為 text,資料內容為 mye.target.id,提供物件的 id 資料
    • 回傳 false
  6. 設計 mdrop(mye) 函數,處理當拖曳物件放到此處並放下 (drop) 的行為:
    • 先以 mye.preventDefault() 方法,取消瀏覽器預設不能拖曳的問題
    • 使用 mye.dataTransfer.getData,取得剛剛傳來的 id 資訊
    • 新增變數名稱 data,內容為透過 document.getElementById 取得上述的 id 資訊
    • 最終使用 mye.target.appendChild(data) 新增這個元素到方塊上
  7. 設計 mover(mye) 函數,處理拖曳物件預設狀態的取消:
    • 使用 mye.preventDefault() 方法
    • 回傳 false
滑鼠單一拖曳事件

關於上面範例當中的許多資料,你可以這樣看:

  • mye;就是事件本身的意思
  • target:而這個事件本身如果使用 target 時,就變成這個事件指向的那個元件。所以,在 mstart(mye) 當中的 mye.target ,就會代表被拖曳的那個元件,因此, mye.target.id ,就變成是,被拖曳的那個元件的 id 名稱。
  • .setData:代表塞入拖曳的行為當中的那個資料,就是一份文字資料,負責傳輸 id 名稱而已!

最終,你就可以將左側的圖形拉動到右側去放下。這就是最簡易的 HTML5 滑鼠拖曳事件的 API 使用方式。不過,你會發現, 怎麼只能從左邊拖曳到右邊呢?這是因為我們只有放行右側的方塊具有 drop 與 dragover 的事件功能,其他的方塊沒有給予啊! 那,能不能給予不同的方塊都有可以放置的功能?也不是不行,透過底下的方式來處理即可。

  • 多重拖曳行為

透過底下的方式來進行拖放事件的監聽:

# 全部頁面都可以拖曳,不過,不建議這樣做!
document.addEventListener("dragover", mover);
document.addEventListener("drop", mdrop);

這樣整個頁面 (document) 的元件都可以讓你拖曳了。不過,這樣做不太好,這是因為我們用這個 API 的拖曳行為,大部分都是為了一個蘿蔔一個坑, 用來設計檔案上傳、資料處理等等,倒不是單純為了好看而已。所以,建議還是針對某個網頁元件來進行設計會比較好。那如何處理不同元件的設計呢?

也許很簡單喔!你可以在 drag source 拖曳來源上面通通給予一個名為 draggable 的名稱,而在允許放置的方塊容器上面 (drop target) 給予 dropped 名稱, 然後透過抓取這兩個名稱,將各自的陣列給予各自的監聽,就能夠達到這些目的了。不過要注意,一般放置行為 (drop) 很可能會影響到拖曳物件, 來看底下這個範例:

例題 9-3-2:多重拖曳與放置目標
  1. 將 unit09-3-1.php 複製成為 unit09-3-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 修改到 unit09-3-2.php 當中,並先查看瀏覽器顯示的狀態。
  3. 在 mymain() 函數內,修改這些資料:
    • 將原本的 paper, mytarget 變數名稱與內容刪除
    • 重新抓取名為 dsource 的變數,內容為透過名稱 (getElementsByName) 取得 draggable 的元件
    • 重新抓取名為 dtarget 的變數,內容為透過名稱取得 dropped 的元件
    • 針對 dsource ,使用 dsource.forEach 的功能,給予變數 data1 ,然後針對 data1 給予個別的 dragstart 事件監聽, 使用 mstart 函數處理
    • 針對 dtarget ,使用 dtarget.forEach 的功能,給予變數 data2 ,然後針對 data2 給予個別的 drop 與 dragover 事件監聽, 分別使用 mdrop 與 mover 函數處理
  4. 其他資料保留原始樣式即可。
滑鼠多重拖曳事件 滑鼠多重拖曳事件

接下來開始測試之後,你就會發現到,所有的拖曳可以在兩個容器之間傳輸了!

  • 避免元件被覆蓋的困擾

不過上面的範例有點困擾喔!你可以測試將剪刀、石頭、布的圖片互相放置看看,咦!怎麼圖片會被吃掉?這是因為我們的事件處理當中, 有開放元件拖曳,結果似乎也導致這些資料可以互相放置到該元件內!就導致這個困擾了。如果你不想要讓元件之間可以替換或者是發生這方面的困擾, 或許也能嘗試加入停止動作來解決。不過,這也許並不是最佳解決方案,只是針對這個案例,可以這樣處理 (多個元件多個坑的情境下)。

# 將這幾行放入 dsource.foreach 內,避免 drop 在拖曳元件上面發生問題!
data1.ondrop = function (mye) {
	mye.stopPropagation();
}

如同前面所述,這個 stopPropagation() 方法,可以讓瀏覽器停止某些轉遞的動作,所以,我們可以將這個行為加入到圖片的放置過程中, 就可以避免圖片被吃掉!讓我們的拖曳行為會比較合理。

  • 一個蘿蔔一個坑

如果用在設計遊戲上,通常遊戲的關卡,他偵測的題目、項目一般是固定的,你可以填寫的答案也是固定的一項, 不會讓你填寫兩項資料啦,所以,當然需要一個容器裡面放置一個元件而已!放置太多反而會出問題。

因此,你在設計這方面的資料時,可能需要考量到,當把兩個物件同時放置到同一個方塊時,會出現什麼問題? 大致上這方面的問題解決的方式通常是::

  • 若有圖片,且指標到該圖片,就讓拖曳物件的圖片與拖曳目標的圖片對調
  • 若有圖片,且指標到容器方塊 (非圖片),同樣讓拖曳目標內的子元素(就是圖片)抽換到拖曳目標的容器內。

來實際測試一下,就能知道問題了:

例題 9-3-3:一個蘿蔔一個坑的設計方式
  1. 新增檔名 unit09-3-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 修改到 unit09-3-3.php 當中,並先查看瀏覽器顯示的狀態。
  3. 讓網頁載入之後,就能立刻執行 mymain 函數
  4. 宣告名為 mysource 的變數,等等要用來作為拖曳物件的上層物件之用。
  5. 建立 mymain() 函數,主要在取得 (1)拖曳物件與放置容器的控制權 (2)設定拖曳監聽事件等任務:
    • 分析網頁資料,可以允許被拖曳的圖片都有 id 以及 name,且 name 都是 draggable,因此, 設定變數名稱 dsource ,內容為使用 getElementsByName ,針對 draggable 的名稱來抓取元素控制權, 最終 dsource 應該會是陣列
    • 同上,設計 dtarget 抓取可放置物件的容器,亦即名稱為 dropped 的物件,同樣的, dtarget 也會是陣列。
    • 加上 dsource 陣列物件的 forEach 方法,設定函數的變數名稱為 data1,而 data1 新增的事件監聽中, (1)dragstart 事件,啟用 fstart 函數, (2)drop 事件啟用 fdrop 函數。
    • 加上 dtarget 陣列物件的 forEach 方法,設定函數變數名稱為 data2,而 data2 監聽事件中, (1)drop 事件啟用 cdrop 函數, (2)dragover 啟用 cover 函數
  6. 建立 fstart(event) 函數,主要針對圖片拖曳,給予圖片的 id 資料傳送
    • 使用 dataTransfer 的 setData 方法,傳送圖片的 id 資料
    • 因為很可能要進行圖片對調,因此,設定 mysource 變數,內容為 event.target.parentNode 這個物件!
  7. 建立 fdrop(event) 函數,當圖片被當成容器放置的情境下,我們需要讓圖片對調才行!
    • 先增加 .preventDefault() 與 .stopPropagration() 兩個函數,避免瀏覽器的干擾
    • 設計 data 變數,內容使用 dataTransfer 找出 getData 的圖片 id,然後根據 id 去取得該圖片的物件,所以,最終 data 是個圖片物件。
    • 因為要變更圖片,所以設計 mytarget 變數,內容為取得目前這個 target 的 id 的元素控制權,因此 mytarget 也是個圖片物件
    • 讓事件目標的上層節點,就是被放置的圖片所在的父節點,增加一個子元件,加入 data 這個子元件在裡面
    • 是拖曳來源 (就是 mysource),增加一個名為 mytarget 的物件在裡面即可。
  8. 建立 cdrop(event) 函數,當拖曳來源放置到這個容器下,要先看有沒有圖片?若有,回傳,若沒有,直接放置
    • 先增加 .preventDefault() 與 .stopPropagration() 兩個函數,避免瀏覽器的干擾
    • 設計 data 變數,內容使用 dataTransfer 找出 getData 的圖片 id,然後根據 id 去取得該圖片的物件,所以,最終 data 是個圖片物件。
    • 判斷 event.target.childNodes.length 是否為 0,如果不是 0 ,代表這個方塊內有其他元件存在!
      • 我們這裡只允許一個元件。因此,在這個判斷式裡面,讓 mysource 增加一個子元件,子元件的內容應該就是 event.target.childNodes[0] 這個被放置方塊內的第一個子元件!
    • 直接讓此 target 新增一個子元素,內容就是 data
  9. 建立 cover(event) 函數,內容只要填入 preventDefault() 即可!主要是要讓可被放置的元件被拖曳經過時, 可以順利顯示被拖曳的圖示。
滑鼠多重拖曳事件

在這個案例中,你仔細分析 fdrop 與 cdrop 兩個函數,會發現在當中的 target 代表的意義實在是不一樣,同樣是 event.target, fdrop 代表一個圖片,而 cdrop 代表一個 div 喔!要注意!要注意!

另外,你可能會想要將滑鼠拖曳事件與 HTML5 的 drag and drop 結合在一起來玩,不過可惜的是,這個物件要設定成什麼 position 呢? 會有許多問題!因此,最多只能使用 dataTransfer.setDragImage 的函數來處理。不過,目前這個函數的支援還不怎麼完整, 留待未來開發囉!

9.4: 鍵盤事件處理

有很多時候我們也是會用到鍵盤來做一些遊戲或者是互動,並不是只能用滑鼠而已。過去最簡單的互動遊戲,當然就是打小飛機! 這時,都會用方向鍵控制砲台來瞄準飛機,將飛機轟炸掉這樣。當然,砲台不能夠被打到!那如果沒有這麼複雜, 我們單純要來打小蜜蜂,可以一步一步來完成一下。

  • 鍵盤事件最重要的就是分析按下什麼按鈕了?

通常,我們會透過 keydown 來分析,到底使用者按下什麼按鈕。先來理解一下,如果按下的按鈕是非數值或文字的, 通常我們會用到的可能就是方向鍵、Tab 按鍵、ctrl、shift、alt 等,這些按鈕基本上都有對應的號碼 (keyCode), 大概是這樣對照的:

按鍵keyCode
倒退鍵 (backspace/delete)8
Tab9
Enter13
Shift16
Ctrl17
Alt18
escape[esc]27
空白鍵 (spacebar)32
PageUp33
PageDown34
End35
Home36
左鍵 (left arrow)37
上鍵 (up arrow)38
右鍵 (right arrow)39
下鍵 (down arrow)40
插入 (Insert)45
刪除 (del)46
數值 048
數值 149
數值 250
數值 351
數值 452
數值 553
數值 654
數值 755
數值 856
數值 957
英文 A65
英文 B66
英文 C67
英文 D68
英文 E69
英文 F70
英文 G71
英文 H72
英文 I73
英文 J74
英文 K75
英文 L76
英文 M77
英文 N78
英文 O79
英文 P80
英文 Q81
英文 R82
英文 S83
英文 T84
英文 U85
英文 V86
英文 W87
英文 X88
英文 Y89
英文 Z90

現在,就讓我們來測試一下,看看如何處理砲台的移動吧!

例題 9-4-1:製作一個可以用鍵盤移動的滑動小砲台
  1. 新增檔名 unit09-4-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 這個檔案內容 修改到 unit09-4-1.php 當中,並先查看瀏覽器顯示的狀態。
  3. 讓網頁載入之後,就能立刻執行 mymain 函數
  4. 宣告兩個全域變數,一個名稱為 fire,目的在取得 fire 元件控制權,一個是 step,初始值為 3,這個是砲台移動像素
  5. 撰寫 mymain() 函數
    • 建立 fire 物件,內容為 fire 元件
    • 修改 fire.style.left 數值為 275px
    • 增加 keydown 的事件監聽,啟用 kdown 函數
    • 增加 keyup 的事件監聽,啟用 kup 函數
  6. 撰寫監聽鍵盤按鍵的 kdown(event) 函數
    • 如果 event.keyCode 為 37 ,那麼就執行 goleft 函數
    • 如果 event.keyCode 為 39 ,那麼就執行 goright 函數
    • step 重新指定為 6 ,這說的是,如果沒有 kup 函數,那麼砲台滑動的速度將會增加一倍
  7. 撰寫向左移動的 goleft 函數
    • 設計 x 變數,內容為 fire.style.left 的『整數』型態
    • 因為向左,所以 x = x - step
    • 當 x 碰到牆壁 (位置為 0) 時,就不能向左移動
    • 變更 fire.style.left 到最新的位置上
  8. 撰寫向右的 goright 函數,都跟 goleft 一樣,但是 (1)必須為加上 step (2)x 碰到牆壁應該是無法往右移動
  9. 撰寫監聽鍵盤按鍵的 kup(event) 函數
    • 因為鍵盤放開了,所以 step 調整回初始值的 3 即可。
砲台移動囉!

有了砲台之後,開始準備要處理發射砲彈,發射砲彈時,如果需要連續發射,那麼就得要使用不同的元件產生,此時恐怕會有重複名稱的問題。 因為每發砲彈要讓它自己跑,就得要有獨立的 id name 才行。所以,我們打算每次發射砲彈,就在 idname 上面 +1 這樣的型態來測試看看。 如此,就不會有砲彈名稱重複的狀況。只是,在設定 timeout 的函數時,得要特別注意到 id name 是可變動的,因此得要加上單引號來處置。

例題 9-4-2:準備發射砲彈
  1. 將 unit09-4-1.php 複製成為 unit09-4-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 如上所述,要了要累加 id name,因此,設定一個全域變數,名為 firenu,初始值為 0 即可。
  3. 因為砲彈只會在 playing 元件中跑,因此,同樣設定全域變數名為 playing,預計取得 playing 元素控制權。
  4. 修改 mymain 函數,新增一行,取得 playing 元素的控制權,使用 playing 變數名稱即可。
  5. 修改 kdown 函數,增加 keyCode 為 32 (空白鍵) 時,呼叫 firenow 函數,該函數在產生砲彈以及砲彈的 id 還有 style 等相關資訊。
  6. 新增 firenow 函數,目的在產生新的砲彈,並提供該砲彈初始化的參數:
    • 取得 fire 元素的 x 座標,並且加上 25 ,這是因為砲彈要從 fire 元件的中央發出
    • 設定 y 值初始化為砲台頂部,大概在 435 即可 (因為扣掉砲台高度)
    • 設定 dot 區域變數,目的為新增元件『 document.createElement('P') 』
    • 將 dot 變成 playing 的子元素 (appendChild)
    • 設定 dot 的大小與形狀之 style,包括 width 與 height 都是 2px,框線實心黑色,padding 與 margin 都是 0 才好。
    • 設定 dot 的位置的 style,包括 position='absolute', left=x+'px', top=y+'px' 等參數
    • 讓 firenu 增加 (+1)
    • 設定 dot.id 這個 id 名稱,變成是 'fire' + firenu 這樣的名稱情境
    • 設定區域變數 target,內容就是 dot.id 即可。
    • 呼叫砲彈自動往上跑的 firego(target) 函數,記得,一定要提供 id name,所以需要有 target 變數名稱在當中。
  7. 新增 firego(target) 函數,目的在 (1)取得 target 的名稱元件 (2)變更該元件的位置 (3)呼叫 timeout 反覆執行 (4)超出邊線就移除該元件
    • 設定區域變數 myobj,內容為 target 這個 id 名稱的元素控制權
    • 取得 myobj 的 x 座標 (left) 以及 y 座標 (top),並且取整數
    • 讓 y = y -2 ,這樣座標就會往上跑
    • 進入 y 值的條件判斷:
      • 當 y >= 0 時,(1)讓 myobj 的 top 變更維新的 y 值,(2)設定 setTimeout 函數, 內容應該要是:『 setTimeout("firego('" + target + "')",10); 』這種格式才行!否則會失敗
      • 若 y 值非上述條件,就從 playing 裡面移除 myobj 元件
發射砲彈

你可以測試一下,就能發現到,可以連續射出砲彈囉!相當有趣的呢!

  • 解決 keydown 事件的邏輯問題

在上面的案例中,如果你按下方向鍵不放,然後按下空白鍵,你可以發現到,怪了,砲台竟然停止了!這跟一般的遊戲想法不太一樣啊! 如此會造成按下方向鍵,卻沒有辦法連續移動的問題。這怎麼解決呢?

透過 google 大神,找到一篇有趣的討論,他的想法是,先宣告整體視窗的一組陣列變數,並且將該陣列內容以布林值來規範。 當按下某按鍵,該按鍵的代碼就會變成 true,放開之後就會是 false。之後每 0.05 秒偵測一次,就能夠抓到確定是按下 (true) 卻沒有作用的按鈕, 並予以開始提供執行。

只是,我們的連續移動速度會變快,從 3 像素增加到 6 像素,而 0.05 秒似乎是鍵盤連續按下的頻率,因此,我們可能需要加倍偵測的頻率, 所以,最終偵測的時間頻率應該要是 0.025 秒比較好。實際撰寫如下:

例題 9-4-3:發射砲彈後,砲台還能夠持續移動
  1. 將 unit09-4-2.php 複製成為 unit09-4-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 如上所述,要了要累加 id name,因此,設定一個全域變數,名為 firenu,初始值為 0 即可。
  3. 在全域變數的環境中,指定名為 window.keysdown 的陣列變數,其內容主要規範 keyCode 為 37, 39, 32 的資料,寫法有點像底下這樣:
    window.keysdown = {
    	37: false,
    	39: false,
    	32: false
    }
  4. 在 kdown(event) 函數中,增加一段程式碼,可以讓 keysdown 的變數變成按下 (true) 的情境:
    if ( window.keysdown.hasOwnProperty(event.keyCode)) 
    	window.keysdown[event.keyCode] = true;
    
  5. 同樣的,在 kup(event) 函數中,又得要將該按鈕取消 (false):
    if ( window.keysdown.hasOwnProperty(event.keyCode)) 
    	window.keysdown[event.keyCode] = false;
    
  6. 在全域變數的環境中,增加一個 timer,內容會有點像底下這樣,每 25 毫秒偵測一次按鍵按下否?
    setInterval (function() {
    	if ( window.keysdown[37] && step == 3 ) goleft();
    	if ( window.keysdown[39] && step == 3 ) goright();
    }, 25);
    

再次測試後,你就可以發現,鍵盤的移動要順暢許多了!

  • 產生飛碟之後,開始打掉飛碟

再來要設計的,就是在遊戲區塊裡面,產生一堆會被打掉的飛碟~這些飛碟必須是隨機產生的,所以又得要計算位置了。 此外,是隨機產生的,因此就得要定義這些飛碟的元素名稱,因為是一堆飛碟,所以可以透過名稱的定義來處理,不需要唯一的 id 名稱。 這個 name 的參數設定比較麻煩,不像 id 設定比較簡單。 name 的設定方式為:

myobj.setAttribute("name", "新的名稱");

隨機產生這些飛碟之後,再來根據砲彈的移動方向,找到 (1)飛碟的面積位置、 (2)砲彈的位置,如果砲彈的位置在飛碟的面積內, 那就將砲彈與飛碟通通移除!而且不執行 timeout 喔!否則會產生找不到 id 的困擾。那就來處理看看:

例題 9-4-4:開始射擊飛碟
  1. 將 unit09-4-3.php 複製成為 unit09-4-4.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
  2. 建立新的名為 createufo() 的函數,這個函數要產生在遊戲區域上方 100px 內的空間,隨機產生 20 個飛碟
    • 建立 maxx 變數,內容為取得 playing 元素的寬度 (clientWidth)
    • 使用 for 迴圈,定義 i 值在 0 到 19 以內,然後進行:
      • 隨機產生 x 軸,範圍在 0~maxx-50 之間,因為等等設計飛碟有 50 個像素寬
      • 隨機產生 y 軸,範圍在 0~100 之間,x 與 y 都要取整數
      • 建立區域變數 ufo,內容為產生新的元素,元素標籤為 P
      • 將 ufo 增加入 playing 的子元件
      • 設計 ufo 的大小與位置的 style ,包括 position='absolute', left 是 x+"px",top 是 y+'px',給予 border, 給予 bacgroundColor,設定 padding 與 margin 均為 0,設計寬度與高度分別是 50x10 像素。
      • 設計 ufo 的 name 成為 'ufo' (用上面談到的 .setAttribute 處理)
  3. 在 mymain 裡面增加執行 createufo() 函數,然後重新載入,有沒有看到 ufo 了?
  4. 在 firego 這個函數內,設計修改『砲彈碰撞到飛碟』的事件,請在判斷 y 是否大於 0 的條件判斷式裡面,在執行了砲彈的 top 設定後, 加入如下的設計:
    • 設計區域變數 ufo,內容為取得名為 'ufo' 的名稱 (getElementsByName) 的元件,因此 ufo 會是陣列
    • 設計區域變數 hit ,初始值為 0 ,這個是定義『是否被射擊到』,0 是沒有, 1 是射擊到了。
    • 進入 for 迴圈,定義 i 為 0 到小於 ufo.length 之間,然後:
      • 定義 x1 為 ufo[i] 的 left 座標取整數
      • 定義 x2 為 x1+50 (為啥?)
      • 定義 y1 為 ufo[i] 的 top 座標取整數
      • 定義 y2 為 y1+10 (為啥?)
      • 判斷, x 位在 x1~x2 之間,且 y 也在 y1~y2 之間,就代表碰撞然後會:
        • 設定 hit = 1
        • 移除 myobj 以及 ufo[i]
        • 跳脫迴圈
    • 重新定位 setTimeout,需要在 hit 為 0 的時候才執行這個項目 (用 if 來定義)
發射砲彈

9.5: 課後作業

  • 9-5-1、寫出一個敲擊怪物的小遊戲

這個網頁的內容為範本,建立一個初始化的環境,你可以自行修改網頁內容的 CSS 樣式, 讓它變得比較適合你自己的樣式。假設檔名為 unit09-5-1.html,這個檔案內,會有許多的全域變數,等等請自行決定是否要使用全域變數。 另外,因為網頁內會產生很多的怪物,因此,可能會使用到多個陣列在內,請自行宣告某些全域變數為陣列即可。

先設定網頁載入就執行的函數,我們課堂上經常稱為 mymain 的那個函數。

設定時間倒數計時器: 畫面上方的兩個方塊中,右邊方塊就是用來倒數的用途!你可以寫一個名為 showtime() 的函數,同時進行:

  • 將一開始就設定的全域變數 (假設為 timeout),讓該數值 - 0.1 (亦即每 0.1 秒更新顯示一次)
  • 將數值帶給 mytime 這個元素的 innerHTML 裡面。
  • 回去更新 mymain,設定一個新的 setInterval,內容為每 0.1 秒執行一次 showtime() 函數。

設定怪獸產生器: 畫面中央的 area 元素要放置怪獸,且這個怪獸會在大約 3 秒內自己隨機找一個時間點出現,並不是固定 3 秒出現一次! 是『 3 秒內隨機自己跳出來』的意思!假設你寫一個名為 monster(nu) 的函數,這個函數可能會被好幾個計數器呼叫, 因此,建議加上一個變數,可以讓每個怪獸的函數資料獨立運作而不會互相干擾。

  • 先取得 area 這個元素的最大寬、高 (clientWidth, clientHeight),取得這個數值的用途,是需要找出放置怪獸的位置, 所以,假設你的怪物預設大小為 50 像素,因此,上面的數值記得要減去 50 喔!
  • 套用上面的最大值,透過亂數產生器,計算這隻怪獸的位置,產生 x[nu] 與 y[nu] 的數值,這就是每支怪獸的座標。
  • 計算怪獸哪個時間點會跑出來 (設計 gogo[nu] 數值):底下會有一隻計算這隻怪獸出現的函數 (不在這個函數內),該函數將 3 秒鐘切成 30 段, 所以,我們這裡要計算出,怪獸會在 30 段時間的哪一段出現?這裡一樣使用亂數產生器,從 0~29 之間隨機產生一個整數數值, 帶給 gogo[nu] 這個陣列即可。
  • 每支怪獸計時器的初始值 (設計 mygo[nu] 數值): mygo[nu] 的初始值應該是 0。mygo[] 就是每隻怪獸的累加等待時間, 等到 mygo[] 大於 gogo[] 時,就讓怪獸跑出來的意思。這個數值與剛剛的 gogo[] 是有關的!
  • 最後,呼叫顯示怪獸的 showmonster(nu) 函數。

怪獸產生器-隨機產生用:撰寫 showmonster(nu) 函數,處理的方式為:

  • 這隻怪獸已經等待多久了,亦即讓 mygo[nu]++
  • 開始比較 mygo 與 gogo 誰比較大,當 mygo 小於 gogo 時,就設定 0.1 秒之後 timeout,且重複呼叫 showmonster(nu) 這個函數。 要特別注意, setTimeout 當中,函數的寫法可能要變成『 "showmonster(" + nu + ")" 』這樣的格式,才能讓 nu 以實際的數值更新。
  • 若 mygo 大於 gogo 時,就進行:
    • 設計 img 為新的圖片 (image) 物件
    • 分別指定 img.src, img.style.position, img.style.left, img.style.top, img.style.width 等
    • 以 area.appendChild 增加這個物件。
    • 提供被點擊殺到怪獸的監聽事件:增加 img 這個物件的 click 監聽事件,若發生 click 時,執行 killme 函數

讓怪獸產生器生效:就是在 mymain 裡面新增 monster(nu) 的計數器即可。你可以寫一個 for 迴圈,假設指定 5 次, 就執行 setInterval 然後加入類似 monster(0) 這樣的函數名稱來執行即可。先指定 5 次,未來可以自己增加怪獸產生量。此外, 依據之前的測試,讓你的怪獸是 3 秒鐘產生一次。

打怪計數器:增加 killme(event) 函數,這個函數是發生到滑鼠點擊怪獸時,才會發生的事件,因此需要有 event 變數名稱(這個名稱可自己調整)。 目的很簡單,click 到怪獸,就將怪獸元件移除,並且讓計數器 +1 即可。移除怪獸的元件要用 area.removeChild(event.target) 喔!

倒數計時結束:在 mymain 裡面,增加遊戲結束的 setTimeout 函數,這個函數會在時間到了之後 (timeout*1000) , 清除所有的 Interval 計數器,同時收集所有的 img 標籤物件,將 click 的監聽取消 (removeEventListener)。

打怪遊戲
  • 9-5-2、製作會下降的飛碟

以例題 9-4-4 為基底,重新設計一下,讓隨機產生飛碟的數量降低到 10 個左右就好,然後,大概每 3 秒鐘,整個飛碟會網往下移動大約 30 或 50 像素, 然後在原本的地方再次產生新的 10 個飛碟。等到飛碟撞到遊戲區域的底部時,遊戲就算結束。最終結算打掉幾部飛碟,同時結算發射幾發砲彈。 如果可以的話,將飛碟的圖示,從醜醜的方塊,替換成飛碟的形狀,會更好!

打擊飛碟
  • 9-5-3、製作拖曳的配對遊戲

設計數個方塊,等待放置答案,然後在下方提供多個答案,必需要比對成功,才能夠給予正確的答案。例如,如果以太陽系的行星為例, 如下圖的狀態,那麼,當拖動圖片到正確的位置後,按下答題的按鈕,就可以去比對每一個方塊當中的圖片 id 是否正確? 若為正確,就計數加一,否則就不計數。但是,在確定方塊內的圖片 id 前,必需要確認方塊內有沒有東西! 所以,就會得到底下的三個圖示了:這一題不可能會有相同的作法,因為每個人的題目應該都會不一樣喔! 這個星球配對遊戲,就只是個範例而已!

圖片拖曳遊戲 圖片拖曳遊戲 圖片拖曳遊戲

9.6:參考資料