它有助于分离输入逻辑,游戏逻辑和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 rate
delta = 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 positions
for (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 pressed
break;
case Event.KEY_RELEASE:
// key released
break;
case Event.MOUSE_DOWN:
// mouse button pressed
break;
case Event.MOUSE_UP:
// mouse button released
break;
case Event.MOUSE_MOVE:
// mouse is being moved
break;
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 world
engine.render(g);
// Draw the entire results on the screen.
appletGraphics.drawImage(screen, 0, 0, null);
// Lock the frame rate
delta = 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 middle
droid.setX(Arena.WIDTH / 2);
droid.setY(Arena.HEIGHT / 2);
arena = new Arena(droid);
}
注意 : Arena
的构造函数需要修改,因此Droid
会在障碍物和敌人之前添加到网格中。
...
// add the droid
grid[(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;
}
@Override
public void render(Graphics g) {
// render the grid
int cellSize = 32; // hard coded
g.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 obstacles
g.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 enemies
g.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 droid
g.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 droid
g.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 middle
droid.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 X
int bearing = 1;
if (droid.getX() > targetX) {
bearing = -1;
}
if (droid.getX() != targetX) {
droid.setX(droid.getX() + bearing * droid.getSpeed() * delta);
// check if arrived
if ((droid.getX() < targetX && bearing == -1)
|| (droid.getX() > targetX && bearing == 1)) droid.setX(targetX);
}
// move on Y
bearing = 1;
if (droid.getY() > targetY) {
bearing = -1;
}
if (droid.getY() != targetY) {
droid.setY(droid.getY() + bearing * droid.getSpeed() * delta);
// check if arrived
if ((droid.getY() < targetY && bearing == -1)
|| (droid.getY() > targetY && bearing == 1)) droid.setY(targetY);
}
// check if arrived
if (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 target
moving = 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 middle
droid.setX(Arena.WIDTH / 2);
droid.setY(Arena.HEIGHT / 2);
arena = new Arena(droid);
// setup renderer (view)
renderer = new SimpleArenaRenderer(arena);
// setup controller
controller = 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 pressed
break;
case Event.KEY_RELEASE:
// key released
break;
case Event.MOUSE_DOWN:
// mouse button pressed
break;
case Event.MOUSE_UP:
// mouse button released
controller.onClick(e.x, e.y);
break;
case Event.MOUSE_MOVE:
// mouse is being moved
break;
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