Локализация парсера

В данной статье пойдет речь о способе локализации парсерной игры, как мне кажется, довольно нетривиальном процессе. Буду рассказывать об игре Антиквест, которая была успешно переведена на английский и участвовала в конкурсе ifcomp, а также была одним из экспонатов на фестивале медиапоэзии «101» в спб. Скажу честно, локализация закладывалась заранее, перед началом проектирования игры. Если мы сначала делаем игру, а потом стараемся локализовать, тогда проблем намного больше и приходиться просто переводить весь исходный код, каждую сущность. Другое дело, когда мы готовимся заранее и выделяем общие части кода, независимые от локали, а потом переводим только строки для вывода, делаем удобную инфраструктуру. Ниже немного нестандартная UML-диаграмма главных компонентов игры, с точки зрения удобства перевода:




Строки

Для большинства строк самое простое решение, используемое во многих движках — сделать макросы и уже их использовать вместо прямого указания текста. Конечно, можно было напрячься и использовать такой инструмент как QtLinguist, немного приспособив исходники, но делать такого не стал. Для удобства, в идентификатор макроса входит принадлежность к объекту и методу/реакции. Пример русского текста(strings_ru.t):
#define IDS_STARTROOM_LDESC 	'Длинная койка посреди пустынной белой комнаты. Выход на западе.'
#define IDS_STARTROOM_WAIT 	 'Я погрузился в глубокие размышления о смысле бытия. Потом любопытство меня пересилило и я вышел в коридор.'
#define IDS_STARTROOM_NOEXIT  'Я с разбегу врезался в стену и пробил её! Ну и силища у меня, оказывается. Потом я медленно вошёл в образовавшийся проём.'
Пример текста(strings_en.t):
#define IDS_STARTROOM_LDESC  'There’s just a long bed in the middle of this empty white room. The exit is to the west.'
#define IDS_STARTROOM_WAIT   'For a while, I’m lost in deep thought about the meaning of life. Finally, curiosity gets the best of me, and I step out into the corridor.'
#define IDS_STARTROOM_NOEXIT 'I throw myself against the wall, and it breaks! Boy, am I strong! After that, I slowly walk through the breach I made.'
Чтобы переводчик мог работать в Word, можно довольно легко поубирать макросы и оставить только кавычки (в Notepad++), а затем он может править не боясь задеть инструкции и не отвлекаясь на подчеркивания аббревиатур. Что собственно я и сделал. Самое главное, не переставлять строки местами.

Объекты

В парсерном движке TADS используется ООП-парадигма, а русские библиотеки Андрея Гранкина (RTADS) являются переводом тех же объектов и классов, которые есть в английском оригинале. Осталось выбрать, как провести черту между логикой с основными объектами игры и их текстовым описанием. Я решил сделать просто — объявления классов сделать в отдельных файлах, а реализацию конкретных методов уже в одном месте — основном файле игры. Определение койки на русском (из файла objects_ru.t):
startbed : anyfixed, beditem
  desc = 'койка/1ж'
  noun = 'койка/1ж' 'кровать/1ж'
  adjective = 'длинная/1пж'
  isHer=true
;
Объект startbed является реализацией классов anyfixed, в котором уже определены заглушки реакций на неподвижный предмет для этой игры, и beditem, который позволяет работать с объектом как с кроватью, на которую можно лечь и встать.
Для английского варианта (файл objects_en.t):
startbed : anyfixed, beditem
  sdesc = "bed"
  noun = 'bed' 'bunk'
  adjective = 'long'  
;
Видим, что для разных языков используются разные лексические свойства (на русском поле desc, и isHer, так как работает генератор падежных форм и автоматически заполняет остальные свойства. Подключаться тот или иной файл будет в зависимости от значения глобального макроса с языком для компиляции. Далее, мы можем вводить игровую логику, не заботясь о языке (antiquest.t):
////////////////////////////////////////////////////////////////
//Своя комната
////////////////////////////////////////////////////////////////
modify startroom
 ldesc = printf(IDS_STARTROOM_LDESC)
 wait={ printf(IDS_STARTROOM_WAIT); Me.travelTo(coridorroom); }
 noexit = { printf(IDS_STARTROOM_NOEXIT); return coridorroom; }
 jump = { printf(IDS_STARTROOM_JUMP); Me.travelTo(coridorroom); }
 dig = { printf(IDS_STARTROOM_DIG); Me.travelTo(coridorroom); }
 west = coridorroom
 //действия с комнатой
 hit = { printf(IDS_STARTROOM_HIT); Me.travelTo(coridorroom); }
 talkdesc = {printf(IDS_STARTROOM_TALK);  Me.travelTo(coridorroom); }
 clean={printf(IDS_STARTROOM_CLEAN); Me.travelTo(coridorroom);}
 search={printf(IDS_STARTROOM_SEARCH); Me.travelTo(coridorroom);}
 listendesc = printf(IDS_LISTEN_DEFAULT)
;

Глаголы

Реакции на глаголы неудобно оборачивать в строки, как и разделять на общую часть и языковую. Тут воспользовался прямым переводом глаголов (сначала был русский вариант). Из файла verbs_ru.t:
replace breakVerb: deepverb
	verb = 'разбить' 'сломать' 'уничтожить' 'порвать' 'ломать' 'разбей' 'сломай' 'уничтожь' 'порви' 'разорвать' 'разорви' 'ломай' 'убить' 'убей' 'ударить' 'ударь' 'врезать' 'врежь' 'бить' 'бей' 'атаковать' 'атакуй' 'напасть на' 'напади на' 'наброситься на' 'набросься на' 'ударить по' 'ударь по' 'ударить в' 'ударь в' 'врезать в' 'врежь в' 'врезать по' 'врежь по' 'дать по' 'дай по' 'дать в' 'дай в' 'бить по' 'бей по' 'бить в' 'бей в' 'стрелять в' 'стреляй в' 'выстрелить в' 'выстрели в'
	sdesc = "сломать"
	doAction = 'Break'
;

replace waitVerb: darkVerb
	verb = 'ж' 'ждать' 'подождать' 'жди' 'подожди'
	sdesc = "ждать"
	action(actor) =
	{
	    if (isclass(actor.location,room)&& !isclass(actor.location,nestedroom)) actor.location.wait;
		else if (actor.location.location != nil && (isclass(actor.location.location,room))) actor.location.location.wait;
		else "Прошло некоторое время...\n";
	}
;

Из файла verbs_en.t:
replace breakVerb: deepverb
	verb = 'break' 'ruin' 'destroy' 'fire' 'hit' 'kill' 'poke' 'jab' 'attack' 'kick'
	sdesc = "break"
	doAction = 'Break'
;

replace waitVerb: darkVerb
    verb = 'wait' 'z'
    sdesc = "wait"
	action(actor) =
	{
	    if (isclass(actor.location,room)&& !isclass(actor.location,nestedroom)) actor.location.wait;
		else if (actor.location.location != nil && (isclass(actor.location.location,room))) actor.location.location.wait;
		else "Pass some time...\n";
	}
;

Выводы

Локализация оказалась достаточно трудоёмким процессом, но к счастью, мне помогал Валентин Коптельцев, без которого вообще не появился бы перевод. Активные тестеры также сыграли значительную роль. Могу сказать так — лучше заранее готовиться к локализации и подготовить архитектуру игры, чем потом долго и упорно переводить на другой язык. С другой стороны, если бы перевод не требовался, то многие решения стали бы неудобными для написания игры и только мешали. Надеюсь, кому-нибудь пригодиться этот проект для реализации своей парсерной игры на нескольких языках.

Ссылки

Ссылка на демо-проект в гит-хабе: github.com/alastochkinheroku/demo_antiquest
Скачать gam-файл на русском: cloud.mail.ru/public/B6ZM/vJkQjc5S1
Скачать gam-файл на английском: cloud.mail.ru/public/DNkm/NscXQnU59

Нет комментариев