Pages

Showing posts with label Unity3d. Show all posts
Showing posts with label Unity3d. Show all posts

October 22, 2013

[Unity] Jenkins로 유니티 자동 빌드 세팅하기 (4) - ios 앱 코드 사이닝


이전 포스트들을 통해 살펴본 Jenkins에서 유니티 프로젝트를 빌드하는 과정을 다시 한번 정리하면 다음과 같다.

  1. Jenkins 설치
  2. Jenkins 실행 계정 변경
  3. Jenkins plugin 설치 및 설정
  4. Git repository 접근 설정
  5. Unity 빌드 스크립트 작성
  6. Jenkins build job 생성
    1. Source code Management 세팅
    2. Unity3d 빌드 세팅
    3. Xcode 빌드 세팅
여기까지 iOS앱을 빌드할 수 있는 과정은 모두 커버하였다. 하지만 빌드만 해서는 앱을 배포할 수는 없다. 이번 포스트에서는 빌드된 앱을 배포할 수 있도록 코드 사이닝하는 과정을 커버하고 다음 포스트에서 Testflight이라는 앱배포 서비스를 사용한는 방법을 알아 보도록 하자.

이전 튜토리얼까지 잘 따라왔다면 Jenkins는 git repository에서 프로젝트 파일을 가져와서 해당 빌드잡을 위한 workspace 디렉토리를 생성하고 그 안에 프로젝트 소스와 빌드 파일들을 보관하고 있을 것이다.



원래 Xcode에서 배포를 위한 프로젝트 빌드를 할 때는 배포 버전의 코드 사이닝을 위해 애플 개발자 센터에서 키페어를 요청하고 app id를 등록하고 Distribution provisioning profile을 만드는 과정을 거쳐야 하는데 이 부분에 관련해서는 다른 블로그의 글을 참고하기를 바란다.

Provisioning profile을 생성했다고 가정하고 다음으로 진행하도록 하자.

Xcode에서 Code Signing Identity 지정하기


Jenkins의 현재 작업중인 빌드 프로젝트 workspace내에 유니티의 iOS 버전 빌드로 생성된 Xcode Project 디렉토리에서 Unity-iPhone.xcodeproj 를 클릭하여 Xcode를 열도록 한다.



Xcode의 Project Navigator에서 최상단 프로젝트를 선택하면 Project Info 화면이 Xcode 가운데에 나타난다. 여기서 새로운 Build configuration을 추가하자.  "+" 버튼을 누르고 "Duplicate "Release" Configuration"을 선택하면 된다. 



 지금 단계에서는 앱스토어 배포가 아닌 Ad hoc 배포를 위한 빌드가 필요하므로 "Ad hoc"이라고 이름을 붙였다. 그리고 상단의 Build Settings 를 클릭하고 아래의 "Ad hoc" configuration의 "Code Signing Identity"를 발급받은 배포용 인증서로 변경한다. 앞서 애플 개발자 센터에서 인증서와 Provisioning Profile을 발급 받고 등록을 했다면 여기서 변경할때 선택할 수 있을 것이다.



이제 상단의 Build&Run버튼 옆의 Unity-iPhone이라고 적힌 부분을 눌러 "Edit Scheme" 다이얼로그를 오픈한다. 그리고 Archive부분을 아래와 같이 설정하도록 하자.




이제 프로젝트를 저장하고 Xcode를 닫는다. 닫기 전에 Xcode 상단 메뉴의 Product -> Archive를 선택하여 Archiving이 제대로 되는지 확인해보는 것도 좋겠다.

Xcode 변경 사항이 유지되도록 Unity 빌드 스크립트 수정


앞 단계에서 Xcode 프로젝트에 새로운 Build Configuration을 추가하였고 이제 남은 것은 Jenkins에서 Xcode 빌드를 수행할때 해당 Build configuration으로 빌드가 되도록 하기만 하면 된다. 

그러나 여기서 생기는 이슈는 현재 Xcode Project는 Unity에서 빌드 할 때 생성되는 것이므로 미리 위와 같은 설정을 넣어서 Source code repository에 저장할 수가 없다는 것과 Jenkins에서 해당 빌드 프로젝트를 빌드할 떄 마다 Xcode Project는 삭제되고 Unity에서 새로이 생성된다는 점이다.

이 문제를 해결해보자. 다시 원래의 Unity 프로젝트 소스코드를 열어 Build 스크립트를 아래와 같이 수정한다. 


using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;


class ProjectBuilder {
    static string[] SCENES = FindEnabledEditorScenes();
    static string TARGET_DIR = "build";

    [MenuItem ("Custom/CI/Build iOS Debug")]
    static void PerformiOSDebugBuild ()
    {
         BuildOptions opt = BuildOptions.SymlinkLibraries |
                            BuildOptions.Development |
                            BuildOptions.ConnectWithProfiler |
                            BuildOptions.AllowDebugging |
                            BuildOptions.Development |
                            BuildOptions.AcceptExternalModificationsToPlayer;         

         PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK; 
         PlayerSettings.iOS.targetOSVersion = iOSTargetOSVersion.iOS_4_3;
         PlayerSettings.statusBarHidden = true;

         char sep = Path.DirectorySeparatorChar;
         string buildDirectory = Path.GetFullPath(".") + sep + TARGET_DIR;

         string BUILD_TARGET_PATH = TARGET_DIR + "/ios";
         Directory.CreateDirectory(BUILD_TARGET_PATH);

         GenericBuild(SCENES, BUILD_TARGET_PATH, BuildTarget.iPhone, opt);
    }

    private static string[] FindEnabledEditorScenes() {
        List<string> EditorScenes = new List<string>();
        foreach(EditorBuildSettingsScene scene in EditorBuildSettings.scenes) {
           if (!scene.enabled) continue;
           EditorScenes.Add(scene.path);
        }

        return EditorScenes.ToArray();
    }

    static void GenericBuild(string[] scenes, string target_path, BuildTarget build_target, BuildOptions build_options)
    {
        EditorUserBuildSettings.SwitchActiveBuildTarget(build_target);
        string res = BuildPipeline.BuildPlayer(scenes, target_path, build_target, build_options);
        if (res.Length > 0) {
            throw new Exception("BuildPlayer failure: " + res);
        }
    }
}



앞에서 보았던 스크립트 코드와 똑같은것 같은데 딱 한가지 바뀐것은 BuildOptions에 BuildOptions.AcceptExternalModificationsToPlayer 옵션을 추가한 것이다. 이 옵션의 의미는 Unity에서 프로젝트를 iOS버전으로 빌드할때 현재 이미 빌드된 프로젝트 파일들이 있을 경우 Replace하지 말고 Merge하라는 의미이다. 따라서 이 옵션을 추가함으로써 윗단계에서 프로젝트 파일을 수정한 내용이 사라지지 않고 남아 있을 수 있다.

그러나 문제는 가장 처음 유니티에서 빌드 할 때 위의 옵션을 추가하고 스크립트를 실행하여 빌드하려고 하면 에러가 발생한다. 이유는 Merge할 대상이 없기 때문이다. 개인적인 의견으로는 유니티에서 좀 더 스마트하게 체크해서 없으면 일반 적인 빌드로 프로젝트 파일들을 만들어 낼 수 있게 되었으면 더 좋았을 텐데 아쉽다.

어쨋든 위와 같이 유니티 빌드 스크립트를 수정하고 git에 commit하고 push하여 중앙 소스 저장소에 수정내용이 반영되도록 한다. 그래야 다음번 Jenkins빌드 Job이 시작될때 수정된 빌드 스크립트가 사용되고 Jenkins workspace에서 변경한 Xcode프로젝트가 초기화되지 않는다.

Jenkins Xcode plugin 설정 수정 및 빌드

자 이제 거의 마지막 단계이다. Jenkins workspace 내의 Xcode 프로젝트를 수정하여 원하는 인증서로 코드 사이닝이 될 수 있도록 Build configuration을 새로 생성하였다. 이제 Jenkins Xcode plugin이 빌드 할때 해당 Build Configuration을 사용하도록만 설정하면 된다.

Jenkins build job의 Configuration을 아래와 같이 수정하도록 하자.



이렇게 설정하고 Jenkins build job을 실행하고 Console log를 확인해보면 설정된 Build configuration으로 빌드가 완료되었음을 확인할 수 있다.



이상으로 iOS 앱이 원하는 인증서로 코드 사이닝되어 빌드 되는 과정을 알아 보았다.

다음에는 Testflight이라는 서비스를 이용해서 팀원혹은 테스터들에게 Ad hoc빌드된 앱을 쉽게 배포하는 방법을 알아 보도록 하겠다.



October 21, 2013

[Unity] Jenkins로 유니티 자동 빌드 세팅하기 (3) - ios 앱 빌드 준비 과정

이번에는 유니티에서 생성된 iOS를 프로젝트를 빌드하고 코드 사이닝을 하는 과정을 Jenkins로 자동화하는 과정을 살펴보자. 몇일간 iOS 프로젝트 빌드를 위해 상당한 시간을 소비했는데 일단 코드 사이닝과정이 안드로이드 버전의 그것에 비해 상당한 iOS앱 배포에 대한 이해를 요구하여서 골치가 좀 아팠지만 어쨌든 자동 빌드 세팅에 결국 성공했다.

차근 차근 과정을 짚어가면서 정리해보도록 하겠다. 그리고 이번 튜토리얼을 쓰면서 유니티에 구현한 빌드 스크립트를 공개하도록 하겠다.

일단 유니티에서 iOS버전으로 빌드를 하면 안드로이드와는 달리 빌드된 바이너리가 생성되지 않고 Xcode 프로젝트와 소스코드가 프로젝트 디렉토리에 생성된다. 이것을 다시 Xcode로 빌드하고 코드 사인하여 최종 앱 바이너리를 만들어 내게 된다.

따라서 Jenkins에서도 iOS 앱 빌드를 위해서는 유니티에서 iOS 타겟으로 빌드하고 다시 생성된 Xcode 프로젝트를 Xcode 커맨드 라인 빌드 툴을 통해 빌드하고 사이닝하는 절차를 진행해주어야 한다.

우선 Xcode 5와 Unity3D 4.2.1은 궁합이 맞질않는다. Xcode 5를 사용해서 빌드를 진행하고자 한다면 Untity3D를 4.2.2로 업데이트를 해야 한다. 

  •  이 튜토리얼은 Xcode 5와 Unity3D 4.2.2버전을 기준으로 진행하도록 한다.
  •  Xcode를 사용하여 iOS앱 빌드를 하는과정은 준비되어 있다고 가정하고 설명을 진행하도록 한다. (Xcode및 관련 Library 설치, 키 파일 생성 및 인증서, Provisioning Profile 등에 대한 과정은 다른 블로그를 참조하도록 하자.)

유니티에서 생성된 Xcode 프로젝트를 Jenkins에서 빌드하도록 하는 방법은 크게 두가지가 있는데 Build step에서  Jenkins xcode plugin을 이용하는 방법과 쉘 스크립트를 작성하여 직접 모든 빌드 명령을 구현하는 방법 두가지가 있다.

내가 사용한 방법은 Jenkins xcode 플러그인  이용하여 빌드에 성공했으므로 이 방법 위주로 설명하고 시간이 나면 쉘스크립트를 통해 빌드하는 방법도 시도해보고 성공하면 결과를 다시 포스팅하도록 하겠다.

Add Unity build script for iOS


이전 글에서는 Android 빌드 과정을 중심으로 설명했는데 이번에는 iOS버전으로 빌드가 되어야 하므로 유니티 빌드 스크립트에 iOS타겟의 빌드 과정을 추가하도록 하자.  이전 글에서 유니티 빌드 스크립트를 유니티 프로젝트 디렉토리의 Assets/Editor/ProjectBuilder.cs라는 파일로 생성했는데 이 스크립트를 iOS 빌드를 지원할 수 있도록 수정하였다.

using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;


class ProjectBuilder {
    static string[] SCENES = FindEnabledEditorScenes();
    static string TARGET_DIR = "build";

    [MenuItem ("Custom/CI/Build iOS Debug")]
    static void PerformiOSDebugBuild ()
    {
         BuildOptions opt = BuildOptions.SymlinkLibraries |
                            BuildOptions.Development |
                            BuildOptions.ConnectWithProfiler |
                            BuildOptions.AllowDebugging |
                            BuildOptions.Development;         

         PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK; 
         PlayerSettings.iOS.targetOSVersion = iOSTargetOSVersion.iOS_4_3;
         PlayerSettings.statusBarHidden = true;

         char sep = Path.DirectorySeparatorChar;
         string buildDirectory = Path.GetFullPath(".") + sep + TARGET_DIR;
         Directory.CreateDirectory(buildDirectory);

         string BUILD_TARGET_PATH = buildDirectory + "/ios";
         Directory.CreateDirectory(BUILD_TARGET_PATH);

         GenericBuild(SCENES, BUILD_TARGET_PATH, BuildTarget.iPhone, opt);
    }

    private static string[] FindEnabledEditorScenes() {
        List<string> EditorScenes = new List<string>();
        foreach(EditorBuildSettingsScene scene in EditorBuildSettings.scenes) {
           if (!scene.enabled) continue;
           EditorScenes.Add(scene.path);
        }

        return EditorScenes.ToArray();
    }

    static void GenericBuild(string[] scenes, string target_path, BuildTarget build_target, BuildOptions build_options)
    {
        EditorUserBuildSettings.SwitchActiveBuildTarget(build_target);
        string res = BuildPipeline.BuildPlayer(scenes, target_path, build_target, build_options);
        if (res.Length > 0) {
            throw new Exception("BuildPlayer failure: " + res);
        }
    }
}




소스 코드를 수정 후 에는 유니티 UI에 생성되는 상단 메뉴(Custom/CI/Build iOS Debug)를 통해 빌드가 문제 없이 일어나는지 확인하도록 하자.




빌드가 성공하였다면 아래 스크린샷과 같이 유니티 프로젝트 폴더에 build/ios 폴더가 생성되고 그 안에 유니티에서 생성한 Xcode 프로젝트 파일들이 있음을 확인할 수 있다.



Install Xcode Plugin 


이제 Jenkins에 Xcode build job을 설정하기 위해  Jenkins에 xcode plugin을 설치하자.

Jenkins 첫 화면 -> Manage Jenkins -> Manage Plugin 페이지로 이동 후 Available 탭에서 Xcode integration 플러그인을 찾아서 인스톨한다.



Xcode plugin을 설치후 Manage Jenkins -> Configure System 페이지에서 Xcode Builder 항목을 찾아 현재 시스템에 설치된 Xcode의 경로가 정확한지 확인한다.  Keychains 부분은 수정할 필요가 없다.



Configure Build Project for xcode plugin

플러그인이 설치되었으면 빌드 프로젝트에 Xcode 빌드 과정을 추가하고 설정하는 과정을 알아보자.

유니티에서 빌드 스크립트를 수정하여 iOS버전 빌드 함수를 추가하였으므로 다음과 같이 Jenkins 빌드 프로젝트의 Unity3d 빌드 스텝의 커맨드 라인 옵션을 수정하여 새로 추가한 iOS 빌드 함수가 수행되도록 한다.



이제 Xcode 빌드 스텝을 추가한다. 아래와 스샷과 같이 Add build step을 클릭하면 Xcode 항목이 보인다.



Xcode 빌드 스텝을 추가하고 아래의 스크린샷과 같이 세부 항목을 설정하도록 한다.
유니티에서 프로젝트가 생성되는 폴더 등에 대한 설정은 앞에서 공개한 유니티 빌드 스크립트  소스코드를 기준으로 작성되었다.



이 설정에서 가장 중요한 것은 Unlock keychain 에 관련한 설정과 Xcode Project Directory/Build Output Directory에 관련한 설정이다.

Xcode 빌드 과정에서 코드 사이닝을 동시에 수행하기 때문에 플러그인이 시스템 키 체인에 반드시 접근할 수 있어야 한다. (코드 사이닝을 위한 키 파일 생성과 인증서 관련한 부분은 이미 처리 되었다고 가정한다.)

Xcode Project Directory/Build Output Directory 관련 부분은 빌드 스크립트에서 통상적으로 유니티가 빌드 프로젝트를 내뱉는 경로와 다른 경로를 사용하도록 했기 때문에 수정이 필요하다.

이렇게 설정을 하였으면 이제 준비는 완료되었다.

Build iOS Project


이제 Jenkins에서 빌드 Job을 실행 해 보자.  위의 과정을 잘 따라왔다면 빌드 Job을 실행 후에는 배포를 위한 ipa파일과 크래쉬 덤프 분석을 위한 dSym 파일이 생성되게 된다.

빌드 프로세스가 실행 중에는 왼쪽의 Build Executor Status에  현재 빌드 진행중 인 Job이 표시되고 이것을 클릭하면 다시 Build History를 볼 수 있고 진행 중인 Build History의 항목을 클릭하면 현재 진행 중인 프로세스의 콘솔 아웃풋을 실시간으로 확인할 수 있다.









콘솔 아웃풋 화면에서 맨 마지막에 "Finished: Success"가 찍히면 문제없이 빌드가 완료된 것이다.





위의 스크린샷은 해당 빌드 Job이 마지막으로 빌드된 결과 파일을 Testflight으로 업로드하게 설정해두어서 업로드가 완료된 후 Finished: Success가 찍힌것을 확인할 수 있다.

빌드 결과 파일들은 콘솔 로그에 보이는 것 처럼 각각


'/Users/내계정/Jenkins/Home/jobs/unity_test_build/workspace/build/ios/build/Unity-iPhone.build/demo-1.0-1.0.ipa' 

'/Users/내계정/Jenkins/Home/jobs/unity_test_build/workspace/build/ios/build/Unity-iPhone.build/demo-1.0-1.0-dSYM.zip'

경로에서 찾을 수 있다.



No end yet for iOS distribution!


이렇게 빌드 작업이 마무리 되었고 내가 원하는 결과파일을 얻었는데 이것으로 자동화 과정이 완료가 된것이라고 볼 수 있을까?

테스터를 위해 내가 만든 앱을 iOS기기에 배포를 하기 위해서는 Ad hoc provisioning profile 혹은 애플 엔터프라이즈 개발자 계정이 있다면 In House provisioning profile을 만들고 앱을 해당 profile에 맞게 code signing 과정을 거쳐야 하는데 지금까지 진행한 과정으로는 내가 지정한 provisioning profile을 지정할 수가 없다. 더욱이 Unity에서도 해당 부분에 대한 설정은 가능하지 않아 보인다. Android 앱의 경우에는 배포를 위해서 필요한  Keypair를 지정할 수 있는 것과는 큰 차이가 있다.

아직도 갈길이 더 남았다..

다음 포스트를 통해 원하는 코드 사이닝 과정을 추가하는 법과 Testflight에 빌드된 앱을 업로드 하는 방법을 살펴보도록 하자.







October 15, 2013

[Unity] Jenkins로 유니티 자동 빌드 세팅하기 (2)

앞글에서 맥OS에 Jenkins를 설치하고 유니티 커맨드라인 빌드를 수행하는 Job을 만들어 실제로 자동 빌드를 수행해보았다. 그렇지만 빌드 콘솔 화면에서 "_RegisterApplication(), FAILED TO establish the default connection to the WindowServer, _CGSDefaultConnection() is NULL." 이러한 에러를 보고 좌절하고 말았는데 이번에는 이 문제를 해결하는 방법을 알아보고자 한다.

이전에 구글링을 통해 밝힌것과 같이 이 문제는 Jenkins가 설치시에 jenkins라는 유저 어카운트를 생성하고 해당 어카운트가 LaunchDaemon의 권한으로 유니티를 실행하려고 해서 발생한 문제다.

이 문제를 해결하는 방법은 두가지가 있는데,


  • 첫번째 Jenkins를 로그인 유저로 실행하기.
  • 두번째 Jenkins Slave Agent 노드를 만들어 해결하기.


두가지 중 첫번째 방법이 좀 더 간단한 방법이지만 Jenkins의 Slave Agent가 어떻게 돌아가는지 궁금하다면 두번째 방법을 시도해보는 것도 좋겠다.


Jenkins를 로그인 유저로 실행하기


Jenkins가 jenkins라는 별개의 유저 어카운트 대신 맥OS에 로그인하는 사용자의 어카운트 권한으로 실행하도록 변경하여 Jenkins가 Unity를 실행하는데 문제가 없도록 만드는 방법이다.

주의 : 이렇게 변경하는 경우 기존의 Jenkins세팅이 모두 초기화 된다.  jenkins계정의 파일들을 로그인 사용자 계정의 Jenkins 홈디렉토리로 옮기면 기존 설정이 옮겨질것 같기도 한데 직접 테스트 해보진 않았다.

터미널을 열어 아래 명령을 입력하여 Jenkins Daemon설정을 수정하자.
sudo vi /Library/LaunchDaemons/org.jenkins-ci.plist



    StandardOutPath
    /var/log/jenkins/jenkins.log
    StandardErrorPath
    /var/log/jenkins/jenkins.log
    EnvironmentVariables
    
        JENKINS_HOME
        /Users/로그인사용자계정/Jenkins/Home
    
    GroupName
    daemon
    KeepAlive
    
    Label
    org.jenkins-ci
    ProgramArguments
    
                /bin/bash
        /Library/Application Support/Jenkins/jenkins-runner.sh
    
    RunAtLoad
    
    UserName
    로그인사용자계정
        SessionCreate
    



위와 같이 JENKINS_HOME과 UserName을 현재 OS에 로그인한 계정으로 변경해준다.

그리고 다음의 명령으로 Jenkins를 다시 시작하도록 한다.

sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist


그리고 잠시 기다리면 젠킨스에 다시 접속할 수 있다. 그러나 이전에 했던 세팅은 모두 날아간 상태. 완전히 초기화된 상태이다. 이전글을 참고하여 처음부터 다시 세팅하도록 하자. 주의할 점은 Jenkins는 이제 로그인계정으로 동작하므로 Key Pair를 만들때 jenkins계정이 아닌 현재 로그인한 계정의 Key Pair를 만들어서 Github혹은 Bitbucket에 다시 등록하여야 한다.


Jenkins Slave Agent 노드를 만들어 해결하기



이 방법은 조금 더 프로페셔널해 보이는 방법인데,  기존의 Jenkins 빌드 Job이 초기화되면 안된다거나,  Jenkins Slave Agent가 어떻게 돌아가는지 관심이 있다면 해볼만한 방법이다. 이 방법을 잘 활용한다면 여러대의 빌드머신으로 각기 다른 프로젝트의 빌드를 여러 다른 빌드머신의 Agent에 할당하여 부하를 분산할 수 도 있다.

Agent노드를 만들어야 하므로 Manage Jenkins -> Manage Nodes 페이지로 간다.


Manage Nodes Page

현재 Jenkins가 돌아가고 있는 master 노드가 보이는데 좌측의 New Node 메뉴를 클릭하여 새로운 노드를 생성하도록 하자.

Creating New Node

노드 네임은 원하는 이름을 입력하도록 하고, # of executers는 동시에 빌드 작업을 이 노드에서 실행할 수 있는 숫자인데 2 정도를 입력해놓고 (Unity는 복수개의 프로세스를 허용하지 않아서 동시에 여러개의 Unity 빌드를 실행할 수 없다.) Usage, Launch method는 아래 스샷과 같이 변경하도록 한다.



그리고 Save 버튼을 누르면 아래 화면이 나타난다. Slave 노드를 실행할 수 있는 방법을 설명한 페이지인데, Launch 버튼을 누르는 순간 Slave Agent가 실행이 되어야 할것 같은데 잘 안된다. (사파리 웹프라우저 사용) 그래서 두번째 커맨드라인 명령을 터미널에서 실행해야 했는데, 자바 런타임을 설치하라고 한다. 그래서 자바 런타임을 설치.



자바 런타임 다운로드

자바 런타임 설치후에 다시 명령을 실행. 아래 내용을 터미널에서 입력한다.

javaws http://localhost:8080/computer/unity_builder/slave-agent.jnlp

다음과 같은 경고창을 볼 수 있는데 체크박스를 체크하고 Run 클릭.




그럼 이렇게 화면상에서 작은 Jenkins Agent창을 볼 수 있고 Connected 메시지를 통해 Jenkins 메인 호스트와 접속이 되었음을 알 수 있다.

이어서 Jenkins 웹대시보드를 확인하면 unity_builder라는 Agent 가 문제 없이 추가되어 있음을 볼 수 있다.



여기서 한가지 귀찮은 점은 Jenkins Agent는 현재 로그인 계정으로 실행되고 있고 Git Repository에 접근할때도 현재 계정의 ssh key pair를 가지고 접근하게 된다. 따라서 현재 로그인 계정이 Git repository에 문제없이 접근할 수 있도록 key pair가 없다면 생성하여 등록하고 이미 key pair가 있다면 가지고 있는 key pair를 등록하도록 하자.

그리고 Job설정에서 Agent가 해당 빌드 작업을 수행하도록 지정을 해주어야 한다. Jenkins Job list page 로 돌아가보면 왼쪽 박스의 Build Executor Status 에 unity_builder agent가 Idle중임을 확인할 수 있다.


이전에 등록했던 job configuration을 수정하도록 하자. 아래 스크린샷과 같이 "Restrict where thie project can be run" 체크 박스를 클릭하고 Slave Agent의 이름을 입력하도록 하자.



그리고 이제 다시 Jenkins Job list page 로 돌아가서 설정된 Job의 빌드를 시작하도록 하면 대시보드 왼쪽 화면에서 unity_builder Slave에서 빌드 작업이 진행됨을 볼 수 있고, 문제 없이 빌드가 완료됨을 확인할 수 있다.

"unity_builder" doing build

Build succeded


이상으로 유니티 빌드가 문제 없이 Jenkins에서 자동화될 수 있도록 하는 방법을 알아 보았고 이 다음은 iOS 프로젝트 빌드와 동시에 디버그를 위한 심볼 파일을 생성하고 생성된 프로젝트 빌드 결과물이 Testflight으로 업로드 되는 작업을 자동화하는 방법을 알아보도록 하자.




October 10, 2013

[Unity] Jenkins로 유니티 자동 빌드 세팅하기 (1)

진행중인 유니티 프로젝트의 CI (Continuous Integration)를 위해 Jenkins를 설치하고 설정하는 방법을 알아보자. 우리 프로젝트는 버전관리툴로 git을 사용중이며 remote repository는 Bitbucket을 사용중이고 젠킨스는 맥OS 버전을 설치를 할 예정이다.

프로젝트의 빌드머신으로는 맥미니를 쓰고 있다. 빌드머신이 맥미니여야 하는 이유는 유니티에서 iOS버전의 앱을 빌드하려면 Xcode가 필요하기 때문이다. (Xcode는 맥OS버전만 존재한다.)

 젠킨스 홈페이지로 가서 패키지를 다운받자. 다행스럽게도 Mac OS X전용 패키지가 존재하여 인스톨은 굉장히 단순해졌다.

젠킨스는 /Applications/Jenkins 에 인스톨된다.

인스톨 후 http://localhost:8080 페이지를 열면 아래와 같이 젠킨스의 첫 화면을 볼 수 있다.

Jenkins의 첫 화면

플러그인 설치


젠킨스는 수많은 서드파티 플러그인을 사용할 수 있어서 더 유용한데 플러그인을 설치하는 방법은 다음 페이지에 잘 설명이 되어 있다. 언제 부터인지 모르겠지만 젠킨스 대쉬보드상에서 플러그인을 설치할 수 있도록 페이지가 제공되어 플러그인 검색및 설치가 엄청 편해졌다.


일단 지금 꼭 설치해야 하는 플러그인은 Git 플러그인, Bitbucket OAuth 플러그인과 유니티 빌드 플러그인이다.

Git 플러그인은 젠킨스가 외부 git 저장소로부터 git을 통해 파일을 받아 오는데 사용되고,
Bitbucket OAuth 플러그인은 Bitbucket 의 계정 로그인 정보를 Jenkins 로그인 시스템과 통합하는데 사용된다. 그리고 유니티 빌드 플러그인은 유니티 프로젝트를 커맨드라인으로 빌드하기 위해 필요하다.

사용가능한 플러그인 페이지(http://localhost:8080/pluginManager/available)에서 Git Plugin, Bitbucket OAuth Plugin과 Unity3dBuilder Plugin을 찾아서 체크하고 설치 후 젠킨스를 리스타트 해준다. (Install하는 페이지에서 Restart Jenkins when installation is complete and no jobs are running 를 체크하면 자동으로 인스톨 완료후 자동 리스타트됨)

플러그인 설치



Unity3dBuilder Plugin 설정


이 플러그인을 설치하지 않아도 커맨드라인에서 유니티프로젝트를 빌드하는 것은 가능하다 그러나 이 플러그인을 사용하여 얻는 가장 큰 이점은 빌드가 진행되는 동안 유니티에서 생성되는 로그를 확인할 수 있다는 점이다. 그 외에도 자잘한 기능들이 더 있다.

젠킨스 대시보드 상에서 Manage Jenkins -> Configure System -> Unity 3d 탭을 확인하여 Unity3d 에 대한 설정을 아래 스크린샷과 같이 추가해준다. 시스템에 여러 버전의 Unity3d가 설치되어 있는 경우 복수의 유니티설정을 추가할 수 있다.


Unity3dBuilder 플러그인 설정


Bitbucket 로그인 연동하기


아래 플러그인 메인 페이지에 Bitbucket 로그인을 연동하는 방법이 있으니 절차대로 따라하면 문제없이 Bitbucket OAuth 인증으로 Jenkins를 이용할 수 있다.

https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+OAuth+Plugin

Git 사용을 위한 준비


일단 Git 원격 저장소에서 파일들을 받아오기 위해서는 Key Pair를 생성할 필요가 있다.
Jenkins를 설치하면서 시스템에 생성된 jenkins 계정을 통해 Git repository에 접근하는 것이기 때문에 해당 계정의 Key Pair를 생성하여 Bitbucket(혹은 GitHub)에 등록해야 한다.
터미널을 열고 다음과 같이 jenkins 계정으로 접근하여 jenkins 계정 홈디렉토리에서 key pair를 생성하자.


/Users/Shared $ sudo su - jenkins
MBP-25:~ jenkins$ pwd
/Users/Shared/Jenkins
MBP-25:~ jenkins$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/Shared/Jenkins/.ssh/id_rsa):
Created directory '/Users/Shared/Jenkins/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/Shared/Jenkins/.ssh/id_rsa.
Your public key has been saved in /Users/Shared/Jenkins/.ssh/id_rsa.pub.
The key fingerprint is:
38:f4:8b:75:b1:1d:94:90:57:20:fb:73:83:1a:d7:49 jenkins@MBP-25.local
The key's randomart image is:
+--[ RSA 2048]----+
|          ooo+.  |
|          .+o    |
|      .   o.. E  |
|     . o   = = . |
|      o S + * =  |
|       + o + o . |
|      . . .      |
|                 |
|                 |
+-----------------+


passphrase는 따로 입력하지 않았다. 이렇게 생성한 public키 정보를 Bitbucket에 등록하도록 하자. 키를 등록하는 과정은 Bitbucket의 경우 내 어카운트 아이콘 클릭 -> ManageAccount -> SSH Keys메뉴를 선택 후 Add Key 버튼을 클릭하여 추가할 수 있다.

Bitbucket에 public key 추가하기


이후 아까 열었던 터미널 화면에서 등록한 public key를 통해 테스트 삼아 git repository를 clone해보자. (튜토리얼을 위해 테스트 저장소를 하나 만들고 유니티 프로젝트 파일들을 commit 해두었다.)


MBP-25:~ jenkins$ mkdir temp
MBP-25:~ jenkins$ cd temp
MBP-25:temp jenkins$ git clone git@bitbucket.org:anonymous/unity_ci_test.git
Cloning into 'unity_ci_test'...
The authenticity of host 'bitbucket.org (131.103.20.168)' can't be established.
RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'bitbucket.org,131.103.20.168' (RSA) to the list of known hosts.
remote: Counting objects: 67, done.
remote: Compressing objects: 100% (67/67), done.
Receiving objects:  52% (35/67), 3.99 MiB | 89.00 KiB/s



파일들을 잘 받아 오는 것을 확인할 수 있다. 이렇게 테스트함으로써 'Are you sure...' 메세지로 자동화 과정이 멈추는 것을 방지할 수 있기도 하다.

그리고  다음과 같이 jenkins유저인 상태에서 git 사용자의 identity도 설정해야 프로젝트 파일을 체크할 때 발생하는 에러를 방지할 수 있다.
 git config --global user.email "you@example.com"
 git config --global user.name "Your Name"

Unity3dBuilder 플러그인을 사용하기 위한 코드 추가


Unity3dBuilder 플러그인은 유니티 프로젝트내에 특별히 프로젝트 빌드를 위한 Editor Class가 추가되어 있음을 가정하고 구현이 되어 있다.

아래 C# 코드를 현재 빌드하려는 프로젝트내의 Asset 폴더아래 Editor폴더를 만들고 적당한 이름을 붙여 생성하자.


using UnityEngine;
using UnityEditor;
using System;
using System.Collections;
using System.Collections.Generic;


class ProjectBuilder {
    static string[] SCENES = FindEnabledEditorScenes();
    static string APP_NAME = "YourProject";

    [MenuItem ("Custom/CI/Build Android")]
    static void PerformAndroidBuild ()
    {
         string target_filename = APP_NAME + ".apk";
         GenericBuild(SCENES, target_filename, BuildTarget.Android ,BuildOptions.None);
    }

 private static string[] FindEnabledEditorScenes() {
  List<string> EditorScenes = new List<string>();
  foreach(EditorBuildSettingsScene scene in EditorBuildSettings.scenes) {
   if (!scene.enabled) continue;
   EditorScenes.Add(scene.path);
  }
  return EditorScenes.ToArray();
 }

    static void GenericBuild(string[] scenes, string target_filename, BuildTarget build_target, BuildOptions build_options)
    {
        EditorUserBuildSettings.SwitchActiveBuildTarget(build_target);
        string res = BuildPipeline.BuildPlayer(scenes, target_filename, build_target, build_options);
        if (res.Length > 0) {
            throw new Exception("BuildPlayer failure: " + res);
        }
    }
}


해당 빌드 코드는 커맨드라인에서 Jenkins가 직접 불러 빌드 과정을 수행하게 된다. 따라서 이 스크립트내에 여러가지 빌드 과정에서 필요한 절차를 추가하여 진행중인 프로젝트에 맞도록 Customize하도록 한다.


자동화 빌드를 위한 Job 생성하기

Job 생성 화면
이제 자동화를 위한 Jenkins Job을 생성해보자. 먼저 위의 스샷과 같이 New Job을 클릭후 나오는 페이지에서  unity_test_build라는 Job을 생성하고 아래와 같이 Source Code Management 에 Git을 선택후 저장소 URL을 입력하였다. 위의 단계 중 잘못 된 부분이 있으면 페이지에 빨간 글씨로 경고가 뜰 것이다. (소스 코드 저장소에 접근을 할 수 없다는 둥...)


Git Repository 설정
그 다음에 Branches to build 란에는 빌드를 할 branch를 쓰면 되는데 기본은 모든 브랜치를 빌드하는 옵션이다. "origin/master" 라고 쓰면 master branch만 빌드하게 된다.

그리고 이제 Unity3dBuilder 플러그인으로 유니티 프로젝트 빌드 설정을 추가한다.
Unity3dBuilder 플러그인 설정때 추가했던 유니티설정을 선택하고 유니티 커맨드라인으로 넘길 파라메터를 입력한다.

-quit -batchMode -executeMethod ProjectBuilder.PerformAndroidBuild


유니티 커맨드 라인 빌드 설정


자 이제 단순하게나마 Job 설정을 마쳤다. 이제 젠킨스 대시보드로 돌아가 빌드를 수행할 시간.

Job등록 상태


참고로 Job 설정에 관한 자세한 내용은 Jenkins 위키 페이지도 참고하도록 하자.


무한 빌드?!!



대시보드에서 해당 Job을 선택하고 왼쪽의 Build Now 메뉴를 클릭. 간단한 테스트 프로젝트이고 그래픽 리소스도 없기 때문에 순식간에 빌드가 끝나야 정상이다. 그러나..

빌드 진행중


세월아~네월아~ 빌드가 끝나질 않는다. 무슨일이 일어난걸까..



진행되고 있는 빌드 히스토리의 현재 진행되고 있는 빌드를 눌러 Console Output을 확인해보자.

빌드 상태 확인


빌드 콘솔 로그 확인


콘솔에 찍힌 로그를 통해서 보건데 아마도 "_RegisterApplication(), FAILED TO establish the default connection to the WindowServer, _CGSDefaultConnection() is NULL." 이 부분이 문제인것 같다.

구글을 통해서 알아본 결과.. MacOS 버전의 유니티는 커맨드라인 빌드를 실행할때 유니티가 시스템 윈도우 서비스에 접근을 해야 하는데 Jenkins는 LaunchDaemon으로 실행되었기 때문에 이 Jenkins가 유니티를 실행하면서 WindowServer 시스템에 접근할 수 있는 권한을 가지고 있지 않기 때문에 발생한 문제이다.. 골치가 아프기 시작한다.

다음 포스팅에서 이어서 문제를 해결해보도록 해야겠다.




October 2, 2013

[unity] NGUI에서 UICamera의 모든 입력 이벤트를 처리하는 GameObject 설정하기

이것도 간단한 팁이지만 메모로 남긴다.

NGUI의 UICamera 소스를 보면 genericEventHandler 라는 스태틱 변수가 있다.


static public GameObject genericEventHandler;

이렇게 선언되어 있고 이 변수에 모든 이벤트를 받고자 하는 GameObject를 할당하면 한 GameObject로 모든 화면 입력 이벤트를 받아 처리할 수 있다.

UICamera밑에 수많은 GameObject 들이 있는 경우 코드를 깔끔하게 만들 수 있지 않을까?

[unity] NGUI에서 UIButton이 아닌 GameObject가 클릭이벤트를 받는 방법

간단하지만 기록을 위해 포스팅.

어떤 GameObject 가 NGUI UICamera의 child이고 그 오브젝트가 Collider를 가지고 있는 경우 UICamera 오브젝트에서 해당 GameObject의 OnPress(bool isPressed) 함수를 호출하게 된다.


  • isPressed == true : 오브젝트가 눌러진 상태에 발생하는 이벤트
  • isPressed == false : 오브젝트가 눌려졌다가 놓아진 상태에 발생하는 이벤트
이 함수를 이용해서 버튼이 아니더라도 클릭혹은 터치 이벤트를 받아 이용할 수 있다.


September 25, 2013

[Unity] 유니티의 메모리 관리

유니티 문서를 보면 언급되는 Managed Memory란 단서에서 흔히 추측하기로 유니티가 알아서 모든 메모리를 잘 관리하고 있을 것 처럼 생각되지만 사실은 그렇지 않다는 것이 함정.

사실은 유니티 시스템은 메모리를 어떻게 처리할 것인가에 대한 단서를 당신이 만드는 코드내에서 제공해주기를 원한다. 따라서 잘못된 코드는 줄줄 새는 메모리로 당신의 앱을 디바이스에서 뻗어 버리게 만들것이다. 요즘은 모바일 디바이스에서 조차 64bit 시스템(iPhone 5s)가 올라가고 기본 장착 메모리가 2Gb 이상이 되는 등 모바일 앱으로서는 무한한 자원이 있는것 같지만 사실상 아직도 지구상의 대부분의 유저는 허접한 디바이스로 연명하고 있다는 것을 기억해야 한다.

유니티 어플리케이션이 사용하는 메모리 종류


유니티에서 사용하는 메모리는 3가지 종류의 영역이 있다.

첫번째는 코드 영역으로 유니티 엔진과 라이브러리 그리고 당신이 만든 게임코드가 컴파일 되어서 디바이스의 메모리 코드 영역에 로딩되게 된다. 사실상 이 부분을 최적화 할 필요성은 크지 않다. 실제로 유니티 프로젝트에서 코드 텍스트 파일들이 차지하는 용량이 얼마나 되는지만 계산 해 봐도 알 수 있다. 유니티 코드가 얼마나 클지는 모르지만 예상컨데 큰 사이즈의 그림 파일 몇장에도 못 미치는 용량일것이다.

다음은 Managed Heap영역인데, 이 부분은 Mono가 관리하고 있는 영역이다. 아시다시피 Mono란 .Net Framework의 오픈 소스 버전이다. 이 Managed Heap영역에 클래스가 instantiated object, variable 들이 거주하게 되는 영역이다. "Managed"인 이유는 Mono Framework이 이 영역의 메모리를 할당하거나 해제하면서 "관리"를 하고 있기 때문이다. 실제로 어플리케이션이 실행중에 이따금씩 Mono가 garbage collection 작업을 통해 할당 되었으나 더이상 참조가 존재하지 않는 메모리 영역을 해제한다. 당신의 코드에서 더이상 사용되지 않는 변수나 클래스 오브젝트를 실수든 부주의에 의해서든 참조를 유지하고 있다면 해당 메모리 영역은 절대 해제되지 않고 메모리를 차지하고 있게 된다.

마지막으로 Native Heap 영역인데 이 부분은 Unity 엔진이 OS에서 메모리를 할당 받아 texture, sound effect, level data 등을 저장하고 있는 영역이다. 유니티 엔진이 이 영역을 담당하여 Mono가 메모리를 관리하는 것 처럼 현재 scene에서 필요 없게 된 리소스가 차지하는 메모리 영역을 해제하는 작업을 수행하기도 하나 manual하게 할당된 리소스에 대해서는 유니티가 관리를 하지 못하므로 부주의 하다면 심각한 메모리 부족 사태에 이를 수 있다.

어플리케이션 코드 영역 메모리 최적화


어플리케이션 코드 영역을 최적화하는 작업은 비교적 쉽게 프로젝트 세팅을 바꾸어 주는 것 만으로도 해결할 수 있다. 그러나 이 최적화 옵션을 바꿈으로 인해 사용가능하지 않을 feature를 당신의 어플리케이션이 사용하지 않을 것 인지 확인할 필요가 있다.

사실 Mono Framework는 수많은 .Net 라이브러리를 포함하고 있는데 이 모든 라이브러리를 로딩하는데 꽤 많은 메모리 리소스를 소모하게 된다. 유니티에서는 이 overhead를 줄일 수 있는 옵션을 제공하는데 유니티 메뉴중 Edit > Project Settings > Player 메뉴를 선택하여 보자.


위와 같은 옵션을 볼 수 있을 것인데 중요한것은 아래쪽 Optimization 부분의 옵션들이다. "Api Compatibility Level"옵션을 ".NET 2.0 Subset"으로 바꾸어 유니티에게 더 적은 수의 .Net 라이브러리를 사용하도록 알릴 수 있다. 그리고 그 아래 "Stripping Level"의 옵션을 바꾸어 더 적은 용량의 빌드된 코드를 사용하도록 할 수 있다.  (Stripping Level 옵션의 자세한 내용에 대해서는 링크를 참조하도록 하자.) 해당 옵션의 수정을 통해 빌드 사이즈를 줄일 수 있으며 줄어든 빌드 사이즈는 곧 더 적은 메모리 사용을 의미한다. Stripping Level옵션은 Unity Pro에서만 제공되는 기능이다.

그럼 그냥 가장 minimal한 것으로 설정을 하면 되겠지 싶은데 세상일이 그렇게 간단하지 않다. 그 전에 당신의 어플리케이션이 옵션 조정으로 인해 빠지게 되는 라이브러리를 사용하지 않는지 확인할 필요가 있다. 가장 많이 쓰지만 이외로 minimize되서 빠지게 되는 라이브러리 중에는 System.Xml이 있다. 이 경우는 3rd party minimal 라이브러리를 사용하면 된다.

옵션에 따라 지원되는 라이브러리를 확인할 수 있는 페이지는 이곳을 참조하도록 하자. 실제로 빠지게 되는 많은 라이브러리들이 게임을 개발할 때는 잘 사용되지 않는 것들이 많다. 따라서 한번 시도해 볼만한 가치가 충분히 있다고 본다.

Managed Heap 영역의 메모리 최적화


유니티에서 최근에 Managed Heap 영역의 메모리를 관리하는 법에 대해 훌륭한 메뉴얼을 추가하였다. Managed Heap영역은 당신이 만든 코드에서 new 키워드나 Instantiate() 함수를 통해 할당한 메모리가 Mono Framework에 의해 관리되는 영역이다. 만약 메모리가 필요해 할당을 하는데 Manges Heap영역에 메모리가 모자랄 경우 heap 사이즈를 키우게 된다. 이 경우가 실제로 디바이스 상에서 메모리가 부족해지는 경우이다.

일반적으로는 어떤 인스턴스가 더이상 필요해지지 않을때 해당 인스턴스를 참조하는 곳이 없어지게 된다. 주기적으로 Mono의 Garbage Collector가 이러한 참조가 없어진 인스턴스 등을 찾아 Manged Heap상에서 해제하게 된다. 그러나 Garbage Collector가 메모리를 수거하는 작업을 하는 동안에는 framerate가 떨어지거나 어플리케이션의 반응이 늦어질 수가 있기 때문에 일반적으로 어플리케이션이 활발히 동작하고 있는 동안에 Garbage Collector의 동작을 원치는 않을 것이다. 따라서 메모리가 새는 것을 방지하기 위해 할당한 변수나 오브젝트가 필요가 없어지게 되면 null을 할당하거나 Destory로 명시적으로 메모리상에서 제거되도록 표시를 해두어야 하지만 동시에 수많은 메모리 해제로 과도한 Garbage Collector의 동작을 바라지도 않을 것이다.

특히 게임의 경우 Destory를 호출해야만 하는 수많은 object가 게임 플레이 도중에 생성될 수 있다. 예를 들면 대포가 수많은 대포알을 발사하는 경우가 그렇다. 각 대포알을 발포 되기전에 메모리를 할당 받고 대포알이 사라지는 순간 해제해야 하는데 이러한 작업이 각 대포알에게 일어나야 하는 것은 너무나도 낭비적인 일이 아닐 수 없다. 이 문제를 해결하는 일반적인 방법은 해당 오브젝트를 생성후 필요 없게 되는 때에 Destory하지 않고 일단 숨긴 후 필요할 때 다시 Activate시켜서 사용하는 이른바 Object Recycling 기법이 있다. 항상 이 방법이 유용한것은 아니지만 과도한 Garbage Collection을 피하는 훌륭한 방법이다. 

Automatic Memory Management와 Object Pool에 관련해서는 이곳의 유니티 문서를 통해 자세한 사항을 확인하도록 하자.

 Garbage Collection이 일어나는 것을 막거나 컨트롤 할 수 있는 방법은 딱히 없지만 시스템에 적당한 시점에 지금이 바로 Garbage Collection을 하기에 좋은 타이밍이라는 것을 힌트를 줄 수는 있다. System.GC.Collect()를 호출하는 방법이 그것인데 이 함수를 호출하는 것이 바로 Garbage Collection을 하도록 하지는 않고 정말 단지 지금이 좋은 타이밍이라는 것을 알려줄 수 있을 뿐이지만 적당한 시점에 예를 들어 화면이 전환되는 시점이나 리소스 로딩이 일어나는 시점에 호출해준다면 큰 효과를 볼 수 있다. 

Native Heap 영역의 메모리 최적화


Unity에서 Scene을 로드하면 그 Scene 에 포함된 모든 Asset들을 같이 메모리 상에 로드되고 이어서 해당 Scene이 유저에게 보여진다. 그리고 해당 Scene이 끝나서 다음 Scene으로 넘어갈 경우 이전 Scene의 리소스중 다음 Scene에서도 사용되지 않는 다면 Asset은 메모리상에서 자동으로 해제된다.

그러나 어떤 두가지의 경우에 Asset이 자동으로 해제되지 않는 경우가 있는데, 첫번째로는 DontDestroyOnLoad(Object target) 함수를 통해 개발자에 의해 다른 Scene이 Loading 되더라도 Destroy되지 않도록 지정해둔 경우로 이 경우 해당 GameObject와 GameObject의 Child GameObject, 연계된 Asset 모두가 Scene 전환에 따라 자동 해제되지 않는다. 실제로 이 함수는 모든 Scene간에 보존되어야 하는 데이터를 전달하는 방법으로 사용되고 있는데 이 경우에 보존되는 Object가 무거운 Asset을 참조하거나 그 자체가 아니도록 주의하여야 한다.

또 다른 경우는 Script내에서 Scene Object를 참조하고 있는 경우이다. 대부분 Script의 참조는 Scene이 전환됨에 따라 같이 파괴되게 되지만 Scene이 전환되더라도 남아 있는 GameObject가 있고 해당 GameObject 가 Script를 가지고 있는데 이 Script내에서 다른 Asset 예를 들어 사운드 이펙트나 텍스쳐에 대한 참조를 유지하고 있는 경우 무심코 필요하지 않은 리소스가 메모리상에 남아서 메모리 자원부족에 이르게 할 위험이 있다. 또 Static 변수 혹은 Singleton인스턴스가 자원에 대한 참조를 가지고 있는 경우도 마찬가지 문제가 발생할 소지가 있다.

따라서 스크립트내에서 이러한 참조를 유지하고 있다면 필요없게 되는 시점에 잊지않고 Destroy함수를 호출하거나 null을 할당하여 참조를 해제할 필요가 있다. 

유니티는 Scene이 Load되는 시점에 해당 Scene의 모든 Asset을 자동으로 같이 Load하지만 이러한 Asset을 해제할 유일한 방법은 Scene을 다시 로드하거나 다른 Scene으로 전환하는 방법밖에 없다는 것에 주의할 필요가 있다.

리소스 로딩을 수동으로 관리하기


약간의 노동이 필요하지만 수동으로 Asset을 현재 Scene에 Load할 방법도 당연히 존재한다. Resources 라는 폴더를 만들어서 Asset들을 그 안에 위치시키면 해당 Asset들은 Resources.Load(resourcePath)함수를 통해 수동으로 Load할 수 있게 된다.  이 Asset들은 텍스쳐, 오디오파일, 프리팹, 매터리얼등이다. 

해당 자원에 대한 이용이 끝났다면 그 자원에 할당된 메모리를 Resources.UnloadUnusedAssets() 함수를 호출하여 강제로 해제할 수 있는데, 유니티가 해당 자원을 해제하기 위해서는 스크립트에서 해당 자원에 대한 참조가 모두 없어져야 한다. (Destroy혹은 null을 할당해서) 그리고 이 함수는 Resources.Load() 함수를 통해서 할당된 자원만 해제를 하고 유니티가 Scene을 Load하면서 자동으로 같이 Load한 자원은 해당되지 않는다는 점에 주의할 필요가 있다.


이 포스트는 이 링크의 내용을 의역한 내용이란 것을 밝힙니다.



September 13, 2013

[Unity] 유니티3D 코루틴의 매커니즘

유니티는 기본적으로 싱글 스레드로 동작한다. 따라서 긴시간이 걸리는 작업을 (Especially I/O Operation) 로직내에서 수행할 경우 프레임이 떨어지는 문제를 겪게 된다.

만약 긴 시간이 걸리는 작업을 멀티스레드를 사용해서 골치아프게 동기화등을 고려하지 않고, 여러 프레임이 지나가는 동안 나누어서 처리할 수 있다면 깔끔하게 문제를 해결할 수 있지 않을까?

이 상황을 해결하는 솔루션으로 유니티는 코루틴(coroutine)이라는 것을 제공한다.

사실 유니티 코루틴은 긴 시간이 걸리는 작업을 분할하여 처리하는데에도 쓰이지만 잠깐 로직이 일정 시간 멈출 필요가 있을때에도 사용된다.

아래는 유니티 C# 스크립트로 만든 코루틴의 예제이다.
IEnumerator HeavyLogic()
{
    while(someCondition)
    {
        /* 일정 부분의 작업 수행 */
 
        // 이 로직은 여기서 멈추고 다음 프레임을 진행한다.
        yield return null;

        /* 다음 프레임 이후에 처리될 로직 */
    }
}

그렇다면 이 코루틴이라는 것은 어떻게 돌아가는 것인가? 사실 내가 글을 쓰는 목적인 코루틴의 활용 보다는 그것이 어떻게 유니티내에서 돌아가는 것인가 하는 매커니즘 파악에 촛점이 맞추어져 있다.

이것을 이해하는 키는 해당 코루틴 로직이 IEnumerator형을 리턴하는 함수라는 것과 yield라는 키워드를 사용하는 점에서 유추해볼 수 있다.

IEnumerator 타입은 일종의 시퀀스에 대한 커서처럼 작용한다. IEnumberator 는 Current라는 멤버변수와 MoveNext()라는 멤버함수를 가진다.

Current는 현재 시퀀스의 커서에 대한 요소를 가진 속성이고, MoveNext() 는 현재 시퀀스에서 다음 시퀀스로 진행하도록 하는 함수이다.

IEnumerator는 단지 Interface이므로 이런 멤버들이 어떻게 Implementation되었는지에 대해서 정의하지는 않는다.

MoveNext() 함수의 구현은 Current에 1을 더하는 로직이 될 수도 있고 인터넷 상에서 이미지를 다운 받아 그것의 해쉬값을 Current 변수에 저장하는 로직이 될 수도 있는 것이다. 또한 이 함수는 처음에는 로직 시퀀스상의 어떤 연산을 하다가 그 다음 호출시에는 처음과는 완전히 다른 로직을 수행할 수 도 있다.

MoveNext()가 호출이 되면 해당하는 로직을 수행후 결과는 Current멤버 변수에 저장이 된다. 그리고 더이상 진행할 것이 없다면 false를 리턴한다.

자 따라서 원래대로라면 IEnumerator를 상속받은 Class를 구현을 해야 한다. 골치가 살살 아파 올것이다 그러나 상심은 아직 금물 C#에서는 몇가지 Rule만 따른다면 컴파일러가 자동으로 IEnumerator 구현체를 컴파일 타임에 자동으로 생성하여 준다.

이렇게 Rule을 따라 정의한 함수를 C#에서는 Iterator block이라고 부르는 것 같다.

그렇다면 Iterator block이란 무엇인가?

Iterator block이란 일반 함수와 다르지 않지만 a) IEnumuerator를 리턴할 것, b) yield 키워드를 사용할 것 이 두가지를 갖추면 Iterator block이 된다. (위의 예제 코드 참고)

그렇다면 yield 키워드는 과연 무엇을 하는 것인가.

yield 키워드는 로직 시퀀스에서 다음에 나올 값이 무엇인지 혹은 값이 더이상 없는지 를 가르쳐주는 키워드이다. 따라서 코드 진행중 yield return X 나 yield break를 마주친 지점이 IEnumerator.MoveNext() 함수가 중단되는 지점이다.

yield return X 는 MoveNext()는 true를 리턴하고 Current 는 X로 할당되도록 한다. 반면에 yield break는 MoveNext()가 false를 반환하도록 한다.

자 여기서 발생할 수 잇는 트릭은 시퀀스가 어떤 값을 실제로 반환하는지는 상관이 없다는 것이다. MoveNext()를 반복해서 호출하고 Current값을 무시할 수 있다. 그러면 계산은 계속해서 진행될 것이며 매번 MoveNext()가 호출 될 때 마다 iterator block은 실제로 어떤 식을 yield 하느냐에 상관 없이 다음 yield 키워드까지 진행되게 된다.

그래서 다음과 같은 Iterator block도 생각해볼 수 있다.

IEnumerator TellMeASecret()
{
   PlayAnimation("LeanInConspiratorially");
   while(playingAnimation)
     yield return null;
 
   Say("I stole the cookie from the cookie jar!");
   while(speaking)
     yield return null;
 
   PlayAnimation("LeanOutRelieved");
   while(playingAnimation)
     yield return null;
}

이 Iterator block은 긴 연속적인 null 값을 생성하게 된다. 그러나 여기서 중요한 것은 이 로직을 실행하기 위해 발생한 사이드 이펙트이다.

아마도 아래와 같은 간단한 루프 내에서 위의 iterator block의 코루틴을 실행할 수 있다.

IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }

좀 더 실용성있는 예문은 다음과 같이 쓸 수 있을것 같다.

IEnumerator e = TellMeASecret();
while(e.MoveNext()) 
{ 
  // 'Escape'키를 누를 경우 컷씬을 건너뛴다.
  if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}

마지막의 예문으로 위의 코루틴이 어떻게 실행될지를 예측해보면,

먼저 PlayAnimation("LeanInConspiratorially"); 함수내에서 애니메이션이 구동되고 playingAnimation 변수는 true로 세팅이 된다.
while(playingAnimation) 에 의해 playingAnimation이 true 인 동안은 while 루프를 반복하게 되는데 루프안에 있는 첫번째 명령은 yield return null; 이다 따라서 여기서 MoveNext의 진행은 멈추고 return 값으로 null을 Current에 할당한다.

원래 유니티에서 Coroutine의 yield값이 null인경우 이번 프레임을 쭉 실행하고 다음 프레임에 다시 해당 코루틴을 중단 지점에서 다시 진행하도록 되어 있으나 이 예제는 Current 멤버 변수를 무시한다고 가정하고 있으므로 그냥 while 루프를 실행하는 MoveNext가 yield에서 잠시 멈추고 애니메이션을 진행하는 작업을 조금 하다가 다음번에 다시 while문으로 돌아 오게 되어 playingAnimation이 값을 다시 평가하고 같은 작업을 반복하게 된다.

중요한것은 MoveNext함수가 yield 키워드를 만나면 그 곳에서 함수의 진행을 멈추고 다른 작업에 제어권을 넘겨주는 점이다.

자 이제 yield로 넘겨주는 값이 Current 멤버 변수에 할당이 되는 점을 고려하여 유니티에서 해당 사항을 어떻게 처리하는지 알아 보자.

위의 예제에서 null을 yield하는 경우는 유니티에서 한프레임을 넘기게 된다. 그러나 만약 애니메이션을 보여주는 데 일정 시간이 필요하다거나 아니면 일정 시간이 지난 후에 무엇인가를 해야 한다거나 하면 위와 같이 yield return null;을 통해 프레임을 넘기고 매번 어떤 상황을 매 프레임 체크하는 것은 꽤나 비효율적이지 않은가?

그래서 유니티는 yield를 통해 Current에 할당된 값에 따른 몇가지 다른 종류의 기다림 로직을 YieldInstruction 타입으로 정의 해 두었다.

WaitForSeconds값을 만났을 경우는 Coroutine을 일정 시간이 지난 후 다시 재개 되도록 하고, WaitForEndOfFrame의 경우는 코루틴을 프레임이 렌더링 된 이후에 재개 되도록 하였다. 그리고 코루틴 자체를 Current 값으로도 받을 수 있는데 예를 들어 Coroutine A가 Couroutine B를 yield 하도록 한 경우는 Coroutine B가 끝나야 Coroutine B가 재개된다.

이러한 유니티의 로직을 코드로 표현해보면,

List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
 
foreach(IEnumerator coroutine in unblockedCoroutines)
{
    if(!coroutine.MoveNext())
        // This coroutine has finished
        continue;
 
    if(!coroutine.Current is YieldInstruction)
    {
        // This coroutine yielded null, or some other value we don't understand; run it next frame.
        shouldRunNextFrame.Add(coroutine);
        continue;
    }
 
    if(coroutine.Current is WaitForSeconds)
    {
        WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
        shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
    }
    else if(coroutine.Current is WaitForEndOfFrame)
    {
        shouldRunAtEndOfFrame.Add(coroutine);
    }
    else /* similar stuff for other YieldInstruction subtypes */
}
 
unblockedCoroutines = shouldRunNextFrame;

더 많은 YieldInstruction 타입의 로직이 어떻게 추가로 구현 되어 각기 다른 상황을 처리 할 것인지는 어렵지 않을 것이다.

한가지 팁은 yield return은 하나의 표현일 뿐이다. 따라서 다음과 같은 응용법도 얼마든지 가능하다.

YieldInstruction y;
 
if(something)
 y = null;
else if(somethingElse)
 y = new WaitForEndOfFrame();
else
 y = new WaitForSeconds(1.0f);
 
yield return y;

위와 같이 something, somethingElse 에 따라 코루틴이 언제 재개 되는지를 다르게 정의할 수 있다.

또한 유니티의 코루틴은 일반적인 C#의 iterator block이므로 해당 코루틴을 iterate 하는 로직을 스스로 만들 수도 있다.

IEnumerator DoSomething()
{
  /* ... */
}
 
IEnumerator DoSomethingUnlessInterrupted()
{
  IEnumerator e = DoSomething();
  bool interrupted = false;
  while(!interrupted)
  {
    e.MoveNext();
    yield return e.Current;
    interrupted = HasBeenInterrupted();
  }
}

위의 예제는 코루틴 내에서 iterator block을 직접 iterate 하는 로직을 구현하고 있다.

이상의 내용들이 유니티에서 코루틴을 사용할 때 무슨 일들이 일어나는지 이해하는데 있어 도움이 될 수 있었으면 좋겠다.

참고: http://www.altdevblogaday.com/2011/07/07/unity3d-coroutines-in-detail/