'Unity'에 해당되는 글 6건

  1. 2019.06.20 [NGUI] Infinite ScrollView (AT) 1
  2. 2018.12.21 #03.JobSequenceManager
  3. 2018.12.21 NGUI #02.UIPanel
  4. 2018.12.20 #02.JobRepeatManager - 03
  5. 2018.12.19 #02.JobRepeatManager - 01
  6. 2018.12.18 NGUI #01.UIRoot
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. 21. 11:20

이번에 적을 글은 전편인 JobRepeatManager 에 이어서 JobSequenceManager 입니다.

(대체 얼마나 할일없으면 나 자신은 이런걸 만드는거니...코딩 덕후 같으니)


다음과 같은 이유나 상황일 때 사용하기 위해 만들었습니다.


 > 순차적인 함수 호출이 필요하다

 > 서로 다른 인스턴스에서의 함수 호출로 인해 순차적 호출 시점 제어가 너무 귀찮다.

 > 각 함수들을 Coroutine 으로 순차적 실행을 하고 싶을 때가 있다.


그래서 만든 것이 JobSequenceManager 입니다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Reflection;
namespace AT.JobManager
{
public class JobSequenceManager : SingletonBase<JobSequenceManager> {
public Queue<Tuple<MethodInfo, object, object[]>> _sequenceJobs = new Queue<Tuple<MethodInfo, object, object[]>>();
public void AddJob<T>(string methodName, params object[] parameter)
{
Type type = typeof(T);
object instance = Activator.CreateInstance(type);
MethodInfo toInvoke = type.GetMethod(methodName);
ParameterInfo[] parameters = toInvoke.GetParameters();
_sequenceJobs.Enqueue(new Tuple<MethodInfo, object, object[]>(toInvoke, instance, parameter));
}
public void OnExecuteSequenceJob()
{
while (_sequenceJobs.Count > 0)
{
var job = _sequenceJobs.Dequeue();
job.Item1.Invoke(job.Item2, job.Item3);
}
}
public void OnExecuteSequenceCoroutine()
{
StopCoroutine(CoExecuteJobSequence());
StartCoroutine(CoExecuteJobSequence());
}
protected IEnumerator CoExecuteJobSequence()
{
while (_sequenceJobs.Count > 0)
{
var job = _sequenceJobs.Dequeue();
job.Item1.Invoke(job.Item2, job.Item3);
yield return null;
}
yield return null;
}
}
}


JobSequenceManager는 다음과 같은 함수와 파라미터를 가지고 있습니다.


 멤버 변수

 설명


 public Queue<Tuple<MethodInfo, object, object[]>> _sequenceJobs


 등록되는 Job을 담고 있는 컨테이너 입니다.



 메소드

 설명 

 public void AddJob<T>(string methodName, params object[] parameter) 


 - 수행시킬 Job을 등록하는 함수입니다. 

 - AddJob<T>에서 <T>에는 본인이 등록 시킬 클래스 타입을 넣어주세요.

 - methodName에는 Type T에 구현되어있는 함수 이름을 넣어주세요.

 - 해당 함수에 파라미터를 전달 하고 싶은게 있을 시엔 parameter에 전달하면 됩니다.


 public void OnExecuteSequenceJob()


 - 등록 된 Job을 수행시킵니다.
 - 모든 Job을 일괄 순차적으로 수행시킵니다.


 public void OnExecuteSequenceCoroutine()


 - 등록 된 Job을 Corotuine 형태로 수행시킵니다.

 - 모든 Job을 순차적으로 수행시킵니다.

 - JobSequenceManager의 CoExecuteJobSequence( )
에서 한 Job마다 수행이 끝날 때마다 yield return null 이 불려집니다.


 protected IEnumerator CoExecuteJobSequence()


 - 등록 된 Job을 IEnumerator(Corotuine) 형태로 수행시키는 함수입니다.

 



사용 방법은 간단하게 아래 샘플로 적어두겠습니다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JobSequenceTest : MonoBehaviour {
void Start () {
Invoke("RegistJobA", 2.0f);
Invoke("ExecuteJob", 5.0f);
}
public void RegistJobA()
{
AT.JobManager.JobSequenceManager.Instance.AddJob<JobSequenceTest>("OnExecuteFunctionA");
AT.JobManager.JobSequenceManager.Instance.AddJob<JobSequenceTest>("OnExecuteFunctionB", 100, 43.123f);
}
public void ExecuteJob()
{
AT.JobManager.JobSequenceManager.Instance.OnExecuteSequenceJob(); // use case 1 - execute at the Sametime
//AT.JobManager.JobSequenceManager.Instance.OnExecuteSequenceCoroutine(); // use case 2 - coroutine - ( loop ) { Job Execute(); yield return null; }
}
public void OnExecuteFunctionA()
{
Debug.Log("Execute OnExecuteFunctionA");
}
public void OnExecuteFunctionB(int count, float point)
{
Debug.LogFormat("Execute OnExecuteFunctionB : {0}, {1}", count, point);
}
}


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

#04.C# Job System_02  (0) 2018.12.23
#04.C# Job System_01  (0) 2018.12.22
#02.JobRepeatManager - 03  (0) 2018.12.20
#02.JobRepeatManager - 02  (0) 2018.12.20
#02.JobRepeatManager - 01  (0) 2018.12.19
posted by REDFORCE 2018. 12. 21. 00:53

Unity 2018.2.1f1 - NGUI 3.12.0 을 기반으로 작성 되었습니다.

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

이번에 작성할 내용은 UIRoot에 이어서 UIPanel입니다.




UIPanel 은 NGUI의 UIWidget들이 담겨져서 보여지게 되는 일련의 가장 뒷편에 깔리는 도화지 같은 개념이라 보시면 됩니다.


 

 설명

 Alpha 


 해당 패널의 Hierarcy 구조상 하위에 붙은 모든 Widget들의 Alpha에 영향을 미치는 값 입니다. 하위에 붙어있는 Panel에는 영향을 주지 않습니다.

 

 Depth


 만약 UIPanel이 2개 이상 UIRoot에 자식으로 활성화되어 있을 시

먼저 보여지게 될 순서를 의미합니다.


 Clipping


 특정 영역을 설정하고 그 영역 밖에 있는 부분을 렌더링하지 않는 것을 의미합니다.


 > None : 클리핑을 하지 않습니다.

 > Texture Mask : 등록 된 Texture 영역을 제외하고 클리핑 합니다.

 > Soft Clipping : 패널 영역을 설정하고 클리핑 합니다. Softness 값을 주어 외곽부분을 부드럽게 클리핑 되도록 합니다.

 > Constrained But Dont Clip : 패널 영역을 지정하지만 클리핑은 하지 않습니다.


 Advanced Options

  

 Render Q :
  - Rendering 되어질 때의 순서를 정할 수 있습니다.

  - UI에 Particle System 을 사용하는 상황 같은 Render Queue 경쟁이 벌어질 수 있는 환경에서 설정하는 값 입니다. 

 Sort Order :
  - 같은 DEpth에서 렌더링 순서를 결정합니다.
  - Unity Sprite의 OrderInLayer와 유사합니다.
  - 높을 수록 위에 출력됩니다.

 Normal : 체크하면 패널이 Light에 영향을 받습니다.

 Cull : ScrollView 패널에서 성능 향상을 위해 사용되어지는 옵션입니다. 패널이 드래그(Drag) 되어 Widget이 패널 영역을 벗어날 시 자식위젯들을 Rendering 하지 않습니다.

 Visible : 패널 내부의 Widget들이 스크린 안에 있는지 계산하여 Rendering 여부를 결정하는 데, 이 계산을 건너띄게 만듭니다.
UIWidget 들이 패널을 벗어날 일이 없을 경우 체크하게 되면 성능이 향상 됩니다.

 Padding : 패널의 외각 부분을 부드럽게 만듭니다.
 (클리핑 옵션이 Softness 인 상태에서는 의미는 없는 듯)

 Static : 패널 내의 위젯들이 이동이 없는 경우 Static으로 설정하면 유니티가 Position, Rotation, Scale 값을 계산하지 않아 성능이 향상 됩니다.


 Anchors


  UIPanel 또는 UIWidget 에서 특정 위치를 기준으로 좌표가 잡히도록 설정하는 값 입니다.


 Show Draw Calls

 Draw Call Tool 창을 불러옵니다.


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

[NGUI] Infinite ScrollView (AT)  (1) 2019.06.20
NGUI #01.UIRoot  (0) 2018.12.18
posted by REDFORCE 2018. 12. 20. 02:56

#02.JobRepeatManager - 01

#02.JobRepeatManager - 02


이번 글은 마지막으로 JobRepeatManager 에 대한 간단한 구조 설명과

사용 예제로 마무리 하겠습니다.


다소 글이 길어질 수 있습니다. 만약 액기스만 뽑아먹을래! 하는 분은 샘플예제만 보셔도 됩니다.

해서...샘플 예제를 먼저 적어두겠습니다.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using AT.JobManager;
public class JobRepeat_Sample : MonoBehaviour {
void Start ()
{
JobRepeatManager.Instance.AddDelegateJob("My_Job_01", OnExecuteTodo, 1.0f, 0, new object[] { "My_Job_01", 0});
JobRepeatManager.Instance.AddDelegateJob("My_Job_02", OnExecuteTodo, 2.0f, 0, new object[] { "My_Job_02", 1});
JobRepeatManager.Instance.AddDelegateJob("My_Job_03", OnExecuteTodo, 2.0f, 10, new object[] { "My_Job_03", 2});
JobRepeatManager.Instance.AddDelegateJob("My_Job_04", OnExecuteTodo, 2.0f, 15, new object[] { "My_Job_04", 3}, OnEndActionWhenDrop);
JobRepeatManager.Instance.AddDelegateJob("My_Job_05", OnExecuteTodo, 1.0f, 20, new object[] { "My_Job_05", 4}, OnEndActionWhenDrop, OnCheckDoCondition);
JobRepeatManager.Instance.AddDelegateJob("My_Job_06", OnExecuteTodo, 1.0f, 20, new object[] { "My_Job_06", 5}, OnEndActionWhenDrop, OnCheckDoCondition, OnCheckDrop);
IEnumerator myJobCoroutine = CoJobExecuteTodo();
JobRepeatManager.Instance.AddCoroutineJob("My_JobCoroutine", myJobCoroutine, 1.0f, 10, new object[] { "My_JobCoroutine" });
}
public IEnumerator CoJobExecuteTodo(params object[] param)
{
for(int i = 0; i < 10; i++)
{
Debug.Log("{0} : JobCoroutine To do!!");
yield return new WaitForSeconds(0.5f);
}
}
public void OnExecuteTodo(params object[] param)
{
// To do
Debug.LogFormat("{0} : To do !!", param[0]);
}
public void OnEndActionWhenDrop(params object[] param)
{
// Execute When Dropped
Debug.LogFormat("{0} : End Action!", param[0]);
}
public bool OnCheckDoCondition(params object[] param)
{
// true -> Available Execute : false -> Block Execute
return true;
}
public bool OnCheckDrop(params object[] param)
{
// true > Job Drop : false > Nothing
Debug.LogFormat("{0} : Job drop", param[0]);
return true;
}
}


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


2. JobRepeatManager


JobRepeatManager 는 Singleton Pattern으로 인스턴스가 생성 됩니다.


싱글톤 패턴으로 생성 되어있는 인스턴스 이므로 만약 JobRepeatManager를 사용하실 분들은 간단히 static으로 인스턴스를 넣어주세요.


JobRepeatManager에는 다음과 같은 Public 함수와 멤버변수가 있습니다.


 멤버변수 및 애트리뷰트

 설명

 public List<JobRepeatBase> JobList


 get; _jobList


 public float MinDelayTime


 get;set; => m_MinDelayTime 



 메소드 이름

 설명 


 public bool AddDelegateJob(string key, 

 JobTodo toDo, 

 float delay = 1.0f,

 int repeatCount = 0,

 object[] parameter = null,             JobEndAction endActionWhenDrop = null,

 JobToDoCondition toDoCondition = null, 

 JobAutoDropCondition autoDropCondition = null, 

 bool isImmediately = true)


 - JobRepeatBase를 생성 및 등록하는 함수

 - 일반적인 Delegate 형태의 Job을 등록할 수 있습니다.


 public AddCoroutineJob (string key, 

 IEnumerator coroutineJobTodo, 

 float delay = 1.0f, 

 int repeatCount = 0,

 object[] param = null,

 JobToDoCondition toDoCondition = null, 

 JobAutoDropCondition autoDropCondition = null,

 bool isImmediately = true)


 - JobRepeatBase를 생성 및 등록하는 함수

 - Coroutine 형태의 Job을 생성합니다.

 - Todo를 추가로 Job에 등록하고 싶으실 땐 AddFunctionChain을 호출하여 사용합니다. 

 public bool JobStart (string key)


 - 특정 등록되어진 Job을 key 값으로 찾아서 Job을 Execute 시킵니다.

 - 정상적인 실행이 성공되면 true를 리턴합니다. 실패 시 return false


 public bool RemoveJob(string key)


 - 특정 등록되어진 Job을 Key 값으로 찾아서 드랍(Drop)시킵니다. 정상적으로 드랍에 성공할 시 true를 리턴합니다.


 public int JobDropAll ( )


 - 등록되어진 모든 Job을 Drop 시킵니다.

 - 드랍시킨 Job 갯수를 리턴합니다.


 public bool ChangeJobDelay (string key, float newDelay)


 - 특정 등록되어진 Job을 Key 값으로 찾은 뒤 해당 Job의 DelayTime을 변경합니다.

 - 만약 수행중(Job Started)인 Job 이었다면 다음 수행부터 Delay가 변경됩니다.

 - 변경에 선공하면 true 를 리턴합니다.


 public bool ChangeJobRepeatCount (string key, int repeatCount)


 - 특정 등록되어진 Job을 Key 값으로 찾은 뒤 해당 Job의 RepeatCount를 변경합니다.

 - 만약 수행중(Job Started)인 Job 이었다면 다음 수행부터 RepeatCount가 변경됩니다.

 - 변경에 성공하면 true를 리턴합니다.



 public bool AddFunctionChain(string key,

 JobTodo todo = null, 

 JobTodoCondition TodoCheck = null,

 JobAutoDropCondition autodropCondition = null, bool isExecuteImmediately = true)


 - key값으로 찾은 Job에 등록되어진 Delegate를 추가로 등록하는 함수입니다.


 public bool RemoveFunctionChain(string key, JobTodo todo = null,   JobTodoCondition todoCheck = null,

 JobAutoDropCondition autodropCondition = null, bool isExecuteImmediately = true)


 -Key 값으로 찾은 Job에 등록되어진 Delegate를 삭제하는 함수입니다.

 public JobRepeatBase GetJobBase(string key)

 - Key 값으로 JobRepeatBase 정보를 찾는 함수입니다.

 private IEnumerator CoAutoDropWorkers()


 - Drop 상태가 된 Job을 삭제시키는 Coroutine 입니다.

 - JobRepeatManager 의 instance가 Start 될 때 실행 됩니다.

 - Drop 상태 체크는 

private WaitForSeconds dropManagingDelay 마다 수행됩니다.

 

 private IEnumerator CoJobHandle(string key)


 - JobRepeatManager에 등록되어진 Job들을 동작시키는 Corotuine 입니다.

 - JobRepeatBase에 담겨져있는 Job.state 값에 따라 Job을 동작시킵니다.



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


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using JobTodo = AT.JobManager.JobRepeatBase.JobTodo;
using JobToDoCondition = AT.JobManager.JobRepeatBase.JobToDoCondition;
using JobAutoDropCondition = AT.JobManager.JobRepeatBase.JobAutoDropCondition;
using JobEndAction = AT.JobManager.JobRepeatBase.JobEndAction;
using JOB_STATE = AT.JobManager.JobRepeatBase.JOB_STATE;
#if UNITY_EDITOR
using UnityEditor;
namespace AT.JobManager
{
[CustomEditor(typeof(JobRepeatManager))]
public class JobRepeatManagerEditor : Editor
{
private JobRepeatManager manager;
public override void OnInspectorGUI()
{
manager = (JobRepeatManager)target;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Empty Job"))
{
int EmptyJobCount = manager.JobList.FindAll(job => job.key.Contains("EmptyJob")).Count;
manager.AddDelegateJob(string.Format("EmptyJob_{0}", EmptyJobCount), null);
}
if (GUILayout.Button("Drop All Job"))
{
if(manager.JobList.Count > 0)
{
foreach(var jobItem in manager.JobList)
{
DestroyImmediate(jobItem.gameObject);
}
manager.JobList.Clear();
}
}
EditorApplication.update();
EditorGUILayout.EndHorizontal();
DrawDefaultInspector();
}
}
}
#endif
namespace AT.JobManager
{
[ExecuteInEditMode]
public class JobRepeatManager : SingletonBase<JobRepeatManager>
{
// Job Information Container - Job Data Struct With Delegate Job
[SerializeField] protected List<JobRepeatBase> _jobList = new List<JobRepeatBase>();
public List<JobRepeatBase> JobList { get { return _jobList; } }
// Coroutine Job Information Container - Job Data Struct With Coroutine Job
[SerializeField] protected Dictionary<string, Coroutine> _coroutineJobList = new Dictionary<string, Coroutine>();
// Min DelayTime
protected float m_MinDelayTime = 0.1f;
public float MinDelayTime { get { return m_MinDelayTime; } set { m_MinDelayTime = value; } }
/// <summary>
/// Job RepeatManager > Adding Job
/// <para>key = JobKeyName,
/// todo = ExecuteFunctionPointer,
/// delay = Update Sequence Delay(Seconds),
/// repeatCount = Total Execute Count,
/// parameter = params object[] : Your Parameter
/// todoCondition = Execute Condition(true = Available Execute, false = Block Execute),
/// autoDropCondition = Job Drop Condition(Flag : true = Drop, false = MoveNext)</para>
/// </summary>
public bool AddDelegateJob(string key, JobTodo toDo, float delay = 1.0f, int repeatCount = 0, object[] parameter = null,
JobEndAction endActionWhenDrop = null,
JobToDoCondition toDoCondition = null, JobAutoDropCondition autoDropCondition = null, bool isImmediately = true)
{
// Already Registered Job Check
if (JobList.Find(job => job.key.Equals(key)) != null)
return false;
GameObject JobObject = new GameObject(key);
JobObject.transform.parent = this.transform;
JobRepeatBase newJob = JobObject.AddComponent<JobRepeatBase>();
newJob.key = key;
newJob.jobCoroutine = null;
newJob.jobTodo = toDo;
newJob.repeatDelay = delay;
newJob.repeatCount = repeatCount;
newJob.excuteCount = 0;
newJob.jobToDoCheck = toDoCondition;
newJob.jobAutoDropCheck = autoDropCondition;
newJob.jobEndAction = endActionWhenDrop;
newJob.state = JOB_STATE.JOB_STANDBY;
newJob.worker = CoJobHandle(key);
newJob.parameter = parameter;
if(toDo == null)
{
Debug.LogWarningFormat("Are You Sure Adding Empty Job? Todo Parameter is null (key:{0})", key);
newJob.state = JOB_STATE.JOB_EMPTY;
}
newJob.repeatDelay = newJob.repeatDelay < m_MinDelayTime ? m_MinDelayTime : newJob.repeatDelay;
JobList.Add(newJob);
if (isImmediately)
{
StartCoroutine(newJob.worker);
}
return true;
}
/// <summary>
/// Job RepeatManager > Coroutine Type Adding Job
/// <para>key = JobKeyName,
/// todo = Coroutine Type Todo,
/// delay = Update Sequence Delay(Seconds),
/// repeatCount = Total Execute Count,
/// parameter = params object[] : Your Parameter
/// todoCondition = Execute Condition(Flag : true = Available Execute, false = Block Execute),
/// autoDropCondition = Job Drop Condition(Flag : true = Drop, false = MoveNext),
/// </summary>
public bool AddCoroutineJob(string key, IEnumerator coroutineJobTodo, float delay = 1.0f, int repeatCount = 0, object[] param = null,
JobToDoCondition toDoCondition = null, JobAutoDropCondition autoDropCondition = null,
bool isImmediately = true)
{
// Already Registered Job Check
if (JobList.Find(job => job.key.Equals(key)) != null)
return false;
GameObject JobObject = new GameObject(key);
JobObject.transform.parent = this.transform;
JobRepeatBase newJob = JobObject.AddComponent<JobRepeatBase>();
newJob.key = key;
newJob.jobCoroutine = coroutineJobTodo;
newJob.jobTodo = null;
newJob.repeatDelay = delay;
newJob.repeatCount = repeatCount;
newJob.excuteCount = 0;
newJob.jobToDoCheck = toDoCondition;
newJob.jobAutoDropCheck = autoDropCondition;
newJob.state = JOB_STATE.JOB_STANDBY;
newJob.worker = CoJobHandle(key);
newJob.parameter = param;
if (coroutineJobTodo == null)
{
Debug.LogWarningFormat("Are You Sure Adding Empty Job? Todo Parameter is null (key:{0})", key);
newJob.state = JOB_STATE.JOB_EMPTY;
}
newJob.repeatDelay = newJob.repeatDelay < m_MinDelayTime ? m_MinDelayTime : newJob.repeatDelay;
JobList.Add(newJob);
if (isImmediately)
{
StartCoroutine(newJob.worker);
}
return true;
}
private void Start()
{
StartCoroutine(CoAutoDropWorkers());
}
public bool RemoveJob(string key)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
DestroyImmediate(findJob.gameObject);
return JobList.Remove(findJob);
}
public bool JobStart(string key)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public int JobDropAll()
{
int droppedJobCount = 0;
if(JobList.Count > 0)
{
JobList.Clear();
droppedJobCount = transform.childCount;
transform.DestroyAllChildren();
}
return droppedJobCount;
}
public bool ChangeJobDelay(string key, float newDelay)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
findJob.repeatDelay = newDelay;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public bool ChangeRepeatCount(string key, int repeatCount)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
findJob.repeatCount = repeatCount;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public bool AddFunctionChain(string key, JobTodo Todo = null, JobToDoCondition toDoCheck = null, JobAutoDropCondition autoDropCondition = null, bool isExecuteImmediately = true)
{
JobRepeatBase Job = JobList.Find(job => job.key.Equals(key));
if (Job == null)
return false;
StopCoroutine(Job.worker);
Job.jobTodo += Todo;
Job.jobToDoCheck += toDoCheck;
Job.jobAutoDropCheck += autoDropCondition;
if (Job.jobTodo == null)
{
Job.state = JOB_STATE.JOB_EMPTY;
return true;
}
if (isExecuteImmediately)
{
Job.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(Job.worker);
}
return true;
}
public bool RemoveFunctionChain(string key, JobTodo Todo = null, JobToDoCondition toDoCheck = null,
JobAutoDropCondition autoDropCondition = null, bool isExecuteImmediately = true)
{
JobRepeatBase Job = JobList.Find(job => job.key.Equals(key));
if (Job == null)
return false;
StopCoroutine(Job.worker);
Job.jobTodo -= Todo;
Job.jobToDoCheck -= toDoCheck;
Job.jobAutoDropCheck -= autoDropCondition;
if (Job.jobTodo == null)
{
Job.state = JOB_STATE.JOB_EMPTY;
return true;
}
if (isExecuteImmediately)
{
Job.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(Job.worker);
}
return true;
}
public JobRepeatBase GetJobBase(string key)
{
return JobList.Find(x => x.key == key);
}
private WaitForSeconds _dropManagingDelay = new WaitForSeconds(3.0f);
private IEnumerator CoAutoDropWorkers()
{
while (gameObject.activeSelf)
{
var dropItems = JobList.FindAll(Job => Job.state == JOB_STATE.JOB_DROP);
foreach(var dropItem in dropItems)
{
dropItem.jobEndAction?.Invoke(dropItem.parameter);
JobList.Remove(dropItem);
DestroyImmediate(dropItem.gameObject);
}
yield return _dropManagingDelay;
}
}
private IEnumerator CoJobHandle(string key)
{
yield return null;
JobRepeatBase findJob = JobList.Find(x => x.key == key);
if (findJob == null)
yield break;
switch (findJob.state)
{
case JOB_STATE.JOB_EMPTY:
yield break;
case JOB_STATE.JOB_STANDBY:
if (findJob.jobToDoCheck != null)
{
if (findJob.jobToDoCheck(findJob.parameter))
{
findJob.state = JOB_STATE.JOB_WORKING;
findJob.jobTodo?.Invoke(findJob.parameter);
if (findJob.jobCoroutine != null)
yield return StartCoroutine(findJob.jobCoroutine);
findJob.excuteCount++;
if (findJob.excuteCount >= findJob.repeatCount && findJob.repeatCount != 0)
findJob.state = JOB_STATE.JOB_DROP;
else
findJob.state = JOB_STATE.JOB_WAITING;
}
}
else
{
findJob.state = JOB_STATE.JOB_WORKING;
findJob.jobTodo?.Invoke(findJob.parameter);
if (findJob.jobCoroutine != null)
yield return StartCoroutine(findJob.jobCoroutine);
findJob.excuteCount++;
if (findJob.excuteCount >= findJob.repeatCount && findJob.repeatCount != 0)
findJob.state = JOB_STATE.JOB_DROP;
else
findJob.state = JOB_STATE.JOB_WAITING;
}
if (findJob.jobAutoDropCheck != null)
{
if (findJob.jobAutoDropCheck(findJob.parameter))
{
findJob.state = JOB_STATE.JOB_DROP;
break;
}
}
break;
case JOB_STATE.JOB_WAITING:
WaitForSeconds WaitForDelay = new WaitForSeconds(findJob.repeatDelay);
yield return WaitForDelay;
findJob.state = JOB_STATE.JOB_STANDBY;
break;
case JOB_STATE.JOB_DROP:
yield break;
}
yield return StartCoroutine(CoJobHandle(findJob.key));
}
}
}


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

#04.C# Job System_01  (0) 2018.12.22
#03.JobSequenceManager  (0) 2018.12.21
#02.JobRepeatManager - 02  (0) 2018.12.20
#02.JobRepeatManager - 01  (0) 2018.12.19
#01.A에서 B로 일정 시간동안 움직이기  (0) 2018.12.18
posted by REDFORCE 2018. 12. 19. 01:45

#02.JobRepeatManager - 01


이번 글에 작성할 내용은 개인적으로 유니티를 사용하면서 이런 스크립트가 있으면 편할 것 같은데? 라는 생각에 만들어 본 스크립트이다.


그 이름은 Job Repeat Manager !!!!!!!!!!!


개인적으로 만들어놓고 그래서 요놈이 효율적일까? 괜찮은가? 안전한가? 

라는 질문을 수 차례했으나 답은...나도 모르겠당...ㅎㅎ


그래도 올려두면 누군가는 쓰지 않을까? 라는 생각에 피드백도 받아볼 겸 적는다.

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


JobRepeatManager는 글쓴이가 심심해서 만들어 본 유틸리티 성향의(?) 스크립트 입니다.

처음에 만들게 된 이유는 다음의 예로 들어보겠습니다.


목적 : 어떤 함수의 내용을 반복적으로 수행하게 하고 싶다
 예) public void MyFunction() { ... } 을 50 번만 2.0초 간격으로 수행하고 싶다.


  + 그러나 public bool isExecute;  라는 변수가 true 일때만 하고 싶다.

  + 만약 public bool isDrop; 이라는 변수가 true 면은 중간에 멈추고 싶다.

  + Invoke나 Coroutine을 쓸 수는 없는 상황이다.

  + 수행되고 있는 펑션의 상태를 Visual로 보고싶다.


위와 같은 상황에서...이걸 어떻게 해결 해야할까...라는 고민중에

결국 다음과 같은 조건을 충족하도록 만들어본 유틸리티 코드입니다.


 조 건 

  1. Update() 를 쓰고 싶지는 않다.
  2. 어떤 내용(To do)을 n번 실행하게 하고 싶다.
  3. 수행되는 내용을 GameObject 로 관리하고 싶다.
  4. 람다(Lambda) 또는 Delegate를 이용한 Event 형태의 Job을 만들어서 관리하고 싶다.


결과적으로 만든 JobRepeatManager 는 아래의 2가지 클래스 파일로 구성 되어 있습니다.


본문에서는 Gist 코드만 남기고 설명은 다음글로 넘기겠습니다.


1) JobRepeatBase.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
namespace AT.JobManager
{
[CustomEditor(typeof(JobRepeatBase))]
public class JobBaseEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
JobRepeatBase job = (JobRepeatBase)target;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Execute Immediately"))
{
if (job.jobTodo != null)
{
job.jobTodo();
job.excuteCount++;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Drop"))
{
job.state = JobRepeatBase.JOB_STATE.JOB_DROP;
}
if(GUILayout.Button("Drop Immediately"))
{
JobRepeatManager.Instance.RemoveJob(job.key);
DestroyImmediate(job.gameObject);
}
EditorGUILayout.EndHorizontal();
}
}
}
#endif
namespace AT.JobManager
{
[System.Serializable]
public class JobRepeatBase : MonoBehaviour {
public enum JOB_STATE
{
NONE,
JOB_EMPTY,
JOB_STANBY,
JOB_WORKING,
JOB_WAITING,
JOB_DROP,
}
public delegate void JobTodo(params object[] param);
public delegate void JobEndAction(params object[] param);
public delegate bool JobToDoCondition(params object[] param);
public delegate bool JobAutoDropCondition(params object[] param);
public string key;
public float repeatDelay;
public int repeatCount;
public int excuteCount;
public IEnumerator jobCoroutine;
public JobTodo jobTodo;
public JobEndAction jobEndAction;
public JobToDoCondition jobToDoCheck;
public JobAutoDropCondition jobAutoDropCheck;
public JOB_STATE state;
public IEnumerator worker;
public object[] parameter;
}
}


2) JobRepeatManager.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using JobTodo = AT.JobManager.JobRepeatBase.JobTodo;
using JobToDoCondition = AT.JobManager.JobRepeatBase.JobToDoCondition;
using JobAutoDropCondition = AT.JobManager.JobRepeatBase.JobAutoDropCondition;
using JobEndAction = AT.JobManager.JobRepeatBase.JobEndAction;
using JOB_STATE = AT.JobManager.JobRepeatBase.JOB_STATE;
#if UNITY_EDITOR
using UnityEditor;
namespace AT.JobManager
{
[CustomEditor(typeof(JobRepeatManager))]
public class JobRepeatManagerEditor : Editor
{
private JobRepeatManager manager;
public override void OnInspectorGUI()
{
manager = (JobRepeatManager)target;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Empty Job"))
{
int EmptyJobCount = manager.JobList.FindAll(job => job.key.Contains("EmptyJob")).Count;
manager.AddDelegateJob(string.Format("EmptyJob_{0}", EmptyJobCount), null);
}
if (GUILayout.Button("Drop All Job"))
{
if(manager.JobList.Count > 0)
{
foreach(var jobItem in manager.JobList)
{
DestroyImmediate(jobItem.gameObject);
}
manager.JobList.Clear();
}
}
EditorApplication.update();
EditorGUILayout.EndHorizontal();
DrawDefaultInspector();
}
}
}
#endif
namespace AT.JobManager
{
[ExecuteInEditMode]
public class JobRepeatManager : SingletonBase<JobRepeatManager>
{
// Job Information Container - Job Data Struct With Delegate Job
[SerializeField] protected List<JobRepeatBase> _jobList = new List<JobRepeatBase>();
public List<JobRepeatBase> JobList { get { return _jobList; } }
// Coroutine Job Information Container - Job Data Struct With Coroutine Job
[SerializeField] protected Dictionary<string, Coroutine> _coroutineJobList = new Dictionary<string, Coroutine>();
// Min DelayTime
protected float m_MinDelayTime = 0.1f;
public float MinDelayTime { get { return m_MinDelayTime; } set { m_MinDelayTime = value; } }
/// <summary>
/// Job RepeatManager > Adding Job
/// <para>key = JobKeyName,
/// todo = ExecuteFunctionPointer,
/// delay = Update Sequence Delay(Seconds),
/// repeatCount = Total Execute Count,
/// parameter = params object[] : Your Parameter
/// todoCondition = Execute Condition(true = Available Execute, false = Block Execute),
/// autoDropCondition = Job Drop Condition(Flag : true = Drop, false = MoveNext)</para>
/// </summary>
public bool AddDelegateJob(string key, JobTodo toDo, float delay = 1.0f, int repeatCount = 0, object[] parameter = null,
JobEndAction endActionWhenDrop = null,
JobToDoCondition toDoCondition = null, JobAutoDropCondition autoDropCondition = null, bool isImmediately = true)
{
// Already Registered Job Check
if (JobList.Find(job => job.key.Equals(key)) != null)
return false;
GameObject JobObject = new GameObject(key);
JobObject.transform.parent = this.transform;
JobRepeatBase newJob = JobObject.AddComponent<JobRepeatBase>();
newJob.key = key;
newJob.jobCoroutine = null;
newJob.jobTodo = toDo;
newJob.repeatDelay = delay;
newJob.repeatCount = repeatCount;
newJob.excuteCount = 0;
newJob.jobToDoCheck = toDoCondition;
newJob.jobAutoDropCheck = autoDropCondition;
newJob.jobEndAction = endActionWhenDrop;
newJob.state = JOB_STATE.JOB_STANDBY;
newJob.worker = CoJobHandle(key);
newJob.parameter = parameter;
if(toDo == null)
{
Debug.LogWarningFormat("Are You Sure Adding Empty Job? Todo Parameter is null (key:{0})", key);
newJob.state = JOB_STATE.JOB_EMPTY;
}
newJob.repeatDelay = newJob.repeatDelay < m_MinDelayTime ? m_MinDelayTime : newJob.repeatDelay;
JobList.Add(newJob);
if (isImmediately)
{
StartCoroutine(newJob.worker);
}
return true;
}
/// <summary>
/// Job RepeatManager > Coroutine Type Adding Job
/// <para>key = JobKeyName,
/// todo = Coroutine Type Todo,
/// delay = Update Sequence Delay(Seconds),
/// repeatCount = Total Execute Count,
/// parameter = params object[] : Your Parameter
/// todoCondition = Execute Condition(Flag : true = Available Execute, false = Block Execute),
/// autoDropCondition = Job Drop Condition(Flag : true = Drop, false = MoveNext),
/// </summary>
public bool AddCoroutineJob(string key, IEnumerator coroutineJobTodo, float delay = 1.0f, int repeatCount = 0, object[] param = null,
JobToDoCondition toDoCondition = null, JobAutoDropCondition autoDropCondition = null,
bool isImmediately = true)
{
// Already Registered Job Check
if (JobList.Find(job => job.key.Equals(key)) != null)
return false;
GameObject JobObject = new GameObject(key);
JobObject.transform.parent = this.transform;
JobRepeatBase newJob = JobObject.AddComponent<JobRepeatBase>();
newJob.key = key;
newJob.jobCoroutine = coroutineJobTodo;
newJob.jobTodo = null;
newJob.repeatDelay = delay;
newJob.repeatCount = repeatCount;
newJob.excuteCount = 0;
newJob.jobToDoCheck = toDoCondition;
newJob.jobAutoDropCheck = autoDropCondition;
newJob.state = JOB_STATE.JOB_STANDBY;
newJob.worker = CoJobHandle(key);
newJob.parameter = param;
if (coroutineJobTodo == null)
{
Debug.LogWarningFormat("Are You Sure Adding Empty Job? Todo Parameter is null (key:{0})", key);
newJob.state = JOB_STATE.JOB_EMPTY;
}
newJob.repeatDelay = newJob.repeatDelay < m_MinDelayTime ? m_MinDelayTime : newJob.repeatDelay;
JobList.Add(newJob);
if (isImmediately)
{
StartCoroutine(newJob.worker);
}
return true;
}
private void Start()
{
StartCoroutine(CoAutoDropWorkers());
}
public bool RemoveJob(string key)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
DestroyImmediate(findJob.gameObject);
return JobList.Remove(findJob);
}
public bool JobStart(string key)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public int JobDropAll()
{
int droppedJobCount = 0;
if(JobList.Count > 0)
{
JobList.Clear();
droppedJobCount = transform.childCount;
transform.DestroyAllChildren();
}
return droppedJobCount;
}
public bool ChangeJobDelay(string key, float newDelay)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
findJob.repeatDelay = newDelay;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public bool ChangeRepeatCount(string key, int repeatCount)
{
JobRepeatBase findJob = JobList.Find(job => job.key.Equals(key));
if (findJob == null)
return false;
findJob.repeatCount = repeatCount;
StopCoroutine(findJob.worker);
findJob.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(findJob.worker);
return true;
}
public bool AddFunctionChain(string key, JobTodo Todo = null, JobToDoCondition toDoCheck = null, JobAutoDropCondition autoDropCondition = null, bool isExecuteImmediately = true)
{
JobRepeatBase Job = JobList.Find(job => job.key.Equals(key));
if (Job == null)
return false;
StopCoroutine(Job.worker);
Job.jobTodo += Todo;
Job.jobToDoCheck += toDoCheck;
Job.jobAutoDropCheck += autoDropCondition;
if (Job.jobTodo == null)
{
Job.state = JOB_STATE.JOB_EMPTY;
return true;
}
if (isExecuteImmediately)
{
Job.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(Job.worker);
}
return true;
}
public bool RemoveFunctionChain(string key, JobTodo Todo = null, JobToDoCondition toDoCheck = null,
JobAutoDropCondition autoDropCondition = null, bool isExecuteImmediately = true)
{
JobRepeatBase Job = JobList.Find(job => job.key.Equals(key));
if (Job == null)
return false;
StopCoroutine(Job.worker);
Job.jobTodo -= Todo;
Job.jobToDoCheck -= toDoCheck;
Job.jobAutoDropCheck -= autoDropCondition;
if (Job.jobTodo == null)
{
Job.state = JOB_STATE.JOB_EMPTY;
return true;
}
if (isExecuteImmediately)
{
Job.state = JOB_STATE.JOB_STANDBY;
StartCoroutine(Job.worker);
}
return true;
}
public JobRepeatBase GetJobBase(string key)
{
return JobList.Find(x => x.key == key);
}
private WaitForSeconds _dropManagingDelay = new WaitForSeconds(3.0f);
private IEnumerator CoAutoDropWorkers()
{
while (gameObject.activeSelf)
{
var dropItems = JobList.FindAll(Job => Job.state == JOB_STATE.JOB_DROP);
foreach(var dropItem in dropItems)
{
dropItem.jobEndAction?.Invoke(dropItem.parameter);
JobList.Remove(dropItem);
DestroyImmediate(dropItem.gameObject);
}
yield return _dropManagingDelay;
}
}
private IEnumerator CoJobHandle(string key)
{
yield return null;
JobRepeatBase findJob = JobList.Find(x => x.key == key);
if (findJob == null)
yield break;
switch (findJob.state)
{
case JOB_STATE.JOB_EMPTY:
yield break;
case JOB_STATE.JOB_STANDBY:
if (findJob.jobToDoCheck != null)
{
if (findJob.jobToDoCheck(findJob.parameter))
{
findJob.state = JOB_STATE.JOB_WORKING;
findJob.jobTodo?.Invoke(findJob.parameter);
if (findJob.jobCoroutine != null)
yield return StartCoroutine(findJob.jobCoroutine);
findJob.excuteCount++;
if (findJob.excuteCount >= findJob.repeatCount && findJob.repeatCount != 0)
findJob.state = JOB_STATE.JOB_DROP;
else
findJob.state = JOB_STATE.JOB_WAITING;
}
}
else
{
findJob.state = JOB_STATE.JOB_WORKING;
findJob.jobTodo?.Invoke(findJob.parameter);
if (findJob.jobCoroutine != null)
yield return StartCoroutine(findJob.jobCoroutine);
findJob.excuteCount++;
if (findJob.excuteCount >= findJob.repeatCount && findJob.repeatCount != 0)
findJob.state = JOB_STATE.JOB_DROP;
else
findJob.state = JOB_STATE.JOB_WAITING;
}
if (findJob.jobAutoDropCheck != null)
{
if (findJob.jobAutoDropCheck(findJob.parameter))
{
findJob.state = JOB_STATE.JOB_DROP;
break;
}
}
break;
case JOB_STATE.JOB_WAITING:
WaitForSeconds WaitForDelay = new WaitForSeconds(findJob.repeatDelay);
yield return WaitForDelay;
findJob.state = JOB_STATE.JOB_STANDBY;
break;
case JOB_STATE.JOB_DROP:
yield break;
}
yield return StartCoroutine(CoJobHandle(findJob.key));
}
}
}



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

#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
#01.A에서 B로 일정 시간동안 움직이기  (0) 2018.12.18
posted by REDFORCE 2018. 12. 18. 01:49

Unity 2018.2.1f1 - NGUI 3.12.0 을 기반으로 작성 되었습니다.


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


NGUI는 Unity에서 UI를 제작하는데 많이 사용되고 있는 UI Plugin이다.


Unity(Unity GUI)가 과거(대략 유니티3.0 시절) 너무나 구닥다리 거지깽깽이 같은 기능성을 제공하는 바람에 Next-Gen UI Kit 이라고해서 Tasharen Entertainment에서 만들었다.


이 글을 작성하는 시점인 현재는 NGUI 개발자가 유니티에 스카웃되어 UGUI를 개발하고 있다고 알고 있다. 덕분에 UGUI가 옛날에 비하여 상당히 편해지고 좋아졌으나 아직까진 한국에서는 NGUI의 비중이 40% 이상인 것으로 알고 있다.


과거 NGUI가 흥하던 시절에는 NGUI 사용량이 UGUI에 비해 8~90%에 육박하였다.

역사적인 서론은 여기까지하고...


이 글을 적는 목적은 구글에서 NGUI에 대해서 검색할 시 흔히 볼 수 있는 글들이 너무나 오래 된 글들이 많아서 최신 버전을 사용하는 사람들에게는 생소하거나 동작되지 않거나 삭제되거나 변경 된 사항들이 자주 등장하다보니 삽질을 하는 일이 많아서 작성하게 되었다.


(사실 본인이 제일 삽질하는 바람에 답답해서 적게되었다)


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


#1. UIRoot


NGUI가 Project에 포함되어 있다면 위와 같이 상단 Toolbar에 "NGUI" 가 보입니다.



상단 툴바에서는 

 - Selection, Create, Attach, Tween 등 다양한 기능들이 들어있습니다.

각 항목을 모두 설명하는 것은 내용이 너무 많으므로 차근차근 사용해가면서 설명하겠습니다.



먼저 NGUI가 작동하려면 UIRootUICamera가 필요합니다.


간단하게 먼저 비어있는 Scene에서 NGUI > Create > Texture를 눌러보겠습니다.



위 사진처럼 Texture를 누르면 2번 항목과 같이 UIRoot와 Camera 그리고 Create에서 Texture를 했으므로 UITexture 컴포넌트가 붙은 "Texture" 이름을 가진 GameObject가 생성됩니다.



생성 된 UIRoot 를 선택해서 Inspector 창에 보여지는 정보를 살펴보면 다음과 같이 나옵니다.



UIRoot - 

 UIRoot 컴포넌트는 UIWidget을 상속받은 컴포넌트 및 오브젝트들의 최상단 패널로써,

해상도 또는 크기를 조절할 수 있는 스케일링 역할을 담당합니다.


UIRoot에는 다음과 같은 파라미터가 있습니다.


 파라미터

 설명

 Scaling Style

 1) Flexible : 화면 해상도에 관계 없이 UI가 픽셀 단위에서 언제나 같은 크기를 유지함. 실제 해상도에 따라 보여지는 크기가 작아지거나 커질 수 있음


 2) Constrained : UI가 화면에서 항상 같은 비율로 보이도록 스케일링 합니다. UI의 오리지날 픽셀 단위의 크기가 비율에 맞춰져 변동될 수 있음


 3) ConstrainedOnMobiles : 모바일 플랫폼에서는 Constrained 방식을 사용, 기타 플랫폼에서는 Flexible 방식을 사용합니다.


 Minimum Height

 

Flexible 방식에서 화면의 최소 높이 값을 지정합니다. 화면 높이가 설정한 값보다 작아지면 UI 크기를 조정합니다.


 Maximum Height

 

Flexible 방식에서 화면의 최대 높이를 지정합니다. 화면 높이가 설정한 값보다 커지면 UI 크기를 조정합니다.



UIRoot를 설정함에 있어 가장 중요한 점은 본인이 만드는 프로젝트의 기본 해상도를 어떻게 잡는지가 중요합니다.


16:9 비율을 쓸 것인가 4:3 비율을 쓸 것인가

1920x1080해상도를 기준으로 잡을 것인가 1280x720 해상도를 기준으로 잡을 것인가

등등, 모바일 플랫폼이라면 안드로이드 기기 별 해상도 차이가 심하기 때문에 고민하게 됩니다.


모바일 플랫폼에서는 이에 대응하기 위해 보통 ConstrainedOnMobiles 로 설정합니다.


ConstrainedOnMobiles로 설정하면 다음과 같이 Content Width, Content Height로 기준 해상도를 설정할 수 있습니다.



1) Content Width/Height 둘중 하나만 Fit 활성화 : 체크 된 항목의 비율을 유지하여 스케일링 됩니다.


2) Width/Height Fit 모두 비활성화 : 기기 해상도에 맞추어 UI가 늘어나지만 Camera에 모든 UI가 들어오지 않을 수 있습니다.


3) Width/Height Fit 모두 활성화 : 강제로 UI 항목들이 스케일링 되어 항상 화면에 보여지게 됩니다.


Fit를 둘다 체크할 시 UI 전체 화면이 스케일링 되어 UICamera에 보여질 수 있으나

기기 해상도 차이로 인해 발생하는 공백 부분은 메인카메라에 설정된 Background 값으로 채워집니다.


현재 회사에서 개발중인 게임(모바일)의 경우 


ConstrainedOnMobile 

 > Content Width(1280) Fit(Disable)

 > Content Height (720) Fit(Enable)


인 상태에서 NGUI의 앵커를 이용하여 해상도를 대응하고 있습니다.


모바일 플랫폼에서 그래서 UIRoot를 어떻게 설정하고해라 라는 정답은 없습니다만 최근 AppStore나 GoogleStore에서 요구하는 정책 사항을 고려해볼 때 위와같이 설정하여 앵커로 해상도를 대응하는 방식으로 가야하지 않을까 생각합니다.


(AppStore의 경우 Letter Camera를 사용하면 안됨. 2018년 기준)

좀 더 편한 방법이 있다면 저도 알려주세요.


UIPanel 에 대해서는 다음글에서 다루고 있습니다


RigidBody - 

 마지막으로 UIRoot에는 RigidBody Component가 붙습니다. 만약 RigidBody가 안 붙어 있다면 NGUI의 Widget들의 Event를 동작시킬 수 없습니다.


버튼, 스크롤, 스위치, 패널 등 Widget에서의 Click, Touch, Press, Drag 등의 Event를 검출하기 위해 붙어 있습니다.

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

[NGUI] Infinite ScrollView (AT)  (1) 2019.06.20
NGUI #02.UIPanel  (0) 2018.12.21