C# Xml 기반 Dialogue Scripting

JunTak Lee·2023년 5월 17일
0

Unity

목록 보기
1/2

최근 활동하고 있는 프로젝트에서 Dialogue 파트가 나한테 주어졌다
사실 내가 먼저 만들고 기획자분께 양식을 넘겨드릴 수 있었지만, 사람이 게으른지라 안하고있다가 명세를 받았다

받은 요구 명세는 다음과 같았다

  • Xml 파일을 사용할 것
  • Block 단위로 나눠서 저장할 것
  • Node의 종류는 여러가지를 사용할 수 있을 것

위 요구명세를 가지고 어떻게 구현할까 고민하다가, 문뜩 Interpret 방식이 생각이났다
사실 이 정도면 switch 쓰는게 coding시간이나 runtime 시간이나 더 효율적이었을 것이다
하지만 재미있어보이길래 무작정 시작했다


Parsing

우선 뭐라도 실행하기 위해서는 Xml 파일을 Parsing해야할 것이다
Parsing을 직접 구현할수도 있겠지만, 이런 복잡한 시스템은 라이브러리를 쓰는게 훨씬 효율적이다
내가 열심히 구현해봤자 느리면 느렸지, 빠를일은 없다

그래서 C#에서 Xml 파일을 어떻게 Parsing하는지 검색해보았다
그 결과 큰 맥락에서 3가지 방법이 가장 널리 사용되는 것 같아보였다

  • System.Xml.XmlDocument
  • System.Xml.Linq.XDocument
  • System.Xml.XmlReader

각 방법에는 장단점을 정리해보니 다음과 같았다

  • 편하다 / 메모리를 많이 먹을 수 있다, 느리다
  • 편하다, 빠르다 / DOM 기반이다
  • 빠르다 / 파일을 읽는 수준일뿐 그 이상의 처리를 하지는 않는다

XmlReader

처음에는 속도만 생각해서 XmlReader로 구현을 해보았다
Block 단위로 쪼갠 후, index 숫자만 가져와서 String 자체를 Dictionary에 저장하는 구조이다

// < Read and store value in string from given xml file >
// 
// 1. Read the xml file from the given path
// 2. Split elements by given deilimiter
// 3. Splitted elements are stored to dictionary(string)
// 
// - Key type of the dictionary is int(Int32)
// - If key_attribute is null, counter value works as a key
// - Conmments and whitespaces are ignored
//
// [ Performance ]
// 86mb, 56385 blocks, 2437666 elements: 5 seconds
// 4kb, 5 blocks, 30 elements: 3 milliseconds
//
static public Dictionary<int, string>
    Parse(TextAsset document, string delimiter, string keyAttribute = null)
{
    // Ignore comments and whitespaces
    XmlReaderSettings settings = new XmlReaderSettings()
    {
        IgnoreWhitespace = true,
        IgnoreComments = true,
        ConformanceLevel = ConformanceLevel.Fragment
    };

    // Create reader object for the given path
    XmlReader reader = XmlReader.Create(new StringReader(document.text), settings);
    reader.ReadToFollowing(delimiter);

    // Create dictionary object;
    Dictionary<int, string> blocks = new Dictionary<int, string>();
    int idx = 0;

    // Read to the end of file
    while (!reader.EOF)
    {
        if (keyAttribute != null && keyAttribute.Length != 0)
        {
            // Get index number from given key_attribute
            if (!Int32.TryParse(reader.GetAttribute(keyAttribute), out idx))
            {
                // Return when the file is at end
                if (!reader.Read())
                    break;

                // Print error when the index number isn't appropriate
                IXmlLineInfo info = (IXmlLineInfo)reader;
                Debug.Log("ERROR: Unable to read XML script\n" +
                    reader.BaseURI + "\n" +
                    info.LineNumber + "::" + info.LinePosition +
                    ": Unable to parse index to key type");
                return null;
            }
        }
        else
        {
            // Use counter when index attribute is not given
            idx++;
        }

        // Add the entire block of nodes to the dictionary
        blocks.Add(idx, reader.ReadOuterXml());
    }

    return blocks;
}

이렇게할 경우, Block과 Block 사이의 Parsing을 건너뛰면서 성능이 더 빠를줄 알았다
작은 파일에서는 XmlDocument가 7ms가 나오면서 위 방식이 3ms로 약 2배 빨랐다
그런데 86mb나 달하는 큰 파일에서는 XmlDocument가 4.5초로 오히려 1초 정도 더 빨랐다
아마도 string 전체를 저장하면서 발생하는 Overhead가 Parsing을 뛰어넘은게 아닌가 싶었다
아니면 XmlReader가 Reading하는 과정에서 Parsing하는 것 일수도 있다
대충 마소가 편하게 만들어놨고 쓰라고 하니 쓰는게 맞는거 같다
결국 다 갈아엎고, XDocument로 다시 만들었다

XDocument

XmlDocument로 안만든 이유는 Child Node를 어떻게 가져오는지 이해를 못해서이다
그리고 Value를 찍으니까 안에 존재하는 모든 Node의 Value가 한번에 찍혀나오길래 바로 갖다 버렸다
공부를 재대로 안한 문제도 있지만, 마소 Reference는 정말 봐도봐도 이해가 안가는게 더 이상 보기가 싫었다
대충 아래와 같이 코드를 짜서 돌려보니 잘 돌아간다

// < Read and store value in XElement from given xml file >
// 
// 1. Read the xml file from the given path
// 2. Split elements by given deilimiter
// 3. Splitted elements are stored to dictionary(XElement)
// 
// - Key type of the dictionary is int(Int32)
// - If keyAttribute is null, counter value works as a key
// - Conmments and whitespaces are ignored
//
// [ Performance ]
// 86mb, 56385 blocks, 2437666 elements: 4.5 seconds
// 4kb, 5 blocks, 30 elements: 10 milliseconds
//
static public Dictionary<int, IEnumerable<XElement>>
    Parse(TextAsset document, string delimiter, string keyAttribute = null)
{
    // Ignore comments and whitespaces
    XmlReaderSettings settings = new XmlReaderSettings()
    {
        IgnoreWhitespace = true,
        IgnoreComments = true,
        ConformanceLevel = ConformanceLevel.Fragment
    };

    XmlReader reader = XmlReader.Create(new StringReader(document.text), settings);

    int counter = 0;
    Dictionary<int, IEnumerable<XElement>> blocks =
        XDocument.Load(reader)
            .Descendants(delimiter)
            .ToDictionary(
                ele => keyAttribute != null && keyAttribute.Length != 0
                    ? (int)ele.Attribute(keyAttribute) : ++counter,
                ele => ele.Elements()
            );

    return blocks;
}

Node

Parser을 만들었으니, 이번에는 실행하기 위한 단위인 Node를 만들어보기로 했다
Node를 만들기 위해 어떠한 환경이 필요한지 다시 복습했다
대충 정리해보니, Shell 명령어 마냥 연속 실행이 필요해 보였고, 실행 후 State를 정의할 필요가 있어 보였다
중간중간 Code를 고치면서 좀 달라지긴 했지만, 결과적으로 생성된 Type과 State는 다음과 같다

State

  • Next: 다음 줄 실행
  • Move: 다음 block으로 이동
  • Suspend: Resume 명령이 내려올때까지 대기
  • Exit: 종료
  • Null: Block의 끝 / 오류 발생

Type

  • Single: 한줄만 실행
  • Parallel: 여러줄 연속 실행, Parallel 혹은 Passthrough가 아닐때 까지 계속 실행
  • Passthrough: Parallel 혹은 Passthrough가 아닌 다음줄까지 실행

위 Parallel과 Passthrough를 잘본다면 뭐가 다를까 싶을 수도 있다
저 둘을 굳이 나눈 이유는 Parallel 혹은 Passthrough 아닌 그 다음줄의 실행 여부이다
그러니까 Parallel 혹은 Passthrough를 계속 실행하다가 아닌 노드를 만났을때 말이다

예를 들어 coin을 지급하는 것과 동시에 문장을 출력하고 싶을 수 있다
이런 경우 Parallel 만으로는 그 효과를 만들기가 어려웠다
하지만 Passthrough type을 사용한다면 coin을 먼저 지급함과 동시에 문장이 출력된다

// < Single line of the xml document >
//
public abstract class XmlNode
{
    // Represent current state of the execution
    //
    // - next: execute next line on signal
    // - move: move to next block without stopping the execution
    // - suspend: dont execute next line until the interpreter has been resumed
    // - exit: exits the block
    // - null: null condition
    //
    public enum ExecState
    {
        Suspend,
        Next,
        Move,
        Exit,
        Null
    };

    // Represent type of the execution
    //
    // - single: execute single line
    // - parallel: execute all lines with type parallel and passthrough in a single execution
    // - paththrough: execute all lines with type parallel, passthrough
    //          and the node after passthrough node, in a single execution
    //
    public enum ExecType
    {
        Single,
        Parallel,
        Passthrough
    };

    // Type of the execution
    public ExecType type;
    
    // Initialize the node from given XElement and interpreter info
    public abstract void Init(XElement element, InterpreterInfo info);

    // Execute the process which this node have
    public abstract ExecState Execute();
}

이제 위 Base Class을 상속받아 Node 종류별로 정의하면 된다
하지만 여기서 한가지 문제점이 발생한다
우리가 가지고 있는 Node의 종류 정보는 string이다(Xml에 기록되어 있다고 가정)
그리고 Object를 생성하기 위해서는 Type이 필요하다
즉, String을 Type으로 바꿔주는 어떠한 메커니즘이 필요하다는 것이다

Creation

원래는 Dictionary, static creator 어쩌구해서 만들었었는데, 더 찾아보니 그럴필요가 없었다
그냥 Class의 이름만 알고 있다면 해당 class instance를 생성할 수 있기 때문이다
이게 처음봤을때는 어이가 없었는데, 몇번 쓰다보니 이제는 그냥 익숙해졌다

우선 class 이름을 정제할 필요가 있을 것이다
만약 대소문자를 틀려서 해당 node가 실행되지 않는다면 많이 슬프기 때문이다
그래서 아래와 같은 function으로 첫글자만 대문자, 나머지는 모두 소문자로 바꿨다

protected override string MapToClassName(string str)
{
    string name = str.ToString().ToLower();
    return char.ToUpper(name[0]) + name[1..];
}

이제 정제된 이름을 바탕으로 Assembly에서 type을 받아오면 된다
Assembly의 경우, 어차피 하나기 때문에 그냥 실행중인 Assembly 가져오면된다

private readonly Assembly assembly = Assembly.GetExecutingAssembly();

위 Assembly를 바탕으로 아래와 같이 Instance를 생성해주면 된다

private XmlNode _CreateNodeOfType(string typeName)
{
    // Get type of node
    string name = MapToClassName(typeName);
    var type = assembly.GetType(name);

    if (type == null)
    {
        Debug.LogError("ERROR: Unknown type " + name + "\n" +
            "Block::" + block.idx + ":" +
            block.Current().ToString());
        return null;
    }

    // Create node from type
    return (XmlNode)Activator.CreateInstance(type);
}

Block

Node를 다 만들고 나서 생각이 든게, Xml String을 담고 있는 Container가 없었다
그래서 XmlBlock을 만들기로 했다

별 생각없이 Dictionary에 저장된 IEnumerable<XElement>를 사용해서 만들었다
그동안 몰랐는데, IEnumerator의 경우, Singly-linked(?)라 Farword Only 방식이다
따라서 MoveBack() 같은건 없기에 나도 따로 만들지 않았다
지금 정리하면서 다시보니 elements가 왜 public인지 모르겠다(아마 잠결에 Coding해서 그런거 같다)

// < Block of the conversation >
//
public class XmlBlock
{
    public int idx;                                 // block index number
    public IEnumerable<XElement> elements;          // block elements
    public IEnumerator<XElement> iterator;          // current position of the block

    public XmlBlock(int idx, IEnumerable<XElement> elements)
    {
        this.idx = idx;
        this.elements = elements;
        this.iterator = elements.GetEnumerator();
        MoveNext();
    }

    // Move iterator to next
    // When iterator reaches end, false is returned
    //
    public bool MoveNext()
    {
        if (!iterator.MoveNext())
        {
            iterator = null;
            return false;
        }
        return true;
    }

    // Current element
    // If iterator has reached end, null is returned
    //
    public XElement Current()
    {
        if (iterator == null)
            return null;

        return iterator.Current;
    }
}

Interperter

마지막 남은 구현이 가장 중요하다고 할 수 있는 Interpreter이다
대충 Class 전체 한번에 다 적고 끝내려다가 너무 길어서 좀 끊어서 정리해놓으려 한다

Load

우선 실행하기 위해서는 Load해야 할 것이다
위에서 만들었던 XmlBlock Object를 Parameter로 받아서 저장했다

// Load block
// 
public void Load(XmlBlock block)
{
    this.block = block;
}

Execute

Load가 끝났으니 실행을 하자
우선 Suspend 상황에서의 Handling을 만들었다
원래는 warning만 띄우고 그냥 무지성 실행을 했었다
그런데 이게 좀 말이 안되는거 같아 그냥 Resume하기 전까지는 아예 막아버렸다
Suspend가 아니라면, Node를 생성하고 실행해주면된다

// Execute the current line(node)
// 
public XmlNode.ExecState Execute()
{
	if (suspended)
    {
    	// warn when user try to execute on suspension
        _UnexpectedInvocation();

		// This will not change current state
        return XmlNode.ExecState.Suspend;
	}

	// Get next node to execute
    Current = _GetNode();

	// Execute the line(node)
    var state = _Execute(Current);

	// Get status of the execution
	switch (state)
	{
    	case XmlNode.ExecState.Suspend:
        	// save suspended state
            suspended = true;
            suspendedNode = Current;
            break;

		case XmlNode.ExecState.Exit:
        	block = null;
            break;

		case XmlNode.ExecState.Next:
        case XmlNode.ExecState.Move:
        	break;
	}

	return state;
}

실행 함수 같은 경우에는 두가지 경우를 나눠서 구현했다
우선 현재 노드를 가져오는것까지는 동일한데, 이후 단일 실행인지 연속 실행인지를 구분해서 구현했다

private XmlNode.ExecState _Execute()
{
	XmlNode node = _GetNode();

	// Exit current block when node is null
    // This condition is executed on the end of the block
    if (node == null)
    	return XmlNode.ExecState.Exit;

	XmlNode.ExecState state = XmlNode.ExecState.Null;

	// Execute line(node) by its type
    switch (node.type)
    {
    	case XmlNode.ExecType.Single:
        	state = _ExecuteSingle(node);
            break;

		case XmlNode.ExecType.Parallel:
        	state = _ExecuteParallel(node);
            break;
	}

	return state;
}

우선 단일 실행의 경우 그냥 실행만 해주었다
대충 여기에 이거쓰면 안된다고 warning을 띄워주긴했는데 뭐 무시해도 된다

// Execute node which has type single
//
private XmlNode.ExecState _ExecuteSingle(XmlNode node)
{
	XmlNode.ExecState state = node.Execute();
    if (state == XmlNode.ExecState.Move)
    {
    	Debug.LogWarning("WARNING: execution state move does not effect" +
        	" on single type nodes. Block::" + block.idx.ToString());
	}
    return state;
}

연속 실행의 경우, Parallel 이후에 Passthrough까지 추가도입함으로 인해 많이 복잡해져버렸다..
원리 자체는 다음 Node를 미리까보고 맞으면 가져와서 실행하는걸 반복하게끔 만들었다
그리고 다음 Node를 미리까보는건 GetNode와 동일하지만, Remove단계만 제거하여 구현했다

 // Execute node which has type parallel
 //
 private XmlNode.ExecState _ExecuteParallel(XmlNode node)
 {
 	XmlNode current = node;
    XmlNode.ExecState state = XmlNode.ExecState.Null;
    XmlNode.ExecState peekState = XmlNode.ExecState.Next;

	// Execute nodes until single typed node appears
    while (peekState == XmlNode.ExecState.Next)
    {
    	state = current.Execute();
        
        // Force terminate conversation
        if (state == XmlNode.ExecState.Exit)
        	return state;

		// Peek next node whether it has samw type or not
        peekState = _PeekNextNodeParallel(current.type, out current);
	}

	// execute passthrough case
    if (current != null)
    {
    	state = current.Execute();
	}

	// Fix: terminate sequence on conversation end
    // This fix terminates the conversation when end node does not exist
    if (state == XmlNode.ExecState.Next && peekState == XmlNode.ExecState.Null)
        state = XmlNode.ExecState.Exit;

	return state;
}

private XmlNode.ExecState
	_PeekNextNodeParallel(XmlNode.ExecType type, out XmlNode node)
{
	node = _PeekNextNode(new XmlNode.ExecType[] {
    XmlNode.ExecType.Parallel, XmlNode.ExecType.Passthrough });

	// Return node when it has type of parallel or passthrough
    if (node != null)
    	return XmlNode.ExecState.Next;

	// return next node when the current node has type of passthrough
    if (type == XmlNode.ExecType.Passthrough)
    {
    	node = _GetNode();
	}
    else
    {
    	// Return null when its type is not passthrough
        node = null;
	}

	// notify that the parallel execution has to end
    return XmlNode.ExecState.Exit;
}

String으로 Store

여기까지만 만들고 끝낼까 고민도 했지만, 86mb parsing에 4.5초가 걸리는걸 보고 이건 아니다 싶었다
Block이 몇십~몇백개 정도 생길텐데, 이걸 한번에 다 Parsing하면 Loading이 길어지기 때문이다
그래서 생각해낸 방법이, 미리 Xml 파일을 Execute하기 좋게 짤라서 저장하고 실행하는 방식이다

그런데 여기서 다시 문제가 생긴다
XElement는 Xml 파일로 저장할 수는 있어도 이걸 가지고 ScriptableObject에 저장할 수는 없다
만약 여기서 XElement를 사용하고자 한다면, 두가지 정도의 선택지가 있다고 생각되었다

  • Block 전체를 Xml 파일로 저장 > Load시에 Block을 다시 Parsing 해야함
  • 각 Line을 따로 추출해서 Xml 파일로 저장 > 파일이 너무 많이 생성됨

그래서 걍 Line별로 String으로 바꿔서 Array에 저장하기로 했다
이렇게 되면, Block 전체를 Line 별로 끊어서 하나의 ScriptableObject에 저장할 수 있기 때문이다
굳이 이 방식을 선택한 이유는, 위 Benchmark 결과에서 4kb 파일 Parsing하는데 10ms가 걸렸기 때문이다
다시 말해 Block이 클 경우 Frame Drop이 발생할 우려가 있다는 뜻이고, 이걸 피하고 싶었기 때문이다
String을 다시 XElement로 Parsing하는 과정에서 Overhead가 발생하기야 하겠지만은, 이것이 최선이라고 생각되었다

다시 구현하려고 하니, 이미 Interperter까지 구현이 완료된 상태라, Forward Only 방식은 그대로 사용하였다
그리고 Elements는 string[]으로, Pointer는 int로 바꾸었다

// < Block of the conversation >
//
public class XmlBlock : ScriptableObject
{
    public int idx;             // block index number
    public string[] elements;   // block elements
    private int pointer;        // current position of the block

    public XmlBlock(int idx, string[] elements)
    {
        this.idx = idx;
        this.elements = elements;
        this.pointer = 0;
    }

    public XmlBlock()
    {}

    // Move iterator to next
    // When iterator reaches end, false is returned
    //
    public bool MoveNext()
    {
        if (++pointer >= elements.Length)
        {
            pointer = -1;
            return false;
        }
        return true;
    }

    // Current element
    // If iterator has reached end, null is returned
    //
    public string Current()
    {
        if (pointer == -1)
            return null;

        return elements[pointer];
    }
}

XElement to String

XElement 기반의 데이터를 string으로 바꾸는 작업 또한 필요하다
뭐 일일히 손으로 바꿀수도 있겠지만, 그건 개발자의 방식이 아니라고 생각해서 만들어봤다
Code 전체를 넣을까 하다가 대부분은 어차피 UnityEditor 관련 부분이라 변환하는 과정만 가져왔다
어떻게 보면 번거로운 작업이긴 하지만, Runtime을 최우선시하는 내 입장에서는 감당할 수 있는 부분이었다
물론 블록 30000개짜리를 직접 돌려보니 몇시간은 돌아갈 기세길래 그냥 중간에 껏다..
그래도 막상 스크립트가 모두 완성되고 돌려보니 1분 안쪽이라 참을만했다(190kb, 2000~2500줄 사이)

void CreateScriptableObject(int idx, IEnumerable<XElement> elements)
{
	XmlBlock block = CreateInstance<XmlBlock>();
    block.idx = idx;

	List<string> list = new();
    foreach (var line in elements)
    	list.Add(line.ToString());

	block.elements = list.ToArray();
    string fileName = storePath + "block_" + block.idx.ToString() + ".asset";

	// if such file exists, delete it
    if (File.Exists(fileName))
    	File.Delete(fileName);

	// create new file
    AssetDatabase.CreateAsset(block, fileName);
}

JIT & AOT

이제 정말로 끝내려고 했는데, 뭔가 부족해보였다
Interperter 방식의 한계인 Delay가 발생할 수 밖에 없는데, 분야가 게임인 만큼 조금이라도 더 줄여보고싶었다
이런 문제들은 내가 백날 고민해봐야 어차피 더 나은 방법들이 존재하기 마련이다
그래서 기억을 더듬던 중 안드로이드의 JIT와 AOT가 생각이났다
기억이 났으니 이제 적용을 해보자

JIT를 구현하기 위해 Caching 방식을 사용했다
Block이 Load되는 시점에 Compile이 시작되고, 비동기적으로 진행하여 미리 끝내놓는 것이다
여기서 Compile이란 String으로 Node를 생성하는 과정을 말한다
미리 완료된 Node는 대기 Queue에 들어가 실행을 기다린다
이제 대충 머리속에서 그림이 그려졌으니 구현을 해보자

우선 block의 끝에 도달하기 전이나 작업이 모두 완료되기 전까지는 동작을 해야한다
그리고 노드를 비동기로 생성한다
마지막으로 Task가 완료되었다면, Task Queue에서 Cached Queue로 옮겨주기만 하면 된다
비동기라고 해봤자 Task에 CreateNode 함수를 밀어넣는것 밖에는 없다

private IEnumerator _JITCompile()
{
	string node;
    while (block != null)
    {
    	node = block.Current();
        if (node == null && taskQueue.Count == 0)
        	break;

		// Add node creation task
        if (node != null)
        {
        	taskQueue.Enqueue(_CreateNodeAsync(node));
            block.MoveNext();
            yield return null;
		}

		// Move completed node to cached node queue
        if (taskQueue.Count > 0)
        {
        	Task<XmlNode> task = taskQueue.Peek();
            if (task.IsCompleted)
            {
            	cachedNode.Enqueue(task.Result);
                taskQueue.Dequeue();
			}
            yield return null;
		}
	}
}

private async Task<XmlNode> _CreateNodeAsync(string element)
{
	// Create Async call for _CreateNode
    return await Task<XmlNode>.Factory.StartNew(() =>
    {
    	return this._CreateNode(element);
	});
}

그런데 막상 돌려보니까 TaskPool에 밀어넣고 결과가져오는데 생기는 delay가 일반적인 delay보다 더 컷다
그래서 기껏 만들언놓은 Async는 그냥 안쓰고 전부다 sync로 돌렸다

AOT는 사실 어느 타이밍에 넣어야할지 모르겠어서 그냥 Load 시점에 때려박았다
뭐 미래의 내가 사용할때 Load는 미리 하게끔 설계하겠지..
위 JIT도 Load 시점에 시작되므로, 모두 합쳐진 Load 함수는 다음과 같다
여기서 JITCompile 함수 자체는 Coroutine에서 호출하였는데, 이로써 다소 비싸지만 확실한 Thread-safety를 챙길 수 있었다
그리고 혹시라도 Performance에 영향을 미칠까 두려워 Task Enqueue 과정과 Task Dequeue 과정을 다른 프레임에서 동작하게끔 하였다
이것은 Unity에서 제공하는 Coroutine을 따른 것인데, Unity는 Cororutine을 Loop한번에 한번 호출되기 때문이다

// Load block
//  - returns true on success
//  - returns false on fail
// 
public bool Load(XmlBlock block)
{
	if (block == null)
    {
    	Debug.LogError("ERROR: Unable to load block\n" +
        	"Input is null. Please check the input");
		return false;
	}

	this.block = block;
    block.Reset();
    cachedNode.Clear();
    taskQueue.Clear();

	for (int i = 0; i < AOTCompileCount; i++)
    {
    	cachedNode.Enqueue(_CreateNode(block.Current()));
        if (block.MoveNext() == false)
        	return true;
	}

	if (type == Type.JITCompile)
    	StartCoroutine(_JITCompile());

	return true;
}

이제 GetNode와 PeekNode도 위 cacheQueue를 확인하도록 세팅해주면 된다
우선적으로 cacheQueue를 확인한 후, 그 다음에 taskQueue를 확인하도록했다
만약 둘다에 존재하지 않을 경우, 그 자리에서 create하도록했다

private XmlNode _GetNode()
{
	// Execute cached node when it exists
    if (cachedNode.Count != 0)
    	return cachedNode.Dequeue();

	// Wait for node to complete creation
    if (taskQueue.Count != 0)
    { 
    	var task = taskQueue.Dequeue();
        task.Wait();
        return task.Result;
	}

	// Create node when non task is queued
    var current = _CreateNode(block.Current());
    block.MoveNext();
    return current;
}

private XmlNode _PeekNextNode()
{
	if (cachedNode.Count == 0)
    {
    	if (taskQueue.Count == 0)
        {
        	if (block.Current() == null)
            	return null;

			// Create node when non task is queued
            cachedNode.Enqueue(_CreateNode(block.Current()));
            block.MoveNext();
		}
        else
        {
        	// Wait for node to complete creation
            var task = taskQueue.Dequeue();
            task.Wait();
            cachedNode.Enqueue(task.Result);
		}
	}

	return cachedNode.Peek();
}

다 만들고 나니 그럭저럭 잘 동작한다
만들다보니 이미지 Load도 아까워서 sync인 경우에만 Init에서 해결보도록했다
그런데 생각해보니 Load가 async인건 아니라서 의미가 있나 싶긴하다
뭐 별로 상관없다고 판단된게 사진하나 Load하는데 20microsecond 밖에 안걸려서이다
수치 자체는 좀 의문스럽기는한데, Unity 내부에서 뭔가 엄청난 최적화가 있어서 이런것이라 믿고 넘겼다
관련글을 찾아봤는데, Resources 폴더 내부에 존재하는 Asset은 모두 binary로 박혀서 그렇다고는 한다..
이 부분도 사진 용량이 커져서 프레임 드랍 생기면 고칠 생각이다

사실 JIT나 AOT라는 수식을 갖다 붙이기 민망할 정도이긴하다(정확히 말해 이게 맞는 개념인지도 모르겠다..)
하지만 원래 그럴듯한 용어 갖다 붙이면 그럴듯해 보이기 마련이므로 한번 붙여보았다
그래도 Interpret 방식을 사용할 때보다 대략 30%의 성능향상이 있다고 판단되어 만족스러웠다

이걸 원래 Open source로 풀려고했는데 프로젝트가 진행됨에따라 프로젝트와의 연관성이 너무 높아졌다..
이걸 어느부분만 떼서 갖고 나올까 고민하다가 그냥 포기했다
이걸 나중에 정리해서 github에 올릴지는 모르겠지만, 당장은 요청이 없다면 별로하고 싶다는 생각이 안든다

profile
하고싶은거 하는 사람

0개의 댓글