∇ 델파이 게임 제작 소품

 ▼ Assembled Sprite

Delphi

소스有

Assembled Sprite가 무엇이냐. 벌써 알고 있었다면 이상한 것이다 내가 지어낸 말이니까.
이것의 아이디어는 Compiled Sprite의 원리를 근간으로 한다.

단, Compiled Sprite는 실시간에 컴파일된 데이터를 만들 수 있고 그것을 아예 어셈블러 수준에서 링크하기도 한다. ({$L ..}옵션이던가..) 하지만 이것은 전처리를 통해 pascal inline assembly language 코드를 생성하고 그것을 다시 델파이 컴파일러가 컴파일하여 스프라이트를 자신의 실행 파일 속으로 숨긴다. 

예전에 MMX를 이용한 '무분기 스프라이트'기법을 소개한 적이 있다.
그때는 MMX 명령어에 기본으로 포함되어 있는 마스킹 명령을 썼던 것이다. 그래서 스프라이트 출력에 필수 코드였던 if 문을 뺄 수 있었다. 그렇다면 이번의 Assembled Sprite는 무엇인가? 이것은 if 문 뿐만 아니라 sprite 출력 루틴이라면 필수적으로 가지고 있던 for 루프 2개를 모두 생략한 것이다.
 

자 그럼 원리부터 설명하기 위한 기본 조건을 보자면, 

1. 델파이는 인라인 어셈블리어를 지원한다.
2. 델파이는 함수형 타입을 선언 할 수 있다.
3. 델파이는 fastcall을 지원한다.

첨부파일 (28K) (Delphi 5에서 작성되었음)

1. 원리

고전적인 스프라이트 출력 루틴은 이렇다.
 
for y = 0 to sprite.height - 1
    for x = 0 to sprite.width - 1
        if sprite.data[x][y] <> color_key
            frame_buffer[x_offset + x][y_offset + y] = sprite.data[x,y]
        end if
    end for
end for
 
이 루틴을 보면 크게 점프문이 3군데 있다. for 루프 복귀를 위한 2개와 if 문을 위한 1개이다. 특히 if문은 for 루프 2개 사이에 항상 호출되는 것이므로 퍼포먼스 저하를 가져오는 주된 원인이 된다. 그래서 나온 것이 흔히 '0번 압축'이라고 불리는 짝퉁 RLE이다. 이것은 if 문은 제거할 수 있지만 적어도 2개의 for 문(패턴에 캐리지 리턴 정보를 넣으면 1개로 가능)이 필요하다.
 
그럼 이제 말하고자 하는 Assembled Sprite를 보자. 이것은 스프라이트의 raw data로부터 전처리를 통해 inline assembly language를 생성한다고 했다. 그리고 그 구조는 다음과 같다.
이것은 스프라이트 데이터이긴 하지만 데이터 자체가 하나의 컴파일 가능한 inline assembly language 이다.
 
    jmp @@START
    [sprite header]
 
@@START:
    [sprite data copying]
    [transparent data skipping]
    .......
    [sprite data copying]
    [transparent data skipping]
 
    ret
 
@@DATA00001:
    [sprite data]
@@DATA00002:
    [sprite data]
    .......
 
보시다시피 메인 루틴에 점프문은 전혀 없다. 계속 정해진대로 데이터를 복사하고 스킵한 후 ret를 만나서 함수를 끝내면 된다.
또한 사용하는 사람도 단지 함수처럼 부르기만 하면 알아서 출력이 된다.
 
데이터가 실행파일 속에 함수의 형태로 숨기 때문에 유출되기도 어렵고, 전체 foot-print도 작아진다.
첨부파일의 샘플을 기준으로 했을 때 , 일반적인 리소스 형식으로는 (22080+bmp헤더)bytes 가량이 증가하는데 반해 이 방식을 쓰면 18432bytes만 늘어 났다. (Delphi 5에서 테스트)
물론 단점도 있다.
 
1. 스프라이트 데이터에 출력 루틴이 삽입되기 때문에 특수효과를 주기 어렵다.
   (특수효과를 주려면 그것이 가능하도록 전처리기를 새로 만들어야 한다.)
2. 클리핑이 어렵다.
   (직접 frame buffer에 쓰는 것은 클리핑이 불가능하고 적어도 double buffering이 되어야 클리핑
    효과가 가능하다.)
3. 스프라이트 형식이 아닐때는 도리어 사이즈가 커진다.
4. 한 소스는 하나의 color depth에만 유효하다.

2. 첨부 파일 설명

Uni1.pas
    Assembled Sprite를 실험할 수 있는 3개의 버튼이 있는 예제 프로그램이다.
    왼쪽 두 개의 버튼은 각각 Assembled Sprite와 일반 스프라이트 출력 루틴에 대한 속도 비교다.
    가장 오른쪽의 버튼은 'test.bmp'를 읽어서 그것은 Assembled Sprite 코드로 출력 해 주는 예제다.
    모든 용법은 이 안에 들어가 있다. 예제는 비록 15bits 컬러에서 동작하지만 충분히 8 bits index
    mode나 32 bits true color mode에서도 동작하도록 수정할 수 있을 것이다.
 
UFrameBuffer.pas
    TBitmap을 상속받아 만든 가상 frame buffer를 구현하기 위한 클래스다.
    이 frame buffer를 통해 스프라이트를 출력하고 최종적으로 form에 나타나게 한다.
 
UAsmSprite.pas
    Assembled Sprite를 만들기 위한 클래스다.
    특정한 bmp와 그에 대한 color key를 지정하면 TStringList의 형태로 코드를 생성해 낸다.
    현재는 15 bits 전용이지만 약간의 수정으로도 충분히 다른 모드용을 만들 수 있을 것이다.
 
USpriteData.pas
    Assembled Sprite를 통해 만들어진 스프라이트 데이터가 있는 unit다.
 

3. 첨부 내용 설명 (UAsmSprite.pas)

    이 unit은 하나의 클래스로 이루어져 있다. 기능은 TBitmap으로 Assembled Sprite 스트링을 생성하는 역할을 한다.

    아래는 그 클래스의 선언이다.

    ==========================================================================
    TAssembledSprite = class
    public
        constructor Create(const srcImage: TBitmap; const Id: string;
                           colorKey: word);
        
    procedure Free();
    public
        spriteCode: TStrings;
    end;
    ==========================================================================

    생성자에는 srcImage라고 하는 TBitmap 클래스의 인스턴스를 받는다. 현재는 srcImage은 PixelFormat이 pf15bit만 지원된다. (하지만 충분히 확장 가능하다) 그리고 Assembled Sprite의 이름의 접미사를 붙일 수 있는 Id라는 스트링을 받으며, 마지막 파라미터로 컬러키를 받는다. 물론 이때의 컬러키도 pf15bit 형태로 주어야 한다.

    이 클래스는 생성 시 생성자에 파라미터를 넘기기만 하면 Assembled Sprite로 구성된 TStringList형의 public 필드인 spriteCode에 그 결과가 들어 가게 된다.

    그렇다면 이번에는 Assembled Sprite를 만드는 원리를 알아 보자.

    다음과 같은 스프라이트 데이터(1byte 단위)가 있으며 컬러키는 $00 이라고 하면,

    00 00 00 00 00 1A 35 36 85 00 00 00 15 7F 35 56 00 00 1E 3D 00 00 00

    아마도 고전적인 코드에는 어떤 정해진 스프라이트 처리 루틴에서 00인 컬러키를 판별하여, 컬러키가 아닐 때만 화면에 출력하려고할 것이다. 하지만 Assembled Sprite에서는 위의 것을 다음과 같은 assembly language 코드로 만들어 준다.

    =========================================================================
    mov  edi, 5 // 출력 버퍼 5 bytes 이동
    lea  esi, @@DATA0001 // @@DATA0001 번지를 esi로 지정
    mov  ecx, 4 // 복사할 바이트 수 지정
    rep  movsb  // 복사

    mov  edi, 3
    lea  esi, @@DATA0002
    mov  ecx, 4
    rep  movsb

    mov  edi, 2
    lea  esi, @@DATA0003
    mov  ecx, 2
    rep  movsb

    mov  edi, 3
    ......

    ret
    // 함수 끝

    @@DATA0001:
        db $1A, $35, $36, $85
    @@DATA0002:
        db $15, $7F, $35, $56
    @@DATA0003:
        db $1E, $3D
    ==========================================================================

    컬러키를 제외한 데이터만 함수 내에 포함되며 스킵되는 컬러키에 대한 정보는 assembly language로 표현된다. 여기서는 어떠한 점프문도 어떠한 비교문도 사용되지 않고 오직 출력을 위해 버퍼 포인터를 증가시키는 명령만이 존재한다.

    이렇게 만들어진 assembly language는 spriteCode라는 TStringList 형 인스턴스 안에 '함수의 소스 코드' 형태로 존재한다. 따라서 이것을 복사한 후 실제 자신의 파일에 삽입하여 컴파일 하기만 하면 모든 것이 끝이다. (내용은 첨부한 예제를 직접 실행시켜 보면 이해가 쉬울 것이다.)

    가장 간단한 사용 형태는 다음과 같다.

    ==========================================================================
    var
        bgImage: TBitmap;
        spriteCode: TAssembledSprite;
    begin
        bgImage := TBitmap.Create;
        bgImage.LoadFromFile(
    './test.bmp');
        bgImage.PixelFormat := pf15Bit;

        spriteCode := TAssembledSprite.Create(bgImage,
    'Test', $001F);
        Memo1.Lines.Assign(spriteCode.spriteCode);
        spriteCode.Free;

        bgImage.Free;
    end;
    ==========================================================================

4. 첨부 내용 설명 (UFrameBuffer.pas)

    이 클래스는 Assembled Sprite를 위해서 특별히 구현한 가상 frame buffer 클래스이며 실제로는 TBitmap을 상속받아 이용하고 있다.

    아래는 그 클래스의 선언이다.

    ==========================================================================
    TSpriteProc = procedure(const pAddr: pointer; const pitch: longint); register;

    TFrameBuffer =
    class(TBitmap)
    public
        constructor Create(w, h: integer);

    private
        m_pSurface: pointer;
        m_pitch: longint;

    public
        procedure DrawImage(x, y: integer; const proc: TSpriteProc); overload;
        
    procedure DrawImage(x, y: integer; const bitmap: TBitmap;
                            
    const colorKey: word); overload;
        
    procedure Flush(const canvas: TCanvas; x, y: integer);

    end;
    ==========================================================================

    TSpriteProc 타입은 Assembled Sprite로 만들어진 함수에 대한 타입이다. 함수이지만 변수처럼 쓸 수 있게 하기 위해서 이런 형태로 선언했다. 자세한 용법은 아래의 내용을 참조하면 된다.

    TFrameBuffer 클래스의 생성자 Create()는 frame buffer의 크기를 정의한다. w와 h는 각각 너비와 높이를 나타내며 color depth는 일단 16 bits로 고정되어 있다. (역시 수정 가능하다.)

    public 메소드는 딱 3개가 있다. 그 중에 첫 번째 DrawImage()는 (x, y)의 위치를 좌상단 좌표로 하여 Assembled Sprite를 출력하게 해준다. 실제 proc은 함수이지만 여기서는 파라미터처럼 사용하면 된다. 두 번째 Drawimage()는 TBitmap 클래스의 이미지 데이터를 받아서 똑같은 출력을 한다. 이것이 존재하는 이유는 단지 속도 비교를 위해서이다. 그리고 마지막 메소드는 Flush()이다. 이것은 지정한 canvas의 (x, y)의 위치를 좌상단 좌표로하여 frame buffer의 내용을 화면에 출력한다.

    구현 자체는 아주 간단하므로 첨부된 소스를 통해서 확인하기 바란다.

5. 첨부 내용 설명 (USpriteData.pas)

    Assembled Sprite를 통해 만들어진 함수를 따로 unit으로 떼었다. 보시다시피 코드 자체의 라인수가 내용에 비해 굉장히 크기 때문에 따로 분리한 것이다. 라인 수는 많지만 이 데이터를 직접 bitmap으로 달고 다니는 것보다는 바이너리의 크기가 작아진다.