카테고리 보관물: unity

코루틴 결과값 리턴받기

코루틴을 사용한 리소스 비동기 로딩 구현 도중 특정 코루틴의 결과값을 리턴받아야 할 일이 생겼습니다.

public Coroutine LoadCharacterAsync(CharacterData data)
{
    return StartCoroutine(LoadCharacterAsyncRoutine(data));
}
  
IEnumerator LoadCharacterAsyncRoutine(CharacterData data)
{
    GameObject characterObject = new GameObject();
    PokoCharacter character = characterObject.AddComponent<PokoCharacter>();
     
    // 캐릭터를 구성하는 각 파츠의 SkinnedMesh를 읽어서 생성
    for(int i = 0; i < 3; ++i)
    {
        GameObject loadedParts = null;
  
        yield return StartCoroutine(LoadCharacterPartsRoutine(data.partsName[i]);
  
        // 위 코루틴의 결과값을 어떻게 받지?!
        character.AddParts(i, loadedParts);
    }
}
  
IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
}

혹시나 이런 게 되나 싶었는데

IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
    return loadedParts;
}

될 리가 없죠;; 

IEnumerator로 선언된 메소드 내에서는 ref, out 같은 키워드도 문법적으로 사용할 수 없습니다. 

해서 구글링을 해 봤더니 C#의 람다를 콜백으로 사용하는 신박한 방법이 있더라구요. 

IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
    return loadedParts;
}

NGUI <-> World 좌표계 상호 변환

1. World 좌표계 -> NGUI 좌표계

  • 사용 예시 : RPG 에서 캐릭터 머리 위에 따라다니는 HP Bar 등
  • 참고 : World 좌표계에서 Viewport 좌표계로 중간 변환하고 나서 다시 NGUI 좌표계로 변환한다.

UICamera.mainCamera.ViewportToWorldPoint (Camera.main.WorldToViewportPoint (worldPosition));

2. NGUI 좌표계 -> World 좌표계  

  • 사용 예시 : 캐릭터 아이콘 UI 에서 실제 월드상의 캐릭터 3D 모델에게 선을 긋는다.
  • 참고 : 위와 같이 Viewport 좌표계 변환이 한 번 있어야 한다.
  • 가장 중요한 포인트!
  • NGUI 좌표계에서 z값이 0 인데 특정 값을 꼭 넣어야 한다.
  • 그렇지 않으면 항상 카메라 위치가 나오게 되므로 2D->3D 가 애초에 안되니 Ray 를 쏘거나 복잡해지는데
  • z값에 값을 넣게 되면 간단히 해결!
public static Vector3 NGUIToWorld(Vector3 nguiPosition)
{
    nguiPosition.z += 5f;
    Vector3 worldPosition = Camera.main.ViewportToWorldPoint(UICamera.mainCamera.WorldToViewportPoint(nguiPosition));
    return worldPosition;
}

마우스의 현재 위치를 가져오기 위해 사용하는 Input.mousePosition은 스크린 좌표계를 사용한다.따라서 이 좌표를 월드 좌표로 변환해주기 위해 사용하는 것이 Camera.ScreenToWorldPoint()인데, 한가지 주의해야할 점이 있다.Input.mousePosition의 z값이 항상 0이라는 것이다.따라서 Camera.main.ScreenToWorldPoint(Input.mousePosition)을 하게 되면 그 결과는 카메라의 월드 좌표값이 나오게 된다.그래서 Input.mousePosition을 그대로 사용하면 안되고, z값을 변경해준 뒤에 사용해야 한다.예를들어 아래와 같이 사용할 수 있다.

Vector3 screenMouse = Input.mousePosition;screenMouse.z = Camera.main.WorldToScreenPoint(Vector3.zero).z;Vector3 worldMouse = Camera.main.ScreenToWorldPoint(screenMouse);

screenMouse의 z 값에 대입한 값의 정체를 생각해보자.월드 좌표계의 원점(0, 0, 0)을 스크린 좌표계로 변환한 값이다. 이는 카메라의 위치에 따라 다른 값이 될 수 있겠다.어쨌든 이렇게 하면 마우스 클릭이 월드 좌표계의 z값 0인 위치에서의 클릭 지점이 되겠다. 

플랫폼 쉽게 초스피드로 변경하기

유니티에서 리소스가 많아질수록 플랫폼 변경시에 시간이 많이 소요되는 문제가 있습니다.포코 메르헨의 경우에는 플랫폼 변경에만 30분정도 걸리네요.그래서 좀 찾아봤습니다.http://www.cliffordroche.ca/fast-platform-switching-in-unity/요약하자면 이런거에요.

  1. 기존에 e:/work 프로젝트 폴더가 있고, 이 프로젝트를 android 플랫폼으로 사용 중이라고 가정하겠습니다.
  2. windows 플랫폼을 하나 더 추가하고 싶다면 e:/ work_windows 폴더를 만들고,
  3. 커맨드 라인에서 e:/work_windows 폴더로 이동한 다음
  4. mklink /j Assets ..\work\Assets
  5. mklink /j ProjectSettings ..\work\ProjectSettings
  6. 유니티를 켜고 work_windows 프로젝트를 연 후 플랫폼을  pc, mac & linux standalone으로 변경합니다.

이렇게 하면 별도의 Library폴더를 가지게 되기 때문에, 코드 및 리소스를 공유를 하고 플랫폼만 다른 두개의 프로젝트를 유지할 수 있습니다.

광고 (IronSource)

IronSource

버전 히스토리

  • 6.10.1 (3.0.0 버전에서 적용)
  • 6.11.1 (3.2.0 버전에서 적용)

이슈

  • 안드로이드 환경에서 라이브러리들의 메소드갯수가 64K를 넘어가면 MultiDex를 사용해야 한다. 광고 라이브러리가 증가하면서 메소드의 갯수가 64K가 넘어가게됨 
    • Gradle시스템에서 MultiDex를 사용하여 해결
  • 안드로이드 5.0 미만의 환경에서 크래시가 나는 문제 발생
    • 안드로이드 5.0 미만의 환경에서 MultiDex를 사용하기 위해서는 추가 작업이 필요함. (메인 Application 클래스를 구현할 때 MultiDexApplication 클래스를 상속받도록 구현해야 하지만 트라이던트에서 TridentApplication을 따로 사용하고 있어 수정할 수 없는 문제가 있음)
    • 트라이던트의 TridentApplication을 상속받아 커스텀 할 수 있는 방법으로 해당 문제 수정
    • 참고 자료 : https://developer.android.com/studio/build/multidex?hl=ko#java

대시보드 주소 : https://platform.ironsrc.com/partners/dashboard

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/unity-plugin/

AdMob

버전 히스토리

  • 4.3.5
  • 4.3.6 (3.0.0 버전에서 적용)
  • 4.3.9 (3.2.0 버전에서 적용)

이슈

  • 안드로이드 환경에서 트라이던트가 사용하는 라이브러리와 충돌이 나는 문제 발생
    • 트라이던트에 있는 라이브러리를 모두 삭제한 뒤 Gradle 시스템으로 최신 라이브러리로 맞춰주도록 변경
    • 트라이던트 라이브러리 목록
      • 기존과 동일한 라이브러리
        • android.arch.core-common-1.0.0 (버전 유지)
        • android.arch.lifecycle:common:1.0.3 (버전 유지)
        • android.arch.lifecycle:runtime:1.0.3 (버전 유지)
      • androidx 용 라이브러리로 교체한 라이브러리
        • com.android.support:appcompat-v7:27.0.2 -> androidx.appcompat:appcompat:1.0.2
        • com.android.support:cardview-v7:23.2.1 -> androidx.cardview:cardview:1.0.0
        • com.android.support:customtabs:25.3.1 -> androidx.browser:browser:1.0.0
        • com.android.support:support-v4:27.0.2 -> androidx.legacy:legacy-support-v4:1.0.0
        • com.android.support:support-core-ui:27.0.2 -> androidx.legacy:legacy-support-core-ui:1.0.0
        • com.android.support:support-core-utils:27.0.2 -> androidx.legacy:legacy-support-core-utils:1.0.0
        • com.android.support:support-compat:27.0.2 -> androidx.core:core:1.0.2
        • com.android.support:support-annotations:27.0.2 -> androidx.annotation:annotation:1.0.2
        • com.android.support:support-fragment:27.0.2 -> androidx.fragment:fragment:1.0.0
        • com.android.support:support-vector-drawable:27.0.2 -> androidx.vectordrawable:vectordrawable:1.0.1
      • 버전업된 라이브러리
        • com.google.firebase:firebase-common:10.0.1 -> 18.0.0
        • com.google.firebase:firebase-iid:10.0.1 -> 18.0.0
        • com.google.firebase:firebase-messaging:10.0.1 -> 18.0.0
        • com.google.android.gms:play-services-auth:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-auth-base:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-base:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-basement:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-tasks:10.0.1 -> 17.0.0
      • 신규 추가 라이브러리
        • androidx.multidex:multidex:2.0.1 (라이브러리들의 메소드 수가 64K를 넘어가면 에러를 발생한다. 이를 위해 multidex를 사용)
        • com.google.android.gms:play-services-ads:18.2.0 (애드몹 광고를 위한 라이브러리)

대시보드 주소 : https://apps.admob.com

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/admob-mediation-guide/

AppLovin

버전 히스토리

  • 4.3.5 
  • 4.3.7 (3.0.0 버전에서 적용)
  • 4.3.8 (3.2.0 버전에서 적용)

이슈

  • 4.3.5 버전에서 아이언소스 무결성 체크 함수를 돌려보면 com.applovin.adview.AppLovinConfirmationActivity – MISSING 메시지가 발생
    • 라이브러리를 열어서 확인해 보면 4.3.4 버전까지는com.applovin.adview.AppLovinConfirmationActivity를 사용했지만 4.3.5 버전 부터는 사용하지않고, com.applovin.sdk.AppLovinWebViewActivity를 사용하도록 변경됨. 하지만 4.3.5 버전에서 무결성 체크에서 과거에 사용하는 com.applovin.adview.AppLovinConfirmationActivity를 체크하는 버그가 있어, 4.3.7 버전으로 업데이트 진행

대시보드 주소 : https://dash.applovin.com/o/analytics

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/applovin-mediation-guide/

Facebook

버전 히스토리

  • 4.3.6
  • 4.3.7
  • 4.3.8 (3.0.0 버전에서 적용)
  • 4.3.9
  • 4.3.10
  • 4.3.11 (3.2.0 버전에서 적용)

이슈

  • AOS환경에서 페이스북 광고를 시청하는 도중 앱을 백그라운드로 내린뒤, 앱 아이콘을 통해 다시 실행하면 광고가 꺼지는 문제 발생
    • 유니티는 SingleTask만 지원하기 때문에, 광고가 종료 되는 문제는 해결 할 수 없다.(페이스북 광고 개발 문서에서도 해당 문제는 스펙으로 처리되어 있다.)
    • 광고가 종료되는 것은 스펙으로 처리하는게 맞지만, 이렇게 광고가 종료되었을 경우 EndCallback이 오지 않는 문제가 있다.(EndCallbackdl 오지 않기 때문에 앱에서는 무한정 광고가 끝나기를 기다리기 때문에 입력이 먹히지 않고 앱이 멈춘것 처럼 보인다)
    • 해당 이슈를 해결하기 위해 4.3.6, 4.3.7, 4.3.8, 4.3.9, 4.3.10 다섯개의 버전으로 모두 테스트 해보았지만 해결할 수 없었다.

대시보드 주소 : https://developers.facebook.com/apps/527156608124582/dashboard/?business_id=154719308447585

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/facebook-mediation-guide/

Maio

버전 히스토리

  • 4.1.3 (3.0.0 버전에서 적용)
  • 4.1.4 (3.2.0 버전에서 적용)

대시보드 주소 : https://maio.jp/publisher/

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/maio-mediation-integration-guide/

Tapjoy

버전 히스토리

  • 4.1.9 (3.0.0 버전에서 적용)
  • 4.1.10 (3.2.0 버전에서 적용)

대시보드 주소 : https://ltv.tapjoy.com/s/partners/d294941a-e233-4d35-b5f7-321d08d21aa3#overview

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/tapjoy-mediation-guide/

TikTok

버전 히스토리

  • 4.1.1 (3.0.0 버전에서 적용)
  • 4.1.2 (3.2.0 버전에서 적용)

이슈

  • 일부 IOS 기기에서 앱 실행시 앱이 중지되는 문제 발생. 해당 이슈는 일본지역에서 그리고 일부 소수의 기기에서만 발생
    • TikTok광고를 끄고 실행하면 정상동작 하는 것을 확인
    • IronSource 6.11.1 버전과 TikTok 4.1.2 버전 업데이트 후 정상 동작하는 것을 확인

대시보드 주소 : https://ad.oceanengine.com/union/media/

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/pangle-audience-network-integration-guide/

Vungle

버전 히스토리

  • 4.1.8 (3.0.0 버전에서 적용)
  • 4.1.9
  • 4.1.10 (3.2.0 버전에서 적용)

이슈

  • AdMob 이슈 해결을 위해 안드로이드 라이브러리를 최신(AndroidX)으로 적용하면서, Vungle의 라이브러리가 AndroidX를 지원하지 않아 광고가 나오지 않는 문제가 발생
    • 벙글쪽에서 AndroidX용 라이브러리를 따로 제공받아 수정(vungle-android-sdk-6.4.11.aar 파일)
  • 4.1.8 버전에서  IOS vungle광고중 일부가 플레이 되지 않는 이슈 발생
    • 4.1.9버전에서 해결되었으나 4.1.9버전에서는 또다른 문제가 발생
  • 4.1.9 버전에서 AOS 권한요청 팝업이 뜨지 않는 문제 발생
    • vungleadapter.jar파일에 있는 manifest파일을 수정하여 해결(WRITE_EXTERNAL_STORAGE 권한 요청 구문 삭제)

업데이트시 주의사항

  • vungleadapter.jar에 있는 manifest파일에서 WRITE_EXTERNAL_STORAGE 권한 요청 구문을 삭제할 것 (하지 않으면 안드로이드 권한요청 팝업이 뜨지 않을 수 있음)
  • vungle-android-sdk-6.4.11.aar 파일은 벙글을 통해 직접 전달받은 커스텀된(AndroidX용) 파일로 버전업시 해당 버전이 AndroidX를 지원하는지 확인하고, 지원하지 않는다면 벙글쪽에 요청해야 한다.

대시보드 주소 : https://publisher.vungle.com/applications

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/vungle-mediation-guide/

※ 붉은색 이슈는 아직 미해결 이슈 입니다.

유니티 (Unity) 2D 모바일 게임 최적화 팁 & 체크리스트

유니티2D로 제작한 모바일 게임에서 60프레임을 안정적으로 지키고, 빌드 용량을 줄이기 위한 팁들입니다.

  • 빌드전 마지막으로 체크해야 될 옵션들 위주로 나열했습니다.
  • 최적화 방법 중 디자인 패턴과 관련된 부분은경우의 수가 너무 많아 제외했습니다.
  • 유니티 5.6 이상의 2D 게임, 안드로이드를 기준으로 했습니다.
    • 다른 버전과 플랫폼에도 일반적으로 적용할 수 있는 내용들도 포함했습니다.

최적화 하기전에!

코드 최적화는 추상적이고 간결하며 범용적인 코드를 만드는 것과 반대되는 경우가 많습니다.

최적화는 알아보기 쉬운 코드를 프로젝트에 집역적으로 만듭니다. 성능은 좋으나 알아보기 힘들고 재활용 할수 없는 형태로 만듭니다.

코드 뿐만 아니라 콘텐츠 자체에도 비슷한 논리가 적용됩니다.

그렇기에 최적화는 게임 개발의 후반부에 신경써야 합니다. 릴리즈 후에는 정말 미친듯이 최적화와 싸워야 합니다.

하지만 릴리즈 전에는 코드 구조와 콘텐츠가 허구한날 새롭게 바뀝니다.

게임 출시전에는 소요되는 시간 대부분을 이미 만든것에 대한 최적화 보다는 새로운 것에 집중하는게 좋을 수도 있습니다. 😛


간단하게 할수 있는 것들

  1. Application.targetFrameRate 를 60으로 설정합니다.
    1. 모바일에서는 30이 기본으로 설정되어 있습니다.
  2. 이상이 없다면 vSyncCount 를 Quality 설정에서 켭니다. 몇몇 안드로이드 기기에서는 60fps 이상 랜더하여 기기가 과열되는 것을 막아줍니다.
  3. 비어있는 유니티 메세지 함수들은 지우세요. Update 등의 함수 내부가 비어있어도, 호출은 계속 됩니다.
  4. GameObject.Find() 같이 씬 전체를 검색하는 함수를 되도록 쓰지 마세요.
    1. Update/FixedUpdate 에서는 절대 안됩니다.
  5. 사용하지 않는 게임 오브젝트는 미리 비활성화 하세요.
  6. 오브젝트를 실시간으로 파괴하는 것보다, 비활성화 하는게 비용이 싸다는 것을 알아두세요.

글로벌 일루미네이션 제거

  • 만약 라이팅을 가지고 노는 일이 전혀 없는 게임이라면, Precomputed Realtime GI, Based GI, Fog 등을 전부 끄세요.

Quality Setting 트윅

  1. default 보다 높은 프로필은 모두 해제하세요. (모바일이라면 Simple)
  2. 사용하지 않는다면, Pixel Light Count 를 0으로 하세요.
  3. Texture Quality 를 가능한 낮게 잡으세요. Half Res 일때도 모바일에서는 눈치채기 힘든 경우가 많은데 75%나 메모리를 절약해줍니다.
  4. 텍스쳐에 확대 축소를 사용하지 않는다면, minmaps 를 해제하세요. 33% 메모리를 절약해줍니다.
  5. 비스듬한 각도에서 텍스쳐를 봐야하는 일이 없다면, Anisotropic Textures 를 해제합니다.
  6. Anti Aliasing 을 끄거나 2 passes 로 설정하세요. 그정도만 해도 지글거림을 해결해줍니다. LineRenderer 를 사용한다면 눈에 거슬릴순 있습니다.
  7. Soft Particle을 끕니다. 메쉬들을 적절히 섞어주는 역할을 하지만, 3D 상에서의 문제입니다.
  8. Shadow 를 끕니다.

여기서 가장 눈에 띄는 효과를 가진 것은, 텍스쳐 설정과 안티 얼라이언싱 입니다.

텍스쳐 퀄리티는 메모리에 직접적인 타격을 가하고 GPU 에 이미지를 보내는데 얼마나 시간을 먹을지 결정합니다.

안티얼라이언싱은 전체 화면에 적용되므로 해상도가 커짐에 따라 비용이 가파르게 증가합니다.


알아차리기 힘든 메모리 할당들

게임 엔진을 쓰면 C++ 로 직접 로우레벨을 통제할때 보다 메모리 관리에 대한 통제권이 적을 수 밖에 없습니다.

그래도 메모리가 언제 점유되는지 알고 쓰는게 최적화에 도움이 됩니다.

오브젝트 풀링 반드시 쓰기

게임 도중에 GC 가 발동된다면 게임이 실행 도중 뚝뚝 끊길수 있습니다.

GC 는 어플리케이션이 메모리를 추가로 요구할때 발동됩니다.

필요한 것들을 미리 게임 초반에 인스턴스화 하여 메모리를 점유하고, 게임 플레이 도중에는 더이상 요구하지 않는 다면 GC 문제를 겪지 않습니다.

실시간으로 많이 찍어내고 파괴하는 오브젝트는 오브젝트 풀링으로 관리하세요.

리듬게임의 노트 처럼 싱크가 중요한 경우라면 더더욱 써야 합니다.

가장 간단한 구현 방법:

  • 오브젝트를 파괴할때: 파괴대신 오브젝트를 끕니다.
  • 오브젝트를 생성할때: 꺼져있는 오브젝트를 가져와서 리셋하고 씁니다.

string 은 사용하는 만큼 생성됩니다.

string message = “Hello” + “World”; 같이 연결하는 부분을 주의하세요.

이 경우 실제로는 3개의 string 이 메모리에 할당합니다.

  • StringBuilder 나 String.Format() 을 사용하세요.

클래스와 구조체의 차이점을 고려하세요.

클래스는 Pass by Reference,  구조체는 Pass by Value 로 동작합니다.

값을 전달할때 class 는 레퍼런스가 할당되고, struct 는 복제본을 전달합니다.

struct

단순 데이터 컨테이터용으로 50개의 struct를 사용해봅시다.

struct는 메모리를 많이 사용합니다.

A->B->C 로 값을 전달하면, 총 150 개의 struct 가 생성되게 됩니다.

레퍼런스와 관련된 처리가 없으므로 더 빠를 수 있습니다.

구조체는 스택에 할당되고, 자신을 호출한 함수가 종료되면 즉시 사라집니다.
GC를 유발하지 않고 빠르게 동작합니다.

오히려 더 느려질 수도 있습니다.

단일 struct 오브젝트의 크기가 너무 커지거나, 너무 긴 리스트에 넣고 쓰면, 복사하는 시간이 길어집니다. 이때 class 형보다 처리 시간이 더 길어질 수 있습니다.

  • 변수가 2~3개 뿐인 단순한 데이터 컨테이너라면 struct 를 사용하는게 빠릅니다.
  • 일반적인 경우에는 class 를 쓰세요.

필자는 그리드형 게임에서 2차원 좌표용으로 struct를 사용합니다.

struct Coord { int x; int y;}

프로파일러를 사용합시다

  1. 유니티 프로파일러로 병목 지점을 확인하세요
  2. CPU Usage 란에서, GC Alloc 기준으로 정렬하면, 누가 할당을 언제 시도하는지 볼 수 있습니다. 할당을 최대한 줄이고 몰아서 하면, 게임이 부드러워집니다. physics.engine 할당 같은 것은 제어할 수 없지만, 그 정도는 괜찮죠.
  3. Memory Area 에서는, 현재 메모리 상태의 스냅샷을 찍을 수 있습니다. 어디에서 메모리 누수가 발생하는지 파악가능합니다.

System.GC.Collect() 로 GC를 원하는 시점에 발동시킬 수도 있습니다.

컷신이 끝난 직후나, 다음 플레이어의 조작이 활성화 되기 전이라던지, 그런 시점에서 쓸수 있습니다.

먼저 GC를 해둠으로서, 예상치 못한 시점에 발동될 확률을 낮추는 거죠.


로그와 메모리 낭비

의외로 성능을 먹는 Debug.Log

Debug.Log() 등의 호출을 가능한 지우는 것이 좋습니다.

  • 로그를 호출하는 함수는, 콘솔창이 없는 빌드된 게임에서도 동작합니다.
  • 의외로 가시적인 성능을 요구합니다.

프로파일링 할때,  Development Build 를 선택하고 프로파일링 해보면 체감이 됩니다. 로그 메세지가 스택 트레이스에 잡힐때 게임이 느려집니다.

  • 입력에 string 을 합쳐서 전달하면 메모리를 더욱더 많이 요구합니다.
  •  ToString 을 사용하면 더 심합니다.
  • 예 – Debug.Log( “Player ” + player + ” is doing something in game ” + game );

해결책

세가지 해결책이 있습니다.

Debug.Log 를 랩핑합니다.

노가다로 Debug.Log 를 전처리기로 랩핑하고 함수를 만듭니다.

public class DebugX
{
    public static void Log(string msg)
    {
        #if UNITY_EDITOR
        Debug.Log(msg);
        #endif
    }
}

하지만 이 방법은 string 입력을 받는 순간에 일어나는 메모리 할당을 막을 수는 없습니다.

Find and Replace

빌드할때만 잠시 모든 디버그 라인을 죄다 찾아가 지웁니다. 가장 무식하지만 효과적입니다.

하지만 다시 매번 디버그 코드를 되돌리는 수고를 해야 합니다.

C# 조건 속성을 사용합니다.

참고하기: https://msdn.microsoft.com/en-us/library/4xssyw96(v=vs.90).aspx

깔끔하고 우아한 방법입니다. 이 방법을 추천합니다.

using System;
using System.Diagnostics;

public class DebugX
{
    [Conditional("UnityEditor")]
    public static void Log(string msg)
    {
        Debug.Log(msg);
    }
}

// PlayerSettings > Scripting Define Symbols 에 ACTIVE_DEBUG 가 없다면 아래 코드 호출은 아예 처음부터 없었던 것으로 처리됩니다.
DebugX.Log( "Hello world" );

클래스 내부에 Conditional 속성이 지정된 static void 함수를 추가합니다.

  1. 만약 Conditional 에 따라오는 심볼이 정의됬다면 static 함수는 존재합니다.
  2. 중요한것은 static 함수 뿐만이 아니라, 그것을 호출하는 부분 까지 모조리 빌드에서 빠져나가므로 매우 간편합니다.

조건 속성으로 묶인 디버그 함수를 사용한 코드들은 빌드시에 처음부터 존재하지도 않은 것이 됩니다.

다만 유니티 콘솔창에서 로그를 더블 클릭하면, 오류가 난 지점이 아닌 디버그를 랩핑한 코드로 이동하는 사소한 불편은 있습니다.


오디오 (사운드)

오디오 기본 설정

Force To Mono 사용

모바일에서 스트레오는 큰 의미를 가지지 않습니다. Force To Mono 를 사용합시다.

Preload Audio Data

Preload Audio Data는 기본 옵션입니다. 미리 오디오 데이터를 로드하므로, 씬 로드에 조금 부담을 줍니다.

이것을 사용안하면, 오디오를 재생하는 순간에 오디오를 로드 합니다. 그러면 게임 도중에 약간의 오버헤드가 생길 수 있습니다.

Load in Background

엄격하게 재생 타이밍을 맞춰야 하는 경우가 아니라면 체크해도 됩니다.

엄격하게 로드 타이밍을 지키지 않고, 느긋하게 백그라운드에서 로드 합니다.

씬이 시작되자 마자 재생을 시도할때, 조금의 지연시간이 지난 다음에야 재생되게 됩니다. 그래도 로딩 속도를 적게나마 향상시킬 수 있습니다.

오디오 매니저 (프로젝트 세팅에 있음)

스트레오 사운드가 필요없으면 오디오 매니저에서 스피커 모드를 Mono나 Raw로 합시다.

로드 타입

메모리 많이 먹고 빠릿함 vs 메모리 적게 먹고 오버헤드

  • Decompress On Load

미리 압축을 해제해서 메모리에 적재하기에, 플레이 하는 순간에 오버헤드가 생기지 않습니다.

적은 용량의 효과음에 사용합시다. 압축을 미리 메모리에 풀기 때문에 메모리를 많이 먹기 때문입니다.

  • Compress On Memory

메모리에 로드할때는 압축되어 있다가 재생시 압축을 해제합니다.

플레이하는 순간에 압축을 푸는 과정이 생기기에, 오버헤드가 생길 수 있습니다. 메모리에 그대로 적재하기는 힘든 큰 용량의 배경 음악에 사용합시다.

오디오 압축 포맷 (Compression Format)

오디오 압축은 기본적으로 아래의 사항에서 적절한 중간 지점을 선택하는 과정입니다.

높은 품질 + 많은 메모리 사용 + 적은 오버헤드 vs
낮은 품질 + 적은 메모리 사용 + 많은 오버헤드

얻는게 있으면 잃는게 있는법이죠.

압축률이 낮을수록 품질도 높고 오버헤드도 적으나, 빌드 용량과 메모리를 많이 먹습니다.

압축률이 높으면 빌드 용량과 메모리를 적게 먹으나, 압축을 푸는 과정에서 재생이 지연될 수 있습니다.

중요: 유니티에 임포트하기전에 미리 손실 압축을 거는 것은 의미 없습니다!

PCM

품질이 제일 높습니다. 무압축 입니다.

  • 압축을 푸는 과정이 없어서 오버헤드가 없고, 재생이 지연 되지도 않습니다.
  • 빌드 용량과 메모리를 제일 많이 차지합니다.
  • 즉시 재생해야 하는 매우 짧은 효과음에 씁시다.

ADPCM

PCM 과 Vorbis/MP3 중간의 퀄리티와 압축률을 가집니다.

  • PCM 과 비교해서: 높은 압축률, 낮은 품질, 높은 오버헤드
  • Vorbis/Mp3 와 비교해서: 낮은 압축률, 높은 품질, 낮은 오버헤드
  • 어디까지나 사실상 무압축인 PCM 보다 용량이 낮고,  품질이 나쁘다는 얘기입니다.
    일반적인 손실 압축에 비해 품질 유지가 뛰어나고 CPU 오버헤드도 크지 않죠.
  • PCM 보다는 지연과 오버헤드가 존재하지만, Vorbis/MP3 와 비교할시 지연과 오버헤드 없이 즉시 재생됩니다.
  • 총격 소리와 같이 무압축에 가까운 품질 까지는 필요없지만, 지연시간 없이 자주 반복 재생 되야 하는 경우 적절합니다.

Vorbis/MP3

가장 높은 압축률을 가집니다.

  • 당연히 품질이 제일 안좋습니다. 하지만 용량을 제일 덜 먹어요.
  • 로드 혹은 재생시 압축을 풀기 때문에 지연 재생될 수 있습니다.
  • 무압축으로 할시 용량을 감당할수 없고, 지연 재생 되어도 무방한 일반적인 배경음에 씁시다.

2D 게임 텍스쳐 관리하기

중요! TinyPNG 같은거 쓰지마세요.
유니티가 알아서 손실/무손실 압축합니다.

임포트 하기전에 손실 압축 하는 것은 의미가 없어요.

이미지(텍스쳐) 설정

Generate Mil Maps 해제

카메라와의 거리를 실시간으로 조정하는 게임이 아닌 이상 필요 없습니다.

필터링

도트형 레트로 스타일의 게임에 필터를 사용하면 안됩니다!

도트형 게임은 필터를 적용하지 않는 Point 로 적용 합시다. 외곽선을 부드럽게 꺽기 때문에 비주얼상 더 난잡해 보일 수 있어요. 이외에는 무난하게 Linear 면 적당.

아틀라스 aka 스프라이트 팩킹

반드시 사용해야 하는 이유

  • 이미지는 2의 배수의 정사각형으로 메모리에 적재됩니다.
    하나의 이미지에 대해서 많은 여백이 남게되죠.
    이 여백을 낭비하지 말고 다른 이미지로 채워서 메모리를 아낍니다.
  • 자주 사용되는 이미지 끼리 하나의 파일로 뭉쳐 한번에 호출하면, 호출을 개별로 여러번 할 필요가 없어서 성능이 절약됩니다.

결론: 메모리와 성능이 절약됩니다.

사용방법

  1. Packing Tag 를 팩킹할 이미지 끼리 같은 이름으로 지정해주면 유니티가 알아서 팩킹해줍니다.
  2. 그외 옵션은 이곳을 참고할수 있습니다. http://lhh3520.tistory.com/350
  3. Resources 폴더의 텍스쳐는 엔진 구조상의 문제로 유니티 기능만으로는 팩킹 불가능합니다.

이미지 압축 (Compression) – 안드로이드 기준

상식

압축 포맷이 빌드 타켓 플랫폼과 호환되는지 확인하세요!

호환되지 않는 포맷은 해당 플랫폼에서 무압축으로 변환되어 사용됩니다. 그리고 압축된 포맷을 무압축으로 해제하는 과정에 오버헤드가 발생합니다.

=> 처음부터 무압축으로 올렸을 때 보다 오버헤드가 더 생깁니다.

알파 값이 없는 이미지라면, 알파값을 지원하지 않는 압축 포맷을 사용합니다.

알파값을 쓰지도 않는데, 알파값에 데이터를 할당하므로 RGB 컬러 퀄리티는 상대적으로 손상될수 밖에 없습니다.

특정 플랫폼에 무관하게 압축을 설정할시

Override 를 사용하지 않고 일괄 적용하는 경우입니다.

  • 압축 옵션의 포맷은 알파값이 있다면 Truecolor 아니면 Compressed 옵션을 추천 합니다.
  • 알파값이 있는 이미지는, Compressed 옵션으로 아낀 용량에 비해, 품질 손상이 좀 많이 심합니다.
  • 반대로 알파값이 없는 경우, 큰 용량 절감 효과를 보여주고 상대적으로 알파값이 있는 경우보다 품질 손상도 적습니다.
    • 알파값이 없는데 알파값이 있는 이미지로서 인식됬다면 Advanced에서 RGBA가 아닌 RGB 압축 포맷으로 지정해줍시다.

ETC2 vs ETC1

안드로이드의 메이저한 압축 포맷은 ETC2 와 ETC1 입니다.

  • ETC2 는 더 나은 성능과 품질을 제공하지만 OpenGL ES3.0 에서 지원됩니다.
    • 즉 안드로이드 4.4 버전 이하에서는 제대로 동작하지 않는 경우가 많습니다.
  • ETC1 은 OpenGL ES2.0 에서 지원되나, 알파 (투명도) 를 지원하지 않습니다.
    • ETC1 을 설정할 경우 Spilt Alpha Channel 옵션을 통해 알파를 분리한 또다른 파일을 생성하여 알파를 지원하는 꼼수가 있습니다.
      • 다만 이 옵션은 버그인지, UGUI 에서 사용할시 비정상적으로 표현됩니다. (유니티 5.6.1 기준)
  • ECT1 지원폰도 ETC2 호환은 됩니다. 다만 무압축으로 변환되서  메모리가 터집니다.

무엇을 선택할 것인가 에 대한 딱 떨어지는 답이 없어요.

  • ETC2 가 대세긴 하지만 아직도 ETC1 만 지원하는 석기시대 안드로이드 폰의 점유율이 작지 않기 때문에, 타겟을 잘 고려해야 합니다.


ES2.0 의 징그러운 생존율.

안드로이드 고유의 파편화 때문에 조금 걸러 볼 필요는 있습니다. 예를 들어, 대시보드에서 ES2.0 이라고 나와도 ES3.0 을 제한적으로 지원하는 경우도 많으니까요.

이외의 압축 포맷

RGBA32

  • 무압축 입니다. 가장 높은 품질을 가집니다. 메모리를 많이 먹지만, 품질을 높게 유지해야 하는 경우 추천 합니다.
  • 특히 유저가 매우 자주 보게 될 UI 에서는 이 옵션을 사용합시다.

RGBA16

  • 알파 품질이 조악합니다.
  • 제한된 16비트에 RGB 는 물론 A 채널까지 포함하니 품질이 나쁠 수 밖에 없죠.

RGB16

  • 쓸만한 압축률과 좋은 컬러 품질을 보여 줍니다.
  • RGBA 와 달리 16비트를 온전히 RGB에 다 쓰기 때문이죠. 물론 알파 값은 지원 못합니다.

RGB24

  • RGBA32 와 동등한 품질을 보여줍니다. 단 알파값을 지원 못합니다.

기타 참고사항

압축시 2의 제곱수를 변길이로 가진 정사각형으로 이미지를 사용합니다.

여백을 활용하기 위해 정사각형에 가깝게 이미지를 제작하는 걸 고려할 수 있겠네요.

  • 예를 들면 900×1600의 해상도를 가진 물체라면 1024×1024와 2048×2048 중에 maxSize를 고민하게 됩니다. 전자는 화질이 손상되고 후자는 낭비니까요.
  • Sprite Packing 기능으로 묶어서 여백을 활용해도 되고요.

Compress Quality 를 통해 손실 압축 포맷들은 압축의 품질을 결정할 수 있습니다.

당연히 Best 에 가까울수록 동일 품질에 대한 압축률이 좋아지겠지만, 유니티 에디터 상에서 최초 압축 및 팩킹시 매우 많은 시간을 소비합니다.

  • 즉시 빌드를 해야할때, 긴 압축시간으로 피볼수도 있습니다.

UGUI 오버헤드 관리

하나의 캔버스에 모든 UI 요소를 넣는 것이 꼭 좋은것은 아닙니다.

캔버스에서 어떤 UI 요소의 스프라이트가 동적으로 변할때, 캔버스에 있는 전체 요소들도 동시에 갱신됩니다.

즉 하나가 변하면, 같은 캔버스에 다른 친구들도 같이 갱신됩니다.

한 UI 오브젝트가 변해도, 갱신 비용은 같은 캔버스 밑에 모든 UI 오브젝트 만큼 드는 거죠.

  • 따라서 서로 같은 타이밍에 내용이 (텍스트나 이미지가) 바뀌는 오브젝트 끼리 같은 캔버스로 묶으면 성능에 좋습니다.

그러나 개발자가 직관적으로 편집하기 좋게 하이라키에 배치하는 것도 고려해야 하니, 어디까지나 케이스 바이 케이스.

폰트

  1. 완성형 한글의 모든 글자를 메모리에 집어넣는 메모리 빌런이 되고 싶지 않다면 Dynamic을 사용합시다. 폰트를 임포트할때 기본값으로 Dynamic 이 지정됩니다.
  2. 만약 해당 폰트를 꼭 써야하는게 아니라면 Incl. Font Data를 해제해도 됩니다. 해당 폰트가 시스템에 존재하면 사용하고, 없으면 시스템 폰트를 가져다 쓸것입니다. 빌드 사이즈가 줄어듭니다.
    다만 의도한 디자인을 유지하기 힘들어서, 이 방법을 추천하진 않네요.

물리 설정

  1. 물리, 레이캐스팅을 사용하지 않는 형태의 게임이라면 충돌 레이어 설정을 해제해도 됩니다. 프로젝트 세팅 중 Physics 에서 Collision 레이어 항목 체크를 해제하면 되죠.
  2. FixedUpdate는 랜더와 상관없이 물리를 위해 독립적으로 일정 간격 호출되는 함수입니다.
    리기드 바디 등을 사용하지 않는 게임이라면, 물리 체크를 자주 할 필요가 없겠죠. 필요에 따라 호출 간격을 길게 해서 성능을 아낄수 있습니다.

기타 & 빌드시 세팅

  1. 절대 Keystore 파일 잃어 먹지 마세요. (미아앱 되기 싫으면 클라우드 저장소에 백업 좀 하셈)
  2. 유니티가 빌드시 알아서 사용되지 않는 애셋은 제외합니다.
    • 하지만 스크립트 파일은 사용되든 사용되지 않든, Editor 폴더에 있지 않는한, 모두 포함되서 빌드됩니다.
  3. Resource 폴더는 안쓸수 있다면 최대한 쓰지 맙시다.
    • 여기 들어가는 요소들은 실제 사용되든 사용되지 않든 빌드에 무조건 포함되고 메모리에 적재됩니다.
  4. 안드로이드 빌드시 세팅
    • x86(데스크탑 안드로이드) 를 사용하는 기기는 거의 없습니다.
    • FAT 은 x86과 ARM 을 모두 빌드하므로 용량이 커집니다.
      • FAT을 하지 말고 ARM 대상으로만 빌드합시다.
      • 혹은 x86 과 ARM 을 따로 빌드해서 구글 플레이 콘솔에 멀티 APK 로 올려도 됩니다.
    • Stripping Level의 경우 가능한 높은 수준을 추천합니다. 다만, micro nmscorlib이나 byte code 세팅이 프로그램을 크래쉬할 수 있으니 테스트는 해봅시다.
    • 32비트의 컬러 품질이 필요한지 비교후, 그렇지 않다면 체크 해제합니다.
    • 호환에 문제가 없다면 빌드 용량을 줄이기 위해 기본값인 .NET 2.0 Subset 을 사용합니다.
  5. 클라우드 테스트와, 안드로이드 실제 기기 테스트를 둘다 애용 합시다.
    • 쓸만한 무료 클라우드 테스트로는 구글 플레이 콘솔에서 지원하는 “출시전 보고서” 기능이 있습니다.
    • 즉시 사용 가능한 유료 클라우드 테스트는, 아마존 디바이스팜과 파이어베이스 테스트 랩이 있습니다.

계속 갱신중.

모든 글보기

홈으로 돌아가기

  1. Dooseop Choi: 공감하는 글입니다. 학점은 좋았으나 코딩실력은 부족했습니다. 다만, 예화로 들려주신 분처럼 팀별 프로젝트를 할 때 파일이…Commented on: 학점은 정말 믿을게 못된다.
  2. 이제민: 1. 본래 네임스페이스와 상관없이 동작해야 하는게 맞습니다. 2. 제 생각에는 네임스페이스 문제가 아니라, VSCode로 열린…Commented on: 유니티에 비주얼 스튜디오 코드 (Visual Studio Code; VSCode) 연동방법
  3. 호나: 오류는 해결했는데 … 자동완성도 안되고 책에 있는 내용중 5장 게임 오브젝트 제어하기 파트에 p.185 ~…Commented on: 유니티에 비주얼 스튜디오 코드 (Visual Studio Code; VSCode) 연동방법
  4. 이제민: 만들고 싶은 것만 찾을 수 있다면 학점이나 시작하는 시기는 별로 상관 없습니다. 실제로 신경 쓰는…Commented on: 학점은 정말 믿을게 못된다.
  5. 뒷이야기: 딱 제 얘기네요. 학점은 4점대인데 코딩 실력은 참 형편없습니다. 후우…Commented on: 학점은 정말 믿을게 못된다.

SearchIndependent Publisher empowered by WordPress

Unity Gradle 빌드에 관하여

Unity 로 앱 및 게임을 구현하는데 있어

안드로이드 Native 기능을 사용하고자 하는 경우,

Plugin 형태(jar, aar)로 만든후 프로젝트에 import 하여 빌드해서 사용하게 된다.

그리고 최근 안드로이드 Native 기능들의 경우 대다수 모듈들이

Gradle 등의 빌드 툴을 통해 그때 그때 필요한 내용을 다운받아 사용하게 되어있다.

이러한 점 때문에 빌드시 문제가 생길 소지가 있다.

Unity가 빌드하는 동안 Native 모듈에서 사용하는 라이브러리를 다운받지 못해 

필요한 Class를 찾지 못하는 등의 문제를 발생시키기 때문입니다.

이를 해결하기 위해 Untiy에는 Gradle 빌드를 지원하고 있다.

이 Gradle 빌드를 사용하는 경우 개발자는 보통 다음의 수순을 따르게 된다.

1. Build System을 Gradle로 설정 및 Export Project 체크 하여 빌드 진행

2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크

    (build.gradle 내용 확인 및 컴파일 이슈 확인)

3. 최종 빌드 확인 후 유니티 상에서 build.gradle 파일 수정

4. Export Project 체크 해제 후 빌드 진행.
   (이 시점부터 안드로이듯 스튜디오로 프로젝트를 열어서 빌드할 필요 없음)

하나씩 자세히 살펴보면 다음과 같다.

1. Build System을 Gradle로 설정 및 Export Project 체크 하여 빌드 진행

우선 유니티에서 빌드를 진행하며 에러나는 상황을 확인하는 것은 스트레스가 크고 시간이 오래걸린다.

그래서 우선 Build System을 Gradle로 설정하고 Export Project 를 체크하여 안드로이드용 프로젝트를 추출하는것을 우선으로 한다.

2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크

    (build.gradle 내용 확인 및 컴파일 이슈 확인)

추출된 프로젝트를 안드로이드 스튜디오로 열고 빌드를 진행하여,

에러 상황을 체크하고 

build.gradle 파일의 repositories 및 dependencies의 내용을 추가 및 수정한다.

이 단계에서는 발생되는 에러가 있어서 (에러가 기억이 안나요…ㅠㅠ) 

보통은 아래 그림과 같이 google(), jcenter() 순서로 기입을 하고 시작하는 것이 좋다.

그 외에 빌드 후 테스트 하는 경우 

유니티가 뽑아주는 build.gradle은 Debug 빌드시에는 Key Signing 하는 부분이 생략되어 있기 때문에, apk 설치시 에러(INSTALL_FAILED_NO_MATCHING_ABIS)가 발생할 수 있다. 

이를 미연에 방지하기 위해 다음의 코드를 추가하는 것이 좋다.

여기까지 빌드 하고 확인이 되었다면 유니티로 돌아가자.

3. 최종 빌드 확인 후 유니티 상에서 build.gradle 파일 수정

이제 유니티 상에서 빌드를 할 수 있도록 build.gradle을 설정해야 한다.

PlayerSerttings -> Publishing Settings로 가면 Custom Gradle Template이라는 항목이 존재한다.

이것을 체크해야 유니티에서 빌드시 사용하는 build.gradle 파일을 수정할 수 있다.

체크하면 Assets/Plugins/Android/mainTemplate.gradle 파일이 생기며 이 파일의 내용을 수정하면

Gradle 빌드시 이 파일을 기반으로 빌드를 진행하게 된다.

mainTemplate의 내용에 추가할 것들은  

앞서 [2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크] 과정에서 진행했던 안드로이드 스튜디오 프로젝트의 build.gradle에 추가한 것들을 옮겨주면 된다.

[ 샘플 파일을 올려두었으니 참고 할 것 ]

4. Export Project 체크 해제 후 빌드 진행. 
   (이 시점부터 안드로이듯 스튜디오로 프로젝트를 열어서 빌드할 필요 없음)

이제 Export Project 체크를 해제하고 빌드를 진행하면 안드로이드 스튜디오 프로젝트 빌드 없이도 정상적으로 빌드되는 것을 확인할 수 있다.

하지만 사용하는 라이브러리 버전에 따라서 

빌드중에 다음과 같이 Gradle 버전 에러가 나는 경우가 있다.

 CommandInvokationFailure: Gradle build failed. C:\Program Files\Java\jdk1.8.0_181\bin\java.exe -classpath “C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Tools\gradle\lib\gradle-launcher-4.2.1.jar” org.gradle.launcher.GradleMain “-Dorg.gradle.jvmargs=-Xmx2048m” “assembleDebug”

이 경우에는 유니티 내의 Gradle 버전을 올리면 해결할 수 있다.

유니티 기본 설치폴더 및 안드로이드 스튜디오 기본 설치 폴더를 기준으로 

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Tools\gradle 내의 lib 폴더(유니티 내에서 사용중인 Gradle 버전)를 

안드로이드 스튜디오

C:\Program Files\Android\Android Studio\gradle\gradle-4.6 폴더 내의 lib 폴더(안드로이드 스튜디오에서 사용한 최신 버전)로 교체하면 된다.

이 작업시에는 유니티를 종료하고 작업해야 한다.

Script Dependencies Function for Unity

using UnityEngine;
using UnityEditor;
using System.Collections; 

public class ComponentLister : EditorWindow
{
    bool forceRefresh=false;

    private bool onlyScripts=false;
    private string filterComponent="";
    private SortedList sets = new SortedList(new myReverserClass());
    private ArrayList scripts = new ArrayList();
    private Vector2 scrollPosition;

   public class myReverserClass : IComparer  {

      // Calls CaseInsensitiveComparer.Compare with the parameters reversed.
      public int Compare( System.Object x, System.Object y )  {
          return ( (new CaseInsensitiveComparer()).Compare( ((System.Type)x).Name, ((System.Type)y).Name ) );
      }
   }

    [ MenuItem( "Component/Component lister" ) ]
    public static void Launch()
    {
        ComponentLister window = (ComponentLister)GetWindow( typeof( ComponentLister ) );
        window.forceRefresh=true;
        window.Show();
    } 


    public void UpdateList()
    {
        Object[] objects; 

        sets.Clear(); 
        scripts.Clear();

        objects = FindObjectsOfType( typeof( Component ) );
        foreach( Component component in objects )
        {
            if (component is MonoBehaviour) {
                //Debug.Log(component.GetType() + " attached to "+component.name);
                scripts.Add(component.GetType());
            }

            if( !sets.ContainsKey( component.GetType() ) )
            {
                sets.Add(component.GetType(),new ArrayList());
            } 
            int i=sets.IndexOfKey(component.GetType());
            ( ( ArrayList )sets.GetByIndex(i)).Add( (string)component.gameObject.name );
        }

        foreach( Component component in objects )
        {
            int i=sets.IndexOfKey(component.GetType());
            ( ( ArrayList )sets.GetByIndex(i)).Sort();
        }       

    } 

    public void OnHierarchyChange() {
        //Debug.Log("OnHierarchyChange");
        UpdateList();
        Repaint();
        forceRefresh = true;
    }

    public void OnGUI()
    {
        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
            GUILayout.Label( "Components in scene:" );

            GUILayout.FlexibleSpace();

            if( GUILayout.Button( "Refresh" ) || forceRefresh )
            {
                //if (forceRefresh) Debug.Log("forceRefresh");
                forceRefresh=false;
                UpdateList();

            }
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
        filterComponent = EditorGUILayout.TextField ("Filter:", filterComponent);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
        onlyScripts = EditorGUILayout.Toggle("Show only scripts", onlyScripts);
        GUILayout.EndHorizontal();

        scrollPosition = GUILayout.BeginScrollView(scrollPosition);


            foreach( System.Type type in sets.Keys )
            {   
                if (type.Name.ToLower().Contains(filterComponent.ToLower())) {
                    bool showGameObjects=false;
                    if (scripts.Contains(type)) {
                        GUI.contentColor = Color.yellow;
                        GUILayout.Label( type.Name + " (Script):" );
                        showGameObjects=true;
                    }
                    else if (!onlyScripts){
                        GUI.contentColor = Color.white;
                        GUILayout.Label( type.Name + ":" );
                        showGameObjects=true;
                    }

                    if (showGameObjects) {
                        GUI.contentColor = Color.white;
                        foreach( string gameObject in ( ArrayList )sets[ type ] )
                        {
                            if( GUILayout.Button( gameObject ) )
                            {
                                Selection.activeObject = GameObject.Find(gameObject);
                            }
                        }
                    }
                }
            } 

        GUILayout.EndScrollView();
    }
}

Curves and Splines, making your own path

This tutorial will take you from creating a simple line all the way to editing your own Beziér splines. You’ll learn to

  • Create a custom editor;
  • Draw in the scene view;
  • Support editing via the scene view;
  • Create Beziér curves and understand the math behind them;
  • Draw curves and their direction of movement.
  • Build Beziér splines by combining curves;
  • Support free, aligned, and mirrored control points;
  • Support looping splines;
  • Move and place objects along a spline.

This tutorial builds on the foundation laid by previous tutorials. If you completed the Maze tutorial then you’re good to go.

This tutorial was made with Unity 4.5.2. It might not work for older versions.

Fun with splines.

Lines

Let’s start simple by creating a line component. It needs two points – p0 and p1 – which define a line segment that goes from the first to the second.

using UnityEngine;

public class Line : MonoBehaviour {

	public Vector3 p0, p1;
}
A simple line.

While we can now create game objects with line components and adjust the points, we don’t see anything in the scene. Let’s provide some useful visual information when our line is selected. We can do this by creating a custom inspector for our component.

Editor-related code needs to be placed inside an Editor folder, so create one and put a new LineInspector script in it.

The inspector needs to extend UnityEditor.Editor. We also have to give it the UnityEditor.CustomEditor attribute. This lets Unity know that it should use our class instead of the default editor for Line components.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Line))]
public class LineInspector : Editor {
}

An empty editor does not change anything. We need to add an OnSceneGUI method, which is a special Unity event method. We can use it to draw stuff in the scene view for our component.

The Editor class has a target variable, which is set to the object to be drawn when OnSceneGUI is called. We can cast this variable to a line and then use the Handles utility class to draw a line between our points.

	private void OnSceneGUI () {
		Line line = target as Line;

		Handles.color = Color.white;
		Handles.DrawLine(line.p0, line.p1);
	}
line
Showing a line.

We now see the line, but it doesn’t take its transform’s settings into account. Moving, rotating, and scaling does not affect them at all. This is because Handles operates in world space while the points are in the local space of the line. We have to explicitly convert the points into world space points.

	private void OnSceneGUI () {
		Line line = target as Line;
		Transform handleTransform = line.transform;
		Vector3 p0 = handleTransform.TransformPoint(line.p0);
		Vector3 p1 = handleTransform.TransformPoint(line.p1);

		Handles.color = Color.white;
		Handles.DrawLine(p0, p1);
	}
untransformed
Untransformed vs. transformed.

Besides showing the line, we can also show position handles for our two points. To do this, we also need our transform’s rotation so we can align them correctly.

	private void OnSceneGUI () {
		Line line = target as Line;
		Transform handleTransform = line.transform;
		Quaternion handleRotation = handleTransform.rotation;
		Vector3 p0 = handleTransform.TransformPoint(line.p0);
		Vector3 p1 = handleTransform.TransformPoint(line.p1);

		Handles.color = Color.white;
		Handles.DrawLine(p0, p1);
		Handles.DoPositionHandle(p0, handleRotation);
		Handles.DoPositionHandle(p1, handleRotation);
	}

Although we now get handles, they do not honor Unity’s pivot rotation mode. Fortunately, we can use Tools.pivotRotation to determine the current mode and set our rotation accordingly.

		Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;
local handles
Local vs. global pivot rotation.

To make the handles actually work, we need to assign their results back to the line. However, as the handle values are in world space we need to convert them back into the line’s local space with the InverseTransformPoint method. Also, we only need to do this when a point has changed. We can use EditorGUI.BeginChangeCheck and EditorGUI.EndChangeCheck for this. The second method tells us whether a change happened after calling the first method.

		EditorGUI.BeginChangeCheck();
		p0 = Handles.DoPositionHandle(p0, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			line.p0 = handleTransform.InverseTransformPoint(p0);
		}
		EditorGUI.BeginChangeCheck();
		p1 = Handles.DoPositionHandle(p1, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			line.p1 = handleTransform.InverseTransformPoint(p1);
		}

Now we can drag our points in the scene view!

There are two additional issues that need attention. First, we cannot undo the drag operations. This is fixed by adding a call to Undo.RecordObject before we make any changes. Second, Unity does not know that a change was made, so for example won’t ask the user to save when quitting. This is remedied with a call to EditorUtility.SetDirty.

		EditorGUI.BeginChangeCheck();
		p0 = Handles.DoPositionHandle(p0, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(line, "Move Point");
			EditorUtility.SetDirty(line);
			line.p0 = handleTransform.InverseTransformPoint(p0);
		}
		EditorGUI.BeginChangeCheck();
		p1 = Handles.DoPositionHandle(p1, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(line, "Move Point");
			EditorUtility.SetDirty(line);
			line.p1 = handleTransform.InverseTransformPoint(p1);
		}

Curves

It is time to upgrade to curves. A curve is like a line, but it doesn’t need to be straight. Specifically, we’ll create a Beziér curve.

A Beziér curve is defined by a sequence of points. It starts at the first point and ends at the last point, but does not need to go through the intermediate points. Instead, those points pull the curve away from being a straight line.

Create a new BezierCurve component and give it an array of points. Also give it a Reset method that initializes it with three points. This method also functions as a special Unity method, which is called by the editor when the component is created or reset.

using UnityEngine;

public class BezierCurve : MonoBehaviour {

	public Vector3[] points;

	public void Reset () {
		points = new Vector3[] {
			new Vector3(1f, 0f, 0f),
			new Vector3(2f, 0f, 0f),
			new Vector3(3f, 0f, 0f)
		};
	}
}

We also create an inspector for the curve, based on LineInspector. To reduce code repetition, we move the code that shows a point to a separate ShowPoint method that we can call with an index. We also turn curvehandleTransform, and handleRotation into class variables so we don’t need to pass then to ShowPoint.

While it is a new script, I’ve marked the differences as if we modified LineInspector.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(BezierCurve))]
public class BezierCurveInspector : Editor {

	private BezierCurve curve;
	private Transform handleTransform;
	private Quaternion handleRotation;

	private void OnSceneGUI () {
		curve = target as BezierCurve;
		handleTransform = curve.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;

		Vector3 p0 = ShowPoint(0);
		Vector3 p1 = ShowPoint(1);
		Vector3 p2 = ShowPoint(2);

		Handles.color = Color.white;
		Handles.DrawLine(p0, p1);
		Handles.DrawLine(p1, p2);
	}

	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(curve.points[index]);
		EditorGUI.BeginChangeCheck();
		point = Handles.DoPositionHandle(point, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(curve, "Move Point");
			EditorUtility.SetDirty(curve);
			curve.points[index] = handleTransform.InverseTransformPoint(point);
		}
		return point;
	}
}
project
A 3-point curve.

The idea of Beziér curves is that they are parametric. If you give it a value – typically named t – between zero and one, you get a point on the curve. As t increases from zero to one, you move from the first point of the curve to the last point.

To show our curve in the scene, we can approximate it by drawing straight lines between successive steps on the curve. We can do this with a simple loop, assuming our curve has a GetPoint method. We also keep drawing the straight lines between the points, but change their color to gray.

	private const int lineSteps = 10;
			
	private void OnSceneGUI () {
		curve = target as BezierCurve;
		handleTransform = curve.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;

		Vector3 p0 = ShowPoint(0);
		Vector3 p1 = ShowPoint(1);
		Vector3 p2 = ShowPoint(2);

		Handles.color = Color.gray;
		Handles.DrawLine(p0, p1);
		Handles.DrawLine(p1, p2);

		Handles.color = Color.white;
		Vector3 lineStart = curve.GetPoint(0f);
		for (int i = 1; i <= lineSteps; i++) {
			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
			Handles.DrawLine(lineStart, lineEnd);
			lineStart = lineEnd;
		}
	}

Now we have to add the GetPoint method to BezierCurve otherwise it won’t compile. Here we again make an assumption, this time that there’s a utility Beziér class that does the calculation for any sequence of points. We feed it our points and transform the result to world space.

	public Vector3 GetPoint (float t) {
		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], t));
	}

So we add a static Bezier class with the required method. For now, let’s ignore the middle point and simply linearly interpolate between the first and last point.

using UnityEngine;

public static class Bezier {

	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
		return Vector3.Lerp(p0, p2, t);
	}
}
bezier
Beziér library and linear interpolation.

Of course, linear interpolation between the end points totally ignores the middle point. So how do we incorporate the middle point? The answer is to interpolate more than once. First, linearly interpolate between the first and middle point, and also between the middle and last point. That gives us two new points. Linearly interpolating between those two gives us the final point on the curve.

	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
		return Vector3.Lerp(Vector3.Lerp(p0, p1, t), Vector3.Lerp(p1, p2, t), t);
	}
A quadratic Beziér curve.

This kind of curve is known as a quadratic Beziér curve, because of the polynomial math involved.

The linear curve can be written as B(t) = (1 – t) P0 + t P1.

One step deeper you get B(t) = (1 – t) ((1 – t) P0 + t P1) + t ((1 – t) P1 + t P2). This is really just the linear curve with P0 and P1 replaced by two new linear curves. It can also be rewritten into the more compact form B(t) = (1 – t)2 P0 + 2 (1 – tt P1 + t2 P2.

So we could use the quadratic formula instead of three calls to Vector3.Lerp.

	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
		t = Mathf.Clamp01(t);
		float oneMinusT = 1f - t;
		return
			oneMinusT * oneMinusT * p0 +
			2f * oneMinusT * t * p1 +
			t * t * p2;
	}

Now that we have a polynomial function, we can also describe its derivatives. The first derivative of our quadratic Beziér curve is B'(t) = 2 (1 – t) (P1 – P0) + 2 t (P2 – P1). Let’s add it.

	public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
		return
			2f * (1f - t) * (p1 - p0) +
			2f * t * (p2 - p1);
	}

This function produces lines tangent to the curve, which can be interpreted as the speed with which we move along the curve. So now we can add a GetVelocity method to BezierCurve.

Because it produces a velocity vector and not a point, it should not be affected by the position of the curve, so we subtract that after transforming.

	public Vector3 GetVelocity (float t) {
		return transform.TransformPoint(Bezier.GetFirstDerivative(points[0], points[1], points[2], t)) -
			transform.position;
	}

Now we can visualize the speed along the curve in BezierCurveInspector‘s OnSceneGUI method.

		Vector3 lineStart = curve.GetPoint(0f);
		Handles.color = Color.green;
		Handles.DrawLine(lineStart, lineStart + curve.GetVelocity(0f));
		for (int i = 1; i <= lineSteps; i++) {
			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
			Handles.color = Color.white;
			Handles.DrawLine(lineStart, lineEnd);
			Handles.color = Color.green;
			Handles.DrawLine(lineEnd, lineEnd + curve.GetVelocity(i / (float)lineSteps));
			lineStart = lineEnd;
		}
Showing velocity.

We can clearly see how the velocity changes along the curve, but those long lines are cluttering the view. Instead of showing the velocity, we can suffice with showing the direction of movement.

		Vector3 lineStart = curve.GetPoint(0f);
		Handles.color = Color.green;
		Handles.DrawLine(lineStart, lineStart + curve.GetDirection(0f));
		for (int i = 1; i <= lineSteps; i++) {
			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
			Handles.color = Color.white;
			Handles.DrawLine(lineStart, lineEnd);
			Handles.color = Color.green;
			Handles.DrawLine(lineEnd, lineEnd + curve.GetDirection(i / (float)lineSteps));
			lineStart = lineEnd;
		}

Which requires that we add GetDirection to BezierCurve, which simply normalizes the velocity.

	public Vector3 GetDirection (float t) {
		return GetVelocity(t).normalized;
	}
Showing direction.

Let’s go a step further and add new methods to Bezier for cubic curves as well! It works just like the quadratic version, except that it needs a fourth point and its formula goes another step deeper, resulting in a combination of six linear interpolations. The consolidated function of that becomes B(t) = (1 – t)3 P0 + 3 (1 – t)2 t P1 + 3 (1 – t) t2 P2 + t3 P3 which has as its first derivative B'(t) = 3 (1 – t)2 (P1 – P0) + 6 (1 – tt (P2 – P1) + 3 t2 (P3 – P2).

	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
		t = Mathf.Clamp01(t);
		float oneMinusT = 1f - t;
		return
			oneMinusT * oneMinusT * oneMinusT * p0 +
			3f * oneMinusT * oneMinusT * t * p1 +
			3f * oneMinusT * t * t * p2 +
			t * t * t * p3;
	}
	
	public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
		t = Mathf.Clamp01(t);
		float oneMinusT = 1f - t;
		return
			3f * oneMinusT * oneMinusT * (p1 - p0) +
			6f * oneMinusT * t * (p2 - p1) +
			3f * t * t * (p3 - p2);
	}

With that, we can upgrade BezierCurve from quadratic to cubic by taking an additional point into consideration. Be sure to add the fourth point to its array either manually or by resetting the component.

	public Vector3 GetPoint (float t) {
		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));
	}
	
	public Vector3 GetVelocity (float t) {
		return transform.TransformPoint(
			Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;
	}
	
	public void Reset () {
		points = new Vector3[] {
			new Vector3(1f, 0f, 0f),
			new Vector3(2f, 0f, 0f),
			new Vector3(3f, 0f, 0f),
			new Vector3(4f, 0f, 0f)
		};
	}

BezierCurveInspector now needs to be updated so it shows the fourth point as well.

		Vector3 p0 = ShowPoint(0);
		Vector3 p1 = ShowPoint(1);
		Vector3 p2 = ShowPoint(2);
		Vector3 p3 = ShowPoint(3);
		
		Handles.color = Color.gray;
		Handles.DrawLine(p0, p1);
		Handles.DrawLine(p2, p3);
A cubic Beziér curve.

It is probably visually obvious by now that we draw our curve using straight line segments. We could increase the number of steps to improve the visual quality. We could also use an iterative approach to get accurate down to pixel level. But we can also use Unity’s Handles.DrawBezier method, which takes care of drawing nice cubic Beziér curves for us.

Let’s also show the directions in their own method and scale them to take up less space.

	private const float directionScale = 0.5f;
	
	private void OnSceneGUI () {
		curve = target as BezierCurve;
		handleTransform = curve.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;
		
		Vector3 p0 = ShowPoint(0);
		Vector3 p1 = ShowPoint(1);
		Vector3 p2 = ShowPoint(2);
		Vector3 p3 = ShowPoint(3);
		
		Handles.color = Color.gray;
		Handles.DrawLine(p0, p1);
		Handles.DrawLine(p2, p3);
		
		ShowDirections();
		Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
	}

	private void ShowDirections () {
		Handles.color = Color.green;
		Vector3 point = curve.GetPoint(0f);
		Handles.DrawLine(point, point + curve.GetDirection(0f) * directionScale);
		for (int i = 1; i <= lineSteps; i++) {
			point = curve.GetPoint(i / (float)lineSteps);
			Handles.DrawLine(point, point + curve.GetDirection(i / (float)lineSteps) * directionScale);
		}
	}
Using Handles.DrawBezier and scaled direction lines.

Splines

Having a single curve is nice, but to create complex paths we would need to concatenate multiple curves. Such a construct is known as a spline. Let’s create one by copying the BezierCurve code, changing the type to BezierSpline.

using UnityEngine;

public class BezierSpline : MonoBehaviour {

	public Vector3[] points;
	
	public Vector3 GetPoint (float t) {
		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));
	}
	
	public Vector3 GetVelocity (float t) {
		return transform.TransformPoint(
			Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;
	}
	
	public Vector3 GetDirection (float t) {
		return GetVelocity(t).normalized;
	}
	
	public void Reset () {
		points = new Vector3[] {
			new Vector3(1f, 0f, 0f),
			new Vector3(2f, 0f, 0f),
			new Vector3(3f, 0f, 0f),
			new Vector3(4f, 0f, 0f)
		};
	}
}

We also create an editor for it, by copying and tweaking the code from BezierCurveInspector. We can then create a spline object and edit it, just like a curve.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(BezierSpline))]
public class BezierSplineInspector : Editor {

	private const int lineSteps = 10;
	private const float directionScale = 0.5f;

	private BezierSpline spline;
	private Transform handleTransform;
	private Quaternion handleRotation;

	private void OnSceneGUI () {
		spline = target as BezierSpline;
		handleTransform = spline.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;
		
		Vector3 p0 = ShowPoint(0);
		Vector3 p1 = ShowPoint(1);
		Vector3 p2 = ShowPoint(2);
		Vector3 p3 = ShowPoint(3);
		
		Handles.color = Color.gray;
		Handles.DrawLine(p0, p1);
		Handles.DrawLine(p2, p3);
		
		ShowDirections();
		Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
	}

	private void ShowDirections () {
		Handles.color = Color.green;
		Vector3 point = spline.GetPoint(0f);
		Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale);
		for (int i = 1; i <= lineSteps; i++) {
			point = spline.GetPoint(i / (float)lineSteps);
			Handles.DrawLine(point, point + spline.GetDirection(i / (float)lineSteps) * directionScale);
		}
	}

	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.points[index]);
		EditorGUI.BeginChangeCheck();
		point = Handles.DoPositionHandle(point, handleRotation);
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(spline, "Move Point");
			EditorUtility.SetDirty(spline);
			spline.points[index] = handleTransform.InverseTransformPoint(point);
		}
		return point;
	}
}
A new spline type.

Let’s add a method to BezierSpline to add another curve to the spline. Because we want the spline to be continuous, the last point of the previous curve is the same as the first point of the next curve. So each extra curve adds three more points.

	public void AddCurve () {
		Vector3 point = points[points.Length - 1];
		Array.Resize(ref points, points.Length + 3);
		point.x += 1f;
		points[points.Length - 3] = point;
		point.x += 1f;
		points[points.Length - 2] = point;
		point.x += 1f;
		points[points.Length - 1] = point;
	}

We’re using the Array.Resize method to create a larger array to hold the new points. It’s inside the System namespace, so we should declare that we’re using it at the top of our script.

using UnityEngine;
using System;

To actually be able to add a curve, we have to add a button to our spline’s inspector. We can customize the inspector that Unity uses for our component by overriding the OnInspectorGUI method of BezierSplineInspector. Note that this is not a special Unity method, it relies on inheritance.

To keep drawing the default inspector, we call the DrawDefaultInspector method. Then we use GUILayout to draw a button, which when clicked adds a curve.

	public override void OnInspectorGUI () {
		DrawDefaultInspector();
		spline = target as BezierSpline;
		if (GUILayout.Button("Add Curve")) {
			Undo.RecordObject(spline, "Add Curve");
			spline.AddCurve();
			EditorUtility.SetDirty(spline);
		}
	}
Adding a curve.

Of course we still only see the first curve. So we adjust BezierSplineInspector so it loops over all the curves.

	private void OnSceneGUI () {
		spline = target as BezierSpline;
		handleTransform = spline.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;
		
		Vector3 p0 = ShowPoint(0);
		for (int i = 1; i < spline.points.Length; i += 3) {
			Vector3 p1 = ShowPoint(i);
			Vector3 p2 = ShowPoint(i + 1);
			Vector3 p3 = ShowPoint(i + 2);
			
			Handles.color = Color.gray;
			Handles.DrawLine(p0, p1);
			Handles.DrawLine(p2, p3);
			
			Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
			p0 = p3;
		}
		ShowDirections();
	}
The whole spline.

Now we can see all the curves, but the direction lines are only added to the first one. This is because BezierSpline‘s method also still only work with the first curve. It’s time to change that.

To cover the entire spline with a t going from zero to one, we first need to figure out which curve we’re on. We can get the curve’s index by multiplying t by the number of curves and then discarding the fraction. Let’s add a CurveCount property to make that easy.

	public int CurveCount {
		get {
			return (points.Length - 1) / 3;
		}
	}

After that we can reduce t to just the fractional part to get the interpolation value for our curve. To get to the actual points, we have to multiply the curve index by three.

However, this would fail when then original t equals one. In this case we can just set it to the last curve.

	public Vector3 GetPoint (float t) {
		int i;
		if (t >= 1f) {
			t = 1f;
			i = points.Length - 4;
		}
		else {
			t = Mathf.Clamp01(t) * CurveCount;
			i = (int)t;
			t -= i;
			i *= 3;
		}
		return transform.TransformPoint(Bezier.GetPoint(
			points[i], points[i + 1], points[i + 2], points[i + 3], t));
	}
	
	public Vector3 GetVelocity (float t) {
		int i;
		if (t >= 1f) {
			t = 1f;
			i = points.Length - 4;
		}
		else {
			t = Mathf.Clamp01(t) * CurveCount;
			i = (int)t;
			t -= i;
			i *= 3;
		}
		return transform.TransformPoint(Bezier.GetFirstDerivative(
			points[i], points[i + 1], points[i + 2], points[i + 3], t)) - transform.position;
	}

We now see direction lines across the entire spline, but we can improve the visualization by making sure that each curve segment gets the same amount of lines. Fortunately, it is easy to change BezierSplineInspector.ShowDirections so it uses BezierSpline.CurveCount to determine how many lines to draw.

	private const int stepsPerCurve = 10;
	
	private void ShowDirections () {
		Handles.color = Color.green;
		Vector3 point = spline.GetPoint(0f);
		Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale);
		int steps = stepsPerCurve * spline.CurveCount;
		for (int i = 1; i <= steps; i++) {
			point = spline.GetPoint(i / (float)steps);
			Handles.DrawLine(point, point + spline.GetDirection(i / (float)steps) * directionScale);
		}
	}
Directions along the entire spline.

It’s rather crowded with all those transform handles. We could only show a handle for the active point. Then then other points can suffice with dots.

Let’s update ShowPoint so it shows a button instead of a position handle. This button will look like a white dot, which when clicked will turn into the active point. Then we only show the position handle if the point’s index matches the selected index, which we initialize at -1 so nothing is selected by default.

	private const float handleSize = 0.04f;
	private const float pickSize = 0.06f;
	
	private int selectedIndex = -1;
	
	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.points[index]);
		Handles.color = Color.white;
		if (Handles.Button(point, handleRotation, handleSize, pickSize, Handles.DotCap)) {
			selectedIndex = index;
		}
		if (selectedIndex == index) {
			EditorGUI.BeginChangeCheck();
			point = Handles.DoPositionHandle(point, handleRotation);
			if (EditorGUI.EndChangeCheck()) {
				Undo.RecordObject(spline, "Move Point");
				EditorUtility.SetDirty(spline);
				spline.points[index] = handleTransform.InverseTransformPoint(point);
			}
		}
		return point;
	}
Showing dots.

This works, but it is tough to get a good size for the dots. Depending on the scale you’re working at, they could end up either too large or too small. It would be nice if we could keep the screen size of the dots fixed, just like the position handles always have the same screen size. We can do this by factoring in HandleUtility.GetHandleSize. This method gives us a fixed screen size for any point in world space.

		float size = HandleUtility.GetHandleSize(point);
		Handles.color = Color.white;
		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
			selectedIndex = index;
		}
Dots with fixed size.

Constraining Control Points

Although our spline is continuous, it sharply changes direction in between curve sections. These sudden changes in direction and speed are possible because the shared control point between two curves has two different velocities associated with it, one for each curve.

If we want the velocities to be equal, we must ensure that the two control points that define them – the third of the previous curve and the second of the next curve – mirror each other around the shared point. This ensures that the combined first and second derivatives are continuous.

Alternatively, we could align them but let their distance from the shared point differ. That will result in an abrubt change in velocity, while still keeping the direction continuous. In this case the combined first derivative is continuous, but the second is not.

The most flexible approach is to decide per curve boundary which contraints should apply, so we’ll do that. Of course, once we have these constraints we can’t just let anyone directly edit BezierSpline‘s points. So let’s make our array private and provide indirect access to it. Make sure to let Unity know that we still want to serialize our points, otherwise they won’t be saved.

	[SerializeField]
	private Vector3[] points;

	public int ControlPointCount {
		get {
			return points.Length;
		}
	}

	public Vector3 GetControlPoint (int index) {
		return points[index];
	}

	public void SetControlPoint (int index, Vector3 point) {
		points[index] = point;
	}

Now BezierSplineInspector must use the new methods and property instead of directly accessing the points array.

	private void OnSceneGUI () {
		spline = target as BezierSpline;
		handleTransform = spline.transform;
		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
			handleTransform.rotation : Quaternion.identity;
		
		Vector3 p0 = ShowPoint(0);
		for (int i = 1; i < spline.ControlPointCount; i += 3) {
			Vector3 p1 = ShowPoint(i);
			Vector3 p2 = ShowPoint(i + 1);
			Vector3 p3 = ShowPoint(i + 2);
			
			Handles.color = Color.gray;
			Handles.DrawLine(p0, p1);
			Handles.DrawLine(p2, p3);
			
			Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
			p0 = p3;
		}
		ShowDirections();
	}
	
	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
		float size = HandleUtility.GetHandleSize(point);
		Handles.color = Color.white;
		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
			selectedIndex = index;
		}
		if (selectedIndex == index) {
			EditorGUI.BeginChangeCheck();
			point = Handles.DoPositionHandle(point, handleRotation);
			if (EditorGUI.EndChangeCheck()) {
				Undo.RecordObject(spline, "Move Point");
				EditorUtility.SetDirty(spline);
				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
			}
		}
		return point;
	}

While we’re at it, we also no longer want to allow direct access to the array in the inspector, so remove the call to DrawDefaultInspector. To still allow changes via typing, let’s show a vector field for the selected point.

	public override void OnInspectorGUI () {
		spline = target as BezierSpline;
		if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
			DrawSelectedPointInspector();
		}
		if (GUILayout.Button("Add Curve")) {
			Undo.RecordObject(spline, "Add Curve");
			spline.AddCurve();
			EditorUtility.SetDirty(spline);
		}
	}

	private void DrawSelectedPointInspector() {
		GUILayout.Label("Selected Point");
		EditorGUI.BeginChangeCheck();
		Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(spline, "Move Point");
			EditorUtility.SetDirty(spline);
			spline.SetControlPoint(selectedIndex, point);
		}
	}

Unfortunately, it turns out that the inspector doesn’t refresh itself when we select a point in the scene view. We could fix this by calling SetDirty for the spline, but that’s not right because the spline didn’t change. Fortunately, we can issue a repaint request instead.

	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
		float size = HandleUtility.GetHandleSize(point);
		Handles.color = Color.white;
		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
			selectedIndex = index;
			Repaint();
		}
		if (selectedIndex == index) {
			EditorGUI.BeginChangeCheck();
			point = Handles.DoPositionHandle(point, handleRotation);
			if (EditorGUI.EndChangeCheck()) {
				Undo.RecordObject(spline, "Move Point");
				EditorUtility.SetDirty(spline);
				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
			}
		}
		return point;
	}
Selected point only.

Let’s define an enumeration type to describe our three modes. Create a new script, remove the default code, and define an enum with the three options.

public enum BezierControlPointMode {
	Free,
	Aligned,
	Mirrored
}

Now we can add these modes to BezierSpline. We only need to store the mode in between curves, so let’s put them in an array with a length equal to the number of curves plus one. You’ll need to reset your spline or create a new one to make sure you have an array of the right size.

	[SerializeField]
	private BezierControlPointMode[] modes;
	
	public void AddCurve () {
		Vector3 point = points[points.Length - 1];
		Array.Resize(ref points, points.Length + 3);
		point.x += 1f;
		points[points.Length - 3] = point;
		point.x += 1f;
		points[points.Length - 2] = point;
		point.x += 1f;
		points[points.Length - 1] = point;

		Array.Resize(ref modes, modes.Length + 1);
		modes[modes.Length - 1] = modes[modes.Length - 2];
	}
	
	public void Reset () {
		points = new Vector3[] {
			new Vector3(1f, 0f, 0f),
			new Vector3(2f, 0f, 0f),
			new Vector3(3f, 0f, 0f),
			new Vector3(4f, 0f, 0f)
		};
		modes = new BezierControlPointMode[] {
			BezierControlPointMode.Free,
			BezierControlPointMode.Free
		};
	}

While we store the modes in between curves, it is convenient if we could get and set modes per control point. So we need to convert a point index into a mode index because in reality points share modes. As an example, the point index sequence 0, 1, 2, 3, 4, 5, 6 corresponds to the mode index sequence 0, 0, 1, 1, 1, 2, 2. So we need to add one and then divide by three.

	public BezierControlPointMode GetControlPointMode (int index) {
		return modes[(index + 1) / 3];
	}

	public void SetControlPointMode (int index, BezierControlPointMode mode) {
		modes[(index + 1) / 3] = mode;
	}

Now BezierSplineInspector can allow us to change the mode of the selected point. You will notice that changing the mode of one point also appears to change the mode of the points that are linked to it.

	private void DrawSelectedPointInspector() {
		GUILayout.Label("Selected Point");
		EditorGUI.BeginChangeCheck();
		Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(spline, "Move Point");
			EditorUtility.SetDirty(spline);
			spline.SetControlPoint(selectedIndex, point);
		}
		EditorGUI.BeginChangeCheck();
		BezierControlPointMode mode = (BezierControlPointMode)
			EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(spline, "Change Point Mode");
			spline.SetControlPointMode(selectedIndex, mode);
			EditorUtility.SetDirty(spline);
		}
	}
Now with adjustable control point mode.

It would be useful if we also got some visual feedback about our node types in the scene view. We can easily add this by coloring the dots. I’ll use white for free, yellow for aligned, and cyan for mirrored.

	private static Color[] modeColors = {
		Color.white,
		Color.yellow,
		Color.cyan
	};
	
	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
		float size = HandleUtility.GetHandleSize(point);
		Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
			selectedIndex = index;
			Repaint();
		}
		if (selectedIndex == index) {
			EditorGUI.BeginChangeCheck();
			point = Handles.DoPositionHandle(point, handleRotation);
			if (EditorGUI.EndChangeCheck()) {
				Undo.RecordObject(spline, "Move Point");
				EditorUtility.SetDirty(spline);
				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
			}
		}
		return point;
	}
Now with color-coded points.

So far we’re just coloring points. It’s time to enforce the constraints. We add a new method to BezierSpline to do so and call it when a point is moved or a mode is changed. It takes a point index and begins by retrieving the relevant mode.

	public void SetControlPoint (int index, Vector3 point) {
		points[index] = point;
		EnforceMode(index);
	}
	
	public void SetControlPointMode (int index, BezierControlPointMode mode) {
		modes[(index + 1) / 3] = mode;
		EnforceMode(index);
	}

	private void EnforceMode (int index) {
		int modeIndex = (index + 1) / 3;
	}

We should check if we actually don’t have to enforce anything. This is the case when the mode is set to free, or when we’re at the end points of the curve. In these cases, we can return without doing anything.

	private void EnforceMode (int index) {
		int modeIndex = (index + 1) / 3;
		BezierControlPointMode mode = modes[modeIndex];
		if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
			return;
		}
	}

Now which point should we adjust? When we change a point’s mode, it is either a point in between curves or one of its neighbors. When we have the middle point selected, we can just keep the previous point fixed and enforce the constraints on the point on the opposite side. If we have one of the other points selected, we should keep that one fixed and adjust its opposite. That way our selected point always stays where it is. So let’s define the indices for these points.

		if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
			return;
		}
		
		int middleIndex = modeIndex * 3;
		int fixedIndex, enforcedIndex;
		if (index <= middleIndex) {
			fixedIndex = middleIndex - 1;
			enforcedIndex = middleIndex + 1;
		}
		else {
			fixedIndex = middleIndex + 1;
			enforcedIndex = middleIndex - 1;
		}

Let’s consider the mirrored case first. To mirror around the middle point, we have to take the vector from the middle to the fixed point – which is (fixed – middle) – and invert it. This is the enforced tangent, and adding it to the middle gives us our enforced point.

		if (index <= middleIndex) {
			fixedIndex = middleIndex - 1;
			enforcedIndex = middleIndex + 1;
		}
		else {
			fixedIndex = middleIndex + 1;
			enforcedIndex = middleIndex - 1;
		}

		Vector3 middle = points[middleIndex];
		Vector3 enforcedTangent = middle - points[fixedIndex];
		points[enforcedIndex] = middle + enforcedTangent;

For the aligned mode, we also have to make sure that the new tangent has the same length as the old one. So we normalize it and then multiply by the distance between the middle and the old enforced point.

		Vector3 enforcedTangent = middle - points[fixedIndex];
		if (mode == BezierControlPointMode.Aligned) {
			enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
		}
		points[enforcedIndex] = middle + enforcedTangent;
Enforced constraints.

From now on, whenever you move a point or change a point’s mode, the constraints will be enforced. But when moving a middle point, the previous point always stays fixed and the next point is always enforced. This might be fine, but it’s intuitive if both other points move along with the middle one. So let’s adjust SetControlPoint so it moves them together.

	public void SetControlPoint (int index, Vector3 point) {
		if (index % 3 == 0) {
			Vector3 delta = point - points[index];
			if (index > 0) {
				points[index - 1] += delta;
			}
			if (index + 1 < points.Length) {
				points[index + 1] += delta;
			}
		}
		points[index] = point;
		EnforceMode(index);
	}

To wrap things up, we should also make sure that the constraints are enforced when we add a curve. We can do this by simply calling EnforceMode at the point where the new curve was added.

	public void AddCurve () {
		Vector3 point = points[points.Length - 1];
		Array.Resize(ref points, points.Length + 3);
		point.x += 1f;
		points[points.Length - 3] = point;
		point.x += 1f;
		points[points.Length - 2] = point;
		point.x += 1f;
		points[points.Length - 1] = point;

		Array.Resize(ref modes, modes.Length + 1);
		modes[modes.Length - 1] = modes[modes.Length - 2];
		EnforceMode(points.Length - 4);
	}

There is yet another constraint that we could add. By enforcing that the first and last control points share the same position, we can turn our spline into a loop. Of course, we also have to take modes into consideration as well.

So let’s add a loop property to BezierSpline. Whenever it is set to true, we make sure the modes of the end points match and we call SetPosition, trusting that it will take care of the position and mode constraints.

	[SerializeField]
	private bool loop;

	public bool Loop {
		get {
			return loop;
		}
		set {
			loop = value;
			if (value == true) {
				modes[modes.Length - 1] = modes[0];
				SetControlPoint(0, points[0]);
			}
		}
	}

Now we can add the loop property to BezierSplineInspector.

	public override void OnInspectorGUI () {
		spline = target as BezierSpline;
		EditorGUI.BeginChangeCheck();
		bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);
		if (EditorGUI.EndChangeCheck()) {
			Undo.RecordObject(spline, "Toggle Loop");
			EditorUtility.SetDirty(spline);
			spline.Loop = loop;
		}
		if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
			DrawSelectedPointInspector();
		}
		if (GUILayout.Button("Add Curve")) {
			Undo.RecordObject(spline, "Add Curve");
			spline.AddCurve();
			EditorUtility.SetDirty(spline);
		}
	}
Optional loop.

To correctly enforce the loop, we need to make a few more changes to BezierSpline.

First, SetControlPointMode needs to make sure that the first and last mode remain equal in case of a loop.

	public void SetControlPointMode (int index, BezierControlPointMode mode) {
		int modeIndex = (index + 1) / 3;
		modes[modeIndex] = mode;
		if (loop) {
			if (modeIndex == 0) {
				modes[modes.Length - 1] = mode;
			}
			else if (modeIndex == modes.Length - 1) {
				modes[0] = mode;
			}
		}
		EnforceMode(index);
	}

Next, SetControlPoint needs different edge cases when dealing with a loop, because it needs to wrap around the points array.

	public void SetControlPoint (int index, Vector3 point) {
		if (index % 3 == 0) {
			Vector3 delta = point - points[index];
			if (loop) {
				if (index == 0) {
					points[1] += delta;
					points[points.Length - 2] += delta;
					points[points.Length - 1] = point;
				}
				else if (index == points.Length - 1) {
					points[0] = point;
					points[1] += delta;
					points[index - 1] += delta;
				}
				else {
					points[index - 1] += delta;
					points[index + 1] += delta;
				}
			}
			else {
				if (index > 0) {
					points[index - 1] += delta;
				}
				if (index + 1 < points.Length) {
					points[index + 1] += delta;
				}
			}
		}
		points[index] = point;
		EnforceMode(index);
	}

Next, EnforceMode can now only bail at the end points when not looping. It also has to check whether the fixed or enforced point wraps around the array.

	private void EnforceMode (int index) {
		int modeIndex = (index + 1) / 3;
		BezierControlPointMode mode = modes[modeIndex];
		if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) {
			return;
		}

		int middleIndex = modeIndex * 3;
		int fixedIndex, enforcedIndex;
		if (index <= middleIndex) {
			fixedIndex = middleIndex - 1;
			if (fixedIndex < 0) {
				fixedIndex = points.Length - 2;
			}
			enforcedIndex = middleIndex + 1;
			if (enforcedIndex >= points.Length) {
				enforcedIndex = 1;
			}
		}
		else {
			fixedIndex = middleIndex + 1;
			if (fixedIndex >= points.Length) {
				fixedIndex = 1;
			}
			enforcedIndex = middleIndex - 1;
			if (enforcedIndex < 0) {
				enforcedIndex = points.Length - 2;
			}
		}

		Vector3 middle = points[middleIndex];
		Vector3 enforcedTangent = middle - points[fixedIndex];
		if (mode == BezierControlPointMode.Aligned) {
			enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
		}
		points[enforcedIndex] = middle + enforcedTangent;
	}

And finally, we also have to take looping into account when adding a curve to the spline. The result might be a tangle, but it will remain a proper loop.

	public void AddCurve () {
		Vector3 point = points[points.Length - 1];
		Array.Resize(ref points, points.Length + 3);
		point.x += 1f;
		points[points.Length - 3] = point;
		point.x += 1f;
		points[points.Length - 2] = point;
		point.x += 1f;
		points[points.Length - 1] = point;

		Array.Resize(ref modes, modes.Length + 1);
		modes[modes.Length - 1] = modes[modes.Length - 2];
		EnforceMode(points.Length - 4);

		if (loop) {
			points[points.Length - 1] = points[0];
			modes[modes.Length - 1] = modes[0];
			EnforceMode(0);
		}
	}
A spline loop.

It is great that we have loops, but it is inconvenient that we can no longer see where the spline begins. We can make this obvious by letting BezierSplineInspector always double the size of the dot for the first point.

Note that in case of a loop the last point will be drawn on top of it, so if you clicked the middle of the big dot you’d select the last point, while if you clicked further from the center you’d get the first point.

	private Vector3 ShowPoint (int index) {
		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
		float size = HandleUtility.GetHandleSize(point);
		if (index == 0) {
			size *= 2f;
		}
		Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
			selectedIndex = index;
			Repaint();
		}
		if (selectedIndex == index) {
			EditorGUI.BeginChangeCheck();
			point = Handles.DoPositionHandle(point, handleRotation);
			if (EditorGUI.EndChangeCheck()) {
				Undo.RecordObject(spline, "Move Point");
				EditorUtility.SetDirty(spline);
				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
			}
		}
		return point;
	}
We start big.

Using Splines

We have been working with splines for a while now, but we haven’t used them for anything yet. There are uncountable things you can do with splines, for example moving an object alongs its path. Let’s create a SplineWalker component that does just that.

using UnityEngine;

public class SplineWalker : MonoBehaviour {

	public BezierSpline spline;

	public float duration;

	private float progress;

	private void Update () {
		progress += Time.deltaTime / duration;
		if (progress > 1f) {
			progress = 1f;
		}
		transform.localPosition = spline.GetPoint(progress);
	}
}

Now we can create a walker object, assign our spline, set a duration, and watch it move after we enter play mode. I simply used a cube and gave it smaller cubes to resemble eyes, so you can see in what direction it’s looking.

walker
Walking the spline.

The walker now walks, but it’s not looking in the direction that it’s going. We can add an option for that.

	public bool lookForward;

	private void Update () {
		progress += Time.deltaTime / duration;
		if (progress > 1f) {
			progress = 1f;
		}
		Vector3 position = spline.GetPoint(progress);
		transform.localPosition = position;
		if (lookForward) {
			transform.LookAt(position + spline.GetDirection(progress));
		}
	}
looking forward
Looking where you go.

Another option is to keep looping the splines, instead of walking it just once. While we’re at it, we could also make the walker move back and forth, ping-ponging across the spline. Let’s create an enumeration to select between these modes.

public enum SplineWalkerMode {
	Once,
	Loop,
	PingPong
}

Now SplineWalker has to remember whether it’s going forward or backward. It also needs to adjust the progress when passing the spline ends depending on its mode.

	public SplineWalkerMode mode;

	private bool goingForward = true;

	private void Update () {
		if (goingForward) {
			progress += Time.deltaTime / duration;
			if (progress > 1f) {
				if (mode == SplineWalkerMode.Once) {
					progress = 1f;
				}
				else if (mode == SplineWalkerMode.Loop) {
					progress -= 1f;
				}
				else {
					progress = 2f - progress;
					goingForward = false;
				}
			}
		}
		else {
			progress -= Time.deltaTime / duration;
			if (progress < 0f) {
				progress = -progress;
				goingForward = true;
			}
		}

		Vector3 position = spline.GetPoint(progress);
		transform.localPosition = position;
		if (lookForward) {
			transform.LookAt(position + spline.GetDirection(progress));
		}
	}
Walking in different ways.

Another thing we could do is create a decorator that instantiates a sequence of items along a spline when it awakens. We also give it a forward-looking option, which applies to the items it spawns. Adding a frequency option to the item sequence allows for repetition. Of course, if either the frequency is zero or there are no items, we do nothing.

We need some items, so create a few prefabs for that purpose as well.

using UnityEngine;

public class SplineDecorator : MonoBehaviour {

	public BezierSpline spline;

	public int frequency;

	public bool lookForward;

	public Transform[] items;

	private void Awake () {
		if (frequency <= 0 || items == null || items.Length == 0) {
			return;
		}
		float stepSize = 1f / (frequency * items.Length);
		for (int p = 0, f = 0; f < frequency; f++) {
			for (int i = 0; i < items.Length; i++, p++) {
				Transform item = Instantiate(items[i]) as Transform;
				Vector3 position = spline.GetPoint(p * stepSize);
				item.transform.localPosition = position;
				if (lookForward) {
					item.transform.LookAt(position + spline.GetDirection(p * stepSize));
				}
				item.transform.parent = transform;
			}
		}
	}
}
decorator and prefabs
Decorating splines.

This works well for loops, but it doesn’t go all the way to the end of splines that aren’t loops. We can fix this by increasing our step size to cover the entire length of the spline, as long as it’s not a loop and we have more than one item to place.

		if (frequency <= 0 || items == null || items.Length == 0) {
			return;
		}
		float stepSize = frequency * items.Length;
		if (spline.Loop || stepSize == 1) {
			stepSize = 1f / stepSize;
		}
		else {
			stepSize = 1f / (stepSize - 1);
		}
Going all the way.

There are many more ways to use splines, and there’s also more features to add to the splines themselves. Like removing curves, or splitting a curve into two smaller ones, or merging two curves together. There are also other spline types to explore, like Centripetal Catmull-Rom or NURB. If you’re comfortable with Beziér, you should be able to handle those as well. So the tutorial ends here, enjoy walking your own path!

Enjoyed the tutorial? Help me make more by becoming a patron!

Downloads

curves-and-splines-01.unitypackageThe project after Lines.curves-and-splines-02.unitypackageThe project after Curves.curves-and-splines-03.unitypackageThe project after Splines.curves-and-splines-04.unitypackageThe project after Constraining Control Points.curves-and-splines-finished.unitypackageThe finished project.

유니티에서 사용되는 코루틴(Coroutine)은 왜 필요한가?

unity_logo

유니티에서 화면의 변화를 일으키기 위해서는 Update() 함수 내에서 작업을 하게 됩니다. 이 Update() 함수는 매 프레임을 그릴때마다 호출되며 60fps의경우라면 초당 60번의 Update() 함수 호출이 발생하게 됩니다. 하나의 프레임 안에서 어떤 작업을 한다면 이 Update() 함수에 코드를 작성하면 될 것입니다.

하지만 다수의 프레임을 오가며 어떤 작업을 수행해야 한다면 어떻게 해야 할까요? 혹은 특정 시간, 가령 3초 동안 특정 작업을 수행해야 한다면 어떻게 해야 할까요? 3초니깐 3 x 60 = 180 프레임동안 작업을 수행하도록 하면 될까요?

안타깝게도 기기의 성능이나 상황에 따라 프레임 드랍(Frame drop)이라는 상황이 발생하게 됩니다. 60fps의 게임일지라 하더라도 디바이스의 성능에 따라 그 이하로 떨어질 수 있다는 의미가 됩니다. 이렇게 되면 더더욱 3초 동안 작업을 수행한다는게 쉽지 않은 일이 됩니다.

예로 다음의 코드를 준비하였습니다. 특정 오브젝트를 페이드 아웃(Fade out) 시키는 예제 코드입니다. 이 코드를 수행하면 스프라이트의 알파값이 점점 작아져서 결국 화면에서 사라지게 됩니다.

public class FadeOut : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    void Start ()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }
    void Update ()
    {
        Color color = spriteRenderer.color;

        if ( color.a > 0.0f )
        {
            color.a -= 0.1f; spriteRenderer.color = color;
        }
    }
}

위의 코드를 보면 매 프레임마다 스프라이트 렌더러의 알파값을 0.1씩 감소시키고 있습니다. Update() 함수가 10번 호출되면 사라지게 되겠네요. 이는 1/6 초만에 사라지게 된다는것을 의미합니다. 이것도 1/6초만에 사라질지 보장받기가 어렵습니다.

그럼 혹시, 1초에 걸쳐 (60fps가 정상적으로 보장될 경우 60 프레임에 걸쳐) 사라지게 하려면 어떻게 하면 될까요? 대충 알파값을 0.017씩 감소시키면 될까요? 프레임이 아닌 시간 단위로 특정 작업을 수행할 수 있을까요? 여기서 생각할 수 있는 수단은 Time.deltaTime 이 있습니다.

하지만 우리가 여기서 알아보고자 하는것은 델타 타임이 아닌 코루틴이므로 코루틴에 대해서 알아보도록 하겠습니다. 코루틴은 프레임과 상관없이 별도의 서브 루틴에서 원하는 작업을 원하는 시간만큼 수행하는 것이 가능합니다.

다음은 코루틴을 사용하여 1초동안 페이드 아웃을 진행하는 예제 코드입니다.

public class FadeOut : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    void Start ()
    {
        spriteRenderer = GetComponent<SpriteRenderer>(); 
        StartCoroutine( "RunFadeOut" );
    }

    IEnumerator RunFadeOut ()
    {
        Color color = spriteRenderer.color;
        while ( color.a > 0.0f )
        {
            color.a -= 0.1f;
            spriteRenderer.color = color;
            yield return new WaitForSeconds( 0.1f );
        }
    }
}

이전 코드에서는 Update() 에서 모든 작업을 처리하던것을 Start() 에서 RunFadeOut() 코루틴을 실행하는것으로 변경된 것을 볼 수 있습니다. 여기서 주목해야 하는 부분은 yield return new WaitForSeconds(0.1f); 부분입니다.

이 복잡해 보이는 코드는 0.1초 동안 잠시 멈추라는 의미를 가진 코드입니다. 이제 위의 코드를 통해서 페이드 아웃 효과는 0.1초씩 10번을 수행하며 1초동안 사라지는 모습을 보여주게 됩니다. 이 코루틴은 Update() 함수에 종속적이지 않으며 마치 별도의 쓰레드와 같이 동작을 하게 됩니다. 이와 같은 코드로 프레임율에 영향을 받지 않는 시간 기반의 서브루틴을 구동할 수 있게 되었습니다.

IEnumerator와 yield는 무엇이며 어떤 관계가 있는가?

그렇다면 여기서 궁금증을 유발하는 부분이 몇가지 있는데요 RunFadeOut의 리턴 타입은 IEnumerator(열거자) 입니다. 또한 while 문 내부에 보면 yield(양보)라는 구문이 보이는군요. 그 뒤로 return이 따라나오는 것도 일반적인 언어에서 보기 힘든 문법입니다. 이것들이 어떤 관계를 가지고 있는지 알아보겠습니다.

우선 다음의 일반적인 C# 코드를 한번 살펴보도록 하겠습니다.

void Main ()
{
    IEnumerator enumerator = SomeNumbers();
    while ( enumerator.MoveNext() )
    {
        object result = enumerator.Current;
        Debug.Log( "Number: " + result );
    }
}

IEnumerator SomeNumbers ()
{
    yield return 3;
    yield return 5;
    yield return 8;
}

위의 Main() 함수를 실행하게 되면 다음과 같은 결과물이 출력됩니다.

Number: 3
Number: 5
Number: 8

조금 헷갈리지만 알고보면 어렵지 않은 코드입니다. 이 코드는 다음과 같은 순서로 동작하게 됩니다.

  1. SomeNumbers() 함수를 실행한 결과를 IEnumerator 열거자로 받습니다. 정확히는 실행된 결과가 아닙니다. enumerator 에 함수의 실행결과가 할당 되었다고 생각될만한 코드지만 여기서는 SomeNumbers() 함수는 한줄도 실행되지 않은 상태입니다. 함수의 포인터를 받았다고 생각하시는게 이해하시기 편할 것 같습니다.
  2. while 문을 만나면서 처음으로 enumerator의 MoveNext()가 호출됩니다. 여기서 SomeNumbers()가 실행이 되며 딱 yield 문을 만날때까지 실행이 됩니다.
  3. 첫번째 yield 문인 yield return 3; 을 만납니다. 여기서는 표현식 그대로 return 3에게 양보한다는 느낌으로 코드를 읽으시면 될 것 같습니다. 우선 여기까지 오면 3을 리턴하는것에 양보가 일어납니다. 이때에 리턴되는 값이 존재하므로 MoveNext()의 결과값으로 true가 반환됩니다.
  4. 이제 enumerator의 Current를 통해서 현재 반환된 값을 꺼내올 수 있습니다. MoveNext()를 통해서 yield return 되는 값이 있는지를 알 수 있고 반환된 값이 있다면 Current에 담겨 있게 됩니다.
  5. Debug.Log를 사용하여 Current를 출력해보면 처음으로 양보 반환된 3이 출력되게 됩니다.
  6. 다시한번 while문이 실행되며 MoveNext()가 호출되면 정말 재미있게도 가장 마지막에 yield return이 일어났던 위치의 다음줄부터 재실행이 되게 됩니다. 다시한번 yield 문을 만날때까지 진행이 됩니다.
  7. 이번에는 두번째 yield문인 yield return 5를 만나게 됩니다. 결과적으로 MoveNext() 의 결과값은 true가 되게 됩니다.
  8. 현재 Current에 할당된 값은 MoveNext()의 양보 반환된 값인 5가 될 것입니다.
  9. Debug.Log를 통해 값을 출력해보면 5가 출력됩니다.
  10. 다시한번 while문의 MoveNext()를 호출하면 yield return 5; 다음줄부터 재시작이 되게 되면 yield return 8;까지 진행이 되게 됩니다.
  11. 8이 양보 반환되었으므로 MoveNext()의 값은 true가 되며 Current에는 8이 들어가있게 됩니다.
  12. Debug.Log로 8이 출력됩니다.
  13. 다시한번 MoveNext() 가 호출되며 yield return 8; 이후의 코드부터 실행이 되지만 함수의 끝을 만나게 되므로 더이상 yield가 일어나지 않습니다.
  14. MoveNext() 의 결과 값으로 false가 반환되며 while 문이 종료됩니다.

조금 특이하지만 함수의 반환값이 IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T> 인 경우에는 위와 같은 동작을 하게 됩니다. 함수의 동작이 비동기적으로 동작하게 되므로 파라미터에 ref나 out을 사용할 수 없다는 제약 사항이 있습니다. 위의 코드 동작 예시는 코루틴이 어떻게 동작하는지 알기위한 기본적인 코드라고 생각됩니다. 이제 다시 코루틴으로 돌아가 보겠습니다.

void Start ()
{
    //StartCoroutine ("RunCoroutine")
    IEnumerator runCoroutine = RunCoroutine();
    while ( runCoroutine.MoveNext() )
    {
        object result = runCoroutine.Current;

        if ( result is WaitForSeconds )
        {
            // 원하는 초만큼 기다리는 로직 수행
            // 여기 예제에서는 1초만큼 기다리게 될 것임을 알 수 있음
        }
        else if ...

    }
}

IEnumerator RunCoroutine ()
{
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
}

StartCoroutine을 직접 구현해 본다면 위와 같은 형태의 코드가 될 것 같습니다. 먼저 코루틴 함수의 포인터 역할을 하는 열거자를 받은 다음에 MoveNext()를 통해 첫번째 yield를 만날때까지 수행하고 그 결과값을 받습니다. 그리고 그 결과값에 맞는 작업을 수행해줍니다. 그리고 이것을 함수가 완료될 때까지 반복합니다.

위의 코드에서는 4번의 MoveNext()가 호출될 것이며 3번의 yield문을 만날 것입니다. 마지막 MoveNext()에서는 false가 반환될 것이므로 코루틴이 종료됩니다. 만약 함수의 실행이 완료되기 이전에 임의로 코루틴을 종료시키고 싶다면 yield break를 호출하면 됩니다. 즉시 MoveNext()에서 false가 반환되어 종료됩니다.

결론적으로 StartCoroutine은 IEnumerator를 반환하는 함수를 인자로 받으며 이 함수는 특이하게도 실행된 결과를 의미하는것이 아니라 함수 포인터와 같은 개념으로 사용이 됩니다.

void Start ()
{
    StartCoroutine( RunCoroutine() );
}

IEnumerator RunCoroutine ()
{
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
    yield return new WaitForSeconds( 1.0f );
}

이 코드를 한번 봐보겠습니다. 일반적인 함수의 개념으로 보자면 StartCoroutine에는 RunCoroutine() 함수의 결과값이 파라미터로 넘겨지게 되어있습니다. 하지만 여기서 RunCoroutine()은 단 한줄도 실행이 되지 않습니다. 함수의 포인터 역할을 하는 IEnumerator가 넘겨지게 되고 MoveNext()를 호출할 때마다 yield 문을 만날때까지 수행됩니다. 만나게 되면 MoveNext()가 true를 반환하고 함수가 끝나거나 yield break; 를 만나게 되면 false를 반환하게 됩니다. true를 반환할 경우 Current를 통해 반환된 값을 꺼내볼 수 있습니다.

StartCoroutine을 수행할 때 사용할 수 있는 두가지 방법

public Coroutine StartCoroutine(IEnumerator routine);

일반적으로 사용할 수 있는 방법입니다. 수행하고자 하는 코루틴의 IEnumerator 열거자를 넘겨서 실행되도록 합니다. 다음과 같은 방법으로 사용이 가능합니다.

void Start ()
{
    StartCoroutine( WaitAndPrint( 2.0F ) );
}
IEnumerator WaitAndPrint ( float waitTime )
{
    yield return new WaitForSeconds( waitTime );
    Debug.Log( "Done. " + Time.time );
}

위와 같은 방법은 일반적인 방법으로 waitTime 파라미터 값을 넘길 수 있으며 코루틴이 실행되는데에 추가적인 오버헤드가 전혀 없는 방법입니다. 뿐만 아니라 Start() 함수의 반환값을 IEnumerator로 변경하여 아예 코루틴이 실행 완료될때까지 기다리도록 의존적인 방법으로 실행하는 것도 가능합니다.

IEnumerator Start()
{
    yield return StartCoroutine( WaitAndPrint( 2.0F ) );
    Debug.Log( "Done " + Time.time );
}
IEnumerator WaitAndPrint ( float waitTime )
{
    yield return new WaitForSeconds( waitTime );
    Debug.Log( "WaitAndPrint " + Time.time );
}

위의 코드는 WaitAndPrint(waitTime) 코루틴이 실행 완료된 이후에야 Done이 출력되는 과정을 보여줍니다.

public Coroutine StartCoroutine(string methodName, object value = null);

대부분의 경우는 StartCoroutine을 사용하기 위해 전자의 방법을 사용합니다. 하지만 StartCoroutine을 문자열 형태의 코루틴 함수 이름으로도 호출하는 것이 가능합니다. 이렇게 호출하면 StopCoroutine 역시 함수 이름만으로 호출하는것이 가능해 집니다.

IEnumerator Start ()
{
    StartCoroutine( "DoSomething", 2.0F );
    yield return new WaitForSeconds( 1 );
    StopCoroutine( "DoSomething" );
}
IEnumerator DoSomething ( float someParameter )
{
    while ( true )
    {
        Debug.Log( "DoSomething Loop" );
        yield return null;
    }
}

위의 코드는 DoSomething(someParameter) 코루틴 함수를 함수 이름과 넘겨질 파라미터를 통해 호출하는 과정을 보여주고 있습니다. 그리고 1초 기다린 뒤에 실행했었던 DoSomething 코루틴을 종료시킵니다. 이러한 함수 이름을 문자열로 넘겨 실행하는 방법은 StartCoroutine을 수행하는데에 오버헤드가 크고 파라미터를 한개밖에 넘길 수 없다는 제약사항이 있습니다. 물론 배열을 넘기는것 역시 가능합니다.

object[] parms = new object[2] { floatParameter, stringParameter };
StartCoroutine ("MyCoroutine", parms);

yield return에서 사용할 수 있는 것들

위에서 본 예시에는 WaitForSeconds 클래스를 양보 반환함으로써 원하는 시간(초)만큼 기다리는 것이 가능하다는것을 알 수 있었습니다. 추가로 더 알아 보도록 하겠습니다.

yield return new WaitForSecondsRealtime (float time);

WaitForSeconds와 하는 역할은 동일하지만 결정적으로 다른것이 있습니다. 유니티상의 시간은 임의로 느리게 하거나 빠르게 하는 것이 가능합니다. 이를 Time.timeScale을 통해서 조정을 할 수 있습니다. 매트릭스에서 보던 총알이 느리게 날아오면서 그것을 피하는 모션을 구현해 본다면 이 값을 1보다 낮추게 되면 현재 시간의 진행보다 느려지게 되며 1보다 빠르게 변경하면 현재의 시간의 진행보다 빨라지게 됩니다. 하지만 WaitForSecondsRealtime는 이러한 Scaled Time의 영향을 받지 않고 현실 시간 기준으로만 동작을 하게 됩니다.

yield return new WaitForFixedUpdate ();

다음 FixedUpdate() 가 실행될때까지 기다리게 됩니다. 이 FixedUpdate()는 Update()와 달리 일정한 시간 단위로 호출되는 Update() 함수라고 생각하시면 됩니다.

yield return new WaitForEndOfFrame ();

하나의 프레임워 완전히 종료될 때 호출이 됩니다. Update(), LateUpdate() 이벤트가 모두 실행되고 화면에 렌더링이 끝난 이후에 호출이 됩니다. 특수한 경우에 사용하면 될 것 같습니다만 잘 모르겠군요.

yield return null;

WaitForEndOfFrame를 이야기 했다면 이것을 꼭 이야기 해야 할 것 같습니다. yield return null; 을 하게 되면 다음 Update() 가 실행될때까지 기다린다는 의미를 갖게 됩니다. 좀 더 정확하게는 Update()가 먼저 실행되고 null을 양보 반환했던 코루틴이 이어서 진행 됩니다. 그 다음에 LateUpdate()가 호출됩니다.

yield return new WaitUntil (System.Func<Bool> predicate);

이번엔 특정 조건식이 성공할때까지 기다리는 방법입니다. WaitUntil에 실행하고자 하는 식을 정의해 두면 매번 Update() 와 LateUpdate() 이벤트 사이에 호출해 보고 결과값이 true면 이후로 재진행을 하게 됩니다. 다음의 예제 코드를 보겠습니다.

public class WaitUntilExample : MonoBehaviour
{
    public int frame = 0;

    void Start ()
    {
        StartCoroutine( Example() );
    }

    IEnumerator Example ()
    {
        Debug.Log( "공주를 구출하기 위해 기다리는 중..." );
        yield return new WaitUntil( () => frame >= 10 );
        Debug.Log( "공주를 구출했다!" );
    }

    void Update ()
    {
        if ( frame <= 10 )
        {
            Debug.Log( "Frame: " + frame );
            frame++;
        }
    }
}

이 코드는 Update() 함수를 통해 매 프레임마다 frame 멤버 변수값을 1씩 최대 10까지 증가시키게 됩니다. 실행중인 코루틴은 frame값이 10또는 10보다 커질때까지 기다리다가 이 식이 충족되게 되면 다음으로 진행을 하게 됩니다. 여기서 사용되는 식은 람다 표기법이 사용됩니다. 다음과 같은 느낌이라고 생각하시면 될 것 같습니다.

Func<int, int> func1 = ( int x ) => x + 1;
Func<int, int> func2 = ( int x ) => { return x + 1; };

yield return new WaitWhile(System.Func<Bool> predicate);

WaitWhile은 WaitUntil과 동일한 목적을 가지고 있지만 한가지만 다릅니다. WaitUntil은 람다식 실행 결과값이 true가 될때까지 기다린다면 WaitWhile은 false가 될때까지 기다립니다. 즉 WaitWhile은 결과가 true인 동안 계속 기다리게 됩니다.

public class WaitWhileExample : MonoBehaviour
{
    public int frame = 0;

    void Start ()
    {
        StartCoroutine( Example() );
    }

    IEnumerator Example ()
    {
        Debug.Log( "공주를 구출하기 위해 기다리는 중..." );
        yield return new WaitWhile( () => frame < 10 );
        Debug.Log( "공주를 구출했다!" );
    }

    void Update ()
    {
        if ( frame <= 10 )
        {
            Debug.Log( "Frame: " + frame );
            frame++;
        }
    }
}

위의 코드는 첫프레임부터 람다식의 결과가 true이게 됩니다. 10프레임에 도달하면 false가 되어서 이후 진행이 되겠네요.

yield return StartCoroutine (IEnumerator coroutine);

이번에는 심지어 코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게 됩니다. 의존성 있는 여러작업을 수행하는데에 유리하게 사용 될 수 있습니다.

void Start ()
{
    StartCoroutine( TestRoutine() );
}

IEnumerator TestRoutine ()
{
    Debug.Log( "Run TestRoutine" );
    yield return StartCoroutine( OtherRoutine() );
    Debug.Log( "Finish TestRoutine" );
}

IEnumerator OtherRoutine ()
{
    Debug.Log( "Run OtherRoutine #1" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Run OtherRoutine #2" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Run OtherRoutine #3" );
    yield return new WaitForSeconds( 1.0f );
    Debug.Log( "Finish OtherRoutine" );
}

위와 같은 코드를 실행해 본다면 결과는 다음과 같이 출력됩니다.

Run TestRoutine
Run OtherRoutine #1
Run OtherRoutine #2
Run OtherRoutine #3
Finish OtherRoutine
Finish TestRoutine

Coroutine 중단하기

public void StopCoroutine(IEnumerator routine);

이 방법은 기존에 StartCoroutine을 실행할 때 넘겨주었던 코루틴 함수의 열거자를 파라미터로 사용하여 그것을 중단시키는 방법입니다. 다음과 같은 사용이 가능합니다.

public class Example : MonoBehaviour
{

    private IEnumerator coroutine;

    void Start ()
    {
        coroutine = WaitAndPrint( 1.0f );
        StartCoroutine( coroutine );
    }

    public IEnumerator WaitAndPrint ( float waitTime )
    {
        while ( true )
        {
            yield return new WaitForSeconds( waitTime );
            Debug.Log( "WaitAndPrint " + Time.time );
        }
    }

    void Update ()
    {
        if ( Input.GetKeyDown( "space" ) )
        {
            StopCoroutine( coroutine );
            Debug.Log( "Stopped " + Time.time );
        }
    }
}

Start() 함수에서 WaitAndPrint(waitTime) 코루틴 함수의 열거자를 획득하여 클래스의 멤버 변수로 설정해 두고 이 코루틴을 실행합니다. 이 코루틴은 1초에 한번씩 WaitAndPrint 를 출력하게 되며 유저가 스페이스키를 누르게 되면 멤버 변수에 담겨 있는 기존 코루틴의 열거자를 이용하여 실행중인 코루틴을 중단시킵니다.

public void StopCoroutine(string methodName);

이 방법은 이전 방식보다 오버헤드는 크지만 간편하게 사용할 수 있는 방법입니다. 다음과 같이 멤버 변수 없이도 간편하게 사용할 수 있습니다.

public class Example : MonoBehaviour
{

    void Start ()
    {
        StartCoroutine( "WaitAndPrint", 1.0f );
    }

    public IEnumerator WaitAndPrint ( float waitTime )
    {
        while ( true )
        {
            yield return new WaitForSeconds( waitTime );
            Debug.Log( "WaitAndPrint " + Time.time );
        }
    }

    void Update ()
    {
        if ( Input.GetKeyDown( "space" ) )
        {
            StopCoroutine( "WaitAndPrint" );
            Debug.Log( "Stopped " + Time.time );
        }
    }
}

이때에 주의할 점으로는 StopCoroutine을 문자열로 종료시키려면 StartCoroutine 역시 문자열로 실행했었어야 한다는 점입니다. StartCoroutine(IEnumerator routine) 으로 실행한 다음에 StopCoroutine(string methodName) 으로 종료시킬 수 없습니다.

public void StopAllCoroutines();

마지막으로 현재 Behaviour (클래스라고 이해하면 될 것 같습니다)에서 실행한 모든 코루틴을 한번에 종료시키는 함수입니다. 이와 같은 방법으로 현재 클래스에서 실행한 모든 코루틴을 한번에 중단시키게 됩니다.

public class ExampleClass : MonoBehaviour
{
    IEnumerator DoSomething ()
    {
        while ( true )
        {
            yield return null;
        }
    }
    void Example ()
    {
        StartCoroutine( "DoSomething" );
        StopAllCoroutines();
    }
}

어디선가 Example()을 실행하게 되면 DoSomething 코루틴이 실행되게 되면 곧바로 StopAllCoroutines() 이 호출되어 모든 코루틴이 종료됩니다.

참고 :
http://docs.unity3d.com/kr/current/Manual/Coroutines.html
http://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html