2015년 5월 3일 일요일

[Unity3D] 유니티에서 최적화 단계 밟기


여기서는 다른곳에서 이미 거론된 Unity3D메모리 최적화에 관련된 이야기를 더 하지 않는다.
texture는 npot를 쓰지말라 라는 등의 이미 나돌고 있는 이야기들은 기본적으로 숙지해야할 이야기들이니, 다른곳을 참조하시라. 개인적으로는 Unity3d를 첫 게임 개발 엔진으로 접한 개발자들에게 도움이 되지 않을까 하는 생각을 한다.

1. Garbage 트레이싱

Profiler를 이용, deep profiler를 사용하여 garbage가 발생하는 구간을 특정해 찾아낸다.

주로 봐야할 부분은 대체적으로 주기적으로 garbage가 축적되어 발생하는 부분들
예를들자면 애니메이션 재생을 위한 스크립트 등에서 주기적으로 garbage가 발생하는 구간.

마냥 garbage를 줄이기 위해 보이는대로 줄여나가기보다는 주기적인 garbage발생처를 우선순위로 생각하자.

대체적으로 작은곳에 함정들이 존재하는데, 매우 작은 garbage가 주기적으로 생성되는 부분이나
key-value boxing이 매우 많은 양이 발생해 묵직한 덩어리가 되는때가 있다.
이런 부분에서는 boxing에 주의가 필요하고, 단순 임시 배열들 (mesh에 대한 vertex입력을 위한 일시 배열 등)은 단순하게라도 pooling을 실시하자.

2. Scene Memory 총량 관리

대체적으로 1개 scene단위로 모든 표시될 가능성이 있는 객체들은 하나도 빠짐없이 scene로딩중에 마치는것이 옳다. 그렇지 않아도 도중에 instantiate할 수 있겠으나. instantiate자체에 현 시점에서는 async로 동작하는 내용이 있지 않기에 덩치가 큰 내용물을 생성할경우 필연적으로 렉이 발생한다.

처음에 숨겨둔 객체들을 다 켜서 메모리의 걱정이 될 정도라면 아에 처음부터 그 scene에 들어가지 않게끔 물량 제어를 하라. 어차피 유저가 모두 한번씩 이용을 한다고 생각하면 최종적으로는 모두 사용될 메모리다.
gc이기에, 잠깐 메모리에 적재했다가 안쓴다고 바로 release한다는 플로우는 생각할 수 없다. gc.collect를 강제로 부른다면 렉의 주범이 될것이고. 반복되는 alloc-dealloc은 최적화에서는 모두 제거되어야할 대상이다.

대체적으로 렉의 주범이 되는 것들은 instantiate의 물량 그 자체와
awake에 맞물려있는 초기화 코드들때문.
디자인 하기 나름이긴 하겠으나, 가급적 Start에서 coroutine을 사용하지 말라.
이는 키자마자 끊어버리면 처음 스택 이후에는 실행 안될 가능성이 높아진다.
나중에 사용할 오브젝트들을 disable상태로 어딘가에 pooling하는 것을 전제로 생각하고
awake에서 비 활성화 상태의 초기화 동작을 모두 마친 후, pooling으로 비 활성화중이던 객체를 생성하라.

Scene단위의 메모리 체크는 특정 scene에 진입한 이후 Profiler - memory - (Simple->Detailed)에서 객체별로 확인하자.

Texture의 용량도 그렇지만 SerializedField에 해당하는 Mono의 개체수도 생각하자. 상상 이상으로 커다란 덩치를 자랑한다. ScriptableObject의 Serialization과 동일한 기능을 이용하고 있을것이라 생각되는데, int 1개에 대해 3KB정도의 용량을 소모하고 있었던 것으로 기억한다. (소스를 보진 않은터라 장담할 순 없으나, 만개정도 생성한후, 비어있는 Mono와의 차이를 쟀을때 이정도였던것으로 기억한다)
운용상에 생긴 garbage등도 포함되어 있으리라.

3. pooling

대체적으로 UI를 보면 주요 이슈가 되는건 이른바 무한 스크롤리스트, 말하자면 pooling scroll list라 할 수 있겠다. 리스트의 종류도 여럿 있는 경우가 대부분이니 (친구, 선물, 퀘스트 등등) 리스트의 entity view구현에 pooling을 제어할 수 있는 인터페이스를 포함해 구현하라. 개인적으로는 오브젝트가 늘어난다고 풀의 구현체를 여럿 만들지는 않는다. 하나로 끝내두고 이를 generic화 한다. gameobject라면 관리용 view에 대해 이를 실시한다.

대체적으로 OnActivate, OnDeactivate, OnRefresh의 이벤트 핸들링이 필요하더라.

4. UI의 객체는 View<->Model로 분할된 코드 디자인으로

사실 이 부분은 호 불호가 갈리는 부분이다. 개인적으로 pooling이 가능한 UI용 객체를 디자인할때, MVVM의 View-Model data binding이 가장 스마트한 방법이라 생각한다. 개인적으론 이 부분의 솔루션으로 Reactive Extension을 이용하고 있으며, 유니티에는 UniRx라는 본가 Rx의 handport 라이브러리가 존재한다. 한번은 타 프레임워크에서 접했었고, 이후에 두어번즈음 독자적인 MVVM을 작성해봤는데 UniRx자체는 신뢰해도 될만한 퀄리티의 라이브러리라 생각된다.

View와 Model을 분할함으로서 처음 디자인할때 다소 복잡하게 느껴지지만 앱 디자인 전체로 봤을때는 나중에 어떤 데이터 처리를 해서 UI상에 반영한다는 상황이 왔을때 일 자체가 심플하게 끝나고 코드 디자인에도 큰 영향을 미치지 않는다.

더불어 pooling을 해야하는 상황이 왔을때 더욱 빛을 발휘한다. view에 대해 자신이 표시할 데이터의 참조만 바꿔주면 끝나기 때문. 대체적으로 view에 대해서는 어떤 데이터를 보러가라는 식으로(table, id)디자인을 하고, 이 레퍼런스를 옮겨주기만 하면 view에 대한 반영이 끝난다. 같은 객체를 재사용해야하는 pooling에 있어서는 보다 일을 심플하게 마칠 수 있다.

5. LINQ사용시의 주의점들

LINQ는 어느 사용자들간에 있어서는 기피 대상으로 자리잡고 있다.
사용은 편하지만 느리다는 말들이 많다. 확실히 극단적인 비교의 예로 Dictionary와 비교를 하자면 느릴수 밖에 없다. 모든 검색이 순차검색인 Linear search에 기반하고 있고, 생각해보면 한 항목마다의 조건부 제어이기 때문에 그럴수밖에 없다.

모든 탐색코드를 LINQ로 실시하란 말은 하지 않는다. 오히려 한번 필터링을 거친 데이터는 필요에 따라 ToDictionary()등의 메소드를 이용해 적절한 자료구조에 배치해야한다.
심지어 foreach는 끝단의 필터링을 마친 데이터에 대해서는 한번은 사용하는게 맞다.
기존 기능의 완전 대체가 아닌 적재 적소에 사용해야할 기능이라 생각하자.

LINQ를 기반으로 한 Functional Programming을 하게되면 코드가 짧아지는 경향이 있을 뿐만 아니라, code에 딱히 주석이 없더라도 어떤 일을 하고 있는 코드인지를 한눈에 알 수 있게된다.

Reactive Extension에서도 IObservable에 대해 LINQ를 사용하고 있는데, 기본적으로 IObservable은 데이터 스트림이긴 하지만 한순간에 큰 데이터가 날아오는 경우가 없기때문에 문제가 되지 않는다.

다만, 현 시점에서 Unity3D의 System.Linq;는 사용하지 않는게 좋다. 그게 iOS빌드라면 더더욱 그렇다. AOT build에서 컴파일 되지 않는 코드가 구버전 Mono Implementation에 존재하고 있고, 현재 Unity3D는 이 해결된 코드가 올라와있지 않다.

대안으로는 https://github.com/RyotaMurohoshi/UniLinq 이 git-repos의 AOT safe한 라이브러리를 대안으로 이용하자, 기존 LINQ코드들을 똑같이 이용할 수 있다. Beta표기라 걱정인가? 문제가 생기면 오픈소스이기에 내부를 들여다 볼 수도 있을것이고. 막상 소스를 살펴보면 일일히 소스에서 for로 찾아가던때가 상기될 것이다. 또한 상기 라이브러리는 개인 개발자의 hand-port-ish가 아니다. 위 기능은 문제가 해결된 mono의 코드에서 발췌한 것이라 들었다.

LINQ의 사용 의의는 코드의 퀄리티의 증진이다. 쉽게 말하자면 읽기 쉬운 코드를 쓰자는 말이다.
비즈니스 로직을 구성하면 끝단의 컨텐츠 구성은 하드코딩일 수밖에 없는데,
그 와중에도 읽기 편한 코드를 구성하려면 이런 기능들을 숙지해 나가는 노력이 필요하다 생각한다.

2015년 3월 25일 수요일

[VirtualBox] Host to guest network access

개요
Virtual Box(OSX)에 인스톨된 CentOS에 대해 Network over access하는 방법을 정리

참고자료
http://askubuntu.com/questions/198452/no-host-only-adapter-selected
http://superuser.com/questions/424083/virtualbox-host-ssh-to-guest
https://www.youtube.com/watch?v=Qz_uj_Po2lA

수순

호스트와 게스트를 연결

1. VirtualBox Preference > Network > Host-only Network 에서 호스트 전용 네트워크 어댑터를 1개 추가.
2. VirtualBox VM #n Settings > Network > Adapter 2(or 3 or 4) 에서
Host-only Adapter에서 #1에서 추가된 어댑터를 추가

인터넷 문제를 해결(CentOS7)
* yum이 안되거나 한다면 일단 이를 확인하자

1. 네트워크 어댑터 목록을 확인
...$ nmcli d
DEVICE TYPE STATUS ...
lo loopback unmanaged
enp0s3 ethernet disconnected // <------

2. ipcfg설정
...$ sudo vi /etc/sysconfig/network-scripts/ifcfg-enp0s3
마지막줄의 ONBOOT=yes로 변경설정

3. service network restart
or reboot

4. 설정을 살핌
ip a

5. 인터넷이 되는지 확인
ping 8.8.8.8

호스트에서 게스트로 연결이 되는지 확인

1. ip a
enp0s8정도 되는 기기의 ip를 특정하자. (Host-only adapter로 추정되는 물건)
이번에는 192.168.56.101이라 나왔음

2. 호스트측에서 접근을 확인
ping 192.168.56.101

3. sshd 설정
yum -y install openssh-server openssh-clients
chkconfig sshd on

4. 외부에서 ssh 접근 확인
ssh [username]@192.168.56.101
이후 비번등을 입력해서 접근.

2015년 3월 22일 일요일

[Chef] Solo 입문 메모

개요
Chef Solo입문에 관한 메모

참고자료
http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/cloud/automation

본문

- Chef는 Code를 통한 Infrastructure Provisioning 툴이다

- 코드로 정의된 인프라 구조를 Recipe라 한다

- Chef는 Server, Solo라는 두가지형태가 크게 있는데,
   Server는 복수대의 서버를 유지관리할 수 있는 기능까지 갖추고 있고,
   Solo는 1개 서버의 설정만 해당된다. Solo는 Server의 일부라 할 수 있다.

- Ruby에 기반한다

- 유사 툴로서 Puppet이 대표적이다.

- Facebook의 인프라 유지에 이용되고 있다.

- Idempotent. 멱등성이 보장된 환경. 같은 코드를 몇번 돌리든 코드가 의도하는 바가 유지된다.

Solo 도입순

chef-dk 인스톨

# path는 https://downloads.chef.io/chef-dk/ 에서
wget https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chefdk-0.4.0-1.x86_64.rpm
rpm -ivh chefdk-0.4.0-1.x86_64.rpm 

chef-repo init
# {REPONAME}에 리포지토리의 이름을 지정한다.
chef generate repo {REPONAME}

knife 설정
knife configure

Cookbook 작성
cd {REPONAME}
knife cookbook create {COOKBOOK_NAME} -o cookbooks

recipe json 작성
{PATH-TO-REPO}/localhost.json
{
 "run_list" : [
 "recipe[hello]"
 ]

}

recipe ruby 작성
# solo.rb
file_cache_path "/tmp/chef-solo"

cookbook_path ["/root/{REPONAME}/cookbooks"]

chef 실행
{PATH-TO-REPO}$ sudo chef-solo -c solo.rb -j localhost.json 

Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 1 resources
Recipe: hello::default
  * log[hi there.] action write
  

Running handlers:
Running handlers complete
Chef Client finished, 1/1 resources updated in 2.792128558 seconds

패키지의 설정 사례

default.rb에 대해 다음과 같은 예제를 추가해보도록 하자

log "hi there"

execute 'yum update -y' do
  ignore_failure true
end

package "ImageMagick" do
 action :install
end

yum update실시후, 임의의 패키지 매니저로부터 ImageMagick을 인스톨 하는 설정이다.
패키지의 인스톨 자체에 관해서는 특정 패키지 매니저를 지정하지 않는다.

yum이 아닌 apt-get(ubuntu)이나 homebrew(OSX)와 같은 환경이라면 해당 update는 제각각 추가를 해주고, 다만 실패하는 경우를 대비해서 ignore_failure true옵션을 걸어줌으로서 환경에 독립적인 스크립트를 작성할 수 있다.

chef 실행
{PATH-TO-REPO}$ sudo chef-solo -c solo.rb -j localhost.json

Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 3 resources
Recipe: hello::default
  * log[hi there] action write
  
  * execute[yum update -y] action run
    - execute yum update -y
  * yum_package[ImageMagick] action install
    - install version 6.7.8.9-10.15.amzn1 of package ImageMagick

Running handlers:
Running handlers complete

Chef Client finished, 3/3 resources updated in 14.833352817 seconds

2015년 3월 19일 목요일

[Node.js] Bluebird Promise Coroutine

개요
Bluebird Promise에서의 Coroutine사용방법

참조
https://github.com/petkaantonov/bluebird/blob/master/API.md#generators

필수
Node.js >= 0.11.2(현 시점에서 0.12.0이 stable)

수순

실행 커맨드는 이하와 같다. app을 기동할때 다음과 같은 옵션이 필요하다

node --harmony-generators app.js
혹은
node --harmony app.js

mocha 테스트에 있어서도 --harmony옵션을 붙여서 실행할 필요가 있다.

참조 공식 문서로부터 수정한 coroutine 사용예제 (for sails.js)

 /**  
  * CoroutineController  
  *  
  * @description :: Server-side logic for managing coroutines  
  * @help    :: See http://links.sailsjs.org/docs/controllers  
  */  
 var Promise = require("bluebird");  
 function PingPong() {  
 }  
 PingPong.prototype.ping = Promise.coroutine(function* (val,lim,cb) {  
   console.log("Ping?", val);  
   yield Promise.delay(1); // ms  
   if(val < lim) {  
     this.pong(val+1,lim,cb);  
   }  
   else  
   {  
     cb();  
   }  
 });  
 PingPong.prototype.pong = Promise.coroutine(function* (val,lim,cb) {  
   console.log("Pong!", val);  
   yield Promise.delay(1); // ms  
   if(val < lim) {  
     this.ping(val + 1,lim,cb)  
   }  
   else  
   {  
     cb();  
   }  
 });  
 module.exports = {  
   test:function(req,res,next) {  
     var pingpong = new PingPong();  
     pingpong.ping(0,5000,function() {  
       console.log("done");  
       res.json({"message":"done"});  
     });  
   }  
 };  

결과

Ping? 0
Pong! 1
...
Ping? 4996
Pong! 4997
Ping? 4998
Pong! 4999
Ping? 5000
Pong! 5001
done

2015년 3월 12일 목요일

[uFrame 1.5] MVCVM인 이유

개요
uFrame에서 Controller가 ViewModel과 분리되어 있는 이유

본문

ViewModel은 기본적으로 복수개가 생성되는게 전제이므로, Controller (Business Logics)의 경우 구조적으로 ViewModel에 포함될 경우 ViewModel의 갯수만큼 메소드 역시 복제되는 결과를 낳는다. 결과적으로 메모리상에 같은 로직이 대거 복제되어 버리는 결과로 이어지기 때문에, Controller와 ViewModel은 분리되어 각 모델별로 Controller는 1개씩만 작성된다. 그렇기 때문에 Controller의 각 메소드의 제 1 인수는 ViewModel이다 (1:n의 관계이기에)

2015년 3월 10일 화요일

[NGUI] UISprite에 대한 Custom Shader 적용

개요

NGUI에서 UISprite에 대한 Custom Shader의 적용 방법을 정리하고자 한다.
여기서 쉐이더는 일단 desaturate시키는 쉐이더를 예로 들어보도록 하자.

참조

http://www.tasharen.com/forum/index.php?topic=1256.0

설명
우선, NGUI의 Sprite는 1개의 Atlas단위로 쉐이더가 지정되어있다.
참고 링크에서도 이야기가 되고 있지만. 사실 기본적인 의도대로라면 1번의 drawcall에서 shader의 변경이란게 불가능하기 때문에
(정말로? 음... 사실 Sprite에서 임의의 쉐이더를 정의해놓고 drawcall을 쪼개는 한이 있더라도 선택 운용하게 해주면 좋겟다.)
수순대로라면 drawcall을 분리해서 사용해야하는 수순이 된다. 그래서 NGUI에서는 Atlas단위에 있어서 근본적인 설계딴에서 별도의 쉐이더를 분류하고 있지 않다.

자, 우선 여기서 생각해야할 제약조건은

1. Atlas단위로 shader는 고정되어있다.
2. shader를 분리해서 별개의 atlas를 작성해서 이용한다면 일단 렌더링시에 별도의 drawcall로 운용이 된다. 유색 - 무색 - 유색으로 끼게되면 하나의 텍스쳐라도 3번의 드로우콜이 필요하게 될것이다.

그리고 우리가 여기서 생각해야할 부분은

1. 그렇다면 Atlas는 우선적으로 제각각 작성해야할 것이다.
2. 그런데 완전 같은 이미지 소스에 대한 desaturation임에도 불구하고 별도의 texture를 작성할 필요가 있는가?
 -> 흑백 이미지와 컬러 이미지를 따로 작성한다면 유지보수에 있어서도 귀찮아지는 문제점이 발생할것이고, 행여나 실수를 하게되면 리소스가 결여되는 문제가 생길 수 있다.
 -> 심지어 메모리가 2배로 사용된다. 어찌되었든 바람직하진 않다.

여기서 간단히 생각해볼 수 있는것은. NGUI를 확장한다거나 하는 방법이 아닌
1개의 texture를 각각 참조하는 UIAtlas를 2개 작성하는 방법이 그 해결책의 하나가 될 수 있다고 생각한다.

우선 Desaturate를 실시하기 위한 shader에 관해선
아래 링크에 괜찮은 샘플들이 있으니 이 부분은 가져다 쓰도록 하자.

http://forum.unity3d.com/threads/desaturation-grayscale-shader-for-ios.82105/

그리고 문제의 UIAtlas인데 수순은 다음과 같다.

예를들어 UIItem이라는 atlas가 있다고 하자.

그럼 이 atlas의 구성은 다음과 같을 것이다.

UIItem.prefab
UIItem.mat
UIItem.png

우리는 이중에서 UIITem.prefab, UIItem.mat의 2개 파일을 복사할 필요가 있다.
Unity상에서 이 두개의 파일을 선택하여 Ctrl+D / OSX: Command+D 를 눌러서 복제를 하자

아마 UIItem 1과 같은 느낌으로 파일명이 바뀌었을건데, 여기서 양쪽파일명을 Desaturate 아니면 Gray와 같이 1의 부분을 대치하여 파일명을 정리해주자.

이후에 UIItem.prefab의 UIAtlas Component의 Material을 복제된 mat파일을 보도록 다시 링크를 이어주자 (복제했더니 원본을 보고있더라)
그리고 mat쪽에서 상기 링크에 있는 흑백화 쉐이더를 붙여준다.

이후에 UIItem을 이용하고 있던 Sprite에 대해서 이번에 복제 작성한 UIItem???를 연결시켜주면
에디터에서는 여전히 컬러가 들어간 이미지가 표시되지만 게임화면에서는 흑백으로 표시되는것을 알 수 있다.

UIAtlas의 instance자체에서 텍스쳐에 대한 매핑정보를 보존하고 있기 때문에, 한번 갱신을 했다면 이 복제 atlas도 다시 만들어줄 필요가 있다.
이 방법도 다소 귀찮긴 하지만. 적어도 Sprite의 렌더링에 있어서 가장 큰 크기를 차지하는 텍스처가 중복되지 않아도 된다는 장점이 있다.

NGUI에서 공식적으로 서포트 되어주었으면 하지만, 적어도 그때까지는 이런 방법을 강구할 필요가 있을것 같다.
필요에 따라서는 복제 갱신하는 툴이나 배치를 작성해도 되지 않을까

2015년 2월 21일 토요일

[SocketIO] BestHTTP vs UniWeb

개요
UniWeb, BestHTTP를 SocketIO에 관련해 비교해 보고자 한다.
구입을 고려하는 개발자분들에게 참고가 되었으면 한다
Socket.IO에 관해서는 1.0과 0.9에 있어서 프로트콜상의 차이가 존재한다.
개인적으로 구현자체에 있어서는 BestHTTP쪽에 손을 들어주고 싶다는 생각이 들지만,
만약 서버쪽의 구현이 Socket.IO 0.9버전이며 이를 대체할 수 없다면 UniWeb이나 다른 구현체를 고려해야 하지 않나 하는 생각이 든다.

· BestHTTP UniWeb
가격
Price
55 USD (Pro) / 35 USD (Basic) 45 USD
Socket IO Transport Handshake with Long Polling & Websocket
(서버의 수평확장시 서버에서 Sticky Session이 지원되어야함)
Websocket
SocketIO Version (1.0을 기점으로 앱에 영향이 있다) ^1.0 ~0.9
기본 운용 방식
Basic Operations
From code From Code + Requires Component Attachment
문서화정도
Documentize
Document exists Sample only
Heartbeat Impl O X (+bugs)
Handshake parameters customization ready X (개발자에게 클레임 완료) O (from options)
Async Operation Event-handled Based Coroutine Based
SocketIO Binary Support O X