HTML5遊戲開發——骰子游戲(一)

出版圈郭志敏發表於2011-09-15

本章內容

  在畫布上繪圖

  隨機處理

  遊戲邏輯

  表單輸出

引言


     HTML5最重要的新特性中就包括canvas。這個元素為開發人員提供了一個途徑,可以採用一種完全自由的方式繪圖、包含影像以及放置文字。與較早的HTML版本相比,這是一個顯著的改進。儘管在以前的版本中也可以完成一些漂亮的格式化處理,但是佈局往往是四四方方的,而且頁面也缺乏動態性。如何在畫布上繪圖呢?你會使用一種指令碼語言,通常是JavaScript。我將介紹如何在畫布上繪圖,並解釋構建一個名為craps的骰子游戲時需要用到JavaScript的哪些重要特性,比如如何定義一個函式,如何呼叫所謂的偽隨機行為(pseudo-random behavior),如何實現這個特定遊戲的邏輯,以及如何向玩家顯示資訊。不過在深入討論這個遊戲實現之前,首先需要了解遊戲的基本規則。
    
     craps遊戲的規則如下:

    玩家扔一對骰子。我們關心的是兩個骰子的數字之和,所以1和3與2和2是一樣的。兩個骰子的數字之和可以是2到12中的任意一個數。如果玩家第一次丟擲7或11,那麼他就獲勝。如果玩家丟擲2、3或12,那麼他就輸了。丟擲其他結果(4, 5, 6, 8, 9, 10),則會記錄為玩家的點數,然後需要繼續擲骰子。後面再丟擲7就輸了,而如果正好又丟擲玩家的點數則獲勝。對於其他情況,遊戲繼續,並遵循剛才繼續擲骰子的規則。
    
     下面來看這個遊戲具體是怎樣的。圖2-1顯示了遊戲剛開始丟擲兩個骰子的結果。

enter image description here

    儘管這裡不能明顯地看出,不過這個骰子游戲應用每次都使用canvas標記畫出兩個骰子的正面。也就是說並不需要下載骰子正面的影像。

    丟擲兩個1意味著玩家輸了,因為規則中定義第一次丟擲2、3或12就輸了。下面的例子顯示玩家贏了,第一次丟擲了7,如圖2-2所示。

enter image description here

    圖2-3顯示下一次丟擲8。此時不贏也不輸,說明還必須繼續擲骰子。

enter image description here

    下面假設玩家最後再次丟擲8,如圖2-4所示。

enter image description here

    由前面的一系列結果可以看出,這裡只考慮骰子面上的數值之和。儘管這裡是由兩個4決定點數為8,但是丟擲一個2和一個6時也會贏。

這個規則說明遊戲中擲骰子的次數並不總相同。玩家第一次擲骰子可能贏也可能輸,也可能隨後還要丟擲多次骰子。作為開發遊戲的人,其職責就是構建能正常玩的遊戲,而正常玩意味著要遵循規則,儘管這意味著可能要持續不斷地玩下去。我的學生有時認為只有他們贏才說明遊戲能正常工作。其實在一個正確的遊戲實現中,玩家可能贏也可能輸。

  2.2 關鍵需求

  要構建這個骰子游戲,首先需要模擬隨機地丟擲骰子。剛開始,這看起來是不可能的,因為程式設計就意味著要明確指定計算機做什麼。幸好類似於大多數其他程式語言,JavaScript有一個內建的功能,可以生成看起來隨機的結果。有時一些語言會以微秒為單位表示時間,並利用這個很長的位串中間的位(1和0)來提供隨機性。具體使用什麼方法對我們來說並不重要。我們假設瀏覽器支援的JavaScript能很好地完成這個工作,這稱為偽隨機處理(pseudo-random processing)。

  現在假設我們可以隨機地得到1到6之間的某個數,像這樣取兩次數,分別表示丟擲兩次骰子的點數,下面需要實現遊戲的規則。這說明我們需要一種方法來跟蹤現在是第一次擲骰子還是後續的一次擲骰子。這有一個正式的名字,叫做應用狀態(application state),表示應用目前的情況,這在遊戲以及其他型別的應用中都很重要。接下來需要使用一些根據條件做出決策的構造。條件構造(如if和switch)是程式語言中的一個標準部分,你很快就會理解為什麼像我這樣教電腦科學的老師(從來沒有去過賭場或夜店)居然也會喜歡這種骰子游戲。

  我們需要為玩家提供一種擲骰子的方法,所以會在螢幕上實現一個按鈕供玩家單擊。然後要向玩家提供資訊,告訴他們發生了什麼。對於這個應用,我會生成圖形化反饋,在螢幕上畫出骰子的正面,另外顯示文字資訊來指示遊戲所處的階段、點數和結果。與使用者的互動原來稱為輸入——輸出(input-output,I/O),那時互動主要涉及文字。現在通常用圖形化使用者介面(Graphical User Interface,GUI)來表示使用者與計算機系統互動的多種方式。這包括使用滑鼠單擊螢幕上某個特定的點,或者結合單擊和拖動來模擬移動一個物件的效果(見第4章的彈弓遊戲)。在螢幕上繪圖需要使用座標系來指定點。大多數程式語言都採用類似的方式實現計算機螢幕的座標系,稍後我會解釋。

  2.3 HTML5、CSS和JavaScript特性

  現在來看實現craps遊戲所需的HTML5、CSS和JavaScript特性。

   2.3.1 偽隨機處理和數學表示式

   JavaScript中的偽隨機處理使用一個名為Math.random的內建方法完成。正式的說法是,random是Math類的一個方法。Math.random方法會生成從0到1(但不包括1)之間的一個小數,例如0.253 012。看起來對我們並沒有直接意義,不過把這個數轉換為一個我們可以使用的數確實很簡單。可以把這個數(不論是什麼)乘以6,這會生成一個從0到6(但不包括6)的數。例如,如果將0.253 012乘以6,會得到1.518 072。這差不多就是我們所要的,但還不盡人意。下一步是去掉小數部分,保留整數部分。為此,要利用另一個Math方法Math.floor。這個方法會刪除所有小數部分,生成一個整數。顧名思義,floor方法會向下取整。對於這個例子,首先得到0.253 012,然後得到1.518 072,所以結果就是整數1。一般來講,將隨機數乘以6再取整時,會得到一個從0到5的數。最後一步是加1,因為我們的目標是反覆得到一個從1到6的數,而且沒有任何特定的規律。

  可以使用類似的方法來得到任何範圍內的整數。例如,如果希望得到1到13之間的數,可以將隨機數乘以13再加1。這對於撲克牌遊戲會很有用。本書中你會看到許多類似的例子。

  可以把所有這些步驟結合到一個表示式(expression)中。表示式是常量、方法和函式呼叫的組合,這些內容我們稍後介紹。要利用操作符把這些項組合在一起,如+表示加法,*表示乘法。

  應該還記得第1章中如何結合使用標記,可以把一個標記巢狀在另一個標記中,來看我們在“最喜愛網站”應用中使用的一行JavaScript程式碼:

  document.write(Date());

  這裡可以使用一個類似的處理。不必分成兩個語句,先寫random呼叫,然後寫floor方法,而可以把random呼叫作為floor方法的一個引數傳入。來看下面的程式碼片段:

  1+Math.floor(Math.random()*6)

  這個表示式會生成從1到6之間的一個數。我把它叫做程式碼片段(code fragment),因為這並不是一個語句。操作符+和*表示算術運算,其含義與你在數學中使用的運算子是一樣的。操作順序為由內向外完成計算。

   呼叫Math.random()得到一個0到1(但不包括1)的小數。

   將這個結果乘以6。

   取這個結果,使用Math.floor去除小數部分,只留整數部分。

   最後加1。

   在最後的程式碼中,你會看到一個包含這個表示式的語句,不過接下來還需要先介紹另外一些內容。

   2.3.2 變數和賦值語句

   類似於其他程式語言,JavaScript也有一個名為變數(variable)的構造,實際上這就是一個可以放入值的位置,如放入一個數。利用變數可以把一個名字與一個值關聯起來。以後可以通過引用這個名來使用這個值。可以拿職位做個比方。在美國,我們經常會談到“總統”。現在美國總統是巴拉克•奧巴馬。而在2009年1月21日之前,總統是喬治•布什。“總統”這個詞包含的值會改變。在程式設計中,變數的值也可以改變,變數也因此得名。

  要用var來宣告(declare)一個變數。

  變數名和函式名(將在下一節介紹)由程式設計師決定。對此有一些規則:變數名和函式名中不能包含空格,另外必須以一個字母字元開頭。不要指定太長的名字,因為你肯定不願意鍵入太多字元,但是也不要太短,以免你忘記它的本意。一定要保持一致,不過不必遵循英語的拼寫規則。例如,如果你想建立一個變數儲存值的總和,而且你以為“總和”(sum)就拼為som,也完全可以。只是一定要保證自始至終一直都使用som。不過如果你想引用JavaScript中內建的某個詞,如function、document或random,就必須使用JavaScript認定的拼法。

  要避免使用JavaScript 中內建構造的名(如random或floor)作為你的變數名。應保證名字是唯一的,而且要容易理解。寫變數名的一種常用方法是使用駝峰式命名法。這表示變數名以小寫字母開頭,然後一個新詞開始時都使用一個大寫字母來指示,例如numberOfTurns或userFirstThrow。可以看出為什麼把它叫做駝峰式命名法——大寫字母構成了單詞中的“駝峰”。並不一定非要使用這種命名方法,不過很多程式設計師都遵循這種約定。

  這裡有一行程式碼包含了上一節解釋的偽隨機表示式,這是一種特殊型別的語句,稱為賦值語句(assignment statement)。例如,

  var ch = 1+Math.floor(Math.random()*);

  這個語句會設定變數ch的值為等號右邊表示式的結果。在一個var語句中使用時,這也稱為初始化語句(initialization statement)。在這種情況下以及後面介紹的賦值語句中,等號(=)用來為變數設定初始值。這裡使用名字ch作為choice(選擇)的簡寫。這對我來說可以理解。不過一般來講,如果需要在一個簡短的名字和一個你能記住的較長名字之間選擇,最好選擇那個長名字!注意語句都以一個分號結束。你可能會問,為什麼不是點號?答案是點號還有另外兩種用途:一是要作為小數點,二是用來訪問物件的方法和屬性,如document.write。

  賦值語句是程式設計中最常見的一種語句。下面這個例子為一個已定義的變數賦值:

  bookname = "The Essential Guide to HTML5";

  這裡使用等號可能讓人有些困惑。可以認為這表示讓“左邊等於右邊生成的結果”為真。本書中你會看到很多其他變數以及操作符和賦值語句的其他用法。

  警告 定義一個變數的var語句稱為一個宣告語句。不同於很多其他語言,JavaScript允許程式設計師忽略宣告語句,直接開始使用一個變數。我儘量避免這樣做,不過你在網上的很多例子中可能會見到不宣告而直接使用變數的情況。

  對於craps遊戲,我們需要定義遊戲狀態的變數,具體來講就是明確這是第一次擲骰子還是後續的一次丟擲,另外要記錄玩家的點數(要記住,這個點數是上一次擲骰子得到的值)。在我們的實現中,這些值會儲存在全域性變數(global variable)中,這是指在任何函式定義之外用var語句宣告從而能保留值的變數(對於在函式內部宣告的變數,其值會在函式執行結束時消失)。

  並不總是需要使用變數。例如,這裡的第一個應用建立了一些變數來儲存骰子的水平和垂直位置。也可以在程式碼中直接用字面量數字,因為我不會再改變這些數。但是由於我會在很多不同地方引用這些值,把值儲存在變數中就意味著一旦我想改變其中一個或兩個值,就只需要在一處進行修改即可。

   2.3.3 程式設計師自定義函式

   JavaScript有很多內建的函式和方法,但是它不會有你想要的一切。例如,就我所知,它沒有專門模擬擲骰子的函式。所以JavaScript允許我們定義和使用自己的函式。這些函式可以有引數(argument),比如Math.floor方法,也可以沒有引數,比如Math.random。引數就是可以傳入函式的值。可以認為它們是額外資訊。函式定義的格式為:首先是function,後面是你希望為函式指定的名字,然後是一對括號,其中包括所有引數名,後面是一個開始大括號,然後是一些程式碼,最後是一個結束大括號。前面說過函式名由程式設計師指定。以下是一個函式定義的例子,它會返回兩個引數的乘積。顧名思義,可以用它來計算一個長方形的面積。

  function areaOfRectangle(wd,ln) {

      return wd * ln;

  }

  注意關鍵字return,它告訴JavaScript把函式的結果返回給我們。在這個例子中,我們寫出類似rect1 = areaOfRectangle(5,10)的程式碼,這會把一個值50(5×10)賦給rect1變數。這個函式定義會寫為script元素中的程式碼。在現實生活中定義這個函式可能有意義,也可能沒有意義,因為在程式碼中寫乘法實在太容易了,不過這可以作為一個程式設計師自定義函式的例子。一旦執行了這個定義(可能載入HTML檔案時),其他程式碼就可以呼叫函式名來使用這個函式,如areaOfRectangle(100,200)或areaOfRectangle(x2-x1,y2-y1)。

  第二個表示式假設x1、x2、y1、y2指示在其他位置定義的一些座標值。

  還可以通過設定某些標記屬性來呼叫函式。例如,body標記中可以包含onLoad屬性的設定:

  <body onLoad="init();">

  我的JavaScript程式碼包含一個init函式的定義。把它放在這個body元素中意味著,當瀏覽器第一次載入這個HTML文件或者玩家單擊過載/重新整理按鈕時,JavaScript就會呼叫我的init函式。類似地,利用HTML5的一個新特性,還可以包含button元素:

  <button onClick="throwdice();">Throw dice </button>

  這會建立一個文字為Throw dice的按鈕。玩家單擊這個按鈕時,JavaScript會呼叫我在script元素中定義的throwdice函式。

  form元素(稍後介紹)會以類似的方式呼叫一個函式。

   2.3.4 條件語句:if和switch

   craps遊戲有一組規則。可以這樣來總結這些規則:如果這是第一次擲骰子,就檢查所擲骰子是否等於某些值。如果不是第一次丟擲,就檢查所擲骰子是否等於其他值。為此,JavaScript提供了if和switch語句。

  if語句要根據條件(condition)判斷,條件可能是一個比較,或者是檢查相等性。例如,一個名為temp的變數是否大於85,或者名為course的變數是否包含值"Programming Games"。比較會產生兩個可能的邏輯值——true或false。到目前為止,你已經見過數字和字串值。邏輯值是另外一種資料型別,也稱為布林值(boolean value),因數學家George Boole得名。前面提到的條件和檢查可以寫為以下程式碼:

  temp>85

  和

  course == "Programming Games"

  可以把第一個表示式讀作:變數temp的當前值是否大於85?第二個表示式可以讀作:變數course的當前值是否等於字串"Programming Games"?

  前一個比較的例子很容易理解,我們使用>來檢查一個值是否大於另一個值,使用<檢查一個值是否小於另一個值。表示式的值可以是兩個邏輯值之一:true或false。

  第二個表示式可能讓人稍有些困惑。你可能不知道兩個等號是什麼,另外可能對引號也不太清楚。JavaScript(和很多其他程式語言)中用於檢查相等性的比較操作符就是這樣兩個等號的組合。之所以需要兩個等號,是因為一個等號在賦值語句中表示賦值,它不能有雙重職責。如果寫作course = "Programming Games",就會把值"Programming Games" 賦至course變數,而不是對這兩項進行比較。引號定義了一個字串,從P開始,包括空格,以s結束。

  有了以上了解,現在來看如何編寫程式碼,在一個條件為true時完成某個工作。

  if (條件) {

     程式碼

  }

  如果希望程式碼僅在一個條件為true時做某件事,而在條件不為true時做另一件事,則格式為:

  if (條件) {

     條件為true時的程式碼

  }

  else {

      條件不為true時的程式碼

  }

  注意這裡我使用了斜體,因為這稱為虛擬碼,而不是包含在HTML文件中的實際JavaScript程式碼。

  以下是一些真正的程式碼示例。其中利用了alert,這是一個內建函式,會在瀏覽器中彈出一個小視窗,其中包含括號之間給定引數指示的訊息。使用者必須單擊OK按鈕才能繼續。

  if (temp>85) {

     alert("It is hot!");

  }

  if (age > 21) {

     alert("You are old enough to buy a drink.");

  }

  else {

     alert("You are too young to be served in a bar.");

  }

  可以只使用if語句編寫craps應用。不過,JavaScript還提供了另外一種構造,可以更容易地完成工作,即switch語句。一般格式如下:

  switch(x) {

  case a:

     codea;

  case b:

     codeb;

  default: codec;

  }

  JavaScript會計算switch語句第一行中x的值,將它與下面各個case中指示的值進行比較。一旦“命中”,也就是說,一旦確定x等於a或b,就會執行相應case標籤後面的程式碼。如果沒有找到匹配,就會執行default後面的程式碼。不一定非要有一個default分支。順其自然的話,即使找到一個匹配的case語句,計算機也會繼續執行完switch語句。如果你希望在找到一個匹配時讓它停止,就需要加一個break語句從switch跳出。

  你可能已經瞭解如何利用if和switch完成這個骰子游戲所需的工作。下一節你會了解具體如何做。不過先來看下面的例子,這裡要由變數mon確定當月的天數,變數mon中儲存有3個字母的縮寫("Jan"、"Feb"等)。

  switch(mon) {

  case "Sep":

  case "Apr":

  case "Jun":

  case "Nov":

          alert("This month has 30 days.");

          break;

  case "Feb":

          alert("This month has 28 or 29 days.");

          break;

  default:

          alert("This month has 31 days.");

  }

  如果變數mon的值等於"Sep"、"Apr"、"Jun"或 "Nov",會執行第一個alert語句,然後由於break退出switch語句。如果變數mon的值等於"Feb",會執行提到28或29天的那條alert語句,然後控制流退出switch。如果mon的值是其他值,包括非法的3字母縮寫,都會執行提到31天的那個alert語句。

  就像HTML會忽略換行符和其他空白符一樣,JavaScript並不要求這些語句有某個特定的佈局。如果願意,可以把所有程式碼放在一行上。不過為了方便你自己,還是使用多行為好。

相關文章