2015년 10월 11일 일요일

자막 smi <-> srt 변환하는 유틸리티

영상을 인코딩하다 보면 자막이 smi이 필요할때도 있고 srt가 필요할때도 있는데요,

서로 변환시키는 프로그램은 손쉽게 구할수 있는데 문제는 한글 자막의 경우,

euc-kr과 utf-8을 혼용해서 사용하는 경우가 많아서 변환한 후에 보면 글자가 모두 깨져보이게 되는 문제가 있다는 거죠.

잠깐 간단하게 설명하자면...

한글을 표현하는 방식에는 조합형과 완성형 방식이 있는데,

조합형(utf-8) - 자음과 모음을 초성, 중성, 종성으로 구분하여 이를 조합해서 표현하는 방식

완성형(euc-kr) - 이미 완성된 문자들을 나열해 놓고 필요한것을 갖다 쓰는 방식

위 두가지로 나눌 수 있어요.

잠깐 생각해보면 조합형이 훨씬 이점이 많겠죠?

그래서 개발자들은 조합형을 원하지만 윈도우는 완성형을 사용해요.

앞으로는 점점 완성형보다 조합형으로 사용될 듯 하지만 현재로써는 이 두가지가 혼용되기 때문에 자막을 변환했을때 어떤것은 제대로 되지만 어떤것은 깨지게 되죠.

지금 소개해드리는 유틸리티는 변환시 모든 설정들을 다 바꿀 수 있어서

조합형 <-> 완성형 <-> smi <-> srt

하는 과정을 한번에 처리하는 유틸리티입니다.

http://blog.myhyuny.com/entry/SMI-SRT-%EC%9E%90%EB%A7%89-%EB%B3%80%ED%99%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-06

위 주소는 이 유틸리티를 개발한 개발자의 홈페이지이고 이곳에서 프로그램을 다운 받을 수 있어요.

변환 후에 자막이 깨져있다면 위에서 설명한 조합형, 완성형의 문제일 확률이 큽니다.

이럴때는 유틸리티의 아웃풋을 Unicode(UTF-8), Korean(Windows, DOS)로 설정해서 해보세요.

사용법은 변환하고 싶은 자막을 유틸리티 프로그램에 드래그 앤 드랍하면 됩니다.

2014년 1월 28일 화요일

unity3D, 다른 방법으로 저장하기

유니티 오브젝트로 저장하는 방법이 있는데요.

이 방법은 생각보다 많이 까다로울 수 있기때문에 오브젝트가 갖는 값을 유추해 볼 수 있는 클래스를 새로 생성한 후 이 클래스의 값을 저장하는 방법을 사용합니다. 

예를 들어, RPC 호출을 통해서 여러개의 AnimationStates를 전달하고자 할때, AnimationStates를 대신할 수 있는 클래스를 만들고 이 값을 전달하는 것이죠.

아래의 예제는 어떤 클래스라도 문자열로 변환할 수 있게 해주는 방법을 보여줍니다. 
이것을 이용해서 모든 파라미터 값을 RPC를 통해 보낼 수 있습니다. 
==============================================================================

using UnityEngine;
using System.Collections;
using System.Collections.Generic;   // 리스트를 쓰기 위해 추가합니다.

// BinaryFormatter를 사용하기 위해서는 반드시 아래의 네임스페이스를 추가해줘야 해요.
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public class csTest : MonoBehaviour 
{
    // 이렇게 클래스를 만들고
    [Serializable]           // 저장하기 위해서 있어야 합니다.
    public class ScoreEntry
    {
        public string name;
        public int score;
    }

    // 만든 클래스를 리스트에 담아서 관리하면 마치 테이블처럼 사용할 수 있습니다. 
    public List<ScoreEntry> highScore = new List<ScoreEntry>();

    [RPC]
    void ReceiveHighScores(byte[] highScores)
    {
        var binaryFormatter     = new BinaryFormatter();
        var memoryStream        = new MemoryStream(highScores);

        // 받은 데이터를 바이트 배열로 변환합니다.
        var otherPlayerScores   = (List<ScoreEntry>)binaryFormatter.Deserialize(memoryStream);
    }

    void UpdateScores(byte[] highScores)
    {
        var binaryFormatter     = new BinaryFormatter();
        var memoryStream        = new MemoryStream();

        // highScores를 바이트 배열로 변환해서 저장합니다.
        binaryFormatter.Serialize(memoryStream, highScores);

        // 그리고 다른 유저에게 전달해주는거죠.
        networkView.RPC("ReceiveHighScores", RPCMode.Others, memoryStream.GetBuffer());
    }
}

==============================================================================
RPC 역시 바이트 배열을 보낼 수 있습니다. 
이 값은 BinaryFormatter를 이용해서 우리가 원하는 값으로 바로 변환할 수 있도록 도와줍니다. 
문자열로부터 변환하는 시간을 줄일 수 있어서  더 좋다고 할 수 있습니다.  

웹으로 전달할때는 WWWForm을 이용합니다. 

또는 Convert.ToBase64String()을 이용해서 할 수도 있습니다.
사용하는 서버에 따라 적절하게 하면 될 것 같네요.

파일로 저장하는 법은 Application.persistentDataPath를 이용해서 하되, MemoryStream 대신 FileStream을 사용하면 됩니다. 
==============================================================================

using UnityEngine;
using System.Collections;
using System.Collections.Generic;   // 리스트를 쓰기 위해 추가합니다.

// BinaryFormatter를 사용하기 위해서는 반드시 아래의 네임스페이스를 추가해줘야 해요.
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public class csTest : MonoBehaviour 
{
    // 이렇게 클래스를 만들고
    [Serializable]           // 저장하기 위해서 있어야 합니다.
    public class ScoreEntry
    {
        public string name;
        public int score;
    }

    // 만든 클래스를 리스트에 담아서 관리하면 마치 테이블처럼 사용할 수 있습니다. 
    public List<ScoreEntry> highScore = new List<ScoreEntry>();

    void SaveScores()
    {
        var binaryFomatter  = new BinaryFormatter();

        // 저장할 파일의 위치를 정해주고 만들어줍시다.
        var fileStream      = File.Create(Application.persistentDataPath + "/highScores.dat");

        binaryFomatter.Serialize(fileStream, highScore);

        // 작업이 끝나면 닫아주는 것을 잊지 마세요
        fileStream.Close();
    }

    void Start()
    {
        // 시작할때 저장한 파일을 불러옵니다.

        // 데이터가 비어있지 않다면
        if (File.Exists(Application.persistentDataPath + "/highScores.dat"))
        {
            var binaryFormatter     = new BinaryFormatter();

            // 불러올 파일 경로와 이름이 틀리지 않도록 주의하시구요.
            var fileStream          = File.Open(Application.persistentDataPath + "/highScores.dat", FileMode.Open);

            highScore       = (List<ScoreEntry>)binaryFormatter.Deserialize(fileStream);

            // 작업이 끝나면 닫아주는 것을 잊지 마세요
            fileStream.Close();
        }
    }
}

==============================================================================







unity3D, 좀 더 복잡한 데이터 저장하기

이전 포스팅에서 간단한 데이터를 저장하는 법을 봤는데요,

만약에 저장할 데이터가 간단하지 않고 좀 복잡하다...라고 한다면 아래처럼 테이블을 만들어서 저장합니다.
==============================================================================

using UnityEngine;
using System.Collections;
using System.Collections.Generic;   // 리스트를 쓰기 위해 추가합니다.

public class csTest : MonoBehaviour 
{
    // 이렇게 클래스를 만들고
    [Serializable]           // 저장하기 위해서 있어야 합니다.
    public class ScoreEntry
    {
        public string name;
        public int score;
    }

    string currentPlayerName;
    int score;

    // 만든 클래스를 리스트에 담아서 관리하면 마치 테이블처럼 사용할 수 있습니다. 
    public List<ScoreEntry> highScore = new List<ScoreEntry>();

    void AddData()
    {
        // 이렇게 새로운 데이터를 추가해주고
        highScore.Add(new ScoreEntry { name = currentPlayerName, score = score });
    }

    void OnGUI()
    {
        // 이렇게 화면에 랭킹 정보를 보여주면 됩니다. 
        foreach (ScoreEntry nowScore in highScore)
        {
            GUILayout.Label(string.Format("{0} : {1:#, 0}", nowScore.name, nowScore.score));
        }
    }
}

==============================================================================
이렇게 정보를 테이블처럼 만든 후에는 BinaryFormatter를 사용해서 저장할 수 있습니다. 

BinaryFormatter는 '파라미터를 갖지 않는 생성자'를 갖는 클래스라면 어떤 클래스도 바이트 배열로 변환하고, 그것을 다시 문자열 값으로 변환할 수 있습니다. 

문자열 값으로 변환이 가능하다면 이전 포스팅에서 설명한 PlayerPrefs에 저장할 수 있게 되겠죠.
==============================================================================

using UnityEngine;
using System.Collections;
using System.Collections.Generic;   // 리스트를 쓰기 위해 추가합니다.

// BinaryFormatter를 사용하기 위해서는 반드시 아래의 네임스페이스를 추가해줘야 해요.
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public class csTest : MonoBehaviour 
{
    // 이렇게 클래스를 만들고
    [Serializable]           // 저장하기 위해서 있어야 합니다.
    public class ScoreEntry
    {
        public string name;
        public int score;
    }

    // 만든 클래스를 리스트에 담아서 관리하면 마치 테이블처럼 사용할 수 있습니다. 
    public List<ScoreEntry> highScore = new List<ScoreEntry>();

    void SaveScores()
    {
        var binaryFormatter     = new BinaryFormatter();
        var memoryStream        = new MemoryStream();

        // score를 바이트 배열로 변환해서 저장합니다.
        binaryFormatter.Serialize(memoryStream, highScore);

        // 그것을 다시 한번 문자열 값으로 변환해서 
        // 'HighScore'라는 스트링 키값으로 PlayerPrefs에 저장합니다.
        PlayerPrefs.SetString("HighScore", Convert.ToBase64String(memoryStream.GetBuffer()));
    }

    void Start()
    {
        // 'HighScore' 스트링 키값으로 데이터를 가져옵니다.
        var data = PlayerPrefs.GetString("HighScore");

        if (!string.IsNullOrEmpty(data))
        {
            var binaryFormatter     = new BinaryFormatter();
            var memoryStream        = new MemoryStream(Convert.FromBase64String(data)));

            // 가져온 데이터를 바이트 배열로 변환하고
            // 사용하기 위해 다시 리스트로 캐스팅해줍니다.
            highScore       = (List<ScoreEntry>)binaryFormatter.Deserialize(memoryStream);
        }
    }
}

==============================================================================







unity3D, 데이터 저장하기

게임을 개발하다 보면 유저가 어디까지 진행했는지, 유저의 캐릭터가 얼만큼 성장했는지 등등을 저장해야 하는 시점이 오게 됩니다. 

가장 기본적으로 저장하는 방법은 PlayerPrefs을 사용해서 저장하는 방법이 있습니다. 

이것은 기본 데이터를 문자열키를 이용해서 같이 저장하게 해주는것인데요, 저장할 데이터가 작고 간단하다면 이 방법을 사용하는것이 가장 무난하고 좋습니다.

사용법은 아래의 코드를 살펴보세요.
using UnityEngine;
using System.Collections;

public class csTest : MonoBehaviour 
{
    int currentScore;
    int highScore;

    string currentPlayerName;
    string highScorePlayerName;

    void SaveData()
    {
        // 이런식으로 스트링키를 이용해서 값을 저장해줍니다.
        PlayerPrefs.SetInt("Score", currentScore);
        PlayerPrefs.SetInt("HighScore", currentScore);
        PlayerPrefs.SetString("HighScoreName", currentPlayerName);
    }

    void GetData()
    {
        // 필요한 데이터를 불러올때는 이렇게 키값으로 불러오게 되죠.
        highScore               = PlayerPrefs.GetInt("HighScore");
        highScorePlayerName     = PlayerPrefs.GetString("HighScoreName", "N/A");
    }
}

이렇게 구성후 정적클래스를 만들어서 관리하거나 싱글턴 클래스를 만들어서 관리할 수 있습니다. 

그런데 정적클래스로 만들면 인스펙터에서는 값을 조절할 수 없다는 단점이 있죠. 

인스펙터와 코드 모두에서 값을 조절하고 싶은 경우엔 아래의 예제 코드처럼 DontDestroyOnLoad 함수를 이용해서 가능하게 만들 수 있습니다. 
using UnityEngine;
using System.Collections;

public class csTest : MonoBehaviour 
{
    public static csTest current;

    public Transform somePrefabs;

    public int score;
    public string playerName;

    void Awake()
    {
        if (current != null && current != this)
        {
            Destroy(gameObject);
        }
        else
        {
            DontDestroyOnLoad(gameObject);
            current = this;
        }
    }
}

// 접근할때는 요렇게 접근합니다.
Transform x = csTest.current.somePrefabs;










2014년 1월 27일 월요일

C# 메모리 관리 기법 - WeakReference와 IDisposable

상식적으로 가비지 컬렉터가 작동된다면 메모리가 늘어나는 것에 대한 걱정은 없어야 하겠지만 실제로는 메모리가 늘어나기도 합니다. 
C#에서는 메모리 해제 명령이 없어서 메모리 릭이 발생하게 되면 당황스러울 수 밖에 없죠.

그러면 어떤 경우에 메모리가 늘어나게 될까요?

가비지는 더 이상 참조가 없는 메모리를 뜻합니다. 
메모리가 어디선가 늘어나고 있다는 뜻은 결국 어디선가 의도하지 않은 참조가 일어나고 있어서 가비지화 되지 못하고 계속 누적되고 있다는 말이 되겠죠.

어떤 경우에 그런 일이 발생할까요?


이렇게 캐릭터 매니져가 세개의 캐릭터를 만들었습니다. 


그리고 캐릭터의 위치를 보여주는 객체가 캐릭터 매니저에 접근해서 캐릭터를 모두 참조하게 됩니다. 


그 후에 필요 없어진 캐릭터를 캐릭터 매니저가 삭제하게 됩니다. 
그러면 가비지가 될것이라 생각하겠지만 여전히 지워진 캐릭터를 디스플레이 캐릭터 포지션 객체가 참조하고 있기때문에 가비지가 되지 않습니다. 
여전히 메모리에 남아서 메모리를 증가시키게 되죠.

이런 일을 만들지 않으려면 위의 경우처럼 의도치 않은 참조를 지워줘야 합니다. 

WeakReference는 가비지 컬렉션에 의한 객체 회수를 허용하면서 객체를 참조하게 됩니다. 
이때, 인스턴스를 참조하려면 WeakReference.Target을 사용하는데 해당 인스턴스가 가비지 컬렉터에게 회수되면 null값을 반환하게 됩니다. 
public class Sample
{
    private class Fruit
    {
        public Fruit(string name) 
        { 
            this.Name = name; 
        }

        public string Name 
        { 
            private set; 
            get; 
        }
    }

    public static void TestWeakRef()
    {
        Fruit apple     = new Fruit("Apple");
        Fruit orange    = new Fruit("Orange");
           
        Fruit fruit1    = apple;   // 강한 참조

        // WeakReference를 이용 
        WeakReference fruit2    = new WeakReference(orange); 

        Fruit target;
           
        target          = fruit2.Target as Fruit;

        // 이 경우 결과는 애플과 오렌지가 나오게 됩니다.
        Console.WriteLine(" (1) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1.Name, target == null ? "" : target.Name);

        // 모두 참조하지 않도록 null값을 넣어줍니다.
        apple   = null;
        orange  = null;

        // 가비지 컬렉터를 작동시킨다
        System.GC.Collect(0, GCCollectionMode.Forced);
        System.GC.WaitForFullGCComplete();

        // 그 후 같은 방법으로 결과를 확인해보면
        // fruit1과 fruit2의 값을 바꾼 적은 없지만, fruit2의 결과가 달라집니다.
        target          = fruit2.Target as Fruit;

        // 결과는 애플만 나오게 된다.
        // 오렌지는 가비지 컬렉터에게 회수되버렸기때문입니다
        Console.WriteLine(" (2) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1 == null ? "" : fruit1.Name,
            target == null ? "" : target.Name);
    }
}
매니저처럼 객체를 직접 생성하고 삭제하는 모듈이 아닌 이상 가능하다면 WeakReference를 사용하시는 것이 좋습니다. 
그렇게 해서 의도치 않은 참조로 인해 메모리가 누적되게 되는 실수를 방지할 수 있습니다. 

주의할 것은 WeakReference.Target의 값을 보관하면 안된다는 것입니다. 
이값을 보관하게 되면 강한 참조가 일어나 가비지 컬렉터가 회수를 하지 않게 됩니다. 

C#은 메모리를 원하는 시점에 정확히 해제하는 것이 불가능합니다만 C/C++처럼 원하는 시점에 삭제하기를 희망한다면 IDisposable을 사용할 수 있습니다. 

이것은 관리되지 않는 메모리(리소스)들을 해제할때 사용하는 인터페이스입니다.
서로 다른 type의 객체라 하더라도 모든 type의 메모리를 정리할 수 있는 장점이 있습니다. 

WeakReference와 IDisposable을 같이 사용해서 원하는 시점에 메모리를 해제할 수 있습니다. 
아래의 예제는 Disposable 인터페이스를 상속받아 구현되었습니다.
namespace MyApp
{
    public class SampleChar : IDisposable
    {
        private IRenderObject m_Render = Renderer.CreateRenderObject();

        public void Dispose()
        {
            // 이후에 더이상 업데이트가 되지 않도록 여기서 제거하면
            SampleCharManager.Remove(this);

            m_Render = null;
        }

        public bool isRemoved 
        { 
            get 
            { 
                return m_Render == null; 
            } 
        }

        public void Render()
        {
            // 화면에서 그려지지 않도록 합니다. 
            if (m_Render == null) return;
        }

        public void Update() { }
    }
}
IRenderObject 인터페이스 구현은 아래와 같습니다.
namespace MyApp
{
    public interface IRenderObject
    {
        void Render();
    }

    public static class Renderer
    {
        public static IRenderObject CreateRenderObject()
        {
            // IRenderObject를 상속받은 더미 객체
            return new DumyRenderObject(); 
        }
    }
}
아래의 코드는 캐릭터 매니저가 등록된 캐릭터들을 일괄적으로 업데이트 시키고 렌더링하는 코드입니다. 
namespace MyApp 
{
    static class SampleCharManager
    {
        private static List<samplechar> m_list = new List<samplechar>();

        public static void Update()
        {
            foreach (SampleChar obj in m_list) 
            {
                obj.Update();
            }
        }

        public static void Render()
        {
            foreach (SampleChar obj in m_list)
            {
                obj.Render();
            }
        }

        public static void Add(SampleChar obj)
        {
            m_list.Add(obj); 
        }
        
        public static void Remove(SampleChar obj)
        {
            m_list.Remove(obj);
        }
    }
}
그 다음 디버깅을 위한 캐릭터의 위치를 표시하는 코드입니다. 
namespace MyDebug
{
    static class DisplayCharInfo
    {
        private static List<weakreference> m_list            = new List<weakreference>();
        private static Queue<weakreference> m_removeQueue    = new Queue<weakreference>();

        public static void Update()
        {
            foreach (WeakReference item in m_list)
            {
                MyApp.SampleChar obj = (item.Target != null) ? item.Target as MyApp.SampleChar : null;

                if (obj == null || obj.isRemoved)
                {
                    m_removeQueue.Enqueue(item);
                }
                // 삭제되지 않은 캐릭터의 정보만을 표시합니다.
                else 
                { 
                    /* 캐릭터 정보 표시 */ 
                }
            }

            // 삭제된 캐릭터는 목록에서 지워줍니다
            while(m_removeQueue.Count > 0)
            {
                WeakReference item = m_removeQueue.Dequeue();
                m_list.Remove(item);
            }
        }

        public static void Add(MyApp.SampleChar obj)
        {
            // WeakReference를 이용해 참조하도록 해줍니다.
            // SampleCharManager에서 캐릭터를 삭제하더라도 안전하게 가비지가 회수됩니다.
            m_list.Add(new WeakReference(obj));
        }
    }
}
Unity3D는 모노에서 관리하는 메모리와 엔진에서 관리하는 메모리로 나뉩니다. 
둘 다, 메모리가 부족하면 heap에 메모리를 할당하는데 이렇게 늘어난 메모리는 줄어들지 않습니다. 
heap에서 메모리가 재사용되므로 무작정 늘어날 일은 없지만 가비지가 늘어날수록 최대 메모리 사용량이 늘어나므로 가능하면 가비지가 덜 생성되록 코드를 짜는 것이 중요하겠죠.
메모리는 한번에 잡는 것이 좋고, caching이나 memory pool을 사용하는것이 좋습니다.