ChatScrollRect
Virtualized scroll view for displaying chat messages. Uses a fixed pool of ChatMessageLine instances that are recycled (cycled) as the user scrolls, enabling efficient rendering of arbitrarily large message histories without creating a UI element per message.
Definition
Namespace: Paragon.Townskeep.ChatSystem.HUD
Assembly: Townskeep.dll
[RequireComponent(typeof(ScrollRect))]
public class ChatScrollRect : ParagonUIBehaviourInheritance: SerializedMonoBehaviour → ParagonBehaviour → ParagonUIBehaviour → ChatScrollRect
Remarks
ChatScrollRect implements a virtualized list pattern. Rather than instantiating a ChatMessageLine for every message in the channel history, it maintains a small pool of line objects sized to the viewport plus a small buffer. As the scroll position changes, lines that leave the viewport are recycled to the opposite end and populated with the appropriate message.
Virtualization Strategy
The pool is stored as a LinkedList<ChatMessageLine>. When scrolling:
Downward (towards older messages) — The first (top) line is moved to the end and populated with the next message beyond the visible range.
Upward (towards newer messages) — The last (bottom) line is moved to the beginning and populated with the previous message.
This cycling is driven by CycleLines(int count), which determines how many lines to cycle based on the delta between the previous and current scroll-position line indices.
Line Count Calculation
Each frame in Update(), the component recalculates:
lineCount — The number of lines needed: viewport capacity + up to 2 buffer lines (clamped by available messages)
bufferHeight — The total scrollable content height minus viewport height
maxScrollPosition — Maximum scroll position based on total message count
If lineCount changes (e.g., window resize), RefreshLines() adds or removes ChatMessageLine instances to match.
Scroll Inputs
The component listens to two scroll sources:
ScrollRect.onValueChanged — Drag-based scrolling within the viewport
Scrollbar.onValueChanged — Direct scrollbar manipulation
Both are normalized to a position value via ScrollToPosition().
Message Indexing
Messages are accessed from the ChatWindow using C# Index with the hat (^) operator, providing reverse indexing from the end of the message buffer. This means the newest message is at ^1, second newest at ^2, etc.
Quick Lookup
Initialize
scrollRect.Initialize(chatWindow)
Add a single message
scrollRect.PushMessage(message)
Add multiple messages
scrollRect.PushMessages(messages)
Clear all displayed messages
scrollRect.ClearMessages()
Scroll to a position
scrollRect.ScrollToPosition(position)
Scroll to a specific line
scrollRect.ScrollToLine(index)
Get current line index
scrollRect.GetCurrentLineIndex()
Read current line count
scrollRect.LineCount
Properties
LineCount
int
Current number of pooled line objects (viewport capacity + buffer)
ScrollPosition
float
Current absolute scroll position in pixels
ScrollValue
float
Normalized scroll value (0–1). Setting this scrolls to the corresponding position.
Fields
ChatMessagePrefab
ChatMessageLine
public
Prefab used when instantiating additional message lines
Methods
Initialize
Sets up the scroll rect with its parent ChatWindow, registers scroll listeners, and performs initial line refresh.
chatWindow
ChatWindow
The parent chat window that provides message data
PushMessage
Adds a single new message to the display. Cycles the last line to the front and populates it with the message.
message
ChatMessage
The chat message to display
PushMessages
Adds multiple messages to the display. Takes the first lineCount messages, reverses them, and pushes each one sequentially.
messages
IEnumerable<ChatMessage>
Collection of messages to display
ClearMessages
Clears all messages from all pooled line objects.
ScrollToPosition
Scrolls to an absolute pixel position. Clamps to [0, maxScrollPosition]. Triggers line cycling if the scroll crosses a line boundary.
position
float
Absolute scroll position in pixels
ScrollToLine
Scrolls to a specific line index with an optional pixel offset.
index
int
—
Target line index (clamped to [1, maxLineIndex])
offset
float
0.0f
Additional pixel offset from the line position
GetLinePosition
Returns the pixel position of a line at the given index.
index
int
Line index (clamped to [1, maxLineIndex])
Returns: Pixel position of the line.
GetLineIndex
Calculates the line index at a given pixel position.
position
float
Pixel position
Returns: The line index at that position.
GetCurrentLineIndex
Returns the line index corresponding to the current scroll position.
Returns: Current top-visible line index.
Common Pitfalls
Must call Initialize() before use
The chatWindow reference is set in Initialize(). Calling PushMessage() or ScrollToPosition() before initialization will cause a NullReferenceException.
PushMessages takes only lineCount messages
PushMessages() applies .Take(lineCount) to the input collection. If more messages are passed, only the first lineCount are displayed. The rest are accessible through scrolling (loaded on demand via CycleLines).
Line height is hardcoded via ChatMessageLine.HEIGHT
All layout calculations assume each line is exactly ChatMessageLine.HEIGHT (20px) plus vertical spacing. Multi-line messages or variable-height content will break the virtualization math.
RefreshLines uses DestroyImmediate
When reducing the line count, DestroyImmediate is used instead of Destroy. This is necessary because LayoutRebuilder.ForceRebuildLayoutImmediate is called immediately after, but it means this path cannot be called from OnDestroy() or during serialization.
ScrollRect and Scrollbar listeners are additive
Initialize() adds listeners via AddListener() but does not remove previous ones. Calling Initialize() multiple times will register duplicate handlers, causing double-scroll behavior.
See Also
ChatWindow — parent component that owns this scroll rect
ChatMessageLine — individual line renderer used in the pool
ChatHUD — root chat UI coordinator
Chat Overview — system architecture
Last updated