본문 바로가기

장난감들

Supreme Commander 한글입력기 구현하기

드디어 슈프림 커맨더(Supreme Commander)의 한글입력기를 구현했다. 이 게임은 오리지날 버전(영어권에서는 Vanilla라고 부른다)은 한글화 번역이 이루어져 출시가 되었으나 번역의 질이 굉장히 낮았고 특히 한글 채팅이 전혀 지원되지 않아 많은 유저들의 불만을 샀다. 그나마 많이 팔리지도 않아 확장팩인 Forged Alliance의 경우 아예 국내 출시조차 이루어지지 않았고, 결국 나(...)를 비롯한 매니아들은 해외 직수입 게임 전문점이나 지인을 통한 구매대행 등을 이용할 수밖에 없는 상황이다.

아무튼 그동안 한국 유저들의 오랜 갈망이었던 한글 채팅을 User Interface mod 형식으로 구현하는 데 성공했다. 슈컴이 워낙에 modding 확장성이 좋다고 알려져 있기는 했지만, 실제로 이번 기회를 통해 만져보니 정말 잘 만들었음을 알 수 있었다. 게임의 핵심적인 구동 엔진을 제외하고 나머지 다른 부분들—유닛의 움직임, 게임 시나리오, UI, 인공지능 등등—상당히 많은 부분이 lua 스크립트 언어를 통해 만들어져 있다. 게임 데이터들은 *.scd라는 파일에 들어있는데 이것은 실상 그냥 zip 압축파일이라서 누구든지 풀어볼 수 있도록 하고 있다. (별도의 복잡한 암호화 이런 거 없다.)

수프림 커맨더 한글입력기 최초 구현

실제 사용 모습.

실제로 이번에 한글입력기 mod를 제작해보니 게임 내부가 어떤 로직으로 돌아가는지 거의 다 들여볼 수 있을 정도로 개방적이었다. 그만큼 게임 품질에 자신감이 있다는 뜻일 수도 있겠다.

요즘 게임 내부 스크립트 엔진용으로 많이 쓰이는 lua는 그러나 약간 실망이었다. 함수형 언어의 특징을 강하게 가지고 있는 lua는 어찌보면 ruby와도 상당히 비슷하다. 하지만 실제로는 매우 단순화된 언어로, 자료 구조라고는 오직 dictionary와 비슷한 table밖에 지원하지 않고, 비트연산자 등 다른 언어에는 당연히 있을 만한 기능들이 없는 경우가 꽤 있다. 유니코드 인코딩도 지원하지 않아 결국 utf-8 인코딩 루틴까지 직접 다 짜야했다. 대신 그 table 자료구조가 굉장히 강력하여(metatable이라는 개념을 제공한다) 함수형 언어의 특징과 결합해 이것만으로 class라는 개념을 언어에 도입하는 것이 가능할 정도이다.

오토마타 루틴은 예전에 CS322 과목 프로젝트로 Python으로 구현한 한글입력기(정확히는, 영타로 입력된 문자열을 한타로 변환하여 한글 문자열로 만들어주는..)를 lua로 그대로 포팅하였다. 둘의 문법이 비슷하여 큰 어려움은 없었고, 다만 lua에서는 table의 index가 0이 아니라 1부터 시작한다는 점 때문에 조금 헷갈리는 정도였다.

걸림돌이 되었던 부분은 오토마타의 결과물로 나온 unicode 숫자 배열을 실제의 utf-8 문자열로 변환하는 것이었는데, 이 과정에서 나머지 연산과 shift 연산이 필요하여 결국 수동으로 구현해야 했다. 특히 lua의 숫자는 무조건 double이고 라이브러리에서 필요시 unsigned long으로 바꾸는 식이기 때문에 나눗셈을 수행할 때 항상 math.floor를 해줘야 한다.

local function modulo(a, b)
    return a - math.floor(a/b) * b
end

와 같이 구현할 수 있고, right shift 연산의 경우 간단하게 2의 제곱승으로 나눠주면 된다.

혹시 필요한 분들을 위해, unicode 숫자가 순서대로 담긴 table을 입력으로 받아 utf-8로 인코딩된 문자열로 내놓는 함수를 소개한다. (테스트는 엄밀하게 해보지 않았지만 적어도 한글은 잘 나온다. =3=3)

function conv2utf8(unicode_list)
    local result = ''
    local w,x,y,z = 0,0,0,0
    local function modulo(a, b)
        return a - math.floor(a/b) * b
    end
    for i,v in ipairs(unicode_list) do
        if v ~= 0 and v ~= nil then
            if v <= 0x7F then -- same as ASCII
                result = result .. string.char(v)
            elseif v >= 0x80 and v <= 0x7FF then -- 2 bytes
                --[[
                y = (v & 0x0007C0) >> 6
                z = v & 0x00003F
                ]]--
                y = math.floor(modulo(v, 0x000800) / 64)
                z = modulo(v, 0x000040)
                result = result .. string.char(0xC0 + y, 0x80 + z)
            elseif (v >= 0x800 and v <= 0xD7FF) or (v >= 0xE000 and v <= 0xFFFF) then -- 3 bytes
                --[[
                x = (v & 0x00F000) >> 12
                y = (v & 0x000FC0) >> 6
                z = v & 0x00003F
                ]]--
                x = math.floor(modulo(v, 0x010000) / 4096)
                y = math.floor(modulo(v, 0x001000) / 64)
                z = modulo(v, 0x000040)
                result = result .. string.char(0xE0 + x, 0x80 + y, 0x80 + z)
            elseif (v >= 0x10000 and v <= 0x10FFFF) then -- 4 bytes
                --[[
                w = (v & 0x1C0000) >> 18
                x = (v & 0x03F000) >> 12
                y = (v & 0x000FC0) >> 6
                z = v & 0x00003F
                ]]--
                w = math.floor(modulo(v, 0x200000) / 262144)
                x = math.floor(modulo(v, 0x040000) / 4096)
                y = math.floor(modulo(v, 0x001000) / 64)
                z = modulo(v, 0x000040)
                result = result .. string.char(0xF0 + w, 0x80 + x, 0x80 + y, 0x80 + z)
            end
        end
    end
    return result
end