레이블이 Unity인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Unity인 게시물을 표시합니다. 모든 게시물 표시

2025년 3월 5일 수요일

Using OpenAI's ChatGPT with Unity C# via API // Unity로 ChatGPT 챗봇 만들기 (API 이용)

# Unity C# 으로 구동하는 챗봇 코드
내가 쓸려고 간단히 만든 것을 기록용으로 공개한다.
아래 내용을 유니티에 붙여 넣는다.

1. 빈 gameobject 를 하나 생성한다.
2. new component 로 ChatbotManager 라는 스크립트를 생성해서 1에 붙인다.
3. 코드에디터로 아래 내용을 복사해서 2의 파일에 붙이고 저장한다.
4. 유니티 내 StreamingAssets 경로에 auth.json 을 생성한다.
 

    /// StreamingAssets 폴더의 auth.json 파일에서 API 키를 로드합니다.
    /// 예시 파일 형식: { "api_key": "your_api_key_here" }


5. 유니티로 돌아와서 플레이 해본다. log 창에 표시가 된다.

# 사용 법
StartNewConversation() : 새로운 대화 스레드 시작
SendUserMessage() : 내 대사 전달

# 메인 코드


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

[System.Serializable]
public class ChatMessage {
    public string role;
    public string content;
}

[System.Serializable]
public class ChatCompletionRequest {
    public string model;
    public List messages;
    public int max_tokens;
    public float temperature;
}

[System.Serializable]
public class ChatCompletionChoice {
    public ChatMessage message;
    public string finish_reason;
    public int index;
}

[System.Serializable]
public class ChatbotResponse {
    public string id;
    public string @object;
    public int created;
    public string model;
    public List choices;
}

[System.Serializable]
public class AuthData {
    public string api_key;
}

public class ChatbotManager : MonoBehaviour
{
    // Inspector에 노출되지 않도록 private const로 선언하여 잘못된 값이 덮어씌워지지 않게 합니다.
    private const string apiUrl = "https://api.openai.com/v1/chat/completions";
    public string model = "gpt-4o-mini";
    // public string model = "gpt-4o-mini"; // "o3-mini"; //"gpt-3.5-turbo"
    
    // 응답 속도 개선을 위한 추가 파라미터
    [Tooltip("응답 생성 시 최대 토큰 수를 제한합니다. 낮을수록 응답이 빨라집니다.")]
    public int maxTokens = 150;
    
    [Tooltip("응답의 창의성을 조절합니다. 0에 가까울수록 결정적이고 빠른 응답이 생성됩니다.")]
    [Range(0f, 1f)]
    public float temperature = 0.3f;
    
    private string apiKey;
    private List messages = new List();
    
    [TextArea(3, 10)]
    [Tooltip("챗봇에게 전달할 초기 시스템 인스트럭션입니다. 챗봇의 역할과 행동 방식을 정의합니다.")]
    public string initialInstruction = "안녕하세요, 챗봇과 대화를 시작합니다. 아래 instruction을 참고하세요.";

    [Tooltip("대화 기록에 유지할 최대 메시지 수입니다. 너무 많은 메시지는 응답 속도를 늦출 수 있습니다.")]
    public int maxMessageHistory = 10;
    
    [Tooltip("대화 기록을 저장하는 배열입니다. 인스펙터에서 확인할 수 있습니다.")]
    [SerializeField] private ChatMessage[] messageHistory;

    void Start()
    {
        LoadApiKey();
        StartNewConversation();
    }

    /// 
    /// StreamingAssets 폴더의 auth.json 파일에서 API 키를 로드합니다.
    /// 예시 파일 형식: { "api_key": "your_api_key_here" }
    /// 
    private void LoadApiKey()
    {
        string authFilePath = System.IO.Path.Combine(Application.streamingAssetsPath, "auth.json");

        if (System.IO.File.Exists(authFilePath))
        {
            try
            {
                string jsonContent = System.IO.File.ReadAllText(authFilePath);
                Debug.Log("로드된 auth.json 내용: " + jsonContent);

                if (!jsonContent.Trim().StartsWith("{") || !jsonContent.Trim().EndsWith("}"))
                {
                    Debug.LogError("auth.json 파일이 올바른 JSON 형식이 아닙니다. 올바른 형식: {\"api_key\":\"your_api_key_here\"}");
                    return;
                }

                AuthData authData = JsonUtility.FromJson(jsonContent);
                if (authData != null && !string.IsNullOrEmpty(authData.api_key))
                {
                    apiKey = authData.api_key;
                    Debug.Log("API 키가 성공적으로 로드되었습니다.");
                }
                else
                {
                    Debug.LogError("auth.json 파일에서 API 키를 찾을 수 없습니다.");
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError("auth.json 파일 파싱 중 오류 발생: " + e.Message);
            }
        }
        else
        {
            Debug.LogError("auth.json 파일을 찾을 수 없습니다. 경로: " + authFilePath);
        }
    }

    /// 
    /// 새 대화를 시작합니다. 기존 대화 기록을 초기화하고 초기 instruction 메시지를 추가합니다.
    /// 
    public void StartNewConversation()
    {
        messages.Clear();

        ChatMessage systemMessage = new ChatMessage { role = "system", content = initialInstruction };
        messages.Add(systemMessage);
        
        // 메시지 배열 업데이트
        messageHistory = messages.ToArray();

        Debug.Log("새 대화가 시작되었습니다. 초기 instruction이 추가되었습니다.");
    }

    /// 
    /// 사용자 메시지를 추가하고 API로 전송합니다.
    /// 
    /// 사용자 입력 메시지
    public void SendUserMessage(string userMessageContent)
    {
        ChatMessage userMessage = new ChatMessage { role = "user", content = userMessageContent };
        messages.Add(userMessage);
        
        // 메시지 수가 제한을 초과하면 오래된 메시지 제거 (시스템 메시지는 유지)
        TrimMessageHistory();
        
        StartCoroutine(SendRequestCoroutine());
    }

    /// 
    /// 대화 기록이 너무 길어지지 않도록 오래된 메시지를 제거합니다.
    /// 
    private void TrimMessageHistory()
    {
        // 시스템 메시지를 제외한 메시지 수가 maxMessageHistory를 초과하면 오래된 메시지 제거
        if (messages.Count > maxMessageHistory + 1) // +1은 시스템 메시지 때문
        {
            // 시스템 메시지는 항상 인덱스 0에 있다고 가정
            int excessMessages = messages.Count - maxMessageHistory - 1;
            messages.RemoveRange(1, excessMessages); // 시스템 메시지 다음부터 제거
        }
        
        // 메시지 리스트를 배열로 변환하여 인스펙터에서 확인할 수 있게 함
        messageHistory = messages.ToArray();
    }

    /// 
    /// 대화 전체를 JSON으로 직렬화하여 API에 POST 요청을 보내고, 응답을 받아 처리합니다.
    /// 
    IEnumerator SendRequestCoroutine()
    {
        if (string.IsNullOrEmpty(apiKey))
        {
            Debug.LogError("API 키가 설정되지 않았습니다. API 요청을 보낼 수 없습니다.");
            yield break;
        }

        ChatCompletionRequest requestObj = new ChatCompletionRequest
        {
            model = model,
            messages = messages,
            max_tokens = maxTokens,
            temperature = temperature
        };
        string jsonData = JsonUtility.ToJson(requestObj);
        Debug.Log("요청 JSON: " + jsonData);

        // UnityWebRequest 생성 시 POST 메서드를 직접 지정합니다.
        UnityWebRequest request = new UnityWebRequest(apiUrl, "POST");
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonData);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");

        // API 키에서 불필요한 문자를 제거합니다.
        string sanitizedApiKey = apiKey.Trim().Replace("\n", "").Replace("\r", "").Replace("\t", "").Replace("\"", "");
        request.SetRequestHeader("Authorization", "Bearer " + sanitizedApiKey);

        Debug.Log("API URL: " + apiUrl);
        Debug.Log("요청 메서드: " + request.method);

        yield return request.SendWebRequest();

        Debug.Log("응답 상태 코드: " + request.responseCode);

        if (request.result == UnityWebRequest.Result.Success)
        {
            string responseText = request.downloadHandler.text;
            Debug.Log("응답 JSON: " + responseText);

            ChatbotResponse response = JsonUtility.FromJson(responseText);
            if (response != null && response.choices != null && response.choices.Count > 0)
            {
                ChatMessage assistantMessage = response.choices[0].message;
                messages.Add(assistantMessage);
                // 메시지 배열 업데이트
                messageHistory = messages.ToArray();
                Debug.Log("Assistant: " + assistantMessage.content);
            }
        }
        else
        {
            Debug.LogError("API 요청 오류: " + request.error);
            Debug.LogError("응답 내용: " + request.downloadHandler.text);
        }
    }
}



내가 도움 받은 만큼 다른 누군가에게 도움이 되기를. 끝.

2018년 3월 26일 월요일

유니티 리소스 폴더의 폰트 불러오기

코드 조각 기록

아래 폰트 경로에 존재하는 폰트 파일을 모두 불러와서 _lFontList 라는 리스트에 넣는다.
Assets/Recources/Fonts/



        UnityEngine.Object[] fileInfo = Resources.LoadAll("Fonts", typeof(Font));

        UnityEngine.Debug.LogWarning(fileInfo.Length);

        foreach (var file in fileInfo)
        {
            _lFontList.Add((Font)file);
        }

2018년 2월 13일 화요일

쉽게 만드는 UI 스크롤뷰

개인적으로 uGUI 의 사용을 늘리고 있다.
NGUI 는 널리 쓰이는 서드파티 에셋이지만, 안드/아이폰 이외의 작업에는 아무래도 호환성 문제가 생기고,
유니티 최신 엔진 업데이트시에 가끔 문제를 일으켰던 기억이 있기 때문이다.
예전에 안드로이드TV를 지원하게 만들다가, NGUI 는 KEY NAVIGATION 지원이 거의 전무하다 시피했던 기억이 난다.

개인 프로젝트는 최대한 uGUI 를 이용해서 만들고 있다.
SizeFitter 라든가 Layout 세팅 관련 컴포넌트 스크립트가 예상보다 많이 준비되어 있어서 간단히 만들 수 있다.

오늘 초보자인 어떤 분이 GUI 를 만들다가 힘들어서 질문을 해왔다.
조금 생각해보니 처음 만드는 사람이라면 확실히 어려울 수 있을거 같다는 생각이 들어서
따라하기 스타일로 아래에 Scroll View 만들기를 정리해보려고 한다.

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

아래와 같은 하단 좌우 스크롤뷰 UI를 만들어보자.


1. Unity 에서 Hierarchy 메뉴에 우클릭 > UI > Canvas 를 하나 만들어서 적당히 세팅해준다. (위치와 크기의 세팅 등)

2. Canvas에서 우클릭해서 UI > Scroll View 를 클릭해서 추가해준다.



3. 생성된 Scroll View 의 사이즈와 위치를 정리해준 후, Inspector 에 아래 내용대로 체크/언체크 해준다.


좌우로만 움직일 스크롤 뷰라서 Horizontal 만 체크한다.


4. 컨텐츠 오브젝트의 제어 콤포넌트 추가. (여기가 어렵다.)
Hierarchy 에 Content 를 선택해 보면 Rect Transform 이외에 아무런 컴포넌트가 없다.
여기에 필요한건 Child 오브젝트를 추가할 경우 "해당 오브젝트 들의 레이아웃 설정"과 "사이즈를 맞춰주는 기능"이 필요하다.
예전에는 직접 코드를 짰는데, 찾아보니 uGUI 에 이미 준비되어 있다.


5. Content 오브젝트에 컴포넌트를 추가한다. (제일 중요한 부분)
A. Horizontal Layout Group 이라는 컴포넌트
B. Content Size Fitter 라는 컴포넌트


COntent Size Fitter 에 Horizontal Fit 는 Preferred Size 로 맞춰준다.

5. Content 이하에 Button 오브젝트 등 필요한 오브젝트를 추가하여 늘어 놓는다.

6. Unity 의 플레이 버튼을 클릭하면 게임 씬에서 아래와 같이 조작이 가능하다.

좌우로 스크롤 해보면 원하는대로 스크롤이 가능!


<끝>

2017년 12월 1일 금요일

인벤토리 기초 구조 코드

다시 게임 제작개시, 이제 간단한 미니게임은 충분한 것 같고..
조금 더 나의 본질이 뭔가, 뭐가 하고 싶은가를 고민하다가 새 게임을 시작.

이번 게임은 수집, 성장 요소가 있는 게임이기 때문에 인벤토리가 필요해서 하나 만들었다.



간만에 풀코드.
InventorySystem.cs


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Kpaper;

namespace Kpaper
{
    public class InventorySystem : MonoBehaviour
    {
        #region Inventory
        
        // 인벤토리 시스템 기초 구조만 담긴 코드
        // _itemID 를 itemTable 에서 찾아서 정보를 표시하거나 하는 부분은 별도로 구현해야 한다.

        // 아이템의 아이디 갯수 itemID, count
        public Dictionary items = new Dictionary();

        // items 에 입력한 아이템 ID와 카운트를 더해준다. (add 다. assign 아님)
        public void setAddItem(int _itemID, int _count)
        {
            // 아이템이 목록에 없다면 넣어준다.(초기 갯수는 0개)
            if (items.ContainsKey(_itemID) == false)
            {
                items.Add(_itemID, 0);
            }

            // 아이템 수량 증가 적용
            items[_itemID] += _count;

            if (Debug.isDebugBuild)
                Debug.Log(string.Format("{0} {1}을 {2} 개", "아이템 삽입", _itemID, _count));
        }

        // items 에서 해당 아이템 키가 있는지 확인하고 있으면 카운트 만큼 빼준다. 카운트가 0인 경우에는 아이템 키를 제거해준다.
        public void setRemoveItem(int _itemID, int _count)
        {
            // 아이템이 목록에 없다면 아무것도 하지 않는다.
            if (items.ContainsKey(_itemID) == false)
            {
                if (Debug.isDebugBuild)
                    Debug.Log("아이템이 목록에 없습니다.");
                return;
            }

            // 아이템 수량 감소 적용
            items[_itemID] -= _count;

            // 아이템 수를 감소 시켰더니 0개 이하이면 아이템을 사전 목록에서 제거한다.
            if (items[_itemID] <= 0)
                items.Remove(_itemID);

            if (Debug.isDebugBuild)
                Debug.Log(string.Format("{0} {1}을 {2} 개", "아이템 제거", _itemID, _count));
        }

        
        // 보유한 아이템 목록
        public List keys = new List();

        // 아이템 목록 새로 고침
        public void setRefreshInventory()
        {
            if (Debug.isDebugBuild)
                Debug.Log(items.Count + " 개의 아이템이 있습니다.");

            // 딕셔너리의 키 묶음을 들고와서 Keys 리스트를 만든다.
            keys = new List(items.Keys);

            // Keys 리스트를 기준으로 아이템 ID와 수량을 출력한다.
            for (int i = 0; i < keys.Count; i++)
            {
                int _itemID = keys[i];
                int _itemCount = items[_itemID];

                if (Debug.isDebugBuild)
                    Debug.Log("아이템 키 : " + _itemID + " 아이템 수량 : " + _itemCount); // 이 부분을 리스트로 만들어 쓰던가 하면 관리하기 편하다.
            }

        }

        // 아이템 아이디를 입력하면 해당 아이템의 보유 수량을 리턴한다. 아이템을 찾을 수 없으면 0을 보낸다.
        public int getItemCountByItemIndex(int _itemID)
        {
            if (items.ContainsKey(_itemID))
            {
                return items[_itemID];
            }
            else
            {
                Debug.Log(string.Format("인벤토리 안에서 찾을 수 없는 아이템ID {0} 를 요청했습니다.",_itemID));
                return 0;
            }
        }

        #endregion

        #region MonoBehavior

        private void Start()
        {
            // 시연
            
            // 임의로 1이라는 itemID를 가진 아이템을 10개 넣고 5개씩 2번 빼서 0으로 만들어 본다.

            // 인벤토리 리프레시
            setRefreshInventory();

            // 아이템 1을 10개 넣고
            setAddItem(1, 10);

            // 인벤토리 리프레시
            setRefreshInventory();

            // 아이템 갯수 찾기 테스트
            Debug.Log(getItemCountByItemIndex(2));

            // 1을 5개 제거
            setRemoveItem(1, 5);

            // 인벤토리 리프레시
            setRefreshInventory();

            // 1을 5개 제거
            setRemoveItem(1, 5);
            
            // 인벤토리 리프레시
            setRefreshInventory();

        }

        #endregion
    }
}

2017년 11월 16일 목요일

인공지능 번역기 툴을 만들다가 남기는 후기

어플리케이션의 스트링 리소스를 한방에 (기계)번역하는 툴을 필요에 의해서 만들게 되었다.
(복붙이 너무 귀찮았어...)

구글 API 는 유료이길래 별 생각 없이 라이벌(?) 이라고 불리는 네이버 PAPAGO Open API 로 번역을 해봤다.

하루종일 삽질해서 Unity 용 툴, 엑셀용 VBA 툴 2가지를 만들었는데... (VBA가 더 어렵네 -_-;; )

막상 PAPAGO 의 번역 품질을 보니 쓰기 어려운 수준이라는 느낌을 받았다.
아래의 번역 시험 결과는 아주 소프트한 예시이다. (그나마 잘된걸 고른 것)

실제로 특수기호나 숫자가 섞이거나, 외래어 표기가 된 한글의 처리가 제대로 되지 않아서 쓰기 힘들었다.
예를 들면 "웹캠" 이라는 단어가 있으면 구글은 Web Cam 으로 PAPAGO는 "웹캠"으로 번역해버린다. (한글로 그대로 출력)

Oh My God.

뭐 가끔 쓰는 정도로 하는걸로 하고 오늘의 작업을 마무리..

Unity 툴은 에셋 스토어에 올려버렸고,

VBA 는 좀 더 다듬어서 블로그에 공개해야겠다.
누군가라도 쓰겠지 랄까 -_-;;;;

- [번역 시험 결과]----------------------------------------------

Source)
장치가 사용 중이거나 응답하지 않습니다.
영상 요청을 중단합니다.
다른 장치로 다시 시도해봐주세요.

Google)
The device is busy or not responding.
Abort video requests.
Please try again with another device.

Papago)
Device is busy or unresponsive.
Stops image request.
Try again with another device.

2017년 11월 14일 화요일

Unity WebcamTexture 의 동작에 문제가 있다면? - 해결 대안 제시

Unity API 를 통해서 장착된 WebCam을 컨트롤 해봤다.
되긴 되는데 신통찮다.

살아있는지 여부를 확인 하기 어렵고(try ~ catch 로 에러 핸들링도 불가),
각 캠이 지원하는 해상도 리스트도 가져올 수 없고 등등 불편하다.
 
안되는 장치도 있다.

Windows 10 출시 이후 발매된 웹캠 (특히, 마이크로소프트 스튜디오 시리즈)은 제대로 인식이 안된다.
장치가 1개만 설치되어 있을  때는 어영부영 돌아가는데, 2개 이상 설치되어 있을 때는 제대로 안된다.

Devices 에 인식이 제대로 안되고, 장치 이름 조차도 제대로 가져오지 못한다.

Logitech 의 웹캠은 대부분 잘 되는데, 이도 문제가 있으니...

Logitech Brio 4K 같은 경우는 고급 웹캠이라 그런지 화질과 동작 성능 양쪽에 문제가 없고
다양한 해상도와 컬러 포맷을 지원해서 그런지 Unity 와의 호환성에 크게 문제가 없다.

문제는 국민캠으로 불리는 Logitech C920 은 큰 문제가 없으나,
이 모델의 후속 기종인 C922가 문제가 된다.

640x480 의 기본 해상도는 별 차이 없이 잘 동작하는데,
1280x720 해상도를 요청하면 C922 의 경우에는 60FPS로만 동작하게 된다.
 
그러면  Unity 화면 상에서 뭔가 끊기는 느낌으로 동작하게 되는데...
왜 이런 거동을 보이는지 이해를 할 수 없지만,
내부를 본게 아니니 이해는 잠시 접어두고 방법 해결에만 집중했다.


이래저래 구글링 해외 포럼을 뒤져보니 비슷한 문제를 가진 사람이 많았고,
DirectShow를 이용해서 웹캠을 출력하면 될거라는 결론에 도달했다.

직접 만들어야 하나?
음... 더 이상 내부에서 작업량을 늘리고 싶지 않았다.


조금 더 찾아보니 다행히도 누군가 만들어둔 플러그인이 있었다. (영국의 RenderHeads 社)
AssetStore에서 아래의 Plugin의 데모 버전으로 시험해보니 큰 문제 없었기에 구입하는 것으로 결정.


[AVPro Live Camera]
https://www.assetstore.unity3d.com/kr/#!/content/3683

덱링크사의 제품까지 지원한다는데 이런 장비가 집에 없으니 모르겠고,
캡쳐카드를 통한 캡쳐도 화면이 이상하게 잘리거나 하는 문제 없이 거의 잘 되는 것 같다.

지원 플랫폼은 DirectShow를 이용하는 탓인지 Window Standalone 전용이기 때문에,
Windows에서 다양한 웹캠을 지원해야 하는
나와 비슷한 특수한 경우에 해당되는 사람들에게 추천한다.

2017년 11월 7일 화요일

Texture2D Rotate 코드

Texture2D 를 회전 시키는 코드인데,
매 Update 프레임에서 쓰기에는 오버헤드가 커서 사용하면 안된다.
그래도 어딘가 누군가에게는 도움이 될거라 생각해서 수집한 내용 기록..

# 출처 : https://answers.unity.com/questions/685656/rotate-an-image-by-modifying-texture2dgetpixels32.html

public static Texture2D RotateImage(Texture2D originTexture, int angle)
        {
            Texture2D result;
            result = new Texture2D(originTexture.width, originTexture.height);
            Color32[] pix1 = result.GetPixels32();
            Color32[] pix2 = originTexture.GetPixels32();
            int W = originTexture.width;
            int H = originTexture.height;
            int x = 0;
            int y = 0;
            Color32[] pix3 = rotateSquare(pix2, (Math.PI / 180 * (double)angle), originTexture);
            for (int j = 0; j < H; j++)
            {
                for (var i = 0; i < W; i++)
                {
                    //pix1[result.width/2 - originTexture.width/2 + x + i + result.width*(result.height/2-originTexture.height/2+j+y)] = pix2[i + j*originTexture.width];
                    pix1[result.width / 2 - W / 2 + x + i + result.width * (result.height / 2 - H / 2 + j + y)] = pix3[i + j * W];
                }
            }
            result.SetPixels32(pix1);
            result.Apply();
            return result;
        }
        static Color32[] rotateSquare(Color32[] arr, double phi, Texture2D originTexture)
        {
            int x;
            int y;
            int i;
            int j;
            double sn = Math.Sin(phi);
            double cs = Math.Cos(phi);
            Color32[] arr2 = originTexture.GetPixels32();
            int W = originTexture.width;
            int H = originTexture.height;
            int xc = W / 2;
            int yc = H / 2;
            for (j = 0; j < H; j++)
            {
                for (i = 0; i < W; i++)
                {
                    arr2[j * W + i] = new Color32(0, 0, 0, 0);
                    x = (int)(cs * (i - xc) + sn * (j - yc) + xc);
                    y = (int)(-sn * (i - xc) + cs * (j - yc) + yc);
                    if ((x > -1) && (x < W) && (y > -1) && (y < H))
                    {
                        arr2[j * W + i] = arr[y * W + x];
                    }
                }
            }
            return arr2;
        }

2017년 11월 6일 월요일

Unity Texture -> RenderTexture -> Texture2D -> Color32[] 로 변환의 험란한 길

영상 처리 작업을 하던 중 Color32[]를 이용할 일이 있었는데.
Unity 로 이걸 할라 하니 이래저래 삽질이 있었다.

적당히 구글링 해보니 Texture 를 Texture2D.ReadPixel 을 이용해서 Texture2D로 읽는 팁이 있었는데,
막상 해보니 그리는 프레임에 이걸 하면 동작하지 않는다.

좀 멀리 돌아온 감이 있는데,
텍스쳐를 렌더 텍스쳐로 바꾸고 이걸 텍스쳐2D로 만든 다음에 Colors32를 들고 오면 된다.

하루종일 삽질한 것 치고 우아하지 않아서 고민하다가..
지쳐서 그냥 이쯤에서 마무리.

C#으로는 한계가 있는 느낌.. 하아...


    private Texture2D _texture2D;
    private Texture _mainTexture;
    private RenderTexture _renderTexture;

    public Color32[] getTexture2D_AsPixel32()
    {
        if (_mainTexture == null)
            _mainTexture = _liveCamera.OutputTexture;

        if (_texture2D == null)
        {
            _texture2D = new Texture2D(_mainTexture.width, _mainTexture.height, TextureFormat.RGBA32, false);
        }
        //RenderTexture currentRT = RenderTexture.active;

        if (_renderTexture == null)
            _renderTexture = new RenderTexture(_mainTexture.width, _mainTexture.height, 32);

        // mainTexture 의 픽셀 정보를 renderTexture 로 카피
        Graphics.Blit(_mainTexture, _renderTexture);

        // renderTexture 의 픽셀 정보를 근거로 texture2D 의 픽셀 정보를 작성
        RenderTexture.active = _renderTexture;

        _texture2D.ReadPixels(new Rect(0, 0, _renderTexture.width, _renderTexture.height), 0, 0);
        _texture2D.Apply();

        return _texture2D.GetPixels32();

    }

2017년 11월 3일 금요일

해상도 값을 이용한 화면 비율 알아내기

웹캠에서 지원하는 해상도를 정리하다가
적당히 만들고 있었는데 번거로워서 만든 것
_width와 _height 를 입력하면 스트링으로 가로세로 비율이 나온다.



    // _width 는 가로 해상도, _height 는 세로 해상도 픽셀 값
    public string getResolutionRatio(int _width, int _height)
    {
        int a;
        int b;

        // 화면의 가로/세로 중 긴 쪽을 앞으로 하기
        if (_width < _height)
        {
            a = _width;
            b = _height;
        }
        else
        {
            a = _height;
            b = _width;
        }

        // 최대 공약수, 유클리드 호제법
        int gcd = gcd3(a, b);

        return string.Format("{0}:{1}", _width / gcd, _height / gcd);
    }

    private int gcd3(int a, int b)
    {
        return a % b == 0 ? b : gcd3(b, a % b);
    }

2017년 10월 14일 토요일

타임서버에서 시각 가져와서 Unity 에서 사용하기 (NST, NIST)

NIST 타임서버에서 시간가져오기 (NST)

아래와 같은 코드로 가져올 수 있다.
Unity 에서 사용할 수 있다.


using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeServer : MonoBehaviour {

    [SerializeField]
    private string _comment = "만료시킬 날짜를 적으세요 (한국시각 기준)";

    public int _yyyy, _mm, _dd;
    private DateTime _expireDateTime, _nowServerDateTime, _nowLocalDateTime;
    private TimeSpan _duration;

 // Use this for initialization
 void Start () {

        // 한국 시각
        _duration = System.TimeSpan.FromHours(9);
        _expireDateTime = new DateTime(Mathf.Clamp(_yyyy, 1900, 3000), Mathf.Clamp(_mm, 1,12), Mathf.Clamp(_dd, 1, 31));

        _nowLocalDateTime = DateTime.Now;
        _nowServerDateTime = GetNISTDate().Add(_duration);

        if (Debug.isDebugBuild)
        {
            Debug.LogWarning("만료지정일 : " + _expireDateTime);
            Debug.LogWarning("현재 로컬 시각 :" + _nowLocalDateTime);
            Debug.LogWarning("현재 서버 시각 :" + _nowServerDateTime);
        }

        if (_nowLocalDateTime < _expireDateTime)
        {
            if (_nowServerDateTime < _expireDateTime)
            {
                // Debug.Log("실행");
            }
            else
            {
                // Debug.Log("서버 체크 결과 만료 됨");
            }
        }
        else
        {
            // Debug.Log("로컬 체크 결과 만료 됨");
        }


    }

#region NTPTIME

    //NTP time 을 NIST 에서 가져오기
    // 4초 이내에 한번 이상 요청 하면, ip가 차단됩니다.

    public static DateTime GetDummyDate()
    {
        return new DateTime(2017, 12, 24); //to check if we have an online date or not.
    }

    public static DateTime GetNISTDate()
    {
        System.Random ran = new System.Random(DateTime.Now.Millisecond);
        DateTime date = GetDummyDate();
        string serverResponse = string.Empty;

        // NIST 서버 목록
        string[] servers = new string[] {
        "time.bora.net",
        //"time.nuri.net",
        //"ntp.kornet.net",
        //"time.kriss.re.kr",
        //"time.nist.gov",
        //"maths.kaist.ac.kr",
        "nist1-ny.ustiming.org",
        "time-a.nist.gov",
        "nist1-chi.ustiming.org",
        "time.nist.gov",
        "ntp-nist.ldsbc.edu",
        "nist1-la.ustiming.org"
    };

        // 너무 많은 요청으로 인한 차단을 피하기 위해 한 서버씩 순환한다. 5번만 시도한다.
        for (int i = 0; i < 5; i++)
        {
            try
            {
                // StreamReader(무작위 서버)
                StreamReader reader = new StreamReader(new System.Net.Sockets.TcpClient(servers[ran.Next(0, servers.Length)], 13).GetStream());
                serverResponse = reader.ReadToEnd();
                reader.Close();

                // 서버 리스폰스를 표시한다. (디버그 확인용)
                if (Debug.isDebugBuild)
                    Debug.Log(serverResponse);

                // 시그니처가 있는지 확인한다.
                if (serverResponse.Length > 47 && serverResponse.Substring(38, 9).Equals("UTC(NIST)"))
                {
                    // 날짜 파싱
                    int jd = int.Parse(serverResponse.Substring(1, 5));
                    int yr = int.Parse(serverResponse.Substring(7, 2));
                    int mo = int.Parse(serverResponse.Substring(10, 2));
                    int dy = int.Parse(serverResponse.Substring(13, 2));
                    int hr = int.Parse(serverResponse.Substring(16, 2));
                    int mm = int.Parse(serverResponse.Substring(19, 2));
                    int sc = int.Parse(serverResponse.Substring(22, 2));

                    if (jd > 51544)
                        yr += 2000;
                    else
                        yr += 1999;

                    date = new DateTime(yr, mo, dy, hr, mm, sc);

                    // Exit the loop
                    break;
                }
            }
            catch (Exception e)
            {
                /* 아무것도 하지 않고 다음 서버를 시도한다. */
            }
        }
        return date;
    }
    #endregion
}


아래 코드 원본에서 주석 변경 및 내가 사용하기 위해 수정함.
경로 : https://stackoverflow.com/questions/516788/getting-current-gmt-time

2017년 9월 18일 월요일

Unity Custom Menu 만들기

귀찮은 작업이라고 생각해서 잘 안하는 편이었는데...
오늘 처럼 뭔가 단순하게 반복해야 하는 작업에는 메뉴로 등록하고 빨리 적용하는게 나을 것 같았다.

작업 내용은 어떠한가 하니
1. 어떤 오브젝트에 필요한 새 컴포넌트를 붙이고,
2. 구 컴포넌트의 값을 새 컴포넌트로 옮기고,
3. 구 컴포넌트를 삭제한 뒤,
4. 주석용으로 사용하는 컴포넌트를 붙여서 작성시각을 기록한다.

일괄로 하기엔 확인하면서 진행해야 해서 메뉴를 이용하는 정도가 딱 좋았다.
(컨텍스트 메뉴가 나았을지도? (웃음))

참고한 Document
https://docs.unity3d.com/ScriptReference/EditorGUILayout.html

예제는 이거면 금방 알 수 있다. (Toggle 레퍼런스에서 옮김)
필요한 변수를 선언하고
editorLayout 으로 연결하고
거기서 조작에 따라 처리할 내용을 기입하면 된다.


using UnityEngine;
using UnityEditor;

public class EditorGUILayoutToggle : UnityEditor.EditorWindow
{
    bool showBtn = true;

    [MenuItem("Examples/Editor GUILayout Toggle Usage")]
    static void Init()
    {
        EditorGUILayoutToggle window = (EditorGUILayoutToggle)EditorWindow.GetWindow(typeof(EditorGUILayoutToggle), true, "My Empty Window");
        window.Show();
    }

    void OnGUI()
    {
        showBtn = EditorGUILayout.Toggle("Show Button", showBtn);
        if (showBtn)
            if (GUILayout.Button("Close"))
                this.Close();
    }
}

2017년 8월 28일 월요일

코드 조각 리스트<클래스>의 소팅 하기

평소에는 List<>.sort() 로 쉽게 정렬을 하지만,
직접 만든 클래스는 정렬이 어려워서 이럴 떄 사용할만한 코드 조각

public class BasicEntity
{
    public int Index = 0;
    public string Name = string.Empty;
    public float Value = 0f;
}

   // true 면 낮은 순부터 0 false 면 반대
    public List<BasicEntity> sortFilterEntity(List<BasicEntity> list, bool order)
    {
        List<BasicEntity> result = list;

        if (order)
        {
            result.Sort(delegate (BasicEntity A, BasicEntity B)
            {
                if (A.Index > B.Index) return 1;
                else if (A.Index < B.Index) return -1;
                return 0;
            });
        }
        else
        {
            result.Sort(delegate (BasicEntity A, BasicEntity B)
            {
                if (A.Index > B.Index) return -1;
                else if (A.Index < B.Index) return 1;
                return 0;
            });
        }

        for (int i = 0; i < result.Count; i++)
        {
            Debug.LogError(result[i].Name);
        }

        return result;
    }

안드로이드 OBB 분할 관련 게시물 타래

이번 게임은 용량을 신경 쓰지 않은 탓에 100메가를 훌쩍 넘어버렸다.

사운드 리소스가 추가되면 더 커질텐데

스토어 등록에 대해서 고민을 하다가...


어차피 유료 패키지 게임이니까 다운로드 용량에는 신경쓰지 말고 만드는 것으로 고민

하지만 스토어 등록이 100메가를 넘으면 문제가 된다.


일단 구글 부터 해결 시도.

OBB 파일로 분할하여 빌드하는 기능이 유니티 5.5에 있어서 이것을 활용하기로 한다.

빌드 자체는 체크 박스 하나만 하면 되서 손쉬웠다.

참고링크 :
https://docs.unity3d.com/kr/current/Manual/android-OBBsupport.html



첫번째 문제
플레이스토어에 어디다가 분할 파일을 올릴 수 있지?

해결 -> 2번째 등록 (업데이트 APK 등록) 시에 분할 파일을 등록할 수 있는 항목이 추가된다.

처음에는 적당히 올리고, 빌드코드 2로 수정해서 새로 빌드하여, 업데이트 등록하면 된다.


두번째 문제
LoadResource Error

에셋번들처럼 뭔가 해줘야 하나?
구글 안드로이드 전용으로 플러그인이 있어서 이것을 최초 씬에서 로드하여 페치를 다운로드 받고 게임을 시작해야 한다.

작업 순서

1. 유니티 플러그인 다운로드
https://www.assetstore.unity3d.com/kr/#!/content/3189

2. 플러그인 폴더 내의 googleplaydownloader.cs 를 내 스크립트 폴더로 이동

3. googleplaydownloader 에 내 구글 개발자 계정의 Public Key를 코드에 추가

4. example 코드를 참고해서, 씬 처음에 부르도록 한다.

5. 내 경우는 obb 전용 씬을 하나 만들어서 먼저 모든 처리를 하고 게임 씬을 부르는 것으로 마무리

6. 스토어에 테스트 업로드 후 동작 확인 완료


사소한 문제는 있었지만, 생각보다 쉽게 적용할 수 있었다.

다음은 인앱결제를 적용해볼 예정이다.

2017년 8월 14일 월요일

Unity C# object 배열 메모

개발하다가 메모 하나...

다른 클래스 타입의 여러가지 오브젝트를 하나의 목록에 넣을 때,
배열 혹은 리스트를 사용하게 되는데...

C#의 object 는 System.Collections.Generic.List<T> 에 넣으면 Null Reference Exception 이 발생한다.

object[] 로 배열로 넣고 사용하면 된다.

2017년 7월 26일 수요일

어떤 오브젝트의 Mirror 된 회전 값은? (쿼터니언 거울 반전)

쿼터니언으로 미러링 할 때 메모

오일러앵글이 아니라 쿼터니언의 회전은 이해하기 어려울 수 있다.
회전각을 반전시키는 것은 더더욱...

예를 들기 위해 obj의 로컬로테이션을 미러하고 싶다고 가정했다.

보통은 아래와 같이 생각하지만,
new Quaternion(obj.localRotation.x * -1.0f, obj.localRotation.y, obj.localRotation.z, obj.localRotation.w);

실제로는 아래와 같아야 하고,
new Quaternion(obj.localRotation.x * -1.0f, obj.localRotation.y, obj.localRotation.z, obj.localRotation.w * -1.0f);

위도 비슷하지만 비틀림이 있기 때문에 아래와 같이 사용하는 것이 옳다.
new Quaternion(obj.localRotation.x, obj.localRotation.y * -1.0f, obj.localRotation.z * -1.0f, obj.localRotation.w);

참고 URL : https://www.gamedev.net/forums/topic/599824-mirroring-a-quaternion-against-the-yz-plane/


추가 메모  :
카메라 방향이 그대로 일 때 어떤 오브젝트의 이동과 회전을 거울 반전시키려면
포지션 x 축도 바꿔준다. (localPosition.x * - 0.1f)

2017년 6월 23일 금요일

Unity 에서 쓸 수 있는 CSV Reader (C#)

간단히 스트링 저장해놓고 불러 쓰는 용도로 사용하려고 준비...
개행은 \n이 아니라 br 태그로 사용하도록 만듬.

# CSVReader Class

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class CSVReader
{
    static string SPLIT_RE = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
    static string LINE_SPLIT_RE = @"\r\n|\n\r|\n|\r";
    static char[] TRIM_CHARS = { '\"' };

    public static List<Dictionary<string, object>> Read(string file)
    {
        
        var list = new List<Dictionary<string, object>>();
        TextAsset data = Resources.Load(file) as TextAsset;


        var lines = Regex.Split(data.text, LINE_SPLIT_RE);

        if (lines.Length <= 1) return list;

        var header = Regex.Split(lines[0], SPLIT_RE);
        for (var i = 1; i < lines.Length; i++)
        {

            if (lines[i].Replace(" ","").Substring(0, 2) == "//")
            {
                Debug.Log(lines[i]);
            }
            else
            {
                var values = Regex.Split(lines[i], SPLIT_RE);
                if (values.Length == 0 || values[0] == "") continue;

                var entry = new Dictionary<string, object>();
                for (var j = 0; j < header.Length && j < values.Length; j++)
                {
                    string value = values[j];
                    value = value.TrimStart(TRIM_CHARS).TrimEnd(TRIM_CHARS).Replace("\\", "");
                    value = value.Replace("</ br>", "\n");
                    value = value.Replace("</ comma>", ",");
                    object finalvalue = value;
                    int n;
                    float f;
                    if (int.TryParse(value, out n))
                    {
                        finalvalue = n;
                    }
                    else if (float.TryParse(value, out f))
                    {
                        finalvalue = f;
                    }
                    entry[header[j]] = finalvalue;
                }
                list.Add(entry);
            }
        }
        return list;
    }
}


#사용법
csv 파일을 하나 만든다 내용은 아래와 같다.

key,value
a,a value
b,"b, value"
c,"c</ br>line break"
Assets/resources/kpaper.csv 에 파일이 있다고 가정하고 아래 코드 실행

List<Dictionary<string,object>> data = CSVReader.Read ("kpaper");
 
        for(var i=0; i < data.Count; i++) {
            Debug.Log ("key " + data[i]["key"] + " " +
                   "value " + data[i]["value"]);
        }









2017년 6월 20일 화요일

Unity 엔진 난독화 도구 Obfuscator


최근 개발 중인 프로그램의 출시 시즌이라 이래저래 배포 문제를 해결하고 있다.

유니티로 개발한 프로그램은 Managed\Assembly_cshap.dll 파일에 C#으로 짠 코드 대부분이 쉽게 보이게 되는 문제가 있다.
쉽게 코드를 보고 변조할 수 없도록 Encryption 을 걸면 좋겠지만, C# 특성상 적용이 어렵다.

최소한 난독화라도 걸고 출시해야 할 것 같아서 여러가지 솔루션을 찾아봤는데...
난독화 도구로는 아래 에셋이 좋은 것 같다. (JJ 감사합니다.)

옵션은 조금 만져줘야 하지만, 크게 손이 안가고 가장 편하고 확실한 느낌이다.
(물론 맞춰서 코드도 조금 고쳐야 하긴 했지만)



사용법은 구입 -> 설치 -> Editor\Beebyte\Obfuscator\ 안의 옵션 파일을 설정해주면 된다.

2017년 6월 13일 화요일

Unity 개발 팁, 자식 게임 오브젝트 찾기 간단 팁

자식찾기 간단 팁

보통은 GameObject.Find("string Object Name")을 사용하지 말라고 여기저기 나와있다.
성능 이슈가 있으면 개선하는게 맞지만,
굳이 개발 편의를 위해 있는 기능을 무조건 지양하는 것도 옳지 않은 것 같다.
코드가 줄어들 수 있고, 이해하기 쉽다면 어느정도는 적극 사용하는것도 좋겠지.

예를 들어 어떤 부모 오브젝트의 자식중에서 라고 한정해서 찾는다면 어떨까?
이것도 너무 빈번하게 찾는다면 문제가 되겠지만, 적은 수에서라면 비교적 덜 문제가 되지 않을까?
이를 시험해서 성능 평가 까지 적으면 좋겠지만, 시간이 없어서 테스트 코드만 간단히...

// 아래와 같이 자식 오브젝트에서 이름을 적은 녀석을 데려올 수 있다.
// transform의 하위에서 이름으로 찾고 그녀석의 gameobject 를 반환한다.
GameObject childObject GameObject.transform.Find("string Child Name").gameobject;

여기다가 SendMessage() 같은걸로 뭔가 동작시켜줄 수 있겠지.

이 두개를 사용하면 아이템 리스트와 하위 아이템을 좀 더 각각 독립적으로 만들어주는데 유용해서 은근히 쓰고 있다.

2017년 5월 29일 월요일

Unity 개발 팁 - isDebugBuild, EditorOnly Tag

빠르게 개발 할 때 많이 사용하고 있는 것이 있어서 개발팁으로 남긴다.

#1
Debug.isDebugBuild
빌드가 디버그 빌드라면 true 를 반환 하기 때문에 여러가지 확인용으로 사용할 수 있다.

샘플 코드

using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    void Example() {
        if (Debug.isDebugBuild)
            Debug.Log("This is a debug build!");
        
    }
}

참고 문서
https://docs.unity3d.com/ScriptReference/Debug-isDebugBuild.html


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

#2
게임 Obeject의 Tag를 EditorOnly 로 지정하면
해당 오브젝트는 Editor 상에서만 사용하고,
실제로 빌드하면 해당 태그가 지정된 오브젝트는 제외되고 빌드되어 편리하다.
테스트용으로 필요하여 하이어라키에 추가해둔 오브젝트를 일일히 삭제할 필요가 없다.

2017년 5월 25일 목요일

Unity 키보드로 히든 키 입력하기

특정 키를 입력받고 있다가 정답이 되면 true 를 돌려주는 간단한 코드.
Start()에 입력된 값을 바꾸거나, Inspector 에서 적당히 미리 정답 값을 입력해두면 된다.

재미로 만들었다. 어딘가에 적당히 써보자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HiddenKeyInput : MonoBehaviour {

    public bool _key = false;

    public List _pool, _seikai;

    void Start()
    {
        // 정답
        // 왼쪽시프트 + 상하좌우AB 를 순서대로 누른다.
        // _key 가 트루가 된다.
        // 시프트를 떼면 리셋, _key 가 false 가 되고 _pool 에 쌓여있던 내용도 초기화 된다.
        _seikai.Add("상");
        _seikai.Add("하");
        _seikai.Add("좌");
        _seikai.Add("우");
        _seikai.Add("에이");
        _seikai.Add("비");
    }

    // Update is called once per frame
    void Update () {

        if (Input.GetKey(KeyCode.LeftShift))
        {
            if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                _pool.Add("상");
                Debug.Log("상");
            }
            if (Input.GetKeyDown(KeyCode.DownArrow))
            {
                _pool.Add("하");
                Debug.Log("하");
            }
            if (Input.GetKeyDown(KeyCode.LeftArrow))
            {
                _pool.Add("좌");
                Debug.Log("좌");
            }
            if (Input.GetKeyDown(KeyCode.RightArrow))
            {
                _pool.Add("우");
                Debug.Log("우");
            }
            if (Input.GetKeyDown(KeyCode.A))
            {
                _pool.Add("에이");
                Debug.Log("에이");
            }
            if (Input.GetKeyDown(KeyCode.B))
            {
                _pool.Add("비");
                Debug.Log("비");
            }

            if (_seikai.Count == _pool.Count)
            {
                
                for (int i = 0; i < _seikai.Count; i++)
                {
                    if (_seikai[i] == _pool[i])
                    {
                        if (i == _seikai.Count - 1)
                            _key = true;
                    }
                    else
                    {
                        break;
                    }
                }

            }
            
        }
        else
        {
            if (_pool.Count > 0)
            {
                _pool.Clear();
            }
            _key = false;
        }
  
 }
}