I’ve been hard at work creating a prototype for what I hope will be my next blog project – a networked multiplayer card game like Hearthstone. Creating enough menus to build and manage decks has resulted in the need to make a bunch of table views (scrolling lists of items whether cards, heroes, decks, etc). Although Unity has some pretty nice new tools for UI, there are still improvements I felt compelled to address.
Problems and Goals
The way I had been creating table views in the past is demonstrated in my blog post on the Sorted Shop. The basic idea was that I attached a “Vertical Layout Group” component to the “Content” of a “ScrollRect”. All of the children would then be automatically stacked and could also be used to drive the bounds of the “Content” by attaching a “Content Size Fitter” component.
Here are a couple of big problems with this approach:
- It doesn’t scale well. You must create ALL of the cells needed all at one time. My simple cell in the shop was made up of 11 game objects plus multiple components on each. If you had a list of several hundred items, you would end up instantiating several thousand game objects and even more components. This has a definite impact on memory and framerate.
- You can’t animate the insertion or removal of cells. The Layout Group controls the position of all its children. If you add or remove an item, it just “pops” into or out of the view and immediately shifts the other cells around to compensate. This might be okay for simple projects, but it prevents you from adding that bit of extra polish.
In order to solve this problem I decided to create my own reusable component called a “TableView” with the following features:
- Reusable cells – the table view will only create as many cells as it needs to fill the viewable area of the scroll rect. As a cell slides out of view, it is pooled and will be reused when a new cell slides into view. This way performance should remain the same whether you have only a little or a lot of content in your list.
- Vertical or horizontal lists. (Grid’s are not directly supported although you could accomplish it by adding multiple children to a cell).
- Uniform or non-uniform cell sizes. If all of the cells in the list are the same size, I want to be able to use math to determine what cells are visible. Non-uniform cell sizes requires more effort, but I also wanted to support it just in case.
- Flows in any direction. I wanted to support Top-to-Bottom, Bottom-to-Top, Left-to-Right, and Right-to-Left.
- Support for inserting or removing one or more cells at a time with animation.
Dependencies
A table view is pretty complex and relies on a variety of other libraries I have already written such as ones for pooling and animation. It might be worth a short introduction on each just in case you haven’t seen them before:
Animation
I first wrote the animation libraries in a few posts called “Dynamic Animation”. See Part 1 and Part 2 if you are curious to know more about its architecture. Additional components were added during the creation of the Tactics RPG project such as with the Anchored UI blog post. I also refactored and added bug fixes during the Magic blog post. In this post I made “Tweener” a subclass of “EasingControl” rather than keeping them as separate components.
Due to the refactoring and bug fixes of the Animation scripts, there was a new error in the Sorted Shop demo’s DialogController script which I had to fix.
Data Types
This folder has a single script called “Point”. I often have need of a simple struct which holds two “int” fields. Note that Unity’s Vector2 holds two “float” fields. For holding the “count” of a thing, an “int” is better and is not susceptible to rounding errors like a “float” can be.
I first created this script in the Tactics RPG Board Generator blog post. I made it implement the “IEquatable” interface in the Tactics RPG Ability Effects blog post.
Extensions
An “extension” is a way to allow you to add functionality to any class – even ones which you did not write yourself. For example, I wanted to be able to grab the last item of a list by something as simple as writing list.Last()
rather than list[list.Count - 1]
which is less “readable” to me. List’s actually have a “Last()” method in .NET Framework 3.5, but unfortunately the version included with Unity is not up to date.
The scripts in this folder are new, they don’t appear in any of my other posts or projects, although I have used extentions before such as with the Notification Center and to add some convenience methods during the Tactics RPG project. I wrote these new scripts while working on my next prototype game, and simply included the whole folder even though we wont actually need them all at this time.
Notification Center
The demo project already included these scripts, and no changes have been made. If you would like to know more about its architecture I would suggest reading my posts on “Social Scripting”:
Pooling
The demo project also already included the pooling scripts. If you would like to know more about the architecture, read the Object Pooling blog post.
I recently added some additional components to this library which I call Poolers. Actually I created them while making the TableView, but felt they deserved their own separate posts. You can read about them here:
UI
In this folder I added the “Panel” and “LayoutAnchor” scripts. They were originally created in the Tactics RPG Anchored UI blog post. The “LayoutAnchor” requires a parent “RectTransform” which had originally been found in the “Awake” method. I will be using these components on the “Cells” of our “TableView”, but the cells will be loaded dynamically as needed, and when they spawn they wont have a parent, so the “Awake” method wont be able to find what it needs. Therefore I modified the script to also update its reference in the “OnTransformParentChanged” method.
Getting Started
I created a new sample project based on the Sorted Shop demo. Grab a copy of the new Demo Project Here. Note that this project was created using Unity version 5.3.1f1.
Demo
The original sorted shop demo scene can be opened and interacted with for reference sake. That scene is named “SortShop”. After you understand what we are trying to fix, check out the new demo scene named “TableView”.
The “TableView” scene is already configured to show four different table views – one for each flow direction. The controller script will automatically “find” whichever TableView is on an active GameObject. If you decide you want to see the “Horizontal (Right To Left)” working then make sure to turn it on, and any other TableViews off such as the default “Vertical (Top To Bottom)” (Note that I mean the entire GameObject not the component).
Feel free to examine the setup differences between each of the four table view examples. The settings on the “ScrollRect” determine which direction the table view scrolls (Horizontal or Vertical), and the “RectTransform” settings on the “Content” for the “ScrollRect” determine the flow direction.
The height (or width) of the cells used in the table views are currently driven by the DemoTableViewController script. If you comment out the following (line #14):
tableView.sizeForCellInTableView = SizeForCellInTableView; // Optional
…then the size of each cell will be driven by the “Cell Size” field which you can configure on the “TableView” component in the inspector.
In the header area of the TableView are two buttons, one for “Remove” and one for “Add”. This allows you to see how cells can be dynamically removed from or inserted into the existing list of cells. The animation is driven by the “Panel” component’s positions and is configured differently for Vertical vs Horizontal implementations.
The table view cells in this demo are intentionally left “empty” as they are not the focus of the lesson. However, once you understand the functionality of the table view, you can also see another scene where I have updated the “SortShop” demo to use our new “TableView” implementation. This scene is cleverly named “SortShop2” and the controller script which drives the shop’s cell display is now called “ItemShop2”. After following along with the lessons it would be a good exercise to try and update the original demo on your own, but you can always refer to the one I completed in case you get stuck. If you have questions, feel free to ask.
DemoTableViewController
Let’s quickly review the script which drives the demo. You can open and review the “DemoTableViewController.cs” script from the project, but I will show and discuss several snippets here:
TableView tableView; List<int> sizes = new List<int>();
I only need two fields for this demo. One is a cached reference to a TableView which will be the first of the four children TableViews it finds to be Active. The “List” of “int” called “sizes” represents either the “widths” or “heights” of cells which should be displayed when using the non-uniform demo code.
void OnEnable () { tableView = GetComponentInChildren<TableView>(); tableView.cellCountForTableView = CellCountForTableView; tableView.sizeForCellInTableView = SizeForCellInTableView; // Optional } void OnDisable () { tableView.cellCountForTableView = null; tableView.sizeForCellInTableView = null; }
The controller uses the “OnEnable” method to acquire a reference to the first TableView it can find, and then registers for one or potentially two of the delegates. Note that “cellCountForTableView” is required, but “sizeForCellInTableView” is not. There are other optional delegates which will likely be used in a normal workflow such as “willShowCellAtIndex” but they are not related to the function of the tableview itself and are not needed now. You can see it implemented in the “SortShop2” demo if you are intereseted.
IEnumerator Start () { for (int i = 0; i < 50; ++i) { sizes.Add( UnityEngine.Random.Range(44, 144) ); } yield return new WaitForEndOfFrame(); tableView.Reload(); }
I have implemented the “Start” method as an IEnumerator. This gives Unity’s layout engine a chance to finish its layout before I actually tell the TableView to load itself. If I didn’t wait, then the TableView would think the height of the ScrollRects visible area was “0” which would have unfortunate consequences in its ability to figure out what cells need to be displayed. Note that before I loaded the table, I also initiated my list with random “sizes” which will help to demonstrate one of the features of my TableView.
int CellCountForTableView (TableView sender) { return sizes.Count; } int SizeForCellInTableView (TableView sender, int index) { return sizes[index]; }
Here are the methods which I registered to the TableView with. I know how many cells need to be displayed based on the count of items in my list. If the Non-Uniform method is being used then the sizes of cells are able to be obtained simply by indexing into this list.
public void OnAddButtonPressed () { HashSet<int> indices = new HashSet<int>(); int start = RandomIndex(); int count = UnityEngine.Random.Range(1, 4); for (int i = 0; i < count; ++i) { int index = start + i; indices.Add(index); sizes.Insert(index, UnityEngine.Random.Range(44, 144)); } tableView.InsertCells(indices); } public void OnRemoveButtonPressed () { if (sizes.Count == 0) return; HashSet<int> indices = new HashSet<int>(); int start = RandomIndex(); int count = UnityEngine.Random.Range(1, 4); for (int i = 0; i < count; ++i) { int index = start - i; if (index >= 0) { indices.Add(index); sizes.RemoveAt(index); } } tableView.RemoveCells(indices); }
These methods are tied to the UI buttons on the TableView’s header bar. Whether adding or removing cells, I attempt to find between 1 and 3 indices that would be currently visible and then either insert or remove them.
int RandomIndex () { if (tableView.visibleRange.y < tableView.visibleRange.x) return 0; int index = UnityEngine.Random.Range(tableView.visibleRange.x, tableView.visibleRange.y); return index; }
This simple method gives me a random index somewhere within the visible cell range of the TableView. I use it to help me find “interesting” cells or locations to add or remove from.
Summary
In Part 1 (of 6) we introduced what the problems are with current table view implementations in Unity and then I called out the list of features which are implemented in my table view. Because the table view is complex it has a lot of dependencies (all my own libraries) which I introduced. Finally I discussed what you can expect to see in the demo project and the code which makes it run.