본문 바로가기

유니티/최적화

[유니티 최적화] 모바일용 최적화 실전 가이드

728x90
반응형

1. 모바일용 최적화 실전 가이드

이 가이드는 모바일 게임 개발에 입문하는 개발자를 위한 가이드입니다.

어떻게 개발해야 할지 막막한 개발자, 새로운 모바일 게임을 기획하거나 프로토타입을 만들려는 개발자, 또는 기존 프로젝트가 모바일 디바이스에서 원활하게 실행되도록 이식하려는 개발자에게 도움이 될 것입니다.

또한 가이드는 모바일 게임이나 구형 PC 또는 넷북을 타겟으로 하는 브라우저 게임을 개발하려는 개발자에게도 유용한 레퍼런스가 될 것입니다.

최적화는 방대한 주제이며, 최적화 방법은 게임에 따라 달라집니다. 

그러므로 이 가이드는 원활한 제품을 보장하는 단계별 가이드라기보다 입문서 또는 레퍼런스로 활용하는 것이 가장 좋습니다.

1.1 모든 모바일 디바이스는 서로 다릅니다

이 정보는 오리지널 iPad, iPhone 3GS, iPod Touch 3세대에 사용된 Apple A4 칩셋, Android의 경우는 Nexus One이나 기타 Android 2.3 Gingerbread를 탑재한 수준의 하드웨어를 염두해 두고 작성되었습니다. 

디바이스는 대부분 2010년 초 부근에 출시되었습니다. 경쟁이 활발히 벌어지는 앱 마켓에서, 해당 디바이스는 상대적으로 느린 구형 디바이스에 속합니다. 

그러나 시장에서 큰 몫을 차지하므로, 해당 디바이스도 지원이 되어야 합니다.

Apple 모바일 디바이스 기술 사양의 개요는 iPhone 하드웨어 문서를 참조하십시오. 

Apple 최저사양 모바일 디바이스(iPhone 3G 등) 및 iPod Touch 1, 2세대는 제약이 아주 크며 최적화에 더 많이 신경 써야 합니다. 

그러나 자신의 디바이스를 업그레이드하지 않고 사용해 온 소비자들이 앱을 구매할 것인지가 의문으로 남습니다. 

따라서, 무료 앱을 개발하는 것이 아니라면 구형 하드웨어를 지원하는 것이 그다지 가치가 있지 않을 수도 있습니다.

이외에도 훨씬 느리거나 훨씬 빠른 스마트폰 역시 존재합니다. 

모바일 디바이스의 연산 기능은 놀라운 속도로 향상되고 있습니다. 

새로운 세대의 모바일 GPU가 이전 세대에 비해 5배 더 빠르다는 소식도 전혀 처음 듣는 얘기가 아닐 것입니다. 

이는 PC 산업과 비교하면 엄청나게 빠른 발전입니다.

1.2 최종 단계가 아닌 기획 단계에서 최적화를 고려해야 합니다.

영국 컴퓨터 과학자 마이클 A. 잭슨(Michael A. Jackson)의 프로그램 최적화 법칙이 자주 인용됩니다.

“프로그램 최적화 제 1법칙: 하지 말아야 합니다. 

프로그램 최적화 제 2법칙(전문가에게만 해당!): 아직은 하지 말아야 합니다.”

그의 주장은 컴퓨터의 속도와 속도가 증가하는 추세를 고려할 때 실제 어떤 프로그램을 실행하는 상황에서는 그것이 충분히 빠르게 실행될 가능성이 높다는 것에 근거를 두고 있습니다. 

이외에도 지나치게 최적화를 하게 되면 프로그램이 너무 복잡해질 수 있고, 자체적으로 제한되어 버그를 만들어낼 수도 있습니다.

그러나 모바일 게임을 개발한다면 고려해야 할 다른 측면이 있습니다. 

현재 시장에 출시된 하드웨어는 보통 사용하는 컴퓨터와 비교할 때 매우 제약이 큽니다. 

따라서 이러한 디바이스에서 아예 실행되지도 않을 무엇인가를 만들게 될 위험을 떠안는 것보다는 처음부터 최적화를 실행하여 지나치게 복잡해질 위험을 감수하는 편이 훨씬 낫습니다.

이 가이드에서는 최적화가 크게 도움이 되는 상황과 최적화가 필요하지 않는 상황을 비교하도록 하겠습니다.

1.2.1 최적화는 프로그래머만을 위한 것이 아닙니다.

아티스트 또한 플랫폼의 제한과 이를 우회하기 위한 방법을 숙지하여, 같은 일을 되풀이하는 일 없이 창의적으로 좋은 결과를 이끌어낼 수 있도록 해야 합니다.

  • 게임 디자인에서 환경이나 조명을 베이크하는 대신 텍스처에 그리도록 요구하는 경우, 아티스트에게 더 큰 책임이 주어집니다.
  • 베이크할 수 있는 요소가 있다면 아티스트는 실시간 렌더링 대신 베이크용 콘텐츠를 만들 수 있습니다. 이를 통해 기술적 제한을 무시하고 자유롭게 개발할 수 있습니다.

 

1.2.2 부드러운 런타임을 위한 게임 설계

다음 두 페이지에서는 게임 퍼포먼스의 일반적인 트렌드를 자세하게 설명합니다. 

또한 게임이 최적화되도록 설계하는 최상의 방법 또는 이미 제작에 돌입한 게임에서 어떤 것을 최적화해야 하는지 직감적으로 알아낼 수 있는 방법에 대해 설명합니다.

  • 스크립팅 최적화와 게임플레이 최적화를 위한 실질적인 방법
  • 스크립팅 최적화와 게임플레이 최적화를 위한 실질적인 메서드

 

1.3 초기에 프로파일링을 자주해야 하는 이유

프로파일링은 과정을 통해 어떤 최적화가 큰 성능 향상을 이끌어냈고, 어떤 최적화가 시간낭비였는지를 파악할 수 있기 때문에 매우 중요합니다. 

렌더링은 별도의 칩(GPU)에서 처리되는 방식이기 때문에, 한 프레임을 렌더링하는 데에 걸리는 시간은 CPU에서 걸리는 시간에 GPU에서 걸리는 시간을 더한 값이 아니라, CPU 또는 GPU에서 걸리는 시간 중 더 오래 걸리는 시간에 해당합니다.

즉 CPU로 인한 속도 저하는 셰이더를 최적화해도 프레임 속도는 전혀 오르지 않으며, GPU로 인한 속도 저하는 물리와 스크립트를 최적화해도 아무 도움이 되지 않습니다.

게임의 각 부분에 따라 또는 상황에 따라 퍼포먼스 역시 종종 달라질 수 있습니다. 

어떤 부분에서는 순전히 스크립트로 인해 100ms 프레임이 느려질 수도 있고, 또 다른 부분에서는 렌더링으로 인해 프레임이 느려지는 현상이 나타날 수도 있습니다. 

그러므로 게임을 최적화하려면 적어도 어디에서 병목 현상이 나타나는지를 파악할 필요가 있습니다.

1.3.1 Unity 프로파일러(Profiler)

Unity의 주 프로파일러는 iOS, Android, Tizen을 타겟으로 할 때 사용할 수 있습니다. 

기본적인 사용 방법은 프로파일러 문서를 참조하십시오.

1.3.2 내부 프로파일러

Android와 iOS에는 빌트인 프로파일러가 있으며, 매 30프레임마다 텍스트를 출력합니다. 

이를 통해 게임에서 어떤 측면(물리, 스크립트, 렌더링 등)이 속도 저하를 일으키는지 알아낼 수 있으나, 아주 세부적인 사항까지 알 수는 없습니다. 예를 들어, 어느 스크립트 또는 어떤 렌더러가 문제를 일으켰는지는 알 수 없습니다.

  • 프로파일러에서 대부분의 프로세싱 시간이 렌더링에 쓰인다고 표시된다면 렌더링 최적화 문서를 참조하십시오.
  • 프로파일러에서 대부분의 프로세싱 시간이 렌더링 이외에 쓰인다고 표시된다면 스크립트 최적화 문서를 참조하십시오.

내부 프로파일러 문서를 통해 작동 방법과 켜는 방법을 참조하십시오.

 

2. 그래픽스 방법론

모바일 디바이스가 할 수 있는 일은 무엇일까요? 이에 맞추어 게임을 어떻게 기획해야 할까요? 게임 실행 속도가 느리다면, 그리고 프로파일러가 렌더링 병목 현상이 있다고 진단한다면, 무엇을 변경해야 할지, 또한 속도 개선을 하면서도 시각적으로 훌륭한 게임을 만들기 위해 무엇을 해야 할지를 어떻게 알 수 있을까요? 

이 페이지는 기술적인 내용보다는 일반적인 방법을 설명하기 위해 작성되었습니다. 

구체적인 내용을 찾아보려면 렌더링 최적화 페이지를 참조하십시오.

2.1 현재 소비자용 모바일 디바이스에서 기대할 수 있는 것

  • 라이트맵이 적용된 정적 지오메트리. 다음의 경우에는 유의해야 합니다.
    - 대량의 알파테스트 셰이더 사용
    - 범프매핑, 특히 빌트인 셰이더를 사용하는 경우
    - 많은 폴리곤 개수
  • 환상적인 셰이더가 있는 애니메이션 캐릭터. 다음의 경우에는 유의해야 합니다.
    - 대규모 군중 또는 하이 폴리 캐릭터
  • 스프라이트를 적용한 2D 게임. 그러나 다음에 유의해야 합니다.
    - 오버드로우 또는 서로 겹쳐 그려진 여러 개의 레이어
  • 파티클 효과. 다음의 경우에는 유의해야 합니다.
    - 큰 파티클에 높은 밀도. 여러 개의 파티클이 겹쳐진 경우 또한 오버드로우된 경우입니다.
    - 터무니없이 많은 수의 파티클 또는 파티클 콜라이더.
  • 물리. 다음의 경우에는 유의해야 합니다.
    - 메시 콜라이더
    - 대량의 액티브 바디

2.2 현재 소비자용 모바일 디바이스에서 기대할 수 없는 것:

  • 글로우 및 피사계심도 등의 풀스크린 이미지 이펙트
  • 동적 픽셀당 조명(중요로 설정되고 라이트맵으로 베이크되지 않은 여러 개의 광원)
    영향 받는 모든 오브젝트는 사용 중인 모든 동적 광원에 대해 추가로 시간을 끌게 되며, 급격히 느려지게 됩니다.
  • 모든 것에 대한 실시간 섀도우
    Unity는 모바일 플랫폼에서 실시간 섀도우를 지원하지만 매우 신중하게 사용해야 하며, 하이 엔드 디바이스에 국한될 가능성이 높습니다.

2.3 예제 - 최고 수준의 모바일 게임을 만드는 방법

2.3.1 Shadowgun

Shadowgun은 현재의 모바일 하드웨어에서 어떤 것들이 가능한지를 보여주는 인상적인 사례입니다. 

더 구체적으로 말하자면, 할 수 없는 작업과 한계를 극복하는 방법에 대한 좋은 예제입니다. 

특히 게임의 작은 부분이 이 블로그 포스트에서 공개적으로 사용할 수 있게 되었기 때문입니다.

다음은 Shadowgun에서 성능을 향상시키기 위해 실행하는 작업에 대한 기본적인 설명입니다.

  • 동적 조명 - 거의 사용되지 않습니다.
    - 리얼 섀도우 대신 블롭 섀도우와 라이트맵이 사용됩니다.
    - 캐릭터에 실제 광원 대신 라이트 프로브가 사용됩니다.
      머즐 플래시가 스크립트를 통해 라이트프로브 데이터에 추가됩니다.
    - 유일한 동적 픽셀당 조명은 캐릭터의 BRDF를 계산하기 위해 사용되는 임의의 광원 방향입니다.
  • 프매핑 - 거의 사용되지 않습니다.
    - 리얼 범프매핑은 캐릭터에만 사용됩니다.
    - 콘트라스트와 디테일을 디퓨즈 텍스처 맵으로 최대한 베이크합니다. 범프맵의 조명 정보도 베이크됩니다.
    - 오른쪽에 보이는 조각상 텍스처 또는 빛나는 벽이 좋은 예입니다. 렌더링할 때 범프맵을 사용하지 않으며, 반사되는 느낌은 텍스처에 베이크하여 구현합니다. 라이트매핑은 버텍스 조명 기반의 스페큘러 하이라이트와 결합되어 이 모델들이 빛나는 것처럼 보이도록 합니다.
    - 이러한 텍스처를 생성하는 법을 알고 싶으면 렌더링 최적화 페이지를 참조하십시오.
  • 밀도 높은 파티클 - 가급적 사용하지 않습니다.
    - 밀도 높은 파티클 효과 대신 UV 스크롤링 텍스처를 사용합니다.
  • 안개 효과 - 가급적 사용하지 않습니다.
    - 게임에서 신의 광선은 손으로 모델링됩니다.
    - 단일 평면을 페이드 인/아웃하는 방식으로, 실제로 안개를 렌더링하지 않고도 영화 같은 안개 효과를 얻습니다.
       이렇게 하면 평면 수가 적고 거리가 멀기 때문에 더 빠릅니다. 그리고 픽셀 또는 셰이더마다 안개를 계산할 필요가 없습니다.
  • 글로우 - 가급적 사용하지 않습니다.
    - 블렌디드 스프라이트 평면을 사용하여 특정 오브젝트에 글로우 효과를 나타냅니다.

 


2.3.2 Sky Castle 데모

이 데모는 Unity가 하이 엔드 Android 장치에서 할 수 있는 것을 표시하도록 설계되었습니다.

  • 동적 조명 - 사용되지 않습니다.
    라이트맵에서만 사용됩니다.
  • 범프매핑 - 사용됩니다.
    벽돌은 모두 범프 맵이며 방향 라이트맵에 의해 점등됩니다. 여기서 “하이 엔드 장치” 부분이 등장합니다.
  • 실시간 반사 - 제한됩니다.
    한 번에 하나만 실행되도록 두 번 렌더링해야 하는 환경을 쉽게 제거할 수 있도록 격리된 영역과 별도로 실시간 반사 표면을 신중하게 배치했습니다.

2.4 결론 - 게임에서 이것은 무엇을 의미합니까?

모바일 디바이스의 한계를 인정하고 이해할수록 게임을 시각적으로 더 훌륭하고 부드럽게 동작하도록 만들 수 있습니다. 

모바일용 고급 게임을 만들려고 할 경우 Unity의 그래픽스 파이프라인을 이해하고 자체 셰이더를 만들 수 있으면 크게 유익합니다. 

그러나 즉시 사용할 수 있는 것을 원할 경우 여기에서 얻을 수 있는 ShadowGun의 셰이더가 유용합니다.

2.4.1 시뮬레이션하지 말고 베이크해야 합니다!

게임이 자연의 법칙을 따르려 한다는 데에는 이견의 여지가 없습니다. 

모든 포물선 발사체, 또는 빛나는 크롬의 각 픽셀 컬러 등은 실제 월드를 관찰하여 이를 모방한 공식으로부터 유래되었습니다. 

그러나 게임은 한편으로 과학적 시뮬레이션이자 예술입니다. 

물리적으로 정확한 렌더링만 가지고는 모바일 시장에서 경쟁할 수 없습니다. 

현실 세계를 완전히 흉내 내기에는 하드웨어가 아직 따라주지 않으며 이런 게임은 결국 제약에 얽매이는 칙칙하고 둔한 게임이 됩니다.

폴리곤과 블렌드 모드를 선택할 때는 마치 그림 붓을 고르듯이 선택해야 합니다.

Shadowgun에서 볼 수 있는 베이크된 범프맵이 훌륭한 예제입니다. 

텍스처에 이미 스페큘러 하이라이트가 들어가 있습니다. 

인간의 눈은 하이라이트가 반사광 및 시점 방향과 일치하지 않는다는 것을 실제로는 알아차리지 못합니다. 

하이라이트는 단순히 텍스처 위에 높은 콘트라스트로 넣은 디테일로 완전히 거짓이지만 결국 훌륭한 시각적 효과를 냅니다. 

이는 인기 있는 여러 게임에서 사용해온 일반적인 속임수 기술 중 하나입니다. 

Halo의 첫 스크린샷의 바이저를 이 릴리스 스크린샷의 바이저와 비교합니다. 헬멧의 윗부분으로부터 돌출된 갑옷이 바이저에 반사된 것으로 보이나, 실제로는 이 반사가 바이저 텍스처에 베이크된 것입니다. 

League of Legends에서는 스펠 효과에 픽셀 광원이 있는 것처럼 보이지만, 실제로는 텍스처가 있는 블렌디드 평면으로서 땅 위에서 빛나는 픽셀 광원의 스크린샷을 가지고 생성한 것처럼 보입니다.

2.4.2 동작이 잘 되는 작업

  • 라이트매핑된 정적 지오메트리
    극적인 조명과 매우 동적인 환경을 함께 사용할 수 없습니다. 둘 중에 하나만 선택해야 합니다.
  • 움직이는 오브젝트용 라이트프로브
    현재의 모바일 하드웨어는 대량의 동적 광원에 적합하지 않으며 섀도우를 만들 수 없습니다. 라이트 프로브는 정적 조명을 사용하는 복잡한 게임 월드에 아주 적합한 해결책입니다.
  • 특화된 셰이더와 세밀한 고대비 텍스처
    ShadowGun의 셰이더는 픽셀당 연산을 최소화하고 복잡한 고품질 텍스처를 최대한 활용합니다. 셰이더가 단순할 때도 멋지게 보이는 텍스처를 만드는 방법에 대한 정보는 렌더링 최적화 페이지를 참조하십시오.
  • 카툰 그래픽스
    게임이 사진처럼 보여야 한다는 사람은 없을 것입니다. 조명과 환경의 책임 소재를 엔진이 아닌 텍스처 아티스트에게 묻는다면, 렌더링 최적화는 신경 쓸 필요도 없게 됩니다.

2.4.3 동작이 잘 되지 않는 작업

  • 글로우 및 기타 포스트 프로세싱 효과
    - 가능할 경우 블렌디드 사각형을 사용하여 이러한 효과를 비슷하게 만듭니다. Shadowgun 프로젝트의 예제를 참고해야 합니다.
  • 범프매핑, 특히 빌트인 셰이더가 있는 경우
    - 되도록 적게, 가장 중요한 캐릭터나 오브젝트에만 사용해야 합니다. 전체 스크린을 차지할 수 있는 것에는 범프맵을 사용하지 말아야 합니다.
    - 범프맵을 사용하는 대신 세부 정보와 콘트라스트를 디퓨즈 텍스처에 더 많이 베이크해야 합니다. League of Legends의 효과는 업계에서 이 전략을 성공적으로 사용한 흥미로운 예입니다.

2.4.4 실제로 어떻게 해야 합니까?

렌더링 최적화 페이지를 참조하십시오.

3. 스크립트와 게임플레이 방법론

이 섹션에서는 모바일 개발자들이 게임의 실행 속도를 높이기 위해 코드를 작성하고 구조를 짜는 방식을 설명합니다. 

핵심 아이디어는 게임 설계와 최적화가 알고 보면 별개의 절차가 아닙니다. 

게임을 설계할 때 내리는 결정이 게임을 재미있고도 빠르게 할 수 있습니다.

 

3.1 과거 예제


플레이어가 화면에서 발사를 한 번만 할 수 있고 재장전 속도도 타이머가 아니라 탄환이 명중했느냐 아니냐 여부에 좌우되었던 옛날 게임을 기억합니다. 오브젝트 풀링(object pooling)이라는 기술은 메모리 관리를 간편하게 해 프로그램이 더욱 부드럽게 실행되게 합니다.

Space Invaders의 제작자는 RAM 용량이 작아서 프로그램이 사용 가능한 이상의 메모리를 할당하지 않는지 확인해야 했습니다. 플레이어가 1초마다 발사하고 재장전 시간을 0.5초로 줄이는 파워업을 제공하는 경우, 최대한 빨리 발사를 하고 탄환도 모두 오랫동안 체공한다면 발사체에 할당할 메모리 공간이 충분한지를 확인할 필요가 있었습니다. 

충분히 문제가 될 수 있는 상황이었기 때문에 제작자는 다른 방법을 취했습니다. 

발사체 하나를 할당하고 그대로 두는 대신, 발사체가 소멸되면 간단히 비활성화하고 위치를 변경한 후 다시 발사되면 활성화했습니다. 

발사체가 메모리에서 동일한 공간에 살아있기 때문에 이동하거나 계속 지우고 재생성할 필요가 없습니다.

3.1.1 최적화 또는 게임플레이의 핵심

현실적이지는 않지만 재미있는 일입니다. 

외계 침입자가 지상에 가까워지면 마치 영화나 소설의 클라이막스처럼 긴장이 최고조에 달합니다. 침입자들이 다가오는 인접도는 능숙한 플레이어에게 거의 동시적인 재장전 시간을 주며 플레이어는 완벽한 시기에 발사 키를 눌러 기적적으로 지구를 지킵니다. 

훌륭한 게임 디자인은 대화형의 이야기와 이를 작동시키는 배경 기술 사이의 절묘한 공간에서 탄생합니다. 

이처럼 환상적이고 재미가 있으며 효과적인 게임을 구상하기란 어려운 일입니다. 

코드의 세부 계획과 사용자의 상호작용은 꽤 거리가 있고 매우 까다로워 둘을 결합해 신선하고 재미있는 결과물을 만들기까지 상당한 사고와 실험이 필요하기 때문입니다.

상호작용과 더불어 모바일 하드웨어에서 동시에 멋진 플레이를 할 수 있는 게임을 만들기 위해 각각의 요소를 세세하게 계획하기란 어렵습니다. 

그보다는, 실험을 하는 도중에 두 요소가 조화를 이루는 “보석”을 우연히 발견할 가능성이 높습니다. 

그렇다고 하더라도 하드웨어에서 코드가 어떻게 실행되는지 제대로 이해하는 것은 도움이 됩니다. 

오브젝트 풀링이 더 나은 이유에 대한 자세한 기술적 설명을 읽고, 또 메모리 할당을 이해하려면 스크립트 최적화 페이지를 참조하십시오.

3.2 X는 모바일에서 빠르게 실행됩니까?

게임 작업에 착수한 경우를 가정해보겠습니다. 

플레이어들에게 다양한 동작과 현란한 볼거리로 깊은 인상을 남기고자 합니다. 

그런 게임은 어떻게 계획해나가야 합니까? 어디가 한계인지 어떻게 알 수 있습니까? 

코인이 몇 개인지, 좀비가 몇인지, 상대 자동차가 몇 대인지 등 게임 용어는 또 어떻습니까? 모두 게임을 어떻게 코딩하느냐에 달려 있습니다.

일반적으로 자신의 게임 코드를 쉽고 가장 일반적이며 다목적 방식으로 작성하면 퍼포먼스 문제에 훨씬 일찍 부딪칩니다. 

게임을 실행할 때 특정 구조체와 트릭에 의존하면 할수록 시야를 넓히고 화면에 더 많은 요소를 넣게 됩니다.

3.2.1 쉽고 다목적이지만 느립니다

  • 2D 게임에서는 리지드바디가 2차원으로 제한됩니다.
  • 발사체에 리지드바디가 있습니다.
  • 인스턴스화를 사용하고 많이 파괴합니다.
  • 수집품이나 캐릭터의 개별 3D 오브젝트가 많습니다.
  • 매 프레임마다 연산을 수행합니다.
  • GUI나 HUD에 OnGUI를 사용합니다.

3.2.2 복잡하고 제한적이지만 빠릅니다

  • 자신만의 2D 게임 물리 코드를 작성합니다.
  • 발사체에 대한 충돌 검사를 스스로 처리합니다.
  • 인스턴스화와 파괴 대신 오브젝트 풀링을 활용합니다.
  • 단순한 오브젝트를 나타내기 위해 파티클에 애니메이션화한 스프라이트를 사용합니다.
  • 몇 프레임에 한 번씩 복잡한 계산을 수행하고 그 결과를 캐싱합니다.
  • 커스텀 GUI 솔루션입니다.

3.3 예제

 

3.3.1 화면 속에서 수백 개의 동전이 회전하고 동적으로 빛나며 수집 가능한 상태를 한번에 구현

  • NO: 각각의 동전이 회전하고 수집이 가능한 리지드바디와 스크립트를 가진 별도의 오브젝트입니다.
  • YES: 동전은 애니메이션화한 텍스처가 있는 파티클 시스템으로, 한 스크립트가 모든 동전의 충돌 테스트를 하며 광원으로부터의 거리에 따라 컬러를 설정합니다.
  •  예제는 스크립트 최적화 페이지에서 구현되었습니다.

3.3.2 커스텀 빌드한 소프트 바디 시뮬레이션

  • NO: 월드 도처에 베개가 널려있어 던지고 쌓을 수 있습니다.
  • YES캐릭터가 베개이고 하나만 있으며 상황이 어느정도 예측 가능합니다(오로지 구체 및 축에 정렬된 큐브와 충돌). 기능이 완벽하지 않은 소프트바디 시뮬레이션을 코딩할 수도 있지만 인상적이면서도 빨리 움직이는 듯 보입니다.

3.3.3 적 캐릭터 30명이 플레이어를 향해 한꺼번에 발사하는 상황

  • NO각각의 적마다 스킨드 메시가 있고 무기는 별도의 오브젝트이며 리지드바디 기반의 발사체가 발사될 때마다 인스턴스화합니다. 각각의 적마다 같은 편의 상황을 복잡한 AI 스크립트에 반영해 매 프레임마다 실행합니다. 
  • YES대다수의 적이 멀리 있고 하나의 스프라이트로 대표되거나 적이 2D이면서 소수의 스프라이트입니다. 적의 모든 탄환은 동일한 파티클 시스템으로 그려지며 기초적인 물리에 불과한 스크립트로 시뮬레이션됩니다. 각각의 적은 섹터의 다른 적의 상황에 따라 AI 상태를 1초에 두 번 업데이트합니다.

3.4 최적화를 하는 방법과 이유

스크립트 최적화 페이지를 참조하십시오.

4. 렌더링 최적화

이 섹션에서는 렌더링 최적화에 대한 전문적인 지식을 소개합니다. 

더 나은 성능을 위해 조명 결과물을 베이크하는 방법과 Shadowgun 개발자들이 조명이 베이크된 고대비 텍스처를 활용하여 훌륭한 시각적 효과를 보이는 게임을 만든 방법 등을 설명합니다. 

모바일에 최적화된 게임이 어떤 모습인지에 대한 일반 정보를 얻으려면 그래픽스 방법론 페이지를 참조하십시오.

4.1 예술성을 발휘해야 합니다

때로는 게임의 렌더링 최적화를 위해 궂은 일을 해야 할 때도 있습니다. 

Unity가 제공하는 모든 구조는 더 빠른 속도를 쉽게 얻을 수 있도록 해 주지만, 제한된 하드웨어에서 최상급의 충실도를 제공하고자 한다면 이러한 구조를 회피하여 스스로 제작하는 것이 정답입니다. 

주요 구조 변화를 통해 훨씬 더 빠른 게임을 만들 수 있기 때문입니다. 

이 때 선택해야 할 툴은 에디터 스크립트, 단순 셰이더, 그리고 전통적인 방식의 아트 프로덕션입니다.

4.1.1 어떻게 뛰어들 것인가

먼저, 셰이더 작성 방법 소개 페이지를 확인해야 합니다.

  • 빌트인 셰이더
    빌트인 셰이더의 소스 코드를 살펴볼 수 있습니다. 기존과 다른 동작을 하는 새로운 셰이더를 만들고자 할 경우, 이미 존재하는 기존의 두 셰이더의 일부분을 가져다가 하나로 합치는 방식으로 만들 수도 있습니다.
  • 표면 셰이더 디버깅(#pragma debug)
    모든 표면 셰이더로부터 CG 셰이더가 생성되며, 그 후 거기에서 완전히 컴파일됩니다. 표면 셰이더의 맨 위에 #pragma debug를 추가하면, 컴파일된 셰이더를 인스펙터로 열었을 때 CG 중간코드를 볼 수 있습니다. 이 방법은 셰이더의 특정 부분이 실제로 어떻게 산출되는지를 살펴보는 데 유용하며, 또한 표면 셰이더에서 원하는 특정 측면을 파악하여 CG 셰이더에 적용하는 데에도 유용합니다.
  • 셰이더 포함 파일
    다수의 셰이더 헬퍼 코드가 모든 셰이더에 포함되어 있으며 보통 사용되지는 않습니다. 그러나 이러한 헬퍼 코드가 존재하기 때문에, WorldReflectionVector와 같이 어디에도 정의되지 않은 듯한 함수를 셰이더가 종종 호출할 수 있습니다. Unity는 이러한 헬퍼 정의를 보유하는 여러 빌트인 셰이더 포함 파일을 제공합니다. 특정 함수를 찾고자 한다면, 서로 다른 포함 사항 모두를 검색해야 합니다.
    이러한 파일은 Unity가 셰이더 작성을 쉽게 하기 위해 사용하는 내부 구조의 주요 구성 부분입니다. 파일은 실시간 그림자, 서로 다른 광원 타입, 라이트맵, 다중 플랫폼 지원 등을 제공합니다.
  • 하드웨어 문서
    잠시 시간을 할애하여 Apple 문서 중 셰이더 작성 베스트 프랙티스 페이지를 참조하십시오. Unity는 부동 소수점 정밀도 힌트에 있어서 좀 더 공격적으로 보는 것을 제안합니다.

4.2 Shadowgun에 대한 상세한 정보

Shadowgun은 게임이 실행되는 하드웨어를 고려할 때 훌륭한 그래픽 결과를 보여줍니다. 

아트 품질이 해결의 열쇠인 것처럼 보이지만, 아티스트가 잠재력을 최대한 발휘할 수 있도록 하기 위해 프로그래머들이 품질을 이끌어내려고 사용한 몇 가지 트릭이 있습니다.

그래픽스 방법론 페이지에서는 Shadowgun의 황금 조각상을 훌륭한 최적화의 예제로 들고 있습니다. 

Shadowgun은 각 조각상을 높은 선명도로 표현하기 위해 노멀 맵을 사용하는 대신, 조명 디테일을 텍스처에 베이크하였습니다. 

이와 유사한 기법을 게임 개발에 어떻게 활용하는지, 그리고 왜 그렇게 해야 하는지에 대해 보여 드리겠습니다.

 

4.2.1 실시간 셰이더(Real-Time Shader) 코드 vs. 베이크된 황금 조각상 셰이더(Baked Golden Statue Shader) 코드

// This is the pixel shader code for drawing normal-mapped
// specular highlights on static lightmapped geometry

// 5 texture reads, lots of instructions

SurfaceOutput o;

fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
fixed4 c = tex * _Color;
o.Albedo = c.rgb;

o.Gloss = tex.a;
o.Specular = _Shininess;

o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));

float3 worldRefl = WorldReflectionVector (IN, o.Normal);
fixed4 reflcol = texCUBE (_Cube, worldRefl);
reflcol *= tex.a;
o.Emission = reflcol.rgb * _ReflectColor.rgb;
o.Alpha = reflcol.a * _ReflectColor.a;

fixed atten = LIGHT_ATTENUATION(IN);
fixed4 c = 0;

half3 specColor;
fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);

const float3x3 unity_DirBasis = float3x3( 
float3( 0.81649658,  0.0, 0.57735028),
float3(-0.40824830,  0.70710679, 0.57735027),
float3(-0.40824829, -0.70710678, 0.57735026) );

half3 lm = DecodeLightmap (lmtex);

half3 scalePerBasisVector = DecodeLightmap (lmIndTex);

half3 normalInRnmBasis = saturate (mul (unity_DirBasis, o.Normal));
lm *= dot (normalInRnmBasis, scalePerBasisVector);

return half4(lm, 1);

 

// This is the pixel shader code for lighting which is
// baked into the texture

// 2 texture reads, very few instructions

fixed4 c = tex2D (_MainTex, i.uv.xy);   

c.xyz += texCUBE(_EnvTex,i.refl) * _ReflectionColor * c.a;

return c;

 

 

4.2.2 텍셀 렌더링

실시간 광원이 확실한 고품질을 보장한다면 베이크된 광원은 엄청난 퍼포먼스 향상을 가져옵니다. 

어떻게 그럴 수 있을까요? 

텍셀 렌더링(Render to Texel )이라 불리는 에디터 툴이 바로 이러한 작업을 수행합니다.

이 툴은 다음 과정을 거쳐 광원을 텍스처로 베이크합니다.

  • 스크립트를 통해 탄젠트 공간 노멀 맵을 월드 공간으로 변환합니다.
  • 스크립트를 통해 월드 공간 포지션 맵을 생성합니다.
  • 두 개의 기존 맵을 사용하여 전체 텍스처의 전체화면 패스를 텍스처로 렌더링합니다. 이때 광원당 하나의 패스를 추가합니다.
  • 여러 다른 관점에서 결과값의 평균을 냅니다. 이를 통해 모든 방향에서, 또는 최소한 게임 내에서 보통 바라보는 시각에서 그럴듯하게 보이는 결과물을 만들 수 있습니다.

이것이 최상의 그래픽스 최적화가 이루어지는 방법입니다. 

엄청난 수의 연산을 에디터에서 또는 게임 실행 전에 수행함으로써 연산 수를 줄입니다. 일반적으로 다음과 같이 하면 됩니다.

 

 

  • 성능은 걱정하지 말고 시각적으로 훌륭한 결과물을 만들어야 합니다.
  • Unity의 라이트매퍼, 텍셀 렌더링, 스프라이트 패커 등 에디터 확장 프로그램을 사용하여 렌더링하기 아주 간단한 요소에 베이크하십시오.
    직접 툴을 만드는 것이 최상의 방법으로, 게임에서 드러나는 모든 문제를 처리할 수 있는 완벽한 툴을 만들 수 있습니다.
  • 셰이더와 스크립트를 생성해서 베이크된 결과물에 일종의 “빛나는” 효과를 더해야 합니다. 눈길을 끄는 효과를 통해 동적 광원이 적용된 것 같은 착시를 유도할 수 있습니다.

 

4.2.3 광원 주파수의 개념

 


오디오 음원의 저음과 고음처럼, 이미지에도 역시 High-Frequency와 Low-Frequency 컴포넌트가 있습니다. 

마치 스테레오에서 서브우퍼와 트위터를 사용하여 충실한 사운드를 만들어내듯이, 렌더링할 때에도 컴포넌트는 서로 다른 방식으로 다루는 것이 좋습니다. 

이미지의 서로 다른 광원 주파수(Light Frequency)를 시각화하는 방법 중 하나는 Photoshop에서 “하이패스” 필터를 사용하는 것입니다. 

필터->기타->하이패스. 오디오 작업을 해 본 적이 있다면 하이패스라는 이름이 익숙할 것입니다. 

기본적으로 필터가 하는 일은 필터로 전달하는 파라미터 X보다 작은 모든 주파수를 제거하는 것입니다. 

이미지에서는 가우시안 블러가 로우 패스의 역할을 합니다.

실시간 그래픽스에서도 이러한 개념을 차용하는데, 주파수를 활용하면 전체를 여러 부분으로 나누고 각 부분을 어떻게 처리해야 할지 쉽게 결정할 수 있기 때문입니다.

예를 들어, 기본 라이트맵 환경에서 최종 이미지는 주파수가 낮은 광원을 처리한 라이트맵과 주파수가 높은 광원을 처리한 텍스처를 합성하여 얻어집니다.

Shadowgun에서는 주파수가 낮은 광원은 라이트 프로브를 통해 캐릭터에 빨리 적용되고, 주파수가 높은 광원은 임의의 광원 방향과 함께 단순 범프맵 셰이더를 사용하여 인위적으로 꾸며집니다.

일반적으로 광원을 렌더링할 때 주파수에 따라 서로 다른 방법(예: 베이크 vs 동적, 오브젝트당 vs 레벨당, 픽셀당 vs 버텍스당 등)을 사용함으로써 제한된 하드웨어에서 충실한 이미지를 생성할 수 있습니다. 

스타일 측면에서의 선택은 차치하고, 다양한 컬러나 값을 낮은 주파수의 광원과 높은 주파수의 광원 양쪽에 적용해보는 것도 좋은 방법입니다.

4.3 실전에서의 광원 주파수: Shadowgun 분석

 

 

  • 상단 열
    울트라 로우 프리퀀시 스페큘러 버텍스 광원 (동적) | 하이 프리퀀시 알파 채널 | 로우 프리퀀시 라이트맵 | 하이 프리퀀시 알베도
  • 중간 열
    스페큘러 버텍스 광원 * 알파 | 하이 프리퀀시 추가 세부 사항 | 라이트맵 * 컬러 채널
  • 하단
    최종 결과물

참고: 보통 이러한 분석은 디퍼드 렌더러에서 단계를 나타내나, 여기서는 그렇지 않습니다. 모든 것이 오직 하나의 패스에서 진행되었습니다. 구성은 두 개의 관련 셰이더를 기반으로 이루어졌습니다.

4.3.1 버텍스당 추가되는 버추얼 글로스가 있는 라이트맵

Shader "MADFINGER/Environment/Virtual Gloss Per-Vertex Additive (Supports Lightmap)" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    //_MainTexMipBias ("Base Sharpness", Range (-10, 10)) = 0.0
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125
    _ScrollingSpeed("Scrolling speed", Vector) = (0,0,0,0)
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;
    samplerCUBE _ReflTex;

    #ifdef LIGHTMAP_ON
    float4 unity_LightmapST;
    sampler2D unity_Lightmap;
    #endif

    //float _MainTexMipBias;
    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float4 _ScrollingSpeed;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        #ifdef LIGHTMAP_ON
        float2 lmap : TEXCOORD1;
        #endif
        fixed3 spec : TEXCOORD2;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);

        o.uv = v.texcoord + frac(_ScrollingSpeed * _Time.y);

        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float3 viewPos = UnityObjectToViewPos(v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        #ifdef LIGHTMAP_ON
        o.lmap = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
        #endif
        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c = tex2D (_MainTex, i.uv);

            fixed3 spec = i.spec.rgb * c.a;

            #if 1
            c.rgb += spec;
            #else           
            c.rgb = c.rgb + spec - c.rgb * spec;
            #endif

            #ifdef LIGHTMAP_ON
            fixed3 lm = DecodeLightmap (tex2D(unity_Lightmap, i.lmap));
            c.rgb *= lm;
            #endif

            return c;
        }
        ENDCG 
    }   
}
}

 

4.3.2 버텍스당 추가되는 버추얼 글로스가 있는 라이트 프로브

Shader "MADFINGER/Environment/Lightprobes with VirtualGloss Per-Vertex Additive" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (1, 1, 1, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125    
    _SHLightingScale("LightProbe influence scale",float) = 1
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #pragma multi_compile _ LIGHTMAP_ON
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;


    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float _SHLightingScale;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 refl : TEXCOORD1;
        fixed3 spec : TEXCOORD3;
        fixed3 SHLighting: TEXCOORD4;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv = v.texcoord;

        float3 worldNormal = UnityObjectToWorldDir(v.normal);       
        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float4 viewPos = UnityObjectToViewPos(v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos.xyz - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        o.SHLighting    = ShadeSH9(float4(worldNormal,1)) * _SHLightingScale;

        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c    = tex2D (_MainTex, i.uv);

            c.rgb *= i.SHLighting;
            c.rgb += i.spec.rgb * c.a;

            return c;
        }
        ENDCG 
    }   
}
}

 

4.4 베스트 프랙티스

4.4.1 GPU 최적화: 알파 테스팅

일부 GPU, 특히 모바일 디바이스용 GPU는 알파 테스팅(또는 픽셀 셰이더에서 discard 및 clip 사용 시)에 있어 높은 성능 부하를 유발합니다. 

따라서 가급적 알파 테스트 셰이더를 알파 블렌디드 셰이더로 교체하여야 합니다.

알파 테스팅을 피할 수 없는 경우, 표시되는 알파 테스트된 픽셀 전체 수를 최소한으로 유지해야 합니다.

4.4.2 iOS 텍스처 압축

일부 이미지, 특히 iOS/Android PVR 텍스처 압축을 사용하는 이미지는 알파 채널에서 의도하지 않은 시각적 결함을 발생시키는 경향이 있습니다. 

이러한 경우 PVRT 압축 파라미터를 이미징 소프트웨어에서 직접 조절해야 할 수도 있습니다. 

PVR 익스포트 플러그인을 설치하거나, 또는 PVRTC 포맷을 제작한 Imagination Tech의 PVRTexTool을 사용하면 됩니다. 

.pvr 확장자를 가지는 압축 이미지 결과물 파일은 Unity 에디터에서 직접 임포트할 수 있고, 명시된 압축 파라미터가 그대로 유지됩니다. 

PVRT 압축 텍스처의 화질이 충분하지 않거나, GUI 텍스처 등을 위해 특별히 선명한 이미지가 필요할 경우 32비트 대신 16비트 텍스처를 사용하는 것을 고려해 보십시오. 

16비트 텍스처를 사용하면 메모리 대역폭과 스토리지 요구 사항을 절반으로 줄일 수 있습니다.

4.4.3 Android 텍스처 압축

OpenGL ES 2.0을 지원하는 모든 Android 디바이스는 ETC1 compression format도 지원합니다. 

그러므로 가능하다면 선호하는 텍스처 포맷으로 항상 ETC1을 사용할 것을 권장합니다.

Nvidia Tegra 또는 Qualcomm Snapdragon 등의 특정 그래픽스 아키텍처를 타겟으로 한다면, 이러한 아키텍처에서 사용 가능한 전용 압축 포맷 사용을 고려해보는 것도 좋습니다. 

Android Market 또한 지원 텍스처 압축 포맷에 기반한 필터링을 허용합니다. 

예를 들어 DXT 압축 텍스처를 포함하는 배포 아카이브 파일(.apk)은 압축 포맷을 지원하지 않는 디바이스에서는 다운로드되지 않도록 제한됩니다.

4.4.4 연습

텍셀 렌더링을 다운로드합니다. 

모델에 조명을 베이크해야 합니다. 

Photoshop에서 위 결과물에 하이패스 필터를 실행해야 합니다.

텍셀 렌더링 패키지에 포함된 “ Mobile/Cubemapped” 셰이더를 편집하여, 제거된 낮은 주파수의 광원 디테일이 버텍스 광원으로 대체되도록 하십시오.

5. 스크립트 최적화

이 섹션에서는 게임에서 사용하는 실제 스크립트와 메서드를 최적화하려 할 때 어떻게 착수하면 될지 설명합니다. 

또한 최적화가 가능한 이유 및 특정 상황에서 이러한 최적화를 적용했을 때 얻는 장점에 대해 자세하게 설명합니다.

5.1 프로파일러가 최고!

프로젝트가 원활히 돌아가도록 보장해주는 체크리스트 같은 것은 없습니다. 

느린 프로젝트를 최적화하기 위해서는 지나치게 시간을 잡아먹는 특정 원인을 프로파일링해야 합니다. 

프로파일링을 하지 않거나 프로파일러에서 얻은 결과물을 철저히 이해하지 않고서 최적화를 하려는 것은 마치 눈을 가리고 최적화하려는 것과 마찬가지입니다.

5.1.1 내부 모바일 프로파일러

내부 프로파일러를 사용하면 물리, 스크립트, 렌더링 중 어떤 프로세스가 게임 속도를 저하시키는지 알아낼 수 있으나, 

실제로 어디가 원인인지를 찾아내기 위해 특정 스크립트 및 메서드 내부까지 파고들 수는 없습니다. 

그러나 특정 기능을 활성화/비활성화하는 스위치를 게임에서 만들어 둠으로써, 가장 문제가 되는 부분이 어디인지 그 범위를 상당히 좁힐 수 있습니다. 

예를 들어, 적 캐릭터의 AI 스크립트를 제거하고 나서 프레임 속도가 두 배가 되었다면, 해당 스크립트 또는 스크립트가 게임에 가져온 무엇인가를 최적화해야 한다는 것을 알 수 있습니다. 

여기서 유일한 문제는 문제점을 찾아낼 때까지 여러 가지 다른 것을 시도해 보아야 한다는 점입니다.

모바일 디바이스에서의 프로파일링에 대한 자세한 정보는 프로파일링 섹션을 참조하십시오.

5.2 디자인에 의한 최적화

처음부터 빨리 실행되도록 구현하려고 하면 어느 정도 위험이 따릅니다. 

너무 느려서 나중에 삭제하거나 다른 것으로 대체하는 일에 많은 시간을 낭비하는 것도 문제이지만 최적화하지 않아도 충분히 빠르게 만드느라 시간을 낭비하는 것도 문제이기 때문입니다. 

이러한 측면에서 결정을 잘 내리려면 직관과 하드웨어에 대한 풍부한 지식이 필요합니다. 

특히 모든 게임은 서로 다르고, 어떤 게임에서 매우 중요한 최적화가 다른 게임에서는 아무런 효과가 없을 수도 있기 때문입니다.

5.2.1 오브젝트 풀링

오브젝트 풀링(Object Pooling)은 스크립트 최적화 방법론 소개에서 좋은 게임플레이와 좋은 코드 디자인 사이의 교차점의 예로 든 바 있습니다. 

잠깐만 사용하는 오브젝트에 오브젝트 풀링을 사용하면 오브젝트를 생성했다가 삭제하는 것에 비해 빠른데, 메모리 할당을 더 간단하게 할 수 있으며 동적 메모리 할당 오버헤드와 가비지 컬렉션(GC)을 없앨 수 있기 때문입니다.

5.2.2 메모리 할당

자동 메모리 관리에 대한 간단한 설명

대부분의 스크립트 언어가 그러하듯, Unity에서 작성하는 스크립트는 자동 메모리 관리를 사용합니다. 

이와 대조적으로, C 및 C++와 같은 로우 레벨 언어는 수동 메모리 할당을 사용하며, 프로그래머가 메모리 주소에서 직접 읽고 쓰는 것을 허용하고 결과적으로 생성한 모든 오브젝트를 삭제하는 것은 프로그래머의 책임입니다. 

예를 들어 C++에서 오브젝트를 생성한다면, 사용이 끝난 후에는 오브젝트가 차지하고 있던 메모리를 수동으로 해제해야 합니다. 스크립팅 언어에서는 단지 objectReference = null;이라고만 명시하면 됩니다.

참고: GameObject myGameObject; 또는 var myGameObject : GameObject;와 같은 게임 오브젝트 변수가 있다고 할 때, myGameObject = null;이라고 하면 왜 오브젝트가 삭제되지 않을까요?

  • 게임 오브젝트는 여전히 Unity가 레퍼런스합니다. 게임 오브젝트를 그리거나 업데이트하는 등의 동작을 위해 오브젝트에 대한 레퍼런스를 Unity가 유지해야 하기 때문입니다. Destroy(myGameObject);를 호출하면 해당 레퍼런스를 제거하고 오브젝트를 삭제합니다.

그러나 Unity가 알지 못하는 오브젝트, 예를 들어 어느 것으로부터도 상속받지 않는 클래스의 인스턴스(이에 반해 대부분의 클래스 또는 “스크립트 컴포넌트”는 MonoBehaviour로부터 상속받음)를 생성한 다음 오브젝트를 레퍼런스하는 변수를 null로 설정하면, 해당 스크립트와 Unity에 관한 오브젝트를 잃어버리게 됩니다. 해당 오브젝트에 접근하거나 다시 볼 수는 없지만 메모리에는 남아 있습니다.

시간이 지나면 가비지 컬렉터가 실행되어 메모리에 남아 있지만 어디서도 레퍼런스되지 않는 오브젝트를 제거합니다.

이렇게 할 수 있는 이유는 메모리의 각 블록에 대한 레퍼런스의 수가 내부적으로 계속 추적되고 있기 때문입니다.

스크립팅 언어가 C++보다 느린 이유 중 하나가 바로 이것 때문입니다.

 

  • 자동 메모리 관리 및 가비지 컬렉터를 읽어볼 수 있습니다.

5.2.3 메모리 할당을 피하는 방법

어떤 오브젝트가 생성될 때마다 메모리가 할당됩니다. 

하지만 코드에서는 대부분 이러한 사실을 인지하지 못한 채 오브젝트를 생성합니다.

 

  • Debug.Log("boo" + "hoo"); 는 오브젝트를 생성합니다. 많은 양의 문자열을 다룰 때는 "" 대신 System.String.Empty을 사용해야 합니다.
  • 즉시 모드 GUI(UnityGUI)는 느리므로 성능이 문제가 될 때에는 절대 사용해서는 안 됩니다.
  • 클래스와 구조체의 차이:
    클래스는 오브젝트이며 레퍼런스로서 작동합니다. Foo가 클래스이고 코드가 다음과 같다면
Foo foo = new Foo(); 
MyFunction(foo); 

 
MyFunction은 힙에 할당된 원본 Foo 오브젝트를 가리키는 레퍼런스를 넘겨받게 됩니다. 

MyFunction에서 foo에 변경이 가해질 경우 foo가 레퍼런스되는 모든 곳에서 이 변경된 내용을 볼 수 있게 됩니다.

클래스는 데이터이며 데이터로서 작용합니다. Foo가 구조체이고 코드가 다음과 같다면

 

Foo foo = new Foo(); 
MyFunction(foo); 

  
MyFunction은 foo의 복사본을 넘겨받게 됩니다. foo는 절대로 힙에 할당되지 않으며 가비지 컬렉션의 대상이 되지도 않습니다. 

MyFunction이 넘겨받은 foo 복사본을 수정한다고 하더라도, 다른 foo에는 영향을 주지 않습니다.

  • 장시간 유지되어야 하는 오브젝트는 클래스여야 하고, 단시간만 사용할 오브젝트는 구조체여야 합니다. Vector3가 아마도 가장 유명한 구조체일 것입니다. Vector3가 클래스였다면 모든 것이 훨씬 느렸을 것입니다.

5.2.4 오브젝트 풀링이 더 빠른 이유

결론적으로 인스턴스화 및 제거를 여러 차례 사용하면 가비지 컬렉터가 해야 할 일이 많아집니다

그리고 이렇게 하면 게임플레이에 “장애”가 생기게 됩니다. 

자동 메모리 관리 페이지에서 설명하듯이, 인스턴스화 및 제거와 관련된 공통 성능 장애 요소를 피할 수 있는 다른 방법이 있습니다. 

예를 들어 아무 일도 진행되지 않을 때 가비지 컬렉터를 수동으로 작동시키는 방법이 있고, 또는 가비지 컬렉터를 매우 자주 작동시켜서, 사용하지 않는 메모리가 대량으로 쌓이지 않도록 방지하는 방법도 있습니다.

또 다른 이유는, 특정 프리팹이 최초로 인스턴스화될 때 때로는 RAM에 추가적인 것이 로드되어야 하며, GPU에 텍스처 및 메시가 업로드되어야 한다는 점입니다.

또한 장애를 유발할 수 있으며, 오브젝트 풀링을 사용하면 게임플레이 도중이 아니라 레벨 로드 중에 일이 이루어집니다.

인형을 조종하는 캐릭터가 하나 있고, 캐릭터가 꼭두각시 인형이 무한대로 나오는 상자를 들고 있다고 상상해 봅시다. 

스크립트에서 캐릭터가 나타나도록 할 때마다 캐릭터는 상자에서 새 인형을 꺼냅니다. 

그리고 캐릭터가 무대를 떠날 때마다 캐릭터는 현재의 인형을 던집니다. 

오브젝트 풀링을 사용하면, 쇼가 시작되기 전에 모든 인형을 즉시 꺼내는 것과 인형이 보이면 안 되는 순간마다 인형을 무대 뒤의 탁자에 놓아두는 것과 같습니다.

5.2.5 오브젝트 풀링이 느린 이유

한 가지 문제는 풀을 생성하면 다른 목적으로 사용할 가용 힙 메모리의 양이 줄어든다는 점입니다. 

따라서 현재 막 생성한 풀 외에도 메모리를 계속 할당한다면, 가비지 컬렉션이 더욱 자주 실행될 수 있습니다. 

뿐만 아니라 가비지 컬렉션에 걸리는 시간은 살아있는 오브젝트의 수에 비례하여 증가하기 때문에 매번 더 느려질 수 있습니다. 이 문제를 고려해 볼 때, 너무 큰 풀을 할당하거나 또는 풀에 있는 오브젝트가 한동안 필요가 없는 상황에서 풀을 활성화하여 유지한다면 성능에 지장이 생기게 됩니다. 

게다가, 오브젝트 중에는 오브젝트 풀링에 비협조적인 오브젝트 타입이 여러 가지 있습니다. 

예를 들어, 상당한 시간 동안 지속되는 주문 효과를 포함하는 게임, 또는 많은 수의 적이 나타나지만 게임 진행에 따라 서서히 죽는 게임이 있을 수 있습니다.

이러한 경우 오브젝트 풀의 성능 오버헤드는 다른 이점을 뛰어넘기 때문에, 오브젝트 풀을 사용해서는 안 됩니다.

5.2.6 구현

다음과 같이 간단한 발사체용의 스크립트를 나란히 비교해 보겠습니다. 

하나는 인스턴스화를, 하나는 오브젝트 풀링을 사용합니다.

 

// GunWithInstantiate.js

  #pragma strict

  var prefab : ProjectileWithInstantiate;

  var power = 10.0;















  function Update () {
      if(Input.GetButtonDown("Fire1")) {
          var instance : ProjectileWithInstantiate = 
              Instantiate(prefab, transform.position, transform.rotation);
          instance.velocity = transform.forward * power;
      }
  }












  // ProjectileWithInstantiate.js

  #pragma strict

  var gravity = 10.0;
  var drag = 0.01;
  var lifetime = 10.0;

  var velocity : Vector3;

  private var timer = 0.0;









  function Update () {
      velocity -= velocity * drag * Time.deltaTime;
      velocity -= Vector3.up * gravity * Time.deltaTime;
      transform.position += velocity * Time.deltaTime;

      timer += Time.deltaTime;
      if(timer > lifetime) {

          Destroy(gameObject);
      }
  }
// GunWithObjectPooling.js

#pragma strict

var prefab : ProjectileWithObjectPooling;
var maximumInstanceCount = 10;
var power = 10.0;

private var instances : ProjectileWithObjectPooling[];

static var stackPosition = Vector3(-9999, -9999, -9999);

function Start () {
    instances = new ProjectileWithObjectPooling[maximumInstanceCount];
    for(var i = 0; i < maximumInstanceCount; i++) {
        // place the pile of unused objects somewhere far off the map
        instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity);
        // disable by default, these objects are not active yet.
        instances[i].enabled = false;
    }
}

function Update () {
    if(Input.GetButtonDown("Fire1")) {
          var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance();
          if(instance != null) {
          instance.Initialize(transform, power);
          }
    }
}

function GetNextAvailiableInstance () : ProjectileWithObjectPooling {
    for(var i = 0; i < maximumInstanceCount; i++) {
          if(!instances[i].enabled) return instances[i];
    }
    return null;
}




// ProjectileWithObjectPooling.js

#pragma strict

var gravity = 10.0;
var drag = 0.01;
var lifetime = 10.0;

var velocity : Vector3;

private var timer = 0.0;

function Initialize(parent : Transform, speed : float) {
     transform.position = parent.position;
     transform.rotation = parent.rotation;
     velocity = parent.forward * speed;
     timer = 0;
     enabled = true;
}

function Update () {
     velocity -= velocity * drag * Time.deltaTime;
     velocity -= Vector3.up * gravity * Time.deltaTime;
     transform.position += velocity * Time.deltaTime;

     timer += Time.deltaTime;
     if(timer > lifetime) {
         transform.position = GunWithObjectPooling.stackPosition;
         enabled = false;
     }
}

 

크고 복잡한 게임이라면 모든 프리팹에서 동작하는 일반적인 해결책을 원할 것입니다.

5.3 다른 예제: 코인 파티

“수집 가능한 수백 개의 동전이 회전하면서 동적으로 빛나고 화면에 동시에 나타나는 것”의 예제가 스크립트 방법론 섹션에서 제공되었습니다. 

예제를 통해 스크립트 코드, 파티클 시스템과 같은 Unity 컴포넌트, 커스텀 셰이더를 사용하여 저사양 모바일 하드웨어에 부담을 주지 않으면서도 놀라울 정도로 멋진 효과를 만들어내는 방법을 보여줄 것입니다.

수많은 동전이 떨어지고 튕기고 회전하는 2D 횡스크롤 게임에서 효과가 나타난다고 상상해 봅시다. 

동전은 점 광원에 의해 동적으로 빛납니다. 

게임을 훨씬 인상적으로 만들기 위해 동전을 반짝이게 하는 광원을 만들어내려 할 것입니다.

하드웨어가 고성능이라면 이 문제를 해결하는 표준 접근 방식을 취할 수 있을 것입니다. 

모든 동전을 오브젝트로 만들고, 버텍스 릿, 포워드 또는 디퍼드 라이팅 중에 하나를 가지고 셰이드하고, 이미지 이펙트로 상단에 글로우 효과를 추가해서 밝게 반사하는 동전이 주변에 광원을 뿌리도록 하면 됩니다.

그러나 모바일 하드웨어에서 이렇게 많은 오브젝트를 만들면 크게 부담이 되며, 글로우 효과는 거의 불가능합니다. 

그러면 어떻게 해야 합니까?

 

 

5.3.1 애니메이션 스프라이트 파티클 시스템

모두 비슷한 방식으로 움직이며 플레이어가 주의 깊게 살펴볼 일이 절대로 없는 오브젝트를 여러 개 표시하려면, 파티클 시스템을 사용하여 순식간에 대량의 오브젝트를 렌더링할 수도 있습니다. 

이러한 기법을 적용할 수 있는 전형적인 몇 가지 사례는 다음과 같습니다.

  • 수집품 또는 동전
  • 날아가는 잔해
  • 단순한 적의 무리나 떼
  • 환호하는 관중
  • 수백 개의 발사체나 폭발

애니메이션 스프라이트 파티클 시스템의 생성을 돕는 스프라이트 패커 무료 에디터 확장 프로그램이 있습니다. 

스프라이트 패커는 오브젝트의 프레임을 텍스처에 렌더링하며, 그 후 이를 파티클 시스템에서 애니메이션 스프라이트 시트로 사용할 수 있습니다. 

예제에서는 회전하는 동전에 스프라이트 패커를 사용할 것입니다.

 

5.3.2 레퍼런스 구현

스프라이트 패커 프로젝트에는 바로 이 문제를 해결하기 위한 해결책을 보여주는 예제가 포함되어 있습니다.

컴퓨팅 성능이 낮은 상황에서 눈부신 효과를 얻기 위해 여러 종류의 에셋을 사용합니다.

  • 컨트롤 스크립트
  • 스프라이트 패커의 결과물에서 생성된 특화 텍스처
  • 컨트롤 스크립트 및 해당 텍스처와 직접 연결된 특화 셰이더

예제에는 readme 파일이 포함되어 있습니다. 

파일은 시스템이 동작하는 방법과 원리를 설명하고, 필요한 기능 및 구현 방법을 결정하는 데 사용된 프로세스를 서술합니다. 

이것이 해당 파일입니다.

여기서의 문제는 “화면 속에서 수백 개의 동전이 회전하고 동적으로 빛나며 수집 가능한 상태를 한번에 구현”하는 것이라고 정의할 수 있습니다.

단순히 접근하자면 동전 프리팹을 여러 개 인스턴스화하면 되겠지만, 그 대신 파티클을 사용하여 동전을 렌더링하려 합니다. 그러나 이를 위해서는 넘어야 할 여러 과제가 있습니다.

  • 파티클에는 시야 각도가 없기 때문에 이 점이 문제가 됩니다.
    카메라는 오른쪽을 위로 간주하고 동전은 Y축 주변을 돈다고 간주합니다.
    스프라이트 패커를 사용하여 묶은 애니메이션 텍스처를 가지고 동전이 회전하는 것처럼 보이는 효과를 만들어냅니다.
    이렇게 하면 새로운 문제가 발생합니다. 모든 동전이 동일한 속도, 같은 방향으로 회전하므로 단조로워집니다.
    이 현상을 고치기 위해, 회전과 수명 주기를 직접 추적하고 파티클 수명 주기에 맞춰 회전을 “렌더링”하는 방식을 사용합니다.
  • 실시간 조명을 사용해야하지만 파티클에는 노멀이 없기 때문에 문제가 생깁니다.
    스프라이트 패커에 의해 만들어진 각 애니메이션 프레임마다 동전 표면에 단일 노멀 벡터를 생성합니다.
    위 리스트에서 얻어낸 노멀 벡터에 기반하여, 스크립트의 각 파티클에 대해 블린-퐁 라이팅을 적용합니다.
    결과물을 컬러로 파티클에 적용합니다.
    동전 표면과 동전 가장자리 부분을 셰이더에서 별도로 처리합니다. 새로운 문제가 발생합니다: 어디가 가장자리인지, 그리고 가장자리의 어느 위치에 위치해야 하는지 셰이더가 어떻게 알 수 있을까요?
    UV는 이미 애니메이션에 사용되기 때문에 여기에 사용할 수 없습니다.
    텍스처 맵을 사용합니다.
    동전에 상대적인 Y 포지션이 필요합니다.
    “동전 표면 위” vs “가장자리 위”에 바이너리가 필요합니다.
    텍스처를 추가로 도입하면 읽어야 하는 텍스처가 많아지고 텍스처 메모리도 많아지기 때문에 이 방법은 피하려 합니다.
    필요한 정보를 하나의 채널로 합쳐서 텍스처 컬러 채널 중에 하나를 정보 채널로 대체합니다.
    이제는 동전 컬러가 잘못되어 버렸습니다. 어떻게 하면 될까요?
    남아 있는 두 개의 채널을 합쳐서 사라진 채널을 다시 복구할 수 있도록 셰이더를 사용합니다.
  • 동전의 반짝이는 광원으로부터 글로우 효과를 얻고자 한다고 생각해 봅시다. 포스트 프로세싱은 모바일 디바이스에서는 너무 비용이 많이 듭니다.
    파티클 시스템을 하나 더 만들어서, 부드럽고 반짝이는 효과를 내는 동전 애니메이션을 만듭니다.
    해당하는 동전의 컬러가 아주 밝은 경우에만 글로우 효과를 입힙니다.
    매 프레임마다 모든 동전에 글로우 효과를 렌더링하면 필레이트가 너무 떨어져 버리므로 이렇게는 처리할 수 없습니다.
    밝기가 0보다 큰 포지션의 경우에만 글로우 효과를 매 프레임마다 초기화합니다.
  • 파티클이 잘 충돌하지 않기 때문에 물리를 구현하고 동전을 모으는 것이 문제가 됩니다.
    빌트인 파티클 충돌을 사용할 수 있습니까?
    그 대신, 충돌을 스크립트로 작성합니다.
  • 마지막으로 문제가 하나 더 남았습니다. 스크립트가 하는 일이 너무 많아서 느려져 버렸습니다.
    퍼포먼스는 활성화된 동전의 수에 비례하여 변합니다.
    최대 동전 수를 제한합니다. 목표를 충분히 달성할 수 있을 정도로 효과적인 100개의 동전, 2개의 광원을 적용하자, 모바일 디바이스에서 매우 빨리 실행됩니다.
  • 다음과 같이 좀더 최적화할 수 있습니다.
    모든 동전에 각각 광원을 계산하는 대신, 월드를 몇 부분으로 나누어서 각 부분에서의 모든 회전 프레임에 대한 광원 조건을 산출합니다.
    - 동전의 포지션과 동전 회전을 인덱스로 하는 룩업 테이블을 사용합니다.
    - 포지션에 대한 쌍선형 보간을 사용하여 정확도를 향상시킵니다.
    - 룩업 테이블 업데이트를 최소한으로 하거나, 아예 정적 룩업 테이블을 사용합니다.
    - 라이트 프로브를 사용합니까? *스크립트에서 광원을 계산하는 대신, 노멀맵 파티클을 사용합니까?
    - 노멀의 프레임 애니메이션을 베이크하기 위해 “디스플레이 노멀” 셰이더를 사용합니다.
    - 광원의 수를 제한합니다.
    - 느린 스크립트 문제를 해결합니다.

예제의 최종 목표 또는 “이야기의 교훈”은, 게임에 정말 필요한 요소가 있으나 기존의 방식으로 요소를 성취하고자 할 때 지연이 발생한다면, 할 수 없다는 의미가 아니라 그저 더 빨리 실행될 수 있도록 시스템에 조작을 더 가해야 합니다.

5.3.3 수천 개의 오브젝트를 관리하는 기법

수백 개 또는 수천 개의 동적 오브젝트가 있는 상황에 적용할 수 있는 특정한 스크립트 최적화 기법이 있습니다. 

이 기법을 게임에서 모든 스크립트에 적용하겠다는 것은 끔찍한 생각입니다. 

기법은 런타임에 굉장히 많은 수의 오브젝트를 다루는 대규모 스크립트용 툴 및 디자인 가이드라인에 사용해야 합니다.

대규모 데이터 세트에서 O(n2) 연산 방지 또는 최소화

컴퓨터학에서 연산의 순서는 O(n)으로 표기하며, 어떤 연산이 적용되는 오브젝트의 수(n)가 증가함에 따라 연산이 계산되어야 하는 횟수가 증가하는 방식을 나타냅니다.

예를 들어, 기본 정렬 알고리즘을 생각해 봅시다. n개의 숫자가 있고 이를 오름차순으로 정렬하고자 합니다.

 

void sort(int[] arr) {
    int i, j, newValue;
    for (i = 1; i < arr.Length; i++) {
        // record
        newValue = arr[i];
        //shift everything that is larger to the right
        j = i;
        while (j > 0 && arr[j - 1] > newValue) {
            arr[j] = arr[j - 1];
            j--;
        }
        // place recorded value to the left of large values
        arr[j] = newValue;
    }
  }

 

중요한 점은 이 때 두 개의 루프가 필요하며, 한 루프가 다른 루프 안에 중첩된다는 점입니다.

 

 for (i = 1; i < arr.Length; i++) {
    ...
    j = i;
    while (j > 0 && arr[j - 1] > newValue) {
        ...
        j--;
    }
  }

 

알고리즘을 실행할 때 가장 최악의 경우를 상정해 봅시다.

바로 내림차순으로 정렬된 숫자가 입력된 경우입니다.

이 경우, 안쪽 루프는 j회 실행됩니다. i가 1에서 arr.Length–1까지 변할 때, j는 평균적으로 arr.Length/2가 됩니다. 

이 경우에는 O(n)에 있어서 arr.Length가 바로 n이므로, 전체적으로 내부 루프는 *nn/2회, 또는 n1/2회 돌게 됩니다. 

그러나 O(n)을 논할 때 1/2와 같은 상수는 제외합니다. 

여기서는 실제 연산 횟수를 따지자는 것이 아니라, 연산 횟수가 어떤 식으로 증가하는지에 대해 논의하고자 하는 것이기 때문입니다. 

따라서 알고리즘은 O(n1)**입니다. 

데이터 세트가 큰 경우 연산 횟수가 기하급수적으로 증가할 수 있기 때문에 연산의 차수는 매우 중요한 의미를 가집니다.

게임에서 O(n2) 연산이 일어날 수 있는 예로, 100명의 적이 있고 각각의 적 AI가 다른 모든 적의 움직임을 고려하는 상황이 있습니다. 

아마도 더 빠른 방법은 맵을 셀로 나누고, 각 적의 움직임을 최근접 셀로 기록하고, 가장 근접한 몇 개의 셀을 각 적이 샘플링하도록 합니다. 이렇게 하면 O(n) 연산이 됩니다.

불필요한 검색 대신 캐시 레퍼런스 사용

게임에 100명의 적이 있고 모두 플레이어를 향해 움직인다고 생각해 봅시다.

 

// EnemyAI.js
  var speed = 5.0;
 
  function Update () {
    transform.LookAt(GameObject.FindWithTag("Player").transform);
    // this would be even worse:
    //transform.LookAt(FindObjectOfType(Player).transform);
 
    transform.position += transform.forward * speed * Time.deltaTime;
  }

 

동시에 달려드는 적의 수가 충분히 많으면 느려질 수도 있습니다. 알려져 있지 않은 사실은, MonoBehaviour(transform, renderer, audio 등)에 있는 모든 컴포넌트 액세서는 각자 해당되는 GetComponent(Transform)와 동일한 동작을 하며 실제로는 약간 더 느리다는 점입니다.

GameObject.FindWithTag는 최적화되어 왔으나, 일부 경우에는(예를 들어 내부 루프에서 또는 많은 수의 인스턴스를 실행하는 스크립트에서는) 스크립트가 조금 느릴 수도 있습니다.

더 개선된 스크립트 버전은 다음과 같습니다.

 // EnemyAI.js
  var speed = 5.0;
 
  private var myTransform : Transform;
  private var playerTransform : Transform;
 
  function Start () {
    myTransform = transform;
    playerTransform = GameObject.FindWithTag("Player").transform;
  }
 
  function Update () {
    myTransform.LookAt(playerTransform);
 
    myTransform.position += myTransform.forward * speed * Time.deltaTime;
  }

 

비용이 큰 수학 함수 최소화
초월 함수(Mathf.Sin, Mathf.Pow 등), 나눗셈, 제곱근 연산 등은 곱셈 연산 대비 100배 정도의 시간을 소모합니다. 

거시적으로 보았을 때는 거의 시간이 걸리지 않으나, 이러한 함수를 프레임당 수천 번씩 호출한다면 누적 시간이 상당해집니다.

가장 일반적인 경우가 바로 벡터 정규화입니다. 

동일 벡터를 반복해서 계속 정규화하는 대신, 한 번만 정규화를 하고 그 결과를 추후 사용할 수 있도록 캐싱하는 것을 고려해 볼 수 있습니다.

벡터의 길이도 사용하고 정규화도 해야 한다면, .normalized 프로퍼티를 사용하는 것보다는 벡터에 길이의 역을 곱해서 정규화 벡터를 얻는 것이 더 빠릅니다.

거리를 비교한다면, 실제 거리를 비교할 필요가 없습니다. 

대신 .sqrMagnitude 프로퍼티를 사용하여 거리의 제곱을 비교하고 제곱근을 저장하면 됩니다.

또 다른 예제로 상수 c로 반복해서 나눗셈 연산을 해야 할 경우, 대신 그 역을 곱하면 됩니다. 

1.1.0/c 연산을 통해 역을 먼저 구해야 합니다.

Physics.Raycast() 같은 비용이 큰 연산은 최대한 지양

비용이 크게 드는 처리를 해야 할 경우, 처리를 덜 하는 방향으로 최적화하고 결과를 캐싱하는 방법이 있습니다. 예를 들어, 레이캐스트를 사용하는 발사체 스크립트를 생각해 봅시다.

 

 // Bullet.js
  var speed = 5.0;
 
  function FixedUpdate () {
    var distanceThisFrame = speed * Time.fixedDeltaTime;
    var hit : RaycastHit;
 
    // every frame, we cast a ray forward from where we are to where we will be next frame
    if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) {
        // Do hit
    } else {
        transform.position += transform.forward * distanceThisFrame;
    }
  }

 

즉시 개선할 수 있는 부분이 있습니다. 스크립트에서 FixedUpdate를 Update로, fixedDeltaTime을 deltaTime으로 대체합니다. 

FixedUpdate는 물리 업데이트를 나타내며, 프레임 업데이트보다 더 자주 일어납니다. 

더 나아가서, 레이캐스팅이 오직 매 n초마다 일어나도록 만들어 봅시다. 

n이 작아지면 일시적으로 해상도가 좋아지며, n이 커지면 성능이 향상됩니다. 

타겟이 크고 느릴수록, 일시적인 앨리어싱이 발생하기 전까지 n 값을 크게 할 수 있습니다(플레이어가 타겟을 맞춘 지점에서 지연이 나타나지만 폭발은 n초 전에 타겟이 있던 곳에 일어나거나, 또는 플레이어가 타겟을 맞추지만 발사체가 뚫고 지나갑니다).

 

// BulletOptimized.js
  var speed = 5.0;
  var interval = 0.4; // this is 'n', in seconds.
 
  private var begin : Vector3;
  private var timer = 0.0;
  private var hasHit = false;
  private var timeTillImpact = 0.0;
  private var hit : RaycastHit;
 
  // set up initial interval
  function Start () {
    begin = transform.position;
    timer = interval+1;
  }
 
  function Update () {
    // don't allow an interval smaller than the frame.
    var usedInterval = interval;
    if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime;
 
    // every interval, we cast a ray forward from where we were at the start of this interval
    // to where we will be at the start of the next interval
    if(!hasHit && timer >= usedInterval) {
        timer = 0;
        var distanceThisInterval = speed * usedInterval;
 
        if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) {
            hasHit = true;
            if(speed != 0) timeTillImpact = hit.distance / speed;
        }
 
        begin += transform.forward * distanceThisInterval;
    }
 
    timer += Time.deltaTime;
 
    // after the Raycast hit something, wait until the bullet has traveled
    // about as far as the ray traveled to do the actual hit
    if(hasHit && timer > timeTillImpact) {
        // Do hit
    } else {
        transform.position += transform.forward * speed * Time.deltaTime;
    }
  }

 

내부 루프에서 콜스택 오버헤드 최소화

함수를 호출하는 것만으로도 그 자체에서 약간의 오버헤드가 발생합니다. 

x = Mathf.Abs(x)와 같은 함수를 프레임당 수천 번씩 호출한다면, x = (x > 0 ? x : -x);와 같이 처리하는 편이 더 낫습니다.

5.4 물리 성능 최적화

Unity가 사용하는 NVIDIA PhysX 물리 엔진은 모바일에서도 사용 가능하지만, 모바일 플랫폼에서는 데스크톱에 비해 하드웨어의 성능 제한에 도달하기가 더 쉽습니다.

물리 엔진을 튜닝하여 모바일에서 더 나은 성능을 얻기 위한 몇 가지 팁을 살펴보겠습니다.

  • Time 창의 Fixed Timestep 설정을 조정하여 물리 업데이트에 드는 시간을 줄일 수 있습니다. 타임스텝을 늘리면 물리 정확도가 떨어지는 대신 CPU 오버헤드가 줄어듭니다. 때로는 정확도를 낮추어 속도 향상을 도모하는 것이 더 나은 경우도 있습니다.
  • Time 창의 Maximum Allowed Timestep을 8–10fps 범위로 설정하여 최악의 시나리오에서 물리 연산에 드는 시간을 제한해야 합니다.
  • 메시 콜라이더는 기본 콜라이더에 비해 퍼포먼스 오버헤드가 훨씬 큽니다. 그러므로 꼭 필요한 경우에만 사용해야 합니다. 기본 콜라이더를 포함하는 자식 오브젝트를 사용하여 대략적으로 메시 형태를 만들 수도 있습니다. 부모 리지드바디는 자식 콜라이더를 단일 복합 콜라이더로 한꺼번에 제어합니다.
  • 휠 콜라이더는 솔리드 오브젝트라는 점에서 엄밀히 콜라이더는 아니지만 높은 CPU 오버헤드를 유발합니다. 
728x90
반응형