Summoners Fate > Game Updates & News

Dev Log

(1/5) > >>

RossD20Studios:
Dev Log: Creating the Card Management System

Greetings all! I was finding myself going down a bit of a feature creep/perfectionist lull with the production tools, so I decided to shift gears and work on our card management system. The card management system is the UI that allows the player to view all of the possible cards in the collection as well as create custom decks. If you’ve played CCG’s like Hearthstone, you’re probably familiar with what this system looks like, but just in case, here’s a quick breakdown of the elements:


* Collection Management (Ability to view all of the cards)
* Crafting (Ability to make new cards)
* Deck Builder

* View cards to place in deck
* View cards in the deck (visualization)
* Deck Stats
* Ability to name deck
* Number of each type of card in deck (Units, Spells, Equipment)
* Average mana cost
* Total mana cost
* Card Quantity (X/20)
* Sort By
* Filter
* Search
* Back Out Button
* Suggest Cards
* “New” Indicator that appears over cards recently added to player’s collection
* Deck Limit (Per card)
* Party/Team Roster (A special case for Summoners Fate to also choose which characters are in your party).
Atm, I’m focused specifically on Deck Builder, as it is the most complex part of the system. The first challenge is layout, which is even more challenging for me because I support both portrait and landscape. The basic idea is to divide the screen into two parts (collection/cards available) and the deck visualization. The most intuitive approach is to drag cards from the collection and drop them into the deck.

My first step was to get a basic idea for the main layout of these elements. Portrait is the most limiting layout, so I started there. After some testing, I discovered I could fit about 3 cards across the narrowest dimension of the screen with the text still being readable. Additionally, I would support a “full size preview” for the user on tap to view the keywords and larger text. This would allow user to browse their collection 6 cards at a time. Additionally, I can fit the 20 cards needed to construct the deck as mini cards (showing only the icon and cost). As shown in images below, these numbers also work for landscape if I expand the number of columns.


Portrait layout for deck management. Cards in your collection are at the bottom, cards in your deck at the top.


Landscape layout for deck management. Cards in your collection are at the bottom, cards in your deck at the top.

Now that I knew the layout, the next step was determining how to effectively render and control display of all the cards. I decided to use Feathers, an open source library designed for AS3 and Starling, which has the exact components I need and (because it is open source) unlimited potential for me to customize for my game’s specific needs.

I use the “List” control component because it offers cell recycling. This is an absolute must for rendering 100+ items (particularly visual ones) because it vastly reduces the demands on the system to maintain/mask the hundreds of items the user will be scrolling. To reduce overhead as much as possible, I wrote a custom ListCellRenderer that takes the card data and renders it directly into a card visualization.

With this, I can get 60FPS scrolling performance. The only issue I’m still running into is that there is a slight “hiccup” the first time a user paginates to a new page of unit cards. This is because there is an overhead in the initial creation of the armatures to assemble the units. In several competitor games I’ve tested, this seems to be an acceptable “norm” though I would love a solution if any developers out there have an idea how to mitigate this.

Now for the hardest part: managing a texture atlas for 400+ cards. I also have to consider that with each game update, that number will grow as cards are added. In my previous dev logs, I’ve discussed how I can achieve optimal performance by scaling all assets to fit a single texture atlas (yielding only 1 draw call). However, this only works because each game instance contains a finite number of resources that I can manage without affecting quality. As the number of cards increases, it becomes impossible to fit all assets on a single atlas, so it’s necessary to paginate the atlas into multiple sheets.


This image shows the initial result of rasterizing the card images. At 400+ draw calls, application responsiveness is very sluggish.

I’m using Gil Amran’s open source DMT library to create my texture atlases dynamically from vector graphics. The library uses a rectangle packer algorithm to sort assets and place them on texture atlases, creating new atlases as needed. The algorithm is optimized to maximize use of area so that textures are packed as tightly as possible to reduce the total number of atlas sheets created. Generally, this works fine because most game levels can render on a single sheet. With hundreds of assets to manage however, this can result in an exponential increase in the number of draw calls when assets needed to render a layered display object (such as a character armature) are split across multiple texture atlases. For those familiar with the painter algorithm (see Daniel Sperl’s amazing depiction here) the best way to reduce draw calls and optimize performance is to plan your texture atlases according to how your game will display the assets.

This is why I use open source technology whenever possible: when modifications are needed to suit particular needs, I can examine the code and make the modifications as needed to adapt and evolve the software. And in turn, share these benefits with other developers once I’ve found and perfected a solution! In this case, the feature I needed was the ability to control how my texture atlases were being created based on how I would render the display objects.

To accomplish this, I created two properties “addedId” and “assetGroupId”. AddedId is a number indicating when something gets added to the raster list. AssetGroupId is a string identifier indicating that the asset to be rastered must have all assets within the same group added to the same texture atlas. Prior to rasterizing, I sort all of the assets by the addedId. Then, as items are packed into the texture atlas, I check to see if the next asset fits. If it doesn’t fit, I check to see if any of his “assetGroupId” buddies were added, and, if so, I pull all of those assets out of that atlas as well, then start a new atlas. Following this procedure, I was able to reduce draw calls down from 400+ to under 40 which, in turn, vastly improved the FPS and responsiveness of the application.


This image shows the draw call result of grouping the textures on each atlas according to how the game will render them. This is a 10X reduction from the initial test and results in substantially better application response.

Every choice has a trade off. In this case, my decision to break and start a new texture atlas rather than taking the next asset that fits can somethings result in an unnecessary number of texture atlases being generated. Where the original algorithm might have fit all of my assets into one atlas, the new algorithm might break it into two. At the same time, I find in practice that the tradeoff is worth it because I can guarantee more predictable performance by controlling which assets appear on each atlas.

PrimaryFeather:
Thanks a lot for sharing the process with us, Ross! I really like the approach of using "addedId" and "assetGroupId" to find out how best to organize the atlas. Very smart!   8)

RossD20Studios:
Dev Log 2: Solving the Tech Challenges of the Deck Builder

In my last update, I shared my solution for optimizing draw calls and performance of the deck builder by grouping assets that render together on the same page of the texture atlas. Well, what I hadn’t considered is just how much GPU memory 400+ cards consumes. It turns out that when rendering the cards at full screen resolution, I exhaust my 512MB of GPU ram and the game crashes. Asset grouping has its advantages when you know you have a finite amount of content, but with something like cards in a CCG, where the collection continues to grow, my previous solution was not going to work.

Back at the drawing board, I considered how an application like Google Photos works. To get the fast response time scrolling through the photos, it needs to render the images on the GPU (just like in the game). But how do you account for hundreds of photos without exhausting the GPU? I needed to conserve the available system resources (GPU memory) by unloading cards not being viewed and loading in the cards the player is scrolling to. Since I can’t anticipate the order in which player will view cards (depending on their unique collection and sorting filters they’ve applied) it’s no longer practical to group cards together on a shared atlas. Instead, I create a unique texture atlas for each individual card. This makes the system entirely module, allowing the loading/unloading of individual cards. It turns out loading and unloading from an image stored on the harddrive is very fast. But, my “all vector” based pipeline had a problem: I don’t have images stored on the harddisk, I’m creating them “on-the-fly” in memory. In order for this new approach to be practical, I needed to “raster” file versions of each card onto the harddrive into an asset cache (also similar to how Google Photos works when it stores thumbnails of your photo collection on your harddrive so you can quickly browse through them).

This brings us to another awesome feature of Gil Amran’s DMT solution: it supports asset caching out of the box. You simply set the “cache” flag to true and your vectors get converted to PNGs appropriately sized for crisp visual quality on any device. However, you still need to wait for the initial “raster” process (drawing all of the vectors into bitmaps) which, with 400+ cards, can be quite a delay. One thing I’m very keen on is providing the user with a fluid experience. Choppiness/delays in apps can be a big turn-off and send potential players looking elsewhere for entertainment. Rastering all the assets up front might take 10-20 seconds, dropping FPS in the application down to 0 during this process. This process would need to run:

* Whenever the screen dimensions (screen area) changes. Not an issue on mobile, but needs to be considered for PC when changing the game window size.
* Whenever the assets get updatedWhen this process happens, it’s important to communicate to the player what’s happening and show a progress bar. If the FPS drops to 0, they might believe the app has frozen. To solve this issue, I started looking at AS3 Workers, which is Adobe AIR’s solution to multi-threading or parallel processing. Normally, all CPU instructions operate in serial (one instruction at a time). FPS drops to zero because the CPU is bottlenecked with rasterizing the images. But, with multi-threading, I can tap into the second CPU core and have 2 sets of instructions running simultaneously without affecting the performance of the other.

To get the cards rasterizing seamlessly, I create a separate application, the BackWorker. When the main app boots up, it loads in the BackWorker app and sends it a job: “Hey, BackWorker, would please rasterize these vectors into PNGs while I entertain the player with a lovely progress bar?” BackWorker kindly obliges, allowing the user to maintain a 60FPS viewing experiencing of the progress. Even better, the BackWorker can do this job completely in the background. This means, I don’t actually have to have the player watch a loading bar if the assets necessary to perform the current task (ex: play the game) are available. Imagine this: The first time you boot up the game, you jump right into your first battle. While you’re playing, the BackWorker is building the asset cache of cards on your hard drive so when you go to open the deck manager the first time, all of the cards are available for you to browse. No waiting necessary!

All seemed well and good with this approach until I hit one final snag: While the spell cards use a single flat image to portray the card icon, characters in Summoners Fate consist of multiple body pieces that must be assembled to create the character. Some of these pieces (like wings and tails) require mesh transformations on the GPU in order to assemble correctly. That means, there’s no way to “pre-raster” a single flat image of the character. They are designed to be constructed as animated armatures. Well, the problem with that is that there is a substantial delay (several milliseconds) involved in creating an armature. While they may not seem like a lot, when a user is rapidly scrolling through a list of characters, those milliseconds add up into noticeable lag spikes, creating an undesirable, choppy experience.

I needed a way to capture the pose data of the character without having to go through the overhead of creating the armature each time the player scrolled. To accomplish this, I turned to my definitions editor to store the display data as a serialized string. Effectively, I wait for the animation to stop on a desirable pose, then “take a photo” that’s stored as text indicating the tree of necessary display objects and transformations necessary to render the character in that pose.


Smile for the camera, Dragon!
The hardest part here was determining how to capture the “mesh transformations” on things like the wings/tails/etc. Thanks to some helpful examples provided by Daniel Sperl, I was able to learn how to encode this into a series of number lists representing the indices and vertices structure.

Finally - one last stickler point with this approach: filesize. It was clear that storing all that mesh data would substantially bloat my definitions filesize. No one wants to download a 10MB definitions when a 1MB file will do. To solve this, I recycle the poses for units with common armature structures (ex: humanoid, dragon, quadruped). I leave the actual asset data abstract so that it can plugin the particular skins unique to each character without having to store display data for every single character.

At last, I have a working solution for browsing all the cards within your ever-expanding collection! See video below for the results.


There’s still more optimizations that can be made (additional flattening would help reduce the draw calls further and improve performance). But for now, I’m happy to have the functionality ready for the pre-release. We’ll get to these improvements later in the polish phase before WW release.

PrimaryFeather:
Putting so much effort into details like these (providing a really smooth experience when browsing the cards) is what makes Summoners Fate so special. Thanks for sharing the process with us, Ross!  ;D

RossD20Studios:

--- Quote from: PrimaryFeather on February 08, 2018, 12:10:24 AM ---Putting so much effort into details like these (providing a really smooth experience when browsing the cards) is what makes Summoners Fate so special. Thanks for sharing the process with us, Ross!  ;D

--- End quote ---

Thank you so much! It means a lot to hear that. I agree that what makes indie games special is the care, love and attention to every aspect - ensuring a quality experience and a quality product through and through. The work isn't just a job, it's a part of you, and reflective of how you want the world to see you.

Navigation

[0] Message Index

[#] Next page

Go to full version