A high-performance, pooling-based scroll list system for Unity that efficiently renders 1000+ items in a ScrollView with minimal draw calls and smooth 60+ FPS scrolling, even on low-end devices.
- π Virtual Scrolling / Object Pooling β only the visible items (plus a small buffer) are instantiated at any time, regardless of the total list size
- π Drastically Reduced Draw Calls β keeps the batch count low by recycling a fixed pool of GameObjects instead of creating one per item
- π Responsive Layout β automatically adapts to screen/viewport size changes
βοΈ Horizontal & Vertical Support β works with both scroll directions- π Lifecycle Callbacks β
OnItemShow/OnItemHideactions andUnityEvents let you update item data cleanly - π§© ScriptableObject Architecture β decoupled data (
ScriptableVariable<T>) and event (EventSO) system for clean, inspector-driven communication
Assets/
βββ Prefabs/ # UI prefabs (inventory item, panels)
βββ Resources/
β βββ Data/ # ScriptableObject assets
β β βββ SelectedItem.asset # IntSO β currently selected item index
β β βββ SelectedData.asset # InventoryItemsDataSO β full item array
β β βββ CurrentSprite.asset # SpriteSO β current item sprite
β β βββ events/ # EventSO assets (data ready, item selected)
β βββ Sprites/
βββ Scenes/
β βββ Main.unity # Main demo scene
βββ Scripts/
βββ OptmizedList/
β βββ ListOptmizer.cs # Core virtual-scroll / pooling engine
β βββ OptmizedListItem.cs # Per-item show/hide UnityEvent hooks
βββ MonoBehaviours/
β βββ InventoryManager.cs # Loads JSON, generates item data
β βββ InventoryHandler.cs # Connects manager β ListOptmizer
β βββ InventoryItem.cs # Individual item UI logic
β βββ InventoryInfoPanel.cs # Info panel (name, description, stat)
β βββ InventoryItemImage.cs # Info panel sprite display
β βββ AspectRatioFitterImageAdjuster.cs
βββ scriptableSystem/
βββ ScriptableVariable.cs # Generic ScriptableObject data container
βββ IntSO.cs / SpriteSO.cs / InventoryItemsDataSO.cs
βββ EventSO.cs # ScriptableObject-based event bus
βββ EventSOListener.cs # MonoBehaviour subscriber for EventSO
- Unity 2020.3 LTS or later (the project uses UGui, TextMesh Pro)
- TextMesh Pro package (included via
Packages/manifest.json)
- Clone or download this repository.
- Open Unity Hub and click Add β select the repository root folder.
- Open the project with a compatible Unity version.
Navigate to Assets/Scenes/Main.unity and open it. Press Play to see the optimized scroll list in action with 1000+ inventory items.
- Create a ScrollRect GameObject in your Canvas (e.g., via GameObject β UI β Scroll View).
- Add the
ListOptmizercomponent to that same ScrollRect GameObject.- It requires a
ScrollRectcomponent on the same object (enforced by[RequireComponent]).
- It requires a
- Create (or use an existing) item prefab β this is the GameObject that will be cloned and recycled to display each row.
- In the
ListOptmizerInspector, assign your prefab to the Item Template field.- The template is hidden at runtime;
ListOptmizeractivates/deactivates it as needed.
- The template is hidden at runtime;
Tip: Make sure the item prefab has a fixed size in the scroll direction so
ListOptmizercan calculate how many items fit in the viewport.
// Example: populate with 1000 items
[SerializeField] private ListOptmizer listOptmizer;
void Start()
{
listOptmizer.PopulateList(
itemsNumber: 1000,
OnItemShow: (index, gameObject) =>
{
// Called every time an item becomes visible.
// Update the item's UI here using `index`.
var item = gameObject.GetComponent<MyItemComponent>();
item.SetData(myDataArray[index]);
},
OnItemHide: (index, gameObject) =>
{
// Optional: called when an item scrolls out of view.
}
);
}| Parameter | Type | Description |
|---|---|---|
itemsNumber |
int |
Total number of items in the list |
OnItemShow |
Action<int, GameObject> |
Callback fired when an item becomes visible; receives the item's data index and its GameObject |
OnItemHide |
Action<int, GameObject> |
(Optional) Callback fired when an item scrolls out of view |
If you prefer wiring callbacks in the Inspector instead of code, add the OptmizedListItem component to your item prefab and connect the OnShow / OnHide UnityEvents:
Item Prefab
βββ OptmizedListItem (component)
βββ OnShow (UnityEvent<int>) β MyItemComponent.SetData
βββ OnHide (UnityEvent<int>) β (optional cleanup)
OnShow receives the item's data index as its int argument.
At any point you can iterate over the currently visible pool:
listOptmizer.ForEachVisible((index, gameObject) =>
{
// e.g., refresh highlight state after a selection change
gameObject.GetComponent<MyItemComponent>().SetSelected(index == selectedIndex);
});ListOptmizer implements a virtual scrolling strategy:
βββββββββββββββββββββββββ ScrollRect Content βββββββββββββββββββββββββββββ
β [rearFiller] β empty RectTransform that simulates hidden items above β
β [Item Pool 0] β
β [Item Pool 1] β only ~(viewport / itemHeight) + 2 items exist β
β [Item Pool 2] β
β ... β
β [frontFiller] β empty RectTransform that simulates hidden items below β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- On
PopulateListβ calculates how many items fit in the viewport, instantiates exactly that many GameObjects (plus 2), and creates invisiblerearFiller/frontFillerRectTransforms. - On scroll β
OnScrollValueChangedcalculates the newstartIndex/endIndex, resizes the fillers to represent the hidden items, and recycles items that have scrolled off one edge by moving them to the opposite edge and invoking theOnItemShowcallback with the new index. - On viewport resize β
OnRectTransformDimensionsChangerecalculates all measures and rebuilds the pool to fit the new size.
The demo uses a lightweight, asset-based architecture to keep components decoupled:
Store shared state as Unity assets. Any MonoBehaviour can read/write them without direct references to other MonoBehaviours.
| Asset | Type | Purpose |
|---|---|---|
SelectedItem |
IntSO |
Index of the currently selected inventory item |
SelectedData |
InventoryItemsDataSO |
Array of all InventoryItemData structs |
CurrentSprite |
SpriteSO |
Sprite of the currently selected item |
EventSO is a ScriptableObject that acts as a named event channel. Any component can call Raise() on it; any EventSOListener subscribed to it will fire its configured callback.
// Raise programmatically
[SerializeField] private EventSO onItemSelected;
onItemSelected.Raise();
// Or press the "Raise" button in the Inspector during Play mode (EventSOEditor)EventSOListener subscribes/unsubscribes automatically via OnEnable / OnDisable, so it is safe to use on pooled objects.
| Component | GameObject | Role |
|---|---|---|
InventoryManager |
Manager | Parses item JSON, populates InventoryItemsDataSO, raises OnDataIsReady |
InventoryHandler |
Handler | Listens for OnDataIsReady, calls ListOptmizer.PopulateList |
InventoryItem |
Item Prefab | Updates icon/name UI; raises OnItemSelected when clicked |
InventoryInfoPanel |
InfoPanel | Listens for OnItemSelected, shows name / description / stat |
InventoryItemImage |
InfoPanel | Listens for OnItemSelected, shows item sprite |
InventoryManager.Start()
ββ Generates InventoryItemData[] from JSON
ββ Raises OnDataIsReady
ββ InventoryHandler.OnDataIsReady()
ββ ListOptmizer.PopulateList(count, OnItemShow β InventoryItem.SetData)
ββ Builds visible pool, fires OnItemShow for each visible item
ββ InventoryInfoPanel.OnDataReady() β displays item[0] on startup
InventoryItem.OnClick()
ββ selectedItemIndex.Value = index (writes IntSO)
ββ Raises OnItemSelected
ββ InventoryInfoPanel.OnItemSelected() β updates text
ββ InventoryItemImage.OnItemSelected() β updates sprite
ββ InventoryItem.OnItemClicked() β refreshes highlight (all visible items)
| Scenario | Without optimization | With ListOptmizer |
|---|---|---|
| 1000 items | ~1000 UI GameObjects | ~12β16 UI GameObjects |
| Draw call batches | Very high (scales with item count) | Low (scales with viewport size only) |
| Memory | O(n) | O(viewport) |
| FPS (low-end device) | <30 FPS while scrolling | 60+ FPS |
This project is provided as an open-source reference implementation. Feel free to use and adapt it in your own Unity projects.