Sebi Posted May 12, 2016 Share Posted May 12, 2016 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 ivan.popelyshev 1 Quote Link to comment Share on other sites More sharing options...
mattstyles Posted May 12, 2016 Share Posted May 12, 2016 There's a few similar projects, this one I've followed along with, it ties Pixi in with React not Angular but the concepts are very similar to how you are building your objects, maybe good for some inspiration or structure. I mix DOM with Pixi is almost all my games, as you say many games have a very large UI section and the DOM/CSS often makes a better fit than doing this in canvas. The problem with tying these large frameworks into renderers like Pixi is always performance. Frameworks like Ember, Angular, Backbone are not made for performance, most applications they are designed to create don't aren't computationally taxing, or, at least, there are only a few short sections that are (loading/transforming data etc), they are build for developer ergonomics i.e. they are built to allow quickly building an app, not building a particularly performant app. The problem this presents is that your average game application is fast paced and computational expensive so the two are not natural bedmates in most situations. Angular, of all the large frameworks, is by far the least performant so the problem is exacerbated. I guess the point is that trying to punt all of what Pixi offers through a framework (even a small one like React, which is more of a library than a full blown framework) is that you negate many of the things that make Pixi great in the first place i.e. its tight scene graph and lightning performance. I'm not sure exactly what your goals are for the project but I'd warn against too tightly coupling your game rendering with the UI rendering. Keep them separate as much as possible, this way Pixi can do what it is good at and Angular can do its own thing. With regards to responsivity, everyone gets hung up about it but (apart from developers) how many times do you actually resize windows? Having been privy to a few usability studies I know its fairly rare. Your issue is almost always in the initial rendering at whatever screen size you're at, usually this is a cross-platform concern, and, yep, I agree, getting that right can be tricky. Good luck, we're just starting to see proper efforts at merging several technologies/projects together (i.e. React/Redux/Immutable, or something like Phaser that takes Pixi/Physics etc and merges them). Quote Link to comment Share on other sites More sharing options...
Sebi Posted May 12, 2016 Author Share Posted May 12, 2016 Thanks for the input, mattstyles. I generally tend to avoid DOM in my pixi projects. Mainly because on my other not-so-good-laptop it seems to get less fps if DOM elements overlay the canvas and for styling reasons. It's not only about screen resizing, but also about the idea of coding something once and having it work on all screen sizes out of the box. I haven't seen that PIXI/React framework yet, so I am not entirely sure how it works. About the framework I am working on, the 3 core concepts are: 1. Being able to easily position / display / size elements based on the screen size. 2. Being able to de-couple logic, views and data. 3. And to abstract away event listeners. I'm not using any angular code, I just mimic its behaviour, so hopefully there won't be any performance issues. All elements that are listened on, either by $watch or inside an expression, are pushed to a linear graph and the time it takes to loop through a few items for dirty checking, should be negligible low. At around 10,000 listeners, the dirty checking takes about 1ms for me. I can't imagine a game that actually needs that many properties, so I would assume that in most cases this will take around 0.1ms to 0.5ms. Most devs probably need close to that time with their own event listeners based on how heavy their UI is. There is still a lot room for improvement, I just quickly coded this up in the past week. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.