Building Sokoban with Polymer
When I first heard about Polymer, I thought about my old Silverlight days. Silverlight used XHTML for markup and C# for the code. Polymer is similar, but Polymer uses HTML and Javascript. See this excellent article for an introduction to Polymer. In this tutorial we will build the classic Sokoban game, leveraging the use of Web Components and an excellent Yeoman generator, generator-polymer, and publish it using Bower.
Setup Polymer
Setting up a Polymer project is as simple as the following two commands:
$ npm install generator-polymer -g
$ yo polymer
It will ask you to include some standard components. Since we don’t need any, you can say no to all.
This is the generated folder structure. All custom elements are in app/elements
folder.
.
|-- Gruntfile.js
|-- app
| |-- elements
| | |-- elements.html
| | |-- soko-ban
| | | |-- soko-ban.html
| | | `-- soko-ban.scss
| |-- index.html
| |-- scripts
| | |-- app.js
|-- bower.json
`-- package.json
To begin development run grunt serve
. It will serve index.html
and watch the files for live-reloading as they change. This is the index.html
, I’ve only included the essential parts to use Polymer.
<html>
<head>
<script src="bower_components/platform/platform.js"></script>
<!-- build:vulcanized elements/elements.vulcanized.html -->
<link rel="import" href="elements/elements.html">
<!-- endbuild -->
</head>
<body unresolved>
<div class="game-container">
<!-- insert your elements here -->
<soko-ban></soko-ban>
</div>
<script src="scripts/app.js"></script>
</body>
</html>
We include platform.js
to enable Polymer, and import elements.html
that further imports all our elements. Notice it’s wrapped in a build:vulcanized
build block that will concatenate all our imported elements in a single file. Finally in the body
we add our custom Polymer elements. I’ve included the final element that we will be building, sokoban-ban
, you can replace it with the other sub elements to test them out as you build.
Custom Element: sprite-el
The first custom element we will build is a sprite element, this will serve as the base for all the sprites, such as boxes and our player. To add a custom element run a single command.
$ yo polymer:el sprite-el
This will create the elements/sprite-el
sub-folder and add two files, sprite-el.html
and sprite-el.scss
. It will also inject sprite-el.html
in elements.html
, basically doing the boilerplate for you.
See sprite-el.html
injected into elements.html
by Yeoman.
File: elements/elements.html
<link rel="import" href="sprite-el/sprite-el.html">
Element Declaration
Let’s define our custom element sprite-el
.
<link rel="import" href="../../bower_components/polymer/polymer.html">
<polymer-element name="sprite-el">
<template>
<link rel="stylesheet" href="sprite-el.css">
<div class="sprite" style="top: {{posY}}px; left: {{posX}}px; height: {{frame.height}}px; width: {{frame.width}}px; background: url({{spriteUrl}}) {{frame.x}}px {{frame.y}}px">
</div>
</template>
<script>
(function () {
'use strict';
Polymer({
publish: {
spriteUrl: 'images/sprites.png',
frame: {
x: 0,
y: 0
},
position: {
x: 0,
y: 0
},
computed: {
posX: 'position.x * 64',
posY: 'position.y * 64'
}
}
});
})();
</script>
</polymer-element>
First we include polymer.html
, and open a polymer-element
tag, with sprite-el
name attribute, which is required and must include a -
. Next, we have two sub tags, template
and script
. template
contains markup for our custom element. Within script
we call the Polymer
function to fire up the custom element. For more info see the documentation.
Element Template
In the template, we include the style sprite-el.css
that is compiled by Grunt from sprite-el.scss
.
Next, we have a div
with a sprite
class, and style
attribute. style
attribute defines top
, left
, height
, width
, and background
, styling to decide the position, and bounds of the sprite and it’s image. We include these styles inline because we have to use data-binding for these style attributes.
Data Binding, Published, and Computed Properties
Properties on the element, can be bound directly into the view, with Polymer expressions, like {{posY}}
, {{frame.height}}
, {{spriteUrl}}
.
posX
and posY
are defined under computed
property, which indicates these are computed properties. They are dynamic properties, that are computed based on other property values. In our case they depend on position.x
and position.y
so whenever position
property changes they are recalculated and updated in the view too.
spriteUrl
and frame
are published properties. That means you are making that property part of the element’s “public API”. So, the users of the element can change them. Published properties are also data-bound and are accessible via {{}}
.
Custom Element: box-el
The next custom element is a box element, this will be composed of our sprite-el
, and will represent the boxes, walls, and the ground. Let’s bother Yeoman once more.
$ yo polymer:el box-el
Game Art and Sprite Frames
All the game art is taken from 1001.com and are licensed CC-BY-SA 4.0. You can find all the sprites and full source code on GitHub.
We have five sprite frames – B
for boxes, BD
for dark boxes, T
for target, W
for walls, and G
for ground. It’s actually better to define moving boxes and background sprites within separate layers, but for simplicity we’re including all of them in one element. Each frame defines the frame position in the sprite-sheet as well as its height and width.
Let’s define our custom element box-el
:
<polymer-element name="box-el">
<template>
<link rel="stylesheet" href="box-el.css">
<sprite-el frame="{{frame}}" position="{{model.position}}" style="height: {{frame.height}}px; width: {{frame.width}}px;"></sprite-el>
</template>
<script>
(function () {
'use strict';
Polymer({
publish: {
model: {
position: {
x: 0,
y: 0
},
type: 'W'
}
},
computed: {
frame: 'boxCoords[model.type]'
},
ready: function() {
this.boxCoords = {
"B": { x:"-192", y:"0", width:"64", height:"64" },
"BD": { x:"-128", y:"-256", width:"64", height:"64" },
"T": { x:"-64", y:"-384", width:"32", height:"32" },
"W": { x:"0", y:"-320", width:"64", height:"64" },
"G": { x:"-64", y:"-256", width:"64", height:"64" }
};
}
});
})();
</script>
</polymer-element>
Inheritance and Composition
The box and the player elements will be using the base sprite element. There are two ways to do this, using inheritance or composition. We will not extend sprite-el
, but rather use composition. For more information about inheritance see this blog post and this reference.
We include sprite-el
in our template, and assign it’s attributes. Remember the published properties frame
and position
? Here we assign them via attributes.
Lifecycle Methods
One extra property box-el
has other than published and computed properties is ready
lifecycle method. ready
lifecycle method is called when the element is fully prepared, we can assign extra properties in this callback, in our case it’s boxCoords
which is used by frame
computed property.
Custom Element: sokoban-el
Our final custom element is the Sokoban game itself. This will be composed of our player-el
, and box, wall, and ground elements.
Game Model, Game Controller, and Input Manager
All the game logic is inside GameController
type. It generates the game map, and directly manipulates the game model. The game model is data bounded to our view, that is the polymer element. So all the changes to the model made by GameController
is automatically updated in the view. I won’t get into detail about the game logic in this article, you can check out the full source code for more details.
Handling user input can be done using declarative event mapping. But, yet there are some caveats. See this question on Stack Overflow. So I used a custom type to handle input, KeyboardInputManager
.
Let’s define our custom element soko-ban
:
<polymer-element name="soko-ban">
<template>
<link rel="stylesheet" href="soko-ban.css">
<template repeat="{{box in boxes}}">
<box-el model="{{box}}"></box-el>
</template>
<player-el model="{{player}}" id="character"></player-el>
</template>
<script>
(function () {
'use strict';
Polymer({
ready: function() {
var controller = new GameController();
var model = controller.getModel();
/** Sample Model **/
/**
this.player = {
position: {
x: 0,
y: 0
}
};
this.boxes = [
{
type: 'W',
position: {
x: 10,
y: 10
}
},
{
type: 'WD',
position: {
x: 10,
y: 100
}
}
];
*/
this.player = model.player;
this.boxes = model.boxes;
var inputManager = new KeyboardInputManager();
var char = this.$.character;
inputManager.on('move', function(val) {
switch (val) {
case KeyboardInputManager.Direction.UP:
controller.move(GameController.Direction.UP);
break;
case KeyboardInputManager.Direction.RIGHT:
controller.move(GameController.Direction.RIGHT);
break;
case KeyboardInputManager.Direction.DOWN:
controller.move(GameController.Direction.DOWN);
break;
case KeyboardInputManager.Direction.LEFT:
controller.move(GameController.Direction.LEFT);
break;
}
if (controller.isGameOver()) {
this.fire('finished', { target: model.target });
}
}.bind(this));
}
});
})();
</script>
</polymer-element>
Note the two properties on our Polymer element player
and boxes
, we set them to our model. You can manually set them to hard coded values, as you can see in the commented code, for testing purposes.
Iterative Templates
The boxes
property is an array of values. We can generate a single template instance for each item in the array. Note the usage of the template
tag and repeat
attribute to iterate over the array of boxes. See the documentation for more information.
Firing Custom Events
You can also fire custom events within your Polymer element using the fire
method. In our case, we fire a finished
event when the game is over. You can listen for events as shown below.
document.querySelector('soko-ban')
.addEventListener('finished', function(e) {
alert('Congratz you have pushed all ' +
e.detail.target + ' boxes!');
});
Publish It
We used generator-polymer
for building our application. There is also another generator, generator-element, and a Polymer boilerplate template for building and publishing custom elements. Once you have built your custom element with the generator, you can publish it using Bower. For more information on publishing, see these excellent articles, here and here.
Don’t forget to add the web-component
tag to your bower.json
. Once you have published it to Bower, your element should be available on the Bower registry. Also make sure to submit it to customelements.io.
Find Out More and Live Demo
In this tutorial we’ve seen Polymer in action by building Sokoban. Generally, you don’t have to build your own custom element, you can use existing ones, composing them to build more engaging elements. Visit the web components gallery at customelements.io.
You can do more with Polymer which we haven’t covered such as styling elements, observing properties, etc. For more information visit the API developer guide. You can find the full source code for this project on GitHub, and see a live demo on my site.