posted by REDFORCE 2020. 1. 29. 13:55

Serializable Dictionary

 

Unity에서 TextMeshPro로 풀링 되어있는 HUD System을 만들다가 구조를 Dictionary로 잡게 되었는데 한가지 문제가 있었다. Unity 상에서 Dictionary 같은 경우엔 시리얼라이즈를 해도 에디터 상에 노출이 안되는 문제였다.

그러나 필요에 의해 Inspector에 노출이 되었으면 하는 KeyValuePair 형태의 Dictionary 구조가 필요할 때가 지금처럼 간혹 발생할 수 있는데 결국 구글링하며 찾아보다가 만들게 되었다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AT.SerializableDictionary
{
[System.Serializable]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
[SerializeField]
private List<TKey> keys = new List<TKey>();
[SerializeField]
private List<TValue> values = new List<TValue>();
// save the dictionary to lists
public void OnBeforeSerialize()
{
keys.Clear();
values.Clear();
foreach (KeyValuePair<TKey, TValue> pair in this)
{
keys.Add(pair.Key);
values.Add(pair.Value);
}
}
// load dictionary from lists
public void OnAfterDeserialize()
{
this.Clear();
if (keys.Count != values.Count)
throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable."));
for (int i = 0; i < keys.Count; i++)
this.Add(keys[i], values[i]);
}
}
}

 

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
namespace AT.HUDSystem
{
public enum HUD_OBJECT_TYPE
{
Damage_Normal,
Damage_Critical,
}
[System.Serializable]
public class DictionaryOfHUDTypeAndTextMesh : AT.SerializableDictionary.SerializableDictionary<HUD_OBJECT_TYPE, TextMeshPro> { }
[System.Serializable]
public class DictionaryOfPoolBase : AT.SerializableDictionary.SerializableDictionary<HUD_OBJECT_TYPE, List<TextMeshPro>> { }
public class HUDSystem : MonoBehaviour
{
/// <summary> HUD System Prefab List </summary>
public DictionaryOfHUDTypeAndTextMesh prefabList = new DictionaryOfHUDTypeAndTextMesh();
/// <summary> HUD System Pool Container </summary>
public DictionaryOfPoolBase pools = new DictionaryOfPoolBase();
public void Initialize()
{
pools.Clear();
foreach (var obj in prefabList)
{
List<TextMeshPro> poolList = new List<TextMeshPro>();
pools.Add(obj.Key, poolList);
}
}
public TextMeshPro CreateItem(HUD_OBJECT_TYPE type)
{
if (prefabList.ContainsKey(type))
{
TextMeshPro newTextMesh = Instantiate(prefabList[type]);
newTextMesh.transform.position = Vector3.zero;
return newTextMesh;
}
Debug.LogErrorFormat("Can not Find Target HUD Object. Type : {0}", type.ToString());
return null;
}
public TextMeshPro GetItem(HUD_OBJECT_TYPE type)
{
for (int i = 0; i < pools[type].Count; i++)
{
if (!pools[type][i].gameObject.activeSelf)
return pools[type][i];
}
return CreateItem(type);
}
}
}
view raw HUDSystem.cs hosted with ❤ by GitHub

'Unity Engine > Unity3D Engine' 카테고리의 다른 글

#04.C# Job System_02  (0) 2018.12.23
#04.C# Job System_01  (0) 2018.12.22
#03.JobSequenceManager  (0) 2018.12.21
#02.JobRepeatManager - 03  (0) 2018.12.20
#02.JobRepeatManager - 02  (0) 2018.12.20
posted by REDFORCE 2019. 6. 20. 15:59

최근 회사에서 채팅UI의 구조 개선에 대한 이슈가 발생하여 만들어두었던 채팅창을 모두 갈아엎게 되었다.

기존의 채팅 UI구조는 채팅메세지가 들어올 때마다 Instantiate ( ) 가 수행되어 말풍선 UI가 계속해서 쌓이는 구조였다.

 

물론 처음 개발 당시엔 채팅메세지가 엄청나게 쏟아지는 경우를 고려하여 말풍선이 쌓이는 갯수를 최대 200개 까지로 제한하여 그 이상 메세지가 쌓일 시엔 가장 오래 된 메세지를 삭제하고 새로 생성하는 형태로 구현했었다.

(Mobile : Android/iOS) Astrokings(아스트로킹즈)

워낙 게임이 채팅이 많이 오가는 게임이 아니라서 (개인적으론 그렇게 생각한다..) 딱히 위 설계대로 구현한 당시엔 큰 문제나 이슈는 없었다.


그러나 오랫동안 접속상태를 유지한 채, 채팅창을 한번도 키지않고 있다가 새로운 메세지가 많이 쌓인 상태에서 채팅창을 킬 시 200개의 메세지를 모두 갱신하느라 하드웨어에 따라 다르지만 약 3~10초 이상의 딜레이가 걸리는 문제로 결국 인해 최적화 이슈가 발생했다.

 

이 문제를 어떻게 해결할까 하다가 결국 Infinite ScrollView의 필요성을 느끼게 되었고, 여러가지 InfiniteScrollView에 대한 Reference를 찾아보고 적용해보았지만 가장 큰 문제가 있었다.

 

구글, 스택오버플로우 등 다양한 곳에서 찾아본 Infinite ScrollView는 100% Grid 형태의 모든 스크롤 항목이 동일한 크기임을 전제로 하여 만들어져 있었다. 그러나 필자가 필요로 하는 Infinite ScrollView의 경우 채팅창의 말풍선들이 내용에 따라 가변적으로 크기가 변하는 조건이 있어서 사람들이 흔히 쓰는 방식으로는 구현할 수가 없었다.

 

결국...NGUI의 UITable 의 Reposition 기능과 더불어 ScrollView의 기능을 자동으로 수행해줄 수 있는 Infinite ScrollView

를 따로 만들게 되었다.

 

[AT] Infinite ScrollView

혹시나 필자가 만들어둔 ScrollView를 사용할 사람들을 위해 주의사항을 몇가지 작성해둔다.

 

1. 각 스크롤 되는 아이템 항목은 가변적으로 늘어나되, 가변으로 아이템의 크기가 변경되는 사항은 NGUI Anchor를 이용한다는 전제로 구현되어 있다.

2. Prefab으로 연결 시킬 Item의 크기에 따라 자동으로 내부의 Pooling이 결정 된다.

3. 여러개의 Column을 지원하지만, 1 Column 체제로 사용하는 것을 추천한다.

4. 필자는 Vertical 기능만 있으면 되서 Horizontal 은 구현하지 않았다. (Horizontal 부분은 구조만 잡혀있다)

5. SetFocus 기능을 이용하면 해당 아이템을 기준으로 Refresh 된다. (Panel 이 Spring 되는 것이 아니다)

6. Position 계산의 Pivot은 Top이 기준이다.

7. Default 설정 값은 가급적 그대로 두고 사용하길 추천한다 (One Direction, Flat, Horizontal 등. 미완성 내용이 많다)

8. Prepare Size 는 Pool에서 미리 잡아두는 예비 Item 사이즈이다. (2~4정도면 적절, Column 갯수가 늘어난다면 문제가 발생할 수 있다)

 

결국 Pooling 형태의 Infinite ScrollView로 변경한 결과, 오래 된 내용이 있을지라도 Instantiate ( ) 를 하지 않고 현재 보여질 내용만 갱신하게 되어 속도와 메모리 측면에서 상당히 좋은 결과를 얻을 수 있었다.

 

다만 단점은 갱신 되는 Item의 크기와 예비 갯수에 따라 스크롤이 매끄럽게 계속해서 이어가다가 끊기는 현상이 다분히 발생할 수 있다.

 

 

아래는 원본 Infinite ScrollView Class Code이다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AT.InfiniteScrollView
{
[System.Serializable]
public class InfiniteItem
{
public int index;
public GameObject gameObject;
}
[System.Serializable]
public class InfiniteScrollView
{
public enum TABLE_NEXT_DIRECTION_TYPE
{ ZIGZAG, ONE_DIRECTION, }
public enum TABLE_POSITION_SORT_TYPE
{ DYNAMIC, FLAT, }
[Header("Scroll View")]
public GameObject scrollView;
public TABLE_NEXT_DIRECTION_TYPE tableDirectionType = TABLE_NEXT_DIRECTION_TYPE.ZIGZAG;
public TABLE_POSITION_SORT_TYPE tableSortType = TABLE_POSITION_SORT_TYPE.DYNAMIC;
[Header("ScrollView DragNotification")]
public UIScrollView.OnDragNotification onDragStarted;
public UIScrollView.OnDragNotification onStoppedMoving;
public UIScrollView.OnDragNotification onDragFinished;
[Header("Item")]
public GameObject itemPrefab;
public int prepareSize = 2;
public Vector2 padding = Vector2.zero;
[Header("Scrollbar")]
public UIProgressBar scrollbar;
public UIScrollView.ShowCondition showCondition = UIScrollView.ShowCondition.OnlyIfNeeded;
public float sliderValue;
public TweenAlpha tweenAlpha;
[Header("Action")]
public System.Action<int, GameObject> OnUpdateItem = null;
public System.Func<int> OnGetItemCount = null;
[Header("(AUTO) Infinite Setting")]
[SerializeField] protected UIPanel panel;
[SerializeField] protected UIScrollView view;
[SerializeField] protected LinkedList<InfiniteItem> itemList = new LinkedList<InfiniteItem>();
[SerializeField] protected bool isHorizontal = false;
[SerializeField] protected int rowCount;
[SerializeField] protected int columnCount;
[SerializeField] protected int maxItemCountInView;
[SerializeField] protected int maxItemCount;
[Header("(AUTO) List View Init Size")]
[SerializeField] protected Vector2 bufferStart; // buffer1
[SerializeField] protected Vector2 bufferEnd; // buffer2
[SerializeField] protected Vector2 initPosition; // Initialize Position
[SerializeField] protected float initBound; // Initialize Bound
public void Init()
{
view = scrollView.GetComponent<UIScrollView>();
panel = view.GetComponent<UIPanel>();
panel.onClipMove += OnMove;
Vector4 clip = panel.baseClipRegion;
Vector2 viewSize = new Vector2(clip.z, clip.w);
Bounds bound = NGUIMath.CalculateRelativeWidgetBounds(itemPrefab.transform, true);
Vector2 itemSize = new Vector2(bound.size.x, bound.size.y);
Vector3 totalItemSize = itemSize + padding;
columnCount = Mathf.RoundToInt(viewSize.x / totalItemSize.x);
rowCount = Mathf.RoundToInt(viewSize.y / totalItemSize.y);
maxItemCountInView = columnCount * rowCount;
if (view.movement == UIScrollView.Movement.Horizontal)
{
isHorizontal = true;
columnCount = columnCount + prepareSize;
initPosition.x = -(viewSize.x * 0.5f) + (totalItemSize.x / 2.0f);
initPosition.y = totalItemSize.y * (rowCount - 1) / 2.0f;
initBound = -(viewSize.x - padding.x) * 0.5f;
}
else if (view.movement == UIScrollView.Movement.Vertical)
{
isHorizontal = false;
rowCount = rowCount + prepareSize;
initPosition.x = -totalItemSize.x * (columnCount - 1) / 2.0f;
initPosition.y = (viewSize.y / 2.0f) - (totalItemSize.y / 2.0f);
initBound = (viewSize.y - padding.y) * 0.5f;
}
maxItemCount = columnCount * rowCount;
bufferEnd.x = totalItemSize.x * columnCount;
bufferEnd.y = totalItemSize.y * rowCount;
bufferStart.x = bufferEnd.x * 0.5f;
bufferStart.y = bufferEnd.y * 0.5f;
// Prefab GameObject InActive
itemPrefab.gameObject.SetActive(false);
// Clear Pool
itemList.Clear();
// Item Add To Pool
InfiniteItem item = null;
for (int i = 0; i < maxItemCount; i++)
{
item = new InfiniteItem
{
index = i,
gameObject = NGUITools.AddChild(scrollView.gameObject, itemPrefab)
};
item.gameObject.transform.name = item.index.ToString();
itemList.AddLast(item);
}
// Calculate Reset Item Position & ScrollView Reset
ResetScroll();
// Scrollbar Tween Alpha
if (scrollbar)
{
tweenAlpha = scrollbar.GetComponent<TweenAlpha>();
if (tweenAlpha == null)
tweenAlpha = scrollbar.gameObject.AddComponent<TweenAlpha>();
tweenAlpha.from = 1;
tweenAlpha.to = 0;
tweenAlpha.duration = 1.0f;
tweenAlpha.SetStartToCurrentValue();
if (showCondition != UIScrollView.ShowCondition.Always)
tweenAlpha.PlayForward();
}
// Remove Drag Notification
view.onDragStarted -= OnDragStarted;
view.onStoppedMoving -= OnStoppedMoving;
view.onDragFinished -= OnDragFinished;
// Add Drag Notification
view.onDragStarted += OnDragStarted;
view.onStoppedMoving += OnStoppedMoving;
view.onDragFinished += OnDragFinished;
}
void OnDragStarted()
{
ShowScrollbars(true);
if (onDragStarted != null) onDragStarted();
}
void OnStoppedMoving()
{
ShowScrollbars(false);
if (onStoppedMoving != null) onStoppedMoving();
}
void OnDragFinished()
{
if (onDragFinished != null) onDragFinished();
}
///<summary> Update - Clipping Event When Panel OnMove </summary>
protected void OnMove(UIPanel panel)
{
// Clipping Item Update
UpdateScrollViewItems();
// Scrollbar Update
if (scrollbar)
UpdateScrollBars();
}
/// <summary> Visible Item Count In View </summary>
public int GetVisibleItemCount()
{
int result = 0;
UIWidget widget = null;
foreach (InfiniteItem item in itemList)
{
widget = item.gameObject.GetComponentInChildren<UIWidget>();
if (widget != null)
if (panel.IsVisible(widget)) result++;
}
return result;
}
/// <summary> ScrollBar Update </summary>
public void UpdateScrollBars()
{
if (OnGetItemCount == null) return;
if (scrollbar == null) return;
int endIndex = 0;
UIWidget widget = null;
foreach (var item in itemList)
{
widget = item.gameObject.GetComponentInChildren<UIWidget>();
if (widget && panel.IsVisible(widget))
endIndex = endIndex < item.index ? item.index : endIndex;
}
// Do - Calculate ScrollBar Value & Size
UpdateScrollBars(scrollbar, endIndex, GetVisibleItemCount(), OnGetItemCount());
}
/// <summary> Calculate ScrollBar Value & Size</summary>
private void UpdateScrollBars(UIProgressBar slider, float lastVisibleIndex, float visibleItemCount, float totalItemCount)
{
// Slider Value For Scroll Position
if (lastVisibleIndex < maxItemCountInView) lastVisibleIndex = 0;
slider.value = Mathf.Clamp01(lastVisibleIndex / (totalItemCount - 1));
// Slider Bar Size as Current Visible Item Count
UIScrollBar sb = slider as UIScrollBar;
if (sb != null) sb.barSize = Mathf.Clamp01((visibleItemCount / totalItemCount));
}
void ShowScrollbars(bool isShow)
{
if (null == scrollbar) return;
switch (showCondition)
{
case UIScrollView.ShowCondition.Always:
tweenAlpha.enabled = false;
scrollbar.alpha = 1.0f;
break;
case UIScrollView.ShowCondition.OnlyIfNeeded:
case UIScrollView.ShowCondition.WhenDragging:
if (isShow == false) tweenAlpha.PlayForward();
else tweenAlpha.PlayReverse();
break;
}
}
/// <summary> Update ScrollView Items - Clipped Item Check </summary>
protected void UpdateScrollViewItems()
{
int realIndex = 0;
bool flag = true;
float distance = 0.0f;
Vector3 pos;
Vector3 size;
Vector4 clip = panel.finalClipRegion;
Transform trans = null;
InfiniteItem item = null;
// Horizontal ScrollView Update
if (isHorizontal)
{
// To do...
}
else // Vertical ScrollView Update
{
// Top Item Clipping Check - When Vertical
while (flag)
{
for (int i = 0; i < columnCount; i++)
{
item = itemList.First.Value;
trans = item.gameObject.transform;
pos = trans.localPosition;
distance = pos.y - clip.y;
size = GetItemSize(trans, false);
if (distance > bufferStart.y + size.y)
{
Transform targetTrans;
Vector3 targetItemSize;
realIndex = item.index + maxItemCount;
item.index = realIndex;
trans.name = realIndex.ToString();
if (realIndex < OnGetItemCount())
{
OnUpdateItem(realIndex, item.gameObject);
UIWidget[] widgets = item.gameObject.GetComponentsInChildren<UIWidget>();
foreach (var widget in widgets) widget.ResetAndUpdateAnchors();
item.gameObject.SetActive(true);
}
else
{
item.gameObject.SetActive(false);
}
LinkedListNode<InfiniteItem> targetNode = itemList.Last;
int index = targetNode.Value.index;
int column = index % columnCount;
while (column != realIndex % columnCount)
{
targetNode = targetNode.Previous;
index = targetNode.Value.index;
column = index % columnCount;
}
targetTrans = targetNode.Value.gameObject.transform;
targetItemSize = GetItemSize(targetTrans, false);
pos.x = targetTrans.localPosition.x;
pos.y = targetTrans.localPosition.y - (targetItemSize.y + padding.y);
trans.localPosition = pos;
itemList.RemoveFirst();
itemList.AddLast(item);
view.InvalidateBounds();
}
}
break;
}
// Bottom Item Cliping Check - When Vertical
flag = true;
while (flag)
{
for (int i = 0; i < columnCount; i++)
{
item = itemList.Last.Value;
trans = itemList.Last.Value.gameObject.transform;
pos = trans.localPosition;
distance = pos.y - clip.y;
if (distance < -(bufferStart.y))
{
Transform targetTrans;
Vector3 targetItemSize;
Vector3 differSize;
Vector3 itemSize;
realIndex = int.Parse(trans.name) - maxItemCount;
if (realIndex >= 0 && realIndex < OnGetItemCount())
{
item.index = realIndex;
trans.name = realIndex.ToString();
OnUpdateItem(realIndex, item.gameObject);
UIWidget[] widgets = item.gameObject.GetComponentsInChildren<UIWidget>();
foreach (var widget in widgets) widget.ResetAndUpdateAnchors();
item.gameObject.SetActive(true);
itemSize = GetItemSize(item.gameObject.transform, false);
LinkedListNode<InfiniteItem> targetNode = itemList.First;
int index = targetNode.Value.index;
int column = index % columnCount;
while (column != realIndex % columnCount)
{
targetNode = targetNode.Next;
index = targetNode.Value.index;
column = index % columnCount;
}
targetTrans = targetNode.Value.gameObject.transform;
targetItemSize = GetItemSize(targetTrans, false);
differSize = itemSize - targetItemSize;
pos.x = targetTrans.localPosition.x;
pos.y = targetTrans.localPosition.y + (targetItemSize.y + padding.y) + differSize.y;
trans.localPosition = pos;
itemList.RemoveLast();
itemList.AddFirst(item);
view.InvalidateBounds();
}
}
}
break;
}
}
}
/// <summary> Refresh Infinite ScrollView - Pivot: Current Top Item, Update Item </summary>
[ContextMenu("Refresh")]
public void Refresh()
{
if (OnGetItemCount == null) return;
int sortingTopIndex = OnGetItemCount();
InfiniteItem topItem = null;
foreach (InfiniteItem item in itemList)
{
if (item.index >= 0 && item.index < OnGetItemCount())
{
OnUpdateItem(item.index, item.gameObject);
UIWidget[] widgets = item.gameObject.GetComponentsInChildren<UIWidget>();
foreach (var widget in widgets) widget.ResetAndUpdateAnchors();
item.gameObject.SetActive(true);
if (item.index <= sortingTopIndex)
{
sortingTopIndex = item.index;
topItem = item;
}
}
else
{
item.gameObject.SetActive(false);
}
}
// Reposition Items
int index = 0;
int row = 0, column = 0;
int curRowOrColumn = -1;
int firstRowOrColumn = isHorizontal ? itemList.First.Value.index / rowCount : itemList.First.Value.index % columnCount;
Vector3 itemSize = Vector3.zero;
Vector3 position = Vector3.zero;
Vector3[] biggerItemSizeInRowsOrColumns = new Vector3[isHorizontal ? columnCount : rowCount];
Vector3[] biggerItemPosInRowsOrColumns = new Vector3[isHorizontal ? columnCount : rowCount];
Vector3[] beforeItemsSize = new Vector3[isHorizontal ? rowCount : columnCount];
Vector2[] beforeItemsPos = new Vector2[isHorizontal ? rowCount : columnCount];
foreach (InfiniteItem item in itemList)
{
itemSize = GetItemSize(item.gameObject.transform, false);
if (isHorizontal)
{
row = index % rowCount;
column = index / rowCount;
}
else
{
row = index / columnCount;
column = index % columnCount;
}
// Horizontal
if (isHorizontal)
{
// To do...
}
else // Vertical
{
if (tableDirectionType == TABLE_NEXT_DIRECTION_TYPE.ZIGZAG)
{
if (tableSortType == TABLE_POSITION_SORT_TYPE.DYNAMIC)
{
// Current Item's Row == First Item's Row ?
if (row == firstRowOrColumn)
{
position.x = item.gameObject.transform.localPosition.x;
position.y = item.gameObject.transform.localPosition.y;
curRowOrColumn = row;
}
else
{
if (curRowOrColumn == row) // Same Line - Next Item
{
position.x = beforeItemsPos[column - 1].x + (beforeItemsSize[column - 1].x + padding.x);
position.y = (row > 0) ? beforeItemsPos[column].y - (beforeItemsSize[column].y + padding.y) : initPosition.y;
}
else
{
position.x = initPosition.x + ((itemSize.x / 2.0f) * (columnCount - 1)) + padding.x;
position.y = (row > 0) ? beforeItemsPos[column].y - (beforeItemsSize[column].y + padding.y) : initPosition.y;
curRowOrColumn++;
}
}
}
else if (tableSortType == TABLE_POSITION_SORT_TYPE.FLAT)
{
if (curRowOrColumn == row) // Same Line - Next Item
{
position.x = beforeItemsPos[column - 1].x + (beforeItemsSize[column - 1].x + padding.x);
position.y = (row > 0) ? beforeItemsPos[column - 1].y : initPosition.y;
}
else // New Line - First Item
{
position.x = initPosition.x + ((itemSize.x / 2.0f) * (columnCount - 1)) + padding.x;
position.y = (row > 0) ? biggerItemPosInRowsOrColumns[row - 1].y - (biggerItemSizeInRowsOrColumns[row - 1].y + padding.y) : initPosition.y;
curRowOrColumn++;
}
if (biggerItemSizeInRowsOrColumns[row].y < itemSize.y)
{
biggerItemSizeInRowsOrColumns[row] = itemSize;
biggerItemPosInRowsOrColumns[row].y = position.y;
}
}
}
}
item.gameObject.transform.localPosition = position;
beforeItemsPos[column] = position;
beforeItemsSize[column] = itemSize;
index++;
}
if(scrollbar)
UpdateScrollBars();
}
/// <summary> Reset Infinite ScrollView - Reset Item Position, ScrollView Clipping </summary>
[ContextMenu("Reset")]
public void ResetScroll()
{
if (OnGetItemCount == null) return;
int index = 0;
foreach (InfiniteItem item in itemList)
{
item.index = index;
item.gameObject.name = index.ToString();
if (index < OnGetItemCount())
{
OnUpdateItem(index, item.gameObject);
item.gameObject.SetActive(true);
UIWidget[] widgets = item.gameObject.GetComponentsInChildren<UIWidget>();
foreach (var widget in widgets) widget.ResetAndUpdateAnchors();
}
else
{
item.gameObject.SetActive(false);
}
++index;
}
CalculateResetPosition();
if (scrollbar)
UpdateScrollBars();
}
/// <summary> Coroutine(Wait for 1 frame) - Calculate Reset Position </summary>
IEnumerator CoCalculateResetPosition()
{
// Wait 1 Frame For Update Each Widget Anchors
yield return new WaitForEndOfFrame();
// Calculate Item in ScrollView
CalculateResetPosition();
}
/// <summary> Calculate Reset Item Position </summary>
private void CalculateResetPosition()
{
Vector3 itemSize = Vector3.zero;
Vector3 position = Vector3.zero;
Vector3[] biggerItemSizeInRowsOrColumns = new Vector3[isHorizontal ? columnCount : rowCount];
Vector3[] biggerItemPosInRowsOrColumns = new Vector3[isHorizontal ? columnCount : rowCount];
Vector3[] beforeItemsSize = new Vector3[isHorizontal ? rowCount : columnCount];
Vector2[] beforeItemsPos = new Vector2[isHorizontal ? rowCount : columnCount];
// Reposition Items
int index = 0;
int row = 0, column = 0;
int curRowOrColumn = -1;
foreach (InfiniteItem item in itemList)
{
itemSize = GetItemSize(item.gameObject.transform);
if (isHorizontal)
{
row = index % rowCount;
column = index / rowCount;
}
else
{
row = index / columnCount;
column = index % columnCount;
}
// Horizontal
if (isHorizontal)
{
// To do...
}
else // Vertical
{
if (tableDirectionType == TABLE_NEXT_DIRECTION_TYPE.ZIGZAG)
{
if (tableSortType == TABLE_POSITION_SORT_TYPE.DYNAMIC)
{
if (curRowOrColumn == row) // Same Line - Next Item
{
position.x = beforeItemsPos[column - 1].x + (beforeItemsSize[column - 1].x + padding.x);
position.y = (row > 0) ? beforeItemsPos[column].y - (beforeItemsSize[column].y + padding.y) : initPosition.y;
}
else // New Line - First Item
{
position.x = initPosition.x + ((itemSize.x / 2.0f) * (columnCount - 1)) + padding.x;
position.y = (row > 0) ? beforeItemsPos[column].y - (beforeItemsSize[column].y + padding.y) : initPosition.y;
curRowOrColumn++;
}
}
else if (tableSortType == TABLE_POSITION_SORT_TYPE.FLAT)
{
if (curRowOrColumn == row) // Same Line - Next Item
{
position.x = beforeItemsPos[column - 1].x + (beforeItemsSize[column - 1].x + padding.x);
position.y = (row > 0) ? beforeItemsPos[column - 1].y : initPosition.y;
}
else // New Line - First Item
{
position.x = initPosition.x + ((itemSize.x / 2.0f) * (columnCount - 1)) + padding.x;
position.y = (row > 0) ? biggerItemPosInRowsOrColumns[row - 1].y - (biggerItemSizeInRowsOrColumns[row - 1].y + padding.y) : initPosition.y;
curRowOrColumn++;
}
if (biggerItemSizeInRowsOrColumns[row].y < itemSize.y)
{
biggerItemSizeInRowsOrColumns[row] = itemSize;
biggerItemPosInRowsOrColumns[row].y = position.y;
}
}
}
else if (tableDirectionType == TABLE_NEXT_DIRECTION_TYPE.ONE_DIRECTION)
{
// To do...
}
}
item.gameObject.transform.localPosition = position;
beforeItemsPos[column] = position;
beforeItemsSize[column] = itemSize;
index++;
}
ResetScrollView();
}
/// <summary> Reset ScrollView Clip, DragAmount </summary>
public void ResetScrollView()
{
if (panel == null) return;
Vector4 clip = panel.finalClipRegion;
if (isHorizontal)
{
if (GetContentsSize() < clip.z)
{
view.ResetPosition();
view.SetDragAmount(padding.x / clip.z, 0.0f, false);
}
else
{
view.ResetPosition();
}
}
else
{
if (GetContentsSize() < clip.w)
{
view.ResetPosition();
view.SetDragAmount(0.0f, padding.y / clip.w, false);
}
else
{
view.ResetPosition();
}
}
}
/// <summary> Get Item's Relative Widget Size in the GameObject </summary>
public Vector3 GetItemSize(Transform tr, bool isConsiderInactive = true)
{
if (tr == null)
return Vector3.zero;
Bounds bound = NGUIMath.CalculateRelativeWidgetBounds(tr, isConsiderInactive);
return bound.size;
}
/// <summary> Total Contents Size - It's not Accurately </summary>
public float GetContentsSize(int targetRowOrColumn = 0)
{
if (OnGetItemCount == null)
return 0;
float result = 0;
// Horizontal
if (isHorizontal)
{
// Each Column Item Size X
foreach (var item in itemList)
{
// Target Row == Item Row ?
if (targetRowOrColumn == (item.index % rowCount))
result += GetItemSize(item.gameObject.transform).x;
}
}
else // Vertical
{
// Vertical << Column Item Size X
foreach (var item in itemList)
{
// Target Column == Item Column ?
if (targetRowOrColumn == (item.index % columnCount))
result += GetItemSize(item.gameObject.transform).y;
}
}
int totalItemCount = OnGetItemCount();
result = result * (totalItemCount / maxItemCountInView);
return result;
}
/// <summary> SetFocus Method Work Only When (1 Row(When Horizontal) || 1 Column(Vertical))
/// <para> Warning! Do not use Immediately after Init, This Method Need a Frame later Init </para>
/// </summary>
public void SetFocus(int index)
{
if (OnGetItemCount == null) return;
if ((isHorizontal && rowCount > 1) || (!isHorizontal && columnCount > 1)) return;
if (index >= OnGetItemCount()) return;
index = Math.Max(index, 0);
LinkedListNode<InfiniteItem> item = itemList.Last;
while (item != null)
{
item.Value.index = index;
item.Value.gameObject.name = index.ToString();
if (index >= 0 && index < OnGetItemCount())
{
OnUpdateItem(index, item.Value.gameObject);
item.Value.gameObject.SetActive(true);
UIWidget[] widgets = item.Value.gameObject.GetComponentsInChildren<UIWidget>();
foreach (var widget in widgets) widget.ResetAndUpdateAnchors();
}
else
{
item.Value.gameObject.SetActive(false);
}
item = item.Previous;
index--;
}
Vector4 clip = panel.baseClipRegion;
Vector2 viewSize = new Vector2(clip.z, clip.w);
int calIndex = 0;
Vector3 position = Vector3.zero;
Vector3 itemSize = Vector3.zero;
Vector3 afterItemSize = Vector3.zero;
Vector3 afterItemPosition = Vector3.zero;
// View Init Position Right & Bottom
afterItemPosition.x = isHorizontal ? -(viewSize.x / 2.0f) : 0;
afterItemPosition.y = !isHorizontal ? -(viewSize.y / 2.0f) : 0;
// Add Current clip Offset + Center
afterItemPosition.x += panel.clipOffset.x + (isHorizontal ? clip.x : 0);
afterItemPosition.y += panel.clipOffset.y + (!isHorizontal ? clip.y : 0);
item = itemList.Last;
while (item != null)
{
itemSize = GetItemSize(item.Value.gameObject.transform);
position.x = afterItemPosition.x - ((calIndex == 0 ? 0 : itemSize.x) + padding.x) * (isHorizontal ? 1 : 0) + (calIndex == 0 && isHorizontal ? (itemSize.x) / 2.0f : 0);
position.y = afterItemPosition.y + ((calIndex == 0 ? 0 : itemSize.y) + padding.y) * (!isHorizontal ? 1 : 0) + (calIndex == 0 && !isHorizontal ? (itemSize.y) / 2.0f : 0);
item.Value.gameObject.transform.localPosition = position;
afterItemSize = itemSize;
afterItemPosition.x = position.x;
afterItemPosition.y = position.y;
calIndex++;
item = item.Previous;
}
view.InvalidateBounds();
}
public void RefreshToLastItem()
{
if (OnGetItemCount == null) return;
}
public void RefreshToFirstItem()
{
if (OnGetItemCount == null) return;
ResetScroll();
}
///<summary> Is Visible State Check Method. as Index Item </summary>
public bool IsVisibleItemFromIndex(int index)
{
if (index >= OnGetItemCount())
return false;
foreach (var item in itemList)
{
if (item.index == index && item.gameObject.activeSelf)
{
UIWidget[] widgets = item.gameObject.GetComponentsInChildren<UIWidget>();
foreach (UIWidget widget in widgets)
{
if (panel.IsVisible(widget))
return true;
}
}
}
return false;
}
}
}

 

'Unity Engine > NGUI' 카테고리의 다른 글

NGUI #02.UIPanel  (0) 2018.12.21
NGUI #01.UIRoot  (0) 2018.12.18
posted by REDFORCE 2018. 12. 23. 00:46

본 글은 Unity 2018.2.1f1 을 기반으로 작성되었습니다.


관련 글 링크 목록 

 #04.C# Job System_01

 #04.C# Job System_02



Unity C# Job System 메뉴얼

https://docs.unity3d.com/kr/2018.1/Manual/JobSystem.html


---------------------



01번 글에 이어서, 사실 Job System을 이해하기전에 개념적으로 알아두어야 할 내용이 하나 더 있습니다.


혹시나 C# Job Sytsem 메뉴얼을 먼저 전부 읽어보았다면 본 내용은 스킵하셔도 됩니다.



멀티 쓰레딩 이라는 것을 공부해보셨다면 아마 "경쟁" 이라는 게 무슨 뜻인지 알 것입니다.

간단히 설명하면 


어떤 int count; 라는 변수를 점유(접근 및 사용)하기 위해 

어떤 스레드가 count = 10; 이라는 작업을 하려고 하지만. 

동 시간대에 다른 스레드가 count = 99; 라고 작업을 하기 위해 


두 스레드간에 자원을 선점하기 위해 레이스에 빠져드는 것을 "경쟁" 이라고 합니다.


Job System은 이런 메모리 선점에 대해서 세이프한 공유 메모리 유형을 제공합니다.

그것이 바로 NativeContainer 입니다.


NativeContainer를 사용하면 이런 제한점을 극복 할 수 있습니다.


NativeContainer 란 무엇입니까?

 - NativeContainer는 네이티브 메모리에 비교적 안전한 C# Wrapper 를 제공하여 관리되는
값 형식 입니다.

 - NavtiveContainer를 사용하면 Job을 통해 사본으로 작업하는 대신 주 스레드와 공유 된 데이터에 접근 할 수 있습니다.


기본 NativeContainer는 NativeArray<T> 가 있습니다.


그러나 Unity - ECS(Entity Component System) 에서는 아래 4가지의 NativeContainer를 제공합니다.

(주의! Unity Package - Unity.Collections를 필요로 합니다.)


  • NativeList - List<T> 와 유사합니다.
  • NativeHashMap - Dictinonary<K, T>와 유사합니다.
  • NativeMultiHashMap - Dictinonary<K, T>와 유사하지만 동일한 Key를 등록할 수 있습니다.
  • NativeQueue - Queue<T>와 유사합니다. (FIFO 선입선출 구조)


자세한 Unity - ECS에 대해서는 추후 ECS 편에서 다루도록 하겠습니다.



개념적인 이론 글은 여기까지 적고, 다음 글에서 이제 실제 Job System을 사용하는 예제를 적어보도록 하겠습니다.


'Unity Engine > Unity3D Engine' 카테고리의 다른 글

C# Unity - Serializable Dictionary  (1) 2020.01.29
#04.C# Job System_01  (0) 2018.12.22
#03.JobSequenceManager  (0) 2018.12.21
#02.JobRepeatManager - 03  (0) 2018.12.20
#02.JobRepeatManager - 02  (0) 2018.12.20