메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

루비로 게임 만들기 1-1

한빛미디어

|

2008-01-31

|

by HANBIT

13,906

제공 : 한빛 네트워크
저자 : Andrea O. K. Wright
역자 : 주재경
원문 : Creating Games in Ruby (Part 1)

루비는 그 특유의 표현의 유연성으로 인해 꽤나 지루한 작업을 쉽게 그리고 재미있게 할 수 있다. 게임 로직 작성과 같은 저수준 처리용 게임 개발 프레임워크용으로 루비는 특히 사용해볼 만 하다.

루비로 작성한 게임을 하는 것이 프로그램을 만드는 것만큼 재미 있을까? 적절한 시간 내에 동작이 이루어지지 않아 너무 느려지는 것은 아닐까? 루비는 게임 부분에서 나름의 역할이 있는 걸까?

2개 부분으로 구성된 비디오 클립에 이러한 질문에 대한 답과 루비 기반의 게임과 특수 효과에 대한 내용이 들어 있다. 이 비디오 클립에서 루비로 작성된 많은 게임을 볼 수 있다. 루비의 gc정책(garbage collection이 진행되는 동안 다른 모든 동작은 정지)이 미래의 루비 게임개발자들에게 걸림돌이며 상업적인 비디오 게임 시장에 루비로 된 게임이 없다는 것이 개발자들의 의욕을 꺾는다는 것을 잘 알고 있다. 이 글의 2번째 부분의 결론에서 이러한 문제들을 다룰 것이다. 그때까지 이 문제들을 잠시 접어두자.

이 글은 루비로 만드는 2D와 3D게임의 리소스에 관한 조사이다. 튜토리얼은 아니지만 게임 작성에 필요한 것을 알려 주고 싶다. 루비가 게임 개발자에게 무엇을 제공해야 하는지를 개괄한다.

Ruby/SDL - Lead Developer and Creator: Ohai

Ruby/SDL은 멀티미디어 지원을 제공하는 크로스 플랫폼 라이브러리로서 오픈 소스이며 SDL에 대한 Ruby/C 확장이다. SDL은 Simple Directmedia Layer의 약어이다. 설명한 다른 라이브러리와 마찬가지로 Ruby/SDL은 사운드와 비디오 통합을 지원하지만 이 글에서는 게임 개발에 관한 이러한 면을 논하지는 않을 것이다. 그래픽 부분에 초점을 둘 것이다.

Ruby/SDL로 작성된 간단한 프로그램의 소스코드로 대부분의 다른 프레임워크에 바로 적용하는 개념을 살펴볼 수 있고 내가 아는 한 가장 저수준의 라이브러리이므로 Ruby/SDL로 시작할 것이다. 이런 식의 많은 아이디어가 다른 라이브러리의 프레임워크 코드에 깊이 감춰져 있다.

SDL은 C로 개발되었다. Ruby 개발자들이 사용 가능한 C 라이브러리 함수 제작은 Ruby C API를 사용하여 이루어지며 두 언어 사이의 차이로 인해 만들어 지는 방식으로 원래 C 함수를 감싸안는 방법으로 이루어 진다.

때때로 원래의 C 코드에 Ruby에서 호출 가능하도록 특별한 조작을 하는 방식이 그렇게 필요하지 않는 경우도 있다. SDL_getTicks() 경우가 그러한데 이 함수는 프로그램이 얼마나 오랫동안 실행되고 있는지를 밀리세컨드 단위로 리턴한다. 아래에 C 기반의 SDL 라이브러리 방법이 있다.
Uint32 SDL_GetTicks(void);
아래에서 Ruby/SDL 코드가 SDL_getTicks()를 감싸 안는다.
static VALUE sdl_getTicks(VALUE mod) 
{
  return UINT2NUM(SDL_GetTicks());
}
UINT2NUM 매크로는 Ruby 호출자가 사용하도록 C 리턴 타입을 Ruby Num 으로 변환하는데 사용된다. C 함수에 아무런 인자가 없다고 하더라도 이를 감싸안는 함수는 하나의 인자를 가진다는 것에 주의할 것. 객체 기반이 아니므로 인자가 전달되지 않으면 C는 함수 호출을 위해 무엇을 해야 될지를 모른다.

이 감싸안는 함수는 루비쪽에서 호출 가능하도록 하기 위해 Ruby C 확장 초기화 루틴 내에서 참조되어야 한다. 아래에 Ruby/SDL 초기화 코드에서 감싸안는 함수가 어떻게 참조되는지를 볼 수 있다.
rb_define_module_function(mSDL, "getTicks", sdl_getTicks,0);
Rb_define_module_function 메서드는 C 확장 코드를 루비 프로그래머가 사용 가능한 메서드로 대응 시킨다. 이 경우의 감싸안는 함수 sdl_getTicks()는 getTicks()로 맵핑된다. 왜 Ruby의 이름 규칙을 사용하지 않고 대소문자를 혼용하는 것일까? 라이브러리 함수를 감싸 안는 함수를 사용할 때 감싸안긴 라이브러리의 이름 규칙 사용을 좋아하는 개발자들이 있다. _를 사용하는 Ruby 스타일의 이름 규칙을 선호하는 개발자들을 위해 get_ticks는 별명 get_ticks, getTicks로 Ruby/SDL라이브러리내에 정의된다.

Ruby/SDL에는 튜토리얼이 없지만 라이브러리의 간단한 응용프로그램 소스를 읽어보면 쉽게 출발할 수 있다. 테트리스의 가장 간단한 버전이 아래 [그림1]에 있다. 앞으로 소개할 샘플은 특정 효과나 테크닉 한가지에만 국한된다. 예를 들면 하나의 샘플은 단지 붉은색 팔각형을 보여줄 뿐이다. 이 이미지는 테트리스 블록을 구성하는 이미지이며, 어두운 부분을 나타내는 이미지이기도 하다. 충돌 검출과 조작을 설명하기 위해서는 임의의 방향으로 움직이고 충돌 시에 방향을 바꿔 서로 부딪히는 예제가 있다. 또 다른 예제는 가장 기본적인 이벤트 핸들링을 보여준다. 여러 개의 붉은색 팔각형이 가운데 하나를 제외하곤 임의로 움직인다. 가운데 있는 움직이지 않는 블록은 사용자가 입력하는 키 입력에 따라 상하 좌우로 움직이는 것과 관련된다. 이 세가지 예제는 좀더 복잡한 게임을 만드는데 필요한 기능에 대한 아주 좋은 본보기를 보여준다.


[그림 1] Ruby/SDL에 있는 테트리스 데모

간단한 이벤트 핸들링 데모 movesp.rb로 시작해서 Ruby/SDL로 만들어진 살펴볼 것이 많은 소행성 모양의 게임 코드가 만들어지는 방법을 살펴보고자 한다. 아래에 모든 붉은색 팔각형을 만들어 내는 코드가 있다.
sprites = []
for i in 1..5
  sprites << Sprite.new
end
sprites << MovableSp.new
Sprite는 임의로 움직이는 팔각형에 대한 클래스이고 MovableSp(Movable Sprite의 줄임 말)는 중앙에서 팔각형을 조종하는 클래스이다. 모든 sprite를 저장하기 위해 sprite배열을 생성한다. 처음 비디오 게임에 흥미를 갖기 시작한 이래로 나는 sprite가 판타지 게임의 숲 속의 요정과 같다고 생각했다. 나는 이 sprite가 SDL게임처럼 컨텍스트에서 사용됨을 알았으며 춤추는 요정을 표현하기 위해 왜 Sprite를 사용하지 않는지가 궁금했다. Sprite는 2D 객체에 대한 컴퓨터 그래픽의 표준 용어임을 알았고 위치는 전형적으로 프레임 사이에서 변한다.

아래에 임의의 움직임에 대한 sprite를 정의한 클래스가 있다. 타원형으로 되어 있으며 x code를 나타내는 대부분의 y code를 제거했다.
class Sprite
  def initialize
    @x=rand(640)
    ...
    @dx=rand(11)-5
  end
  
  def move
    @x += @dx
    if @x >= 640 then
      @dx *= -1
      @x = 639
    end
    if @x < 0 then
      @dx *= -1
      @x = 0
    end
    ...
  end
  
  def draw(screen)
    SDL.blitSurface($image,0,0,32,32,screen,@x,@y)
  end
end
초기화 과정에서 각 프레임에 대해 x축과 y축을 따라 Sprite의 움직이는 양을 표현하는 @dx와 @dy뿐만 아니라 sprite의 x와 y초기 좌표 또한 임의로 설정된다. move메서드는 @dx와 @dy값을 더해서 x와 y좌표에 값을 할당한다. Sprite가 화면 끝에 도달 하면(x좌표가 640과 같거나 큰 경우) Sprite의 방향을 변경하기 위해 @dx에 -1을 곱한다.(즉 x축을 기준으로 반대 방향이 설정됨). Draw 메서드에서 호출하는 SDL.blitSurface는 붉은색 팔각형을 display surface에 복사한다.

중앙에 있는 sprite를 위한 클래스 MovableSp클래스 정의가 아래에 있다.
class MovableSp
  def initialize()
    @ud=@lr=0;
  end
  
  def move()
    @ud=@lr=0;
        @lr=-1 if SDL::Key.press?(SDL::Key::H)
          or SDL::Key.press?(SDL::Key::LEFT)
    @lr=1  if SDL::Key.press?(SDL::Key::L)
      or SDL::Key.press?(SDL::Key::RIGHT)
    @ud=1  if SDL::Key.press?(SDL::Key::J)
      or SDL::Key.press?(SDL::Key::DOWN)
    @ud=-1 if SDL::Key.press?(SDL::Key::K)
      or SDL::Key.press?(SDL::Key::UP)
  end
  
  def draw(screen)
    SDL.blitSurface($image, 0, 0, 32, 32, screen,
      300+@lr*50,200+@ud*50)
  end
end
move가 호출되면 @ud(위/아래)와 @lr(좌/우)를 0으로 초기화 한 다음 입력되는 화살표 키 값에 따라 @ud와 @lr값을 설정한다. draw메서드에 있는 마지막 두 인자는 MovableSp를 위한 새로운 x,y좌표를 나타낸다. 이 두 인자가 0이 되면 sprite는 화면 중앙에 그려진다. 그렇지 않은 경우 정해진 방향의 중앙으로부터 +/-50픽셀씩 움직인다.

이벤트 큐 처리와 게임 루프를 설정하는 코드가 아래에 있다.
while true
  while event = SDL::Event2.poll
    case event
    when SDL::Event2::Quit
      exit
    when SDL::Event2::KeyDown
      exit if event.sym == SDL::Key::ESCAPE
    end
  end
  
  screen.fillRect(0,0,640,480,0)
  SDL::Key.scan
  
  sprites.each {|i|
    i.move
    i.draw(screen)
  }
  screen.updateRect(0,0,0,0)
end
이 이벤트 루프는 사용자가 ESC키를 입력 하거나 x키를 입력하면 프로그램을 종료한다.(SDL::Event2::Quit이벤트는 사용자가 게임윈도우의 타이틀 바에 있는 x 버튼을 클릭하는 순간 발생한다) 각 프레임에서 move와 draw는 sprite의 각 멤버에 대해 호출된다. updateRect를 호출하면 화면이 갱신된다. 앞으로 이 글의 나머지 부분에서 이 와 같은 유형을 계속해서 보게 될 것이다. Sprite는 모양을 얼마나 변경 시키고 움직여야 되는지를 결정하고 이 이미지를 display surface에 복사한 다음 display는 이 변화를 반영하기 위해 화면을 갱신한다.

위에서 말한 소행성 모양의 게임에 대한 Steven Davidovitz의 비디오 클립이 여기 있다. Nebular Gauntlet

Nebular Gauntlet는 배경화면을 스크롤 하고 로켓 쏘는 것을 시험하는 것과 같은 SDL 테크닉을 익히기에 아주 좋은 곳이다. 작업은 여전히 진행 중이지만 이미 작업이 끝난 것이 많이 있다. 맵 에디터가 있는 게임을 저장할 수 있다. 이 임의의 위치를 클릭 하여 쉴드와 봇을 원하는 곳에 위치 시킨다. 봇은 우주선을 추적하도록 프로그램 되어 있다. 봇은 플레이어의 우주선을 추적한다. 임무를 완수 했는지를 결정하는 코드(화면에 점수를 표시하여)와 여러 레벨을 통해 다음 단계로 가도록 하는 코드가 있다. 비디오를 통해 본 것 처럼 화면 상의 동작을 추적하는 위쪽 왼쪽 구석의 작은 레이더 영역에 주의하라.

Nebular Gauntlet에서 가져온 메인 이벤트 루프는 moves.rb를 제어하는 루프와도 크게 다르지 않다. 입력을 검사하고 우주선이나 다른 것들의 위치를 변경시킬 필요가 있는지 결정한 다음 화면을 갱신한다. 아래는 Nebular Gauntlet에 있는 메인 이벤트 루프이다. 따라오는 코드는 여기에서 호출하는 몇 개의 메서드이다. do_think와 do_render가 호출하는 think와 render코드에 대한 호감으로 인해 do_think와 do_render에 대한 코드는 보지 못했다. render를 호출한 후 화면 전체를 갱신하는 역할을 하는 것은 do_render이다.
def start
  ...
  # Main loop; Loop until quit
  while !@@quit
    handle_input() # Handle keyboard input
    do_think() # Do calculations/thinking
    do_render() # Render everything
    ...
  end
end

def think(elapsedtime)
  return if elapsedtime > 100
  $map.check_objs
  ...
  $entities.move(elapsedtime)
  $entities.collide_with($fires)
  $entities.collide_with($ship)
  $entities.collide_with($entities)
  $entities.collide_with($map)
  ...
end

def render      
  $map.draw
  $ship.draw
  ...
  $camera.draw
  $fires.draw
  $entities.draw
  @console.draw
  @interface.draw
  ...
end
RUDL
Creator: Danny Van Bruggen

지난 몇년 동안 RUDL 프로젝트는 활동이 없었다. 그러나 여기에는 여전히 좋은 아이디어가 많으며 흥미로운 것들이 패키징 되어 있다.

RUDL은 초기화 코드에서 처럼 개발자가 작성해야 되는 반복적인 코드의 양을 최소화 하고 Ruby/SDL보다 더 Ruby의 문법과 스타일을 잘 따르는 SDL을 사용하기 위한 방편으로 만들어 졌다. SDL의 SDL_setVideoMode함수를 감싸 안는 RUDL의 함수는 이러한 목적을 충족 시키는 RUDL의 접근 방법에 대한 좋은 본보기 이다. SDL_setVideoMode메서드는 그래픽에 SDL을 사용하는 모든 프로그램에서 호출되어야 한다. 이 함수는 화면에 표시될 이미지를 display surface에 생성한다. 어떤 랜더링 시스템을 사용할지를 플래그로 전달할 수 있다.(소프트웨어 surface: SWSURFACE, 하드웨어 surface: HWSURFACE or OPENGL). HWSURFACE는 피하는 것이 좋다. 모든 비디오 드라이버가 SDL게임에 적합 하도록 최적화 되어 있지는 않으며 하드웨어 가속 기능이 모든 플랫폼에 있는 것도 아니다. OpenGL이 아마도 가장 속도를 빠르게 하는 선택이 되겠지만 Ruby/SDL이나 RUDL로의 통합작업이 그리 잘 되어 있지는 않다. 랜더링 엔진으로 이를 선택한다면 드로잉을 위해 이해하기에는 어렵지 않지만 조금은 성가신 OpenGL API를 사용해야 한다. SDL_setVideoMode가 RUDL에 어떻게 감싸여져 있는지 살펴본 다음 OpenGL API를 살펴볼 것이다.

SDL라이브러리에 있는 SDL_setVideoMode함수이다.
SDL_Surface *SDL_SetVideoMode(int width, int height,
                              int bpp,Uint32 flags);
RUDL의 DisplaySurface.new함수가 SDL_setVideoMode함수를 감싸 안고 있다. RUDL을 만든 Danny Van Bruggen은 새로운 display surface를 생성하고 호출된 SDL_setVideoMode에 결과를 리턴하는 메서드는 최소 놀람의 원칙(the principle of least surprise) 에 위배 된다고 생각했다. 그는 사용자가 새로운 DisplaySurface를 생성하면 DisplaySurface가 리턴 될 것이라고 기대한다고 생각했다. 그리고 그는 픽셀당 비트수를 표시하는 3번째 인자가 16으로 설정된 경우와 랜더링 시스템을 나타내는 마지막 인자가 SWSURFACE로 설정된 경우 4가지 인자 모드를 넘겨 줘야 한다는 것을 생각하지 못했다.

아래에 RUDL의 SDL_setVideoMode함수를 감싸안는 함수인 DisplaySurface.new함수가 있다. 이 함수는 Ruby/SDL의 감싸안는 함수가 좀더 기본에 충실한 반면 RUDL의 그것은 좀더 간단하다.
# RUDL의 setVideoMode랩퍼에 대한 간단한 호출
display = DisplaySurface.new([640,480])

# Ruby/SDL의 SetVideoMode 랩퍼에 대한 간단한 호출
display = SDL::setVideoMode(640,480,16,SDL::SWSURFACE)
아래에 RUDL이 초기화 될 때 DisplaySurface.new를 랩퍼에 맵핑시키는 코드와 SDL_SetVideoMode를 감싸 안는 RUDL코드를 발췌해 놓았다.
static VALUE displaySurface_new(int argc, VALUE* argv, VALUE self)
{
  ...
  surf = SDL_SetVideoMode(w, h, depth, flags);
  currentDisplaySurface =
    Data_Wrap_Struct (classDisplaySurface, 0, 0, surf);
  return currentDisplaySurface;
})

rb_define_singleton_method(classDisplaySurface,
  "new", displaySurface_new, -1);
RUDL로 패키징된 가장 유용한 자료들 중의 하나는 Martins Stannard가 유명한 네온 헬륨 OpenGL 튜토리얼을 Ruby로 옮겨놓은 것이다. 랜더링 시스템으로 OpenGL을 사용하지만 이벤트 처리와 같은 이외의 모든 것은 Ruby를 사용한다. RUDL에 관해 이야기를 하고 있으므로 어떤 OpenGL API가 이점과 같은지를 보여주는 것이 이치에 맞을 것이다.

아래에 삼각형을 랜러링한 후 바로 이어서 기본적인 2차원 도형을 생성하는 스크린샷이 있다. OpenGL은 3차원 랜더링 시스템이므로 점은 x,y,z좌표 값으로 표시된다. z축은 화면의 앞 뒤 방향이다. 0은 화면 표면을 가리킨다. 터미널의 앞 쪽에 당신이 앉아 있다면 앞 방향은 당신을 향해 튀어 나오는 방향이고 뒷 방향은 화면 뒤의 가상 공간으로 들어가는 방향이다. 물체를 화면 속으로 들어가게 하려면 z값을 감소 시키면 된다.

이와 똑 같이 2차원 화면은 z값을 항상 0으로 설정하여 OpenGL을 사용하여 생성할 수 있다. 삼각형의 세 꼭지점을 설정하기 위해서는 GL.Vertex를 세번 호출해야 하며 각 꼭지점의 색깔을 설정하기 위해 GL.Color을 세번 호출해야 한다. 삼각형을 그리기 위해서는 각 꼭지점을 연결하기 위해 GL.Begin과 GL.End를 호출해야 한다.

3차원 피라미드를 만들기 위해서는 3개의 면을 정의하기 위해서 GL::Vertex를 호출한 횟수의 3배를 호출해야 한다. 모양에 색깔을 입히는 대신 이미지를 화면에 나타내기 위해서는 GL.BindTexture를 사용한 후 GL.Color를 호출하는 대신 GL.TexCoord를 사용해야 한다.


[그림 2] Martin Stannard가 RUDL로 포팅한 네온 헬륨 OpenGL튜토리얼
display = Proc.new {
  GL.Clear(GL::COLOR_BUFFER_BIT|  
           GL::DEPTH_BUFFER_BIT)        
  GL.Color(1.0, 1.0, 1.0)
  GL.LoadIdentity()
  GL.Translate(-1.5, 0.0, -6.0)         
  # 삼각형 그리기
  GL.Begin(GL::POLYGON)                    
  GL.Color3f(1.0, 0.0, 0.0)             
  GL.Vertex3f(0.0, 1.0, 0.0)            
  GL.Color3f(0.0, 1.0, 0.0)             
  GL.Vertex3f(1.0,-1.0, 0.0)            
  GL.Color3f(0.0, 0.0, 1.0)             
  GL.Vertex3f(-1.0,-1.0, 0.0)           
  GL.End()                                              
  GL.Translate(3.0,0.0,0.0)             
}
TAG :
댓글 입력
자료실