Table of Contents
It's been a long time since I wrote my last article on the ASP.Net subject. This time I decided to write something about it, and more specifically, about ASP.Net MVC.
The MVC approach is, by far, my favorite way of doing things in ASP.Net. The simple fact that you don't have the code behind, nor view state, deserves much of my consideration.
That goes without saying that you don't care about post backs.
Also, the application uses the awesome jQuery library to make our lives easier. As the readers will see, I tried to use jQuery wherever I could. This library
is really a blessing for those of us developers who need to work on web applications.
To use the MVC Bricks game provided with this article, if you already have Visual Studio 2010, that's fine. If you don't, you can download the following 100% free development tool directly from Microsoft:
Visual Web Developer 2010 Express
Also, you MUST open it in a CSS3-enabled browser, because this application uses CSS3 Webkit Gradient.
When the game starts, there is a simple jQuery animation, which makes the "MVC BRICKS" title raise up to the middle of the screen. This is accomplished by
animating the top
css property of the title div:
function showSplashScreen() {
$('.subTitle').css('visibility', 'hidden');
$('.press').css('visibility', 'hidden');
$('.title').animate({
top: 200
}, 1000, 'swing', function () {
$('.subTitle').css('visibility', 'visible');
$('.press').css('visibility', 'visible');
});
}
The above code shows that, when the animation ends, the title reaches the middle of the screen and the subtitle is shown.
The game is all controlled by the keyboard. When the game is in Intro mode, you can use the space bar to start a new game. When the
game is playing, you use the space bar again if you want to pause the game. And then use the space bar again to resume the game.
When the game is over, you push the space bar once again to open the game Intro screen.
Use the left arrow key and right arrow key to move the falling piece to the left or to the right. Press the up arrow key
to rotate the piece 90 degrees. Finally, press the down arrow key to speed up the fall of the piece.
Key |
Command |
Space bar |
Start game / Pause game / Resume game / Restart game |
Left arrow key |
Move falling piece to the left |
Right arrow key |
Move falling piece to the right |
Down arrow key |
Speed up falling piece |
You must be very familiar with this kind of game, but I have to explain the game rules anyway.
Here we have an empty 10 x 16 board, containing 160 empty positions. As soon as the game starts,
the game engine will randomly generate one new piece at a time, which will fall from the top of the
board, falling at the speed of 1 square per second. When the falling piece find an obstacle (that is,
part of another piece that is fixed at the bottom of the board) then it can't fall anymore, so that
falling piece gets stuck. Then the game engine will produce new random pieces, and they are piled up
until the pile reaches the top of the board, and at this moment the game ends. The user will have to
control each falling piece, by moving it to the left, to the right, or rotating it, placing the new
piece in the lowest possible empty place in the board where the new piece fits in, in a way to avoid
the piled pieces to reach the top of the board. Also, when the user fills any the board rows, these
rows are cleared, thus giving some extra space and prolonging the game.
|
|
|
|
The "I" shape |
The "L" shape |
The "J" shape |
The "O" shape |
|
|
|
The "T" shape |
The "S" shape |
The "Z" shape |
The game engine can randomly generate any of the above shapes. As we can see, each shape is associated with a letter which resembles it.
For each cleared line, the user scores a total of 10 multiplied by the game level. That is, each row cleared in the first level will give 10 points. The second level
will give 20 points per cleared row, and so on.
Each level is completed when the user has cleared 10 rows. That is, to reach the 5th level, the user must have cleared 40 rows.
When the game is over, the game score is compared with the previous high score, and replaces it if there is a new record.
The Next piece gives the user the opportunity to place the current piece in a way that makes it easier to accomodate the next falling piece.
The beauty of MVC, as I see it, is its adherence to the principle of separation of concerns.
Unlike "classic" (non-MVC) ASP.net, you don't put business logic on your views. Instead, you use the views only for presentation logic (such as parsing and rendering of raw
data or input validation), and for presentation itself. On the other side, you reserve the Controllers (or another layer, such as Service layer) for your business rules.
If you look at the javascript in the application, you won't find the business logic. Instead, you'll find out that it is really thin and light. Fortunately,
since I already had the Bricks game logic from previous projects, I was able to maintain my managed code almost intact on the server side,
and make the view communicate with it through a new ViewModel
I designed specifically for this MVC project.
The model is defined by the BoardViewModel
and BrickViewModel
classes, and contains all information needed
by the View to render the game board, the score board and to know whether the game is over or not. As we can see below, most of the properties
of BoardViewModel
class are native types, except for the Bricks
and Next
properties, which are
2-dimensional arrays of the BrickViewModel
and hold the data for the bricks and empty spaces that forms the current snapshot
of the game board and the bricks corresponding to the Next
piece that will fall from the top of the game board.
The low-level BrickViewModel
class has informations about each individual brick: row, column and the color name. These values
will be used by the View to find the corresponding divs and update their background color accordingly.
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; }
}
}
The view really contains no business logic (in our case, no game logic). Here we can see its basic goals:
- Set up a timer, which calls the
Controller
every 200 milliseconds to get a ViewModel containing an updated game board snapshot (serialized as JSON).
- Parse the returned JSON to render the board, score, high score, level and next piece.
- Set up another timer that calls the
Controller
every 1000 milliseconds to request a new movement for the falling pieces.
- Listen to the keyboard events, and calls the
Controller
to start a new game, to pause of resume the game, to move or rotate the falling piece, or to restart the game.
This is pretty much what the View does. Notice that, in a traditional (non-MVC) ASP.net application, we would have the code behind class, which would
probably hold some business logic. Thanks to MVC, the principle of separation of concerns is maintained, and we can move the business logic away from our View.
We handle user's gesture by attaching a function to the keydown
event of the page document using jQuery syntax:
$(document).keydown(function (event) {
switch (event.keyCode) {
case 32: 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: if (gameState.current_state == 'playing')
moveLeft();
break;
case 38: if (gameState.current_state == 'playing')
moveUp();
break;
case 39: if (gameState.current_state == 'playing')
moveRight();
break;
case 40: if (gameState.current_state == 'playing')
moveDown();
break;
}
});
The following table shows the events (such as timer ticks or key pressing), the jQuery-like ajax call on the BricksView
side and the methods (actions) called on the BricksController
side.
Event / Key |
The View code |
The Controller code |
Every 1000 milliseconds |
$(document).everyTime(1000, function (i) {
$.ajax({
type: "GET",
url: "Tick",
cache: false,
dataType: "json",
error: function (xhr, status, error) {
},
success: function (json) {
}
});
});
|
public ActionResult Tick()
{
BricksManager.Instance.Presenter.Tick();
return new JsonResult()
{ Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
Every 200 milliseconds |
$(document).everyTime(200, function (i) {
if (gameState.current_state == 'playing') {
$.ajax({
type: "GET",
url: "GetBoard",
cache: false,
dataType: "json",
error: function (xhr, status, 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) {
},
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) {
},
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) {
},
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) {
},
success: function (json) {
}
});
}
|
public ActionResult MoveUp()
{
BricksManager.Instance.Presenter.Rotate90();
return new JsonResult() { Data = "",
JsonRequestBehavior =
JsonRequestBehavior.AllowGet };
}
|
Here goes basically all the html we need for the View
. Notice that all elements you see on the game screen are there,
except for the bricks:
<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>
This happens because the bricks are divs
generated dinamically, when the application starts. Instead of hard-coding this divs into
the html, the dinamic generation makes it a lot easier to control how the bricks are generated, how they are rendered and so on.
Here is the code that generates all the bricks, incluiding the bricks in the "Next" section on the score board. Notice how elegant is the jQuery
syntax for appending html to existing html elements:
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');
}
}
One of the most important parts on the BricksView
side is the game board rendering. Notice that we don't use
images for the bricks; instead, we use the CSS3 Webkit Gradient generator, which works only on CSS3 enabled browsers:
$('#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);
});
This is a snapshot of the Json shown by Json viewer. Notice the red bricks represented in Json:
And here are the Json data of Score, High Score, Lines and Level information presented in the view:
As we can see below, the Controller
is even dumber than the View
. Its goal is merely to expose actions
that are consumed/called by the view, call the hard-working method on the GameManager
side, and return (or not) a JSON-serialized 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 };
}
}
}
As you might expect, the game must have a means to control the states, so that you don't have falling pieces before the game has started,
or after the game is over. Also, the score shouldn't be displayed while in the Intro mode.
At first I was using boolean variables on the javascript side to handle the game state (i.e. "isPlaying", "isPaused", "isGameOver", and so on),
but this didn't seem elegant nor efficient. So, at the middle of the development I noticed the application had to use a better state management,
such as a finite state machine (FSM). So I searched and finally found out a simple, easy-to-use
finite state machine for JavaScript, written by Anthony Blackshaw.
The implementation for Blackshaw's FSM is quite simple. First, you declare a state machine object (in our case, gameState
). Then you add transitions
to the state machine instance. Each transition defines:
- FSM.prototype.add_transition = function ( action, state, callback, next_state )
- The action name, which is invoked to process the transition.
- The initial state the machine must hold for the transition to work.
- The callback function which is called when the transition occurs.
- The next state to which the state machine is switched at the end of the transition.
And here is the real implementation:
<script type="text/javascript">
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");
The play action hides the subtitle, and animates the top of the title o the position 0 (top of the screen). Besides,
both the scorePanel
and the boar
are made visible thanks to an fade-in effect animation on
their opacity css property..
function changeIntroToPlaying() {
initializeBoard();
$('.subTitle').css('visibility', 'hidden');
$('.press').css('visibility', 'hidden');
$('.title').animate({
top: 0
}, 1000, 'swing', function () {
$('.scorePanel').animate({
opacity: 1.0
}, 1000, 'swing', function () {
$('.scorePanel').css('visibility', 'visible');
});
$('.board').animate({
opacity: 1.0
}, 1000, 'swing', function () {
$('.board').css('visibility', 'visible');
});
});
}
The pause action just shows the gamePaused
div over the game board.
function changePlayingToPaused () {
$('#gamePaused').css('visibility', 'visible');
}
The pause itself in the game play happens because the state is switched to "paused" and as
we can see below, the controller's action is not called unless the game state machine is on the "playing" state:
...
$(document).everyTime(200, function (i) {
if (gameState.current_state == 'playing') {
$.ajax({
type: "GET",
url: "GetBoard",
...
The continue action just hides the gamePaused
div over the game board.
function changePausedToPlaying() {
$('#gamePaused').css('visibility', 'hidden');
}
Since the state is switched back to "playing", the game continues because the controller's action
can now be called.
The end action just shows the gameOver
div over the game board, indicating that the pile
of bricks reached the top of the board.
function changePlayingToGameOver () {
$('#gameOver').css('visibility', 'visible');
}
The showIntro action fades-out both the score panel and the game board, and animates the title back to the center of the screen. In addition,
the subtitle credits are shown again.
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 () {
$('.board').css('visibility', 'hidden');
$('.title').animate({
top: 200
}, 1000, 'swing', function () {
$('.subTitle').css('visibility', 'visible');
$('.press').css('visibility', 'visible');
});
});
}
The GameManager
is a class that holds all methods needed by the BricksController
so that the BricksView
requests can be communicated to the game engine, and the responses can be sent back
to the 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();
}
When the DisplayScore
is called, all the scoreboard data are gathered so that they can be consumed by the view through a single call from the controller.
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);
}
The GetBricksArray
method converts both the game board bricks array and the next shape array into a system of colors which
the view can understand.
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();
}
Thanks a lot for reading my MVC Bricks article. I hope it may be useful for you in some way, either by the MVC concepts covered here, or the jQuery syntax which makes
the javascript section thin and elegant, or even because the fun of the game itself. Feel free to comment below. Please share your ideas, complaints, advices so the
next articles will get better and better.
- 2011-04-23: Initial version.
- 2011-04-29: Images corrected.
- 2011-05-03: Json viewer images attached.
- 2011-05-05: Game Manager explained.