Next: Graphics backends, Previous: Translating, Up: Hacker's guide [Contents][Index]
Technically, Liquid War 6 is a collection of C functions which are exported to Guile. The main binary embeds a Guile interpreter, which will run a Guile script. This script calls the exported C functions, and glues them together.
It should be possible to implement the game without using Guile at all, using C code to make the various modules communicate together. This might seem an easier way to go, not involving several languages. However, using this script level is a good way to achieve several important goals:
Having Guile to implement high-level stuff also decreases, to some extent, the need for object-oriented features of C++. The big picture is : low level code that require speed, optimized data processing, is written in C. Code which is more high level and requires abstraction is written in scheme.
Liquid War 6 makes a heavy usage of threads. Early versions of the game did not have this feature but starting with 0.0.7beta, one can really consider the game is heavily threaded.
There’s basically:
So globally, if you have an SMP system, the game will be happy with it. It will also run on a single processor, as the program uses POSIX pthreads it’s capable to run on any computer which has pthreads implemented for it.
But, and this is a strong limitation, without pthreads, the game won’t run. At all. Or at least, not unless it’s almost completely rewritten.
The C code is splitted into several internal libraries. This allow independant testing of various game modules.
The main module, the most important one, is libker
, (stands for “kernel”).
This is were the core algorithm is. To some extent, the rest of the code is
just about how to provide this module with the right data and environment.
Logically, if you profle the game, you should find out that a great part
of the CPU time is spent here. Code here is about spreading gradients, moving
fighters and cursors.
The libmap
module is here to handle maps, it contains the code to manipulate
maps in memory. But it does not know how to load them from disk. This is the
responsability of another module, libldr
, which is linked against libraries
such as libpng or
libjpeg and does the job of transforming those
standard formats into a usable in-memory structure. The libgen
module
also works the same way, creating pseudo-random maps. There’s still a another
moduled involved in map handling, it’s libtsk
, whose job is to load a
level in the background. It has a 2-steps asynchronous loading system which allows
the game to load maps while the user interface is still responsive, and give
a preview of the map as soon as possible, when loading continues in the background,
building optimizing structures which are usefull when playing but not mandatory
just to show the map.
At the other end of the algorithm-chain, the libpil
module will “pilot”
things. It’s this module which will translate text readable orders (typically
adapted for network usage) into function calls. It has event lists, keeps
them in the right order, and will also permanently maintain
three different states of the game. A backup state which can be used any time
to go back in time and get the game in a stable 100% sure state. A reference state which
is correct but ever changing. Basically backup plus all the orders received
between backup and reference gives reference. And finally a draft state which
is as up to date as possible but might be wrong. This is typically interesting
in network game, where we want to show something moving, something fast, even
if there’s lag on the network and other computers fail to send information in time.
In this case we display draft while still keeping reference and updating it
when we finally receive valid informations. Backup would be used to send
bootstrap information when people are joining a new game, or to check up if
things are going right.
A special libbot
module is here to handle bot algorithms. A bot is just
a simple move
function which takes a game state as an input, and returns
an x,y
position, just the way a mouse handler would. How complex a
bot is “under the hood” depends on the type of bot. Current bots are really
basic. Additionnally, libsim
will run dummy fight simulations to find
out wether some team has a real advantage on another one, speaking of team
profiles depending on colors.
The libgfx
module handles all the graphics stuff. It is itself splitted
in several sub-modules, that is, it does not do anything but load a module
such as mod-gl1
which will actually contain the implementation. In an
object-oriented language, it would be an abstract class, an inteface. The
implementation does not need to be thread-safe. It’s better if it is, for
theorically it could be possible to fire Liquid War 6 with two display
backends running at the same time on the same game instance, but this code
has yet to be written, and it’s a rare dual headed configuration which
probably has no real-life usage. If only one graphics backend is activated
at a time, the rest of the implementation garantees there will never
be two concurrent calls to a function is this module. It is the
libdsp
(“display”) which handles this. It fires a thread for
rendering purposes, and sends information to this thread, detecting
automatically if it’s necessary to acquire a mutex and update rendering
informations. For the caller, this is transparent, one just has to
call an update function from time to time. The module will even perform
“dirty-reads” on a game state being calculated, to render things
in real time, as soon as possible.
An experimental libvox
module is under design/development and
might, in the future, provide a real-time voxel renderer. Still pre-alpha
stage.
To ease up the implementation of different graphics backends, a libgui
module contains code which is meant to be used by any graphics backend.
It’s just a factorisation module, containing common code and interfaces,
related to displaying things. This is where, for instance, one can find
a high level menu object. In the same spirit, libmat
contains
generic math, vector and matrix code, which is commonly used in 3D interfaces.
The libsnd
module handles sound. It’s also an abstract class, an interface,
which uses dynamic backends as implementations.
The libnet
module is a wrapper over different network APIs, it
handles Winsock and POSIX sockets in a uniform manner.
The libcli
and libsrv
contain network client and server code,
implementing the various protocols in dynamically loadable sub-modules.
It’s the role of libp2p
to glue this together, handle the list
of available servers, the message queue, verifying nobody is cheating,
and so on. All this modules share information about current game
state using code & structures defined in libnod
,use message
utilities (format, parse) defined in libmsg
and share code concerning connections in libcnx
. Additionnally, libdat
provides facilities to store old network messages and sort them.
The libsys
module contains most system and convenience functions, it handles
logs, type conversions, timer, memory allocation, it’s the fundamental
module every other module depends on. It has a compation libglb
module with all the Gnulib shared code.
The libhlp
is used to handle keywords and internal self-documentation
(this is what is used by --list
and --about
), libcfg
knows how to read and save config files, libcns
handles the console,
and libdyn
can load .so
shared files dynamically.
To glue all this, there are some Guile bindings with helper functions
available in libscm
which fills two needs, one being an easy way
to check if Guile linking is working correctly without requiring all other
modules to be available, and also performing automatic checks on some actions
such as registering or executing a function.
Finally there are small modules like libimg
(to make screenshots of the game)
which
have been separated because they required special libraries to link with
and/or did not really fit in existing modules for dependencies reasons.
So well, this is a lot of modules. The list might move a bit, but the big picture is here. Each module is testable separately.
Below is a Graphviz diagram, which shows the modules dependencies.
The most important memory structures in Liquid War 6 are:
lw6map_level_t
) : this contain the map immutable informations.
This is what resides in memory after a map has been loaded from the disk.
It contains all the various .png
and .jpeg
files stored
as pixel arrays, resampled if need, and also contains the various map
attributes. Once this structure is ready, the game is capable of displaying
the map on the screen, but it can not do anything with it yet.
lw6ker_game_struct_t
) : this one contains the same informations
as the previous structure, only the information has been post-treated so that it’s
ready for use by the core algorithm. It will, for instance, contain the famous
mesh structure, which groups squares by packets of 1, 4, 16, 64 or more. The reason
it’s been separated from the level is that operations such as creating the mesh
might require a lot of time. So to allow players to see the level while black magic
is still running in the background, it was required to make a difference between
what is required to view the map (“level”) and what is required to play on
it (“game_struct”).
lw6ker_game_state_t
) : contains all the variable, ever
changing game data. This is where the position of fighters is stored, their
health, and such things. It is designed to be synchronizable by using mostly
simple calls to memcpy
. It heavily relies on the previous structures,
the idea is that one can have several “game_state” plugged on a
single “game_struct”.
All these structures are defined in the ker/ker.h
header.
The core Liquid War 6 algorithm is 100% predictable, that is to say, given the same input, it will produce the same results, on any computer. Previous versions of the game also had this property. This is very important for network games, since in a network only informations such as “cursor A is at position x,y” are transmitted. Every node maintains its own internal game state, so it’s very important that every node comes with the same output given the same input.
For this reason Liquid War 6 never uses floating point numbers for its core algorithm, it uses fixed point numbers instead. It also never relies on a real “random” function but fakes random behavior by using predictable pseudo-random sources, implementation independant, such as checksums, or modulos.
There are also some optimizations which are not possible because of the predictability requirement, for instance one can not spread a gradient and move the fighters in concurrent threads, or move fighters from different teams in different threads.
If you read the code, you’ll find lots of checksums here and there, a global checksum not being enough for you never know where the problem happened. The test suite uses those facilities to garantee that the game will run the same on any platform.
Not being able to rely on a predictable algorithm would require to send whole game states on the network, and this is certainly way too much data to transmit. A moderate 200x200 map has a memory footprint of possibly several megabytes, so serializing this and sending it to multiple computers at a fast-paced rate is very hard, if possible at all, even with a high bandwidth. We’re talking about Internet play here.
Next: Graphics backends, Previous: Translating, Up: Hacker's guide [Contents][Index]