使用MVC模式制作游戏-教程和简介
它有助于分離輸入邏輯,游戲邏輯和UI(渲染)。 在任何游戲開發(fā)項目的早期階段,其實用性很快就會被注意到,因為它允許快速更改內(nèi)容,而無需在應(yīng)用程序的所有層中進行過多的代碼重做。
下圖是模型視圖控制器概念的最簡單邏輯表示。
| 模型-視圖-控制器模式 |
用法示例
在玩家控制機器人的示例游戲中,可能會發(fā)生以下情況:
- 1 –用戶單擊/輕擊屏幕上的某個位置。
- 2 – 控制器處理單擊/輕擊并將事件轉(zhuǎn)換為適當(dāng)?shù)牟僮鳌?例如,如果地形被敵人占領(lǐng),則會創(chuàng)建攻擊動作;如果地形為空,則會創(chuàng)建移動動作,最后,如果用戶輕拍的地方被障礙物占據(jù),則不執(zhí)行任何操作。
- 3 – 控制器相應(yīng)地更新機器人 ( 模型 )的狀態(tài)。 如果創(chuàng)建了移動動作,那么它將改變位置,如果發(fā)起了攻擊,則將射擊。
- 4 – 渲染器 ( 視圖 )收到有關(guān)狀態(tài)更改的通知,并渲染世界的當(dāng)前狀態(tài)。
這一切意味著,模型(機器人)對如何繪制自己或如何更改其狀態(tài)(位置,命中點)一無所知。 他們是愚蠢的實體。 在Java中,它們也稱為POJO(普通的舊Java對象)。
控制器負(fù)責(zé)更改模型的狀態(tài)并通知渲染器。
為了繪制模型,渲染器必須引用模型(機器人和任何其他實體)及其狀態(tài)。
從典型的游戲架構(gòu)中我們知道, 主循環(huán)充當(dāng)超級控制器,超級控制器更新狀態(tài),然后每秒將對象呈現(xiàn)到屏幕上多次。 我們可以將所有更新和渲染與機器人一起放入主循環(huán),但這很麻煩。 讓我們確定游戲的不同方面(關(guān)注點)。
型號
- 玩家控制的機器人
- 機器人可以移動的競技場
- 一些障礙
- 一些敵人要開槍
控制器
- 主循環(huán)和輸入處理程序
- 控制器處理玩家輸入
- 在玩家的機器人上執(zhí)行動作(移動,攻擊)的控制器
觀點
- 世界渲染器–將對象渲染到屏幕上
創(chuàng)建項目
為簡單起見,我這次選擇了applet,并將嘗試使其簡短。 該項目具有以下結(jié)構(gòu):
| MVC –項目結(jié)構(gòu) |
文件Droids.java是applet,包含主循環(huán)。
package net.obviam.droids;import java.applet.Applet; import java.awt.Color; import java.awt.Event; import java.awt.Graphics; import java.awt.image.BufferedImage;public class Droids extends Applet implements Runnable {private static final long serialVersionUID = -2472397668493332423L;public void start() {new Thread(this).start();}public void run() {setSize(480, 320); // For AppletViewer, remove later.// Set up the graphics stuff, double-buffering.BufferedImage screen = new BufferedImage(480, 320, BufferedImage.TYPE_INT_RGB);Graphics g = screen.getGraphics();Graphics appletGraphics = getGraphics();long delta = 0l;// Game loop.while (true) {long lastTime = System.nanoTime();g.setColor(Color.black);g.fillRect(0, 0, 480, 320);// Draw the entire results on the screen.appletGraphics.drawImage(screen, 0, 0, null);// Lock the frame ratedelta = System.nanoTime() - lastTime;if (delta < 20000000L) {try {Thread.sleep((20000000L - delta) / 1000000L);} catch (Exception e) {// It's an interrupted exception, and nobody cares}}if (!isActive()) {return;}}}public boolean handleEvent(Event e) {return false;} } 將上述代碼作為applet運行,無非是設(shè)置主循環(huán)并將屏幕涂成黑色。
結(jié)構(gòu)中有3個程序包,各個組件都將放在那兒。
net.obviam.droids.model將包含所有模型
net.obviam.droids.view將包含所有渲染器
net.obviam.droids.controller將包含所有控制器
創(chuàng)建模型
機器人
Droid.java
package net.obviam.droids.model;public class Droid {private float x;private float y;private float speed = 2f;private float rotation = 0f;private float damage = 2f;public float getX() {return x;}public void setX(float x) {this.x = x;}public float getY() {return y;}public void setY(float y) {this.y = y;}public float getSpeed() {return speed;}public void setSpeed(float speed) {this.speed = speed;}public float getRotation() {return rotation;}public void setRotation(float rotation) {this.rotation = rotation;}public float getDamage() {return damage;}public void setDamage(float damage) {this.damage = damage;} } 它是一個簡單的Java對象,對周圍世界一無所知。 它具有位置,旋轉(zhuǎn),速度和損壞。 這些狀態(tài)由成員變量定義,可通過getter和setter方法訪問。
游戲需要更多模型:地圖上的障礙物和敵人。 為簡單起見,障礙物將僅在地圖上定位,而敵人將是站立的物體。 該地圖將是一個二維數(shù)組,其中包含敵人,障礙物和機器人。 該地圖將被稱為Arena以區(qū)別于標(biāo)準(zhǔn)Java地圖,并且在構(gòu)建地圖時會填充障礙物和敵人。 Obstacle.java
Enemy.java
package net.obviam.droids.model;public class Enemy {private float x;private float y;private int hitpoints = 10;public Enemy(float x, float y) {this.x = x;this.y = y;}public float getX() {return x;}public float getY() {return y;}public int getHitpoints() {return hitpoints;}public void setHitpoints(int hitpoints) {this.hitpoints = hitpoints;} }Arena.java
package net.obviam.droids.model;import java.util.ArrayList; import java.util.List; import java.util.Random;public class Arena {public static final int WIDTH = 480 / 32;public static final int HEIGHT = 320 / 32;private static Random random = new Random(System.currentTimeMillis());private Object[][] grid;private List<Obstacle> obstacles = new ArrayList<Obstacle>();private List<Enemy> enemies = new ArrayList<Enemy>();private Droid droid;public Arena(Droid droid) {this.droid = droid;grid = new Object[HEIGHT][WIDTH];for (int i = 0; i < WIDTH; i++) {for (int j = 0; j < HEIGHT; j++) {grid[j][i] = null;}}// add 5 obstacles and 5 enemies at random positionsfor (int i = 0; i < 5; i++) {int x = random.nextInt(WIDTH);int y = random.nextInt(HEIGHT);while (grid[y][x] != null) {x = random.nextInt(WIDTH);y = random.nextInt(HEIGHT);}grid[y][x] = new Obstacle(x, y);obstacles.add((Obstacle) grid[y][x]);while (grid[y][x] != null) {x = random.nextInt(WIDTH);y = random.nextInt(HEIGHT);}grid[y][x] = new Enemy(x, y);enemies.add((Enemy) grid[y][x]);}}public List<Obstacle> getObstacles() {return obstacles;}public List<Enemy> getEnemies() {return enemies;}public Droid getDroid() {return droid;} }Arena是一個更復(fù)雜的對象,但是通讀代碼應(yīng)該易于理解。 它基本上將所有模型歸為一個世界。 我們的游戲世界是一個競技場,其中包含機器人,敵人和障礙物等所有元素。
WIDTH和HEIGHT是根據(jù)我選擇的分辨率計算的。 網(wǎng)格上的一個像元(塊)將寬32像素,所以我只計算有多少個像元進入網(wǎng)格。
在構(gòu)造函數(shù)(第19行)中,建立了網(wǎng)格,并隨機放置了5個障礙物和5個敵人。 這將構(gòu)成起步舞臺和我們的游戲世界。 為了使主循環(huán)保持整潔,我們將把更新和渲染委托給GameEngine 。 這是一個簡單的類,它將處理用戶輸入,更新模型的狀態(tài)并渲染世界。 這是一個很小的粘合框架,可實現(xiàn)所有這些目標(biāo)。 GameEngine.java存根
要使用引擎,需要修改Droids.java類。 我們需要創(chuàng)建GameEngine類的實例,并在適當(dāng)?shù)臅r候調(diào)用update()和render()方法。 另外,我們需要將輸入處理委托給引擎。
添加以下行:
聲明私有成員并實例化它。
private GameEngine engine = new GameEngine();修改后的游戲循環(huán)如下所示:
while (true) {long lastTime = System.nanoTime();g.setColor(Color.black);g.fillRect(0, 0, 480, 320);// Update the state (convert to seconds)engine.update((float)(delta / 1000000000.0));// Render the worldengine.render(g);// Draw the entire results on the screen.appletGraphics.drawImage(screen, 0, 0, null);// Lock the frame ratedelta = System.nanoTime() - lastTime;if (delta < 20000000L) {try {Thread.sleep((20000000L - delta) / 1000000L);} catch (Exception e) {// It's an interrupted exception, and nobody cares}}}高亮顯示的行(#7-#10)包含對update()和render()方法的委托。 請注意,從納秒到秒的轉(zhuǎn)換是幾秒鐘。 在幾秒鐘內(nèi)工作非常有用,因為我們可以處理現(xiàn)實價值。
重要說明 :更新需要在計算增量(自上次更新以來經(jīng)過的時間)之后進行。 更新后也應(yīng)調(diào)用渲染器,這樣它將顯示對象的當(dāng)前狀態(tài)。 請注意,每次在渲染(涂成黑色)之前都會清除屏幕。
最后要做的是委派輸入處理。
用以下代碼片段替換當(dāng)前的handleEvent方法:
public boolean handleEvent(Event e) {return engine.handleEvent(e);} 非常簡單明了的委托。
運行小程序不會產(chǎn)生特別令人興奮的結(jié)果。 只是黑屏。 這是有道理的,因為除了每個周期要清除的屏幕之外,所有內(nèi)容都只是一個存根。
初始化模型(世界)
我們的游戲需要機器人和一些敵人。 按照設(shè)計,世界就是我們的Arena 。 通過實例化它,我們創(chuàng)建了一個世界(檢查Arena的構(gòu)造函數(shù))。
我們將在GameEngine創(chuàng)建世界,因為引擎負(fù)責(zé)告訴視圖要渲染的內(nèi)容。
我們還需要在此處創(chuàng)建Droid ,因為Arena需要它的構(gòu)造函數(shù)。 最好將其分開,因為機器人將由玩家控制。
將以下成員與初始化世界的構(gòu)造函數(shù)一起添加到GameEngine 。
注意 : Arena的構(gòu)造函數(shù)需要修改,因此Droid會在障礙物和敵人之前添加到網(wǎng)格中。
...// add the droidgrid[(int)droid.getY()][(int) droid.getX()] = droid; ...再次運行該applet,不會更改輸出,但是我們已經(jīng)創(chuàng)建了世界。 我們可以添加日志記錄以查看結(jié)果,但這不會很有趣。 讓我們創(chuàng)建第一個視圖,它將揭示我們的世界。
創(chuàng)建第一個視圖/渲染器
我們在創(chuàng)建競技場和世界上付出了很多努力,我們渴望看到它。 因此,我們將創(chuàng)建一個快速而骯臟的渲染器來揭示整個世界。 快速而骯臟的意思是,除了簡單的正方形,圓形和占位符以外,沒有別致的圖像。 一旦我們對游戲元素感到滿意,就可以在更精細(xì)的視圖上進行操作,以用精美的圖形替換正方形和圓形。 這就是去耦能力的光芒所在。
渲染世界的步驟。
- 繪制網(wǎng)格以查看單元格在哪里。
- 障礙物將被繪制為藍(lán)色方塊,它們將占據(jù)單元格
- 敵人將是紅色圓圈
- 機器人將是帶有棕色正方形的綠色圓圈
首先,我們創(chuàng)建渲染器界面。 我們使用它來建立與渲染器交互的單一方法,這將使創(chuàng)建更多視圖而不影響游戲引擎變得容易。 要了解更多關(guān)于為什么是一個好主意,檢查這個和這個 。
在view包中創(chuàng)建一個接口。
Renderer.java
package net.obviam.droids.view;import java.awt.Graphics;public interface Renderer {public void render(Graphics g); } 就這些。 它包含一種方法: render(Graphics g) 。 Graphics g是從applet傳遞的畫布。 理想情況下,接口將與此無關(guān),并且每個實現(xiàn)都將使用不同的后端,但是此練習(xí)的目的是描述MVC而不是創(chuàng)建完整的框架。 因為我們選擇了applet,所以我們需要Graphics對象。
具體的實現(xiàn)如下所示:
SimpleArenaRenderer.java (在view包中)
package net.obviam.droids.view;import java.awt.Color; import java.awt.Graphics;import net.obviam.droids.model.Arena; import net.obviam.droids.model.Droid; import net.obviam.droids.model.Enemy; import net.obviam.droids.model.Obstacle;public class SimpleArenaRenderer implements Renderer {private Arena arena;public SimpleArenaRenderer(Arena arena) {this.arena = arena;}@Overridepublic void render(Graphics g) {// render the gridint cellSize = 32; // hard codedg.setColor(new Color(0, 0.5f, 0, 0.75f));for (int i = 0; i <= Arena.WIDTH; i++) {g.drawLine(i * cellSize, 0, i * cellSize, Arena.HEIGHT * cellSize);if (i <= Arena.WIDTH)g.drawLine(0, i * cellSize, Arena.WIDTH * cellSize, i * cellSize);}// render the obstaclesg.setColor(new Color(0, 0, 1f));for (Obstacle obs : arena.getObstacles()) {int x = (int) (obs.getX() * cellSize) + 2;int y = (int) (obs.getY() * cellSize) + 2;g.fillRect(x, y, cellSize - 4, cellSize - 4);}// render the enemiesg.setColor(new Color(1f, 0, 0));for (Enemy enemy : arena.getEnemies()) {int x = (int) (enemy.getX() * cellSize);int y = (int) (enemy.getY() * cellSize);g.fillOval(x + 2, y + 2, cellSize - 4, cellSize - 4);}// render player droidg.setColor(new Color(0, 1f, 0));Droid droid = arena.getDroid();int x = (int) (droid.getX() * cellSize);int y = (int) (droid.getY() * cellSize);g.fillOval(x + 2, y + 2, cellSize - 4, cellSize - 4);// render square on droidg.setColor(new Color(0.7f, 0.5f, 0f));g.fillRect(x + 10, y + 10, cellSize - 20, cellSize - 20);} }第13 – 17行聲明了Arena對象,并確保在構(gòu)造渲染器時設(shè)置了該對象。 我將其稱為ArenaRenderer是因為我們將渲染競技場(世界)。
渲染器中唯一的方法是render()方法。 讓我們一步一步地看看它的作用。
#22 –聲明像元大小(以像素為單位)。 它是32。與Arena類中一樣,它是硬編碼的。 #23 –#28 –正在繪制網(wǎng)格。 這是一個簡單的網(wǎng)格。 首先,將顏色設(shè)置為深綠色,并以相等的距離繪制線條。
繪制障礙物–藍(lán)色方塊
#31 –將筆刷顏色設(shè)置為藍(lán)色。
#32 –#36 –遍歷舞臺上的所有障礙物,并為每個障礙物繪制一個藍(lán)色填充的矩形,該矩形稍小于網(wǎng)格上的單元格。 #39 –#44 –將顏色設(shè)置為紅色,并通過遍歷舞臺中的敵人,在相應(yīng)位置繪制一個圓圈。 #47 –#54 –最后將機器人繪制為綠色圓圈,頂部帶有棕色正方形。
請注意 ,現(xiàn)實世界中的競技場寬度為15(480/32)。 因此,機器人將始終位于相同的位置(7,5),并且渲染器通過使用單位度量轉(zhuǎn)換來計算其在屏幕上的位置。 在這種情況下,世界坐標(biāo)系中的1個單位在屏幕上為32個像素。 通過修改GameEngine以使用新創(chuàng)建的視圖( SimpleArenaRenderer ),我們得到了結(jié)果。
注意突出顯示的行(5、15、22)。 這些是將渲染器(視圖)添加到游戲中的行。
結(jié)果應(yīng)如下圖所示(位置與玩家的機器人分開是隨機的):
| 第一次查看的結(jié)果 |
這是測試舞臺并查看模型的絕佳視圖。 創(chuàng)建一個新視圖而不是用形狀(正方形和圓形)顯示實際的精靈非常容易。
處理輸入和更新模型的控制器
到目前為止,該游戲什么都不做,只顯示當(dāng)前世界(競技場)狀態(tài)。 為簡單起見,我們將僅更新機器人的一種狀態(tài),即其位置。
根據(jù)用戶輸入移動機器人的步驟為:
- 鼠標(biāo)懸停時,檢查網(wǎng)格上單擊的單元格是否為空。 這意味著它確實包含任何可能是Enemy或Obstacle實例的對象。
- 如果單元格為空,則控制器將創(chuàng)建一個動作,該動作將以恒定的速度移動機器人直到到達目標(biāo)。
以下細(xì)分說明了邏輯和重要位。
#08 – unit代表一個像元中有多少像素,代表世界坐標(biāo)中的1個單位。 它是硬編碼的,不是最佳的,但是對于演示來說已經(jīng)足夠了。
#09 –控制器將控制的Arena 。 在構(gòu)造控制器時設(shè)置(第16行)。 #12 –點擊的目標(biāo)坐標(biāo)(以世界單位表示)。 #14 –機器人在移動時true 。 這是“移動”動作的狀態(tài)。 理想情況下,這應(yīng)該是一個獨立的類,但是為了演示控制器并保持簡潔,我們將在控制器內(nèi)部共同編寫一個動作。 #20 –一種update方法,該方法根據(jù)以恒定速度經(jīng)過的時間更新機器人的位置。 這非常簡單,它會同時檢查X和Y位置,如果它們與目標(biāo)位置不同,則會考慮其速度更新機器人的相應(yīng)位置(X或Y)。 如果機器人在目標(biāo)位置,則更新move狀態(tài)變量以完成移動動作。
這不是一個很好的書面動作,沒有對沿途發(fā)現(xiàn)的障礙物或敵人進行碰撞檢查,也沒有發(fā)現(xiàn)路徑。 它只是更新狀態(tài)。
#52 –發(fā)生“鼠標(biāo)向上”事件時,將調(diào)用onClick(int x, int y)方法。 它檢查單擊的單元格是否為空,如果為空,則通過將狀態(tài)變量設(shè)置為true來啟動“移動”操作
#53-#54 –將屏幕坐標(biāo)轉(zhuǎn)換為世界坐標(biāo)。
這是控制器。 要使用它,必須更新GameEngine 。
更新的GameEngine.java
package net.obviam.droids.controller;import java.awt.Event; import java.awt.Graphics;import net.obviam.droids.model.Arena; import net.obviam.droids.model.Droid; import net.obviam.droids.view.Renderer; import net.obviam.droids.view.SimpleArenaRenderer;public class GameEngine {private Arena arena;private Droid droid;private Renderer renderer;private ArenaController controller;public GameEngine() {droid = new Droid();// position droid in the middledroid.setX(Arena.WIDTH / 2);droid.setY(Arena.HEIGHT / 2);arena = new Arena(droid);// setup renderer (view)renderer = new SimpleArenaRenderer(arena);// setup controllercontroller = new ArenaController(arena);}/** handle the Event passed from the main applet **/public boolean handleEvent(Event e) {switch (e.id) {case Event.KEY_PRESS:case Event.KEY_ACTION:// key pressedbreak;case Event.KEY_RELEASE:// key releasedbreak;case Event.MOUSE_DOWN:// mouse button pressedbreak;case Event.MOUSE_UP:// mouse button releasedcontroller.onClick(e.x, e.y);break;case Event.MOUSE_MOVE:// mouse is being movedbreak;case Event.MOUSE_DRAG:// mouse is being dragged (button pressed)break;}return false;}/** the update method with the deltaTime in seconds **/public void update(float deltaTime) {controller.update(deltaTime);}/** this will render the whole world **/public void render(Graphics g) {renderer.render(g);} } 更改將突出顯示。
#16 –聲明控制器。
#28 –實例化控制器。 #46 –委托鼠標(biāo)上移事件。 #60 –在控制器上調(diào)用update方法。 運行小程序,您可以單擊地圖,如果單元格為空,則機器人將移動到那里。
練習(xí)
- 創(chuàng)建一個視圖,該視圖將顯示實體的圖像/精靈,而不是繪制的形狀。
提示 :使用BufferedImage來實現(xiàn)。 - 將移動動作提取到新類中。
- 單擊敵人時添加新的動作(攻擊) 提示:創(chuàng)建被發(fā)射到目標(biāo)的項目符號實體。 您可以以更高的速度使用移動動作。 當(dāng)hitpoint降到0時,敵人被摧毀。 使用不同的圖像表示不同的狀態(tài)。
源代碼
https://github.com/obviam/mvc-droids或下載為zip文件。 您也可以使用git
$ git clone git://github.com/obviam/mvc-droids.git
參考: 使用MVC模式構(gòu)建游戲– JCG合作伙伴的 教程和簡介 ? 反對谷物博客的Impaler。
翻譯自: https://www.javacodegeeks.com/2012/02/building-games-using-mvc-pattern.html
總結(jié)
以上是生活随笔為你收集整理的使用MVC模式制作游戏-教程和简介的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 地球连续 35 年收到神秘规律信号,源头
- 下一篇: 并发模式:生产者和消费者