목차

반응형
래그돌 날리는 프로젝트

 

유니티에서 래그돌을 날려버리는 부분을 구현하는데 생각보다 막혔던 부분과 어려운 점이 많았다.

큼직한 이슈가 무려 7개나 있는데 한 번에 다 쓰기에는 양이 너무 많아서 나눠서 쓰고자 한다.

위 동영상처럼 구현하려면 더 많은 기능이 필요하기 때문에 래그돌 발사하는 부분만 중점적으로 다루겠다.

 

 

유니티 ragdoll 발사하기 글 모음

1. 목차

2. 래그돌 만들기

3. 래그돌 발사하기

4. 래그돌이 날아갈 때 한쪽 축으로만 이동되게 하기

5. 현재 래그돌이 멈췄는지 안멈췄는지 판단하기

6. 래그돌 멈추기

7. 래그돌 고장 방지

반응형
반응형

유니티의 애드몹은 안내가 매우 친절해서 딱히 가이드까지 올릴 필요는 없지만 가이드에 나와있지 않은 오류들을 설명하고자 한다.

 

유니티 애드몹 광고 출시 버전 모아놓은 곳

https://github.com/googleads/googleads-mobile-unity/releases

 

googleads/googleads-mobile-unity

Official Unity Plugin for the Google Mobile Ads SDK - googleads/googleads-mobile-unity

github.com

 

2019년 8월 1일 기준 유니티 애드몹 최신 버전은 3.18.2다.

적용도중 발생한 오류들을 기재하겠다.

 

목차

1. 애드몹 패키지 resolve 오류

2. 앱 빌드, 실행 시 AndroidJavaException: java.lang.ClassNotFoundException: com.google.android.gms.ads.initialization.OnInitializationCompleteListener 오류

3. 앱 빌드, 실행 시 java.lang.RuntimeException: Unable to get provider com.google.android.gms.ads.MobileAdsInitProvider: java.lang.IllegalStateException 오류

4. 광고 테스트 시 유의 사항

 

 

1. 애드몹 패키지 resolve 오류

애드몹 패키지 임포트 후 android dependency resolve 과정

 

android resolve 도중 오류 발생

유니티 애드몹 패키지 임포트를 하면 알아서 android resolve를 진행하지만 위와 같이 Resoultion failed가 발생하면서 정상적으로 유니티 애드몹 패키지의 설치가 되지 않는다.

이 에러를 무시하고 빌드를 하게 되면 본문의 2번 오류로 이어질 가능성이 높다.

해당 에러 메시지가 난 후 Assets\Plugins\Android 경로로 이동해서 jar, aar 파일이 충분히 있는지 확인해보자

본인은 resolution failed 에러로 인하여 jar 파일과 aar 라이브러리 파일이 충분히 설치되지 않아서 빌드하고 실행하면 에러로 이어졌다.

정상적으로 설치가 되었다면 아래와 같이 파일들이 있어야 한다.

유니티 애드몹 패키지 정상 설치

해결 방법

간단하다. 현재 본인이 설치한 유니티 애드몹 패키지보다 높거나 낮은 버전으로 설치해본다.

본인은 2019.1.8f1 버전으로 Google Mobile Ads Unity Plugin v3.18.2를 설치했으나 위의 오류가 발생해서

3.18 버전으로 설치했더니 잘 작동했다.

https://github.com/googleads/googleads-mobile-unity/releases/tag/3.18.0

 

googleads/googleads-mobile-unity

Official Unity Plugin for the Google Mobile Ads SDK - googleads/googleads-mobile-unity

github.com

 

 

2. 앱 빌드, 실행 시 AndroidJavaException: java.lang.ClassNotFoundException: com.google.android.gms.ads.initialization.OnInitializationCompleteListener 오류

1번의 연장선상의 문제인데 이 에러는 되게 자주 보는데 설치가 정상적으로 안됐을때 뿐만 아니라 설치를 정상적으로 했지만 라이브러리에 필수 요소가 빠져있거나 하면 자주 만나는 오류 메시지다.

스스로 원인을 파악하고 해결 방법을 찾아내는 방법 과정에 대해서 말하겠다.

 

저 오류 메시지는 라이브러리 파일안에 OnInitializationCompleteListener 클래스가 없어서 발생하는 오류다.

일단 Assets\Plugins\Android 폴더로 가면 aar 파일과 jar 파일이 있다.(본인의 경우 해당 오류가 발생했을때 unity-plugin-library.jar 파일 하나밖에 없었다.)

파일을 압축해제 프로그램으로 열어보면

 

해당 라이브러리는 com.google.unity.ads 형식의 구조를 가지고 있음을 알고 있다.

아까 오류 메시지에서는 com.google.android.gms.ads.initialization.OnInitializationCompleteListener이라고 했으니까 구조가 달라도 한참 다르다는 것을 알 수있다.

(이번에 발생한 오류는 위의 오류가 아니지만 jar 파일들을 뒤지다가 오류와 똑같은 구조지만 에러가 발생하는 경우는 라이브러리의 버전이 달라서 발생하는 오류다.)

그렇다면 도대체 어떤 라이브러리를 받아야 저 클래스가 있는지 확인해보도록 하자

 

확인하는 방법은 maven 라이브러리 사이트로 가서 해당 구조를 검색하면 된다.

https://mvnrepository.com/artifact/

 

Maven Repository: com.google.android.gms » play-services-ads

 

mvnrepository.com

 

어느정도 상위 구조에서 검색을 한다. com.google.android.gms로 검색을 하였다.

검색 결과가 매우 많다. gms 아래에 다양한 서비스가 존재해서 그렇다.

그 중에 우리는 ads에 대하여 설치를 했지만 오류가 생긴것이니까 3번째가 가장 의심된다. 들어가보도록 하자

 

다양한 버전이 있는데 최신 버전을 눌러보자

 

Gradle을 선택하고 하단에 가보면 저렇게 다양한 하위 라이브러리들이 존재함을 알 수 있다.

분명 저중에 하나의 라이브러리는 com.google.android.gms.ads.initialization.OnInitializationCompleteListener 클래스를 가지고 있을 것 이다.

하지만 나한테는 지금 하나의 jar 파일밖에 없으니까 라이브러리의 설치가 정상적으로 진행되지 않았음을 짐작할 수 있다.

시도해볼수있는 방법은 저 라이브러리를 수동으로 다운받아서 넣어주거나 gradle에다가 넣어서 설치해봐도 되지만 android resolve 과정에서 오류가 났기 때문에 쉽게 해결될 오류가 아니라서 유니티 애드몹 버전을 낮추거나 높여서 설치해보도록 하자.

 

 

3. 앱 빌드, 실행 시 java.lang.RuntimeException: Unable to get provider com.google.android.gms.ads.MobileAdsInitProvider: java.lang.IllegalStateException 오류

 

앱을 빌드하고 실행하면 바로 종료되면서 아래 오류가 발생한다.

 

유니티 애드몹 키값을 입력하지 않아서 발생하는 문제다.

 

위의 경로로 따라가서 app id를 기입해주면 정상작동 한다.

 

4. 광고 테스트 시 유의 사항

요건 나도 몰랐던건데 애드몹에서 광고 작동 테스트를 하려고 광고 호출할때는 항상 테스트 광고를 사용하라는 경고가 있다.

광고를 탭하지 않아도 정지당할 수 있다니까 꼭 유의해야겠다.

반응형
반응형

유니티 프로젝트를 공유하거나 버전관리를 하다보면 한가지 의문이 생기는 부분이 있다.

라이브러리 폴더가 왜 그렇게 용량을 많이 먹는거고 꼭 있어야하는건지, 라이브러리 폴더는 어떻게 해야할지 의문이 생겨서 확인해보고 결과를 찾았다.

결론만 얘기하자면 라이브러리 폴더는 반드시 있어야한다.

 

목차

1. 유니티 오피셜

2. 웹상의 의견

3. 본인 경험, 회사 상사 의견

 

 

1. 유니티 오피셜

라이브러리 폴더가 무엇을 하는 것인지는 유니티 오피셜을 인용하자면

"Unity는 에셋 폴더에 추가하는 파일을 읽고 처리하여 파일 콘텐츠를 데이터의 내부 게임 레디 버전으로 변환합니다. 실제 에셋 파일은 수정되지 않은 상태로 유지되고 처리 및 변환된 데이터 버전은 프로젝트의 Library 폴더에 저장됩니다.

이 폴더는 캐시 폴더와 유사하다고 생각할 수 있습니다. 사용자는 라이브러리 폴더를 수동으로 변경하지 않아도 되며, 변경하려고 하면 Unity 에디터에서 프로젝트의 기능에 영향을 미칠 수 있습니다. 하지만 프로젝트가 열려 있지 않은 라이브러리 폴더를 Unity에서 언제든지 안전하게 삭제할 수 있습니다. 이 폴더의 데이터는 모두 에셋 및 ProjectSettings 폴더에 저장된 데이터로부터 생성되기 때문입니다. 따라서 라이브러리 폴더를 버전 관리에 포함시켜서도 안 됩니다."

간단하게 생각하면 프로젝트의 캐시데이터를 저장하고 라이브러리 폴더를 굳이 버전관리에 포함 안해도 ProjectSettings나 Asset폴더에 세팅값이 있어서 캐시데이터가 다시 생성되기 때문에 버전 관리에 포함 시켜서 안된다(왜 안해도 된다가 아니라 하면 안되는것처럼 표현했지)고 적혀있다.

https://docs.unity3d.com/kr/current/Manual/BehindtheScenes.html

 

씬의 기반 작업 - Unity 매뉴얼

Unity는 에셋을 자동으로 임포트하고, 에셋을 임포트하는 데 사용할 임포트 설정과 프로젝트 전반에서 에셋이 사용되는 위치 같은 에셋에 대한 다양한 추가 데이터를 관리합니다. 아래에는 이 프로세스가 진행되는 방법이 설명되어 있습니다.

docs.unity3d.com

여기까지 보면 Library 폴더는 없어도 무방한 것처럼 보인다.

라이브러리 폴더가 겁나 크기 때문에 걸리적 거리기도 한다. 나 같은 경우는 라이브러리 폴더만 2~3GB한다.

 

2. 웹상의 의견

대충 찾아보면 웹상 의견도 없애도 되고 어차피 알아서 생성되니까 필요없다고 한다.

Library 폴더에 대한 웹상의 의견

대부분은 삭제해도 괜찮음, 버전관리에서 포함시킬 필요없음.

이라고 한다.

 

3. 본인 경험, 회사 상사 의견

사실상 이 글의 핵심인데 내가 말하고자 하는 것은 다르다.

일단 나의 경험상으로는 분명히 Library를 삭제해도 된다고 나와 있어서 프로젝트의 Library 폴더를 삭제하고 다른 팀원한테 프로젝트를 공유해봤더니 특정 데이터의 Inspector의 값이 제대로 안들어가져 있어서 프로젝트가 정상적으로 돌아가지 않았다. 그래서 Library 문제가 맞는지 다시 한번더 전부다 압축해서 보낸뒤 구동해봤더니 정상적으로 구동됐다.

따라서 Library 폴더를 삭제하면 Cache 데이터가 100% 일치하지 않음을 확인했기 때문에 유니티의 공식 답변, 웹상의 의견과 다르게 항상 Library 폴더를 공유해야함을 확인했다.

 

이와 더불어서 회사 상사분께 내가 겪은 문제와 웹상에서 찾아본 내용으로 질문을 하니 본인도 항상 Library 폴더를 같이 공유하고 있다고 하였다.

 

결론 : 라이브러리 폴더는 필요하다.

 

ps : 근데 라이브러리 폴더 압축안하면 2~3gb지 압축하면 수백mb로 용량 줄어드니까 그냥 하자

반응형
반응형

문제

문제의 메인 씬

자꾸 특정 씬으로 넘어갈 때 지연이 발생했다.

하지만 해당 씬에서는 문제가 될만한 스크립트도 없고 별 이유가 없어서 프로파일러를 켜서 확인해봤다.

 

프로파일러 내용

프로파일러로 확인해봤더니 File.Read에서만 2000개의 인스턴스가 생성되어서 지연되는 것으로 확인되었다.

하지만 나는 스크립트 상에서 파일을 읽는 부분은 전혀 넣지 않았는데 전혀 짚이는 것이 없었다.

 

게임에서 세이브 로드하는 부분을 전부다 주석 처리해봐도 효과는 없었고, 프로파일러 자세하게 돌려봐도 딱히 나오는 거 없었다.

결국 모든 오브젝트, 스크립트를 하나씩 삭제를 하면서 원인을 찾았다.

 

원인

원인은 특정 오브젝트에서 ScriptableObject(이하 SO)를 매우 많이 불러서 발생하는 문제였다.

이 SO를 엄청나게 많이 부르는 것도 문제지만 씬을 로드할 때마다 해당 오브젝트도 새로 생성되어서 약 1초 이상의 지연이 발생하는 것이었다.

헷갈릴만한 것이 유니티에서 인스펙터로 끌어다가 넣어주는 SO는 저렇게 잡혀서 잘 몰랐던 것이다.

 

이런 식으로 SO가 SO를 불러오고 그 SO가 또 다른 SO를 불러오고... 엄청난 지연이 발생할 만하다. 멍충아

렉이 저것밖에 안 걸리는 게 더 신기하다.

 

해결

SO를 대량으로 부르는 게임 오브젝트를 인스턴스, 싱글턴화, DontDestroyOnLoad 하여 게임을 시작할 때만 불러오고 이후로는 계속해서 오브젝트를 유지하는 방식으로 대체하였더니 해결되었다.

그리고 해당 문제를 겪어보니 게임에서 불러와야 하는 대량의 데이터가 있는 경우 게임을 로딩하거나 시작할 때 일괄적으로 불러오고 메모리에 항상 저 데이터를 들고 있다가 필요할 때 바로바로 꺼내와서 사용해야 한다고 생각이 들었다.

요즘 최적화와 오브젝트 풀링에 대해서 공부하고 있는데 좋은 경험이 되었다.

반응형
반응형

문제

유니티에서 게임 오브젝트에 스크립트를 드래그해서 컴포넌트로 추가할 때 아래와 같은 오류 메시지가 발생

Can't add script component 'TestScript' because the script class cannot be found. Make sure that there are no compile errors and that the file name and class name match.

또는

Can't add script behaviour TestScript ,Can't Add Script The script needs to derive from MonoBehaviour!

 

원인

유니티를 공부하는 분이 이 에러 때문에 막히고 있어서 직접 봐보았더니 스크립트명과 스크립트의 클래스명이 달라서 생기는 문제였다.

스크립트 파일 이름과 스크립트에서 구현되어 있는 클래스의 이름이 다르면 컴포넌트로 인식을 못해서 불러오지 못한다.

 

해결

스크립트나 파일명을 하나로 통일한다.

위의 경우 파일명은 TestScript고 스크립트에서 클래스 이름은 Test인데 클래스 이름을 TestScript로 바꾼다.

정상적으로 처리되었다면 다음과 같이 Add Component에서 추가한 컴포넌트가 보인다.

 

반응형
반응형

목차

1. 개요

2. 차이점

2.1 불러오는 이미지 차이

2.2 Sprtie Packer 비활성화 여부

2.3 카메라의 2D뷰 3D뷰

2.4 Directional Light 오브젝트

2.5 카메라 위치와 시점

2.6 환경 변경

3. 참고자료

 

 

 

1. 개요

유니티에서 프로젝트를 새로 생성할 때 2D 프로젝트와 3D 프로젝트를 고르는 옵션이 있다.

항상 어떤 차이가 있는지 정확히는 모르고 주변 사람들한테는 '아 그거 어차피 비슷하니까 뭘로 해도 상관없어요 그냥 3d로 하세요' 이런 식으로 말하곤 했는데 이번 기회에 정확하게 차이가 무엇인지 알아보고자 한다.

미리 결론을 말하자면 차이는 없으나 기본적인 세팅 정도만 달라진다.

 

 

2. 차이점

2D 프로젝트와 3D 프로젝트 차이점에 대하여 유니티에서 공식적으로 올라와 있는 내용

이미지를 불러올 때 텍스쳐로 불러올지 스프라이트로 불러올지 카메라 시점이 다르다 등의 내용

더욱 구체적으로는 아래와 같이 차이가 발생한다고 적혀있다.

 

2D vs 3D mode settings

2D or 3D mode determines some settings for the Unity Editor. These are listed below.

In 2D Project mode:

  • Any images you import are assumed to be 2D images (Sprites
    ) and set to Sprite mode.

  • The Sprite Packer
     is enabled.

  • The Scene View
     is set to 2D.

  • The default game objects do not have real time, directional light.

  • The camera
    ’s default position is at 0,0,–10. (It is 0,1,–10 in 3D Mode.)

  • The camera is set to be Orthographic. (In 3D Mode it is Perspective.)

  • In the Lighting Window:

    • Skybox
       is disabled for new scenes
      .

    • Ambient Source is set to Color. (With the color set as a dark grey: RGB: 54, 58, 66.)

    • Precomputed Realtime GI is set to off.

    • Baked GI is set to off.

    • Auto-Building set to off.

In 3D Project mode:

  • Any images you import are NOT assumed to be 2D images (Sprites).

  • The Sprite Packer is disabled.

  • The Scene View is set to 3D.

  • The default game objects have real time, directional light.

  • The camera’s default position is at 0,1,–10. (It is 0,0,–10. in 2D Mode.)

  • The camera is set to be Perspective. (In 2D Mode it is Orthographic.)

  • In the Lighting Window:

    • Skybox is the built-in default Skybox Material.

    • Ambient Source is set to Skybox.

    • Precomputed Realtime GI is set to on.

    • Baked GI is set to on.

    • Auto-Building is set to on.

 

 

더욱 자세하게 알아보도록 하자

 

2.1 불러오는 이미지의 차이

왼쪽 2D 오른쪽 3D

두 프로젝트 똑같은 이미지를 Asset 폴더에 넣었는데 2D에서는 기본적으로 Sprite로 불러오고 3D에서는 Default로 지정된다.

2D에서는 매우 편리하게 드래그하면 2D 이미지로 사용하는 등 스프라이트로 바로 사용이 가능하다.

 

2.2 Sprtie Packer 비활성화 여부

Sprite Packer

스프라이트 팩커는 스프라이트를 효율적으로 사용할 수 있도록 해주는 것인데 2D에서는 기본적으로 활성화되어있고 3D에서는 기본적으로 위의 이미지처럼 비활성화되어 있다고 한다.

하지만 확인해보니 둘 다 비활성화 처리되어있었다.

 

2.3 카메라의 2D뷰 3D뷰

왼쪽 2D 오른쪽 3D

2D의 경우 2D버튼이 눌려있어 기본적으로 2D뷰가 활성화되어있고 3D는 꺼져있어서 기본적으로 3D뷰로 보인다.

시각적인 차이라서 큰 차이는 없어서 2D 프로젝트 3D 프로젝트 둘 다 번갈아가며 사용하는 기능이다.

 

2.4 Directional Light 오브젝트

왼쪽 2D 오른쪽 3D

3D에서는 기본적으로 Directional Light 오브젝트가 생성되어있다.

 

2.5 카메라 위치와 시점

왼쪽 2D 오른쪽 3D

2D의 카메라는 기본적으로 Y값이 0이고 3D 카메라는 1이다.

또한 2D는 Projection이 Orthographic이고 3D는 Perspective이다.

Perspective는 기본적으로 현실적으로 원근감이 반영되는 것으로 대부분의 3d 게임에서 사용하는데 위의 말대로 3D 프로젝트 환경에서 2D 게임을 만들기 위해서 카메라 시점을 Orthographic으로 바꾸어서 사용하기도 한다.

 

2.6 환경 변경

왼쪽 2D 오른쪽 3D

프로젝트를 잘못 만들었거나 설정의 변경이 필요하다면 Edit > Project Settings > Editor > Default Behavior Mode를 바꿔주도록 하자.

 

 

3. 참고자료

https://forum.unity.com/threads/difference-between-2d-and-3d-projects.554728/

 

Difference between 2d and 3d projects

hi guys i know the difference between 2d and 3d obviously but what implications does this have on creating a project for either? the space shooter...

forum.unity.com

https://unity3d.com/difference-between-2d-and-3d-games

 

Create 2D and 3D games with Unity - Unity

You have a full-featured professional toolset in Unity to create both 2D and 3D games. Get an intro to how to do it.

unity3d.com

 

반응형
반응형

맥에서 유니티의 Application.persistentDataPath 경로에 데이터를 저장하고 불러올 때 직접 들어가서 수정을 하거나 삭제를 하려는데 해당 경로로 이동하기가 쉽지 않다.

 

윈도우는 익스플로러에 경로를 넣으면 바로 들어가지지만 맥은 그렇지 않다.

 

문제

Application.persistentDataPath 로그

MAC에서 유니티의 Application.persistentDataPath 경로는 위와 같다.

위의 경로로 이동을 하려고 한다.

 

 

해결

1. 터미널 사용

터미널

맥 하단의 터미널을 켜서 접근한다.

 

  • cd ~/Library -> 라이브러리 폴더까지 바로 이동
  • 탭을 이용하면 편리함 ex) cd /U 탭 -> cd/Users/ 이렇게 알아서 자동완성시켜줌
  • Application Support의 경우 띄어쓰기를 해야 돼서 Application 치고 탭을 눌러서 자동완성시켜주는 것이 편하다.

 

이후 rm이나 vi 명령어로 삭제나 수정을 한다.

 

2. 파인더 사용

파인더를 켠다.

파인더에서 Cmd + Shift + G 를 눌러서 경로를 입력해서 이동하는 기능을 호출한다.

 

아까 유니티에서 콘솔에 찍힌 경로로 이동을 하려고 붙여 넣어서 Go를 누르면 아무 반응이 없다.

이는 Library 경로가 숨김 처리되어 있어서 그런데 아래 명령어를 통하여 숨김 처리를 해제하면 해결된다.

 

터미널에 다음과 같이 입력한다.

chflags nohidden ~/Library/

그리고 다시 로그에 나온 경로를 입력하고 Go를 누르면 아래와 같이 이동이 된다.

 

반응형
반응형

유니티에서 다른 언어를 지원하기 위해서 로컬라이제이션이 필요하다.

유니티에서 제공하는 로컬라이제이션 튜토리얼을 보고 로컬라이제이션을 숙지하게 되었다.

생각보다 어려운 내용이라서 소화하기 편하도록 자세하게 준비했다.

추가적으로 엑셀을 통하여 로컬라이제이션을 할 수 있는 방법까지 고려했다.

크... 배려심 오졌고

 

목차


1. 실제 프로젝트 시연

2. 로컬라이제이션 이미지 도식화

3. 로컬라이제이션 스크립트 핵심 부분 설명

4. 샘플 프로젝트 및 참고 링크

 

 

 

1. 실제 프로젝트 시연

 

실제로 로컬라이제이션을 사용하는 프로젝트 동영상

토글 형식으로 언어를 바꾸면 즉시 모든 텍스트 요소들이 언어에 맞추어 바뀐다.

한글 로컬라이제이션 데이터


영어 로컬라이제이션 데이터

각 데이터는 키와 밸류로 관리되고 있다.

눈여겨볼 점은 key값은 통일되어야 한다는 점이다.

입력은 엑셀로 하고 유니티에서 csv 파일을 json파일로 저장해서 json파일을 (정확히는 json형식의 txt 파일) 게임에서 불러와서 로컬라이제이션을 한다.

나중에 다시 설명을 할 것이지만 로컬라이제이션 세팅을 하고 나면 text 컴포넌트에 Localized Text 컴포넌트를 붙이고 텍스트에 해당하는 key값을 적어주면 알아서 언어 값에 따라서 바뀐다.



2. 로컬라이제이션 이미지 도식화

 

오늘따라 친절이 포텐터져서 이해하기 쉽도록 도식화까지 해놨다.

먼저 csv파일로 로컬라이제이션 데이터를 저장한다.

1. 유니티 에디터(LocalizedTextEditor)를 통하여 csv 파일을 json 형태의 txt 파일로 저장한다.

2. LocalizationManager에서 로컬라이제이션 json(txt) 데이터를 불러온다.

3. 텍스트 컴포넌트에 부착된 LocalizedText 컴포넌트가 키값에 해당하는 데이터를 불러와서 텍스트 컴포넌트의 텍스트를 바꿔준다.

4. 인게임에서 바뀐 텍스트가 정상적으로 출력된다.



3. 로컬라이제이션 스크립트 핵심 부분 설명

아래 4개의 스크립트에서 핵심적인 부분만 설명을 하겠다.

사용 방법은 4번 샘플 프로젝트에서 설명하기 때문에 스크립트에 대한 이해가 필요 없어도 괜찮으니 넘어가도 좋다.

 

 

LocalizedTextEditor 

 

유니티 GUI 에디터에 대한 부분은 생략하도록 하겠다. (사실 잘 모르기도 함)

 

private void LoadCSVFile()
    {
        string filePath = EditorUtility.OpenFilePanel ("Select localization data file", Application.dataPath, "csv");
        
        if (!string.IsNullOrEmpty (filePath)) 
        {
            string dataAsJson = File.ReadAllText (filePath, Encoding.UTF8);
            string[] stringBigList = dataAsJson.Split('\n');
            localizationData = new LocalizationData();
            localizationData.items = new LocalizationItem[stringBigList.Length];
            for (var i = 1; i < stringBigList.Length; i++)
            {
                string[] stringList = stringBigList[i].Split(',');
                for (var j = 0; j < stringList.Length; j++)
                {
                    localizationData.items[i - 1] = new LocalizationItem(stringList[1], stringList[2]);
                }
            }
        }
    }

csv파일을 불러오는 함수다.

윈도 창에서 Application.dataPath 경로에 해당하는 부분으로 열어준 다음에 csv 파일 확장자를 불러온다.

만약에 파일이 공백이 아니라면 UTF8로 불러와서 \n 개행 문자열로 스플릿 해서 string 배열로 가져온다.

하지만 문제점이 있는데 이상하게 csv 파일에는 항상 마지막 부분에 개행문자가 들어가도록 되어있는지 추가적으로 공백 데이터가 인식돼버린다. 조금 거슬려서 처리해보려고 노력했으나 쉽지 않아서 나중에 예외처리가 가능하니까 내버려두었다.

읽어낸 데이터를 다시 쉼표(,)로 스플릿 하여 키와 밸류로 분리한 뒤 Serializable 클래스 LocalizationData의 Items에 LocalizationItem으로 삽입을 한다.

stringList에서 0열은 인덱스라서 사용하지 않는다.

 

private void LoadGameData()
    {
        string filePath = EditorUtility.OpenFilePanel ("Select localization data file", Application.dataPath, "txt");

        if (!string.IsNullOrEmpty (filePath)) 
        {
            string dataAsJson = File.ReadAllText (filePath);

            localizationData = JsonUtility.FromJson<LocalizationData> (dataAsJson);
        }
    }

    private void SaveGameData()
    {
        string filePath = EditorUtility.SaveFilePanel ("Save localization data file", Application.dataPath, "", "txt");

        if (!string.IsNullOrEmpty(filePath))
        {
            string dataAsJson = JsonUtility.ToJson(localizationData);
            File.WriteAllText (filePath, dataAsJson);
        }
    }

SaveGameData는 csv파일로 데이터를 불러오면 해당 데이터를 json 형태로 txt 확장자로 저장을 한다. (json 파일을 읽어 올 수 있다면 json으로 저장해도 되지만 Resources.Load<TextAsset>("Texts/"+fileName);로 텍스트 데이터를 불러올 때 json으로 잘 안 불러지길래 txt로 저장하였다.)

LoadGameData는 저장된 json 형태의 txt 파일을 조회한다.

 

 

LocalizationData 

텍스트 데이터를 들고 다니기 쉽도록 포장처리하는 데이터 형태라고 보면 된다.

 

[System.Serializable]
public class LocalizationData 
{
    public LocalizationItem[] items;
}

[System.Serializable]
public class LocalizationItem
{
    public LocalizationItem(string key, string value)
    {
        this.key = key;
        this.value = value;
    }
    public string key;
    public string value;
}

너무 간단하다.

json으로 데이터 읽고 쓰기 가능하도록 Serializable 선언

하나의 LocalizationItem의 배열 형태 items에 현재 언어에 해당하는 LocalizationItem 데이터를 전부 다 넣고 나중에 key로 조회를 한다.

 


LocalizationManager 

로컬라이제이션을 총괄하는 스크립트다.

게임의 곳곳에서 참조를 해야 하기 때문에 싱글턴 패턴으로 선언한다. 싱글턴 패턴 설명에 대해서는 생략한다.

 

missingTextString는 키값이 없어서 불러오지 못한 텍스트에 대한 기본값이다.

fileName은 불러와야 하는 파일명으로 eng.txt면 eng를 넣으면 된다. 자세한 설명은 나중에 하겠다.

 

public void LoadLocalizedText(string fileName)
    {
        localizedText = new Dictionary<string, string> ();
        TextAsset mytxtData=  Resources.Load<TextAsset>("Texts/"+fileName);
        //파일 정상적으로 읽어오는지 확인
//        Debug.Log(Resources.Load<TextAsset>("Texts/"+fileName));
//        Debug.Log(fileName);
//        Debug.Log(mytxtData);
        string txt=mytxtData.text;
        if (txt!="" && txt!= null) {
            string dataAsJson = txt;
            LocalizationData loadedData = JsonUtility.FromJson<LocalizationData> (dataAsJson);

            for (int i = 0; i < loadedData.items.Length; i++)
            {
                //불러오는 데이터 확인
                //Debug.Log(loadedData.items[i].key + ":" + loadedData.items[i].value);
                //공백데이터가 여러개 들어가면 오류발생
                if (!localizedText.ContainsKey(loadedData.items[i].key))
                    localizedText.Add(loadedData.items[i].key, loadedData.items[i].value);
            }

            Debug.Log ("Data loaded, dictionary contains: " + localizedText.Count + " entries");
        } else 
        {
            Debug.LogError ("Cannot find file!");
        }

        isReady = true;
    }

Debug.Log는 참고용으로 주석 처리해놨는데 필요시 주석 해제하면 무엇이 문제인지 확인이 쉽다.

해당 함수는 fileName에 해당하는 즉 언어에 해당하는 txt 파일을 불러와서 딕셔너리 형태의 localizedText에 언제든 꺼내 쓸 수 있도록 전부 다 넣어준다.

여기서 아까 csv파일의 특성으로 인해 생기는 공백 데이터가 문제를 일으키는데 지금 넣어놓은 

if (!localizedText.ContainsKey(loadedData.items[i].key)) 이 부분으로 예외처리를 해서 문제는 발생하지 않을 것이다.

 

만약 저렇게 예외처리를 하지 않는다면 공백 데이터가 2개 들어가 버려서 txt 저장할 때는 문제없는데 게임 실행하면 저렇게 오류가 발생한다.

 

public string GetLocalizedValue(string key)
    {
        string result = missingTextString;
        if (localizedText.ContainsKey (key)) 
        {
            result = localizedText [key].Replace("\\n","\n");
        }

        return result;

    }

나중에 텍스트 컴포넌트에 부착된 LocalizedText에서 key로 value값을 조회할 때 사용된다. \n 치환은 개인적으로 저 부분이 자꾸 문제를 일으켜서 넣어놨는데 없애도 괜찮다. (왜있는거지 ?? 나란 괴물...)

 

public void ReloadTexts(string fileName)
    {
        LoadLocalizedText(fileName);
        foreach (LocalizedText text in FindObjectsOfType<LocalizedText>())
        {
            text.ReloadText();
        }
    }

나중에 버튼과 연동시켜서 언어를 바꿀 때 쓰는 기능이다.

유의할 점은 FindObjects 함수는 게임 오브젝트가 enabled 된 컴포넌트만 가져오기 때문에 꺼져있는 텍스트는 바뀌지 않는다.

저렇게 Reload를 한다고 전부 다 바뀌는 것이 아님을 알아둬야 한다.

컴포넌트에 OnEnable을 사용하여 별도로 새로고침 하는 과정이 필요하다.

 


LocalizedText 

Text 및 TextMeshPro에 붙어서 로컬라이제이션 값을 가져와 교체해주는 컴포넌트

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LocalizedText : MonoBehaviour {

    public string key;
    private Text Text;
    private TextMeshProUGUI TextPro;
    // Use this for initialization
    void Start () 
    {
        
    }

    private void OnEnable()
    {
        Text = GetComponent<Text> ();
        TextPro = GetComponent<TextMeshProUGUI> ();
        ReloadText();
    }

    public void ReloadText()
    {
        if (Text != null)
        {
            Text.text = LocalizationManager.Instance.GetLocalizedValue (key);
        }
        if (TextPro != null)
        {
            TextPro.text = LocalizationManager.Instance.GetLocalizedValue (key);
        }
    }
}

아까 말한 대로 OnEnable을 통하여 활성화되었을 때 새로고침을 하도록 한다.

ReloadText 함수는 텍스트 컴포넌트가 존재하면 로컬라이제이션 매니저에다가 값을 질의하여 텍스트를 교체해준다.

 

여기까지 핵심 스크립트에 대한 설명은 끝났다. 자세한 사용 방법을 알아보도록 하자.



4. 샘플 프로젝트 및 참고 링크

먼저 영상을 보고 무슨 샘플 프로젝트인지 보자. 음식 이름에 대해서 고찰해보는 프로젝트다.

 

 

한글
영어

먼저 csv 파일을 2개 준비한다.

각각 영어 데이터와 한글 데이터를 넣어둔다.

key값은 동일해야 한다.

 

Window > Localized Text Editor > LoadCSVFile

아까 만들었던 csv 파일을 불러온다.

 

한글 데이터부터 먼저 가져온다.

 

정상적으로 가져온 것 같지만 2가지 문제가 있다.

한글 데이터 인코딩 문제와 4, 5번 엘리먼트에는 공백이 담겨있다.

공백 데이터에 대해서는 우리는 예외처리를 했으니까 별 상관은 없지만 엘리먼트 숫자를 줄여서 없애도 되고 마음대로 하자

 

인코딩 문제는 다음과 같이 불러온 데이터가 깨져서 나온다.

웹 쪽 일을 조금 해봤다면 이럴 때 어떻게 해야 하는지 알 것이다.

그렇다 있는 척 좀 해봤다.

C# 내에서 인코딩 건드려서 불러와도 되고 나는 메모장에서 인코딩을 utf-8로 바꿨다.

 

요렇게 해서 덮어쓰기로 저장한다.

 

그럼 이제 잘 나온다.

 

그러면 Save data를 눌러서 위의 경로에 맞추어 저장하도록 하자

파일명은 kor eng로 하겠다.

 

영어도 똑같이 한다.

 

그리고 텍스트 컴포넌트가 붙어있는 텍스트 오브젝트에 Localized Text의 Key에 교체되길 원하는 key값을 적어 넣는다. 우리의 경우 food1이나 food2다.

 

Localization Manager 오브젝트도 넣고 인스펙터의 File Name에는 최초로 활성화하고 싶은 언어 파일명을 넣는다.

 

한글 버튼에다가 다음과 같이 온 클릭 이벤트를 걸어놓는다.

영어 버튼에다가는 eng를 넣으면 되겠다.

 

요런 식으로 사용하면 된다.

이하 참고 링크와 샘플 프로젝트를 올리며 마무리하겠다. (ㅃㅇ~)

사실 해당 소스는 유니티에서 친절하게 제공하는 튜토리얼을 보고 따라 만든 것이다.

아래 링크를 보면 자세하게 설명이 나와있다.

https://unity3d.com/kr/learn/tutorials/topics/scripting/localization-manager

 

김치와 불고기의 로컬라이제이션에 대하여 고찰하는 샘플 프로젝트다.

localization sample.zip
0.52MB

반응형
반응형

목차

1. 개요

2. 프로토타입 게임

3. 핵심 소스코드 및 설명

4. 유니티 세팅 방법

 

1. 개요

게임 중에 음성을 인식하여 진행하는 방식의 하이퍼 캐주얼 게임이 있다.

주목할만한 점은 게임성만을 놓고 봤을때 그렇게 중독성이 있거나 하지는 않지만 유튜버들이 이목을 끌기 좋은 콘텐츠라서 유튜버들이 올리면 사람들이 많이 본다.

인플루언서들(influncer) 마케팅을 통하여 해당 게임이 성장하였다고 추측된다.

 

유튜버들이 해당 게임을 플레이하는 영상 시청률이 적지 않다.
음성인식 게임, 1000만 다운로드 이상에 반응도 매우 좋다.

 

이번 글에서는 음성인식 게임의 핵심 부분을 다뤄보고자 한다.

 

2. 프로토타입 게임

 

음성인식 프로토타입 게임

캐릭터는 계속해서 이동하고 소리를 내면(어디 아픈 사람 같지만 아니다.) 내 공의 색이 바뀐다. 공의 색이 바뀐동안 상대방 공을 때린다고 생각하면 된다.

소리를 내서 펀치나 발차기를 하는 게임이라고 상상하자

 

음성인식 프로토타입 게임 2

엄마는 게임기를 숨겼다. 느낌의 게임이라고 생각하면 된다.

소리를 내서 불량배들을 쫓아내는 게임이다.

 

3. 핵심 소스코드 및 설명

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;

public class MicrophoneListener : MonoBehaviour
{
    [SerializeField] private Image _imageSound;
    [SerializeField] private Text TextVol;
    public float sensitivity = 100;
    public float loudness = 0;
    public float pitch = 0;
    AudioSource _audio;

    public float RmsValue;
    public float DbValue;
    public float PitchValue;
 
    private const int QSamples = 1024;
    private const float RefValue = 0.1f;
    private const float Threshold = 0.02f;
 
    float[] _samples;
    private float[] _spectrum;
    private float _fSample;

   //Written in part by Benjamin Outram
 
     //option to toggle the microphone listenter on startup or not
     public bool startMicOnStartup = true;
 
     //allows start and stop of listener at run time within the unity editor
     public bool stopMicrophoneListener = false;
     public bool startMicrophoneListener = false;
 
     private bool microphoneListenerOn = false;
 
     //public to allow temporary listening over the speakers if you want of the mic output
     //but internally it toggles the output sound to the speakers of the audiosource depending
     //on if the microphone listener is on or off
     public bool disableOutputSound = false; 
 
     //an audio source also attached to the same object as this script is
     AudioSource src;
 
     //make an audio mixer from the "create" menu, then drag it into the public field on this script.
     //double click the audio mixer and next to the "groups" section, click the "+" icon to add a 
     //child to the master group, rename it to "microphone".  Then in the audio source, in the "output" option, 
     //select this child of the master you have just created.
     //go back to the audiomixer inspector window, and click the "microphone" you just created, then in the 
     //inspector window, right click "Volume" and select "Expose Volume (of Microphone)" to script,
     //then back in the audiomixer window, in the corner click "Exposed Parameters", click on the "MyExposedParameter"
     //and rename it to "Volume"
     public AudioMixer masterMixer;
 
 
     float timeSinceRestart = 0;
 
     void Start() {
         //start the microphone listener
         if (startMicOnStartup) {
             RestartMicrophoneListener ();
             StartMicrophoneListener ();
             
             _audio = GetComponent<AudioSource> ();
             _audio.clip = Microphone.Start (null, true, 10, 44100);
             _audio.loop = true;
             while (!(Microphone.GetPosition(null) > 0))  {}
             _audio.Play();
             _samples = new float[QSamples];
             _spectrum = new float[QSamples];
             _fSample = AudioSettings.outputSampleRate;
             //유니티 5.x 부터는 audio source에서 mute를 하면 정상적으로 음성이 안나온다.
             //audio mixer에서 master volume의 db를 -80으로 하여 소리 출력만 안되도록 하면 된다.
             //_audio.mute = true;
         }
     }
 
     void Update(){    
         //can use these variables that appear in the inspector, or can call the public functions directly from other scripts
         if (stopMicrophoneListener) {
             StopMicrophoneListener ();
         }
         if (startMicrophoneListener) {
             StartMicrophoneListener ();
         }
         //reset paramters to false because only want to execute once
         stopMicrophoneListener = false;
         startMicrophoneListener = false;
 
         //must run in update otherwise it doesnt seem to work
         MicrophoneIntoAudioSource (microphoneListenerOn);
 
         //can choose to unmute sound from inspector if desired
         DisableSound (!disableOutputSound);
 
         loudness = GetAveragedVolume() * sensitivity;
         GetPitch();
         
         //아래는 커스텀 한 소스
         //소리가 
         if (loudness > 5f)
             _imageSound.fillAmount = 1f;
         else
         {
             _imageSound.fillAmount = 0.65f;             
         }

         FindObjectOfType<GameManager>().currentLoud = loudness;
         //TextVol.text = "vol:" + loudness;
     }
 

    float GetAveragedVolume() {
        float[] data = new float[256];
        float a = 0;
        _audio.GetOutputData (data, 0);
        foreach(float s in data) 
        {
            a+=Mathf.Abs(s);
        }
        return a/256;
    }

    void GetPitch() {
        GetComponent<AudioSource>().GetOutputData(_samples, 0); // fill array with samples
        int i;
        float sum = 0;
        for (i = 0; i < QSamples; i++)
        {
            sum += _samples[i] * _samples[i]; // sum squared samples
        }
        RmsValue = Mathf.Sqrt(sum / QSamples); // rms = square root of average
        DbValue = 20 * Mathf.Log10(RmsValue / RefValue); // calculate dB
        if (DbValue < -160) DbValue = -160; // clamp it to -160dB min
        // get sound spectrum
        GetComponent<AudioSource>().GetSpectrumData(_spectrum, 0, FFTWindow.BlackmanHarris);
        float maxV = 0;
        var maxN = 0;
        for (i = 0; i < QSamples; i++)
        { // find max 
            if (!(_spectrum[i] > maxV) || !(_spectrum[i] > Threshold))
                continue;
            maxV = _spectrum[i];
            maxN = i; // maxN is the index of max
        }
        float freqN = maxN; // pass the index to a float variable
        if (maxN > 0 && maxN < QSamples - 1)
        { // interpolate index using neighbours
            var dL = _spectrum[maxN - 1] / _spectrum[maxN];
            var dR = _spectrum[maxN + 1] / _spectrum[maxN];
            freqN += 0.5f * (dR * dR - dL * dL);
        }
        PitchValue = freqN * (_fSample / 2) / QSamples; // convert index to frequency
    }
 
     //stops everything and returns audioclip to null
     public void StopMicrophoneListener(){
         //stop the microphone listener
         microphoneListenerOn = false;
         //reenable the master sound in mixer
         disableOutputSound = false;
         //remove mic from audiosource clip
         src.Stop ();
         src.clip = null;
 
         Microphone.End (null);
     }
 
 
     public void StartMicrophoneListener(){
         //start the microphone listener
         microphoneListenerOn = true;
         //disable sound output (dont want to hear mic input on the output!)
         disableOutputSound = true;
         //reset the audiosource
         RestartMicrophoneListener ();
     }
     
     
     //controls whether the volume is on or off, use "off" for mic input (dont want to hear your own voice input!) 
     //and "on" for music input
     public void DisableSound(bool SoundOn){
         
         float volume = 0;
         
         if (SoundOn) {
             volume = 0.0f;
         } else {
             volume = -80.0f;
         }
         
         masterMixer.SetFloat ("MasterVolume", volume);
     }
 
 
 
     // restart microphone removes the clip from the audiosource
     public void RestartMicrophoneListener(){
 
         src = GetComponent<AudioSource>();
         
         //remove any soundfile in the audiosource
         src.clip = null;
 
         timeSinceRestart = Time.time;
 
     }
 
     //puts the mic into the audiosource
     void MicrophoneIntoAudioSource (bool MicrophoneListenerOn){
 
         if(MicrophoneListenerOn){
             //pause a little before setting clip to avoid lag and bugginess
             if (Time.time - timeSinceRestart > 0.5f && !Microphone.IsRecording (null)) {
                 src.clip = Microphone.Start (null, true, 10, 44100);
                 
                 //wait until microphone position is found (?)
                 while (!(Microphone.GetPosition (null) > 0)) {
                 }
                 
                 src.Play (); // Play the audio source
             }
         }
     }}

여기 업데이트 부분을 보면 loudness가 소리 크기를 받아오는 부분이고 GetPitch 함수를 통해 PitchValue에 소리의 높낮이 값을 저장한다.

 

4. 유니티 세팅 방법

위의 소스코드만 넣는다고 해서 바로 적용이 되지는 않는다.

인터넷에 돌아다니는 소스 코드를 적용해보았더니 내가 내는 소리가 출력이 되어야지 작동을 했었다.

(audio source에 mute를 넣으면 작동을 안함)

 

프로젝트 우클릭 > Create > Audio Mixer 생성

 

생성된 Audio Mixer 더블클릭 > Groups 새로 생성

 

새로 생성한 그룹의 이름을 microphone이라고 지정

 

새로 생성한 그룹 클릭 > 우측에 Volume이 보이는데 우클릭

 

첫 번째 메뉴 선택

 

우측 상단에 Exposed Parameters 생성됨 > 클릭

 

이미 이름이 지정되어 있는데 Volume으로 이름 변경

 

Master 그룹의 dB를 -80으로 변경

 

유니티 인스펙터에서 MicrophoneListener 컴포넌트에 다음과 같이 등록

반응형
반응형

오류

유니티에서 ios 빌드 xocde 프로젝트를 빌드하면 Mopub.h File Not Found 같은 오류가 나는 경우가 있다.

 

또는 프레임워크의 CocoaPods 설치 오류로 인하여

unity Setting up CocoaPods master repo fatal: not a git repository (or any of the parent directories): .git [!] The `master` repo is not a git repo. 

오류가 발생하는 경우가 있다.

 

원인

근본적인 원인은 유니티 ios 빌드하는 경우 cocoapods에 문제가 발생하여 프레임워크가 xcode 프로젝트에 정상적으로 안들어간 상태에서 빌드하는 경우 오류가 발생하였다.

cocoapods 문제로 mopub 광고 프레임워크가 xcode 프로젝트에 정상적으로 들어가지 않은 것 이다.

 

해결

해결하기 위해서는 cocoapods 재설치가 제일 깔끔한 것 같다.

먼저 문제가 있는지 확인을 하기 위하여 아래 디렉토리로 이동한다.

 

pod 파일이 있는 디렉토리로 이동

cd /Users/계정이름/.gem/ruby/2.3.0/bin/

 

pod setup 명령어를 치면 오류가 발생한다.(유니티에서도 위와 같은 오류가 발생 할 것이다.)

./pod setup 명령어를 쳐보면 문제가 있는지 확인 가능하다.

 

cocoapods 제거

sudo gem uninstall cocoapods 명령어로 모든 버전을 제거한다.

 

cocoapods 재설치

gem install --user-install cocoapods

 

pod setup

./pod setup 명령어로 설치를 진행한다.

 

대략 10분정도 걸렸는데 설치 후 유니티에서 ios 빌드를 하면 정상적으로 빌드가 된다.

반응형