它有助于分離輸入邏輯,游戲邏輯和UI(渲染)。 在任何游戲開發項目的早期階段,其實用性很快就會被注意到,因為它允許快速更改內容,而無需在應用程序的所有層中進行過多的代碼重做。
下圖是模型視圖控制器概念的最簡單邏輯表示。
![]() |
模型-視圖-控制器模式 |
用法示例
在玩家控制機器人的示例游戲中,可能會發生以下情況:
- 1 –用戶單擊/輕擊屏幕上的某個位置。
- 2 – 控制器處理單擊/輕擊并將事件轉換為適當的操作。 例如,如果地形被敵人占領,則會創建攻擊動作;如果地形為空,則會創建移動動作,最后,如果用戶輕拍的地方被障礙物占據,則不執行任何操作。
- 3 – 控制器相應地更新機器人 ( 模型 )的狀態。 如果創建了移動動作,那么它將改變位置,如果發起了攻擊,則將射擊。
- 4 – 渲染器 ( 視圖 )收到有關狀態更改的通知,并渲染世界的當前狀態。
這一切意味著,模型(機器人)對如何繪制自己或如何更改其狀態(位置,命中點)一無所知。 他們是愚蠢的實體。 在Java中,它們也稱為POJO(普通的舊Java對象)。
控制器負責更改模型的狀態并通知渲染器。
為了繪制模型,渲染器必須引用模型(機器人和任何其他實體)及其狀態。
從典型的游戲架構中我們知道, 主循環充當超級控制器,超級控制器更新狀態,然后每秒將對象呈現到屏幕上多次。 我們可以將所有更新和渲染與機器人一起放入主循環,但這很麻煩。 讓我們確定游戲的不同方面(關注點)。
型號
- 玩家控制的機器人
- 機器人可以移動的競技場
- 一些障礙
- 一些敵人要開槍
控制器
- 主循環和輸入處理程序
- 控制器處理玩家輸入
- 在玩家的機器人上執行動作(移動,攻擊)的控制器
觀點
- 世界渲染器–將對象渲染到屏幕上
創建項目
為簡單起見,我這次選擇了applet,并將嘗試使其簡短。 該項目具有以下結構:
![]() |
MVC –項目結構 |
文件Droids.java
是applet,包含主循環。
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運行,無非是設置主循環并將屏幕涂成黑色。
結構中有3個程序包,各個組件都將放在那兒。
net.obviam.droids.model
將包含所有模型
net.obviam.droids.view
將包含所有渲染器
net.obviam.droids.controller
將包含所有控制器
創建模型
機器人
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對象,對周圍世界一無所知。 它具有位置,旋轉,速度和損壞。 這些狀態由成員變量定義,可通過getter和setter方法訪問。
游戲需要更多模型:地圖上的障礙物和敵人。 為簡單起見,障礙物將僅在地圖上定位,而敵人將是站立的物體。 該地圖將是一個二維數組,其中包含敵人,障礙物和機器人。 該地圖將被稱為Arena
以區別于標準Java地圖,并且在構建地圖時會填充障礙物和敵人。 Obstacle.java
package net.obviam.droids.model;public class Obstacle {private float x;private float y;public Obstacle(float x, float y) {this.x = x;this.y = y;}public float getX() {return x;}public float getY() {return y;}
}
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
是一個更復雜的對象,但是通讀代碼應該易于理解。 它基本上將所有模型歸為一個世界。 我們的游戲世界是一個競技場,其中包含機器人,敵人和障礙物等所有元素。
WIDTH
和HEIGHT
是根據我選擇的分辨率計算的。 網格上的一個像元(塊)將寬32像素,所以我只計算有多少個像元進入網格。
在構造函數(第19行)中,建立了網格,并隨機放置了5個障礙物和5個敵人。 這將構成起步舞臺和我們的游戲世界。 為了使主循環保持整潔,我們將把更新和渲染委托給GameEngine
。 這是一個簡單的類,它將處理用戶輸入,更新模型的狀態并渲染世界。 這是一個很小的粘合框架,可實現所有這些目標。 GameEngine.java
存根
package net.obviam.droids.controller;import java.awt.Event;
import java.awt.Graphics;public class GameEngine {/** 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 releasedbreak;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) {// empty}/** this will render the whole world **/public void render(Graphics g) {// empty}
}
要使用引擎,需要修改Droids.java
類。 我們需要創建GameEngine
類的實例,并在適當的時候調用update()
和render()
方法。 另外,我們需要將輸入處理委托給引擎。
添加以下行:
聲明私有成員并實例化它。
private GameEngine engine = new GameEngine();
修改后的游戲循環如下所示:
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()
方法的委托。 請注意,從納秒到秒的轉換是幾秒鐘。 在幾秒鐘內工作非常有用,因為我們可以處理現實價值。
重要說明 :更新需要在計算增量(自上次更新以來經過的時間)之后進行。 更新后也應調用渲染器,這樣它將顯示對象的當前狀態。 請注意,每次在渲染(涂成黑色)之前都會清除屏幕。
最后要做的是委派輸入處理。
用以下代碼片段替換當前的handleEvent
方法:
public boolean handleEvent(Event e) {return engine.handleEvent(e);}
非常簡單明了的委托。
運行小程序不會產生特別令人興奮的結果。 只是黑屏。 這是有道理的,因為除了每個周期要清除的屏幕之外,所有內容都只是一個存根。
初始化模型(世界)
我們的游戲需要機器人和一些敵人。 按照設計,世界就是我們的Arena
。 通過實例化它,我們創建了一個世界(檢查Arena
的構造函數)。
我們將在GameEngine
創建世界,因為引擎負責告訴視圖要渲染的內容。
我們還需要在此處創建Droid
,因為Arena
需要它的構造函數。 最好將其分開,因為機器人將由玩家控制。
將以下成員與初始化世界的構造函數一起添加到GameEngine
。
private Arena arena;private Droid droid;public GameEngine() {droid = new Droid();// position droid in the middledroid.setX(Arena.WIDTH / 2);droid.setY(Arena.HEIGHT / 2);arena = new Arena(droid);}
注意 : Arena
的構造函數需要修改,因此Droid
會在障礙物和敵人之前添加到網格中。
...// add the droidgrid[(int)droid.getY()][(int) droid.getX()] = droid;
...
再次運行該applet,不會更改輸出,但是我們已經創建了世界。 我們可以添加日志記錄以查看結果,但這不會很有趣。 讓我們創建第一個視圖,它將揭示我們的世界。
創建第一個視圖/渲染器
我們在創建競技場和世界上付出了很多努力,我們渴望看到它。 因此,我們將創建一個快速而骯臟的渲染器來揭示整個世界。 快速而骯臟的意思是,除了簡單的正方形,圓形和占位符以外,沒有別致的圖像。 一旦我們對游戲元素感到滿意,就可以在更精細的視圖上進行操作,以用精美的圖形替換正方形和圓形。 這就是去耦能力的光芒所在。
渲染世界的步驟。
- 繪制網格以查看單元格在哪里。
- 障礙物將被繪制為藍色方塊,它們將占據單元格
- 敵人將是紅色圓圈
- 機器人將是帶有棕色正方形的綠色圓圈
首先,我們創建渲染器界面。 我們使用它來建立與渲染器交互的單一方法,這將使創建更多視圖而不影響游戲引擎變得容易。 要了解更多關于為什么是一個好主意,檢查這個和這個 。
在view
包中創建一個接口。
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傳遞的畫布。 理想情況下,接口將與此無關,并且每個實現都將使用不同的后端,但是此練習的目的是描述MVC而不是創建完整的框架。 因為我們選擇了applet,所以我們需要Graphics
對象。
具體的實現如下所示:
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
對象,并確保在構造渲染器時設置了該對象。 我將其稱為ArenaRenderer是因為我們將渲染競技場(世界)。
渲染器中唯一的方法是render()
方法。 讓我們一步一步地看看它的作用。
#22 –聲明像元大小(以像素為單位)。 它是32。與Arena
類中一樣,它是硬編碼的。 #23 –#28 –正在繪制網格。 這是一個簡單的網格。 首先,將顏色設置為深綠色,并以相等的距離繪制線條。
繪制障礙物–藍色方塊
#31 –將筆刷顏色設置為藍色。
#32 –#36 –遍歷舞臺上的所有障礙物,并為每個障礙物繪制一個藍色填充的矩形,該矩形稍小于網格上的單元格。 #39 –#44 –將顏色設置為紅色,并通過遍歷舞臺中的敵人,在相應位置繪制一個圓圈。 #47 –#54 –最后將機器人繪制為綠色圓圈,頂部帶有棕色正方形。
請注意 ,現實世界中的競技場寬度為15(480/32)。 因此,機器人將始終位于相同的位置(7,5),并且渲染器通過使用單位度量轉換來計算其在屏幕上的位置。 在這種情況下,世界坐標系中的1個單位在屏幕上為32個像素。 通過修改GameEngine
以使用新創建的視圖( SimpleArenaRenderer
),我們得到了結果。
public class GameEngine {private Arena arena;private Droid droid;private Renderer renderer;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);}/** ... code stripped ... **//** this will render the whole world **/public void render(Graphics g) {renderer.render(g);}
}
注意突出顯示的行(5、15、22)。 這些是將渲染器(視圖)添加到游戲中的行。
結果應如下圖所示(位置與玩家的機器人分開是隨機的):
![]() |
第一次查看的結果 |
這是測試舞臺并查看模型的絕佳視圖。 創建一個新視圖而不是用形狀(正方形和圓形)顯示實際的精靈非常容易。
處理輸入和更新模型的控制器
到目前為止,該游戲什么都不做,只顯示當前世界(競技場)狀態。 為簡單起見,我們將僅更新機器人的一種狀態,即其位置。
根據用戶輸入移動機器人的步驟為:
- 鼠標懸停時,檢查網格上單擊的單元格是否為空。 這意味著它確實包含任何可能是
Enemy
或Obstacle
實例的對象。 - 如果單元格為空,則控制器將創建一個動作,該動作將以恒定的速度移動機器人直到到達目標。
package net.obviam.droids.controller;import net.obviam.droids.model.Arena;
import net.obviam.droids.model.Droid;public class ArenaController {private static final int unit = 32;private Arena arena;/** the target cell **/private float targetX, targetY;/** true if the droid moves **/private boolean moving = false;public ArenaController(Arena arena) {this.arena = arena;}public void update(float delta) {Droid droid = arena.getDroid();if (moving) {// move on Xint bearing = 1;if (droid.getX() > targetX) {bearing = -1;}if (droid.getX() != targetX) {droid.setX(droid.getX() + bearing * droid.getSpeed() * delta);// check if arrivedif ((droid.getX() < targetX && bearing == -1)|| (droid.getX() > targetX && bearing == 1)) droid.setX(targetX);}// move on Ybearing = 1;if (droid.getY() > targetY) {bearing = -1;}if (droid.getY() != targetY) {droid.setY(droid.getY() + bearing * droid.getSpeed() * delta);// check if arrivedif ((droid.getY() < targetY && bearing == -1)|| (droid.getY() > targetY && bearing == 1)) droid.setY(targetY);}// check if arrivedif (droid.getX() == targetX && droid.getY() == targetY)moving = false;}}/** triggered with the coordinates every click **/public boolean onClick(int x, int y) {targetX = x / unit;targetY = y / unit;if (arena.getGrid()[(int) targetY][(int) targetX] == null) {// start moving the droid towards the targetmoving = true;return true;}return false;}
}
以下細分說明了邏輯和重要位。
#08 – unit
代表一個像元中有多少像素,代表世界坐標中的1個單位。 它是硬編碼的,不是最佳的,但是對于演示來說已經足夠了。
#09 –控制器將控制的Arena
。 在構造控制器時設置(第16行)。 #12 –點擊的目標坐標(以世界單位表示)。 #14 –機器人在移動時true
。 這是“移動”動作的狀態。 理想情況下,這應該是一個獨立的類,但是為了演示控制器并保持簡潔,我們將在控制器內部共同編寫一個動作。 #20 –一種update
方法,該方法根據以恒定速度經過的時間更新機器人的位置。 這非常簡單,它會同時檢查X和Y位置,如果它們與目標位置不同,則會考慮其速度更新機器人的相應位置(X或Y)。 如果機器人在目標位置,則更新move
狀態變量以完成移動動作。
這不是一個很好的書面動作,沒有對沿途發現的障礙物或敵人進行碰撞檢查,也沒有發現路徑。 它只是更新狀態。
#52 –發生“鼠標向上”事件時,將調用onClick(int x, int y)
方法。 它檢查單擊的單元格是否為空,如果為空,則通過將狀態變量設置為true
來啟動“移動”操作
#53-#54 –將屏幕坐標轉換為世界坐標。
這是控制器。 要使用它,必須更新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 –委托鼠標上移事件。 #60 –在控制器上調用update
方法。 運行小程序,您可以單擊地圖,如果單元格為空,則機器人將移動到那里。
練習
- 創建一個視圖,該視圖將顯示實體的圖像/精靈,而不是繪制的形狀。
提示 :使用BufferedImage來實現。 - 將移動動作提取到新類中。
- 單擊敵人時添加新的動作(攻擊) 提示:創建被發射到目標的項目符號實體。 您可以以更高的速度使用移動動作。 當
hitpoint
降到0時,敵人被摧毀。 使用不同的圖像表示不同的狀態。
源代碼
您也可以使用git
$ git clone git://github.com/obviam/mvc-droids.git
參考: 使用MVC模式構建游戲– JCG合作伙伴的 教程和簡介 ? 反對谷物博客的Impaler。
翻譯自: https://www.javacodegeeks.com/2012/02/building-games-using-mvc-pattern.html