First Demo - Step 1
在深入瞭解LibGDX詳細的API之前,讓我們先建立一個簡單的遊戲,它會對每個模組都有接觸,我們將介紹一些概念:
- 資原始檔訪問
- 清除螢幕
- 繪製圖像
- 使用相機
- 基本輸入處理
- 播放聲音
遊戲設計
- 用水桶捕捉雨滴;
- 桶位於螢幕的下部;
- 雨滴每秒鐘隨機產生螢幕頂部,並向下加速;
- 玩家可以通過滑鼠/觸控水平拖動水桶。
遊戲資源
要使遊戲資源可用於遊戲,我們必須將它們放在Android資產資料夾中。 我命名了4個檔案:drop.wav,rain.mp3,drops.png和bucket.png,並將它們放在android / assets /中。 配置啟動類 為了方便除錯,我們先開啟桌面專案的啟動類:Desktoplauncher.java,並加入3行config程式碼import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.badlogic.drop.Drop; public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); config.title = "Drop"; config.width = 800; config.height = 480; new LwjglApplication(new Drop(), config); } }
為了節約手機電量,我們開啟Android的啟動類AndroidLauncher.java,插入兩行config程式碼
import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.badlogic.drop.Drop; public class AndroidLauncher extends AndroidApplication { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); config.useAccelerometer = false; config.useCompass = false; initialize(new Drop(), config); } }
載入資原始檔
我們的第一個任務是載入資產並存儲對它們的引用。 資源通常在create()方法中載入。對於每個資原始檔,在Drop.java中都有對應的變數指向它,這樣我們就可以在後期隨時隨地使用它。
create()方法中的前兩行載入雨滴和桶的影象。紋理表示儲存在Video Ram中的影象,通常傳遞一個來自Assets的資原始檔的FileHandle控制代碼到Texture 來建立一個紋理,FileHandle 例項是通過Gdx.files提供的方法之一獲得的。我們使用【internal】來獲得Assets資料夾下的資原始檔,internal 檔案位於Android專案的 assets 目錄中。 如前所述,桌面和HTML5專案引用同一目錄。
接下來我們載入聲音效果和背景音樂。Music 通常比較大。根據經驗,如果您的聲音資原始檔短於10秒,您應該使用Sound例項,而更長音訊片段則使用Music例項。
聲音或音樂例項的載入是通過Gdx.audio.newSound()和Gdx.audio.newMusic()完成的。 這兩種方法都採用FileHandle,就像Texture建構函式一樣。
import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Texture; public class Drop extends ApplicationAdapter { private Texture dropImage; private Texture bucketImage; private Sound dropSound; private 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(); }
相機和SpriteBatch
接下來,我們要建立一個攝像頭和一個SpriteBatch。 我們將使用前者(Camera)來確保我們可以使用800x480畫素的目標解析度渲染,無論實際的螢幕解析度如何。 SpriteBatch是一個特殊的類,用於繪製2D影象,像載入我們的紋理。
我們在類上新增兩個新的欄位,我們稱之為相機和batch:
private OrthographicCamera camera; private SpriteBatch batch;
在create()方法中,我們這樣初始化相機
camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480);
這將確保相機總是向我們展示我們的遊戲世界的800x480單位寬的區域。 把它看作是我們世界的虛擬窗戶。 我們目前將單位預設 為畫素。 沒有什麼可以阻止我們使用其他單位, 米或任何你可以想得到的單位。 相機非常強大,您可以在本基礎教程中做很多事情。檢視開發者指南的其餘部分了解更多資訊。
接下來我們仍在create()方法中建立SpriteBatch例項:
batch = new SpriteBatch();
新增桶
接下來我們來新增我們的桶還有雨滴:- 一個桶/雨滴在我們的800x480單位世界的x / y位置。
- 一個桶/雨滴的寬度和高度以我們世界的單位表示。
- 桶/雨滴具有圖形表示,我們已經具有我們載入的Texture例項的形式。
所以,要描述桶和雨滴,我們需要儲存他們的位置和大小。 Libgdx提供了一個可以用於此目的的Rectangle類。 我們首先建立一個代表我們的儲存桶的Rectangle類。 我們新增一個新欄位:
private 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採用的是笛卡爾座標系(以左下角為原點)。
渲染桶
是時候來繪製我們的桶了。我們要做的第一件事是用深藍色來清除螢幕。 只需將render()方法更改為如下所示:@Override public void render() { Gdx.gl.glClearColor(0, 0, 0.2f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); }如果您使用高階類(如Texture或SpriteBatch),這兩行就是您需要了解OpenGL的唯一內容。 第一個呼叫會將清屏的顏色設定為藍色。 引數是該顏色的紅色,綠色,藍色和alpha值,分別在[0,1]範圍內。 下一個呼叫指示OpenGL實際清除螢幕。
接下來,我們需要告訴我們的相機,以確保實時更新。 相機使用稱為矩陣的數學實體,負責設定渲染座標系。 每當我們更改相機的屬性(如位置)時,相機都需要重新計算這些矩陣。 我們不會在簡單的例子中做到這一點,但是通常每幀更新一次攝像機一般是一個很好的做法:
camera.update();
現在我們可以渲染我們的桶了:
batch.setProjectionMatrix(camera.combined); batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); batch.end();
第一行告訴SpriteBatch使用相機指定的座標系。 如前所述,這是通過稱為矩陣的東西來完成的,更具體地說,是一個投影矩陣。 camera.combined欄位是這樣一個矩陣。 從SpriteBatch上將會在前面描述的座標系中呈現所有內容。
接下來我們告訴SpriteBatch開始一個新批量渲染。 為什麼我們需要這個,什麼是批量渲染? OpenGL討厭不高效的繪製渲染。 它希望被告知有關儘可能多的影象需要渲染,並一次性渲染完畢。
SpriteBatch 使得 OpenGL 在渲染檢視上更加得心應手,它將記錄SpriteBatch.begin()和SpriteBatch.end()之間的所有繪圖命令。 一旦我們呼叫SpriteBatch.end(),它將一次提交我們所做的所有繪圖請求,加快渲染速度。 這一切可能在一開始就看起來很麻煩,但是它使得渲染500個精靈之間的差異是每秒60幀,並以每秒20幀的速度渲染100個精靈。
使水桶移動
時間讓使用者控制桶。 之前我們說過我們允許使用者拖動儲存桶。 讓我們讓事情變得更容易一些。 如果使用者觸控式螢幕幕(或按下滑鼠按鈕),我們希望水桶將水平放置在該位置的周圍。 將以下程式碼新增到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()來訪問輸入模組當前是否觸控式螢幕幕(或按下滑鼠按鈕)。 接下來,我們要將觸控/滑鼠座標轉換為我們的相機的座標系。 這是必要的,因為觸控/滑鼠座標的座標系可能與我們用於表示我們世界中的物件的座標系不同。
Gdx.input.getX()和Gdx.input.getY()返回當前的觸控/滑鼠位置(libgdx也支援多點觸控,但這是另一篇文章的主題)。 要將這些座標轉換到我們的攝像機座標系,我們需要呼叫camera.unproject()方法,它需要一個三維向量Vector3作為引數。 我們建立這樣一個向量,設定當前的觸控/滑鼠座標並呼叫方法。 向量現在將包含我們的桶所在座標系中的觸控/滑鼠座標。最後,我們改變桶的位置,以觸控/滑鼠座標為中心。
注意:總是例項化一個新的物件,如Vector3例項這是一個非常糟糕的做法。 因為垃圾收集器必須頻繁地收集這些短命的變數。在桌面應用上可能還好,但是在Android上,GC可能會導致停留時間達幾百毫秒,從而導致卡頓。 為了在這種特殊情況下解決這個問題,我們可以簡單地讓TouchPos成為Drop類的一個全域性變數,而不是一直例項化它。touchPos是一個三維向量。 你可能會想知道為什麼我們只用2D操作但是使用一個三維向量。 OrthographicCamera實際上是一個3D相機,也考慮到z座標。 想想CAD應用程式,他們也使用3D攝相機。 我們只是濫用它來繪製2D圖形。
新增雨滴
對於雨滴,我們設計了一個Rectangle 列表例項,每個例項都跟蹤雨滴的位置和大小。 我們將該列表新增為一個欄位:
private Array<Rectangle> raindrops;
Array類是一個libgdx實用程式類,而不是像ArrayList這樣的標準Java集合。 後者的問題是以各種方式生產垃圾。 Array類嘗試儘量減少垃圾。 Libgdx還提供其他垃圾收集器感知集合,如雜湊圖或集合。
我們還需要記錄上次我們產生的雨滴的時間,所以我們新增另一個欄位:
private long lastDropTime;我們將把時間精確到納秒,這就是為什麼我們使用了long 型別的原因。
為了方便建立雨滴,我們將編寫一個名為spawnRaindrop()的方法,該方法例項化一個新的Rectangle,將其設定為螢幕頂部邊緣的隨機位置,並將其新增到雨滴陣列中。
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()方法中,我們例項化了雨滴陣列,併產生了我們的第一個雨滴:
我們需要在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(); } 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類,都具有dispose()方法,在不再使用後需要手動進行清理。 在我們的例子中,紋理,聲音和音樂以及SpriteBatch都是如此。 作為好的程式設計師,我們複寫ApplicationAdapter.dispose()方法如下:
@Override public void dispose() { dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); }
處理資源後,您不應該以任何方式訪問它。
Disposables (本機資源)通常是無法被Java垃圾收集器處理的。 這就是為什麼我們需要手工處理它們的原因。 Libgdx提供了各種幫助資產管理的方式。 閱讀開發指南的其餘部分來發現它們。
處理暫停/恢復
每當使用者打電話或按住主頁按鈕時,Android都會暫停和恢復您的應用程式。 在這種情況下,Libgdx會為您自動執行許多操作,例如 重新載入可能已經丟失的影象(OpenGL上下文丟失,一個可怕的主題),暫停和恢復音樂流等。
在我們的遊戲中,實際上不需要處理暫停/恢復。 一旦使用者回到應用程式,遊戲就會繼續下去。 通常會實現暫停螢幕,並要求使用者觸控式螢幕幕以繼續。 這是為讀者留下的一個練習 - 檢視ApplicationAdapter.pause()和ApplicationAdapter.resume()方法。