ASP.net 的 MVC 砖块游戏






4.96/5 (119投票s)
学习如何使用 ASP.net MVC、jQuery、状态机和 CSS3 渐变创建简单游戏
目录
引言
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
距离我上一篇关于 ASP.Net 的文章已经很久了。这次我决定写一些关于它的内容,更具体地说,是关于 ASP.Net MVC 的。
MVC 方法是迄今为止我在 ASP.Net 中处理事情最喜欢的方式。没有代码隐藏,也没有视图状态,仅凭这一点就值得我高度关注。更不用说您不必关心回发了。
此外,该应用程序使用了出色的 jQuery 库,让我们的生活更轻松。正如读者将看到的,我尽可能多地使用了 jQuery。这个库对于我们这些需要处理 Web 应用程序的开发人员来说真是一种福音。
系统要求
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
要使用本文提供的 MVC 砖块游戏,如果您已经安装了 Visual Studio 2010,那很好。如果没有,您可以直接从 Microsoft 下载以下100%免费的开发工具。
此外,您必须在支持 CSS3 的浏览器中打开它,因为此应用程序使用了 CSS3 Webkit 渐变。
启动画面
游戏启动时,会有一个简单的 jQuery 动画,使“MVC BRICKS”标题升到屏幕中央。这是通过动画标题div 的top
css 属性实现的。
function showSplashScreen() {
$('.subTitle').css('visibility', 'hidden');
$('.press').css('visibility', 'hidden');
$('.title').animate({
top: 200
}, 1000, 'swing', function () {
// Animation complete.
$('.subTitle').css('visibility', 'visible');
$('.press').css('visibility', 'visible');
});
}
上述代码显示,动画结束后,标题到达屏幕中央,副标题显示出来。
游戏控制
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
游戏完全由键盘控制。当游戏处于介绍模式时,您可以使用空格键开始新游戏。当游戏正在进行时,如果您想暂停游戏,请再次使用空格键。然后再次使用空格键恢复游戏。当游戏结束时,再次按下空格键打开游戏介绍屏幕。
使用左箭头键和右箭头键将下落的方块向左或向右移动。按下上箭头键将方块旋转90度。最后,按下下箭头键加速方块的下落。
键 | 命令 |
空格键 | 开始游戏 / 暂停游戏 / 恢复游戏 / 重启游戏 |
左箭头键 | 将下落的方块向左移动 |
右箭头键 | 将下落的方块向右移动 |
下箭头键 | 加速下落的方块 |
游戏规则
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
您一定对这类游戏非常熟悉,但我还是要解释一下游戏规则。
这里有一个空的10x16板,包含160个空位置。游戏一开始,游戏引擎会随机生成一个新的方块,它会从板的顶部以每秒1格的速度下落。当正在下落的方块遇到障碍物(即固定在板底部的另一个方块的一部分)时,它就不能再下落了,所以这个下落的方块就会卡住。然后游戏引擎会产生新的随机方块,它们会堆积起来直到堆积到板的顶部,此时游戏结束。用户必须控制每个下落的方块,通过将其向左、向右移动或旋转,将新方块放置在板上最低的可能空位,以便新方块能够适应,从而避免堆积的方块达到板的顶部。此外,当用户填满板的任何行时,这些行将被清除,从而提供一些额外的空间并延长游戏时间。
![]() |
![]() |
![]() |
![]() |
“I” 形 | “L” 形 | “J” 形 | “O” 形 |
![]() |
![]() |
![]() |
|
“T” 形 | “S” 形 | “Z” 形 |
游戏引擎可以随机生成上述任何形状。我们可以看到,每个形状都与一个字母相关联,这个字母与其形状相似。
每清除一行,用户将获得总计10乘以游戏等级的分数。也就是说,在第一个等级中清除的每一行将获得10分。第二个等级清除的每一行将获得20分,以此类推。
当用户清除10行时,完成一个等级。也就是说,要达到第5个等级,用户必须已经清除了40行。
游戏结束后,游戏分数将与之前的最高分进行比较,如果出现新记录则替换。
下一个方块让用户有机会以一种更容易容纳下一个下落方块的方式放置当前方块。
模型-视图-控制器
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
在我看来,MVC 的美妙之处在于它坚持了关注点分离的原则。与“经典”(非 MVC)ASP.net 不同,您不会将业务逻辑放在视图中。相反,您只将视图用于表示逻辑(例如解析和渲染原始数据或输入验证)以及表示本身。另一方面,您将控制器(或其他层,例如服务层)保留用于业务规则。
如果你查看应用程序中的javascript代码,你会发现没有业务逻辑。相反,你会发现它非常精简和轻量。幸运的是,由于我之前项目已经有了Bricks游戏逻辑,我得以在服务器端几乎完整地保留了我的托管代码,并通过我专为这个MVC项目设计的新的ViewModel
与视图进行通信。
模型
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
模型由BoardViewModel
和BrickViewModel
类定义,包含视图渲染游戏板、记分板以及判断游戏是否结束所需的所有信息。如下所示,BoardViewModel
类的大部分属性都是原生类型,除了Bricks
和Next
属性,它们是BrickViewModel
的二维数组,存储了构成游戏板当前快照的砖块和空白区域的数据,以及对应于将从游戏板顶部下落的Next
方块的砖块数据。
低级的BrickViewModel
类包含每个独立砖块的信息:行、列和颜色名称。视图将使用这些值来查找相应的div并相应地更新它们的背景颜色。
public class BrickViewModel
{
public int Row { get; set; }
public int Col { get; set; }
public string Color { get; set; }
}
public class BoardViewModel
{
public BoardViewModel()
{
IsGameOver = false;
}
public BrickViewModel[] Bricks { get; set; }
public int Score { get; set; }
public int HiScore { get; set; }
public int Lines { get; set; }
public int Level { get; set; }
public BrickViewModel[] Next { get; set; }
public bool IsGameOver { get; set; }
}
}
View
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
视图确实不包含任何业务逻辑(在我们的例子中,不包含游戏逻辑)。这里我们可以看到它的基本目标。
- 设置一个计时器,每200毫秒调用一次
Controller
以获取包含更新的游戏板快照(序列化为JSON)的ViewModel。 - 解析返回的 JSON 以渲染板、分数、高分、级别和下一个方块。
- 设置另一个计时器,每1000毫秒调用一次
Controller
以请求下落方块的新移动。 - 监听键盘事件,并调用
Controller
开始新游戏,暂停或恢复游戏,移动或旋转下落的方块,或重新开始游戏。
这就是视图所做的大部分工作。请注意,在传统的(非 MVC)ASP.net 应用程序中,我们会有一个代码隐藏类,其中可能包含一些业务逻辑。多亏了 MVC,关注点分离的原则得以保持,我们可以将业务逻辑从视图中移开。
我们通过使用 jQuery 语法将一个函数附加到页面文档的keydown
事件来处理用户的手势。
$(document).keydown(function (event) {
switch (event.keyCode) {
case 32: //space
if (gameState.current_state == 'intro')
gameState.process('play');
else if (gameState.current_state == 'paused')
gameState.process('continue');
else if (gameState.current_state == 'gameOver')
gameState.process('showIntro');
else
gameState.process('pause');
break;
case 37: //left
if (gameState.current_state == 'playing')
moveLeft();
break;
case 38: //up
if (gameState.current_state == 'playing')
moveUp();
break;
case 39: //right
if (gameState.current_state == 'playing')
moveRight();
break;
case 40: //down
if (gameState.current_state == 'playing')
moveDown();
break;
}
});
下表显示了事件(例如计时器滴答或按键)、BricksView
侧的 jQuery 式 Ajax 调用以及BricksController
侧调用的方法(操作)。
事件/按键 | 视图代码 | 控制器代码 |
![]() 每1000毫秒 |
$(document).everyTime(1000, function (i) {
$.ajax({
type: "GET",
url: "Tick",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
},
success: function (json) {
}
});
});
|
public ActionResult Tick()
{
BricksManager.Instance.Presenter.Tick();
return new JsonResult()
{ Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
![]() 每200毫秒 |
$(document).everyTime(200, function (i) {
if (gameState.current_state == 'playing') {
$.ajax({
type: "GET",
url: "GetBoard",
cache: false,
// data: {},
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
//alert(error);
},
success: function (json) {
...
}
});
}
});
|
public ActionResult GetBoard()
{
return new JsonResult() { Data =
BricksManager.Instance.CurrentBoard,
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
![]() |
function initializeBoard() {
$.ajax({
type: "GET",
url: "InitializeBoard",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
alert(xhr.status);
},
success: function (json) {
}
});
}
|
public ActionResult InitializeBoard()
{
BricksManager.Instance.InitializeBoard();
return new JsonResult() { Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
![]() |
function moveLeft() {
$.ajax({
type: "GET",
url: "MoveLeft",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
},
success: function (json) {
}
});
}
|
public ActionResult MoveLeft()
{
BricksManager.Instance.Presenter.MoveLeft();
return new JsonResult() { Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
![]() |
function moveRight() {
$.ajax({
type: "GET",
url: "MoveRight",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
},
success: function (json) {
}
});
}
|
public ActionResult MoveRight()
{
BricksManager.Instance.Presenter.MoveRight();
return new JsonResult() { Data = "",
JsonRequestBehavior = J
sonRequestBehavior.AllowGet };
}
|
![]() |
function moveDown() {
$.ajax({
type: "GET",
url: "MoveDown",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
},
success: function (json) {
}
});
}
|
public ActionResult MoveDown()
{
BricksManager.Instance.Presenter.MoveDown();
return new JsonResult() { Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
![]() |
function moveUp() {
$.ajax({
type: "GET",
url: "MoveUp",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
//alert(xhr.status);
},
success: function (json) {
}
});
}
|
public ActionResult MoveUp()
{
BricksManager.Instance.Presenter.Rotate90();
return new JsonResult() { Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
这里基本上包含了View
所需的所有 HTML。请注意,您在游戏屏幕上看到的所有元素都在这里,除了砖块。
<body>
<br />
<div class="screen">
<div id="title" class="title">
<img src="../../Content/images/Title.png" />
<div class="subTitle">©2011 Marcelo Ricardo de Oliveira<br />
Made for The Code Project<img src="../../Content/images/Bob.png" class="bob"/></div>
<br />
<div class="press">Press SPACE to start game!</div>
</div>
<div class="centerPanel">
<div class="board">
</div>
<div class="scorePanel">
<div>
Score</div>
<div id="divScore" class="scoreText">000000</div>
<br />
<div>
HiScore</div>
<div id="divHiScore" class="scoreText">000000</div>
<br />
<div>
Lines</div>
<div id="divLines" class="scoreText">0</div>
<br />
<div>
Level</div>
<div id="divLevel" class="scoreText">0</div>
<br />
<div>
Next</div>
<div id="divNext" class="scoreText"></div>
</div>
</div>
<div id="gamePaused">
GAME PAUSED<br />Press SPACE to continue!</div>
</div>
<div id="gameOver">
GAME OVER<br />Press SPACE to restart!</div>
</div>
</div>
</body>
</html>
这是因为砖块是应用程序启动时动态生成的div
。动态生成比将这些 div 硬编码到 html 中更容易控制砖块的生成方式、渲染方式等。
这是生成所有砖块的代码,包括记分板“下一个”部分的砖块。请注意 jQuery 语法将 html 附加到现有 html 元素是多么优雅。
function createCells() {
for (var row = 0; row < 16; row++) {
for (var col = 0; col < 10; col++) {
var divId = 'cell_' + row + '_' + col;
var imgId = 'img_' + row + '_' + col;
var divTag = '<div id="' + divId + '" name="brick" class="colorChip clearfix"></div>';
$(divTag).appendTo('.board');
}
$('<div class="clear">').appendTo('.board');
$('</div>').appendTo('.board');
}
for (var row = 0; row < 2; row++) {
for (var col = 0; col < 4; col++) {
var divId = 'next_' + row + '_' + col;
var imgId = 'nextImg_' + row + '_' + col;
var divTag = '<div id="' + divId + '" name="brick" class="colorChip clearfix"></div>';
$(divTag).appendTo('#divNext');
}
$('<div class="clear">').appendTo('#divNext');
$('</div>').appendTo('#divNext');
}
}
BricksView
端最重要的部分之一是游戏板渲染。请注意,我们没有使用图片作为砖块;相反,我们使用了CSS3 Webkit Gradient生成器,它只在支持 CSS3 的浏览器上工作。
$('#divScore').text(json.Score);
$('#divHiScore').text(json.HiScore);
$('#divLines').text(json.Lines);
$('#divLevel').text(json.Level);
$.each(json.Bricks, function (i, val) {
$('#cell_' + val.Row + '_' + val.Col).css('background-image',
'-webkit-gradient(linear, left top, right bottom, color-stop(0.0, ' + val.Color + '),
color-stop(1.0, rgba(0, 0, 0, 0.0)))');
$('#cell_' + val.Row + '_' + val.Col).css('border-color', val.Color);
});
for (var row = 0; row < 2; row++) {
for (var col = 0; col < 4; col++) {
$('#next_' + row + '_' + col).css('background-image',
'-webkit-gradient(linear, left top, right bottom, color-stop(0.0, #000), color-stop(1.0, #000))');
$('#next_' + row + '_' + col).css('border-color', '#333');
}
}
$.each(json.Next, function (i, val) {
$('#next_' + val.Row + '_' + val.Col).css('background-image',
'-webkit-gradient(linear, left top, right bottom, color-stop(0.0, ' + val.Color + '),
color-stop(1.0, rgba(0, 0, 0, 0.0)))');
$('#next_' + val.Row + '_' + val.Col).css('border-color', val.Color);
});
这是 Json viewer 显示的 Json 快照。请注意 Json 中表示的红色砖块。
以下是视图中显示的分数、最高分、行数和等级信息的 Json 数据。
控制器
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
如下所示,Controller
甚至比View
更“愚蠢”。它的目标仅仅是公开供视图使用/调用的操作,调用GameManager
端辛勤工作的方法,并返回(或不返回)一个 JSON 序列化的ViewModel
。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MVCBricks.Core;
namespace MVCBricks.Controllers
{
[System.Web.Mvc.OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public class BricksController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult GetBoard()
{
return new JsonResult() { Data = BricksManager.Instance.CurrentBoard,
JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult Tick()
{
BricksManager.Instance.Presenter.Tick();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult MoveLeft()
{
BricksManager.Instance.Presenter.MoveLeft();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult MoveUp()
{
BricksManager.Instance.Presenter.Rotate90();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult MoveRight()
{
BricksManager.Instance.Presenter.MoveRight();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult MoveDown()
{
BricksManager.Instance.Presenter.MoveDown();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
public ActionResult InitializeBoard()
{
BricksManager.Instance.InitializeBoard();
return new JsonResult() { Data = "", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
}
}
有限状态机
正如您可能预期的那样,游戏必须有一种控制状态的方法,这样在游戏开始之前或游戏结束之后就不会有掉落的方块。此外,在介绍模式下也不应该显示分数。
起初我使用 JavaScript 端的布尔变量来处理游戏状态(即“isPlaying”、“isPaused”、“isGameOver”等等),但这看起来既不优雅也不高效。因此,在开发过程中我意识到应用程序必须使用更好的状态管理,例如有限状态机 (FSM)。所以我搜索并最终找到了一个简单易用的 JavaScript 有限状态机,由Anthony Blackshaw 编写。
Blackshaw 的 FSM 实现非常简单。首先,您声明一个状态机对象(在我们的例子中是gameState
)。然后您向状态机实例添加转换。每个转换定义:
- FSM.prototype.add_transition = function ( action, state, callback, next_state )
- 操作名称,用于处理转换。
- 机器必须持有的初始状态,以便转换工作。
- 转换发生时调用的回调函数。
- 转换结束时状态机切换到的下一个状态。
这是真实的实现
<script type="text/javascript">
//Finite State Machine for JavaScript
//by Anthony Blackshaw
//http: //antsdev.wordpress.com/2008/06/18/a-simple-js-finite-state-machine/
var gameState = new FSM("intro");
gameState.add_transition("play", "intro", changeIntroToPlaying, "playing");
gameState.add_transition("pause", "playing", changePlayingToPaused, "paused");
gameState.add_transition("continue", "paused", changePausedToPlaying, "playing");
gameState.add_transition("end", "playing", changePlayingToGameOver, "gameOver");
gameState.add_transition("showIntro", "gameOver", changeGameOverToIntro, "intro");
play操作隐藏副标题,并将标题的顶部动画到位置0(屏幕顶部)。此外,scorePanel
和boar
都通过它们的不透明度 css 属性的淡入效果动画变得可见。
function changeIntroToPlaying() {
initializeBoard();
$('.subTitle').css('visibility', 'hidden');
$('.press').css('visibility', 'hidden');
$('.title').animate({
top: 0
}, 1000, 'swing', function () {
// Animation complete.
$('.scorePanel').animate({
opacity: 1.0
}, 1000, 'swing', function () {
$('.scorePanel').css('visibility', 'visible');
});
$('.board').animate({
opacity: 1.0
}, 1000, 'swing', function () {
$('.board').css('visibility', 'visible');
});
});
}
pause操作只是在游戏板上方显示gamePaused
div。
function changePlayingToPaused () {
$('#gamePaused').css('visibility', 'visible');
}
游戏中的暂停本身发生是因为状态切换到“paused”,正如我们下面看到的,除非游戏状态机处于“playing”状态,否则不会调用控制器的操作。
...
$(document).everyTime(200, function (i) {
if (gameState.current_state == 'playing') {
$.ajax({
type: "GET",
url: "GetBoard",
...
continue操作只是隐藏游戏板上的gamePaused
div。
function changePausedToPlaying() {
$('#gamePaused').css('visibility', 'hidden');
}
由于状态切换回“playing”,游戏继续进行,因为现在可以调用控制器的操作了。
end动作只是在游戏板上方显示gameOver
div,表明砖块堆积已达板的顶部。
function changePlayingToGameOver () {
$('#gameOver').css('visibility', 'visible');
}
showIntro操作淡出计分面板和游戏板,并将标题动画回屏幕中心。此外,副标题字幕再次显示。
function changeGameOverToIntro() {
$('#gameOver').css('visibility', 'hidden');
$('.scorePanel').animate({
opacity: 0.0
}, 1000, 'swing', function () {
$('.scorePanel').css('visibility', 'hidden');
});
$('.board').animate({
opacity: 0.0
}, 1000, 'swing', function () {
// Animation complete.
$('.board').css('visibility', 'hidden');
$('.title').animate({
top: 200
}, 1000, 'swing', function () {
// Animation complete.
$('.subTitle').css('visibility', 'visible');
$('.press').css('visibility', 'visible');
});
});
}
游戏管理器
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
GameManager
是一个类,它包含BricksController
所需的所有方法,以便BricksView
的请求可以传达给游戏引擎,并且响应可以发送回BricksView
。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MVCBricks.Core
{
public class GameManager : MVCBricks.Core.IView
{
private static GameManager instance = null;
private static BricksPresenter presenter = null;
private static BoardViewModel currentBoard = null;
private GameManager()
{
currentBoard = new BoardViewModel();
currentBoard.Bricks = new BrickViewModel[] { };
presenter = new BricksPresenter(this);
presenter.InitializeBoard();
presenter.Tick();
}
当调用DisplayScore
时,所有记分板数据都被收集起来,以便通过控制器的一次调用即可被视图使用。
public void DisplayScore(int score, int hiScore, int lines,
int level, MVCBricks.Core.Shapes.IShape next)
{
currentBoard.Score = score;
currentBoard.HiScore = hiScore;
currentBoard.Lines = lines;
currentBoard.Level = level;
currentBoard.Next = GetBricksArray(next.ShapeArray.GetUpperBound(1) + 1,
next.ShapeArray.GetUpperBound(0) + 1, next.ShapeArray);
}
GetBricksArray
方法将游戏板砖块数组和下一个形状数组转换为视图可以理解的颜色系统。
private BrickViewModel[] GetBricksArray(int rowCount, int colCount, IBrick[,] array)
{
var bricksList = new List<BrickViewModel>();
for (var row = 0; row < rowCount; row++)
{
for (var col = 0; col < colCount; col++)
{
var b = array[col, row];
if (b != null)
{
bricksList.Add(new BrickViewModel()
{
Row = row,
Col = col,
Color = b.Color.ToString().Replace("Color [", "").Replace("]", "")
});
}
else
{
bricksList.Add(new BrickViewModel()
{
Row = row,
Col = col,
Color = "rgba(0, 0, 0, 1.0)"
});
}
}
}
return bricksList.ToArray();
}
最终思考
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
非常感谢您阅读我的 MVC 砖块文章。我希望它能以某种方式对您有所帮助,无论是通过本文涵盖的 MVC 概念,还是使 JavaScript 部分简洁优雅的 jQuery 语法,甚至是因为游戏本身的乐趣。请在下方随意评论。请分享您的想法、抱怨、建议,以便下一篇文章会越来越好。
历史
- 2011-04-23: 初始版本。
- 2011-04-29: 图像已更正。
- 2011-05-03: Json viewer 图像已附加。
- 2011-05-05: 解释了游戏管理器。