Skip to content

Integrating the new Design

Ryan Neufeld edited this page Jul 9, 2013 · 13 revisions

To follow along with this section, start with tag v2.0.14.

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, the game will use JavaScript for drawing.

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-app 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 you go through all of this code, keep in mind that it could all be written in ClojureScript.

All of the drawing for the game is done with the Raphael JavaScript library.

This section is not about pedestal-app at all. If you are interested in the JavaScript code which draws the game, then go through this section. Otherwise, you can checkout the tag v2.1.0 and start work on the new renderer.

Adding a new page for game UI design

Before you start writing the code, let's first set up the project so that there is a place to work on the UI code in isolation. Start by adding a new page to the Design area of the project.

In the file tutorial-client/tools/public/design.html add the following link

<li><a href="/design/game.html">Game</a></li>

Remember that you only need to add this to give you an easy way to get to this page.

Add the game template file tutorial-client/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>
    </div>

  </div>

  <script id="script-driver" src="/game-driver.js"></script>

</_within>

Download raphael.js and place it in the folder tutorial-client/app/assets/javascripts/. The code shown below uses Raphael version 2.1.0. Along with this file, create two other JavaScript files: game.js and game-driver.js. As you may have noticed, the game.html file above loads the game-driver.js file.

Separating the driver code from the main JavaScript file in this way and using the pedestal-app template system to load the file allows you to have a driver which exists in the design view but will not be included in any other version of the application.

Update application.html to load the new JavaScript files by adding the following script tags 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>

You are now ready to write some JavaScript.

Drawing the Game

The code for the game is shown below. There will be a brief comment for each section. All of this code should be go into the file game.js.

Creating a simple bar chart

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 two distinct bar charts. Each one with a single bar that can show multiple values.

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);
  }

  var destroy = function() {
    for(var i in bars) {
      if(bars.hasOwnProperty(i)) {
        bars[i].bar.stop();
        bars[i] = null;
      }
    }
  }

  return {
    setSize: function(name, n) {
      resizeBar(bars[name].bar, n);
    },
    vals: vals,
    destroy: destroy
  }
}

This object gives you the ability to set the size of a part of the bar if you know its name. It would be nice to be able to set the size by name for any part of multiple bars.

The code below takes an array of bars and provides this functionality.

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;
    }
  }

  var destroy = function() {
    for(var i in bars) {
      bars[i].destroy();
    }
  }

  return {
    setSize: function(name, n) {
      var b = index[name];
      if(b)
        b.setSize(name, n);
    },
    destroy: destroy
  }
}

Drawing circles

Instead of clicking a button to increment a counter the game will draw circles on the screen and move them around.

The code below handles the drawing and moving of 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 removeAll = false;

  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) {
    if(c && !removeAll) {
      c.animate({r: 0}, removeAnimateTime, function() {
        c.remove();
      });
    }
  }

  var moveCircle = function(c) {

    if(c && !removeAll) {
      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 birth = new Date();
    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() {

      var death = new Date();
      var t = death - birth;
      var points = 1;
      if(t <= 500)
        points = 3;
      else if(t <= 1000)
        points = 2;

      if(reportScoreFn)
        reportScoreFn(points);
      removeCircle(circle);
    });
  }

  var destroy = function() {
    removeAll = true;
  }

  return {
    addCircle: function() {
      makeCircle();
    },
    removeCircle: function() {
      removeCounter++;
    },
    addScoreReporter: function(f) {
      reportScoreFn = f;
    },
    destroy: destroy
  }
}

Showing a player's score

The game will have a leaderboard showing the top scores. The code below handles drawing each name and score on the leaderboard.

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 leaderboard

The leaderboard code is straight forward.

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

The BubbleGame object pulls everything together. This code includes a loop that creates new bubbles every 2 seconds. This loop will be removed once bubble creation is being controlled from outside the drawing code.

var BubbleGame = function(id) {

  var paper = Raphael(id, 800, 400);

  var bars = Bars([Bar(paper, 0, 380, [{name: "total-count"},
                                       {name: "max-count"},
                                       {name: "average-count"}]),
                   Bar(paper, 0, 357, [{name: "dataflow-time-max"},
                                       {name: "dataflow-time-avg"},
                                       {name: "dataflow-time"}])]);

  var circles = Circles(paper, 500, 380);

  var leaderboard = Leaderboard(paper, 550, 0);

  var destroy = function() {
    circles.destroy();
    circles = null;
    bars.destroy();
    bars = null;
    leaderboard = null;
    paper.remove();
    paper = null;
  }

  // This will be removed as we make improvements to the game.
  // The dataflow will control when circles are created.
  var makeCircles = function() {
    if(leaderboard) {
      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,
    destroy: destroy
  }
}

A JavaScript game Driver

While working on the code above, it is helpful to have a JavaScript driver which can make things work. The driver is located in the file tutorial-client/app/assets/javascripts/game-driver.js. The driver simulates all of the activities of the application.

var me = {name: "Me", score: 0};
var players = [me,
               {name: "Fred", score: 0},
               {name: "ahbhgtre", score: 0}];
var game = null;
var gameActive = false;

var rand = function(n) {
  return Math.floor(Math.random() * n) + n;
}

var randPlayer = function() {
  return Math.floor(Math.random() * players.length);
}

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;
  }
}

var updateCounts = function() {
  if(gameActive) {
    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("average-count", avg);

    setTimeout(updateCounts, 1000);
  }
}

var updateDataflowStats = function() {
  if(gameActive) {
    game.setStat("dataflow-time-max", rand(100));
    game.setStat("dataflow-time-avg", rand(50));
    game.setStat("dataflow-time", rand(10));
    setTimeout(updateDataflowStats, 1000);
  }
}

var updatePlayerScores = function() {
  if(gameActive) {
    var p = players[randPlayer()];
    if(p.name != "Me") {
      p.score += 1;
      game.setScore(p.name, p.score);
      game.removeBubble();
    }
    updateCounts();
    setTimeout(updatePlayerScores, 1000);
  }
}

var updatePlayerOrder = function() {
  if(gameActive) {
    sortPlayers();
    for(var i in players) {
      var p = players[i];
      game.setOrder(p.name, i);
    }
    setTimeout(updatePlayerOrder, 2000);
  }
}

var startGame = function() {
  console.log("start game");
  game = BubbleGame("game-board");

  gameActive = true;

  game.addHandler(function(points) {
    me.score += points;
    game.setScore("Me", me.score);
    updateCounts();
  });

  for(var i in players) {
    game.addPlayer(players[i].name);
  }
  updateCounts();
  updateDataflowStats();
  updatePlayerScores();
  updatePlayerOrder();
}

var endGame = function() {
  console.log("end game");

  gameActive = false;

  game.destroy();
}

setTimeout(startGame, 1000);
setTimeout(endGame, 10000);
setTimeout(startGame, 15000);

With all of this in place, you can try out the game by visiting the Design page (http://localhost:3000/design.html) and clicking on the Game link.

Next steps

In the next section you will update the renderer to use this for drawing instead of an HTML template.

The tag for this section is v2.1.0.

Home | Rendering the Game

Clone this wiki locally