Wednesday, December 3, 2014

[Libgdx] 크로스 플랫폼 게임 개발 - 두 번째 이야기

Libgdx 두 번째 이야기에서는 프로젝트를 시작할 수 있도록 해주는 '기본 뼈대가 되는 프로젝트'를 생성해 보려고 합니다. 너무도 간단한 과정만 거치면, 다양한 플랫폼을 타겟으로 하는 기본 프로젝트들을 쉽게 생성할 수 있고, 여러분은 그 프로젝트 내에 여러분이 구상하는 모든 것을 작성해 넣으시면 됩니다.





당장 프로젝트를 생성해 봅시다


먼저 여러분이 개발하실 컴퓨터에 모든 Libgdx에 필요한 것들이 설치되었는지 확인해봐야겠죠? [Libgdx] 크로스 플랫폼 게임 개발 - 첫 번째 이야기를 읽고 오셨다면 아무런 문제가 없을 거예요. 혹시 준비가 안되셨다면 얼마 안되니 첫 번째 이야기를 읽고 오시는 게 좋습니다.

Libgdx는 빌드 프로세스를 처리하기 위해 Gradle을 이용합니다. Gradle은 오픈 소스 빌드 자동화 툴로, Apache Ant나 Apache Maven과 아주 비슷하죠. Gradle은 여러분이 작성하는 프로젝트의 의존성(dependency)를 관리하면서 필요한 경우엔 외부의 라이브러리도 다운로드 합니다. 여러분은 그냥 여러분의 프로젝트에 필요한 것들만 선언해 주시면 되요.

다행스럽게도, Libgdx 프로젝트를 시작한다고 Gradle에 대해서 깊게 공부해야 하는 것은 아닙니다. 여러분이 지금 공부하고 사용할 프레임워크에는 여러분이 사용할 기본적인 것들 모두가 포함된, 뼈대로 사용될 애플리케이션(skeleton application)을 만들어주는 툴이 딸려 있거든요. 바로 Gdx-setup Tool 이라는 것입니다.

gdx-setup tool은 아주 간단한 유저 인터페이스로 구성되어 있습니다. 명령어 라인 옵션도 제공되지요. 여러분이 편한 방법을 선택하시면 됩니다. 하지만, 아무래도 유저 인터페이스가 있는게 편하겠죠? 먼저 그걸 살펴보도록 할께요.


Libgdx 프로젝트 셋업 툴을 사용하기

1. 먼저 최신 버전의 툴을 다운로드 합니다. libgdx 공식 홈페이지 첫 화면에서도 다운로드할 수있고,  nightlies 버전을 다운로드 하실 수도 있어요. 편하신데로 하면 됩니다.  http://libgdx.badlogicgames.com/download.html

2. .jar 파일을 실행합니다. 그럼 아래 그림과 같은 화면이 보일 거예요. 그리고, 입력 항목에 맞는 정보들을 입력해줍니다.  순서대로 프로젝트 폴더 이름, 자바 패키지 이름, 게임의 시작이 될 클래스 이름, 실제 프로젝트가 위치할 경로, 그리고 안드로이드 SDK위치를 입력해주시면 되요.

3. Sub Proejcts는 모두 체크가 되어 있을텐데요. 아래 그림처럼 저는 Html은 체크를 해제했습니다. 필요가 없을거 같아서요.

4. Advanced 버튼을 클릭하시면 오른쪽 그림과 같은 창이 뜨는데, 여러분이 사용하시는 IDE에 맞게 체크하시면 됩니다.

5. 자 모두 준비가 되었으면 Generate 버튼을 클릭해서 프로젝트를 생성해 볼까요?아래의 여백에 빌드과정이 상세히 표시되고, 얼마 시간이 지나지 않아 완료될 것입니다.



6. 생성된 프로젝트들을 이클립스에 import할 차례입니다. Package Explorer에서 오른쪽 마우스 클릭하신 후에 Import ,  Gradle탭 내의 Gradle project를 차례로 선택하세요. 그리고, 아래 그림처럼 창이 나타나면, 프로젝트를 생성한 폴더를 지정해 주시면 됩니다.

그리고, Build Model 버튼을 클릭하시면 프로젝트 목록이 뜰거예요. 그럼 아래처럼 선택해 주세요. 완료되면 Finish를 툴러 import를 마무리 하시면 됩니다.



7. [Libgdx] 크로스 플랫폼 게임 개발 - 첫 번째 이야기에서 처럼 각각의 프로젝트를 Run Configuration을 하나하나 설정해가며 실행시켜보시기 바랍니다. 아래 그림처럼 실행이 될 거예요. 아무 문제 없이 실행이 되나요?




여러분이 새롭게 생성하고 실행시켜본 Libgdx 프로젝트는 실행시켜서 본 것처럼, 각각의 플랫폼에서 제대로 동작하는 프로젝트입니다. Gradle은 이 과정에서 의존성 문제를 해결해 주고, 필요한 라이브러리가 있으면 다운로드 해주고, 컴파일 과정도 관리해주죠. 물론, 처음 빌드하는 과정은 다소 시간이 걸리기도 합니다. 하지만, 그 다음부턴 훨씬 나아지죠.




만들어진 프로젝트를 살펴보기

이제 Libgdx 프로젝트의 구조를 살펴보도록 하죠. 타겟이 되는 플랫폼 마다 하나씩 프로젝트가 별도로 생성되었고 거기에 core 프로젝트가 하나 더 있네요. 여기서 core 프로젝트는 여러분의 게임에 들어갈 실제 로직들이 포함됩니다. 플랫폼에 종속된 다른 프로젝트들은 런처(launcher)만을 포함하고 있을 뿐이죠.

Gradle은 여러개의 프로젝트를 관리하는 솔루션으로 제격인데요. Ant나 Maven처럼, XML이 아닌 DSL(Domain Specific Language)를 이용해서 타겟(target)은 물론 의존성까지 정의합니다. 여러분이 프로젝트를 빌드해달라고 하면 Gradle은 build.gradle 파일을 이용해서 의존성 관계를 표시하는 '방향성 비싸이클 그래프(Directed Acyclic Graph, 방향성을 가지면서도 싸이클이 존재하지는 않는 그래프 )'를 그리게 되고, 올바른 순서로 의존성을 구축합니다. 실제 Libgdx에 의해 만들어지는, 뼈대가 되는 프로젝트간의 의존성은 아래와 같은 그래프로 표현될 수 있을 것입니다.



Gradle은 개발자나 개발자가 처한 다양한 환경에 맞게 세세하게 설정할 수도 있는데요. Gradle에 대한 자세한 사항은 아래 링크에서 참조하도록 하세요.
http://www.gradle.org/docs/current/userguide/userguide_single.html





프로젝트 구조와 애플리케이션 라이프사이클


이후의 설명은 Libgdx의 전형적인 프로젝트 구조에 대한 것이예요. 이것을 기억하고 있다면 크로스 플랫폼 개발을 좀 더 수월하게 할 수 있도록 도울거라 생각합니다. 약간은 지겨운 이야기가 될 수도 있겠네요

그리고, 해상도, 색상, OpenGL 버전 등과 같은 파라메터 값들을 조정하기 위해서 특정 플랫폼에 대한 런처를 설정하는 방법도 보게 될 것입니다.

또한, Libgdx 애플리케이션의 라이프 사이클(life cycle)도 알아볼 거예요. 보통 라이프사이클이라 하면 쉽게 지나치는 경우가 많습니다. 하지만, 라이프 사이클이 바로 여러분이 만들 게임의 가장 핵심 부분이거든요. 새겨둘만한 충분한 가치가 있는 것이니 꼼꼼히 읽어보고 기억하는게 필요해요.

그럼, 바로 전에 생성한 프로젝트들을 다시한번 볼까요?

그림에서 보는 것처럼, Libgdx 애플리케이션은 몇 개의 개별 프로젝트로 나뉩니다. core, desktop, Android, iOS 이렇게 총 네 개의 프로젝트로 이루어져있고요.(마지막에 보이는 Tutorial 프로젝트는 Gradle등 Libgdx 애플리케이션 개발을 위한  일반적인 설정정보를 확인하기 위해서 import 시킨 것이예요. HTML 프로젝트는 빼버린거 기억하시죠?)

-android, -ios, -desktop 처럼 플랫폼의 종류가 명시된 것들은 그 플랫폼에서 실행하기 위한 진입점(entry point)의 역할을 합니다. 이들의 임무는 각각의 플랫폼에서 런처를 이용해서 애플리케이션을 실행하면,  -core 프로젝트의 메인클래스를 호출함과 동시에, 게임의 실행을 위한 기본적인 설정 파라메터값들을 전달하는 것이죠.

정리해보자면, core프로젝트에는 여러분이 만들고자 하는 게임이나 애플리케이션의 실제 구현이 자리잡게 되는 것이고(여러분이 core 프로젝트에 구현하게 된다는거죠), 다른 플랫폼별 프로젝트에는, 그 플랫폼에서 게임이나 애플리케이션을 실행하기 위한 준비만을 담당하게 된다는 겁니다. 게다가, 필요한 경우, 게임에서 사용할 리소스도 서로 '공유'합니다.

이렇게 각각의 프로젝트가 서로 공유하고, 참조하기 때문에 Libgdx 프로젝트의 구조에 대한 이해가 필요한 것입니다.

타겟 플랫폼을 안드로이드만 한다면, (좌측 그림처럼 플랫폼별 런처와 게임 로직들을 분리한 형태가 아닌) 플랫폼에 영향을 받지 않던 리소스와 안드로이드에 특정된 파일들로만 채워진 단 하나의 프로젝트로 처리해도 아무런 문제는 없을 거예요. 하지만, 이것은 좋지 않은 방법입니다. 나중에 다른 플랫폼에 적용할 필요가 생긴다면 아주 난감해지겠죠?  어느 누가 프로젝트 구조를 새로운 환경에 맞게 다시 작성해야하는 작업을 좋아라 하겠어요.

어떤 플랫폼이나 디바이스에서 작업을 하든, 앞으로 어떤 상황이 될지 모르니,  가능하다면 분리하는 게 좋아요



LibGDX - Life Cycle

플랫폼에 특화된 프로젝트들(-android, -ios, -desktop 등)을 살펴보기이전에, 실제 게임 로직과 관련된 부분부터 살펴볼께요. 명심하세요. 실제 여러분이 작성할 '게임 로직'과 관련된 부분입니다. 플랫폼과 관련된 부분이 아닙니다.

모든 Libgdx 애플리케이션은 아주 잘 정의된 라이프사이클을 가지고 있어요. 주어진 순간에 애플리케이션의 상태(state)를 제어하기 위해서죠.

애플리케이션은 "생성되고(creation), 일시적으로 멈추고(pausing), 다시 시작하게되고(resuming), 화면에 표현하고(rendering), 소멸되는(disposing)" 다양한 상태로 존재하고, 또 변화합니다.

그리고, 이러한 라이프사이클은 ApplicationListener 인터페이스에 선언되있으며, 게임 로직의 시작점(플랫폼별 런처의 시작점이 아니예요)이 되게 하려면 이 인터페이스를 반드시 구현해줘야 하죠.

core 프로젝트의 FirstExample 클래스가 바로 그것입니다. 이제 곧 이 클래스(FirstExample.java)를 살펴볼 것입니다

그럼 먼저 API를 통해 ApplicationListener 인터페이스를 살펴볼까요? Libgdx API (link)에서 com.badlogic.gdx 패키지에 있는 AppliacationListener 인터페이스를 찾으시면 됩니다.

Libgdx API에 설명된 것처럼, create()은 Application이 처음 생성될 때, dispose()는 Application이 소멸될 때, pause()는 Application이 비활성화되거나 보이지 않게 될 때, render()는 새로 Application이 렌터링이 되어야 할 때, resize()는 Application의 크기가 다시 설정되어야 할 때, resume()은 Application이 다시 일시멈춤 상태에서 다시 시작할 때(포커스를 다시 갖게 될 때) 호출이 됩니다.


여기서 Application이라는 말이 항상 등장하는데요. com.badlogic.gdx.Application 인터페이스이며, 위 설명에서의 Application은 Application 인터페이스를 구현한 타입을 일컫습니다. API에서 com.badlogic.gdx.Application을 다시 찾아보시겠어요?


이 인터페이스를 구현한 클래스가 GwtApplication, IOSApplication, LwjglApplication 등이 있네요. 여기에 나열되진 않았지만, AndroidApplication이라는 클래스도 이 인터페이스를 구현한 클래스입니다. 이 클래스들이 바로 플랫폼 별 런처(launcher)와 직접적인 연결이 되는 것들입니다. AndroidApplication 클래스를 API에서 찾아 보시면 설명이 약간 부족합니다. Application 인터페이스를 구현하고 있는 클래스라는 설명만 있지 실제 어떤식으로 구현하고 있는지에 대해선 설명이 없거든요. 그래서 짤막하게 말씀드리고 넘어갈께요.

com.badlogic.gdx.backends.android.AndroidApplication 클래스는, 안드로이드의 Activity 클래스를 확장하면서 같은 패키지 내의 AndroidApplicationBase 인터페이스를 구현하는 클래스입니다. 그리고, AndroidApplicationBase인터페이스는 Application 인터페이스를 확장한 인터페이스고요.  API를 통해 확인하시면 좋겠지만, 위의 API 링크를 따라가 확인하시면 com.badlogic.gdx.backends.android 패키지의 많은 클래스와 인터페이스들이 누락되어 있습니다. 이클립스 내에서 해당 정보를 확인하셔야 할 거예요.


자, 다시 라이프사이클로 돌아갑시다.

애플리케이션 개발자 입장에서 라이프사이클에 해당하는 메소드를 보면, 애플리케이션의 상태가 변화할 때마다 그 메소드들이 호출되므로 다음과 같은 일들을 처리하는 데 유용합니다.

  • create() : 애플리케이션의 서브시스템을 초기화하고 리소스를 로드(load)하는 데 사용됩니다.
  • resize() : 새로운 스크린 사이즈를 세팅하는데 사용됩니다. UI를 구성하는 엘리먼트를 재배열하거나 카메라 객체를 재설정할 때 사용되죠.
  • render() : 게임 엘리먼트를 업데이트 하거나 렌더링하는데 사용됩니다. update() 메소드가 없다는 걸 기억하셔야 해요. render()가 모두 처리합니다.
  • pause() :  애플리케이션이 포커스를 잃었을 때(전화가 오거나 해서) 게임 상태 정보들을 저장하기 위해 사용됩니다. (개발자가 원하지 않는다면 )실제 게임 플레이가 꼭 멈춤 상태가 되어야 하는 것은 아닙니다.
  • resume() : 일시 멈춤 상태에서 되돌아 올 때 저장해 두었던 게임정보로 복구해야겠죠?
  • dispose() : 사용했던 리소스를 깨끗하게 정리하는 데 사용됩니다.

라이프사이클에 해당하는 메소드들이 애플리케이션의 상태가 변화에 맞게 호출된다고 했는데요. 실제로 그러한지 눈으로 확인해보는 게 좋지 않을까요? core 프로젝트의 FirstExample.java 파일에 아래와 같이 코드를 추가해서 직접 확인해보도록 하죠.

  1. ApplicationAdapter 클래스는 ApplicationListener의 어댑터 클래스예요. ( 어댑터 클래스는 인터페이스에 선언된 메소드들 중 원하는 메소드만 구현해서 쓸 수 있도록 해주죠. 원래 인터페이스를 구현할때는 선언된 모든 메소드를 '반드시' 구현해줘야 하잖아요? 굳이 재정의 해줄 필요없는 메소드까지 손수 구현해줘야 하는 번거로움을 없애준답니다)
  2. renderInterrupted 변수는 render()가 수시로 호출되어 render 로그가 계속 프린트 되는 걸 피하기 위해 추가시켰습니다.
  3. Logger 클래스를 이용해서 콘솔에 메소드가 호출될 때마다 그것을 출력하도록 했어요.
  4. 데스크탑, 안드로이드, iOS에서 각각을 실행해 보시고, 애플리케이션의 상태변화에 따라 각각 해당 메소드가 호출되는지 살펴보시면 됩니다.

package how2quit.gdx.tutorial;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Logger;

public class FirstExample extends ApplicationAdapter {
     SpriteBatch batch;
     Texture img;
     
     private Logger logger;
     private boolean renderInterrupted = true;
     
     @Override
     public void create () {
          logger = new Logger("Application Lifecycle", Logger.INFO);
          logger.info("create()");
          
          batch = new SpriteBatch();
          img = new Texture("badlogic.jpg");
     }

     @Override
     public void render () {
          if(renderInterrupted){
               logger.info("render()");
               renderInterrupted = false;
          }
          
          Gdx.gl.glClearColor(1, 0, 0, 1);
          Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
          batch.begin();
          batch.draw(img, 0, 0);
          batch.end();
     }

     @Override
     public void resize(int width, int height) {
          logger.info("resize");
          renderInterrupted = true;
     }

     @Override
     public void pause() {
          logger.info("pause");
          renderInterrupted = true;
     }

     @Override
     public void resume() {
          logger.info("resume");
          renderInterrupted = true;
     }

     @Override
     public void dispose() {
          logger.info("dispose");
     }
}




Starter classes and configuration

앞서 수차례 언급했던 것처럼, 우리가 다루고 있는 각각의 플랫폼별 프로젝트들은 스타터 클래스(starter class)라는 '진입점(entry point)에 해당하는 클래스가 있습니다. 각 플랫폼에서의 애플리케이션의 실행은 이것으로부터 시작합니다. 우리가 진행중인 예제 프로젝트에서 찾아보면, IOSLauncher, Android Launcher, DesktopLauncher가 되겠네요.

이 스타터 클래스에는 플랫폼에 특화된 백엔드를 구축해야하는 책임이 주어집니다. 거창한 말 같지만 의외로 간단합니다. 플랫폼 별로 제공되는 Application 타입의 객체들(백엔드, 아까 Application 인터페이스 타입에 대해 짧게 설명한거 기억하세요?)을 준비하면 되거든요. 이렇게 Application 타입의 객체를 준비과정에서, 직전에 살펴본 ApplicationListener를 구현한 객체(현재의 예제에서는 FirstExample이 되겠네요)와 따로 준비된 설정 정보를 함께 백엔드에 전달하는 겁니다.

  1. 스타터 클래스는 플랫폼별 백엔드를 구축한다.
  2. 백엔드 구축에는 ApplicationListener를 구현한 객체와, 플랫폼별 설정정보가 필요하다


각각의 플랫폼별 스타터 클래스를 함께 보며 실제 어떻게 구현되는지 보도록 할까요?


Desktop Starter
스타터 클래스인 DesktopLauncher의 코드를 직접 보시는 게 빠를 듯 합니다.

package how2quit.gdx.tutorial.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import how2quit.gdx.tutorial.FirstExample;

public class DesktopLauncher {
     public static void main (String[] arg) {
          LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
          new LwjglApplication(new FirstExample(), config);
     }
}
보시는 바와 같이, LwjglApplicationConfiguration 객체를 생성한 뒤에, FirstExample 객체와 함께 LwjglAppliation 객체 생성을 위한 파라메터로 전달하고 있네요. (지금은 여기서 LwjglApplicationConfiguration 클래스의 속성값들을 살펴보진 않을테니 궁금하시면 API를 참조하세요)


Android Starter

안드로이드를 위한 스타터 클래스도 소스코드를 보기로 할까요?

package how2quit.gdx.tutorial.android;

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import how2quit.gdx.tutorial.FirstExample;

public class AndroidLauncher extends AndroidApplication {
     @Override
     protected void onCreate (Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
          initialize(new FirstExample(), config);
     }
}

AndroidApplication 클래스는 Android SDK의 Activity를 상속하고 있고, 이와 함께 AndroidApplicationBase 인터페이스를 구현하고 있어요. 다시 AndroidApplicationBase 인터페이스는 Application 인터페이스를 확장(extend)하고 있고요. (기억나세요? 위에서 Application 인터페이스를 처음 소개하면서 언급한 적이 있습니다.)

요약하면, 데스크탑의 DesktopLauncher와는 다르게, AndroidLauncher 자체가 Application 타입이라는 얘기죠. 그래서, 클래스 내부에서는 FirstExample인스턴스와 설정정보가 담긴 인스턴스만 준비하고 있습니다.


iOS Starter

다음 코드는 iOS 스타터의 코드입니다.

package how2quit.gdx.tutorial;

import org.robovm.apple.foundation.NSAutoreleasePool;
import org.robovm.apple.uikit.UIApplication;

import com.badlogic.gdx.backends.iosrobovm.IOSApplication;
import com.badlogic.gdx.backends.iosrobovm.IOSApplicationConfiguration;
import how2quit.gdx.tutorial.FirstExample;

public class IOSLauncher extends IOSApplication.Delegate {
    @Override
    protected IOSApplication createApplication() {
        IOSApplicationConfiguration config = new IOSApplicationConfiguration();
        return new IOSApplication(new FirstExample(), config);
    }

    public static void main(String[] argv) {
        NSAutoreleasePool pool = new NSAutoreleasePool();
        UIApplication.main(argv, null, IOSLauncher.class);
        pool.close();
    }
}

iOS 백엔드를 위한 설정 객체는 IOSApplicationConfiguration 타입이네요. 데스크탑 스타터와 비슷하게 IOSApplication객체 생성을 위한 파라메터로 FirstExample과 설정정보 객체를 넘기고 있네요.



마무리 하면서

지금까지 Libgdx 애플리케이션이 어떻게 구성되고, 각각의 플랫폼에서 어떤 식으로 애플리케이션이 동작하게 되는지를 간단하게 살펴보았는고, 라이프사이클과 관련된 예제를 통해서 실제 어떤게 동작하고, 애플리케이션의 어떤 상태 변화에 따라 어떤 메소드들이 호출되는지도 보았습니다.

이제는 좀 더 높은 수준에서 전체를 살펴볼면서 어떤 식으로 잘 어울리게 되는지 그림을 통해 보면서 마무리 해볼까요?

아래의 UML 다이어그램은 애플리케이션이 시작되는 과정에서 각각의 플랫폼 런처들이 어떤 방식으로 ApplicationListener 타입의 구현체(가운데 있는 FirstExample)과 연관되는 지를 설명하는 것입니다. 각각의 런처와 설정정보가 담긴 객체들간의 연관성도 포함되었지요. 앞서 읽은 내용들을 상기하면서 아래 다이어그램을 살펴봅시다.






다음 그림은 Libgdx 애플리케이션의 라이프사이클을 보여주는 다이어그램입니다.

애플리케이션이 시작될 때마다 create() 메소드가 호출되고, 곧바로 resize()가 호출되어 현재의 화면 크기에 맞춘 후에 애플리케이션의 실제 로직으로 들어가게 됩니다. 이때엔 render()가 끊임없이 호출되어 지속적으로 화면에 뭔가를 보여주게 되겠죠. 입력되는 정보나 이벤트를 처리하면서요.

애플리케이션이 포커스를 잃게되면(예로, 전화가 온다거나 해서) pause()가 호출되고, 다시 포커스를 갖게되면 resume()이 호출되어서 다시 애플리케이션이 동작하게 됩니다.
사용자가 창의 크기를 변경하면 resize()가 호출될테고, 애플리케이션을 종료하게 되면 pause()가 호출되고 이어 dispose()가 호출됩니다.


다음(준비중): [Libgdx] 크로스 플랫폼 게임 개발 - 세 번째 이야기

No comments:

Post a Comment