-
Notifications
You must be signed in to change notification settings - Fork 0
Integrating the new Design
All of the code that draws stuff on the screen will be written in JavaScript. This may seem strange but it will clearly illustrate the differences between rendering and state management. Instead of the DOM and HTML, we will use JavaScript as our rendering technology.
It is typical to work on a project with multiple people. Doing all of the drawing in JavaScript will also show that it is possible to divide the work of building an application between people with different skills and technology preferences. By clearly separating rendering from application logic, Pedestal makes this kind of division of labor easy to accomplish.
This section of the tutorial will go over all of the JavaScript which handles the drawing for the game. As we go through all of this code, keep in mind that it could all be written ClojureScript.
All of the drawing that the rendering code is supported by the excellent Raphael JavaScript library.
Some initial design work has been done and the JavaScript API which defines the meeting place between application logic and rendering has been defined. The API is shown below as a collection of JavaScript functions.
Add these functions to the file app/assets/javascripts/game.js
// Create a new game object
var createGame = function(id) {
return BubbleGame(id);
}
// Add a player
var addPlayer = function(g, name) {
g.addPlayer(name);
}
// Add the handler function to call when a point is scored
var addHandler = function(g, f) {
g.addHandler(f);
}
// Set the score for a player
var setScore = function(g, n, x) {
g.setScore(n, x);
}
// Add a new bubble to the screen
var addBubble = function(g) {
g.addBubble();
}
// Remove a bubble from the screen
var removeBubble = function(g) {
g.removeBubble();
}
// Set the value of one of the game statistics
var setStat = function(g, n, v) {
g.setStat(n, v);
}
// The the order or position in the leaderboard for a player
var setOrder = function(g, n, i) {
g.setOrder(n, i);
}The first thing we will do is set up our project so that we have a place to work on the UI code in isolation. We start by adding a new page to the Design area of the project.
In the file tools/public/design.html add the following link
<li><a href="/design/game.html">Game</a></li>and then add the file app/templates/game.html.
<_within file="application.html">
<div id="content">
<div class="row-fluid" template="tutorial" field="id:id">
<div id="game-board"></div>
<!-- We need this because of a bug in Pedestal. Remove when
fixed. -->
<div field="content:something"></div>
</div>
</div>
<script id="script-driver" src="/game-driver.js"></script>
</_within>We will need to add three files to the
The new UI will require raphael.js. Download this library and put it
in app/assets/javascripts/. Next to this file create a JavaScript
file named game.js. This file will contain all of our game rendering
code. In the HTML file above, we refer to the game-driver.js
file. Create this file next to game.js as well.
Finally, add the following scripts tags to application.html just
before the <script id="script-driver"></script> tag at the bottom of
the body section.
<script src="/raphael.js"></script>
<script src="/game.js"></script>The designers on this project have created the UI for the game in JavaScript. This could also be written in ClojureScript. For this project, to emphasize the difference between rendering and application state, we will use the JavaScript for the UI.
The code for the game is shown below. We will add a brief comment for
each section. All of this code should be go into the file game.js.
Instead of just showing numbers on the screen, the total, maximum and average counter values as well as the dataflow statistics will be shown as to distinct bar charts. Each will with single bar that can show multiple values. The code for the bar is shown below.
// A multi-valued bar
// ================================================================================
var Bar = function(paper, x, y, vals) {
var barAnimateTime = 2000;
var barHeight = 20;
var colors = ["#0f0", "#00f", "#f00"];
var rect = function(x, y, w, h, color) {
return paper.rect(x, y, w, h).attr({fill: color, stroke: "none"});
}
var bars = {};
for(var i in vals) {
var b = vals[i];
var size = b.size || 0;
b.bar = rect(x, y, size, barHeight, colors[i % colors.length]);
bars[b.name] = b;
}
var resizeBar = function(bar, size) {
bar.animate({width: size}, barAnimateTime);
}
return {
setSize: function(name, n) {
resizeBar(bars[name].bar, n);
},
vals: vals
}
}We can set the size of a bar by name but it would be nice to have to have a way to set any value in any bar without having to find the bar that contains the value we would like to set.
The code below takes an array of bars and does this for us.
// A group of bars with unique value names
// ================================================================================
var Bars = function(bars) {
var index = {};
for(var i in bars) {
var bar = bars[i];
var vals = bar.vals;
for(var j in vals) {
var val = vals[j];
index[val.name] = bar;
}
}
return {
setSize: function(name, n) {
var b = index[name];
if(b)
b.setSize(name, n);
}
}
}Instead of clicking a button to increment a counter the game will draw circles on the screen and move them around. You have to put your cursor over a circle to make it dissapear and get a point. Every two seconds one circle will be drawn for each player in the game. For every bubble that a player pops, a bubble will be taken away from the other players. There are limited resources and we are all fighting for points.
--- MOVE THE EXPLANATION OF THE GAME SOMEWHERE ELSE ---
We will need to be able to draw circle and move them around. The code for this is shown below.
// Circles
// ================================================================================
var Circles = function(paper, w, h) {
var defaultRadius = 20;
var padding = 50;
var createAnimateTime = 500;
var removeAnimateTime = 200;
var moveAnimateTime = 1000;
var reportScoreFn;
var removeCounter = 0;
var randomPoint = function() {
var maxHeight = h - padding;
var x = Math.floor(Math.random() * w);
if(x < padding)
x = padding;
var y = Math.floor(Math.random() * h);
if(y < padding)
y = padding;
if(y > maxHeight)
y = maxHeight;
return {x: x, y: y};
}
var removeCircle = function(c) {
c.animate({r: 0}, removeAnimateTime, function() {
c.remove();
});
}
var moveCircle = function(c) {
if(c) {
var point = randomPoint();
c.animate({"cx": point.x, "cy": point.y}, moveAnimateTime, function() {
if(removeCounter > 0) {
c.animate({fill: "#000"}, 100)
removeCircle(c);
removeCounter--;
}
moveCircle(c);
});
}
}
var makeCircle = function() {
var point = randomPoint();
var circle = paper.circle(point.x, point.y, 0).attr({
fill: "#f00", stroke: "none", opacity: 0.6
});
circle.animate({r: defaultRadius}, createAnimateTime);
moveCircle(circle);
circle.mouseover(function() {
if(reportScoreFn)
reportScoreFn(1);
removeCircle(circle);
});
}
return {
addCircle: function() {
makeCircle();
},
removeCircle: function() {
removeCounter++;
},
addScoreReporter: function(f) {
reportScoreFn = f;
}
}
}We will create a leaderboard for player scores. The code below will show one player's score.
// Player
// ================================================================================
var Player = function(paper, x, y, name) {
var nameLength = 150;
var fontSize = 20;
var score = 0;
var nameText = paper.text(x, y, name).attr({
"font-size": fontSize,
"text-anchor": "start"});
var scoreText = paper.text(x + nameLength, y, score).attr({
"font-size": fontSize,
"text-anchor": "end"});
var st = paper.set();
st.push(nameText, scoreText);
return {
setScore: function(n) {
score = n;
scoreText.attr({text: score});
},
moveTo: function(y) {
st.animate({y: y}, 400);
}
}
}The code below is responsible for drawing the leaderboard.
// Leaderboard
// ================================================================================
var Leaderboard = function(paper, x, y) {
var playerSpacing = 30;
var players = {};
var playerY = function(i) {
return 50 + (i * playerSpacing);
}
var countPlayers = function() {
var count = 0;
for(var i in players) {
if(players.hasOwnProperty(i))
count++;
}
return count;
}
return {
addPlayer: function(name) {
var i = countPlayers();
var p = Player(paper, x, playerY(i), name);
players[name] = p;
},
setScore: function(name, score) {
var p = players[name];
p.setScore(score);
},
setOrder: function(name, i) {
var p = players[name];
p.moveTo(playerY(i));
},
count: function() {
return countPlayers();
}
}
}The Bubble Game code pulls this all together.
// The Bubble Game
// ================================================================================
var BubbleGame = function(id) {
var paper = Raphael(id, 800, 400);
var bars = Bars([Bar(paper, 0, 380, [{name: "total-count"},
{name: "max-count"},
{name: "avg-count"}]),
Bar(paper, 0, 357, [{name: "max-dataflow"},
{name: "avg-dataflow"},
{name: "current-dataflow"}])]);
var circles = Circles(paper, 500, 380);
var leaderboard = Leaderboard(paper, 550, 0);
// This will be removed as we make improvements to the game.
// The dataflow will control when circles are created.
var makeCircles = function() {
var p = leaderboard.count();
for(var i=0;i<p;i++) {
circles.addCircle();
}
}
setInterval(makeCircles, 2000);
return {
addHandler: circles.addScoreReporter,
addPlayer: leaderboard.addPlayer,
setScore: leaderboard.setScore,
setOrder: leaderboard.setOrder,
setStat: bars.setSize,
addBubble: circles.addCircle,
removeBubble: circles.removeCircle
}
}var game = BubbleGame("game-board");
var me = {name: "Me", score: 0};
var players = [me,
{name: "Fred", score: 0},
{name: "ahbhgtre", score: 0}];
var sortPlayers = function() {
players.sort(function(a, b) {
if(a.score < b.score) return 1;
if(a.score > b.score) return -1;
return 0;
});
for(var i=0;i<players.length;i++) {
players[i].newIndex = i;
}
}
for(var i in players) {
game.addPlayer(players[i].name);
}
var updateCounts = function() {
var total = 0;
var max = 0;
for(var i in players) {
var score = players[i].score;
total += score;
if(score > max)
max = score;
}
var avg = total / players.length;
game.setStat("total-count", total);
game.setStat("max-count", max);
game.setStat("avg-count", avg);
}
setInterval(updateCounts, 1000);
game.addHandler(function(points) {
me.score += points;
game.setScore("Me", me.score);
updateCounts();
});
var rand = function(n) {
return Math.floor(Math.random() * n) + n;
}
var randPlayer = function() {
return Math.floor(Math.random() * players.length);
}
var updateDataflowStats = function() {
game.setStat("max-dataflow", rand(100));
game.setStat("avg-dataflow", rand(50));
game.setStat("current-dataflow", rand(10));
}
setInterval(updateDataflowStats, 1000);
var updatePlayerScores = function() {
var p = players[randPlayer()];
if(p.name != "Me") {
p.score += 1;
game.setScore(p.name, p.score);
game.removeBubble();
}
updateCounts();
}
setInterval(updatePlayerScores, 1000);
var updatePlayerOrder = function() {
sortPlayers();
for(var i in players) {
var p = players[i];
game.setOrder(p.name, i);
}
}
setInterval(updatePlayerOrder, 2000);The tag for this step is step11.