Libgdx Developer's Guide(Libgdx開發者手冊)-8(一個簡單的遊戲)
在潛入libgdx提供的API之前,讓我們建立一個非常簡單的“遊戲”,這個遊戲每個模組都將觸及一點以讓我們有種整體感覺。我們會引入一些概念,但不會太深入。
- 基本檔案訪問
- 清屏
- 繪圖
- 使用照相機
- 基本輸入處理
- 播放音效
專案設定
按照 Project Setup中的步驟進行。使用以下名稱:
- 應用程式名: drop
- 包名: com.badlogic.drop
- 遊戲類: Drop
一旦匯入到Eclipse中,你應該擁有4個工程: drop, drop-android, drop-desktop 和 drop-html5。
遊戲
遊戲的想法很簡單:
- 用水桶捕捉雨滴
- 水桶位於螢幕下方
- 雨滴每秒隨機從螢幕頂部產生並加速落下
- 玩家可以通過滑鼠/觸屏或者左右鍵來水平移動水桶
- 遊戲沒有結尾,想像它是一禪宗般的體驗 :)
資源
我們需要一些圖片和音效來使遊戲看起來稍微漂亮一些。對於圖形,定義目標解析度為800x480畫素(Android橫屏)。如果遊戲執行的裝置沒有該解析度,則縮放所有東西以適應螢幕。注意:對於 高質量的遊戲,你可能考慮會不同的螢幕解析度提供不同的資源。這本身是一個大話題,這裡不深入。
雨滴和水桶在垂直方向佔用的螢幕應該小於十分之一,因此定義它們的大小為64x64畫素。
從以下地址獲取資源:
- 水滴聲: http://www.freesound.org/people/junggle/sounds/30341/
- 雨: http://www.freesound.org/people/acclivity/sounds/28283/
- 液滴: https://www.box.com/s/peqrdkwjl6guhpm48nit
- 水桶: https://www.box.com/s/605bvdlwuqubtutbyf4x
為了讓遊戲可以使用這些資源,必須把它們放在Android工程的assets資料夾下。把這4個檔案命名為:drop.wav, rain.mp3, droplet.png 和 bucket.png,並把它們放在 drop-android/assets/
配置啟動類
鑑於我們的需求,我們現在開始配置不同的啟動類。先從桌面應用開始。開啟drop-desktop/下的類Main.java。我們需要一個 800x480 的視窗並設定標題為"Drop"。程式碼如下:
package com.badlogic.drop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
public class Main {
public static void main(String[] args) {
LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration();
cfg.title = "Drop";
cfg.width = 800;
cfg.height = 480;
new LwjglApplication(new Drop(), cfg);
}
}
轉到Android工程,我們想要應用在橫屏執行。因此需要修改工程根目錄下的AndroidManifest.xml,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.badlogic.drop"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="5" android:targetSdkVersion="15" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:configChanges="keyboard|keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
配置工具已經為我們填寫了正確的值,android:screenOrientation設定為"landscape"。如果要在豎屏模式運行遊戲,就把該屬性設定為 "portrait"。
我們希望節省電池並禁用振動器和指南針。這需要在工程的MainActivity.java中做以下修改:
package com.badlogic.drop;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
public class MainActivity extends AndroidApplication {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
cfg.useAccelerometer = false;
cfg.useCompass = false;
initialize(new Drop(), cfg);
}
}
我們不能定義Activity的解析度,這是由Android作業系統設定的。像之前設定的一樣,無論裝置解析度是多少,我們會縮放800x480的目標解析度到當前裝置解析度。
最後,我們要確保HTML5工程也使用一個800x480的繪製區。因此需要修改html5工程下的GwtLauncher.java檔案:
package com.badlogic.drop.client;
import com.badlogic.drop.Drop;
public class GwtLauncher extends GwtApplication {
@Override
public GwtApplicationConfiguration getConfig () {
GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(800, 480);
return cfg;
}
@Override
public ApplicationListener getApplicationListener () {
return new Drop();
}
}
注意: 我們不需要為該平臺指定使用的OpenGL版本,因為它只支援2.0。
現在所有的啟動類都配置完成了,讓我們開始實現這個有趣的遊戲。
程式碼
我們希望把程式碼分成幾部分。簡單起見,我們把所有東西都放在核心工程的Drop.java檔案中。
載入資源
第一個任務是載入資源並儲存它們的引用。通常在ApplicationListener.create()方法中載入資源,因此程式碼如下:
public class Drop implements ApplicationListener {
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
@Override
public void create() {
// load the images for the droplet and the bucket, 64x64 pixels each
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// load the drop sound effect and the rain background "music"
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// start the playback of the background music immediately
rainMusic.setLooping(true);
rainMusic.play();
... more to come ...
}
// rest of class omitted for clarity
每個資源都在Drop類中擁有一個欄位,因而後續我們可以引用它。create()方法的前兩行載入雨滴和水桶的圖片。Texture表示一個儲存於視訊RAM裡的已載入圖片。通常不直接繪製Texture。 Texture通過向其構造器傳入一個資原始檔的FileHandle來載入。這種FileHandle的例項是通過byGdx.files裡其中一個方法來獲得的。不同的檔案型別有很多,我們在這裡使用 "internal" 檔案型別來引用資源。Internal 的檔案位於Android工程的assets目錄中。Eclipse中,桌面應用和HTML5工程通過連結以引用該目錄。
接下來載入音效與背景音樂。Libgdx 區分音效和音樂,音效儲存在記憶體裡,音樂無論儲存在哪都會被轉換為流。音樂通常太大不能完全儲存在記憶體裡,因此作此區分。根據經驗,如果你的示例小於10秒則要使用一個Sound例項,更長的音訊就要使用Music例項。
通過Gdx.app.newSound() 和 Gdx.app.newMusic()來載入Sound 或 Music。這兩個方法都需要一個FileHandle,像Texture的構造器一樣。
在create()方法末尾,我們讓Music例項迴圈並立即播放。 如果你執行這個應用,你會看到一個漂亮的粉紅色背景,能聽到落雨聲。
Camera 和 SpriteBatch
接下來我們建立 一個Camera 和 SpriteBatch。我們使用前者以保證使用目標解析度800x480畫素來呈現應用,而不管實際的解析度是多少。SpriteBatch 是一個特殊的類用來繪製2D圖形,比如我們已經載入的紋理。
我們向類中加入兩個新欄位,命名為 camera 和 batch:
OrthographicCamera camera;
SpriteBatch batch;
在 create() 方法中我們首先這樣建立 camera :
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
這樣就可以確保camera一直為我們展示一個800x480單位寬的遊戲區。想象它是一個虛擬視窗。目前我們把畫素作為單位,這樣簡單一些。使用其它單位也沒什麼,如meters或其他什麼。Cameras非常強大,它能作很多事,我們在此基礎手冊中不再詳述。檢視剩下的使用者手冊來獲取更多資訊。
然後建立 SpriteBatch (仍然在 create()方法中):
batch = new SpriteBatch();
通過建立這些,我們差不多已經完成所有運行遊戲所需要的東西。
加入水桶
最後缺少的水桶和雨滴。讓我們想想要用代表描述什麼:
- 一個有x/y座標的水桶/雨滴在800x480大小的空間。
- 在遊戲區表示出水桶/雨滴的寬高。
- 水桶/雨滴的圖形表示,我們已經通過Texture例項載入過了。
因此,為了描述水桶與雨滴,我們需要儲存它們的位置和大小。Libgdx提供一個Rectangle類可以達成這個目的。開始先建立一個表示水桶的Rectangle。新增一個新欄位:
Rectangle bucket;
在 create() 方法中例項化Rectangle並指定其初始值。我們想讓水桶比底部高出20畫素,並水平居中。
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2;
bucket.y = 20;
bucket.width = 64;
bucket.height = 64;
我們將水桶水平居中,並放在離螢幕底部20畫素高的地方。等等,為什麼bucket.y設定為20,不應該是480 - 20嗎?預設情況下,所有在libgdx(與OpenGL)中顯示的東西其y軸都指向上方。水桶的x/y座標定義在水桶的左下角,繪圖的原點位於螢幕左下角。矩形的寬高設定為64x64,小於目標解析度高度的十分之一。
渲染水桶
是時候渲染水桶了。首先要做的是用深藍色清屏。更改render() 方法如下:
@Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
... more to come here ...
}
如果你使用高階類如Texture 或 SpriteBatch,那關於OpenGL你只需要知道這兩行。第一個呼叫把清屏色設定為藍色。其引數分別是紅,綠,藍和該顏色的透明度,每個的取值範圍都是[0, 1]。下一個呼叫命令OpenGL直接去清屏。
然後呼叫camera去更新。Cameras 使用一個稱作矩陣的數學實體負責建立渲染的座標系。每次更改camera屬性都要重新計算這些矩陣。我們不在這個簡單的例子中做這些,但每幀更新一次camera是一個很好的實踐:
camera.update();
現在可以顯示水桶了:
batch.setProjectionMatrix(camera.combined); batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); batch.end();
第一行告訴 SpriteBatch 使用camera指定的座標系。如前所述,這是由一種叫做矩陣的東西完成的,確切地說,叫投影矩陣。camera.combined欄位就是這樣一個矩陣。SpriteBatch將從那裡在座標系中渲染前面描述過的所有東西。
下面告訴 SpriteBatch 啟動一個新的batch。為什麼要這麼做,batch又是什麼?OpenGL 最討厭只告訴它一個單獨的圖片,它希望一次性告訴它儘可能多的多個圖片。
SpriteBatch 類就可以幫助 OpenGL 。它會記錄SpriteBatch.begin() 和 SpriteBatch.end()之間的所有繪製命令。一旦呼叫SpriteBatch.end(),它會一次性把提交所有的繪畫請求,這讓渲染過程加速很多。剛開始這些或者看起來很煩,但正是這一點造成了每秒60幀顯示500個sprite和每秒20幀顯示100個sprite之間的差別。
使水桶移動起來 (觸屏/滑鼠)
是時候讓使用者控制水桶了。之前我們提到過要讓使用者拖動水桶。讓我們稍微簡化一下。如果使用者觸控式螢幕幕(或按下滑鼠),我們希望水桶圍繞這一點水居中。在render() 方法最後面新增以下程式碼:
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
首先我們通過呼叫 Gdx.input.isTouched()來查尋輸入模組當前螢幕是否被觸控(或滑鼠被按下)。接下來我們把觸屏/滑鼠人座標轉換到camera的座標系。這很有必要,因為觸屏/滑鼠的座標系很可能我我們用來顯示物件的座標系不一致。
Gdx.input.getX() 和 Gdx.input.getY() 返回當前 觸控/滑鼠位置(libgdx也支援多點觸控,但這是另一個話題了)。要把這些座標轉換到我們的camera的座標系,需要呼叫camera.unproject() 方法,這需要一個Vector3, 一個三維向量。建立這樣的向量,設定當前觸控/滑鼠的座標並呼叫該方法。該向量就會包含水桶所在座標系的觸控/滑鼠的座標。最後,我們更改水桶位置以圍繞觸控/滑鼠的座標居中。
注意: 總是例項化新的物件是非常非常壞的一種方法,比如這裡的Vector3物件。原因是垃圾回收器不得不頻繁地清除這些短命的物件。在桌面應用中這不是一個大問題,但在Android裡,垃圾回收器會導致幾百毫秒的暫停因而會很卡。為了解決這個特殊問題,可以簡單地將touchPos作為Drop類的一個欄位,而不是總是例項化。
注意#2: touchPos 是一個三維向量。你可能想知道為什麼我們只操作2D時還需要它。OrthographicCamera實際上是3D camera,它也有z座標。想想CAD應用,它們也使用3D正交camera。我們只是簡單地用它來繪製2D圖形。
使水桶移動起來 (鍵盤)
在桌面和瀏覽器中,也可以接收鍵盤輸入。當左右方向鍵被按下時,使水桶移動起來。
我們希望水桶移動時不振動,無論向左或向右,每秒200畫素單位。要實現這種基於時間的移動,我們需要知道最近一幀和當前幀之間經過的時間。下面是相應的做法:
if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();
Gdx.input.isKeyPressed() 方法告訴我們特定的鍵是否被按下。列舉類Keys包含所有libgdx支援的鍵碼。Gdx.graphics.getDeltaTime()方法返回最近上幀和當前幀之間所經歷的秒數。我們只需修改水桶的x座標,每次加上/減去 100單位。
同時還要保證水桶處於螢幕範圍內。
if(bucket.x < 0) bucket.x = 0;
if(bucket.x > 800 - 64) bucket.x = 800 - 64;
新增雨滴
對雨滴來說,我們儲存一個Rectangle例項列表,每一個用來跟蹤一個雨滴的位置及大小。為這個列表加入一個欄位:
Array<Rectangle> raindrops;
Array 類是libgdx的一個公共類用來替代標準Java集合如ArrayList。後者的問題是很多情況下它會產生垃圾。Array類嘗試儘可能多地減少垃圾。Libgdx提供其他垃圾回收器可以回收的集合,如hashmaps或sets等。
我們也需要保持跟蹤產生雨滴的最後時間,因此我們新增另一個欄位:
long lastDropTime;
我們要用納秒來儲存這個時間,因此使用long型別。
為便於建立雨滴,我們寫一個方法叫spawnRaindrop(),它例項化一個新的Rectangle,把它設定到螢幕頂部的一個隨機位置,並新增到raindrops陣列。
private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800-64);
raindrop.y = 480;
raindrop.width = 64;
raindrop.height = 64;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
該方法很明瞭。MathUtils類是一個libgdx類提供豐富的數學相關的靜態方法。這個例子中,它返回一個介於0和 800-64 之間的隨機數。TimeUtils是另一個libgdx類,它提供一個非常基礎的時間相關的靜態方法。該例中我們以納秒記錄當前時間,後續我們要以此判斷是否產生一個新雨滴。
我們在create()方法中例項該陣列:
raindrops = new Array<Rectangle>();
spawnRaindrop();
接下來在render()方法中新增幾行,來檢查自從產生一個新雨滴以來所經歷的時間,如果需要的話再建立一個新雨滴:
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();
我們也需要讓雨滴動起來,讓我們採取簡單的方法,讓它們以每秒 200畫素/單位 的恆定速度移動。如果雨滴位置螢幕底部以下,我們就從數組裡移除它。
Iterator<Rectangle> iter = raindrops.iterator();
while(iter.hasNext()) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
if(raindrop.y + 64 < 0) iter.remove();
}
雨滴需要渲染。在SpriteBatch中新增渲染程式碼後如下:
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();
最後一個調整:如果雨滴碰到了水桶,我們希望播放下雨聲並從陣列移動該雨滴。我們簡單地向雨滴迴圈更新處新增下面幾行:
if(raindrop.overlaps(bucket)) {
dropSound.play();
iter.remove();
}
Rectangle.overlaps() 方法檢查是否一個矩形和另一個矩形重疊。在此例中,我們讓下雨音效播放並從陣列移除該雨滴。
清理
使用者可以在任何時候關閉應用。對這個簡單的例子而言沒什麼要處理的。然而,通常來說幫助作業系統收拾殘局是一個很好的主意。
任何實現了Disposable介面的libgdx類並且因此帶有adispose()方法,都需要在不使用時手動銷燬。在我們的例子中紋理,聲音,音樂和SpriteBatch都是符合條件類。身為好市民,我們如下這樣實現{ApplicationListener#dispose() 方法:
@Override
public void dispose() {
dropImage.dispose();
bucketImage.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
一旦你銷燬一個資源,就不可以在任何地方訪問它。
可銷燬資源通常是一些不能被Java的垃圾回收器處理的本地資源。這就是我們為什麼要手動銷燬的原因。Libgdx 提供豐富的方法來管理資源。讀剩下的開發手冊檢視這些方法。
處理暫停/恢復
每次使用者接到一個電話或按下home鍵時,Android都 有暫停和恢復應用程式的標記。Libgdx在這種情況下會自動為你做很多事情,比如:重新載入可能丟失的圖片(OpenGL上下文丟失,對它而言是很嚴重的一個話題),暫停和恢復音樂流等。
我們的遊戲其實不需要處理暫停和恢復。當用戶回到應用時,遊戲會接著上次離開時的狀態繼續執行。通常人們會實現一個暫停按鈕讓使用者點選螢幕繼續。這留給讀者作為練習。檢視下ApplicationListener.pause()和ApplicationListener.resume()方法。
完整的原始碼
這是我們這個簡單遊戲的原始碼:
package com.badlogic.drop;
import java.util.Iterator;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
public class Drop implements ApplicationListener {
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
SpriteBatch batch;
OrthographicCamera camera;
Rectangle bucket;
Array<Rectangle> raindrops;
long lastDropTime;
@Override
public void create() {
// load the images for the droplet and the bucket, 64x64 pixels each
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// load the drop sound effect and the rain background "music"
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// start the playback of the background music immediately
rainMusic.setLooping(true);
rainMusic.play();
// create the camera and the SpriteBatch
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();
// create a Rectangle to logically represent the bucket
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2; // center the bucket horizontally
bucket.y = 20; // bottom left corner of the bucket is 20 pixels above the bottom screen edge
bucket.width = 64;
bucket.height = 64;
// create the raindrops array and spawn the first raindrop
raindrops = new Array<Rectangle>();
spawnRaindrop();
}
private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800-64);
raindrop.y = 480;
raindrop.width = 64;
raindrop.height = 64;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void render() {
// clear the screen with a dark blue color. The
// arguments to glClearColor are the red, green
// blue and alpha component in the range [0,1]
// of the color to be used to clear the screen.
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
// tell the camera to update its matrices.
camera.update();
// tell the SpriteBatch to render in the
// coordinate system specified by the camera.
batch.setProjectionMatrix(camera.combined);
// begin a new batch and draw the bucket and
// all drops
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();
// process user input
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();
// make sure the bucket stays within the screen bounds
if(bucket.x < 0) bucket.x = 0;
if(bucket.x > 800 - 64) bucket.x = 800 - 64;
// check if we need to create a new raindrop
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();
// move the raindrops, remove any that are beneath the bottom edge of
// the screen or that hit the bucket. In the later case we play back
// a sound effect as well.
Iterator<Rectangle> iter = raindrops.iterator();
while(iter.hasNext()) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
if(raindrop.y + 64 < 0) iter.remove();
if(raindrop.overlaps(bucket)) {
dropSound.play();
iter.remove();
}
}
}
@Override
public void dispose() {
// dispose of all the native resources
dropImage.dispose();
bucketImage.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
}
接下來怎麼走
這是一個非常基礎的例子,展示怎樣用Libgdx建立一個簡易的遊戲。一些東西還可以再改進,比如使用Pool類來迴圈使用Rectangles,我們在刪除雨滴時讓垃圾回收器都把它們回收了。如果一個批處理中給它許多不同圖片,OpenGL就不好用。在我們的例子中沒問題,因為我們只有兩個圖片。通常我們會把所有不同圖片放在單個Texture裡,也被稱作TextureAtlas。
我強烈推薦你讀剩下的開發手冊,並檢出Git倉庫裡的demo和測試。程式設計快樂。