2025년 9월 14일 일요일

Unity Android Storage Access Framework(SAF)

 Android Storage Access Framework(SAF)

안드로이드 앱을 만들다 보면 “사용자가 파일을 직접 선택해서 열거나 저장하게 하고 싶다” 는 순간이 꼭 찾아옵니다. 예전에는 단순히 파일 경로를 받아 File API로 접근하면 됐지만, 보안이나 호환성 문제 때문에 이제는 그렇게 단순하지 않습니다. 실제 해보면 권한 없다고 오류가 발생합니다.

막상 문서 찾고 하다보면 생각보다 잘안됩니다.

이때 등장하는 게 Storage Access Framework(SAF) 입니다. 

SAF는 Android 4.4 (KitKat)부터 도입된 파일 접근 방식으로,
앱이 사용자가 직접 선택한 파일이나 폴더에만 접근할 수 있도록 도와주는 프레임워크입니다.

즉, 개발자가 마음대로 기기 저장소를 뒤지지 못하고, 사용자가 시스템이 제공하는 파일 선택 화면에서 골라준 파일에만 접근할 수 있습니다.

Unity에서 SAF를 사용하면 안드로이드 파일 선택창이 뜨게 됩니다. 여기에서 사용자가 골라주는 파일에 대해서 Read/Write가 가능해집니다.

현재 Android 전체적으로 설명할것이 아니기 때문에 Unity에서 사용하는 방법만 설명을 드리겠습니다.

일단 GPT를 이용해서 SAF로 만들어 달라면 잘 만들어 줍니다. 그런데 코틀린 소스 파일도 만들어야 하고 폴더 구정에 넣어서 그래들 빌드까지 해야하는데 실제 빈 프로젝트에서는 잘 동작될지 몰라도 기존에 이미 Plugins에 Android를 사용중인 경우 뭔가 잘 되지 않습니다.


Native File Picker for Android & iOS

Asset Store 에서 유용한 Free Asset 을 찾았습니다. 

https://assetstore.unity.com/packages/tools/integration/native-file-picker-for-android-ios-173238

예제는 github 보시면 이해가 될것 입니다.

https://github.com/yasirkula/UnityNativeFilePicker#example-code

github 소스를 보면 이미 빌드가된 aar 형태로 들어있습니다. 따로 안드로이드 빌드는 안해도 되는데 NativeFilePicker.aar의 소스는 공개가 안되어 있습니다.

구조는 간단한데 예제가 거의 없어서 사용하는데 시간이 좀 걸렸습니다.

제가 사용한 예제 코드를 공유합니다. 예제에서는 샘플이므로 동작하지는 않습니다.

💾 저장하기 (Save)

아래 코드는 SaveDataStruct 데이터를 JSON으로 직렬화한 뒤, 임시 저장소(temporaryCachePath)에 기록하고, 이후 SAF 기반으로 사용자가 선택한 경로로 내보냅니다.

// 저장 코드 public void SaveGameData(SaveDataSturct data, string fileName) { // JSON 직렬화 string json = JsonConvert.SerializeObject(data, Formatting.Indented); string combinedData = $"{json}"; try { #if UNITY_ANDROID && !UNITY_EDITOR // 임시 저장 경로 string downloadPath = Application.temporaryCachePath; string fullPath = Path.Combine(downloadPath, fileName); // 파일 저장 File.WriteAllText(fullPath, combinedData); Debug.Log($"Data saved to: {fullPath}"); // SAF Export bool permission = NativeFilePicker.CheckPermission(); if (permission == true) { NativeFilePicker.ExportFile(fullPath, OnFileExported); } else { NativeFilePicker.RequestPermissionAsync((result) => { if (result == NativeFilePicker.Permission.Granted) { NativeFilePicker.ExportFile(fullPath, OnFileExported); } else { Debug.LogError("사용자가 권한을 거부했습니다."); } }); } #endif } catch (Exception ex) { Debug.LogError($"Failed to save data: {ex.Message}"); } } // ExportFile 콜백 private void OnFileExported(bool success) { if (success) { Debug.Log("File exported successfully!"); } else { Debug.Log("File export failed or cancelled."); } }

📂 불러오기 (Load)

아래 코드는 SAF 파일 선택기를 호출해 사용자가 직접 파일을 선택하고, 선택한 파일을 불러옵니다.

// 파일 로드 public void LocalLoad() { #if UNITY_ANDROID && !UNITY_EDITOR if (NativeFilePicker.IsFilePickerBusy()) return; bool permission = NativeFilePicker.CheckPermission(); if (permission == true) { OpenFilePicker(); } else { NativeFilePicker.RequestPermissionAsync((result) => { if (result == NativeFilePicker.Permission.Granted) { OpenFilePicker(); } else { Debug.LogError("사용자가 권한을 거부했습니다."); } }); } #endif } private void OpenFilePicker() { NativeFilePicker.PickFile((path) => { if (path == null) { Debug.Log("Operation cancelled"); } else { Debug.Log("Picked file: " + path); LocalLoadReal(path); } }, new string[] { "text/plain" }); // 허용할 파일 MIME 타입 지정 } private void LocalLoadReal(string fullPath) { if (!File.Exists(fullPath)) { Debug.LogWarning("Save file not found!"); return; } try { // 파일 읽기 샘플 string[] lines = File.ReadAllLines(fullPath); Debug.Log($"Data loaded from: {fullPath}"); // TODO: lines를 역직렬화해서 SaveDataStruct로 변환 } catch (Exception ex) { Debug.LogError($"Failed to load data: {ex.Message}"); return; } }


2025년 9월 13일 토요일

Playstore console 앱이 16KB 메모리 페이지 크기를 지원해야 합니다.

 얘네(구글)들은 뭔가 변경 사항을 미리미리 예측못하는가? 갑자기 저런 공지를 날려서 3개월만에 해결하라고 ... 

앱단이 아니라 OS 단에서 패딩을 넣어주면 되는것 아닌가 싶기도 한데... 출시한지 오래된 앱의 경우, 갑작스런 대응이 쉽지는 않습니다.


 unity 관련 공지는 아래에서 찾을 수 있었습니다.

https://discussions.unity.com/t/info-unity-engine-support-for-16-kb-memory-page-sizes-android-15/1589588

아래 이상의 버전을 사용해서 빌드만 하면 되는데 문제는 버전업하면 어떤 일이 일어 날지 모르니... API가 조금 바뀌는 경우도 있고.... 적절한 버전을 업데이트 해서 해결하면됩니다.

  • Unity 6000.1+ released February 9, 2025
  • Unity 6000.0.38f1+ released February 14, 2025
  • Unity 2022.3.56f1+ released January 15, 2025
  • Unity 2021.3.48f1+ released January 22, 2025 accessible under extended LTS support for Enterprise/Industry customers

저의 경우 2022.3.10f1 사용중이라서 에디터 버전 2022.3.62f1 으로 업데이트 하였습니다.

다행이 오류는 없었고, target API Level을 최신으로 올려서 빌드하였습니다.

target API level은 Player setting Other 쪽에 있습니다.




-업데이트-

2개의 앱을 작업했는데 하나의 앱에서 여전히 에러가 사라지지 않는 경우가 있어서 추가로 확인을 하였습니다.


아래 라이브러리들이 여전히 안되는 경우가 있음

Play Console 에 번들 패키지를 올리면 지원 유무를 알 수 있습니다.

메모리 페이지 크기
16KB를 지원하지 않음
16KB를 지원하지 않는 라이브러리: base/lib/arm64-v8a/libAdaptivePerformanceAndroid.so base/lib/arm64-v8a/libAdaptivePerformanceHint.so base/lib/arm64-v8a/libAdaptivePerformanceThermalHeadroom.so

찾아보니 해당 라이브러리는 "Unity Adaptive Performance" 사용함으로서 발생하는 포함되는 lib 인데

https://developer.android.com/games/engines/unity/unity-adpf?hl=ko

Unity Adaptive Performance는 제공자가 기기에서 필요한 모든 정보를 가져와야 합니다. Android 제공업체는 Adaptive Performance 5.0 이상에서 지원되며 Unity 2021.3 이상을 지원합니다. Unity 2021 및 2022 버전 패키지 관리자는 Adaptive Performance 4.0을 다운로드하므로 버전 5.0으로 수동 업데이트해야 합니다.


5.0으로 업데이트 하라는 문구는 보이는데 해결 될지 몰라서 성능 관련 내용이라 삭제하기로 마음 먹고 삭제 했습니다.




2025년 3월 30일 일요일

Python의 requests 큰파일 다운로드시 처리 방법

 requests 사용시 아무 생각 없이 사용하곤 했었습니다. 그런데 어느 순간 thread가 강제 종료 되는 현상이 발생하였습니다. 구체적인 사유는 없는데 큰파일 다운로드시 발생하는 증상이 었습니다. 제가 사용할때는 ubuntu에서 3G 크기의 파일이었습니다.

메모리 부족 같아서 검색해보니 stream을 사용해야 한다고 합니다.

GPT를 이용해서 가이드를 받아서 코드를 수정했지만 사유없이 종료되는 경우 메모리 부족을 의심하시기 바랍니다.


import requests


url = 'https://example.com/largefile.zip'  # 다운로드할 파일의 URL

file_name = 'largefile.zip'  # 저장할 파일 이름


# 청크 단위로 다운로드

with requests.get(url, stream=True) as response:

    response.raise_for_status()  # 요청이 성공했는지 확인

    with open(file_name, 'wb') as file:

        for chunk in response.iter_content(chunk_size=8192):  # 8KB 단위로 읽기

            file.write(chunk)


print(f"{file_name} 다운로드 완료.")



2025년 3월 3일 월요일

stable diffusion webui with amd gpu

 RX 7700 XT GPU를 가지고 windows에서 stable diffusion을 동작시키기 위한 시행 착오에 대해서 정리해보았습니다.

누군가는 동일한 시행 착오를 격지 않길 바라는 마음에 정리를 해보았습니다.

따라하지 마시고 끝까지 다읽어 보시는걸 추천 드립니다. 


NVIDIA의 막강한 무기가 CUDA인걸 모르는지 시간이 많이 지났음에도 AMD의 개발 환경은 여전히 쉽지 않았습니다.

요즘 딥러닝하면 pytorch를 많이 사용합니다. 그래서 pytorch site에서 설치하는 방법을 살펴 보았습니다.

여기에서부터 고행은 시작되었습니다.

 


ROCm(Radeon Open Compute)은 AMD에서 개발한 오픈 소스 기반의 GPU 컴퓨팅 플랫폼입니다. 이는 NVIDIA의 CUDA와 유사한 개념으로, AMD GPU에서 고성능 병렬 컴퓨팅을 수행할 수 있도록 설계되었습니다.
ROCm 이 Windows 에서 설치가 불가능 하였습니다.

Linux 에서는 설치가 가능했기 때문에 WSL을 이용해서 설치를 시도하였습니다.

**WSL(Windows Subsystem for Linux)**은 Windows에서 직접 Linux 환경을 실행할 수 있도록 하는 기능입니다.
즉, 가상 머신(VM) 없이 Windows에서 Linux 명령어, 도구, 애플리케이션을 사용할 수 있도록 지원하는 Microsoft의 서브시스템입니다.


아래를 참고해서 6.1.3 버전을 설치하였습니다.

https://www.reddit.com/r/ROCm/comments/1ep4cru/rocm_613_complete_install_instructions_from_wsl/

문제는 여전히 CUDA를 인식하지 못하는 이슈가 있었습니다.

다시 아래 AMD의 WSL 가이드를 이용하여 ROCm 6.2.3 버전 설치를 시도 하였습니다.

https://rocm.docs.amd.com/projects/radeon/en/latest/docs/install/wsl/install-radeon.html

여전히 unsupported GPU라고 뜹니다.

여기저기 찾아보니 내장 그래픽 카드를 off 하라고 해서 BIOS에서 Off 설정을 해보았지만 여전히 동일하였습니다.


여기에서 자세한 설명은 안하였지만 wsl 내에서 docker 이미지를 이용해서도 설치해보았습니다. 여전히 인식하지 못하였습니다.

중요한 문서가 있는데... 지원되는 그래픽 카드가 정해져 있다는 것입니다.

https://rocm.docs.amd.com/projects/radeon/en/latest/docs/compatibility/wsl/wsl_compatibility.html

이쯤와서는 거의 다 포기하였습니다.

stable diffusion 은 directml(윈도우의 DirectX12 API) 이란것을 이용해서 동작시키는 방법이 있었습니다. 또 다른 방법은 ZLUDA를 이용하는 방법인데 AMD GPU를 이용해서 CUDA 흉내를 내는 방식입니다.

그래서 아래 코드를 참고해서 git 을 받고 

https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu

아래와 같이 실행하였습니다. 물론 그전에 python 3.10.6 버전을 꼭 설치해야합니다.

webui-user.bat --use-zluda


Using ZLUDA

Most of AMDGPUs are compatible.

Start WebUI with --use-zluda.


정리해 보면 아래와 같습니다.

Automatic Installation on Windows

  1. Install Python 3.10.6 (Newer version of Python does not support torch), checking "Add Python to PATH".
  2. Install git.
  3. Download the stable-diffusion-webui-amdgpu repository, for example by running git clone https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu.git.
  4. Run webui-user.bat from Windows Explorer as normal, non-administrator, user.

4번 실행시 webui-user.bat --use-zluda 라고 실행 시키면 됩니다.


동작하다가 numpy 관련 오류가 발생하는데 아래 가이드 보고 pip install numpy==1.23.5 로 새로 설치하였습니다.

https://myview7144.tistory.com/96


최초 동작시 NVIDIA 사용할때보다 굉장히 느립니다.

동작은 되는데 이미지 크기를 키우면 굉장히 느려집니다.

 

2025년 1월 27일 월요일

Unity New Input System pinch zoom, 핀치 줌

Unity6가 되면서 기본적으로 new input system을 기본으로 포함해서 따로 추가 설치가 필요 없게 되었습니다.

android로 개발하다 보니 pinch zoom이 필요하게 되어서 여기저기 검색도 해봤는데 멀티 터치가 생각보다 디버깅이 힘들었습니다.

제가 작업한 코드를 공개합니다. 디버깅 코드를 풀어서 디버깅 해가면서 적절하게 값을 조절 해서 사용해야 합니다.

카메라의 아래값을 변경하는 코드인데 이 환경은 프로젝트 마다 다를 수 있으므로 해당 부분을 참고하는것보다 저기에 넘기는 값을 보시기 바랍니다.

_camera.orthographicSize

아래는 전체 코드입니다. 다음 feature 부분을 참고 하세요. USE_NEW_INPUT

using UnityEngine;
#if USE_NEW_INPUT
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
#endif

public class CameraFollow : MonoBehaviour
{
    public Transform player; // 플레이어의 Transform
    public float smoothSpeed = 0.125f; // 부드러운 이동 속도
    public Vector3 offset;         // 카메라와 플레이어 간의 거리

    public bool lockX = false;     // X축 고정 여부
    public bool lockY = false;     // Y축 고정 여부

    public bool useBounds = false; // 경계 제한 사용 여부
    public Vector2 minBounds;      // 카메라 이동 최소값 (X, Y)
    public Vector2 maxBounds;      // 카메라 이동 최대값 (X, Y)

    public Camera _camera;
    public int skipFrame = 1;
    private int curFrame;

    public bool useZoom = false;
    public float zoomSpeed = 1f;
    public float minSize = 2f;
    public float maxSize = 10f;
    void Start()
    {
        if( _camera ==null ){
            _camera = Camera.main;
        }
       
    }
    private void OnEnable() {
        #if USE_NEW_INPUT
        UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable();
        #endif
    }
    private void OnDisable() {
        #if USE_NEW_INPUT
        UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Disable();
        #endif
    }
    void LateUpdate()
    {
        if (player == null) return;
        //if (curFrame < skipFrame)
        //{
        //    curFrame++;
        //    return;
        //}
        //curFrame = 0;

        // 목표 위치 계산
        Vector3 targetPosition = new Vector3(
            lockX ? transform.position.x : player.position.x + offset.x, // X축 고정 여부 확인
            lockY ? transform.position.y : player.position.y + offset.y, // Y축 고정 여부 확인
            transform.position.z // Z축은 고정
        );

        // 부드럽게 이동
        Vector3 smoothedPosition = Vector3.Lerp(transform.position, targetPosition, smoothSpeed);

        // 경계 제한 적용
        if (useBounds)
        {
            smoothedPosition.x = Mathf.Clamp(smoothedPosition.x, minBounds.x, maxBounds.x);
            smoothedPosition.y = Mathf.Clamp(smoothedPosition.y, minBounds.y, maxBounds.y);
        }

        // 새로운 위치 적용
        transform.position = smoothedPosition;

        if(useZoom){
            float scrollInput = 0;
            #if USE_NEW_INPUT
                Vector2 scrollInputV2 = Mouse.current.scroll.ReadValue();
                scrollInput = scrollInputV2.y;
            #else
                scrollInput = Input.GetAxis("Mouse ScrollWheel");
            #endif


            if (scrollInput != 0)
            {
                _camera.orthographicSize -= scrollInput * zoomSpeed;
                Debug.Log("scrollInput: " + scrollInput);
                _camera.orthographicSize = Mathf.Clamp(_camera.orthographicSize, minSize, maxSize);
            }
            #if USE_NEW_INPUT
                if( UnityEngine.InputSystem.EnhancedTouch.Touch.activeTouches.Count == 2 ){
                    UnityEngine.InputSystem.EnhancedTouch.Touch touch1 = UnityEngine.InputSystem.EnhancedTouch.Touch.activeTouches[0];
                    UnityEngine.InputSystem.EnhancedTouch.Touch touch2 = UnityEngine.InputSystem.EnhancedTouch.Touch.activeTouches[1];
                    Vector2 touch1PrevPos = touch1.screenPosition - touch1.delta;
                    Vector2 touch2PrevPos = touch2.screenPosition - touch2.delta;

                    float prevTouchDelta = (touch1PrevPos - touch2PrevPos).magnitude; // 이전 프레임에서의 거리
                    float currentTouchDelta = (touch1.screenPosition - touch2.screenPosition).magnitude; // 현재 프레임에서의 거리

                    float deltaMagnitudeDiff = prevTouchDelta - currentTouchDelta;
                    //Debug.Log("scrollInput1: " + deltaMagnitudeDiff);
                    deltaMagnitudeDiff = Mathf.Clamp(deltaMagnitudeDiff, -0.5f, 0.5f);
                    //Debug.Log("scrollInput2: " + deltaMagnitudeDiff);
                    _camera.orthographicSize += deltaMagnitudeDiff * zoomSpeed;
                    _camera.orthographicSize = Mathf.Clamp(_camera.orthographicSize, minSize, maxSize);
                }
                if( UnityEngine.InputSystem.EnhancedTouch.Touch.activeTouches.Count >= 2 ){
                    GameManager.instance.isMultiTouch = true;
                }else{
                    GameManager.instance.isMultiTouch = false;
                }
            #else
                // 두 개의 터치 입력을 받았는지 확인
                if (Input.touchCount == 2)
                {
                    Touch touch1 = Input.GetTouch(0);
                    Touch touch2 = Input.GetTouch(1);

                    // 각 터치의 현재 및 이전 위치 간 거리 계산
                    Vector2 touch1PrevPos = touch1.position - touch1.deltaPosition;
                    Vector2 touch2PrevPos = touch2.position - touch2.deltaPosition;

                    float prevTouchDelta = (touch1PrevPos - touch2PrevPos).magnitude; // 이전 프레임에서의 거리
                    float currentTouchDelta = (touch1.position - touch2.position).magnitude; // 현재 프레임에서의 거리

                    float deltaMagnitudeDiff = prevTouchDelta - currentTouchDelta;
                    Debug.Log("scrollInput1: " + deltaMagnitudeDiff);
                    deltaMagnitudeDiff = Mathf.Clamp(deltaMagnitudeDiff, -0.5f, 0.5f);
                    Debug.Log("scrollInput2: " + deltaMagnitudeDiff);
                    _camera.orthographicSize += deltaMagnitudeDiff * zoomSpeed;
                    _camera.orthographicSize = Mathf.Clamp(_camera.orthographicSize, minSize, maxSize);
                }
            #endif
        }
    }
   
}

2025년 1월 6일 월요일

dialogue system for unity 사용중 android 장치에서 터치 동작이 안되는 경우

프로젝트에서 Dialogue Manager를 사용하면 Android 휴대전화에서 터치가 전혀 작동하지 않습니다. 에디터 모드에서는 정상 동작하였습니다.

Dialogue Manager 에 Input Device Manager 를 제거 해서 테스트 해보시기 바랍니다. (개발자로부터 답변 받음) 저는 이렇게 해결했습니다.


Input Device Manager - 조이스틱이나 마우스 장치 전환을 위한 것이라고 합니다. 따라서 터치를 사용하는 휴대폰의 경우 필요 없다고 합니다.

이것 때문에 하루를 소비했네요. 진작 개발자에게 문의해볼 걸 그랬습니다. 참고로 개발자 답변이 굉장히 빠릅니다....


2025년 1월 5일 일요일

unity android debugging시 무한 경고

 

Unity 에서 안드로이드 디버깅시 이런식으로 무한 로그가 발생하는 경우 

2025-01-05 21:53:07.298 23202 23385 Warn Unity VULKAN: GENERAL WARNING: The following warning was triggered: VKDBGUTILWARN003. Please refer to the Adreno Game Developer Guide for more information: https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/snapdragon-game-toolkit/learn_guides.html


https://developer.qualcomm.com/sites/default/files/docs/adreno-gpu/snapdragon-game-toolkit/learn_guides.html 여기 홈페이지 가봐도 링크 깨져 있습니다.

로그가 무한이 나와서 다른 로그를 볼 수 없는 상태가 됩니다.

불칸을 디버깅 했을때 발생하는 현상인데 자세한 내용을 찾을 수 없어서 그래픽쪽 문제가 아니라면 불칸을 끄면 해결이 됩니다.

끄는 방법은 unity6에서 player > other settings > auto graphics API (선택해제) > OpenGLES3 를 위쪽으로 끌어 올리면 됩니다. 

디버깅 후 auto graphics API 값이 기본 enable 이므로 check 하시면 됩니다..





2024년 12월 31일 화요일

SPUM 사용법 정리


SPUM 

 SPUM 은 Unity 에서 사용하는 2D 픽셀 디자인의 캐릭터를 생성할 수 있도록 도움을 주는 캐릭터 생성기 에셋입니다.

홈페이지는 2D Pixel Unit Maker - SPUM | 2D Characters | Unity Asset Store 오랜만에 사용하려고 했더니 Documentation 페이지 링크가 깨져 있네요.

GitHub에서 사용법을 찾긴했는데 Home ko KR · soonsoon2/SPUM Wiki 문서가 구버전 문서라(오늘 날짜 기준, 개발자분이 미래에는 업데이트 했을 수도 있습니다.) 현재 버전과 내용이 맞지 않습니다. 개발자쪽에서 문서도 부지런히 버전업을 해야 할것 같은데... ㅡㅡ;;;

개인적으로 사용하면서 문서가 정리가 안되어 있는 듯 싶어서 사용에 필요한 부분만 정리해 보았습니다.

먼저 캐릭터를 만드는건 예전 문서를 보아도 충분히 사용이 가능합니다.

Assets>SPUM>Scene>SPUM_Scene 를 열어서 캐릭터를 생성하면 됩니다. 이건 누구나 쉽게 할 수 있어서 패스...


177 버전이고 Unity 6에서 사용하는 중인데

Random color check시  동작이 제대로 동작이 안되는 문제가 있습니다.





색상 버튼을 한번 눌러주면 정상 반영됩니다.

SAVE UNIT 버튼을 눌러 저장을 하면 Assets>SPUM>Resources>Units 폴더 아래에 저장이 됩니다.

사용하기

이제 불러내는 방법을 배우기 위해서는 Sample Scene를 열어봅니다.

Assets>SPUM>Sample>Scene>Sample 입니다.

지금까지 저장했던 모든 Unit들이 보여집니다. 기본 2개 포함

그런데 기본 2개는 뭔가 오류가 계속 발생합니다. ㅡㅡ. 


(기존 생성된 2개에서 오류가 발생한 것이라) 나머지 생성된것을 사용하면 되니까 오류는 무시해 줍니다. 

구조를 살펴보면 PlayObject 아래 PlayerObject가 있습니다. PlayObject는 Scale이 5임을 참고해서 실제 prefab에서 가져오면 scale을 조정하도록 해야 합니다.



그리고 PlayerManager에서 Assets>SPUM>Resources>Units 여기 폴더를 보고 해당 위치에 있는 모든 Units들을 보여주는 형태라(PlayerManager 소스 코드에서는 GetPlayerList() ) 만약 개인적인 프로젝트에서 사용하려면 Prefab을 복사해서 drag drop 형태로 사용하면 되는데 상위에 Object를 하나 만들고 그 아래에 Unit을 배치하도록 합니다.

여기 예제에서는 SamplePlayer prefab에 SPUM Unit을 부착하게 되는데,

SamplePlayer prefab에는 collider 2d와 rigidbody2d가 기본으로 있는 prefab입니다.

그리고 PlayerObj도 있는데 여기에 SPUM 기본 처리를 위한 부분들이 있어서 수동으로 Prefab을 꺼낼때 추가해주는것이 좋습니다.

그래서 개인 프로젝트에서 사용할때는 SamplePlayer 를 꺼내주고 그 아래에 SPUM 자식으로 넣어주고 Prefebs에 SPUM 연결해주면 됩니다.




넣은 SPUM object의 RectTransform은 Reset해주도록 합니다.

이러면 기본적인 동작 자체는 잘됩니다. 이동하는 것은 기존 object처럼 움직이면 됩니다.
애니메이션관련해서는 아래와 같은 코드를 이용하면 쉽게 사용이 가능합니다.

PlayerObj.cs 파일의 Update() 함수에 테스트를 위해서 일부를 추가 하였습니다. 마우스 왼쪽 버튼을 누르면 동작하는 코드입니다.
    void Update()
    {
        if(isAction) return;
        // 추가한 코드 ----
        if(Input.GetMouseButtonDown(0)){
            SetStateAnimationIndex(PlayerState.ATTACK, 5);
            PlayStateAnimation(PlayerState.ATTACK);
        }
        // 추가한 코드 ----

        transform.position = new Vector3(transform.position.x,transform.position.y,transform.localPosition.y * 0.01f);
        switch(_currentState)
        {
            case PlayerState.IDLE:
           
            break;

            case PlayerState.MOVE:
            DoMove();
            break;
        }
        PlayStateAnimation(_currentState);

    }

추가한 코드를 살펴보면 PlayStateAnimation은 공격인지 idle 동작인지를 선택하는 함수이고, SetStateAnimationIndex함수는 공격이라면 세부 항목을 선택하는 함수입니다. 공격의 경우 여러 모션이 있기 때문에 순번을 선택하는 것입니다.

SPUM 아래 ATTACK_List 살펴보면 아래와 같은 6개 리스트가 있음을 알 수 있습니다.


요약

지금까지 내용을 요약 하자면 아래와 같습니다.

1. Assets>SPUM>Scene>SPUM_Scene 를 열어서 캐릭터를 생성

2. SamplePlayer Prefab 꺼내고 아래에 SPUM Unit을 자식으로 만들고 붙여줍니다. RectTransform Reset 필요

3. Scale 맞춰 줍니다.

4. PlayerObj.cs base로 SamplePlayer Prefab 추가 수정 필요

5. 이동은 기존 Object와 동일 Animation 은 SetStateAnimationIndex, PlayStateAnimation 사용합니다.


추가 업데이트

실제 사용하다보니 Object 뒤로 가림 문제가 있네요. Sprite Renderer 의 Order in Layer 숫자를 조절을 해야하는데, Unit은 여러개의 파츠로 나뉘어 있다보니 조정하기 쉽지 않습니다.

SPUM Unit 아래에 UnitMain 자식이 있는데요.  여기 보면 Sorting Group 이라는 컴포넌트가 있습니다. 여기 있는 Order in layer를 조절하면 됩니다.

반영전 : 기본값 5


200으로 반영 후 (이건 프로젝트 마다 적절한 값을 조절 필요) 수정 후 건물 보다 앞으로 나왔습니다.







2024년 12월 28일 토요일

PixelArt Sprite 확대할때 방법 비교

Aseprite에 이미지 확대하는 방법이 3가지가 있습니다. 확인해본건 아니지만 다른툴에서도 여러가지 방법이 존재할것입니다.


그중에서 픽셀 작업한 이미지 확대할 때는 2가지 정도를 사용하는데요. 두가지를 비교해보았습니다.



16*16으로 작업한 이미지를 두배 확대한 이미지입니다.
자세히보면 Nearest-neighber에서는 부드러운 느낌이 없습니다. 픽셀만 커졌는 상태입니다.

Rotsprite 방식
보간(interpolation)을 통해 픽셀 아트를 확대하거나 회전하는 경우 기존의 픽셀 정렬과 색상을 최대한 보존합니다.

Nearest-Neighbor 방식
가장 가까운 픽셀의 색상을 복제해서 이미지를 확대하는 간단한 알고리즘입니다.

픽셀 이미지는 Rotsprite 사용하시면 좋습니다.

2024년 12월 25일 수요일

일본에서 많이 사용하는 한자

 일본어 글자셋 사용 빈도를 구하는 코드를 만들었습니다.

물론 GPT를 사용하긴 하였지만 100%완벽하게 만들어주는 것이 아니다 보니 이것저것 수정 하고 원하는 대로 출력을 하기 위해서는 역시 사람의 손이 좀 필요하였습니다.

사용 용도는 unity TMP 글자셋에서 일본어 한자를 넣는 범위를 어떻게 해야할지 모르는 상황에서 최소한의 한자를 넣기 위해서 만들어 봤습니다.

지난글 참고 바랍니다.

https://swlock.blogspot.com/2024/12/unity-textmeshpro.html


대략 일본에서 유명하다는 web site 몇개 첫페이지만 가져와서 사용하는 문자들을 빈도순으로 나누었습니다. 아무래도 html 그대로 사용하다보니 영문 사용 빈도가 높았습니다. 

웹페이지를 분석해보니 아래 정도 글자를 사용하고 있습니다.

Summary:
Basic Latin:0000-007F : 98
Latin-1 Supplement:0080-00FF : 4
General Punctuation:2000-206F : 6
Letterlike Symbols:2100-214F : 1
Number Forms:2150-218F : 2
Arrows:2190-21FF : 1
Mathematical Operators:2200-22FF : 1
Enclosed Alphanumerics:2460-24FF : 1
Box Drawing:2500-257F : 1
Block Elements:2580-259F : 1
Geometric Shapes:25A0-25FF : 1
Miscellaneous Symbols:2600-26FF : 5
Dingbats:2700-27BF : 2
CJK Symbols and Punctuation:3000-303F : 20
Hiragana:3040-309F : 75
Katakana:30A0-30FF : 81
CJK Unified Ideographs:4E00-9FFF : 1382
Variation Selectors:FE00-FE0F : 1
Halfwidth and Fullwidth Forms:FF00-FFEF : 44
Specials:FFF0-FFFF : 1


간략하게 요약하자면 아래에서 파란부분은 모두 넣고

히라가나U+3041 - U+3096,

U+309D, U+309E

가타카나

전자/반자 문자

U+30A1 - U+30FA, U+30FC
간지CJK 통합 한자 참조
더블바이트 숫자FF10 - FF19

추가로 일본어에서는 아래 영역도 많이 사용합니다.

CJK Symbols and Punctuation:3000-303F : 20

、 「。」【】々『』〜〇〈〉《》〆〒〝〟

Halfwidth and Fullwidth Forms:FF00-FFEF : 44 위의 더블 바이트 숫자 영역입니다. 범위가 넓은데 확인해보니 아직 정의가 안된  부분이 있습니다. 위에 더블바이트 숫자영역만 사용하면 됩니다.

!/~)?(&:10%2+|Jx4=-R<>7heDAK3@FarnitSGWTV68Z

그리고 나서 CJK 한자 영역을 추가하면됩니다.

CJK Unified Ideographs:4E00-9FFF : 1382

天日新年気楽報月話品中晴時見人更連一最用曇作情生地載世界大小無間料子説公画開予本結転方事分雪知代場会女意検覧注愛上記全完像雨国出関県異前合恋入商選物特器現定集始索家電約象済高式者水行金今力有円通実文東市産俺取書買魔能自表山使目車美探週利得田法主登機社保北読所以別規海運役土強動信外手投部具計星語士死設発録後要版化感食花放心変体険容名明言様線安移道着介風初解次族納配加神庫二悪総送戦歴節理性稿直創正業冬対応除示季原野込師売都来広超平男止空題雲販験比台度近貴洗葉婚協石伝図降回較口評教紹和的提真万落点震横税藤店値旅暮音編防数木不受字夫元朝論遷終銀住積布指収酒好災向募島続長毎便内問学短達周活育冷炊況司末謝波付籍競各寝極覚味速切賞専過身下購証引番命員試術芸型津先省衛頃阪撃史累王氏件輸必町告資充宅座域優香房薬京飯装児確組他念福門催症去識嬢奇参乙思束境了貯期観光額雑医失追調剤務供占職申反立徹粉戸幸岡率私良温寒夜賀令華鮮訴効府券馬損太限位辺再工白典屋視企流持村区幼未染断急尋南履当焼濯鍋三球童郵求在違由決底策満宮暖十西測火重裏可僕億成亡霊宇須休号価何割削誌健菓経奈届紅多輪蔵熱科写井春歌素眠河疲娘戻雷四川詩弱属隊虚塚殺馴宙展恵詳客校株掲洋待獲康護豊富団修歳独泣漫息返麗夏千剣側友相談路紫影佐攻復勇双黒夢笑同駆居害破角査故百級背融費施半倍量靴敗財菜果消仕古飾民喫聴宿想簡単局認案願個針改任港第圧避振肥早夕基闘邪那崩荒帰監存嫁怪略幕少勝刀政八刑罪退軍谷膏駅松標犬館条斎忘突将概採揃沢閉貨腕択抜挙製梅援飲印弁顔室掛敷枕材技格託快適倒善処塾甘静倉油睡婦趣髪岩常服霜差散畿算響旧治労領禁奴否舎君冒武姫壊詰勤媒帝御浜織痛張遺議緑声庁増交賠償袋準端昇委助状免責項宝括訳際酎筋肌類米種酸玩習凍舗置抱猫備究講絵頭暗面停請欲却歯郎羽豆歩残進綺迎青警飛潟鹿九州沖縄城崎細許考密苦逐昔態園隷似秘七呼聖拒導溺起遠志病獄魅紀焦親系滅含虎営巨渡堀犯陽俳寿屍劇還判兆推環討救窓永湾祭湿鑑勢鏡肉魚茶赤柔軟掃扱即軽管祝帯植就映街贈簿徴枚清媛伊坂脳浴液慌担呆躍剛彩芽猛暑片隈斗晶黄砂傘艇釣桜枠札幌釧仙覇甲陸並低氷森秋形鳥旦契遇慢余裕鋼仲騎醒偽忌抗龍贄礼澤跡孤負囲麻批打非敵秀讐奪被陰謀譚賭囚才謙堅努固爵練誰既輩垢畜支軒脇功絶浩蛍辞押減炎湘奨授諮燃涙聞与迷秒挑恐守扇眼乳炭耗仏箱乗紙冠仮奏制航拠等泊審訪稼鹸塩革磨浄草湊圭吾樹薄宰鼻酔粧包丁暴走献射至玉茨梨兵曜彼毒舟匿稚握傲椎執蒼伏章荻離庭畑酷希弟煽糸英雄錬嫉妬賢匠辛怖懸萌頼召喚逢若課芯遭晨紡幻伽鬼賊博宵瞳咲首貰折妖暁鈴浪訓籠船皇兄妹竿埋構頑漢袖襟寺忠句棚遼騙霧架興嗜替滑輝敦彦擢到瑠沈黙惑官因困荷鉄併列般驚琴漏整撮摘寂曲権義暫濃傑板搭継縮沸丹贅旬叶狙該我寄宏帽兼精穀麺惣漬干泡壇筒膳厨操泳畳照壁毛農建築研鉢造飼衣葬刷骨董煙吹弦票療払複肢係望顧促透及延洒貼酬揺尽奮栄養滴豪乾燥覆昼伴沿央埼栃群岐阜滋根徳熊勘瘴狂紛律筆嘘卜涼泉孫刻羊唐杯叩潰懐稀惚尊敬六阻析激鞘喬褒丼繰是塞亘妙隠敢櫻岾貞曹洛呉妄彰両馨露柊統鉱脱鋸鎚杉峯捧又誘拐励槍普尉冥榮程毘沙艦蔑皆虹喜朗嶺諦裸副母梢絡誤致滝巻克逆純蛮尾虐歓俊哉爽佳誕橙乃著之携呈貸均疑裁靖綿肺訟控衝拡預巡争候補輔愕緯菅五毅鎮烈舞吉李緊厚債泰倫衆咳諭徒逮捕塗難仰例威嚇血芦接触察團遥久招賑共璧源唱潤弾斉騒萎悲騰跳慎芝益旨汚換勧絞頂

한자의 경우 웹사이트에서 추출한 1382자를 사용해보고 누락되는 글자를 추가해 주는 것도 좋을 것 같습니다.
CJK 통합 한자일반4E00 - 9FEA
확장 AU+3400 - U+4DB5
CJK 호환용 한자F900 - FA6D

그리고 이번에 웹사이트를 분석해보니 CJK 통합한자 중에 확장A, CJK 호환용 한자는 사용 안하고 있었습니다.


일본어 unicode 영역 정리

3000-303F,3041-3096,309D,309E,30A1-30FA,30FC,FF10-FF19,4E00-9FCB

4E00-9FCB 영역은 필요에 따라 삭제하고 위에 있는 글자셋으로 입력하면 됩니다.


여기에서 사용했던 코드는 아래 링크 참고 바랍니다.

https://github.com/donarts/sourcecode/blob/main/python/example/_80_gather_charset/TextExtractionCharset.py