Contents
1. 스터디 소개 ¶
- 그래픽스 엔진 등 공학적인 주제에 초점을 맞춤
- RPG만들기 에뮬레이터 스마트폰 버전
- 참고 위키: RPG만들기 한글, 영문
- Java로 만든 3D 물리 엔진으로 2D 엔진 만들기
- 툴이 아닌 에뮬레이터 위주
- UDK로 뭔가 삽질 하다가 멘붕하고 선회한 스터디 -ㅅ-
2. 목표 ¶
- 알피지 만들기(RPG Maker, 이하 알만툴)은 누구든지 손쉽게 바람의나라 같은 형식의 2D 롤플레잉 게임을 만들 수 있도록 도와주는 툴이다.
요즘 한창 날리는 UDK(이쒸....)나, Unity같은 게임엔진과 딸려오는 툴과 같은 개념이다.
현재 VX Ace버전까지 나와있으며 아직도 전세계에서 많은 이들이 알만툴로 게임을 만들고 있다.
- PC에서 돌아가는 게임 엔진(런타임)은 있지만 요즘 대세인 모바일에서 돌릴 수 있는 런타임이 존재하지 않는다.
따라서 이번 기회에 알만툴용 모바일 버전 엔진을 만들어 보려고 한다.
- 제작 순서는 다음과 같이 진행하려고 한다.
RPGMaker VX Version을 Java로 -> Android용으로 -> Windows용으로... (겨울 방학 끝날 때 쯤엔 완성 되겠지...)
실질적으로 2단계서부터 모바일화가 진행되며 1단계가 가장 오래 걸릴 듯 싶다.
2.1. 얻고 싶은 것 ¶
- 3D 그래픽스 지식 및 기본 그래픽스 개념
- 리버스 엔지니어링 노하우
- 모바일 프로그래밍
- 게임 로직 및 흐름의 이해
- 설계 패턴
- 다양한 프로그래밍 언어의 습득(Java, Ruby, C# 등등..)
3.1.1.1. 프로젝트 설정 ¶
- 이클립스 프로젝트 생성은 생략. 게다가 이미 svn repository에 등록해놓았음
- PC용 jPCT 엔진을 받는다
- 받은 엔진 안에는 jpct에 해당하는 jpct.jar과 또다른 그래픽 라이브러리인 lwjgl이 있다. lwjgl.jar까지 라이브러리로 등록한다
- lwjgl.jar은 내부적으로 native 함수를 호출한다. 이 native함수는 dll파일에 담겨져 있는데 이클립스에서 Window -> preferences -> java/installed JREs 에서 vm 설정에 -Djava.library.path=D:\Workspace\rma\libs 형식으로 lwjgl.dll 또는 lwjgl64.dll의 폴더 경로를 입력함으로써 참조하게 한다. 이와 관련된 오류는 java.lang.UnsatisfiedLinkError이다.
3.1.1.2. 또다른 방법(2012.08.30) ¶
- 이클립스 메뉴에서 window -> preferences -> java -> build_path -> user_libraries로 가면 유저 라이브러리를 등록할 수 있다.
jPCT와 LWJGL을 등록하는데 둘이 묶어서 할 수도 있고 서로 다른 라이브러리로 등록해도 된다.
lwjgl의 경우 참조하는 dll(안드로이드라면 so) 파일을 등록해야 하는데 이건 native library location에서 경로를 지정해주면 된다.
- 프로젝트 속성 -> java build path에서 방금 등록한 유저 라이브러리를 추가한다.
(개인적으로 이게 더 깔끔한 방법인 것 같다)
3.2.1. 한 것 ¶
- 3D에서 정점, UV좌표, 삼각형의 표현에 대해 설명하고 실제로 객체를 제작해봄
- 다음 스터디까지 과제로 오각형 만들어오삼
- 파일 분석 정보를 기반으로 parser 제작 시작함. *.rvdata는 들어가는 정보의 종류는 다르지만 일정한 포맷이 있는듯함
예상보다 파일분석은 빨리 끝날지도.
3.3.1. 한 것 ¶
- 과제 검사(FAIL) - 코딩에 앞서 엄밀한 수학 모델링을 먼저 해보는 것이 필요할듯
- Actors 파일 해석법을 소개하고 다른 파일을 맛보기로 분석해봄
- Actor를 읽어들이는 Actors.rvdata용 파일 파서 제작 완료
3.4. 뻘짓(2012.08.09) ¶
- 알만툴을 이해하기 위해 데이터베이스에 해당하는 *.rvdata를 뜯어봐야겠다 라는 생각을 초기에 했었지만
곧 뻘짓임을 알았다 -_-;;
.rvdata 파일은 단지 클래스들을 Marshal 모듈을 이용하여 덤프한 내용을 기록한 것임을 알게 되었기 때문이다.
게다가 알만툴에서 F1키 누르면 나오는 도움말에 Built-in 클래스나 모듈을 제외한 모든 자료구조(RPG::Actor, RPG::Map 등등..)의 소스가 오픈되어 있었다.
- 따라서 파일분석은 곧바로 때려치고 루비스크립트로 각종 .rvdata들을 불러오는 작업을 시작했다.
3.9.1. 한 것 ¶
- 글자의 정렬 구현
- 프레임워크에서 비트맵과 스프라이트의 개념, uv좌표에 대한 과외(...)
- ogg파일 출력
- Tilemap 작업
Tilemap은 loop이 있는 경우에는 좀더 머리를 써야할듯 싶다
3.10. 2012.09.18 ¶
- 그래픽 엔진을 2D 라이브러리인 Slick2D로 교체하고 작업중
branch 주소 http://nforge.zeropage.org/svn/rma/branches/slick_version
- 메모리 사용 퍼포먼스가 많이 좋아졌다.
- 코드가 '많이' 간결해졌다.
- 3D에서도 2D를 구현할 수 있음을 jPCT를 통해 충분히 보였고 이 이상의 작업은 3D지식을 필요로 하지 않기 때문에 3D구현은 여기서 끝내도 좋을 것이라 판단되었다.
4.1. 직교투영 렌더링 ¶
- jPCT에서는 명시적으로 Orthographic rendering(직교투영 렌더링)을 지원하지 않는다
http://www.jpct.net/forum2/index.php/topic,1789.msg13175.html#msg13175
따라서 근사적으로라로 직교투영을 만든다
- 근사 직교투영 좌표계 만들기 : 안드로이드 jPCT-AE로 (근사) 직교투영(Orthogonal Projection) 실현하기
한줄요약: 카메라를 겁나 멀리 갖다두면 근사적으로 직교투영이 됨ㅋ
- 실제 구현 코드
4.1.1. 보정(Interpolation) ¶
- 이를 이용하면 직교투영 좌표가 된 것처럼 보이긴 하지만 depth에 해당하는 z값이 바뀌면 조금이라도 오차가 생기기 때문에 이를 보정하여야 한다
특히 렌더링 순서를 z값을 조정함으로 결정시키기 때문에 정확한 렌더링을 위해서는 보정 테크닉을 아는 것이 필수이다.
[PNG image (14.84 KB)]
- 위 그림에서 볼 수 있듯이 실제 Object의 중점과 사람이 느끼는 Object의 위치는 다르다. 크기 역시 마찬가지이다.
만약 위 그림처럼 Object가 스크린에서 delta만큼 떨어져 있다면 간단한 비례식으로 (length-delta)/length 만큼 크기가 조정되어야 한다
중심의 이동은 Object의 중심을 O, Projection의 중심을 P라고 할 때 벡터 PO만큼을 이동시키면 된다. 벡터 PO는 카메라 위치와 Object 중심을 잇는 직선의 연장선상에 있다는 것을 생각하면 계산이 간편해진다.
- interpolation 구현 코드
4.2.1. 삼각형 ¶
- 모든 도형의 기초가 되는 도형이다. 모든 도형은 삼각형만으로 구성할 수 있고 또 쪼갤 수 있다.
- jpct에서 텍스쳐 있는 삼각형을 그리기 위해 필요한 정보는 다음과 같다
- 정점(vertex, 꼭지점) : 삼각형은 서로 다른 3개의 한 직선 위에 있지 않은 정점들로 구성된다.
- uv좌표 : 텍스쳐가 매핑되는 좌표계를 설정한다. (0, 0)부터 (1, 1)까지의 좌표에 텍스쳐 하나가 들어가게 된다
텍스쳐에 대해서는 나중에 할 말이 있을것.
- 정점들을 잇는 순서 : 이 순서에 따라 삼각형의 보이는 면과 보이지 않는 면이 결정된다.
다른 곳은 잘 모르겠으나 openGL에서는 오른나사의 법칙에 따라 엄지가 가리키는 방향의 면이 보이는 면이다.
CCW 판별 알고리즘을 이용해서 순서를 하드코딩하지 않고 자동으로 계산하게 할 수도 있다.
- Texture : 도형을 색칠하기 위한 정보가 들어있는 2차원 정사각형이라고 생각해도 좋다. 한 변의 길이는 속도를 위해 2의 제곱수여야 한다.
- 정점(vertex, 꼭지점) : 삼각형은 서로 다른 3개의 한 직선 위에 있지 않은 정점들로 구성된다.
4.2.3. 직선(Line) 그리기 ¶
- 직선도 실제로는 너비를 가져야 렌더링이 가능하기 때문에 다음과 같은 구조를 가져야 한다
[PNG image (7.47 KB)]
- vLine = vEnd - vStart
- normal(vLine과 수직인 벡터) = vLine × (-z방향 벡터) -> normalize하여 길이가 1인 벡터로 만듦.
- (u1, v1)과 (u1, v2)는 vStart ± (normal/2)
- (u2, v1)과 (u2, v2)는 vEnd ± (normal/2)
- 실제 구현
4.3. Viewport ¶
뷰포트는 일종의 창이라고 할 수 있다.
[PNG image (8.53 KB)]
운영체제 윈도우의 개념을 생각하면 편한데, 각 윈도우의 영역을 벗어나는 그래픽 조작은 철저히 차단된다. 이 차단하는 과정을 그래픽스에서는 clipping이라 한다.
jPCT에서는 FrameBuffer에서 setClippingPlane 함수를 통해 클리핑을 지원하므로 이와 적절한 카메라 조작으로 뷰포트를 구현할 수 있었다.
아래는 실제 구현 화면이다.
[PNG image (681.93 KB)]
위 사진에서는 가운데 아래쪽 윈도우가 뷰포트이다. 윈도우를 가로지르는 굵은 선을 출력했으나 뷰포트 바깥으로는 나가지 않는걸 확인할 수 있다.
4.4. 그래픽 엔진의 구조 ¶
※ 이 설계는 변할 수 있음
현재 제작중인 그래픽 엔진의 구조는 크게 두 부분으로 나눌 수 있으며 다음과 같다.
[PNG image (7.18 KB)]
인터페이스 IDrawable 하위 클래스들은 jPCT의 Object3D 객체를 포함하고 있으며 특히 RMPolygon 및 하위 클래스는 내부에 여러개의 IDrawable을 가질 수 있다.
또한 Viewport는 화면의 일부분만을 보여줄 수 있는 클래스로서 IDrawable을 포함할 수 있으며, Sprite, Window 등의 클래스의 슈퍼클래스이다.
가방 안에 가방을 또 넣는 것처럼 뷰포트 안에 또 뷰포트가 들어갈 수 있으며(트리 구조 가능), 최상위 뷰포트는 화면 전체를 아우르는 크기를 갖는다.
5. RPG Maker VX 분석 ¶
알만툴로 만든 게임은 크게 세 부분으로 되어 있다.
- 게임의 메인 로직을 담당하는 스크립트(루비로 작성됨)
- 캐릭터 정보, 아이템 정보, 스킬 정보 등의 리소스 부분(파일로 저장되어 있음)
- 그래픽, 사운드 등 게임의 엔진에 해당하는 부분
알만툴에서는 메인 로직을 수정할 수 있는 스크립트 에디터를 제공함으로 게임 제작에 유연성을 더한다.
툴이 RPG에 최적화된 환경일 뿐이지, 슈팅이나 횡스크롤 게임 등도 얼마든지 만들 수 있다. 대신 알만툴의 기능이 쓸모없어질 뿐.
5.1. 데이터베이스(리소스) ¶
- 알만툴에서 데이터베이스에 해당하는 .rvdata 파일은 루비 클래스를 덤프한 내용을 기록한 것이다.
이를 불러오고 저장하는 load_data함수와 save_data함수를 도움말에서 제공하고 있다.
def load_data filename File.open(filename, "rb") { |f| obj = Marshal.load(f) return obj } end def save_data filename, obj File.open(filename, "wb") { |f| Marshal.dump(obj, f) } end
5.2. 게임의 메인 로직(스크립트) ¶
알만툴에서 F11을 누르면 나오는 스크립트는 게임이 어떻게 진행되는지에 관련된 내용이 담겨 있다.
하지만 우리가 제작하려는 것은 게임 콘텐츠가 아닌 에뮬레이터이기 때문에 구현 참고용으로만 사용할 계획이다.
에뮬레이터에서 메인로직과 관련해 할 일은 단지 메인 로직을 불러주는 일뿐이다.
5.2.1. Java에서 루비스크립트 실행하기 ¶
- JRuby 홈페이지에서 JRuby를 설치한다. 64bit OS에서도 32비트 전용으로 설치해야 돌아가는듯 하다.
- JRuby설치경로/lib 에 위치한 jruby.jar파일을 프로젝트 라이브러리 폴더에 넣는다.
- 이클립스에서 .jar파일 오른쪽 클릭 메뉴에서 build path->add to build path로 등록한다.
- 간단하게 코딩한다.
아래 코드는 프로젝트 내에서 src/ruby/main.rb 파일을 호출하는 코드.
public static void main(String[] args) throws IOException, ScriptException { ScriptEngine rbEngine = new ScriptEngineManager().getEngineByName("jruby"); // start rubyscript rbEngine.eval(new FileReader("src/ruby/main.rb")); }
5.3. 코어(엔진) ¶
메인 로직을 보면 각종 그래픽 처리, 입력 처리, 자료구조 등은 따로 제작된 모듈을 사용하는 것을 볼 수 있다.
도움말을 보면 대부분의 게임 내에서 사용하는 이러한 클래스에 대한 명세를 제공하고 있다.
하지만 실제 구현에 대한 소스코드를 제공하지 않기 때문에 이런 Built-in 클래스와 모듈은 명세만 보고 직접 구현해야 한다.
예제: Table 클래스의 구현
5.3.1. JRuby에서 자바 클래스의 사용 ¶
그낭 루비가 아닌 자바로 구현된 JRuby의 경우 루비스크립트 내에서 자바 클래스들을 사용할 수 있다.
사용방법은 간단한다. 스크립트 상단에 require "java"를 명시하고, 각종 클래스들을 import하거나 패키지 이름을 모조리 명시하면 자바클래스를 그대로 사용할 수 있다.
사용방법은 간단한다. 스크립트 상단에 require "java"를 명시하고, 각종 클래스들을 import하거나 패키지 이름을 모조리 명시하면 자바클래스를 그대로 사용할 수 있다.
- 예제: Color 클래스의 구현
5.3.2. 직접 구현해야 하는 클래스/모듈 목록 ¶
* Built-in class
- Bitmap: 이미지 데이터가 저장되는 클래스
- Color: 색상 정보(RGBA)를 가지고 있는 클래스
- Font: 글꼴 정보를 가지고 있는 클래스. .ttf파일을 지원해야 한다.
- Plane: 이미지가 표시되는 평면 클래스. 이미지가 타일형으로 반복표시된다.
- Rect: top-left 코너와 너비, 높이 정보를 가지는 클래스.
- Sprite: Bitmap의 표현이 이루어지는 클래스.
- Table: 3차원 배열 클래스.
- Tilemap: 맵의 표시와 관련된 클래스(?)
- Tone: 이미지의 색상보정을 위한 클래스(?)
- Viewport: 2D상의 뷰포트를 구현한 클래스
- Window: 알만툴 내부에서의 윈도우 표시를 담당하는 클래스.
- RGSSError: 에러처리 클래스. StandardError클래스를 상속함
5.4.1. 나와라 소리야 ¶
- Audio를 위한 Java OpenAL 튜토리얼
- .wav파일을 불러 오는 것이 다인데 문제는 사용해야 할 파일들이 midi 아님 ogg 파일 <- 코덱이 있어야 한다나 -ㅅ-
- 참고 1
- 참고 2
- 참고 3?
- OGG 파일을 사용하기 위해선 Loading Sound에서 말하길 Slick-Util zip을 받아 라이브러리에 추가하라고 함... 그럼 된다나..
- 다운로드
- jorbis-*.jar 과 jogg-*.jar library jar files 를 classpath 에 넣어야 하는데 그게 Slick-Util zip 있다고..
- 다운로드
- 참고 1
- 파일이 재생이 안 됨
- 박재민: 이 링크가 재생오류시 도움이 될듯..
- 박재민: 이 링크가 재생오류시 도움이 될듯..
public void bgm_start(String filename, int volume, double pitch) { // Initialize OpenAL and clear the error bit. try { AL.create(); } catch (LWJGLException le) { le.printStackTrace(); return; } AL10.alGetError(); // Load the wav data. if (loadALData("test") == AL10.AL_FALSE) { System.out.println("Error loading data."); return; } setListenerValues(); AL10.alSourcePlay(source.get(0)); killALData(); AL.destroy(); }
- 맨 마지막 함수 killALData() 함수와 AL.destroy()가 실행하자마자 종료시킴.. 주석처리 하니까 소리는 나옴
5.4.2. 여러 개 소리가 같이 날 수 있게 만들긔 ¶
- 오디오 라이브러리에 playAsSoundEffect()를 여러번 호출하면 여러번 음악 파일이 실행되는 것을 확인 함.
try { // you can play wavs by loading the complete thing into // a sound wavEffect = AudioLoader.getAudio("WAV", ResourceLoader.getResourceAsStream("sound/Test.wav")); } catch (IOException e) { e.printStackTrace(); } while (Keyboard.next()) { if (Keyboard.getEventKeyState()) { if (Keyboard.getEventKey() == Keyboard.KEY_T) { // play as a one off sound effect wavEffect.playAsSoundEffect(1.0f, 1.0f, false); } if (Keyboard.getEventKey() == Keyboard.KEY_G) { wavEffect.stop(); } } }
- 위와 같은 방식으로 키보드를 통한 음악 실행과 정지가 가능하다....
- 근데 이렇게 하고 보니 기존에 했던 코드가 아무짝에도 쓸모 없다는 사실을 알았다. -_-;
- 근데 이렇게 하고 보니 기존에 했던 코드가 아무짝에도 쓸모 없다는 사실을 알았다. -_-;
public void init(InputStream filename) { try { // you can play oggs by loading the complete thing into // a sound oggEffect = AudioLoader.getAudio("OGG", filename); // java.lang.ArithmeticException: / by zero oggEffect.playAsSoundEffect(1.0f, 1.0f, false); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // polling is required to allow streaming to get a chance to // queue buffers. SoundStore.get().poll(0); }
- 왜 저기서 0으로 나눠서 문제가 된다는거지???????
박재민: 좀더 검색해보니까 길이가 짧은 스테레오 ogg파일은 문제가 있는듯 보임. 모노로 변환해보니깐 되더라;;
- 난 안됨 - 권순의
- 난 안됨 - 권순의
public void init(InputStream filename) { URL file = null; try { file = new URL(filename); } catch (MalformedURLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { // you can play oggs by loading the complete thing into // a sound oggStream = AudioLoader.getStreamingAudio("OGG", file); oggStream.playAsMusic(1.0f, 1.0f, true); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // polling is required to allow streaming to get a chance to // queue buffers. SoundStore.get().poll(0); }
- 위와 같이 바꾸었을 경우 getStreamingAudio()에 들어가는 인자가 String, String이 될 수 없고 URL로 바꾸어 주어야 함. 그래서 소리가 날 까 했는데 no protocol: assets/rpgvx_resources/Audio/SE/Cursor.ogg 이라는 아름다운 문자를 날려줌. -ㅅ-...
- 이 경우 file = new URL(filename); 를 file = new URL("file://" + filename);로 바꾸어 주면 소리가 날 것 처럼 사람들이 써 놨으나 실패. 그래서 절대 경로를 모두 적어주었지만 역시나 파일을 읽는데 실패했다.
- 이 경우 file = new URL(filename); 를 file = new URL("file://" + filename);로 바꾸어 주면 소리가 날 것 처럼 사람들이 써 놨으나 실패. 그래서 절대 경로를 모두 적어주었지만 역시나 파일을 읽는데 실패했다.
public void init(InputStream filename) { try { // you can play oggs by loading the complete thing into // a sound oggStream = AudioLoader.getStreamingAudio("OGG", ResourceLoader.getResource(filename)); oggStream.playAsMusic(1.0f, 1.0f, true); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } // polling is required to allow streaming to get a chance to // queue buffers. SoundStore.get().poll(0); }
- Test로 실행이 되었던 소스였으나, 여기선 java.io.BufferedInputStream.read(Unknown Source)라며 즐을 날려 준다 -_-.... 아니 왜!!!
- 맨 위 처음 Test한 방법과 같은 방법으로 시도 해 보았으나 소리가 안 나서 멘붕하고 있음
- 맨 위 처음 Test한 방법과 같은 방법으로 시도 해 보았으나 소리가 안 나서 멘붕하고 있음
6.1. Project setup ¶
pc와 안드로이드는 다른 환경이다. 자바라는 언어가 플랫폼간의 이질성을 초월하는 언어라고는 하지만, 어느 정도는 native한 기능들도 있기 때문에 그에 맞추어 환경을 설정해야 한다.
6.1.1. ADT plugin setup ¶
- 이클립스에서 help - install new software로 가서 업데이트 사이트에 http://dl-ssl.google.com/android/eclipse/ 를 입력하면 가능한 업데이트 목록이 나온다.
- 나오는 목록들 중에 Developer tools - Android DDMS, Android development tools를 선택한 뒤 설치를 진행한다.
- 이클립스를 재부팅하면 Android SDK를 설치할 거냐고 묻는 경우가 있는데, 설치해야 한다.
6.1.2. Slick-AE 빌드 ¶
- slick은 Mercurial(HG)로 버전관리를 하기 때문에 미리 Mercurial을 설치한다.
- [hg clone https://bitbucket.org/kevglass/slick] 명령으로 slick의 전체 코드를 다운받는다.
- 이클립스에서 slick의 코드들 중 Slick과 Slick-AE만 각각 프로젝트로 import한다.
- 이클립스의 package explorer에 보면 Slick-AE에 build.xml이 있다. 이것을 실행하면 빌드가 수행되며, target폴더에 slick-ae.jar 파일이 생성된다.
6.1.3. 안드로이드 프로젝트 생성 ¶
- 안드로이드 환경에서 루비스크립트를 실행하기 위해서는 ruboto라는 프레임워크가 필요하다.
내부적으로 JRuby를 사용하고 있기 때문에 우리 프로젝트와도 잘 맞는 프레임워크라 할 수 있다.
컴퓨터에 ruboto 세팅하기
※ ruboto를 설치/빌드하기 위해서는 JRuby(또는 그냥 Ruby), Ant(자바용 make)가 필요하다.
커맨드라인에서 ruboto gen app --package ... 으로 앱을 생성한 뒤 폴더로 가서 rake install start를 하면 폰으로 디버깅이 가능해진다.
[PNG image (198.25 KB)]