May
26
Written by:
Michael Washington
5/26/2015 5:29 PM
NOTE: You can try the application at this link: http://lsfrackinggame.lightswitchhelpwebsite.com/HTMLClient/ (use your username and password from LightSwitchHelpWebsite.com)
To demonstrate how powerful the Visual Studio LightSwitch HTML Client framework is, we created the beginnings of a turn based multi-user game. Right now the game doesn’t do much. It simply displays a game board and allows you to move. However, it does show the moves of other players in real-time and handles all security and game play collisions.
What would we do this?
The reason Thomas Capps (he collaborated on a previous endeavor here) and I decided to use LightSwitch for the proof of concept for the game we are planning, is time. We don’t have a lot of it. In fact we only have 30-45 minutes a week, and we frequently skip weeks. Even with the amount of functionality we implemented, the project has a surprisingly small amount of custom user code. In addition, all of the back-end server and OData code will be used, as is, in the final version of the game. We plan to use Unity as the client for the final game.
The Game
After logging in, players are presented with a multicolored game board with five controls at the bottom of the screen.
The player is represented by a red “P”. All other players are represented by black “X”’s.
Clicking the Left, Right, Up and Down buttons will move the entire game board.
Clicking the Roll Dice button will display possible moves.
The player can then click on one of the highlighted spaces to move to it.
Players who are currently logged in will see the moves of other players in real-time.
About The Game
This article will cover the important programming aspects of the game including:
- Start-Up
- Command Table Pattern
- WCF RIA Services
- JavaScript Client
- Display Game Board
- Taking A Turn
- Rolling Dice
- Moving Player
- Real-Time Multiplayer (SignalR)
Start-Up
When the player logs into the game, the game board displays, and the following JavaScript code runs:
myapp.BrowseGetGameBoardSpacesForUser.GetGameBoardSpacesForUser_render = function (element, contentItem) {
// This method is called so that player profile (and extra game board spaces) can be created if needed
msls.promiseOperation(CallGetUserName).then(function PromiseSuccess(GetUserNameResult) {
strCurrentUserName = GetUserNameResult;
});
This calls an .ashx file handler that uses ServerApplicationContext to call the CreateSpaces method using the Command Table Pattern (covered in the following section).
public void ProcessRequest(HttpContext context)
{
using (var serverContext = ServerApplicationContext.CreateContext())
{
// Get the current user
string strCurrentUserName = serverContext.Application.User.Name.ToLower();
// Try to get the Profile
var objPlayer = (from player in serverContext.DataWorkspace.ApplicationData.Players
where player.UserName.ToLower() == strCurrentUserName
select player).Execute().FirstOrDefault();
if (objPlayer == null) // No Profile found
{
// Create a Default Profile
Player objNewPlayer;
objNewPlayer = serverContext.DataWorkspace.ApplicationData.Players.AddNew();
objNewPlayer.UserName = strCurrentUserName;
objNewPlayer.NetWorth = 0;
objNewPlayer.LastNetWorthCalculatedTime = DateTimeOffset.Now;
objNewPlayer.RemainingRolls = 5;
objNewPlayer.CounterResetTime = DateTimeOffset.Now.AddHours(24);
objNewPlayer.Credits = 100;
serverContext.DataWorkspace.ApplicationData.SaveChanges();
GameSessionCommand objGameSessionCommand =
serverContext.DataWorkspace.ApplicationData.GameSessionCommands.AddNew();
// Create game board spaces and player if necessary
objGameSessionCommand.CommandAction = "CreateSpaces";
objGameSessionCommand.CommandParameter = " ";
serverContext.DataWorkspace.ApplicationData.SaveChanges();
}
context.Response.ContentType = "text/plain";
context.Response.Write(serverContext.Application.User.Name.ToLower());
}
}
The Command Table Pattern
As we discovered in the previous endeavor, Manipulyte: A Proof Of Concept Visual Studio LightSwitch Turn Based HTML5 SPA Game, there was the need for, and the implementation of, the Command Table Pattern. Essentially it means that:
You need a method to allow a user to perform an action that you cannot simply give them the ability to do directly.
Even if you are not creating a video game you will find you may have this need. It is frequently required in business applications. For example, you may not want to allow a user to cancel an order directly.
You can see a detailed explanation at the following link: OData Security Using The Command Table Pattern And Permission Elevation.
For our current game, we implemented the Command Table Pattern by creating a table called GameSessionCommands.
We then blocked off some, and in some cases, all access to all the other tables (to everyone but administrators) using code such as this:
partial void Players_CanDelete(ref bool result)
{
result = (this.Application.User.HasPermission(Permissions.SecurityAdministration) ||
this.Application.User.HasPermission(Permissions.CanWriteToTables));
}
partial void Players_CanUpdate(ref bool result)
{
result = (this.Application.User.HasPermission(Permissions.SecurityAdministration) ||
this.Application.User.HasPermission(Permissions.CanWriteToTables));
}
The only way a player can make a move is by inserting a record into the
GameSessionCommands table.
These methods contain code to enforce game rules and prevent cheating.
For example, the following code is used for CreateSpaces:
if (entity.CommandAction == "CreateSpaces")
{
// Elevate the user's permission to allow the Entity to be updated
this.Application.User.AddPermissions(Permissions.CanWriteToTables);
// Get count of GameBoardSpaces
int CountOfGameBoardSpaces = GameBoardSpaces.Count();
// Get count of Occupied GameBoardSpaces
int CountOfOccupiedGameBoardSpaces =
GameBoardSpaces.Where(x => x.SpaceOwner != null).Count();
// If there are not 100 spaces for every 10 wells drilled make 100 spaces
if (CountOfOccupiedGameBoardSpaces * 10
< CountOfGameBoardSpaces || CountOfGameBoardSpaces == 0)
{
CreateNewGameSpaces();
}
// See if current user already has a space assigned
// Get the current user
string strCurrentUserName = this.Application.User.Name.ToLower();
var objCurrentSpace = (from gameSpace in this.GameBoardSpaces
where gameSpace.SpaceOccupier.UserName.ToLower()
== strCurrentUserName
select gameSpace).FirstOrDefault();
if (objCurrentSpace == null)
{
// The user is not on a current space, so find him one and place him there
var objCurrentPlayer = (from player in this.Players
where player.UserName.ToLower()
== strCurrentUserName
select player).FirstOrDefault();
var openSpace = (from gameSpace in this.GameBoardSpaces
where gameSpace.SpaceOccupier == null
select gameSpace).OrderBy(x => x.Latitude)
.ThenBy(x => x.Longitude).FirstOrDefault();
if (openSpace != null)
{
openSpace.SpaceOccupier = objCurrentPlayer;
}
}
}
WCF RIA Service
Because most of the tables have their access blocked, when data needs to be shown, such as the game board, a WCF RIA Service allows granular access.
The following method is used to return a section of the game board when passed a players current latitude and longitude:
public IQueryable<WCFGameBoardSpace> GetGameBoardSpacesForUser(int? Latitude, int? Longitude)
{
var colWCFGameBoardSpace = (from objGameBoardSpace in this.Context.GameBoardSpaces
where objGameBoardSpace.Latitude >= Latitude
where objGameBoardSpace.Latitude < (Latitude + 10)
where objGameBoardSpace.Longitude >= Longitude
where objGameBoardSpace.Longitude < (Longitude + 10)
orderby objGameBoardSpace.Latitude, objGameBoardSpace.Longitude
select new WCFGameBoardSpace
{
Id = objGameBoardSpace.Id,
Latitude = objGameBoardSpace.Latitude,
Longitude = objGameBoardSpace.Longitude,
Playable = objGameBoardSpace.Playable,
SpaceType = objGameBoardSpace.SpaceType,
SpaceOwner = objGameBoardSpace.SpaceOwner.UserName.ToLower(),
SpaceOccupier = objGameBoardSpace.SpaceOccupier.UserName.ToLower()
}).ToList();
return colWCFGameBoardSpace.AsQueryable();
}
The HTML Client (JavaScript)
The application contains a single screen that contains a Custom Control that displays the game board and the players.
Display Game Board
If we click the Edit Render Code link for the control, we are taken to the JavaScript that calls the GetGameStatus.ashx file handler that returns the data that indicates the status of the player and the other players on the board. It then calls the showItems method that calls the WCF RIA Service that provides the game board data:
// Call CallGetGameStatus to get latest game session info
msls.promiseOperation(CallGetGameStatus).then(function PromiseSuccess(GetGameStatusResult) {
// Parse the JSON returned
var objGameStatus = jQuery.parseJSON(GetGameStatusResult);
// Set lblMessage
objScreen.lblMessage = objGameStatus.PlayerLastStatusMessage;
objScreen.PlayerLatitude = objGameStatus.PlayerLatitude;
objScreen.PlayerLongitude = objGameStatus.PlayerLongitude;
var boolRecenterPlayer = false;
// If player is off the board, reset the upper-left-hand corner to correct area
if ((objScreen.PlayerLatitude >= (objScreen.Latitude + 10)) ||
(objScreen.PlayerLatitude < objScreen.Latitude)) {
objScreen.Latitude = objScreen.PlayerLatitude;
boolRecenterPlayer = true;
}
if ((objScreen.PlayerLongitude >= (objScreen.Longitude + 10)) ||
(objScreen.PlayerLongitude < objScreen.Longitude)) {
objScreen.Longitude = objScreen.PlayerLongitude;
boolRecenterPlayer = true;
}
if (boolRecenterPlayer === true) {
showItems([]);
}
});
The ShowItems method actually displays the squares on the game board. It allows an array of possible moves to be highlighted when called as part of a player taking a turn (coved in the next section).
function showItems(paramColValidMoves) {
var i = 0;
var tablecontentrowTemplate = [$("<tr></tr>")];
var lastLatitude = 0;
NumberOfSpacesReturned = objScreen.GetGameBoardSpacesForUser.count;
// Loop through all GameBoardSpaces
$.each(objScreen.GetGameBoardSpacesForUser.data, function (i, GameBoardSpace) {
// Insert row break if latitude changes
if (lastLatitude != 0 && lastLatitude != GameBoardSpace.Latitude) {
// Create tableRow
tablecontentrowTemplate[0].appendTo($(objTable));
// Reset the row
tablecontentrowTemplate = [$("<tr></tr>")];
}
lastLatitude = GameBoardSpace.Latitude;
// Set IsPossibleMove
var IsPossibleMove = false;
for (var i = 0, len = paramColValidMoves.length; i < len; i++) {
if (paramColValidMoves[i].Latitude == GameBoardSpace.Latitude
&& paramColValidMoves[i].Longitude == GameBoardSpace.Longitude) {
IsPossibleMove = true;
break;
}
}
// Create cell
if (IsPossibleMove) {
$("<td style='padding: 0px; vertical-align: center'></td>")
.html(GetGameSpaceImage(GameBoardSpace, IsPossibleMove))
.appendTo($(tablecontentrowTemplate))
.click(function () {
MoveGamePiece(GameBoardSpace.Latitude, GameBoardSpace.Longitude);
return false;
});
}
else {
$("<td style='padding: 0px; vertical-align: center'></td>")
.html(GetGameSpaceImage(GameBoardSpace, IsPossibleMove))
.appendTo($(tablecontentrowTemplate));
}
});
// Create tableRow
tablecontentrowTemplate[0].appendTo($(objTable));
};
More information on the method used to dynamically create a table in LightSwitch can be found here: LightSwitch HTML Client For The Desktop Web Browser.
Rolling Dice
When the Roll Dice button is pressed, the following code runs that calls the RollDie code in the GameSessionCommands table. This updates the database. Then the GetGameStatus.ashx file handler is called, this time returning an array of possible moves. showItems is called to display the game board and the players, this time with the possible moves highlighted:
myapp.BrowseGetGameBoardSpacesForUser.RollDice_execute = function (screen) {
// Create a new Game Session Command
var GameSessionCommand = new myapp.GameSessionCommand();
GameSessionCommand.CommandAction = "RollDie";
GameSessionCommand.CommandParameter = " ";
var filter = "(UserName eq " + msls._toODataString(strCurrentUserName, ":String") + ") ";
myapp.activeDataWorkspace.ApplicationData.Players.filter(filter).execute().then(function (result) {
// Get the results of the query
var objPlayer = result.results[0];
// Set the player
GameSessionCommand.setPlayer(objPlayer);
// Save the GameSessionCommand to execute a die roll
return myapp.activeDataWorkspace.ApplicationData.saveChanges().then(function () {
// Call CallGetGameStatus to get latest game session info
msls.showProgress(msls.promiseOperation(CallGetGameStatus)
.then(function PromiseSuccess(GetGameStatusResult) {
// Parse the JSON returned
var objGameStatus = jQuery.parseJSON(GetGameStatusResult);
// Set lblMessage
screen.lblMessage = objGameStatus.PlayerLastStatusMessage;
screen.PlayerLatitude = objGameStatus.PlayerLatitude;
screen.PlayerLongitude = objGameStatus.PlayerLongitude;
// Refresh game board
screen.GetGameBoardSpacesForUser.load().then(function (results) {
// clear the table
$("#gameTable tr").remove();
showItems(objGameStatus.colValidMoves);
});
}));
}, function fail(e) {
myapp.cancelChanges();
throw e;
});
}, function (error) {
alert(error);
});
};
Moving Player
When possible moves are highlighted, a player can click on one of the squares to move to it.
The code to move a game piece is simple. However it still calls the GameSessionCommand table and then gets a response by refreshing the game board to ensure that only a valid move can be made.
function MoveGamePiece(Latitude, Longitude) {
// Create a new Game Session Command
var GameSessionCommand = new myapp.GameSessionCommand();
GameSessionCommand.CommandAction = "MovePlayer";
GameSessionCommand.CommandParameter = Latitude + ',' + Longitude;
var filter = "(UserName eq " + msls._toODataString(strCurrentUserName, ":String") + ") ";
myapp.activeDataWorkspace.ApplicationData.Players.filter(filter).execute().then(function (result) {
// Get the results of the query
var objPlayer = result.results[0];
// Set the player
GameSessionCommand.setPlayer(objPlayer);
// Save the GameSessionCommand to move the player
return myapp.activeDataWorkspace.ApplicationData.saveChanges().then(function () {
// Set lblMessage
objScreen.lblMessage = "";
objScreen.PlayerLatitude = Latitude;
objScreen.PlayerLongitude = Longitude;
// Refresh game board
objScreen.GetGameBoardSpacesForUser.load().then(function (results) {
// clear the table
$("#gameTable tr").remove();
showItems([]);
});
}, function fail(e) {
myapp.cancelChanges();
throw e;
});
}, function (error) {
alert(error);
});
}
Real-Time Multiplayer (SignalR)
When a player makes a move, all other players have their screens updated instantly. This is done using SignalR.
When a player makes a move, the following C# code in the GameSessionCommands_Inserting method runs to create a message, describing the move, and who is making it, and broadcasting it:
string message = strCurrentUserName + ","
+ CurrentPlayerSpace.Latitude + ","
+ CurrentPlayerSpace.Longitude + ","
+ LatitudeMove + "," + LongitudeMove;
var context = GlobalHost.ConnectionManager.GetHubContext<ContactHub>();
context.Clients.All.broadcastMessage(message);
The following JavaScript listens for the broadcasts and updates the game board:
myapp.BrowseGetGameBoardSpacesForUser.created = function (screen) {
// Write code here.
screen.Latitude = 1;
screen.Longitude = 1;
objScreen = screen;
// This sets up SignalR to work on the page
$(function () {
contact = $.connection.contactHub;
contact.client.broadcastMessage = function (message) {
// only show updates for players on visible 10x10 window
var arrayPlayerMove = message.split(',');
var PlayerName = arrayPlayerMove[0];
var PlayerLatitudeOld = arrayPlayerMove[1];
var PlayerLongitudeOld = arrayPlayerMove[2];
var PlayerLatitudeNew = arrayPlayerMove[3];
var PlayerLongitudeNew = arrayPlayerMove[4];
// This filters out any players not on visible board
if ((PlayerLatitudeNew >= (objScreen.Latitude - 10))
&& (PlayerLatitudeNew <= (objScreen.Latitude + 20))) {
if ((PlayerLongitudeNew >= (objScreen.Longitude - 10))
&& (PlayerLongitudeNew <= (objScreen.Longitude + 20))) {
screen.updates = message;
// Do not show messages of your own moves
if (strCurrentUserName.toLowerCase() !== PlayerName.toLowerCase()) {
// Refresh game board
objScreen.GetGameBoardSpacesForUser.load().then(function (results) {
// clear the table
$("#gameTable tr").remove();
showItems([]);
});
}
}
}
};
$.connection.hub.start()
.done(function () {
})
.fail(function () {
alert("Could not Connect! - ensure EnableCrossDomain = true");
});
});
};
Links
Manipulyte: A Proof Of Concept Visual Studio LightSwitch Turn Based HTML5 SPA Game
OData Security Using The Command Table Pattern And Permission Elevation
LightSwitch HTML Client For The Desktop Web Browser
Download Code
The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
(you must have Visual Studio 2013 (or higher) installed to run the code)