Tuesday, July 15, 2014

Crossbridge Quake1 Example Simplified

I made some minor modifications of the official Crossbridge Quake1 example so that the source code is easier to use and mod. The original source code uses a very different workflow from the old Alchemy, that is, compile everything, including the C/C++ code, AS code and data file, into the final swf file directly via a single Makefile. Although this approach is elegant and simple as a sample of the Crossbrige SDK, it will cause several inconveniences for a real project. The drawback is obvious, once you changed something, either the C/C++ code, AS code or the data file, you need to recompile everything.

On the other hand, the old Alchemy workflow, that is, compile all the C/C++ code into an independent swc first, then import the swc in the AS project, is more efficient. Although it is possible to use several separated makefiles for different steps of the whole compiling process, the old Alchemy workflow is more friendly to Flash developers.

So what I did in the simplified version is

1. Changed Makefile to compile the swc instead of swf.
The original Makefile mixes gcc and asc, where gcc is used to compile all the C source files into .o files and the final swf file, and asc is used to compile the "Console.as" file. I just removed the part for compiling the "Console.as" file. (The modified "Console.as" file will be used as the Main class for the AS project, in which it will be linked with the swc file later.) Then, changed the option "-emit-swf" for gcc to "-emit-swc" so the swc file, instead of the swf file, will be created.

2. Simplified the way for supplying the file "pak0.pak" to C/C++, so no "genfs" needed.
Crossbrige introduces genfs for the file system. This can be seen as an advantage over Alchemy, but personally, I feel it is not so easy to use. Crossbrige uses the genfs tool to convert all your files/folders needed in your C/C++ code into plain AS text files so the compiler can compile the converted data files into your swf. However, this is a disaster for modding - every time you change the data files, you need to genfs them into AS files first before they can be used in your Corssbrige projects. Fortunately, there is a simple alternative way (the Alchemy-like way, not officially documented, but actually use the same API as the genfs way) for supplying data to C/C++, see this post http://bruce-lab.blogspot.com/2013/11/migrating-from-alchemy-to.html for details. In this way, you handle all the data using AS3 only, so you can embed files using AS3 code, or load them on the run using a URLLoader.

There is one problem, beyond the quake example. That is when you need to supply many files or folders to C/C++. Manually embedding them in pure AS3 is troublesome. In this scenario, it seems the genfs tool will save your the trouble. However, this can also be solved by pure AS3. My solution is zip everything as one package first, then use some AS3 zip library (such as http://nochump.com/blog/archives/15, http://codeazur.com.br/lab/fzip/) to supply all files programmably. (Actually, the quake data file is a zipped package of many files and folders in a custom format ".pak", and the engine itself takes care of all the unzipping, parsing and loading processes.)

3. Created the FlashDevelop project.
Added the swc file generated previously to lib, modified the "Console.as" file, which imports classes/packages needed from the swc file, and embed the game data file in the Main class "Console.as". Besides, an unimplemented preloader class is added in the AS project. The official way for adding preloader directly for Crossbrige generated swf is also not very handy. With an AS project, it's trivial work to implement the preloader.

You can find the source code:
(SVN, source code only) http://flaswf.googlecode.com/svn/trunk/flaswfblog/Tutorials/CrossBridge_Example_Quake1_Simplified/
(All in One Package, everything you need to compile) http://code.flaswf.tk/2014/07/sdlquake1-for-crossbridge-simplified.html

What I learned from the original quake example

The most important files in the example are "Console.as", "sys_sdl.c", "snd_mix.c". The file "sys_sdl.c" is where you can find the C main function, besides, in the file "sys_sdl.c", function
engineTick() is the main game loop, and
engineTickSound() is the main sound loop.

The sdlquake example almost answers most questions when you want to port an SDL based application or game to Flash with Crossbridge.

Q1. How to start the main game loop?
There are two ways showed in the example to run the main game loop in each frame. The first one is to call the C/C++ main loop function engineTick() from AS3 in an EnterFrame event handler, see the line

CModule.callI(enginetickptr, emptyArgs)
in "Console.as". This is single threaded and everything, including both the main game loop and the screen buffer rendering, runs in the main UI worker only.
The second one is to use a background worker, so you can put the main loop function in an infinite while(true) loop in the C main function. In this case, you need two threads, the background worker (all C code) is running the main game loop and does the blitting job while the main UI worker only renders the screen buffer in the EnterFrame event handler. see this post http://bruce-lab.blogspot.com/2014/05/migrating-from-alchemy-to.html for more.

Q2. How to render the ScreenBuffer?
As I explained in http://bruce-lab.blogspot.com/2012/12/migrating-from-alchemy-to-flascc.html, all you need to do is to get pointer to the Screen Buffer(data/array of colour values) of your C/C++ code, see the line
vbuffer = CModule.getPublicSymbol("__avm2_vgl_argb_buffer")
in "Console.as", then create a BitmapData and use the setPixels method to render the buffer.

Q3. How to get the Keyboard input?
Of course you need to listen the KEY_UP and KEY_DOWN events in AS3. Then you can implement the "read" function in "Console.as", so that key inputs can be read by C using normal C IO.

Q4. How to get the Mouse input?
The example showed how to let C know the mouse position. Firstly, mouse position "mx" and "my" can be captured in AS3.
If you're running the main loop in the UI worker (ST), get the pointer of the C variables for storing mouse position:
vgl_mx = CModule.getPublicSymbol("vgl_cur_mx")
vgl_my = CModule.getPublicSymbol("vgl_cur_my")
Then use domain memory to update the values of the two C variables directly in AS3:
CModule.write32(vgl_mx, mx)
CModule.write32(vgl_my, my)

If you're in MT mode, things are a little tricky. The "handleFrame()" in "sys_sdl.c" is for getting mouse input. The function used inline asm to get the values for the mouse positions.
inline_as3(
"import com.adobe.flascc.CModule;\n"
"%0 = CModule.activeConsole.mx\n"
"%1 = CModule.activeConsole.my\n"
: "=r"(vgl_cur_mx),"=r"(vgl_cur_my) :
);
Since you're running the main loop in the background and mouse position can only be retrieved from the UI worker, you need the
avm2_ui_thunk(handleFrame, NULL);
(in C, the main game loop, where you need to update the mouse position, to queue up uiThunk request for calling handleFrame on the UI Worker)
and
CModule.serviceUIRequests()
(in AS3, the EnterFrame handler, to service the pending uiThunk request) combination.

Q5. How to play the sound data?
Use Sound and SoundChannel class in AS3 to play sound data. To get the sound sample data, use inline asm to writeFloat, and the proxy variable "sndDataBuffer" in AS3, see "snd_mix.c" and "Console.as" for details.

Q6. How to supply the files to C?
See the previous section where I talked about the file system.

Some other notes:

1. If you're using 32-bit system with 32-bit java, you need to pass the option -jvmopt="-Xmx1G" to gcc/g++, otherwise, you may get the error "LLVM ERROR: Error: Unable to launch the Java Virtual Machine. This usually means you have a 32bit JVM installed or have set your Java heap size too large. Try lowering the Java heap size by passing "-jvmopt=-Xmx1G" to gcc/g++."

2. On windows, when compiling the original source code, if the compiler complains that "Console.as" is locked and not accessible, try to use some software to unlock the file "Console.as", then compile again.

Special thanks to Michael Guidry (MadGypsy on quakeone.com) for motivating me to write this post.