30 ноября 2012 в 05:30

Dune 2: The Building of a Dynasty

Разработчик: 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_ */


Синхронная природа игры мутировала в асинхронную, что порадило несколько забавных багов:
  • Если вызвать меню строительства непосредственно перед тем, как компьютерный противник определит следующее строение для постройки, то можно получить доступ к его сооружениям (исправлено)
  • При загрузке сценария существовала возможность что сооружения противника получат 20 000 — 30 000 едениц жизни вместо 150 — 200 (исправлено)
  • Из за ошибок синхронизации — игровая карта может перерисоваться прямо поверх диалога с ментатом, правда проявляется это редко (не исправлено)


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


Из за того, что штат тестеров состоит из меня и моих вымышленных друзей, известно только что:
  • Игра работает в браузерах Firefox, Chrome, Opera, Chrome (Android ~4)
  • Игра полностью пройденна за дом Харконненов и серьезных проблем не найденно
  • Небольшое количество миссий пройдено за два других дома, проблем так же не было
  • Игровой курсор никогда не меняется (вне зависимости от выбранного действия), сделанно намеренно (он подтормаживает)
  • Для прокрутки карты используйте миникарту или стрелочки клавиатуры (больше используйте клавиатуру в игре)
  • Есть музыка, но эффектов нет
  • На игровой карте могут появлятся артефакты (очень редко, вы сразу поймете), в этом случае помогает открытие/закрытие игрового меню
  • В меню работают пункты: сохранить, загрузить, рестарт миссии
  • В игре только один слот для сохранения (на все дома)

Всё остальное работает, либо должно работать.
Трекер: https://github.com/caiiiycuk/play-dune

Играем?


http://play-dune.com/

71

UPD1. Горячие клавиши: habrahabr.ru/post/159501/#comment_5516325

UPD2. Сервер просядает, даю прямые ссылки со статикой:
Игра за дом Atreides
Игра за дом Ordos
Игра за дом Harkonnen

UPD3. Сервер уперся в ограничение tcpsndbuf — cуммарный размер буферов, которых может быть использован для отправки данных через TCP-соединения. Ограничение которое ставит мне провайдер, более производительный сервер позволить себе не могу, извините если вам не удалось поиграть. Ждем когда нагрузка придет в норму.

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

UPD5. В космопорте не работает кнопка «Invoice», будте внимательны, если вы не заказали ни один юнит в космопорте и нажали «Send Order», то автоматически нажмется кнопка «Invoice» и все зависает.

UPD6. Новый сайт проекта.
+159
20231
218
Caiiiycuk 77,0 G+

Комментарии (95)

+14
solver, #
Отличная работа.

Добавить мультиплеер и вы поможете потратить лишние рабочие часы многим работникам… ))
+1
vitaly_KF, #
Поддерживаю! Всегда мечтал о мультиплеере в дюне!
+3
raspezdal, #
Да, спасибо теперь я знаю как проведу свой вечер.

Одна из любимых игр детства, сколько часов провел за игрой то. Даже кучка кодов ещё помнится.
+2
dasty, #
Splurgeola и Lookaround навсегда поселились в моей голове :)
–1
raspezdal, #
Иногда грешил и с playtester, а вот с карт то пожалуй только sonicblast и duneruner вспоминается.
0
dasty, #
Из карт мне почему-то Firementat запомнился.
0
Wendor, #
Devastator…
+5
CraSS, #
А я играл без кодов, бо их не знал. Странно, кстати, т.к. iddqd и idkfa сидят в памяти железно.
0
Wendor, #
Коды давались после прохождения каждой миссии, чтобы можно было его ввести и продолжить следующую.
+3
stalker_by, #
o_O Коды?!

Моя жизнь никогда не будет прежней…
0
raspezdal, #
да ладно.
0
silvansky, #
А я ломал её artmoney и патчерами сэйвов… Тоже впервые про коды слышу )
+1
80x86, #
Дэз хэнд аппроачин.

Спасибо.
+32
dasty, #
Вообще-то я хотел поработать сегодня, но ладно.
+3
iGloom, #
Охохо! Потрясно!

Раздражают только теперь мои собственные попытки обводить юниты рамкой, и тыкать правой кнопкой мыши для приказов.
Обленились мы с этими интерфейсами новыми )
+3
Skerrigan, #
Но с другой стороны — почему бы все-таки не сделать маленькое улучшение?
+2
Caiiiycuk, #
Кстати есть проекты сохраняющие стилистику оригинальной Dune 2, но в тоже время вводящие улучшения в юзабилити (Dune Legacy). Мне же просто хотелось оригинальной игры, ностальгия…
+1
Skerrigan, #
На сколько мне не изменяет память, там нету у летающих транспортиров атоподбора харвестеров/поломанной техники. Это сильно омрачает. А ваш порт хорошо сделан. Ваш JS сильнее моего JS ^_^
+1
Newbilius, #
Таких проектов несколько, из рабочих минимум 4 штуки: gamesrevival.ru/games/55
0
Sterpa, #
У меня почему-то ностальгия именно по русифицированной Dune2.
Прям мурашки по коже бегут от томного голоса с легким киевским акцентом: «Дюна… Битва древних династий»)
и потом…
«На связи!» «Исполняю!»

«Строительство завершено»

Все-таки, в обязательном нажатии на кнопку с действиея, после выбора юнита, есть некая притягательная первородность. Попробовав его однажды, я так и не научился ловить потом кайф от выделения правой и указания места левой…
0
dasty, #
Кстати, в статье не упоминается о наличии горячих клавиш на клавиатуре: Attack, Move.
0
Caiiiycuk, #
Забыл, спасибо! Еще там работают:
Harvest, Retreat, Guard, Tab, Build
0
dasty, #
Пожалуйста. А еще у вас юниты не умеют ходить строем. Вроде бы их можно было привязывать друг к другу цепочкой давая команду Move на другого юнита, а управлять колонной перемещая головного.
0
raspezdal, #
да, кстати это тоже заметил, пробовал по всякому но строем не пошли, ибо по одному юниту на другой конец карты многовато времени уходило
0
dasty, #
Хот-кеи хоть как-то спасают.
0
Newbilius, #
А можно хоткеями переключаться между юнитами?
0
Caiiiycuk, #
Только в одном экране через Tab
0
Newbilius, #
И в оригинале так же? Блин, надо было читать документацию…
–6
lostmsu, #
Жаль, что не что-нить более новое вроде TiberianSun или Red Alert.
+6
sandello, #
Даешь Starcraft II в Firefox!
0
lostmsu, #
Это совершенно другое. Для Tiberian Sun и Red Alert, как и для дюны, есть open source движки.
0
Newbilius, #
Для RedAlert — сиигловые глючны, мультиплейерные… мультиплейерные.
А вот про Tiberian Sun переделку не слышал, не поделитесь?
+3
Caiiiycuk, #
Я думал о Warcraft II, но пока не решил. Времени капитально не хватает.
0
lostmsu, #
Варкрафт и так на многих платформах есть в виде Stratagus.
+2
Megas, #
По мере появления времени я работаю над воссозданием Dark Reign. Разработка ещё только на начальной стадии, но уже можно потыкать немножко: qmegas.info/dark-reign-html5/
+1
INCWADRA, #
Классно
0
mrjj, #
Невероятно круто и по технологическому стеку и из-за того, что игра классная и вызывает море ностальгии, медаль вам.
+1
SergioBarbery, #
Подскажите, а как отключить музыку? Я почему-то не нашел настройки.
0
Caiiiycuk, #
Эм) Похоже что никак, даже как то не подумал. Можно отключить audio tag в браузере.
0
ValdikSS, #
Блин, а в опере нет звука
+5
zikkuratvk, #
Ех, когда я в нее играл раньше, мне казалось, что в ней есть даже крутая графика.
0
barev, #
Тогда так и было.
+3
aszhitarev, #
Land of a sand. Home of a spice.
Тёплая ламповая дюна.
Спасибо!
+3
modernstyle, #
только харконнены только хардкор!
+2
BlessMaster, #
Хардкор — это ордосы. Харконнены — слишком сильная раса ))
+1
Ryotsuke, #
Гм, первая миссия за Харконенов прошлась сама. Я выбрал дом в меню и ушел на некоторое время, оставив игру открытой. Потом вернулся, нажал Proceed и меня сразу поздравили с прохождением миссии )
Opera
0
Caiiiycuk, #
:) Успешности миссии начинает проверятся спустя пол минуты — минуту после старта. Похоже предполагалось что игрок начнет что-либо строить за это вермя. При старте первой миссии у игрока достаточно денег для её успешного завершения. Вот такой вот костыль из OpenDune.
0
Ryotsuke, #
так миссия должна начинаться только тогда когда я игровое поле вижу, а не в брифинге, не?
0
Caiiiycuk, #
Не, структура достижений очищается перед брифингом, затем загружаются все данные миссии и брифинг в том числе. Ну вообще баг налицо, нужно исправить будет.
0
EGarbuzov, #
Тоже самое было ещё в оригинале. В первой миссии изначально на счету имелось нужное для победы количество кредитов, но иногда сразу после старта 1-3 кредита снимались. А иногда не сразу :trollface:
0
INCWADRA, #
Все очень здорово, а харвестором давить солдатиков нельзя? Или я что-то не так делаю?
0
Caiiiycuk, #
Можно, я это длеаю через хоткей M и указываю противника.
0
INCWADRA, #
попробую, я делал M и за противника, и он объезжал. Кажется в классике было так
0
blare, #
Миникарты не отображает открытый туман войны на хроме
0
Caiiiycuk, #
Миникарта показывает только если у вас построен и запитан Radar Outpost. Я играл в хроме и она работала. Если проблема сохраняется укажите вашу версию хрома.
0
blare, #
Тогда прошу прощения) Не знал об этом
+1
NeoN88, #
ИМХО неплохо бы найти художника, который отрисует всё заново. Тогда будет няшка
0
megaweber, #
Вперед на Kickstarter!
+2
silvansky, #
А мне кажется, после перерисовки потеряется весь шарм.
0
lexxpavlov, #
А мне кажется отличная идея. Только не навязывать перерисованную версию, а предлагать. Или открывать — выиграл в классической — стала доступна новая, и не одна. Дать пользователям возможность создавать свои «шкурки»
0
Dv0rsky, #
500 Internal Server Error
+1
Caiiiycuk, #
Сервер просядает, даю прямые ссылки со статикой:
play-dune.com/atreides/
play-dune.com/ordos/
play-dune.com/harkonnen/
+2
Maximus43, #
В 1993 держал с собой две дискеты с Dune 2. Каждый раз приходя к отцу на работу, ставил эту игру с дискет, играл, потом записывал файл с сохранением на дискету и стирал с диска, так как места там свободного было мало. А в 1994 дома появился собственный 386DX40, я тогда спаял Covox для параллельного порта и слушал хриплые голоса юнитов в динамиках советского магнитофона «Юность». И как хватало сил играть всю ночь напролет и потом идти на первую пару в универе? Вот времена был! Эх…
+1
ArjLover, #
О, черт, неужели за игрой в ЭТО прошло несметное кол-во часов моей жизни??? не верю!!! :)
0
silvansky, #
Супер! Сегодня день старых игрушек =)))
Жаль, на iPad не загрузилась.
Если добавите поддержку, можно будет сделать почти полноценную версию под iOS — весь движок в app запихнуть. В AppStore не пропустят, но можно ведь и в Cydia ;)
0
sphinks, #
По-моему сломали сайт.:) Хабраэффект или что-то обновляется/допиливается?
0
Caiiiycuk, #
No buffer space available

total       used       free     shared    buffers     cached
Mem:           500        433         66          0          0          0
-/+ buffers/cache:        433         66
Swap:            0          0          0


Не знаю что это, думаю как починить…
0
sphinks, #
Хм, судя по варнингу исчерпали лимит на память, но судя по выкладке свободной памяти прилично (если под буффер нет отдельной области памяти).
0
Caiiiycuk, #
Я нашел проблему — ограничение переменной numtcpsock = 450. Насколько я понял это максимальное количество открытых tcp сокетов доступных моему серверу. В настройках nginx я использовал 500 воркеров они захватывали все ресурсы сервера и парализовали его. Теперь все хорошо.
0
DaFive, #
Спайс маст флоу.
0
unwrecker, #
TTD на JS раньше не видел. Посмотрел. Впечатления двоякие: с одной стороны «Вау! Полноценная реалтайм игрушка на JS в браузере!»; с другой стороны оригинальный TTD у меня нормально работал на 486DX2, а этот умудряется лагать на Core i3. Нужен ли нам такой хоккей?

С Дюной, наверное, будет лучше: всё же она делалась под 286
0
dasty, #
Хм, у меня даже на Core 2 duo летает.
0
unwrecker, #
Ну не знаю. Firefox 17.0, Ubuntu 12.10 x64.
Скролинг подлагивает.
0
banderlog, #
как понять это:

0
Caiiiycuk, #
Идет загрузка данных, когда она дойдет до конца игра начнется. Если она прервалась это значит я перезапускал сервер и просто надо перезагрузить страницу. Сервер отдает медленно, т.к. большая нагрузка.
0
Caiiiycuk, #
Хмм… у вас случилось переполнение буфера судя по цифрам, возможно проблема в том что в JavaScript произошла ошибка. Какой у вас браузер?
0
MichaelBorisov, #
Автор, поздравляю! Construction complete!

Всегда мечтал поиграть в хорошие римейки Дюны, повторяющие динамику оригинала. В поделках вроде D2TM авторы стали намеренно менять баланс юнитов, пытаясь, например, повысить силу пехоты против танков. Но это уже не та Дюна получается. Спасибо вам!
0
mishinoleg, #
Отличная игра, мегареспект!

Немного подвисает когда возвращаюсь в активную вкладку, но это ничего.
Капитально повисла при постройке космопорта, заказал привести хавестер и всё повисло.
awesomescreenshot.com/04bo5dvc3

Chrome 23.0.1271.91 m
0
Caiiiycuk, #
Обидно что повисла, почаще сохраняйтесь ;)
0
mishinoleg, #
загрузился с сохранения, зависает также при заказе любого товара в Космопорте
0
Caiiiycuk, #
Я вам послал сообщение что нужно сделать что бы я смог решить эту проблему.
0
Caiiiycuk, #
Chrome Версия 24.0.1312.2 dev — работает, не работает кнопка «Invoice». Если вы зашли в космопорт, ничего не заказали и нажали «Send Order» то игра зависнет.
0
MichaelBorisov, #
Автор, а со звуком как, у меня в Хроме только музыка играет, причем не вся. Только в Ментате или во время атаки противника. Звуков игры нет вообще. Это так и должно быть, или у меня что-то с настройками браузера?
0
Caiiiycuk, #
Так и должно быть, в игре есть только музыка. Звуков нет.
Музыка должна быть вся, скорее всего не играет из за того что сервер не успевает ее отдовать.
0
SPBNike, #
А где взять оригинальные исходники Дюны? Я бы её улучшил :)
+1
Caiiiycuk, #
Дизассемблировать. OpenDune — 100% клон Dune 2.
+1
tmnTurtle, #
Эх, а я на 2000й вырос…
+1
CyberAP, #
Чтобы в неё по сетке порубиться надо было целый квест пройти, но игра отменная.
0
Pe4enie, #
Автор очень большой молодец! Здорово получилось! Теперь знаю, как проведу выходные :)
0
de_arnst, #
Спасибо вам большущее! Жаль планшет мой не выдерживает и останавливает работу хрома.
0
BlessMaster, #
Не хватает мышиных курсоров — очень дезориентирует.
А так — прикольно, даже постройки автоматически продолжают строиться при появлении кредитов — чего не было в тех версиях, в которые я играл.
0
c0d3r, #
Очень тормозно работает на встроенной графике Intel 4000 под Ubuntu 12.04
0
taktike, #
Гм, у меня пропали все сохранения :(
0
Caiiiycuk, #
Может вы браузер сменили или компьютер?
0
taktike, #
нет, не менял

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.