在本文中,我會間接的講解構建一個遊戲引擎和元件的模組,而且我將演示如何使用libgdx庫快速開發一個遊戲原型。
你將學到:
- 建立一個非常簡單的2D射擊平臺遊戲。
- 一個完整的遊戲架構是什麼樣的。
- 在不瞭解OpenGL的情況下如何使用OpenGL的2D圖形技術。
- 不同的實體如何組成了遊戲和在遊戲的世界中它們是如何聯絡在一起的。
- 如何為遊戲加入音效。
- 如何在桌面構建遊戲併發布到Android上,是的,就是那種魔法。
建立遊戲的步驟
- 有一個遊戲的想法
- 把你想象中的遊戲畫面以及樣子在紙上簡單畫出一些場景。
- 分析思路,不斷調整迭代幾個版本,決定遊戲在初始版本中應該有哪些東西
- 選擇一種技術然後開始設計原型
- 開始進行編碼,建立遊戲的資源
- 試玩-測試,不斷改進和持續小步前進進行直至完成
- 美化併發布
遊戲理念
因為這是一個為期一天專案,時間非常有限而且我們的目標是學習製作遊戲的技術,而不是實際的流程。出於這個目的,我冒昧的借鑑了其他遊戲的思路,將重點放在這一過程的技術方面。
我在很大程度上借鑑了一個叫做 星際守護的遊戲,它是Vacuum Flowers製作的一個小的精品遊戲。它是一個很簡單的射擊平臺遊戲,風格簡單並且有街機的感覺。
這個遊戲的想法是帶領我們的英雄殺掉敵人,躲開其他試圖殺死我們的東西來衝過關卡。
操作也非常簡單,方向鍵向左或向右移動英雄,Z是跳躍,X是發射鐳射。按住跳躍鍵的時間越長,英雄跳的越高。可以在空中改變方向和射擊。稍後我們將會把這些東西移動到Android平臺。
接下來的步驟(2和3)可以跳過,因為有現成的遊戲,所以我們已經做完了。
啟動Eclipse
這是我們的起點,我會使用libgdx庫建立遊戲。為什麼是libgdx?它是開發遊戲最好的類庫(在我看來),使開發遊戲很容易而不需要知道太多相關的底層技術。它允許開發者在桌面上建立自己的遊戲,不需要任何更改就可以部署到Android上。它提供了遊戲中使用的所有元素,並且隱藏了與特定的技術和硬體打交道的複雜度。漸漸的這一點會變得越來越明顯。
建立專案的簡單方法
我們將使用Aurelien Ribon的LibGdx Project Setup UI工具。建議按照libgdx的wiki頁面上介紹的步驟來,但是我為星際守護提供一個快速的解決方法。
首先下載可執行的jar檔案:http://libgdx.badlogicgames.com/nightlies/dist/gdx-setup-ui.jar。
雙擊執行,如果不成功,嘗試在jar的下載路徑下執行命令 :
1 2 |
; html-script: false ] java -jar gdx-setup-ui.jar |
你將看到初始化畫面。
點選 建立按鈕,使用顯示的值完成接下來的畫面。
另外,還要確保為專案選擇了合適的目標。
點選高亮的按鈕自動下載最新版本的libgdx(穩定版),等待LibGDX 文字變綠。
一旦變綠,點選右下角的 Open the generation screen 按鈕。
在接下來的視窗中,點選 啟動按鈕等待完成。成功的標誌是顯示”全部成功!”。
專案已經就緒,可以從被選擇建立的目錄匯入到Eclipse中。
在Eclipse中,單擊 檔案->匯入…->通用->工作區間的工程,並且指向專案所在目錄。
單擊 完成會展現出可以被匯入的專案清單,將它們全部匯入。
單擊完成,所有都設定完畢。
困難的設定工程(手動)
首先我們需要下載類庫。
訪問http://libgdx.badlogicgames.com/nightlies/下載檔案ibgdx-nightly-latest.zip,解壓縮。
在Eclipse中建立一個簡單的Java工程,我們叫它 star-assault。
建立工程的時候使用預設配置,右擊它,選擇 新建->資料夾,建立一個名為libs的目錄。
從解壓開的libgdx-nighly-latest目錄中,將gdx.jar檔案拷貝到新建立的libs目錄。同樣把gdx-sources.jar檔案拷貝到libs目錄。它在解壓後的gdx目錄的sources 子目錄下。在eclipse中,你可以簡單的把jar檔案拖拽到你的目錄中做到這點。如果你使用資源管理器、查詢器或者其他方式拷貝,不要忘記按F5重新整理你的Eclipse專案。
這個結構應該像下圖:
新增gdx.jar作為專案的依賴。具體做法是右擊工程的名稱,選擇 屬性。在這個畫面上選擇 Java Build Path,單擊 Libraries 選項卡,單擊 Add JARs…,進入libs 目錄選擇 gdx.jar,然後單擊確定。
為了能夠訪問gdx 原始碼並能夠輕鬆除錯遊戲,新增dx.jar檔案的原始碼是一個好主意。要做到這一點,展開gdx.jar節點,選擇 Source attachment,單擊 Edit…,然後 Workspace…,選擇gdx-sources.jar單擊確定直到關閉所有的彈出視窗。
使用libgdx建立專案的完整文件可以在官方的Wiki上找到。
這個專案是遊戲的核心專案。它將包含遊戲的機制,引擎,其他所有事情。我們還需要建立2個專案,我們的2個目標平臺的基本啟動器。一個用於Android,一個用於桌面。這些專案會極其簡單,只包含在各自平臺執行遊戲所需的依賴關係。把他們當做包含main方法的類。
為什麼我們需要將這些分為獨立的專案?因為libgdx 隱藏了處理底層作業系統(圖形、音訊、使用者輸入、檔案I/O等)的複雜度,每個平臺都有一個具體的實現,我們只需要包含目標平臺上需要的實現(繫結)。也因為應用程式生命週期,資源載入(載入圖形、音訊等)和應用程式的其他常見方面被大大的簡化,平臺的特定實現位於不同的JAR檔案中,只有我們目標平臺需要的檔案才會被新增。
桌面版本
像前面的步驟一樣建立一個簡單的Java工程,命名為star-assault-desktop。同時參照建立libs目錄的步驟。這次從下載的壓縮檔案裡面所需的jar檔案是:
- gdx-natives.jar,
- gdx-backend-lwjgl.jar,
- gdx-backend-lwjgl-natives.jar.
像前面的專案一樣,也要把這些jar檔案新增到專案的依賴中去。(右擊project -> Properties -> Java Build Path -> Libraries -> Add JARs,選擇這3個jar檔案然後單擊確定)。
我們還需要把star-assault專案新增到依賴中。要做到這一點,單擊 Projects選項卡,單擊 新增,選中star-assault專案然後單擊確定。
重要! 我們需要設定star-assault專案為傳遞依賴,這意味著這個專案的依賴關係被當成依賴於本專案的專案依賴關係。要執行此操作:右擊主專案, Properties -> Java Build Path -> Order and Export,選擇gdx.jar然後單擊確定。
Android版本
為此,你需要安裝Android SDK。
在Eclipse中建立一個Android工程: File -> New -> Project -> Android Project。
將它命名為star-assault-android。對於構建版本,選擇”Android 2.3″。指定包名為”net.obviam”或者其它你喜歡的名字。在接下來的”建立Activity”中輸入StarAssaultActivity,點選 完成。
進到專案的路徑下,建立 libs子資料夾(可以在eclipse中完成操作)。從 nightly zip檔案中,拷貝gdx-backend-android.jar檔案和the armeabi 和armeabi-v7a 目錄到新建立的libs目錄下。
在eclipse中,右擊project -> Properties -> Java Build Path -> Libraries -> Add JARs,選擇 gdx-backend-android.jar,單擊OK。
再次單擊 Add JARS,選擇主工程下(star-assault)的gdx.jar,單擊OK。
單擊 專案標籤,單擊 Add,選中主工程然後單擊兩次OK。
下面是展示的結構:
重要!
對於ADT釋出版17或者更新版本,gdx jar檔案需要被顯示的標記為匯出。
具體做法:
- 在Android專案上單擊
- 選擇 Properties
- 選擇 Java Build Path (第一步)
- 選擇 Order and Export(第二步)
- 選擇所有的引用,例如gdx.jar, gdx-backend-android.jar,主專案。(第三步)。
下面的影像顯示了最新的狀態。
另外,在此處查詢有關該問題的詳細資訊。
共享資源(圖片、聲音和其他資料)
因為遊戲需要在Android和桌面版本上保持一致,但是每個版本又需要從獨立的專案構建。我們需要將影像、聲音和其他資料放到一個共享位置。理想的情況是把這些內容放到主專案中,因為Android和桌面版都包含主專案。但是因為Android對於如何存放這些檔案有嚴格的規則,我們必須把資源放到那裡。它就在Android專案自動建立的資源目錄下。在eclipse中,有一種連結目錄的方式,就像 linux/mac上的符號連結或者Windows下的快捷方式。若要將Android專案中的資源目錄連結到桌面版本,請執行以下操作:
右擊 star-assault-desktop 專案->Properties -> Java Build Path -> Source tab -> Link Source… -> Browse… ,瀏覽到star-assault-android專案的assets目錄,然後單擊完成。你也可以擴充套件變數列表而不必瀏覽到assets 目錄。建議將專案設定為檔案系統無關。
另外,確保assets 目錄被新增到原始檔目錄中。具體做法:在eclipse中(桌面版本)的assets目錄上右擊,選擇 Build Path -> Use as Source Folder。
本階段我們的設定已經準備完畢,可以繼續進行遊戲的工作。
建立遊戲
計算機應用程式是一個執行在機器上的軟體,它啟動,做一些事情(甚至什麼都不做),並在一種或另一種條件下停止。電腦遊戲是一種特殊的應用,它在”做一些事情”的部分做的是遊戲。所有應用的開始和結束都基本上是一樣的。另外,遊戲有一個非常簡單的基於連續迴圈的架構。可以在這些地方找到更多關於架構和迴圈的東西。
由於有libgdx,我們的遊戲像在劇場上演戲劇一樣拼湊起來。所有你需要做的就是,把遊戲想象為一個舞臺劇。我們將定義舞臺,演員,角色和行為,但我們把編劇委託給玩家。
為了建立遊戲我們需要採用下面的步驟:
1. 啟動應用
2. 載入所有的圖片和聲音,儲存在記憶體裡
3. 為我們的遊戲建立舞臺,利用演員和它的行為(它們之間的互動規則)
4. 將控制權交給玩家
5. 建立基於從控制器接收的輸入來操作演員在舞臺上的行為的機制
6. 決定遊戲何時結束
7. 結束顯示
它看起來很簡單並且它確實是很這樣。我將會在它們出現的時候介紹概念和元素。
為了建立遊戲,我們僅僅需要1個類。
讓我們在star-assault專案中建立StarAssault.java,這個專案中的每個類都有2個例外。
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 |
; html-script: false ] package net.obviam.starassault; import com.badlogic.gdx.ApplicationListener; public class StarAssault implements ApplicationListener { @Override public void create() { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void render() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } } |
只是實現了gdx中的ApplicationListener介面,eclipse 會自動生成需要實現的方法的存根程式碼。
這些都是我們需要實現的應用程式生命週期的方法。我們簡單的認為需要為Android或者桌面設定所有的程式碼,來初始化OpenGL上下文和所有那些枯燥(困難的)的任務。
方法create()首先被呼叫。發生時機是該應用程式已準備就緒,可以開始載入我們的資源、建立舞臺和演員。想象成在所有的東西都被運到那裡,並準備好後為演出搭建舞臺。根據劇場的位置和你到達的方式,物流肯定是個噩夢。你可以通過人力、飛機、卡車進行運輸…我們不知道。我們在其中所有東西都已經就緒,可以開始著手組裝它們。這就是libgdx幫我們完成的事情。不區分平臺的幫我們搬運並交付東西。
resize()方法在每次繪畫介面改變的時候被呼叫。這讓我們有機會再繼續播放前重新安排程式碼。例如視窗(如果遊戲有視窗)調整時會觸發此事件。
每個遊戲的核心都是render()方法,它無非就是一個無限的迴圈。這個方法被不斷呼叫,直到遊戲結束或者我們終止遊戲。這是遊戲的過程。
注:對於電腦來說遊戲結束與其他程式是不一樣的。所以它只是一個狀態。程式是在遊戲結束的狀態,但它仍然在執行。
當然,遊戲可以被暫停中斷,也可以恢復。每當Android或者桌面應用程式進入到後臺模式時pause()方法會被呼叫。當應用程式重新恢復到前臺時resume()方法被呼叫。
當遊戲完成後,應用程式被關閉時,dispose()方法被呼叫,這是時候做一些清理工作了。它類似於演出已經結束,觀眾已經離開,舞臺將要被拆除。不再回來了。更多關於生命週期的內容可以在這裡找到。
角色
讓我們開始實際的開始做遊戲。第一個里程碑是有一個我們的傢伙可以移動的世界。這個世界是由水平面組成,每個平面都包含地形。地形無非是一些我們的傢伙無法通過的障礙物。
目前為止在遊戲中確定演員和實體很容易。
我們的傢伙(讓我們叫它Bob,libdgx有關於Bob的教程)和障礙物組成了這個世界。
玩過星際守護的話,就會發現Bob有幾個狀態。當我們不操作的時候,Bob處於空閒狀態。他也可以移動(在兩個方向),也能跳。此外,當他死了,就不能做任何事情了。Bob在任何時候的狀態只能是4個確定的狀態中的一個。也還有其他的狀態,但我們現在不考慮。
Bob的狀態:
- 空閒 – 當沒有移動或者跳躍並且或者
- 移動 – 以恆定的速率向左或向右
- 跳躍 – 也是面向左或者右並且有高有低
- 死亡 – 它甚至不可見或者再生
障礙物是其他的演員。為簡單起見,我們也只有幾個障礙物。水平面由放置在一個二維空間內的障礙組成。為簡單起見,我們將使用一個網格。
將Star Guard的開局變為障礙物和Bob的結構,將看起來像這樣:
上面一個圖片是原始的,下面的是我們的世界的表示。
我們已經想像出了世界,但我們需要工作在一個有意義的測量系統。為簡單起見,我們把世界裡的一個塊是當成1個單位寬、1個單位高。我們可以用米來表示使其更簡單,但因為Bob是半個單元,這讓他才半米高。讓我們認定遊戲世界中的4個單位為1米,因此Bob的身高是2米。
這一點很重要,因為我們將計算鮑勃跑動的速度等等,我們需要知道我們在做什麼。
讓我們來建立世界。
我們的主角是Bob。
Bob.java類像這樣:
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 |
; html-script: false ] package net.obviam.starassault.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Bob { public enum State { IDLE, WALKING, JUMPING, DYING } static final float SPEED = 2f; // unit per second static final float JUMP_VELOCITY = 1f; static final float SIZE = 0.5f; // half a unit Vector2 position = new Vector2(); Vector2 acceleration = new Vector2(); Vector2 velocity = new Vector2(); Rectangle bounds = new Rectangle(); State state = State.IDLE; boolean facingLeft = true; public Bob(Vector2 position) { this.position = position; this.bounds.height = SIZE; this.bounds.width = SIZE; } } |
#16-#21行定義了Bob的屬性。這些屬性值定義了任何時間Bob的狀態。
position – Bob在世界中的位置。用世界座標系來表示(後面將詳細討論)。
acceleration – 決定Bob跳躍時的加速度。
velocity – 用來進行計算並用在Bob移動上。
bounds – 遊戲中的每個元素都會有一個邊框。這只是一個矩形,以便知道Bob是否撞到了牆,是否被被子彈殺死或是否擊中敵人。其將被用於碰撞檢測。想像成在玩方體。
state – Bob的當前狀態。當我們發出步行的動作,狀態將變成運動,基於這個狀態,我們知道如何繪製螢幕。
facingLeft – 代表Bob的朝向。作為一個簡單的2D平臺遊戲,我們只有兩個方向,左和右。
#12-#15 行定義了一些常量,我們將用它來計算在世界中的速度和位置。這些將在後面進行調整。
我們還需要一些障礙來組成世界。
Block.java 看起來像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
; html-script: false ] package net.obviam.starassault.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Block { static final float SIZE = 1f; Vector2 position = new Vector2(); Rectangle bounds = new Rectangle(); public Block(Vector2 pos) { this.position = pos; this.bounds.width = SIZE; this.bounds.height = SIZE; } } |
障礙只不過是擺在世界上的一堆矩形。我們將使用這些障礙來構建地形。我們有一個簡單的規則。沒有任何東西可以穿過它們。
libgdx提示
你可能已經注意到我們使用了libgdx的Vector2型別。因為它為我們提供了處理歐幾里得向量需要的所有事情,這使得我們的生活容易了很多。我們將使用向量來定位實體,計算速度,並左右移動東西。
關於座標系統和單位
和現實世界一樣,我們的世界也有尺寸。在一個平面上想象一個房間。它具有寬度、高度和深度。我們把它當做是二維的,不考慮深度。如果房間是5米高,3米寬,我們可以說我們在一個度量系統裡描述房子。很容易想象在房子中間放置1個1米寬1米高的桌子。我們無法通過該桌子、穿過它,我們需要跳到它的上面,走1米,然後跳下來。我們可以使用多個桌子來建立一個金字塔,並在房間裡製作一些奇怪的設計。在我們Star Assault的世界裡,世界代表了現實世界的房間、障礙物代表了現實世界的桌子、單位代表了現實世界的米。
如果我每小時跑10KM,轉換一下就是2.77777778每秒(10 * 1000 / 3600)。將這些轉換成Star Assault的座標,要代表10KM/h的速度,我們將使用2.7單位/秒。
檢查下面的利用世界座標系表示的圖表,裡的有帶著邊界的盒子和Bob。
紅色的方塊是障礙物的邊界框。綠色的方塊是Bob的邊界框。空的方塊只是個空。網格僅供參考。這就是我們將要模擬的世界。座標系的原點位於左下角,所以向左走10,000單位/小時意味著Bob位置的x座標將以2.7單位每秒的速度減少。
還要注意的是成員的訪問許可權是包預設許可權,模型在一個單獨的包裡。我們要想從引擎中訪問,必須要建立存取方法(getter和setter)。
建立世界
作為第一步,我們將只建立世界作為一個硬編碼的小房間。它有10單位寬和7單位高。我們將像下面顯示的圖片一樣放置Bob和障礙物。
World.java看起來像這樣:
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 |
; html-script: false ] package net.obviam.starassault.model; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class World { /** The blocks making up the world **/ Array blocks = new Array(); /** Our player controlled hero **/ Bob bob; // Getters ----------- public Array getBlocks() { return blocks; } public Bob getBob() { return bob; } // -------------------- public World() { createDemoWorld(); } private void createDemoWorld() { bob = new Bob(new Vector2(7, 2)); for (int i = 0; i < 10; i++) { blocks.add(new Block(new Vector2(i, 0))); blocks.add(new Block(new Vector2(i, 7))); if (i > 2) blocks.add(new Block(new Vector2(i, 1))); } blocks.add(new Block(new Vector2(9, 2))); blocks.add(new Block(new Vector2(9, 3))); blocks.add(new Block(new Vector2(9, 4))); blocks.add(new Block(new Vector2(9, 5))); blocks.add(new Block(new Vector2(6, 3))); blocks.add(new Block(new Vector2(6, 4))); blocks.add(new Block(new Vector2(6, 5))); } } |
它只是一個簡單的世界的實體的容器類。當前實體是障礙物和Bob。在構造器中,障礙物被新增到blocks陣列,並且Bob被建立。暫時這一切都是硬編碼的。請記住,原點在左下角。
建立遊戲並顯示世界
為了把世界渲染到螢幕上,我們需要建立一個畫面,並讓它來渲染這個世界。在libgdx中,有一個便捷的叫做Game的類,我們將重寫StarAssault類作為libgdx的Game類的子類。
關於畫面
一個遊戲可以包含多個畫面。甚至我們的遊戲將包含3個畫面。 開始遊戲畫面, 播放遊戲畫面, 遊戲結束畫面。每個畫面都只是關注自己的事情而不關注其他畫面的。例如 開始遊戲畫面將會包含選單選項 開始 和 退出 。它有2個元素(按鈕),它關注的是處理這些元素上的 單擊/觸控 。它始終顯示這兩個按鈕,如果我們單擊/觸控 開始 按鈕,它會通知主遊戲載入 播放遊戲畫面 並退出當前畫面。播放畫面將執行我們的遊戲,並處理相關的所有事情。一旦遊戲滿足結束狀態,它告訴主遊戲過渡到遊戲結束畫面,後者的唯一目的是顯示最高得分,並監聽重新開始的單擊事件。
讓我們重構程式碼並暫時只建立遊戲的主畫面。我們將會跳過遊戲的開始和結束畫面。
GameScreen.java
1 |
; html-script: false ] |
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 |
; html-script: false ] package net.obviam.starassault.screens; import com.badlogic.gdx.Screen; public class GameScreen implements Screen { @Override public void render(float delta) { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void show() { // TODO Auto-generated method stub } @Override public void hide() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } } |
StarAssault.java就變得非常簡單。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
; html-script: false ] package net.obviam.starassault; import net.obviam.starassault.screens.GameScreen; import com.badlogic.gdx.Game; public class StarAssault extends Game { @Override public void create() { setScreen(new GameScreen()); } } |
GameScreen 實現了Screen介面,該介面非常像ApplicationListener,但是新增了2個重要的方法。
show() – 當主程式啟用此畫面的時候呼叫
hide() – 當主程式啟用其他畫面的時候呼叫
StarAssault 只實現了一個方法,create()方法只是啟用了新例項化的GameScreen。換句話說,它建立了它,呼叫show()方法,隨後每個週期將呼叫它的render()方法。
GameScreen變成了我們下一部分的焦點,因為它是遊戲存活的地方。記住遊戲的迴圈是render()方法。但是為了渲染某些東西我們首先需要建立一個世界。世界可以在show()方法中建立,因為沒有其他的畫面會打斷我們的遊戲。目前,遊戲畫面僅在遊戲開始的時候顯示。
我們會為類新增2個成員變數,並實現render(float delta) 方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
; html-script: false ] private World world; private WorldRenderer renderer; /** Rest of methods ommited **/ @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); renderer.render(); } |
world屬性是World的例項,它儲存了障礙物和Bob。
renderer是一個將world 繪製/渲染到畫面的類(不久我將解釋)。
方法 render(float delta)。
讓我們建立WorldRenderer 類。
WorldRenderer.java:
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 |
; html-script: false ] package net.obviam.starassault.view; import net.obviam.starassault.model.Block; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.World; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private World world; private OrthographicCamera cam; /** for debug rendering **/ ShapeRenderer debugRenderer = new ShapeRenderer(); public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(10, 7); this.cam.position.set(5, 3.5f, 0); this.cam.update(); } public void render() { // render blocks debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Line); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x + rect.x; float y1 = block.getPosition().y + rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // render Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x + rect.x; float y1 = bob.getPosition().y + rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } } |
該WorldRenderer只有一個目的。獲取世界的當前狀態,並呈現在螢幕上。它只有單獨一個公共的render()方法,該方法被主迴圈呼叫(GameScreen中)。渲染器需要訪問世界物件,所以我們在構造它的時候將world傳入。對於第一步,我們將呈現元素(塊和Bob)的邊框來看看我們到現在有了什麼。利用原生的OpenGL繪圖是相當乏味,但libgdx自帶的ShapeRenderer使這個任務變得很容易。
重要的行進行說明。
#14 – 宣告world 為一個成員變數。
#15 – 宣告一個OrthographicCamera,我們使用這臺攝像機以垂直的角度看世界。當前世界還是比較小的,它只佔了一個螢幕,但我們會有一個更廣泛的空間,當Bob在裡面移動的時候,攝像機會跟隨著它。它類似於現實生活中的相機。更多關於正交投影的內容可以在這裡找到。
#18 – 宣告瞭ShapeRenderer。我們將用它來繪製實體的圖元(矩形)。這是一個輔助渲染器,可以像畫很多圖元,比方說直線,矩形,圓。對於熟悉基於圖形的畫布的人來說,這應該很簡單。
#20 – 構造器以world作為引數。
#22 – 我們建立了一個擁有10單位寬、7單位高視口的攝像機。這也就意味著在用塊(寬=高=1)來填充螢幕的時候,X軸上顯示10個框,Y軸上顯示7個。
重要說明 這與解析度無關。如果螢幕解析度為480×320,這意味著480畫素代表10個單位,所以一個框將是48個畫素寬。這也意味著,320畫素代表7個單位,所以在螢幕上盒框是45.7畫素高。這不會是一個完美的正方形。這取決於縱橫比。在我們這個例子裡縱橫比為10比7。
#23 – 這一行定位攝像機來看在房子的中間。預設情況下,它看在(0,0),這是房間的一角。相機的(0,0)在中間,就和一個普通相機一樣。下圖顯示了世界和攝像機設定的座標。
#24 – 相機內部的矩陣被更新。 在每次相機操作(移動,縮放,旋轉等)的時候都必須呼叫update方法。 OpenGL被完美隱藏。
render() 方法:
#29 – 我們將相機裡的矩陣應用到渲染器。這是有必要的,因為我們已經定位了相機,我們希望它們是一致的。
#30 – 告訴渲染器我們要繪製矩形。
#31 – 我們要繪製塊,所以要迭代訪問世界裡的所有塊。
#32 – #34 – 提取每個塊的邊界矩形的座標。 OpenGL要使用頂點(點),所以它繪製矩形時必須知道起點座標和寬度。需要注意,我們在使用攝像機的座標工作,它與世界座標重合。
#35 – 矩形的顏色設定為紅色。
#36 – 在X1,Y1位置利用給定的寬度和高度繪製矩形
#39 – #44 – 對於Bob做同樣的操作。但這次的矩形是綠色的。
#45 – 我們讓渲染器知道我們已經繪製完矩形。
我們需要將renderer和world新增到GameScreen(主迴圈),然後在行動中看到它。
像這樣修改GameScreen。
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 |
; html-script: false ] package net.obviam.starassault.screens; import net.obviam.starassault.model.World; import net.obviam.starassault.view.WorldRenderer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL10; public class GameScreen implements Screen { private World world; private WorldRenderer renderer; @Override public void show() { world = new World(); renderer = new WorldRenderer(world); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); renderer.render(); } /** ... rest of method stubs omitted ... **/ } |
render(float delta)有3行程式碼。前2行程式碼用黑色清空螢幕,第3行程式碼簡單的呼叫renderer的render()方法。
World 和 WorldRenderer 在畫面顯示的時候被呼叫。
為了在桌面和Android上測試,我們需要為這兩個平臺建立啟動器。
建立桌面和Android的啟動器
我們在開始的時候已經建立了2個專案,star-assault-desktop和star-assault-android,後者是一個Android專案。
桌面專案非常簡單,我們需要建立一個擁有main方法的類,該方法例項化一個libgdx提供的application類。
在桌面專案中建立StarAssaultDesktop.java類。
1 2 3 4 5 6 7 8 9 10 |
; html-script: false ] package net.obviam.starassault; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; public class StarAssaultDesktop { public static void main(String[] args) { new LwjglApplication(new StarAssault(), "Star Assault", 480, 320, true); } } |
這就是了。第 #7 行做了所有事情。它例項化了一個新的LwjglApplication應用程式,傳入一個新生成的StarAssault例項,它是一個遊戲的實現。第二和第三引數告訴了視窗的尺寸。我選擇了480×320,因為它是眾多Android手機的支援的解析度,我想就像它在桌面上。最後一個引數告訴libgdx使用OpenGL ES2。
作為一個普通的Java程式執行該應用,應該輸出以下結果:
如果有錯誤,追溯一下,確保設定的正確並且遵循了所有的步驟,包括檢查star-guard專案properties -> Build Path->export選項卡內的gdx.jar。
Android版本
在 star-assault-android 專案裡有一個單獨的類,叫做StarAssaultActivity。
轉到StarAssaultActivity。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
; html-script: false ] package net.obviam.starassault; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; public class StarAssaultActivity extends AndroidApplication { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); config.useAccelerometer = false; config.useCompass = false; config.useWakelock = true; config.useGL20 = true; initialize(new StarAssault(), config); } } |
注意,新的activity 繼承自AndroidApplication。
在 #13行,一個AndroidApplicationConfiguration物件對建立。我們可以設定有關Android平臺的所有型別的配置。他們都不需要解釋,但要注意,如果我們想要使用Wakelock,還需要修改AndroidManifest.xml檔案。這要從Android中請求許可權,以保持裝置執行,防止我們不觸碰的時候螢幕變暗。
新增以下行到AndroidManifest.xml檔案裡標籤內的某個地方。
1 2 |
; html-script: false ] 原文中沒有此XML內容:( |
另外,在 #17行,我們告訴Android使用的OpenGL ES 2.0。這意味著我們將只能在裝置進行測試,因為模擬器不支援OpenGL ES 2.0。但如果是出了問題,把它改為false。
#18行初始化Android應用程式並執行。
有一個連線到eclipse的裝置,應用就會被直接部署到上面,下面你可以看到一張在Nexus One上執行應用程式的照片。它看起來和桌面版本一樣。
MVC 模式
在這麼短的時間裡我們走了這麼遠真是相當令人印象深刻啊。注意使用MVC模式。它非常簡單和有效。模型是我們想要顯示的實體。檢視是渲染器。檢視把模型繪製到了螢幕上。現在,我們需要和實體(尤其是Bob)進行互動,我們將介紹一些控制器了。
要了解更多的MVC模式的東西,可以看看我的其他文章,或在網路上進行搜尋。這是非常有用的。
新增圖片
到目前為止,一切都很好,但顯然我們要使用一些適當的圖形。 MVC的功能就派上用場了,我們將修改渲染,讓它繪製影像,而不是矩形。
在OpenGL中顯示影像是一個相當複雜的過程。首先,它需要被載入,變成紋理,然後被對映到被幾何描述的表面。 libgdx使得這個過程變得非常簡單。要硬碟中的影像轉換成一個紋理只用1行程式碼。
我們將用2個影像,因此也就是2個紋理。一個紋理用於Bob,另一個用於塊。我已經建立了這兩個影像,塊和Bob。 Bob是Star Guard裡的傢伙的山寨。這些都是簡單的PNG檔案,我將它們複製到 assets/images目錄裡。我有兩個影像: block.png和bob_01.png 。最終,Bob將成為一個活動的形象,所以我加了數字字尾(為未來考慮)。
首先讓我們稍微整理一下WorldRenderer,把矩形的繪製提取到一個單獨的方法中,因為我們除錯的時候將用到它。
我們需要載入這些紋理,並把它們渲染在螢幕上。
一起來看看新WorldRenderer.java。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
; html-script: false ] package net.obviam.starassault.view; import net.obviam.starassault.model.Block; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.World; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private static final float CAMERA_WIDTH = 10f; private static final float CAMERA_HEIGHT = 7f; private World world; private OrthographicCamera cam; /** for debug rendering **/ ShapeRenderer debugRenderer = new ShapeRenderer(); /** Textures **/ private Texture bobTexture; private Texture blockTexture; private SpriteBatch spriteBatch; private boolean debug = false; private int width; private int height; private float ppuX; // pixels per unit on the X axis private float ppuY; // pixels per unit on the Y axis public void setSize (int w, int h) { this.width = w; this.height = h; ppuX = (float)width / CAMERA_WIDTH; ppuY = (float)height / CAMERA_HEIGHT; } public WorldRenderer(World world, boolean debug) { this.world = world; this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT); this.cam.position.set(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f, 0); this.cam.update(); this.debug = debug; spriteBatch = new SpriteBatch(); loadTextures(); } private void loadTextures() { bobTexture = new Texture(Gdx.files.internal("images/bob_01.png")); blockTexture = new Texture(Gdx.files.internal("images/block.png")); } public void render() { spriteBatch.begin(); drawBlocks(); drawBob(); spriteBatch.end(); if (debug) drawDebug(); } private void drawBlocks() { for (Block block : world.getBlocks()) { spriteBatch.draw(blockTexture, block.getPosition().x * ppuX, block.getPosition().y * ppuY, Block.SIZE * ppuX, Block.SIZE * ppuY); } } private void drawBob() { Bob bob = world.getBob(); spriteBatch.draw(bobTexture, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bob.SIZE * ppuY); } private void drawDebug() { // render blocks debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Line); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x + rect.x; float y1 = block.getPosition().y + rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // render Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x + rect.x; float y1 = bob.getPosition().y + rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } } |
我來講一下重要的程式碼行:
#17 & #18 – 宣告視口尺寸的常量,用於攝像機。
#27 & #28 – 宣告2個紋理,將用於Bob和塊。
#30 – 宣告SpriteBatch,SpriteBatch 負責紋理的對映、顯示等等。
#31 – 這是一個在建構函式中設定的屬性,確定我們是否也需要渲染除錯畫面。記住,除錯渲染只會渲染遊戲元素的邊框。
#32 – #35 – 這些變數都是正確顯示元素所必須的。width和height儲存了螢幕的畫素大小,是作業系統在調整尺寸的步驟裡傳進來的。ppuX 和ppuY是每個單位長度的畫素的數量。
因為我們將相機設定為在世界座標中具有10×7的視口(這意味著我們橫向顯示10個框和垂直顯示7個),最終結果是我們要處理畫素,我們需要將這些值對映為實際的畫素座標。我們選擇了在480 ×320解析度下工作。這意味著,480水平畫素等效為10個單位,意味著一個單位將包括螢幕上48個畫素。
如果我們試圖用相同的單位來表示高度( 48畫素),我們得到336畫素( 48 * 7 = 336)。但我們只有320畫素,而我們想要顯示完整的7塊的高度。要垂直方向達到這種顯示效果,我們得到垂直的1個單元是320 /7 = 45.71個畫素。我們需要稍微扭曲一下影像以適應我們的世界。
這樣做完全正常,OpenGL實現起來也非常容易。在我們改變電視的長寬比時也會發生這種情況,有時影像被拉長或壓扁,以適應螢幕上,或者我們只是簡單地選擇削減影像保持縱橫比的選項。
注意:在這裡我們使用了float,即使螢幕解析度為整數,OpenGL喜歡使用浮點數,我們就這樣給它。 OpenGL的將計算出尺寸和放置畫素的地方。
#36 – setSize (int w, int h) 方法在每次螢幕調整的時候被呼叫,它簡單的計算(重新計算)單位的畫素。
#43 – 建構函式稍微改變了一下,但它是非常重要的。它例項化SpriteBatch並載入紋理(#50行 ) 。
#53 – loadTextures()方法和它的名字一樣:載入紋理。你看這是多麼的簡單。要建立一個紋理,我們需要傳遞一個檔案處理器,它建立了一個紋理出來。在libgdx檔案處理器非常有用,因為我們並不區分Android和桌面,我們只是指定要使用一個內部檔案,它知道如何載入它。請注意,對於路徑,我們跳過了assets,因為assets是原始碼目錄,這意味著該目錄下的所有內容都會被拷貝到最終包的根目錄下。這樣assets類似於一個根目錄。
#58 – 新的render()方法只包含幾行。
#59 & #62 – 閉合SpriteBatch的渲染塊/會話。 每次我們想通過SpriteBatch 來在OpenGL裡渲染影像時,都需要呼叫begin(),渲染我們的東西,然後在結束的時候呼叫end()。必須要這樣做,否則它無法工作。你可以在這裡閱讀更多關於SpriteBatch的東西。
#60 & #61 –簡單地呼叫2個方法,首先呈現塊,然後Bob。
#63 & #64 – 如果除錯啟用,呼叫該方法來渲染框。前面詳細的介紹過drawDebug方法。
#67 – #76 – drawBlocks和drawBob方法類似。每個方法都使用紋理引數呼叫SpriteBatch的繪製方法。理解這一點很重要的。
第一個引數是紋理(從磁碟載入的影像)。
第二個和第三個引數告訴SpriteBatch顯示影像的地方。注意,我們使用從世界座標到螢幕座標的轉換。這裡使用了ppuX和ppuY。你可以自己動手進行計算,看看影像應該顯示在什麼地方。SpriteBatch預設使用原點(0,0)在左下角的座標系統。
就這些了。只是要保證你修改了GameScreen類,讓resize方法呼叫render,同時還要把render的debug引數設定為true。
GameScreen中的修改。
1 2 3 4 5 6 7 8 9 10 11 |
; html-script: false ] /** ... omitted ... **/ public void show() { world = new World(); renderer = new WorldRenderer(world, true); } public void resize(int width, int height) { renderer.setSize(width, height); } /** ... omitted ... **/ |
執行該應用程式應該產生以下結果:
沒有除錯。
除錯渲染。
太棒了!給它在Android上一試了,看看它的外觀。
處理桌面端和Android的輸入
我們已經走過了很長的路,但到目前為止,世界還是靜止的,沒有什麼有趣的事情。要做一個遊戲,我們需要新增輸入處理,來攔截按鍵和觸控,並基於這些建立一些行動。
桌面上的控制模式非常簡單。箭頭鍵將控制Bob左右移動,Z是跳躍,X是發射武器。在Android上,我們將有不同的方法。我們將為這些功能指定一些按鈕,並把它們放到螢幕上,通過按壓相應區域,我們會認為某個鍵被按下。
遵循MVC模式,我們會將控制Bob和世界其他部分的類與模型和檢視類分開。建立包net.obviam.starassault.controller,所有的控制器都放在那裡。
對於一開始,我們將控制鮑勃通過按鍵。要發揮我們需要跟蹤的4個按鍵的狀態比賽:向左移動,向右移動,跳躍和射擊。因為我們將使用2種型別的輸入(鍵盤和觸控式螢幕),實際事件需要被送入一個處理器可以觸發動作。
每個動作都由事件觸發。
向左移動的動作由左箭頭鍵按下或螢幕的特定區域被觸控時的事件引發。
當Z鍵被按下時跳躍動作被觸發等等。
讓我們建立一個非常簡單的控制器,叫做WorldController。
WorldController.java
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
; html-script: false ] package net.obviam.starassault.controller; import java.util.HashMap; import java.util.Map; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.Bob.State; import net.obviam.starassault.model.World; public class WorldController { enum Keys { LEFT, RIGHT, JUMP, FIRE } private World world; private Bob bob; static Map<Keys, Boolean> keys = new HashMap<WorldController.Keys, Boolean>(); static { keys.put(Keys.LEFT, false); keys.put(Keys.RIGHT, false); keys.put(Keys.JUMP, false); keys.put(Keys.FIRE, false); }; public WorldController(World world) { this.world = world; this.bob = world.getBob(); } // ** Key presses and touches **************** // public void leftPressed() { keys.get(keys.put(Keys.LEFT, true)); } public void rightPressed() { keys.get(keys.put(Keys.RIGHT, true)); } public void jumpPressed() { keys.get(keys.put(Keys.JUMP, true)); } public void firePressed() { keys.get(keys.put(Keys.FIRE, false)); } public void leftReleased() { keys.get(keys.put(Keys.LEFT, false)); } public void rightReleased() { keys.get(keys.put(Keys.RIGHT, false)); } public void jumpReleased() { keys.get(keys.put(Keys.JUMP, false)); } public void fireReleased() { keys.get(keys.put(Keys.FIRE, false)); } /** The main update method **/ public void update(float delta) { processInput(); bob.update(delta); } /** Change Bob's state and parameters based on input controls **/ private void processInput() { if (keys.get(Keys.LEFT)) { // left is pressed bob.setFacingLeft(true); bob.setState(State.WALKING); bob.getVelocity().x = -Bob.SPEED; } if (keys.get(Keys.RIGHT)) { // left is pressed bob.setFacingLeft(false); bob.setState(State.WALKING); bob.getVelocity().x = Bob.SPEED; } // need to check if both or none direction are pressed, then Bob is idle if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) || (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) { bob.setState(State.IDLE); // acceleration is 0 on the x bob.getAcceleration().x = 0; // horizontal speed is 0 bob.getVelocity().x = 0; } } } |
#11 – #13 – 為Bob執行的動作定義一個列舉。每一次按鍵/觸控都會觸發一個動作。
#15 – 定義遊戲中世界。我們將控制世界裡面存在的實體。
#16 – 定義Bob作為私有成員,它只是世界裡面的Bob的一個引用,但是我們需要它,因為每次引用它比在需要它的時候每次都獲取要容易。
#18 – #24 – 它是一個靜態的儲存了鍵和狀態的雜湊對映。如果鍵被按下,則為true,否則為false。它被靜態的初始化了。這個對映會用來在控制器的update方法中計算如何處理Bob。
#26 – 這是使用World 作為引數的構造器,同時也得到了Bob的引用。
#33 – #63 – 這些方法只是簡單的進行回撥,在動作按鈕被按下或者在指定的區域觸控。這些方法在我們進行任何輸入的時候都會被呼叫。它們簡單地設定對映內對應的按鍵按下的值。正如你所看到的,控制器也是一個狀態機,它的狀態是由按鍵對映決定的。
#66 – #69 –在主迴圈每次呼叫時都會呼叫的更新方法。目前它做了2件事情:1-處理輸入,2-更新Bob。Bob有一個專門的更新方法,後面我們會看到。
#72 – #92 – ProcessInput方法輪詢鍵對映的鍵,並由此來設定Bob的值。例如#73 – #78行,檢查向左運動的按鍵被按下,如果是這樣,設定Bob面朝左邊、狀態為State.WALKING但它的速度是負值。負值是因為在畫面上,左為負方向(原點在左下方並且指向右側)。
向右也是一樣。有一些額外的檢查,如果這兩個鍵被按下或都沒有按下,在此情況下,Bob變成State.IDLE狀態,它的水平速度為0。
讓我們看看Bob.java中改變的部分。
1 2 3 4 5 6 7 8 9 10 |
; html-script: false ] public static final float SPEED = 4f; // unit per second public void setState(State newState) { this.state = newState; } public void update(float delta) { position.add(velocity.cpy().scl(delta)); } |
只是把SPEED常量改為4單位(塊)/秒。
還新增了setState方法,因為我之前忘了。
最有意思的是新獲得的update(float delta),它在WorldController中被呼叫。本方法簡單的根據Bob的速度更新它的位置。簡單起見,我們只做了這麼多而沒有檢查它的狀態,因為控制器負責根據Bob的方向和狀態設定它的速度。在這裡我們使用了向量數學,libgdx起了很大作用。
我們簡單的把Bob的當前位置加上delta秒內移動的距離。我們使用velocity.tmp(),因為tmp()方法建立了一個與速度物件具有相同值的新物件,我們把這個物件的值與所經過的時間delta相乘。在java中我們必須小心的使用引用,因為速度和位置都是Vector2物件。關於向量的更多的資訊請訪問這裡。
我們幾乎做了所有的事情,我們只需要在事件發生的時候呼叫正確。 libgdx有一個輸入處理器,它有幾個回撥方法。因為我們在使用GameScreen 作為遊戲介面,它自然就成為了輸入處理器。要做到這一點,GameScreen中將實現InputProcessor。
新的GameScreen.java
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
; html-script: false ] package net.obviam.starassault.screens; import net.obviam.starassault.controller.WorldController; import net.obviam.starassault.model.World; import net.obviam.starassault.view.WorldRenderer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL10; public class GameScreen implements Screen, InputProcessor { private World world; private WorldRenderer renderer; private WorldController controller; private int width, height; @Override public void show() { world = new World(); renderer = new WorldRenderer(world, false); controller = new WorldController(world); Gdx.input.setInputProcessor(this); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); controller.update(delta); renderer.render(); } @Override public void resize(int width, int height) { renderer.setSize(width, height); this.width = width; this.height = height; } @Override public void hide() { Gdx.input.setInputProcessor(null); } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { Gdx.input.setInputProcessor(null); } // * InputProcessor methods ***************************// @Override public boolean keyDown(int keycode) { if (keycode == Keys.LEFT) controller.leftPressed(); if (keycode == Keys.RIGHT) controller.rightPressed(); if (keycode == Keys.Z) controller.jumpPressed(); if (keycode == Keys.X) controller.firePressed(); return true; } @Override public boolean keyUp(int keycode) { if (keycode == Keys.LEFT) controller.leftReleased(); if (keycode == Keys.RIGHT) controller.rightReleased(); if (keycode == Keys.Z) controller.jumpReleased(); if (keycode == Keys.X) controller.fireReleased(); return true; } @Override public boolean keyTyped(char character) { // TODO Auto-generated method stub return false; } @Override public boolean touchDown(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftPressed(); } if (x > width / 2 && y > height / 2) { controller.rightPressed(); } return true; } @Override public boolean touchUp(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftReleased(); } if (x > width / 2 && y > height / 2) { controller.rightReleased(); } return true; } @Override public boolean touchDragged(int x, int y, int pointer) { // TODO Auto-generated method stub return false; } @Override public boolean mouseMoved(int x, int y) { // TODO Auto-generated method stub return false; } @Override public boolean scrolled(int amount) { // TODO Auto-generated method stub return false; } } |
變化如下:
#13 – 該類實現了InputProcessor。
#19 – Android觸控事件使用的寬度和高度。
#25 – 利用world例項化WorldController。
#26 – 設定當前畫面作為應用程式的當前輸入處理器。libgdx 把輸入處理器當作全域性的,所以如果不共享的話每個畫面都需要單獨設定。在這種情況下,畫面本身處理輸入。
#47 & #62 – 我們設定全域性的輸入處理器為null只是為了清理。
#68 – 每當物理鍵盤上的鍵被按下時都會觸發keyDown(int keycode)方法。引數keycode就是按鍵的值,在這種方式下,我們可以查詢它,如果是我們期望的鍵就做一些操作。這正是事情的原理。基於我們想要的鍵,我們把事件傳遞給控制器。該方法也返回true,讓輸入器知道輸入已經被處理。
#81 – keyUp 與keyDown 方法完全相反,當按鍵被釋放時,它只是委託給WorldController。
#111 – #118 – 這裡就讓它變得有趣了。它只在觸控式螢幕上發生,座標和按鈕同時被傳入。指標是多點觸控,代表了它捕獲的觸控事件的ID。
這些控制都非常簡單,只是為了簡單的演示之用。螢幕被分為4部分,如果點觸左下象限,那麼會被當作觸發左移的操作,並且把與桌面版相同的事件傳遞給控制器。
對於touchUp也是完全一樣的東西。
警告: -這是非常錯誤並且不可靠的,因為沒有實現touchDragged方法,當手機劃過的時候會搞亂所有的事情。這當然會被修復,其目的是為了演示多個硬體輸入和如果把它們關聯在一起。
在桌面版和Android上執行程式都會演示控制。在桌面版上使用方向鍵,在Android上觸控螢幕下部角落將移動Bob。
在桌面版上,你會發現使用滑鼠來模擬觸控也可以用。這是因為touchXXX 也處理桌面上的滑鼠輸入。為了解決這個問題,在touchDown和touchUp方法的開始處新增下面的程式碼。
1 2 3 |
; html-script: false ] if (!Gdx.app.getType().equals(ApplicationType.Android)) return false; |
如果應用程式不是Android,程式返回false並且不執行方法其餘部分的程式碼。請記住,false意味著輸入沒有被處理。
正如我們看到的,Bob移動了。
簡短回顧
到目前為止,我們介紹了不少遊戲開發的東西,並且已經做出來一個可以展示的東西。
我們逐漸為我們的應用引入了工作元素,並且一步一步的做了些東西。
我們仍需要新增:
* 地形的互動(塊碰撞,跳躍)
* 動畫
* 跟隨Bob的鏡頭
* 敵人和攻擊敵人的武器
* 音效
* 控制的定義和微調
* 遊戲結束和開始的更多畫面
* libgdx更有趣東西
本專案的原始碼可以在這裡找到:
https://github.com/obviam/star-assault
分支是 part1。
用git檢出:
git clone -b part1 git@github.com:obviam/star-assault.git
你也可以下載一個壓縮檔案。
你也可以進入這個系列的下一篇文章,在裡面我為Bob加入了動畫。