Note: This is still work in progress
Hi folks,
I haven't done much with pixi lately, most of my work resolves around MEAN stack and I usually don't have enough time for anything not work related.
A few weeks ago, when I had more time, I started working on my new game. My game looked ace on my laptop, but literally looked terrible for anyone who didn't use anything close to my screen resolution. On smaller devices, my content barely fit, if at all. On bigger devices, I had a lot empty spaces or the fonts were too small. I tried fixing most of the issues, but this bloated my files with a lot UI related code. But that wasn't the only issue. Most of my UI is way too repetitive to code.
Character inventories, which display an array of items, friendslists, which display an array of ingame friends, ingame stores, that display an array of cash shop items, skillbars, that, well.. display an array of skills. Eventually I ended up telling myself to screw this.
So I was thinking on. Why is it, that I can easily create a website that fits any screen resolution, manipulate content in a few lines of code, but when it comes to pixi (or canvas for that matter), it often takes me at least a day to add a new component to my game?
And the answer is obviously that there are just better tools for that task in the DOM world.
What I am currently doing - and I have literally no idea, if this is worth it at all or not - is to mimic the behaviour of angular and bootstrap for pixi.
I have a simple demo here: http://mokgames.com/redhawk-demo/
Small Screen | Medium Screen | Large Screen
Here is a short overview of how RedHawk is supposed to work:
Scenes instead of Controllers
Unlike Angular, RedHawk is using scenes. Scenes consist of the following methods: init (called before preload), preload (loads the assets), create (called after preload), update (called pre render), render (called after the scene is rendered to canvas) and shutdown (called when the scene changes). Took inspiration from phaser when I decided for those methods.
Every scene comes with a $scope, which holds all scene related methods and is also used to dynamically update objects. You can inject different modules/components to your scene by simply adding those as arguments to the scene methods.
Some of those predefined modules are:
$timeout, $interval, $raf; Start / stop timeouts, intervals and animation frames. Unlike the window methods, those timers are cleared when the scene changes.
$http: Handles ajax requests (also bound to the scene. requests are aborted if not done before changing the scene)
$q: Also bound to the current scene, something that acts similiar to Q / Promises.
$scene: used to change scene.
$layout: Holds the device class, screen width / height and several other methods.
Example:
RedHawk
.addScene("Shop", {
init: function ($scope) {
$scope.something = 123;
},
//...
})
.boot("Shop");
Services
Services can be injected to any scene and are generally used to separate data from logic.
Example: Demo: http://mokgames.com/redhawk-demo/item.html
RedHawk
.addService(function ItemService($http) { // request $http module
function getInfo(id) {
return $http.json("./items/" + id + ".json");
}
return { // expose public methods
getInfo: getInfo
}
})
.addScene("Shop", {
init: function ($scope, ItemService) { // request item service
ItemService.getInfo(1).then(function (response) {
$scope.item = response.data;
});
},
create: function ($scope) {
var item = $scope.$hawk("Text", {text: "Name: {{item.name}}\nPrice: {{item.price}}", show: "item", style: {fill: "#fff"}});
$scope.$add(item);
}
});
Factories
Factories are pretty similiar to services, but with one difference. Factories return an instance everytime they are called.
Example:
RedHawk
.addFactory("Monster", function () {
this.health = 100;
});
RedHawk config:
RedHawk
.config({
ticker: {fps: true}, // rendering loop, fps: true populates $scope.fps with the current fps
renderer: "auto", // can be a PIXI renderer or auto | webgl | canvas
options: { ... }, // renderer options
target: document.body, // element to append the renderer view to
boot: "StartScene" // tells redhawk which scene to boot
});
Booting RedHawk:
RedHawk.boot("Scene"); // unless already specified in config
Watching a scope property: Demo: http://mokgames.com/redhawk-demo/watchMe.html
RedHawk
.addScene(function SomeScene($scope) {
$scope.watchMe = 0;
$scope.$watch("watchMe", function (newValue, oldValue) {
console.log("watchMe changed: ", newValue, oldValue);
});
this.render = function (elapsed, time) {
$scope.watchMe = time;
};
});
Bootstrap
But how do we copy bootstrap? If you ever used bootstrap, then you know quite a few of the most convenient bootstrap classes.
1. Device class
The $layout component, which is responsible for screen resizes / detection, knows about the following classes:
xs - screens that are less than 768px wide (usually phones)
sm - screens between 768px and 991px (tablets / phones)
md - screen between 992px and 1199px (tablets / desktop)
lg - screen that are 1200px or wider (desktop / smart tv / etc)
Order is xs -> sm -> md -> lg
2. Columns
The bootstrap grid consists of 12 columns. Each column therefor takes up 1/12 of the parents width. One of the major advantages is that you can define different column sizes for different device classes. For instance:
col-md-6 would be 50% of the width in bootstrap. We use something similiar, which I will explain at hawk objects
3. Rows
Unlike Bootstrap, we not only use columns, but also rows. So our grid is actually 12x12.
4. Visibility classes
hidden-xs, hidden-sm, hidden-md, hidden-lg: only hides the object on the specified device class.
visible-xs, visible-sm, visible-md, visible-lg: only shows the object on the specified device class.
visible-*-up: only shows the object on * and up e.g. visible-sm-up: visible on sm, md, lg
visible-*-down: only shows the object on * and down e.g. visible-sm-down: visible on xs, sm
same for hidden-*-up and hidden-*-down
Hawk Objects
So, now with all this fancy stuff, how would we use it? Simple, we need to call $scope.$hawk (or the shortcut $hawk for that matter) on any PIXI object.
Example: Demo: http://mokgames.com/redhawk-demo/profile.html
RedHawk.
.addScene("Profile", {
init: function ($scope) {
$scope.player = { name: "Desu", level: 9001 };
},
create: function ($scope, $hawk) {
var name = new PIXI.Text("", {fill: "#fff", font: "10pt Arial"});
$scope.$hawk(name, {text: "{{player.name}}"}); // magic: every expression needs to be written as {{exp}}
$scope.$add(name);
// another way to do this : note $hawk and $scope.$hawk act exactly the same
var level = $hawk("Text", {
text: "Level: {{player.level}}",
style: {fill: "#fff", font: "10pt Arial"},
y: 10
});
$scope.$add(level);
}
});
Whenever $scope.player.name changes it's value, name gets updated automatically. Same for level when $scope.player.level changes.
RedHawk property list:
var sprite = $hawk(new PIXI.Sprite(), {
classes: "visible-sm-up xs-3x3 right", // {String} shortcut for visibility, grid and position classes
visibility: "hidden-lg hidden-xs", // {String} List of all visbility classes
grid: "md-6 xs-12 lg-4x4", // {String} List of grid classes *-(cols)x(rows) or *-(cols),
show: "player.level > 1", // {String} expression that determines whether or not this element should be displayed
repeat: "friend in friends", // {String} Iterates over an array. possible values: "value in scope.var", "(key, value) in scope.var", "value in [1,2,3]"
bounds: { width: "25%", height: 100 }, // {Object} widths/height can either be % or a number, defines their width/height without scaling
width: "25%", // object width, can either be a number or a percentage
height: "50%", // object height, can either be a number or a percentage
x: "10%", // object x, can either be a number or a percentage
y: 100, // object y, can either be a number or a percentage
position: "top right" // {String} positions the element at (left|center|right) (top|middle|bottom)
});
Any other property of the object works as well. E.g.: texture (for sprites), text (for texts), or anything else.
So, to get back to the example that I posted earlier. http://mokgames.com/redhawk-demo/
If you look at the source code, here is what happens:
var desu = {
/* StartScene: create */
create: function($scope, $hawk) {
/* PIXI.Text
*
* text and font update based on the properties of
* $scope.$device - provided by $layout
* $scope.$width - provided by $layout
* $scope.$height - provided by $layout
* $scope.fps - provided by $ticker
*
* Every time any of those change, the text gets automatically updated
* if the fps are lower than 10, a leading 0 is displayed: {{fps < 10 ? '0' + fps : fps}}
* the font size changes according to the $device class: {{font[$device]}}px arial (fonts is defined in init
*/
var debug = $hawk("Text", {
text: "FPS: {{fps < 10 ? '0' + fps : fps}}\nDevice: {{$device}}\nScreen: {{$width}} x {{$height}}\nTap to add an fps indicator.\nTap 4 times to change the scene.",
style: {fill: "#fff"},
font: "{{font[$device]}}px arial"
});
// add the text to the stage
$scope.$add(debug);
/* PIXI.Container
*
* Outer container for our fps indicator sprites
* Container is fixed to bottom center
* Container is 50% of the parents width and 25% of the parents height
* since the stage is the parent, the width equals 50% of $width ($layout)
* and 25% of $height ($layout)
*/
var itemContainer = $hawk("Container", {
bounds: { width: "50%", height: "25%" },
position: "bottom center"
});
/* PIXI.Sprite
*
* Creates automatically a Sprite (repeat) for each element in $scope.items (repeat: "item in items")
* Sets the texture of that Sprite to "./img/fps-{{Math.min(3, fps / 15 | 0)}}.png" (gets updated when $scope.fps changes)
* basically 0-14 fps red, 15-29 orange, 30-44 yellow, otherwise green
* Each sprite gets stretched to grid: "xs-4x12"
* Grid: 4 cols and 12 rows, so width: 33.33% and height 100% of the parent
* The parent is the Container, which means we get 33.33% of 50% of $layout.width and 100% of 25% of $layout.height
* Each Sprite is positioned at $index * this.width ($index is the index in the repeat object) and this.width is the Sprite's width
*/
var items = $hawk("Sprite", {
image: "./img/fps-{{Math.min(3, fps / 15 | 0)}}.png",
grid: "xs-4x12",
repeat: "item in items",
x: "{{$index * this.width}}"
});
itemContainer.addChild(items);
// outer container is added to the stage
$scope.$add(itemContainer);
}
}
Well, that's enough for now.. Didn't plan to write so much, and to display so many examples, but instead I just wanted to ask for opinions.
Maybe some of you also use angular / bootstrap or you have suggestions for me.
I intentionally keep the redhawk.js minified, since it is still work in progress. I just thought showing a minimalistic demo would make my intention clearer.
And generally, would anyone use a framework like this? It is intended to mainly handle UI and to allow you to quickly push out a game state/scene. I'm pretty sure that this would work well with Phaser or any other game engine, so while RedHawk solves all your UI nightmares, an actual game engine could hook in to render the game.
My goal is to be able to render a huge amount of hawk objects in preferable less than 1ms, so that the remaining time in the rendering loop can be used to render the game.
When playing some of the most successful games in app stores, I realized that those games are at least 50% fancy UI, the games usually are minimalistic. Which lead me to my decision to focus on UI first, before finishing my game.
Anyways, would like to hear some thoughts on this.
Cheers