Разработчик: Westwood Studios
Издатель: Virgin Games
Жанр: Strategy (Real-time) / Top-down
Системные требования:
Вместо введения
Dune 2 — это великолепная стратегия, одна из первых в жанре. Нет смысла распинаться и говорить насколько это великая игра для целого поколения детей 90х. А так как я, неожиданно для себя нахожу в удовольствие возиться с кодом и портировать его в JavaScript, то конечно же моей целью после TTD (статья) неизбежно стала Dune 2. По счастливой случайности я не додумался начать с неё, поскольку боюсь я бы не справился. Как оказалось, хоть Dune 2 и проще по функционалу чем TTD, но портировать ее было сложнее, но об этом далее.
Кодовая база
Выбор «правильной» кодовой базы является главным фактором успешного портирования проекта с применением emscripten. Например, использование SDL, отсутствие многопоточности являются хорошим маркером того что портирование пройдет с успехом. Я перебрал похоже все проекты так или иначе связанные с Dune 2, и остановился на OpenDune. Фишка которая меня зацепила — полное копирование всего поведения оригинальной игры включая все её баги. Похоже, код этого проекта изначально был получен полуавтоматическим путем из оригинала. В коде тут и там встречаются переменные с именем local_03FF, очень много глобальных переменных, код читать очень тяжело. Самый серьезный недостаток исходной кодовой базы в многопоточности, она вызвала много проблем при портировании. Но зато результат действительно радует, в браузере игра похожа на оригинал очень сильно, за исключением новой пачки багов.
Итак, сухие факты:
Язык: C
Количество исходных файлов: 143
Количество строк кода: 59151
Размер бинарника: 423.2 Кб
Размер эквивалентного JavaScript: ~1000 Кб
Время потраченное на портирование: ~ 2 месяца
Далее в этой статье будут описаны сложности с которыми я столкнулся при портировании. Наверняка это интересно не каждому, если так, то опустите этот подраздел до «известных проблем».
Многопоточность VS асинхронность
OpenDune имеет достаточно интересную модель многопоточности основывающуюся на прерываниях. Для обеспечения многопоточности, игровой код в момент простоя крутится в бесконечных циклах, выглядит это примерно так:
while (true) {
uint16 key;
key = GUI_Widget_HandleEvents(w);
if (key = 13) {
break;
}
sleepIdle();
}
При старте приложения инициализируется интервальный таймер функцией setitimer. Этот таймер вызывает прерывание через равные промежутки времени. Оно приостанавливает основной поток выполнения и позволяет выполнить произвольный код. Для JavaScript реализация аналогичного таймера тривиальна, тем не менее был выбран другой путь портирования дабы искусственно не делить проект на JavaScript и C реализации. Было решено полностью отказаться от использования функции setitimer, вместо этого вызов sleepIdle() был замещен функцией обработки событий по таймеру, т.е. вместо простоя эта функция определяет какие запланированные события подошли и запускает их на выполнение.
Более серьезная проблема — внутрение циклы while, любое появление такого цикла в JavaScript вызовет неминуемое зависание открытой вкладки браузера (или браузера в целом). Это связано с тем, что большинство циклов ожидают пользовательского ввода (нажатие кнопки мыши, клавиатуры), однако браузер не может обработать события от устройств ввода, они ставятся в цепочку исполнения уже после текущего исполняемого блока JavaScript. Возможный способ решения этой проблемы — ручная правка кода и перевод проблемного кода в асинхронный режим.
Небольшой примеричик. Вот черновик кода который вызывает проблемы:
void someProblemFunction() {
{
//open 1
}
while (true) {
// open 2
while (true) {
// code 2
}
// close 2
}
{
//close 2
}
}
После мучительных умозрительных манипуляций, асинхронный код:
void asyncSomeProblemFunction() {
Async_InvokeInLoop(
asyncSomeProblemFunctionOpen1,
asyncSomeProblemFunctionCondition1,
asyncSomeProblemFunctionLoop1,
asyncSomeProblemFunctionClose1);
}
void asyncSomeProblemFunctionOpen1() {
// code from open 1
}
void asyncSomeProblemFunctionCondition1() {
// code from loop 1 condition
}
void asyncSomeProblemFunctionLoop1() {
Async_InvokeInLoop(
asyncSomeProblemFunctionOpen2,
asyncSomeProblemFunctionCondition2,
asyncSomeProblemFunctionLoop2,
asyncSomeProblemFunctionClose2);
}
void asyncSomeProblemFunctionClose1() {
// code from close 1
}
Адская работа. Ядром всей системы является функция Async_InvokeInLoop.
void Async_InvokeInLoop(
void (*open)(),
void (*condition)(bool* ref),
void (*loop)(),
void (*close)());
Async_InvokeInLoop — позволяет заменить любой цикл while (true) асинхронным эквивалентом. Функция гарантирует вызов open до начала цикла, а close после завершения цикла. Ссылки на функции condition и loop являются равноправными участниками асинхронной итерации, что они делают ясно из названия. Итерация реализуется через функцию Async_Loop:
void Async_Loop() {
ScheduledAsync *top = STACK_TOP;
switch (top->state) {
case ScheduledAsync_OPEN: {
top->open();
top->state = ScheduledAsync_CONDITION;
return;
}
case ScheduledAsync_CONDITION: {
top->condition(&top->conditionValue);
top->state = ScheduledAsync_LOOP;
return;
}
case ScheduledAsync_LOOP: {
if (top->conditionValue) {
top->loop();
top->state = ScheduledAsync_CONDITION;
} else {
top->state = ScheduledAsync_CLOSE;
}
return;
}
case ScheduledAsync_CLOSE: {
popStack();
top->close();
free(top);
return;
}
default:
abort();
}
}
Игровой цикл (или таймер в JavaScript) переодически дергает эту функцию заставляя всё в игре крутится. Если исходная функция должна возвращать результат, то проблемы удваиваются — приходится сохранять результат в памяти глобально, и потом извлекать его в других функциях. Все работает по соглашению. В результате у меня получился адовый фреймворк для асинхронизации проекта, вот его интерфейс:
/*
* async.h
*
* Created on: 19.10.2012
* Author: caiiiycuk
*/
#ifndef ASYNC_H_
#define ASYNC_H_
#include "types.h"
extern void async_noop();
extern void async_false(bool *condition);
extern void async_true(bool *condition);
extern void Async_InvokeInLoop(void (*open)(), void (*condition)(bool* ref), void (*loop)(), void (*close)());
extern bool Async_IsPending();
extern void Async_Loop();
extern void Async_InvokeAfterAsync(void (*callback)());
extern void Async_InvokeAfterAsyncOrNow(void (*callback)());
extern void Async_Storage_uint16(uint16* storage);
extern void Async_StorageSet_uint16(uint16 value);
#endif /* ASYNC_H_ */
Синхронная природа игры мутировала в асинхронную, что порадило несколько забавных багов:
Если вызвать меню строительства непосредственно перед тем, как компьютерный противник определит следующее строение для постройки, то можно получить доступ к его сооружениям (исправлено)
При загрузке сценария существовала возможность что сооружения противника получат 20 000 — 30 000 едениц жизни вместо 150 — 200 (исправлено)
Из за ошибок синхронизации — игровая карта может перерисоваться прямо поверх диалога с ментатом, правда проявляется это редко (не исправлено)
Известные проблемы
Из за того, что штат тестеров состоит из меня и моих вымышленных друзей, известно только что:
Игра работает в браузерах Firefox, Chrome, Opera, Chrome (Android ~4)
Игра полностью пройденна за дом Харконненов и серьезных проблем не найденно
Небольшое количество миссий пройдено за два других дома, проблем так же не было
Игровой курсор никогда не меняется (вне зависимости от выбранного действия), сделанно намеренно (он подтормаживает)
Для прокрутки карты используйте миникарту или стрелочки клавиатуры (больше используйте клавиатуру в игре)
Есть музыка, но эффектов нет
На игровой карте могут появлятся артефакты (очень редко, вы сразу поймете), в этом случае помогает открытие/закрытие игрового меню
В меню работают пункты: сохранить, загрузить, рестарт миссии
В игре только один слот для сохранения (на все дома)
Всё остальное работает, либо должно работать.