아주 오랜만의 새 글이다.

 

24년 7월, 첫 1인 개발 게임을 출시했다.

설치자 수는 친구들 포함 20명 내외... 두번째 세번째 게임을 내면서 더 발전했어야 하는데 그렇게 하지 못한 것을 반성. 그냥 거기서 멈춰버렸다. 멈추려고 한 건 아닌데, 결과적으로 멈춘 게 되었다.

 

24년 말에는 드디어 대학원이란 것을 졸업했고

25년부턴 한 스타트업에 개발팀장으로 들어가게 되었다.

기존의 게임들 뜯어고치느라, 마이그레이션하고, 리뉴얼 하느라, 그 이후로는 정신없이 일했지 뭐.

 

26년 2월. 이제서야 25년 전체도 다시 되돌아본다.

5개의 레거시 게임들을 정비하고, 2개의 신규 게임을 론칭하고, 세일즈 현장에 출장도 다녔다.

대표님의 사고방식, 소통방식이 처음에는 (세대 차이 때문인가...) 상당히 답답했는데 이제 완벽 적응했다.

팀원들과의 관계도 너무나 만족스럽다. 척하면 척, 나와 합을 적극적으로 맞춰준다. 이게 참 감사하다.

 

회사도 성장, 팀도 성장하기를 항상 바라면서도...

이 회사를 떠난 나의 커리어를 항상 생각해야 한다는 경각심이 있다.

초지능 오는 순간... 뭐하지?

크롬 브라우저 캐시 때문이었다. 인터넷 사용 기록 삭제 > 캐시된 이미지 및 파일을 삭제해주니 해결되었다.

 

참고: https://discussions.unity.com/t/linkerror-webassembly-instantiate/809139

Unity WebGL 빌드 중 에러.

Player Settings > Publishing Settings > Decompression Fallback 을 enable 해주자.

 

https://docs.unity.cn/kr/2023.2/Manual/webgl-deploying.html

 

Deploy WebGL application - Unity 매뉴얼

To deploy a WebGL build, you must configure your server and make sure you’re using the correct response headers, so that the browser receives a proper response and processes it correctly.

docs.unity.cn

 

참고자료:

https://www.dragonflydb.io/error-solutions/unable-to-locate-package-redis-stack-server

 

Redis를 설치하려고 할 때 "Unable to locate package redis-server" 오류가 발생하는 이유는 패키지를 검색할 때 사용되는 소스 목록에 해당 패키지가 포함되지 않았기 때문이다. 이는 우분투와 같은 리눅스 배포판에서 기본적으로 활성화되지 않은 Universe 리포지토리 때문일 가능성이 크다. 이 글에서는 문제의 원리를 자세히 설명하고 해결 방법을 제시한다.


Universe 리포지토리란 무엇인가?

Universe 리포지토리는 우분투에서 제공하는 추가적인 패키지 저장소이다. 기본적으로 우분투는 안정성과 보안성을 높이기 위해 공식적으로 지원하는 Main 리포지토리만 활성화되어 있다. 그러나 Main 리포지토리에는 모든 소프트웨어가 포함되지 않으며, 많은 오픈 소스 소프트웨어는 Universe 리포지토리를 통해 제공된다.

redis-server 패키지도 Universe 리포지토리에 포함되어 있어, 이를 활성화하지 않으면 설치할 수 없다.


패키지 소스 목록과 업데이트의 원리

리눅스 배포판에서 패키지 관리자는 소스 목록에 따라 설치 가능한 패키지를 검색한다. 우분투의 경우, 이 목록은 /etc/apt/sources.list 또는 /etc/apt/sources.list.d/ 파일에 저장된다.

sudo apt-get update 명령어는 이 소스 목록을 기반으로 최신 패키지 정보를 가져와 로컬 캐시를 업데이트한다. 따라서 소스 목록에 Universe 리포지토리가 추가되지 않았다면 apt-get 명령어는 redis-server와 같은 Universe 패키지를 찾을 수 없게 된다.


문제 해결: Universe 리포지토리 활성화

이 문제를 해결하기 위해 Universe 리포지토리를 활성화하고 패키지 목록을 업데이트하는 절차는 다음과 같다.

1. Universe 리포지토리 활성화

Universe 리포지토리를 활성화하면 패키지 관리자가 이 저장소에 접근할 수 있게 된다. 이는 아래 명령어로 가능하다:

sudo add-apt-repository universe

이 명령어는 시스템의 /etc/apt/sources.list 파일에 Universe 리포지토리의 주소를 추가한다.


2. 패키지 목록 업데이트

Universe 리포지토리를 추가한 후, 패키지 목록을 업데이트하여 새로운 저장소의 패키지 정보를 가져와야 한다. 이를 위해 아래 명령어를 실행한다:

sudo apt-get update

이 과정을 통해 패키지 관리자는 Universe 리포지토리의 패키지들을 로컬 캐시에 저장한다.


3. Redis Server 설치

이제 redis-server 패키지를 설치할 수 있다. 명령어는 다음과 같다:

sudo apt-get install redis-server

이 명령어는 redis-server 패키지를 Universe 리포지토리에서 검색하여 다운로드하고 설치한다.

C# 12에서는 Alias Any Type이라는 새로운 기능이 추가되었다. 이 기능은 기존의 using 문법을 확장하여, 모든 타입에 대한 별칭(alias)을 지정할 수 있게 한다. 이를 통해 코드의 가독성과 재사용성을 높이고, 복잡한 타입을 간결하게 표현할 수 있다.


Alias Any Type이란?

Alias Any Type 기능은 특정 타입이나 복잡한 제네릭 타입에 대해 별칭을 정의할 수 있는 문법이다. 기존의 using 문법은 네임스페이스를 별칭으로 정의하거나 단순한 타입만 지원했으나, C# 12에서는 보다 유연하게 확장되었다.

문법

using MyTypeAlias = FullNamespace.ComplexType<T1, T2>;

기존 방식과 비교

Alias Any Type 이전에는 복잡한 타입을 반복적으로 사용할 때 코드가 길고 읽기 어려웠다.

기존 방식

List<Dictionary<string, List<int>>> myList = new List<Dictionary<string, List<int>>>();

위와 같은 복잡한 타입을 코드 내 여러 곳에서 반복하면 가독성이 크게 떨어진다.

Alias Any Type 활용

using ComplexList = List<Dictionary<string, List<int>>>;

ComplexList myList = new ComplexList();

Alias Any Type을 사용하면 복잡한 타입 이름을 간단하게 표현할 수 있어, 코드가 훨씬 간결하고 이해하기 쉬워진다.


주요 활용 사례

1. 복잡한 제네릭 타입 단순화

Alias Any Type은 복잡한 제네릭 타입의 선언을 간단히 표현하는 데 유용하다.

기존 코드

Dictionary<string, List<int>> data = new Dictionary<string, List<int>>();

개선된 코드

using MyAlias = Dictionary<string, List<int>>;

MyAlias data = new MyAlias();

2. 타입 변경에 유연성 제공

별칭을 사용하면 나중에 타입을 변경해야 할 때, 별칭 정의만 수정하면 된다.

using DataAlias = Dictionary<string, List<int>>;

// 나중에 타입 변경
// using DataAlias = ConcurrentDictionary<string, List<int>>;

DataAlias data = new DataAlias();

3. 플랫폼 간 호환성 개선

Alias Any Type은 특정 플랫폼 또는 프레임워크에 종속된 타입을 추상화하여 코드의 이식성을 높일 수 있다.

#if NET6_0
using MyLogger = Microsoft.Extensions.Logging.ILogger;
#else
using MyLogger = System.Diagnostics.ILogger;
#endif

public class App
{
    private readonly MyLogger _logger;

    public App(MyLogger logger)
    {
        _logger = logger;
    }
}

4. 타입 안전성과 가독성 향상

복잡한 제네릭 타입의 경우, Alias Any Type으로 타입 이름을 단순화하면 의도를 더 명확히 전달할 수 있다.

using UserPermissions = Dictionary<string, List<string>>;

UserPermissions permissions = new UserPermissions();

위 코드에서는 permissions가 "사용자 권한"을 나타낸다는 의도를 명확히 알 수 있다.


제약 사항

  1. 별칭 범위 제한
    별칭은 정의된 파일 내에서만 유효하다. 파일 간에 공유하려면 별칭을 공용 클래스로 정의해야 한다.
  2. 별칭 중첩 사용 불가
    별칭을 다른 별칭 안에 중첩해서 사용할 수 없다.
  3. 의미 모호성
    너무 많은 별칭을 정의하면 코드를 읽는 사람이 실제 타입을 추론하기 어려울 수 있다.

활용 팁

  • 가독성을 높이는 이름 선택: 별칭은 간단하면서도 의도를 명확히 전달할 수 있는 이름으로 지정해야 한다.
  • 프로젝트 전체 적용: 복잡한 타입을 여러 파일에서 사용한다면 공통 별칭 정의 파일을 만들어 관리할 수 있다.
  • 필요한 경우에만 사용: Alias Any Type은 복잡한 타입에 적합하며, 단순한 타입에 사용하면 오히려 코드가 더 복잡해질 수 있다.

2D 텍스쳐 크기가 커도 너무 컸다.

4000*3000 정도도 있고... 작은 것도 있긴 하고... 뭐 다양했다.

모바일 빌드하니 fps 가 12가 나왔다. 헉.

유니티 프로파일러를 켜보니 CPU보다 GPU가 문제였다.

아. 그냥 렌더를 하는데 너무 많은 부하가 있구나.

 

3D 만들 때는 Mesh로 유니티가 알아서 Occlusion culling을 해주고

Static으로 설정해준 오브젝트들도 연산이 가벼워지는 이점이 있었는데

2D에서는 이런 테크닉이 전혀 작동하지 않는다.

 

따라서 렌더 자체를 덜 하게 하기 위해 Frustum culling 코드를 테스트해보았다.

        private void Update()
        {
            foreach (var spriteRenderer in _childSpriteRenderers)
            {
                var spriteBounds = spriteRenderer.bounds;
                spriteRenderer.enabled = IsBoundsVisible(spriteBounds);
            }
        }
        
        private bool IsBoundsVisible(Bounds bounds)
        {
            var cameraPlanes = GeometryUtility.CalculateFrustumPlanes(mainCamera);
            // Check if the sprite's bounds intersect with the camera's frustum
            return GeometryUtility.TestPlanesAABB(cameraPlanes, bounds);
        }

 

잘되는걸!!!!

12fps 에서 60fps (모바일 최대 설정)까지 끌어올려졌다.

구글 플레이 콘솔에서 새 앱 번들 올릴 때 이런 경고가 뜬다면, 버전 숫자를 이전보다 낮게 적어서 그렇다.

예를 들어 0.0.249 라면, 0.0.250 이상의 번호를 써주자.

 

누군가 버튼 박스가 필요하다고 100만원짜리 버튼 키트를 주문하려고 하길래...

"그거 만들어서 쓰면 되는데!" 한마디 했다가 직접 만들어주게 되었다.

 

참고한 영상은 이건데, led는 굳이 할 필요가 없으므로 버튼, 저항 (10K옴 사용), 케이블 등 설명한 부분을 참고했다.

https://youtu.be/yBgMJssXqHY?si=D35ELMQ_k1ETEB0Y

 

버튼 7개 필요하다 해서 연결 완료한 모습
완성한 모습

 

완제품 룩을 위해.. 그리고 사용자 안전을 위해 박스를 잘라서 씌워놓았다.

아두이노 뽀짝뽀짝 만들고 있으면 참 재밌다.

잘못하면 CPU 태워먹기도 한다는데, 어떻게 하면 태워먹을 수 있는지 모르겠지만 일단 이번엔 안 태워서 다행이다.

물론 12시간이 나의 최장 디버깅 시간은 아니다. 그러나...

이번 디버깅은 역대급 끈기를 요구했는데, 그 이유는 무엇이 문제인지 전혀 감이 오지 않았기 때문.

결론적으로 모든 커밋을 하나하나 따라가보며 언제 빌드가 망가졌는지 보기로 했다.

 

문제가 된 커밋은 바로 변경사항이 2700개 정도 되었던, 안드로이드 개발 환경에서 커밋했던 내역이다. 왜이렇게 많은 변경사항을 한꺼번에 커밋하였나? 하.. 원래는 그러지 않으려고 하는데, 이번 업데이트 때 파이어베이스 연동과 언어 현지화를 구현하면서 여러 외부 SDK, UI와 씬 작업 내용까지 섞여서 거대 커밋이 되어버렸다. (안드로이드에서 나눠서 커밋할걸. 그러나 나는 너무 피곤했었다.ㅎ)

이걸 iOS에서 열어서 빌드를 하니 문제가 생긴 것이다.


디버깅 과정

0. Cocoapods

xcworkspace 파일이 생성되지 않아서 pod 싹 제거 후 homebrew로 재설치했다. ruby version 같은 건 이전에 해놓아서 큰 문제는 없었다. 이전 포스팅: https://gamemakers.tistory.com/28

 

1. GoogleMobileAds 의존성 문제로 xCode 빌드가 실패

이때의 podfile 상태: 

source 'https://cdn.cocoapods.org/'
source 'https://github.com/CocoaPods/Specs'

platform :ios, '13.0'

target 'UnityFramework' do
  pod 'Firebase/Auth', '>= 0'
  pod 'Firebase/Core', '11.0.0'
  pod 'Firebase/Storage', '10.20.0'
  pod 'Google-Mobile-Ads-SDK', '~> 10.14'
end
target 'Unity-iPhone' do
end
use_frameworks! :linkage => :static

 

platform ios 13은 원래 12였는데, Firebase의 최소 요건을 충족하면서 13으로 올렸다.

Google-Mobile-Ads-SDK가 아직 버전 오류가 남아있어 검색을 열심히 해보니 간단하게 해결이 되었다.

최신 패키지에서 해결이 된 듯하니 이걸 받아서 업데이트 해준다. https://github.com/googleads/googleads-mobile-unity

 

2. 빌드는 성공했으나 앱 크래시 시작!

오류 메시지로 이런 것이 계속 떴다~

  • libc++abi: terminating due to uncaught exception of type nsexception
  • terminating due to uncaught exception of type nsexception

구글링을 해봐도 내 환경에서 참고할만한 게 나오지 않았다. 흔한 오류가 아닐 것 같을 뿐더러 주로 Swift 앱에서 발생하는 것 같기도.

 

그래서 나는 바로 해결책을 찾기보다 원인을 찾기로 했다! ^ㅡ^

변경사항을 적당한 단위로 쪼개서, 각각이 반영된 빌드를 해서 기기에서 실행해보고 크래시가 일어나는지 확인하기로.

 

외부 패키지에서 오류가 났는가?

문제의 커밋에서 변경사항이 2700개정도 있었으므로, 일단 외부 패키지들을 각각 잘라서 보았다. 이 과정이 이미 몇 시간이 걸렸는데, 놀랍게도 외부 패키지의 문제가 아니었다.

 

아트 애셋은 아니겠거니 하며 넘겼다. 실제로 아니었다.

 

그렇다면 내 스크립트 중 하나라는 것인데. 깃 lfs 같은 것 때문에 뭔가 꼬였나? 어쨌든 디버깅한 결과 파이어베이스 관련 로직이 좀 들어있던 스크립트가 문제였다. 이 특정 스크립트를 찾아내는데 거의 6시간이 걸린 것 같다. ㅎㅎ

 

Firebase 연동 테스트하는 코드가 문제였다. 이런 부분들을 쭉 주석처리해주니 앱 크래시가 일어나지 않았다. 

 

왜일까? 모르겠다. 일단 디버깅이 끝나서 너무 즐겁다!! 급 마무리.

회사에서는 별도의 현지화 툴 없이, 전부 스프레드시트와 scriptable object로 해결했었다. 

이 경우 (프로그래머 입장에서의) 장점은, 모든 string 값의 할당 로직을 코드에서 관리할 수 있다는 것이다.

그러나 이번에 사모사에서는 좀 더 간편한 툴로 현지화를 해결하고 싶었다.


조건

  1. 일단 스프레드시트는 사용하고 싶었다. 왜냐하면 =GOOGLETRANSLATE로 언어간 자동번역이 가능했기 때문. 이 기능 정말 편리하다.
  2. 무료 솔루션. 귀찮으면 유료 솔루션 구매해서 쓰는 습관이 생기고 있었는데, 이런 단순한 것까지 유료로 하기는 싫었다.
  3. 안정성.

찾아보니 유니티의 공식 Localization 패키지가 있었다. 공식 솔루션인데다가 무료고, 에디터 GUI도 잘 되어 있고, Google Spreadsheets API와 연동이 아주 쉽게 되어 있어서 이 방식을 채택하기로 했다. 새 시트 만들기, 해당 시트에 데이터 push, pull, 누락된 데이터 무시 등등 편리한 기능들이 있었다.


세팅 방법

아래 세 영상을 참고했다. 아주 쉬운 세팅 영상이다.

그 다음, 실제 Text나 TextMeshProUGUI 컴포넌트에서 우클릭 > Localize를 누르면 Localize String Event 컴포넌트가 붙는다. 여기에서 String Reference를 눌러서 해당 데이터의 키값을 검색, 선택해주면 된다. 처음 세팅이 익숙하지 않아서 좀 걸리지, 그 다음부터는 아주 빠르고 수월하다.


주의사항

  • TMP 애셋에 Fallback Asset을 넣어주어야 한다.
  • TMP 애셋에 Multi atlas 활성화 설정을 해야 폰트가 안 깨졌다.

추가로 알아보고 싶은 것은,

  • 관련 요소가 최초로 Initialize 되는 로직과 시점 -> 게임 맨 앞 씬에 뜨는 로고가 애셋 테이블로 현지화되었으면 좋겠는데, 이때 딜레이가 있어서 스프라이트가 흰색으로 보이는 문제가 있어서 일단 기능을 꺼두었다. Localization 패키지가 어드레서블 시스템을 사용하기 때문에 어드레서블 로딩 때문에 그럴 것 같다는 예상을 하고 있으나, 아직 테스트는 안 했다.
  • TMP Font Atlas 등 최적화 -> 한국어만 넣었을 때는 Atlas 크기도 넉넉하게 늘리고 별 걱정이 없었는데, 여러 언어가 추가되면서 TMP 애셋의 사이즈가 각각 16mb를 차지하게 되고... 앱 사이즈가 갑자기 훅 커졌다. 이거 최적화하는 방법을 봐야할 것 같다.

 

 

root object에 바로 sprite renderer를 붙이지 않고, 빈 rect transform을 parent로 두고 하위에 스프라이트를 두면 된다.

그럼 부모 오브젝트의 앵커를 바꿀 수 있다.

 

오늘 나가기 전 자투리 시간에 만든 엘리베이터. ㅋㅋ

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

BEFORE

 

AFTER

 

막 그리지 말고 연구하고 그리자 ^^;;

+ Recent posts