История портирования Dune 2

Разработчик: Westwood Studios
Издатель: Virgin Games
Жанр: Strategy (Real-time) / Top-down
Системные требования:
     moder browsers

Вместо введения


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_ */


Синхронная природа игры мутировала в асинхронную, что порадило несколько забавных багов:


Известные проблемы


Из за того, что штат тестеров состоит из меня и моих вымышленных друзей, известно только что:

Всё остальное работает, либо должно работать.
Что бы сохранять и загружать игры нужно войти или зарегистрироваться



Войти через социальные сети