2024년 12월 19일 목요일

Unity TextMeshPro 유니티 텍스트 메시 프로 한글이 네모로 나올때, 일본어 폰트 추가

 최근 일본어 추가 작업하면서 TMP 사용시 charset range 어떻게 해야할지 정리된것이 없어서 여기저기 찾아보고 정리하였습니다. 그리고 한글에 관해서도 몰랐던 사실도 정리해봅니다.

1. 들어가기에 앞서

FontAsset Creator로 생성할때 Character Set 부분 확실하게 알고 넘어가야 합니다.

중요한 3가지만 알면 됩니다.
아래 3개는 결국 같은 의미이고 표현하는 것에만 차이가 있습니다.
Custom Range : 10진수
Unicode Range(Hex) : 16진수
Custom Charactors : 글자
추가로 입력간에는 공백없이 , 나 -(범위 지정시)를 사용합니다.

예를 들어 인터넷 검색 해서 범위를 이렇게 설정하라고 한다면

32-126,44032-55203,12593-12643,8200-9900

위 내용은 이것은 10진수 이므로 Custom Range로 설정을 하면 됩니다.

만약 0020-007E,AC00-D7A3,3131-3163,2008-26AC 이렇게 주어진다면

이것은 16진수입니다. Unicode Range(Hex) 이것으로 설정하면 됩니다.


2. 한글 영역

검색해보면 32-126,44032-55203,12593-12643,8200-9900 이렇게 알려주는 곳이 있는데 16진수로 해보면 0020-007E,AC00-D7A3,3131-3163,2008-26AC 같은 값입니다.

해당 영역을 세분화 해보면 아래와 같습니다.


영어 범위 : 0020-007E  : 94

한글 범위 : AC00-D7A3  : 11171

한글 자모 : 3131-3163  : 50   ->  ㄱ, ㄴ, ㄷ 이런식입니다. 

특수 문자 : 2008-26AC  : 1700 ->  " 따옴포 같은 특수 문자들 


위 영역은 유니코드 한글 영역의 전체이므로 메모리상 여유가 된다면 추가해도 되긴 하는데 사실 게임에서 많이 사용안하는 코드가 많을 것입니다.

유니코드 많이 사용 하기 전 시절 상용 한글 이라는 것이 있었습니다. 많이 사용하는 한글 2350글자를 모아둔 코드입니다.

그래서 일부 개발자 분들은 상용 한글 2350+영문+특수  이렇게 공유하는 분들도 있습니다. 이 글자만 해도 충분하니까요. 제가 생각해도 충분할 것 같습니다. 문제는 코드는 유니코드에서는 연속 영역이 아닌 부분이 많아서 범위 영역으로 공유를 하지 않고 있습니다. 글자 그대로 복사해서 Custom Charactors로 넣는것을 추천드립니다.

상용한글 2350+영문+특수:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmonpqrstuvwxyz{|}~ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣ가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤샥샨샬샴샵샷샹섀섄섈섐섕서석섞섟선섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄업없엇었엉엊엌엎에엑엔엘엠엡엣엥여역엮연열엶엷염엽엾엿였영옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응읒읓읔읕읖읗의읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝

다음과 같이 넣습니다.



3. 일본어

사실 이글을 쓰게 된 이유 중 하나가 일본어 때문인데요, 일본어는 한자가 있어서 어렵습니다.

제일 아래 표에 따르면 아래와 같이 구성 되어 있습니다. 

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

U+309D, U+309E

가타카나

전자/반자 문자

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

이것을 16진수로 만들어보면, 다음과 같습니다.

3041-3096,309D,309E,30A1-30FA,30FC,FF10-FF19

이것만 해서는 안됩니다. 일본어에 한자가 많이 있어서 일부 네모 상자로 나옵니다.

일반4E00 - 9FEA
확장 AU+3400 - U+4DB5
CJK 호환용 한자F900 - FA6D

결론은 한자 포함해서 아래와 같이 해야 합니다. 

모든 문자를 누락하지 않기 위해서 atlas 크기가 굉장히 커집니다.

3041-3096,309D,309E,30A1-30FA,30FC,FF10-FF19,3400-4DB5,4E00-9FCB,F900-FA6A

일본에서 주로 사용하는 한자를 분리 가능하신 분이 있다면 제보 부탁드립니다.


저의 경우 너무 많은 메모리 사용으로 인한 낭비가 심해서 실제 사용하는 부분만 추출해서 해당 부분만 코드를 넣었습니다.

코드는 아래와 같이 작성하였고 text 부분에 문장을 넣으면 unicode 영역을 리턴합니다.

그중에 영어 범위나 한글 범위를 제외하고 범위를 생성 하였습니다. 매번 문장이 변경되거나 추가될때 일본어 폰트를 다시 작업해 줘야 하는 단점이 있습니다.

https://github.com/donarts/sourcecode/blob/main/python/example/_70_unicode_range/get_unicode_range.py


4. 일본어 폰트

폰트는 라이센스가 자유로운 구글 폰트 사용하였습니다. 아래 링크에서 다운로드 가능합니다.

https://fonts.google.com/noto/specimen/Noto+Sans


5. Fallback Font Assets 설정

설정한 폰트가 없을때 다른 font assets을 찾도록 하는 기능입니다.

폰트를 선택하면 아래쪽에 메뉴가 있습니다.

다국어를 지원 할때 유용한 기능입니다.




6. 부록 전체 코드 범위

링크에서 가져왔습니다.

언어문자범위
아랍어

U+0600 - U+0605, U+0620 - U+0669, U+066E - U+06D3

CJK 통합 한자일반4E00 - 9FEA
확장 AU+3400 - U+4DB5
CJK 호환용 한자F900 - FA6D
Chinese중국 간지CJK 통합 한자 참조
그리스어

모든 알파벳 문자(대문자 및 소문자)가 지원됩니다. 확장 문자의 경우(ά έ ή ί ϊ ΐ ό ύ ϋ ΰ ώ €)

U+0370 - U+0374,

U+0376 - U+0377,

U+037A - U+037D,

U+037F - U+0386,

U+0388 - U+038A,

U+038C, U+038E - U+03A1, U+03A3 - U+03F5,

U+03F7 - U+03FF,

U+1F00 - U+1F15,

U+1F18 - U+1F1D,

U+1F20 - U+1F45,

U+1F48 - U+1F4D,

U+1F50 - U+1F57, U+1F59, U+1F5B, U+1F5D,

U+1F5F - U+1F7D,

U+1F80 - U+1FB4,

U+1FB6 - U+1FBC, U+1FBE,

U+1FC2 - U+1FC4,

U+1FC6 - U+1FCC,

U+1FD0 - U+1FD3,

U+1FD6 - U+1FDB,

U+1FE0 - U+1FEC,

U+1FF2 - U+1FF4,

1FF6 - 1FFC

일본어히라가나U+3041 - U+3096,

U+309D, U+309E

가타카나

전자/반자 문자

U+30A1 - U+30FA, U+30FC
간지CJK 통합 한자 참조
더블바이트 숫자FF10 - FF19
한국어한글 음절AC00 - D7A3
라틴어a-z

A-Z

U+0061 - U+007A

U+0041 - U+005A

라틴어 확장

(체코어, 덴마크어, 네덜란드어, 핀란드어, 프랑스어, 독일어, 헝가리어, 이탈리아어, 노르웨이어, 폴란드어, 포르투갈어, 루마니아어, 스페인어, 스웨덴어)

라틴어-1 보충

U+00C0 - U+00D6,

U+00D8 - U+00F6,

00F8 - 00FF

라틴어 확장-A

U+0100 - U+017F

라틴어 확장-B

U+0180 - U+024F

라틴어 확장-C

2C60 - 2C7F

통화 기호U+20AC
숫자0-9U+0030 - U+0039
러시아어키릴 문자

U+0400 - U+0481,

U+0483 - U+0487,

U+048A - U+04FF

특수 문자하이픈(-) 및 밑줄(_)U+002D, U+005F


참고 링크

https://docs.automationanywhere.com/ko-KR/bundle/enterprise-v2019/page/enterprise-cloud/topics/aae-client/bot-creator/using-variables/unicode-range.html







itch.io 게임 올리기

 최근에 playstore에 등록하고나서 다른곳을 알아보던 중 itch.io에 올리기로 마음을 먹었습니다.

itch.io에는 수익을 올리기 위한 목적은 아니라서 홍보겸 무료 배포 설정하고 작업한 직접 작업한 리소스에 대해서는 무료로 배포하였습니다.

수익을 위해서 미국내 세금 관련 뭔가 복잡한것 같아서 시도해보지는 않았습니다.


https://donarts.itch.io/magebrain

이번에 제작한 게임 홍보입니다.

https://play.google.com/store/apps/details?id=com.donarts.mage_brain


준비

1. 광고

올리기전에 고려해볼것은 광고입니다. 

admob 특성상 (확인해보니 Android or IOS 두가지 밖에 없어서)스토어 기준으로 광고가 배포되기 때문에 광고 처리 부분을 모두 삭제해야만 했습니다. 

코드에서는 #if UNITY_WEBGL사용 하여서 막았습니다.

2. 로그인 부분

GPGMS도 또한 웹에서 지원이 안될것 같아서, GPGMSGooglePlayGames 사용한곳을 막았습니다.

3. 저장 부분

제가 사용한 저장 에셋에 문제가 있어서 WebGL 사용시 저장을 제대로 못하는 부분이 있었습니다.

unity 기본으로 코드로 구현했다면 문제 없겠지만 브라우저 특성상 저장 부분도 한번 테스트해보세요.


등록

1. Dashboard->Create

Dashboard 메뉴가 우측 계정을 누르면 나타납니다.


2. 기본 정보들 입력하기

Kind of porject 에 Unity WEBGL로 빌드했으니 HTML 선택을 해줍니다.


3. 파일 올리기

빌드한 폴더에 가서 전체를 zip으로 압축을 합니다.

최상위에 index.html 파일이 존재하여야 합니다.

파일을 업로드하면 check box가 여러개 있는데 This file will be played in the browser 를 선택해줍니다.

4. 나머지 항목들 입력해주기

나머지는 번역해가면서 넣어주면 됩니다. Embed in page 하고 Auto-Detect size 선택해줍니다.

스크린샷에는 없는데 AI로 생성한것인지 문의하는 부분이 있는데 저의 경우 이미지를 포함해서 일부분 GPT를 이용했기 때문에 Check box에 check하였습니다.


5. visibility & access

만든 페이지의 권한을 주는 내용입니다. 처음에는 Draft로 해서 layout 도 보고 게임도 정상동작하는지 확인해본 후, 최종적으로 Public으로 하면 됩니다.




2024년 12월 15일 일요일

Windows 11 WSL Remove(WSL 삭제)

WSL 기능을 사용하다가 계속 방치하고 있었는데요.

어느 순간 SSD 용량이 부족하다보니 설치한것들 검색해보던중 WSL 관련된 내용이 보여서 삭제를 하게 되었습니다.

제 기준으로는 약 20GB 정도 SSD를 소모하고 있었습니다.

3 가지 정도 하면 됩니다. (Windows 11)


1) 설정 > 앱 > 설치된 앱 ( ubuntu 삭제 )



2) 같은 위치, 설정 > 앱 > 설치된 앱 ( linux update 삭제 )


3) 설정 > 시스템 > 선택적 기능 > 기타 Windows 기능 > Linux용 Windows 하위 시스템(선택해제)

최근 윈도우에서 위치가 바뀌었는지, 다른 분들 블로그와 메뉴 위치 차이가 나서 정리하였습니다.





2024년 12월 13일 금요일

Unity : Graphics device is null. (admob 사용시)

AdMob 보상형 광고 문제 해결: Unity Editor에서 Android 단말기로의 전환


최근 Unity를 사용하여 AdMob의 보상형 광고를 구현하는 과정에서, 에디터에서는 정상적으로 작동하던 코드가 Android 단말기에서 오류를 발생시키는 문제가 있었습니다. 이를 해결하기 위해 ADB(Android Debug Bridge)를 통해 로그를 확인한 결과, 다음과 같은 오류 메시지를 발견했습니다.


 I Unity   : Rewarded ad full screen content opened.

 I Unity   : Rewarded ad recorded an impression.

 I Unity   : Rewarded ad paid 0 USD.

 E Unity   : Graphics device is null.

 E Unity   :  #0 0x75f8780ce4 (libunity.so) ? 0x0

 E Unity   :  #1 0x75f8c292e8 (libunity.so) ? 0x0

 E Unity   :  #2 0x75f8a68e20 (libunity.so) ? 0x0

 E Unity   :  #3 0x75f8caef40 (libunity.so) ? 0x0

 E Unity   :  #4 0x75f8cb1c20 (libunity.so) ? 0x0

 E Unity   :  #5 0x75f8cb1fc4 (libunity.so) ? 0x0

 E Unity   :  #6 0x75f844a8e0 (libunity.so) ? 0x0

 E Unity   :  #7 0x75f52cfd30 (libil2cpp.so) ? 0x0

 E Unity   :  #8 0x75f3adc6b0 (libil2cpp.so) ? 0x0

 E Unity   :  #9 0x75f3adc5fc (libil2cpp.so) ? 0x0

 E Unity   :  #10 0x75f8692c10 (libunity.so) ? 0x0

 E Unity   :  #11 0x75f86a1f18 (libunity.so) ? 0x0

 E Unity   :  #12 0x75f86a48d4 (libunity.so) ? 0x0

 E Unity   :  #13 0x75f86ed6fc (libunity.so) ? 0x0

 E Unity   :  #14 0x75f86b1b10 (libunity.so) ? 0x0

 E Unity   :  #15 0x75f86b1f20 (libunity.so) ? 0x0

 E Unity   :  #16 0x75f86b1a7c (libunity.so) ? 0x0

 E Unity   :  #17 0x75f86ef248 (libunity.so) ? 0x0

 E Unity   :  #18 0x75f86ef124 (libunity.so) ? 0x0

 E Unity   :  #19 0x75f84e348c (libunity.so) ? 0x0

 E Unity   :  #20 0x75f845ca24 (libunity.so) ? 0x0

 E Unity   :  #21 0x75f3bb0880 (libil2cpp.so) ? 0x0

 E Unity   :  #22 0x75f3bb1168 (libil2cpp.so) ? 0x0

 E Unity   :  #23 0x75f49dc658 (libil2cpp.so) ? 0x0

 E Unity   :  #24 0x75f3adc6b0 (libil2cpp.so) ? 0x0

 E Unity   :  #25 0x75f3adc5fc (libil2cpp.so) ? 0x0

 E Unity   :  #26 0x75f3a


이중 "Unity   : Graphics device is null."가 문제였으며 구글링을 해보았습니다.

이 오류는 Unity의 그래픽 장치가 null이라는 것을 의미하며, UI 관련 작업이 메인 스레드에서 제대로 처리되지 않았음을 암시합니다. 여러 자료를 참고한 결과, UI를 메인 스레드에서 처리해야 한다는 공통적인 의견이 있었습니다. 

참고 블로그들...

https://bonnate.tistory.com/552

https://sam0308.tistory.com/84


문제의 원인


문제를 분석해보니, 보상형 광고가 표시된 후 사용자를 보상하는 과정에서 UI 업데이트가 메인 스레드 외부에서 이루어져 오류가 발생한 것으로 보입니다. 이를 해결하기 위해, UI 처리를 메인 스레드로 옮기는 작업이 필요했습니다.


해결 방법


기존의 `GetReward` 메소드는 광고 보상 처리를 즉시 진행하고 있었으나, 이를 메인 스레드에서 처리하도록 수정했습니다. 아래는 수정된 코드입니다.


개선 전 코드


```

public void GetReward(Reward reward)

{

    GameManager.instance.ShowRewardGetNoGen(GetItem.ItemNo.GEM, 5);

}

```


개선 후 코드


변수를 추가하여 플래그를 설정하고, UI 처리를 `Update` 메소드로 옮겼습니다.


```

private bool getReward = false;


public void GetReward(Reward reward)

{

    getReward = true;

}


private void Update()

{

    if (getReward)

    {

        getReward = false;

        GameManager.instance.ShowRewardGetNoGen(GetItem.ItemNo.GEM, 5);

    }

}

```


이렇게 수정한 후, 문제는 해결되었고 광고가 정상적으로 작동했습니다.


검색해보니 아래와 같은 것도 있던데 실제 사용해 보지는 않았습니다. 참고용으로만 적어둡니다.

MobileAds.RaiseAdEventsOnUnityMainThread = true;



결론


AdMob 보상형 광고를 Unity에서 사용할 때, UI 업데이트를 메인 스레드에서 처리하는 것이 필요합니다. 그리고 여기에서는 이러한 방식으로 문제를 해결할 수 있었습니다.


2024년 12월 5일 목요일

Unity GooglePlayGamesPlugin-2.0.0 잘안될때 확인해야 할것

Unity GPGS 때문에 여기까지 검색해봤다면 기본적인 것은 알고 있다고 생각됩니다.

1. 가장 먼저 확인할 부분은 키부분입니다. 해당 부분은 다른 게시글에서도 쉽게 찾을 수 있을 것입니다.

2. 여기에서는 Authenticate 함수 호출시 에러가 나는 부분에 대한 설명입니다.


개발자 문서 : https://developer.android.com/games/pgs/unity/unity-start?hl=ko

github : https://github.com/playgameservices/play-games-plugin-for-unity


v11부터 바뀐것 같기는 하나 v11로 테스트 해보지는 않았지만, v11과 동일하게 2.0.0에서 signin 필요 없습니다.



위 링크를 보면 아래 내용이 나옵니다.

게임이 열리면 로그인 서비스를 사용하여 Play 게임즈 서비스로 연결이 자동으로 시도됩니다. 연결되면 게임에 로그인 메시지가 표시되고 Unity용 Google Play 게임즈 서비스 플러그인을 사용할 준비가 됩니다.

사용자가 기기에서 Google Play 게임즈 서비스를 사용한 적이 없는 경우 일회성 설정 화면으로 자동으로 안내되어 Play 게임즈 계정을 생성합니다.

스크립트의 Start 메서드에서 자동 로그인 시도 결과를 수신 대기하고, 인증 상태를 가져오고, Play 게임즈 서비스 기능을 중지(사용자가 로그인하지 않은 경우)합니다.

Unity 플러그인 버전이 v11 이전인 경우 로그인 기능을 사용할 수 없습니다.

    using GooglePlayGames;

    public void Start() {
      PlayGamesPlatform.Instance.Authenticate(ProcessAuthentication);
    }

    internal void ProcessAuthentication(SignInStatus status) {
      if (status == SignInStatus.Success) {
        // Continue with Play Games Services
      } else {
        // Disable your integration with Play Games Services or show a login button
        // to ask users to sign-in. Clicking it should call
        // PlayGamesPlatform.Instance.ManuallyAuthenticate(ProcessAuthentication).
      }
    }

즉 저 코드를 넣으면 된다고 되어있는데 실제해보면 동작하지 않습니다.

핵심은 주석 처리 되어 있는 아래쪽 코드인데, 해당 주석을 풀면 동작을 하게 됩니다. 자동로그인으로 바뀌었고 최초 한번은 수동 로그인이 필요하기 때문에 ManuallyAuthenticate 호출하도록 하던가 아니면 Authenticate 함수 실패시 ManuallyAuthenticate가 자동으로 호출 하도록 해당 함수를 사용하면 된다는 의미입니다.

단순히 주석을 제거하지 말고 버튼을 만들어서 수동으로 ManuallyAuthenticate 코드가 수행되도록 작업하는 것이 좋습니다. 위와 같은 상태로 구현을 하면 사용자가 로그인을 안한 경우 매번 실행시 구글 로그인을 하라는 팝업이 뜨는 경험을 겪게 됩니다.

막상 주석을 풀어서 사용하기 위해, 주석된 코드를 자세히 보면 ManuallyAuthenticate(ProcessAuthentication) 호출 후 callback으로 실패하게되면 ProcessAuthentication 함수가 무한이 불리게 됩니다.

제가 사용하는 참고 코드입니다.



    void Start()
    {
        PlayGamesPlatform.Activate();
        PlayGamesPlatform.Instance.Authenticate(PorcessAuthenticationStart);
    }

    public void GPGS_Login()
    {
        PlayGamesPlatform.DebugLogEnabled = true;
        PlayGamesPlatform.Instance.ManuallyAuthenticate(PorcessAuthentication);
    }
    internal void PorcessAuthentication(SignInStatus signInStatus)
    {
        if (signInStatus == SignInStatus.Success)
        {
            string displayName = PlayGamesPlatform.Instance.GetUserDisplayName();
            string userID = PlayGamesPlatform.Instance.GetUserId();
            this.displayName = displayName;
            this.userID = userID;
            logText.text = "logined:" + displayName;
        }
        else
        {
            logText.text = "*** Failed to authenticate with " + signInStatus;
        }
    }
    internal void PorcessAuthenticationStart(SignInStatus signInStatus)
    {
        if (signInStatus == SignInStatus.Success)
        {
            string displayName = PlayGamesPlatform.Instance.GetUserDisplayName();
            string userID = PlayGamesPlatform.Instance.GetUserId();
            this.displayName = displayName;
            this.userID = userID;
            Debug.Log("start login Success");
            logText.text = "logined:"+displayName;
        }
        else
        {
            Debug.Log("start login failed");
        }
    }


추가로 여러번 테스트할때 자동 로그인이 되어서 불편합니다. 로그아웃 하는 방법은 Play 게임 앱을 스토어에서 설치를 하면 내데이터 항목내 게임 계정 변경 메뉴가 있습니다. 해당 메뉴의 게임별 계정 변경 메뉴를 선택하면 게임별 로그 아웃 가능합니다.



2024년 11월 21일 목요일

langchain + llama 3.2 + ollama 이용 RAG 구현

Langchain을 이용하여 LLaMA와 Ollama를 사용한 Retrieval-Augmented Generation (RAG) 시스템을 구현하려면, Langchain의 Document Loaders, Vector Stores, 그리고 LLM 연결 기능을 결합해야 합니다. 

LLaMA를 쉽게 사용하기 위해서 Ollama 를 선택했습니다. 여러가지 사유가 있겠지만 GPU설정도 따로 안해줘도 되고 proxy 설정이 다른 것보다 쉬웠는 측면도 있었습니다.

다른 예제들을 보면 OpenAI api를 이용하는 경우가 있었는데 여기에서는 local Embeddings을 사용해보도록 하겠습니다.


1. 일단 Ollama를 설치해줍니다. 

아래 다운로드 주소에서 본인 운영체제에 맞는 적당한 버전을 다운로드 해줍니다. (설치 위치 변경이 안되는 부분이 조금 아쉽습니다.)

https://ollama.com/download


2. python 설치 + venv 사용

이 부분은 인터넷 검색해보시기 바랍니다. windows와 ubuntu venv 명령이 조금 다릅니다.

여기에서는 python은 3.12사용 하였습니다.

 

3. llama 설치

hugging face에서 llama를 직접 다운 받아서 하는 방법도 있겠지만 확장자도 변환해야 하고 복잡하므로 ollama 에서 직접 설치하도록 합니다. 3.2 3b 모델로 설치하겠습니다. 지원 모델은 아래 링크에서 확인이 가능합니다. 대략 2GB 정도 하네요

https://ollama.com/library/llama3.2

ollama run llama3.2

llama가 제대로 실행되었다면 prompt가 나옵니다. 몇가지 질문을 해보면 잘 동작되는 모습을 볼 수 있습니다.


지금 설치된 LLM이 뭐가 있는 확인할때는 ollama ls 명령을 사용합니다.
(langchain) D:\dev\venv\langchain>ollama ls
NAME               ID              SIZE      MODIFIED
llama3.2:latest    a80c4f17acd5    2.0 GB    11 minutes ago

4. python 필요 패키지 설치

pip install langchain faiss-cpu langchain-community langchain_ollama

주피터 노트북 설치 편하게 작업하기 위해서 추가로 설치합니다.

pip install notebook

python -m notebook 으로 주피터노트북 실행합니다.

기타 패키지들이 있는데 에러가 발생하면 추가해서 pip로 설치해 줍니다. 이것 저것 시도하다보니 정확하게 어떤 패키지였는지 기억하지 못했습니다.


5. embedding 모델 설치

local Embeddings 을 하기 위해서 ollama에 기본적으로 nomic-embed-text 모델을 제공해 줍니다. embedding 모델에 대한 정보는 다음 링크 참고하시기 바랍니다.

https://ollama.com/blog/embedding-models

ollama pull nomic-embed-text

막상해보니 한글이라서 그런지 생각보다 잘안됩니다. 그래서 추가로 large 모델을 사용하였습니다.

ollama pull mxbai-embed-large

2024.11.24 추가 수정

확인해보니 LLM으로 사용했던 모델을 그대로 사용해도 embedding 하는데 전혀 문제가 없었습니다. 따라서 아래와 같이 수정하고 나서는 결과가 더 좋게 나왔습니다.

local_embeddings = OllamaEmbeddings(model="llama3.2:latest") 


6. llama3.2 를 Ollama에서 실행

정상적으로 ollama 가 동작하고 모델도 다운로드 되었다면 "부산"이라고 나오게 됩니다.


from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import OllamaLLM

# 기본 예제
llm = OllamaLLM(model="llama3.2:latest")


template = "{country}의 수도는?"

prompt = PromptTemplate.from_template(template=template)
chain = prompt | llm | StrOutputParser()

result = chain.invoke({"country", "한국"})
print(result)

부산

7. 복잡한 동작 텍스트 분할하기

여기에서는 긴텍스트가 있다고 파일로 존재하고 있다고 생각하고 구현하였습니다.

해당 파일에서 파일을 읽어서  chunk size로 나누게 합니다. 여기에서는 좀 작게 여러개로 나누어 봤습니다.


from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document

# 텍스트 파일 경로 설정
file_path = "data/data.txt"

# 텍스트 파일을 읽어서 로딩
try:
    with open(file_path, "r", encoding="utf-8") as file:
        text = file.read()
except FileNotFoundError:
    raise Exception(f"Failed to load text file at path: {file_path}")

# 텍스트를 문서 형태로 변환 (단순히 리스트로 사용 가능)
document = Document(page_content=text)

# 텍스트를 분할하기 위해 텍스트 분할기 사용
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents([document])

# 결과 출력
i = 1
for split in all_splits:
    print(f"Chunk {i}:{split.page_content}\n")
    i += 1

8. Chroma 를 이용해 벡터스토어에 저장을 합니다. 

Chroma는 오픈 소스 벡터 데이터베이스입니다. 파일에 저장도 하는데 여기에서는 따로 저장하지는 않았습니다.

https://python.langchain.com/docs/integrations/vectorstores/chroma/




# https://ollama.com/blog/embedding-models

from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

#local_embeddings = OllamaEmbeddings(model="nomic-embed-text") # 이것 사용했는데 잘안됨

## ollama pull mxbai-embed-large 다른것 설치함
local_embeddings = OllamaEmbeddings(model="mxbai-embed-large") 
vectorstore = Chroma.from_documents(documents=all_splits, embedding=local_embeddings)

embedded_query = local_embeddings.embed_query("LangChain 에 대해서 상세히 알려주세요.")
# 임베딩 차원 출력
len(embedded_query)


9. 유사도 검색

similarity_search 메서드는 Chroma 데이터베이스에서 유사도 검색을 수행합니다. 

실제 해보면 한글이라서 그런지 검색이 제대로 되지는 않았습니다. FAISS, Chroma 모두 테스트 해봤는데 결과는 비슷합니다. Embeddings이 문제인것 같았으나 추가 테스트는 해보지 못했습니다.



question = "대한체육회장 선거 날짜는?"
#question = "부상을 당한 사람은 누구인가요?"
docs = vectorstore.similarity_search(question)
len(docs)

10. 종합


from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)


RAG_TEMPLATE = """
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.


{context}


Answer the following question:

{question}"""

rag_prompt = ChatPromptTemplate.from_template(RAG_TEMPLATE)

chain = (
    RunnablePassthrough.assign(context=lambda input: format_docs(input["context"]))
    | rag_prompt
    | model
    | StrOutputParser()
)

#question = "토트넘은 몇위가 되었나요?"

docs = vectorstore.similarity_search(question)

# Run
chain.invoke({"context": docs, "question": question})


https://github.com/donarts/sourcecode/blob/main/ml/_01_langchain_RAG/langchain_llama%203.2_ollama_RAG.ipynb