Developer: Westwood Studios
Publisher: Virgin Games
Genre: Strategy (Real-time) / Top-down
System requirements:
Instead of introducing
Dune 2 is a great strategy, one of the first in the genre. It makes no sense to grovel and say how great this game is for a whole generation of children of the 90s. And since I unexpectedly find it my pleasure to mess around with the code and port it in JavaScript, then of course my goal after TTD (
article ) inevitably became Dune 2. By luck, I didn’t think of starting with it, because I'm afraid I wouldn’t handled it. As it turned out, although Dune 2 is simpler in functionality than TTD, it was more difficult to port it, but more on that later.
Code base
The choice of the “correct” code base is the main factor in the successful porting of a project using
emscripten . For example, the use of
SDL , lack of multithreading is a good marker of the fact that porting will be successful. I looked through all the projects related to Dune 2, and stopped at
OpenDune . The chip that hooked me was a complete copy of all the behavior of the original game, including all its bugs. It seems that the code of this project was originally obtained semi-automatically from the original. In the code here and there are variables called
local_03FF , a lot of global variables, the code is very difficult to read. The most serious disadvantage of the source code base in multithreading, it caused a lot of problems when porting. But the result is really good, in the browser the game is very similar to the original, with the exception of the new pack of bugs.
')
So, the dry facts:
Language: C
Number of source files: 143
Number of lines of code: 59151
Binary size: 423.2 Kb
Size equivalent javascript: ~ 1000 Kb
Porting time: ~ 2 months
Later in this article will be described the difficulties that I encountered when porting. Surely it is not interesting to everyone, if so, then lower this subsection to "known problems".
Multithreading vs asynchrony
OpenDune has a rather interesting interrupt based multithreading model. To ensure multithreading, the game code is spinning in endless cycles at the moment of idle, it looks like this:
while (true) { uint16 key; key = GUI_Widget_HandleEvents(w); if (key = 13) { break; } sleepIdle(); }
When the application starts, the interval timer is initialized with the
setitimer function. This timer triggers an interrupt at regular intervals. It pauses the main thread of execution and allows the execution of arbitrary code. For JavaScript, the implementation of a similar timer is trivial, however, another porting path was chosen in order not to artificially divide the project into JavaScript and C implementations. It was decided to completely abandon the use of the
setitimer function; instead, the
sleepIdle () call was replaced with a timer event function, i.e. instead of idle, this function determines which scheduled events have come up and launches them for execution.
A more serious problem is internal
while loops; any occurrence of such a loop in JavaScript will inevitably cause the open tab of the browser (or the browser as a whole) to hang. This is due to the fact that most of the cycles expect user input (pressing the mouse button, keyboard), but the browser cannot handle events from input devices, they are placed in the execution chain after the current JavaScript block being executed. A possible way to solve this problem is to manually edit the code and transfer the problem code to asynchronous mode.
Small primerichik. Here is a draft code that causes problems:
void someProblemFunction() { { //open 1 } while (true) { // open 2 while (true) { // code 2 } // close 2 } { //close 2 } }
After agonizing speculative manipulations, asynchronous code:
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 }
Hell of a job. The core of the entire system is the
Async_InvokeInLoop function.
void Async_InvokeInLoop( void (*open)(), void (*condition)(bool* ref), void (*loop)(), void (*close)())
Async_InvokeInLoop - allows you to replace any
while (true) loop with an asynchronous equivalent. The function guarantees to call
open before the start of the cycle, and
close after the end of the cycle. References to the
condition and
loop functions are equal participants in asynchronous iteration, which is clear from the name. Iteration is implemented through the
Async_Loop function:
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(); } }
The game loop (or the timer in JavaScript) periodically jerks this function forcing everything in the game to spin. If the original function should return a result, then the problems are doubled - you have to save the result globally, and then retrieve it in other functions. Everything works by agreement. As a result, I have a hellish framework for asynchronizing a project, here is its interface:
#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
The synchronous nature of the game mutated into asynchronous, which pleased several funny bugs:
- If you call up the construction menu immediately before the computer opponent determines the next building to be built, then you can access its facilities (corrected)
- When loading the script, it was possible that the enemy’s facilities would receive 20,000–30,000 units of life instead of 150–200 (corrected)
- Due to synchronization errors - the game map can be redrawn directly on top of the dialogue with the mentat, though this is rarely shown (not fixed)
Known Issues
Due to the fact that the tester staff consists of me and my fictional friends, it is only known that:
- The game works in browsers Firefox, Chrome, Opera, Chrome (Android ~ 4)
- The game is completely passed for the house of the Harkonnens and no serious problems were found.
- A small number of missions completed in two other houses, there were no problems either
- The game cursor never changes (regardless of the selected action), it is intentional (it slows down)
- To scroll the map, use the minimap or keyboard arrows (use the keyboard more in the game)
- There is music, but no effects
- Artifacts may appear on the game map (very rarely, you will immediately understand), in this case opening / closing of the game menu helps
- The menu items work: save, load, restart the mission
- There is only one save slot in the game (for all houses)
Everything else works, or should work.
Tracker:
https://github.com/caiiiycuk/play-duneAre we playing?
http://play-dune.com/71
UPD1. Hotkeys:
habrahabr.ru/post/159501/#comment_5516325UPD2. The server is asking for, I give direct links with statics:
House Play AtreidesHouse Game OrdosHouse Play HarkonnenUPD3. The server has rested against the restriction tcpsndbuf - the total size of buffers, which can be used to send data via TCP connections. The restriction that the provider puts to me, I cannot afford a more productive server, sorry if you could not play. We are waiting for the load to return to normal.
UPD4. Yet the problem was in my crooked hands, now the server has to cope with the load.
UPD5. The “Invoice” button does not work in the spaceport, be careful, if you have not ordered a single unit in the spaceport and clicked “Send Order”, the “Invoice” button will automatically be pressed and everything will hang.
UPD6. New
project site.