Developing the game Disciples 2 on Avalonia and WPF

This is how it feels

The article will discuss the resulting “engine”, the original game resources and how I adapted them for the project.

So Betrezen dies.

Let's start with the nuances:

  1. Only a battle of two squads is implemented (all types of attacks are available, but there are no bonuses from equipment/spells/potions), as well as several start screens. There is a battle mode of two random squads.

  2. I have no experience in game development, only classic desktop applications. Because of this, the architecture of the game “engine” was developed by intuition / needs, and should not be taken seriously.

  3. The code is available at GitHub under the MIT license. But the rights to Disciples II belong to Strategy First.

Navigation

  1. About Disciples II

  2. Where did the development start?

  3. General principle of operation

  4. Game database

  5. Text output

  6. Resource files, MQDB format

  7. Images and animation

  8. Music and sounds

  9. Video

  10. Arrangement of interface elements

  11. Run on Linux

  12. Conclusion

About Disciples II

Disciples II: Dark Prophecy (Russian: “Followers II: The Dark Prophecy”) — a turn-based strategy game for the PC by Strategy First, released on January 24, 2002. The second game in the series Disciples. The game is a sequel to the 1999 game Disciples: Sacred Lands.

The gameplay includes three main components:

  • Upgrading the player's capital, which gives access to new troops and opportunities. Learning new spells.

  • Using hero leaders leading small squads to scout, attack and capture territories.

  • Battle mode between two squads.

While retaining the above-mentioned key features of its predecessor, Disciples II's gameplay has been significantly improved.

Later additions were released “The Return of Gallean” (actually, a combination of two additions “Guard of Light” And “Guard of Darkness”), and “The Revolt of the Elves” (which added a new race and a campaign for it).

Where did the development start?

It was 2018 and I came across a performance@kekekeks where did he show the game BattleCity on Avalonia UI. I was amazed at how little markup and code it took to get working sprite graphics. Avalonia positioned itself as a cross-platform replacement for WPF, so I decided to combine business with pleasure: start learning the framework by developing a game (funny observation: I didn't manage to learn Avalonia this way).

The choice fell on Disciples 2 for several reasons:

  1. I love this game and was pretty familiar with its mechanics.

  2. It has sprite graphics that are easy to work with.

  3. There are a large number of articles and tools for modders that help them understand how to work with the original game's resources.

It quickly became clear that very little of Avalonia's capabilities were being used: controls Canvas, Image, TextBlockplus handling input events. For the sake of interest, I allocated interfaces and made an implementation for WPF. It became possible to launch the game on two different frameworks and compare the behavior. Another observation: WPF renders much slower than Avalonia.

The phrase “a samurai has no goal, only a path” perfectly symbolized the work on the project: I didn't know why I was doing it, but I was interested in doing it. In the last couple of years, this project has also become an outlet for me: at my main job, Skype has replaced Visual Studio, and I was very worried that I was losing my programming skills.

Now the project has reached the stage where I understand that further development does not make sense, as there are major problems that are difficult to solve on these frameworks. Nevertheless, the development of battles is complete, and it turned out quite interesting, especially with the creation of random squads. With this article I wanted to sum it up and share the developments that may be useful to someone.

Well, let's move from words to code!

General principle of operation

The game's “engine” is built on three main types:

IScene – scene. The entire game is divided into scenes: the main menu scene, the save selection scene for loading, the battle scene, etc. The scene contains a list of objects to display, manages it, and processes the player's actions.

ISceneObject – a visual object on the scene. An array of these objects is placed in Canvas and with the help of DataTemplate converted to the required control: TextBlock, Image or VideoView. As a result, the entire layout takes up about 100 lines! Here Avaloniaand here WPF.

GameObject – game object. Example of objects: button, animation, unit portrait. Combines some logic, can draw elements on the scene, store state, react to user actions. Supports component logic, which allows you to assemble an object like a constructor. For example, a button and a unit portrait must process a mouse click on them, so both contain MouseLeftButtonClickComponentwhere the general logic for processing clicks is implemented.

There is also a timer that tries to trigger 60 times per second (= 60 FPS). Each time it triggers, the player's actions are processed and, if necessary, individual objects are redrawn (the next frame of animation is displayed, the object is selected, etc.).

As you can see, there is no rocket science here. If you add a new one to the array ISceneObjectthen it will be displayed on the screen, as it will change ItemsSource at Canvas. It is similar with deletion. You can also change objects, due to INotifyPropertyChangedthey will also be redrawn. That is, Avalonia/WPF takes care of all the work.

Game database

In the original game, all information about units, buildings, artifacts and the like is in .dbf files. You can view them using Microsoft Excel.

The part of the table that describes the unit type

The part of the table that describes the unit type

These files can be easily read and parsed using NDbfReader (now marked deprecated). But it turned out to be inconvenient to work with this further, my soul demanded ORM, which works only with real databases. I solved the problem head-on: I made a migrator that reads data from .dbf files and uploads them to the corresponding database tables sqlite. The game only interacts with sqlite by using EntityFramework.

There are a lot of tables in the game, trying to figure out what each column means is very tedious. Therefore, the migrator is written only for files that are needed for the battle scene. DB structure sqlite turned out to be the following:

The number of relationships between tables is one of the reasons why I wanted to use ORM

The number of relationships between tables is one of the reasons why I wanted to use ORM

Code links:

  1. DBF file parser.

  2. DBF Migrator -> sqlite.

  3. sqlite + EF project.

Text output

The rows are stored in the database in two tables:

  1. GlobalTextResource (in the original game file Tglobal.dbf) contains the names of units, spells, items, etc. All text links within the DB point to this table (for example, UnitType.NameTextId).

  2. InterfaceTextResource (in the original game file TApp.DBF) contains lines for interface elements: buttons, hints, headings, etc. Some of the links are in the interface file (more on that below in “Arrangement of interface elements”), the rest is hardcoded.

Lines lying in InterfaceTextResourcecan also contain formatting and placeholders. For example, a string with an identifier X005TA0423 contains a description of the unit's characteristics and looks like this:

\s110;\fmedbold;Level:\t\fnormal;%LEVEL%\n\fMedbold;XP:\t\fNormal;%XP%\n\fMedbold;HP:\t\fnormal;%HP1% / %HP2%\n\fMedbold;Armor:\t\fNormal;%ARMOR%\n\fMedbold;Immunities:\t\fNormal;\p110;%IMMU%\mL0;\fMedbold;Wards:\t\fNormal;\p110;%WARD%
And it is displayed like this

And it is displayed like this

Formatting allows you to set the font, specify any RGB color for text/text outline/background, and set alignment and indents.

The fonts in the original game are in the folder Interf and have an extension .mft. The file contains a pixel mask for each character. More details structure described here.

Menu font in the utility from Sergey A. Klochkov (HSerg)

Menu font in the utility from Sergey A. Klochkov (HSerg)

The correct solution for displaying text is to use a font file to generate a pixel-by-pixel image and place it on the stage, but I opted for a simpler option, so the text is displayed using a ready-made control. TextBlock. To each .mft The file is associated with font size, boldness, style (italic/normal). Since fonts can alternate within one line, then is used InlineCollectionconsisting of individual Run/TextBlock with different styles.

Solution with TextBlock has the following disadvantages:

  1. The font used is not the original game font, but a visually similar one PT Serif.

  2. The text is in vector format, and the images are raster – so it stands out against the background of the stretched 800*600 image.

  3. Support for text outlines turned out to be too complex, as well as some types of indents, so text in some places merges with the background, and in others it is incorrectly formatted.

Code links:

  1. Text style parser.

  2. Converter for creating InlineCollection Avalonia.

  3. Special TextBlock for WPF.

Resource files, MQDB format

All images and animations, some sounds and videos are packed into files, each of which has the following structure:

MQDB
[28_MAGIC_BYTES]
MQRC[RECORD_1]MQRC[RECORD_2]...MQRC[RECORD_N]
[FILE_1][FILE_2]...[FILE_N]

MQDB And MQRC – these are ordinary lines.

Structure RECORD contains the file identifier, its size, and the start in the resource file (offset in bytes from the start of the file).

Structure FILE – the contents of the file, which can be anything.

The container described always contains a special file that contains the names of all other files in the resource. It has the following structure:

[FILENAME_1][RECORD_INDEX_1]
[FILENAME_2][RECORD_INDEX_2]
...
[FILENAME_X][RECORD_INDEX_X]

The game accesses files in the resource by name. To find its contents, you need to:

The resource may also contain other special files (usually named -{XXX}.DAT or -{XXX}.OPT), which contain additional metadata (examples of such files are given for the images below).

Code links:

  1. Basic parser for MQDB files.

Images and animation

Images and animations packed in files with the extension .ff (format MQDB), in the original game can be found in folders Imgs And Interf. To extract something from this file and display it on the screen, you need to dance with a tambourine for a long time and persistently. Almost all the files inside the container are ordinary images with the extension .pngbut without an alpha channel. And almost every one of these images is a puzzle that needs to be assembled.

Images can be divided into 4 types:

  1. A ready-made image (extremely rare).

The face of Ashkael, guardian of the capital of the Legions of the Damned

The face of Ashkael, guardian of the capital of the Legions of the Damned

  1. It is possible to assemble one other image.

Portrait of Ashkael: Before and After Transformation

Portrait of Ashkael: Before and After Transformation

  1. You can collect many small images (usually these are elements of the scene interface).

All elements of the battle scene interface

All elements of the battle scene interface

  1. Several images are collected to form an animation.

Ashkael attack animation

Ashkael attack animation

Final animation

The “slicing” algorithms are described in three special files:

-INDEX.OPT: contains records that the image with the name XXX is in the base image YYY.

-IMAGES.OPT: contains records from which parts of the base image YYY the image consists of XXX. There is also additional metadata here: palette, transparent color, transparency application algorithm.

-ANIMS.OPT: contains records that animation with the name ZZZ consists of images XXX1, XXX2…, XXXN.

Displaying an image on stage XXX it turned out like this:

  1. Find the base image file YYYusing the library SkiaSharp We convert it into a byte array and set up transparency.

  2. Calculate the final image dimensions XXX. The image can be 800*600 in size, but in fact the opaque part will be 100*100. This is what we will display on the stage – this saves memory and speeds up rendering.

  3. Create a new byte array according to the image size, fill it by copying parts from the image YYY.

  4. We pass the byte array to the Avalonia/WPF framework, where it is converted into WritableBitmap and is brought onto the stage.

The algorithm is terribly non-optimal, both in time and memory. I'm sure the original game engine handles all of this much more efficiently. But in my case, this behavior is also explained by the following features:

  1. I initially built my game “engine” on the output of a finished sprite, and not on pieces of the original image, so I had to glue everything together in advance.

  2. Transparency application algorithm – pain in all parts of my body.

    Firstly, Avalonia/WPF do not directly support colorkey (or I did not find it), because of this you need to iterate over each pixel in the image and set the transparency to 0.

    Secondly, there may be several fully transparent colors (BGR) in the image: #FF00FF, #FE00FF, #FF01FF and so on (that is, in some range from the true transparent color – #FF00FF), and this is not specified in the metadata. This range also has to be checked and processed.

    Third, some images have computed transparency. For example, unit shadows have a transparency of 128, and various aura animations have a transparency of 0 to 255, depending on the color index in the palette.

As a result, a lot of processor time is spent on image processing. There was an idea to repack all images at once with an embedded alpha channel, but for now I decided to leave it as is.

Code links:

  1. Image and animation unpacker.

  2. Creating an Avalonia Bitmap.

  3. Creating a WPF Bitmap.

Music and sounds

Some of the audio in the original game is stored in separate files with the extension .wav in folder Music And Briefing. Here you can find various background music for scenes (main menu, battles, map), as well as voiceovers for the beginning and end of missions.

The rest of the audio is in the folder Sounds and packed into files with the extension .wdb (MQDB format): this is where the sounds of units during battle are stored (Battle.wdb), sounds of movement on the global map (Midgard.wdb) and just various ambient sounds (AudioRgn.wdb).

There is also a separate file for units. Battle.wdtwhich has the structure MQDBlike the rest. It specifies for each unit type its:

  1. Attack sounds.

  2. The moment when to start and stop playing the sound (animation frame numbers).

  3. Hit sounds (if item 1 is always played during an attack, then this sound is only played when the enemy is successfully hit).

  4. The moment when to start and stop playing the hit sound (animation frame numbers). Together with the start of playing this sound, the calculation and display of the attack result (successful hit, miss, etc.) also occurs.

  5. Sounds of getting hit.

  6. Sounds of movement on the global map.

“Sounds” are listed in the plural, as each unit may have several variants for one action. When playing, a random one is selected.

A cross-platform library is used to play music and sounds ManagedBass.

Code links:

  1. Sound Resource Unpacker.

  2. Sound mapping unpacker.

  3. Class for playing sounds.

Video

As with audio, some of the videos in the original game are stored as separate files: with the extension .bikin directories Video And Briefing. Here you can find opening videos, as well as mission start and end videos.

The rest of the videos are packed into resources, for example, MenuAnim.ff (format MQDB) – they are used as transition animations between pages or portraits of leaders.

Examples of such videos
Transition from the menu to the race selection page

Transition from the menu to the race selection page

Choosing a campaign for people

Choosing a campaign for people

For both WPF and Avalonia, I used a cross-platform library to display videos on the screen. libvlcsharp. However, it has a big problem that could not be overcome: before playing any video, a black screen is displayed for a moment. In order to prevent the flickering screen from spoiling the impression of the game, all animations for transitions between pages were disabled 🙂

Code links:

  1. Resource unpacker (same as for images).

  2. Avalonia Video Control Model.

  3. Model for WPF Video Control.

Arrangement of interface elements

In the original game, the interface of each scene is generated dynamically using a file Interf\Interf.dlg. It contains a detailed description of the interface of each scene: its dimensions, background, cursor, as well as a list of all elements (text, images, animations, buttons, etc.) with their dimensions. As an example, let's look at the scene for selecting a save for a single game:

DIALOG	DLG_LOAD,0,0,800,600,DLG_LOAD_BG1TRANS,_CUDEFAUL,0,0,0,0,640,480,0
BEGIN
	BUTTON	BTN_BACK,381,553,451,600,DLG_LOAD_CUSTOM_TOURNEMENT_GCAN01BN,DLG_LOAD_CUSTOM_TOURNEMENT_GCAN01BH,DLG_LOAD_CUSTOM_TOURNEMENT_GCAN01BC,DLG_LOAD_CUSTOM_TOURNEMENT_GCAN01BD,"X100TA0040",0,27
	BUTTON	BTN_GAME_LIST_DOWN,500,245,537,282,_MENU_ARROW_DOWN_N,_MENU_ARROW_DOWN_H,_MENU_ARROW_DOWN_C,_MENU_ARROW_DOWN_D,"",1,40
	BUTTON	BTN_GAME_LIST_UP,500,87,537,124,_MENU_ARROW_UP_N,_MENU_ARROW_UP_H,_MENU_ARROW_UP_C,_MENU_ARROW_UP_D,"",1,38
	BUTTON	BTN_LOAD,730,553,800,600,DLG_LOAD_CUSTOM_TOURNEMENT_GOK01BN,DLG_LOAD_CUSTOM_TOURNEMENT_GOK01BH,DLG_LOAD_CUSTOM_TOURNEMENT_GOK01BC,DLG_LOAD_CUSTOM_TOURNEMENT_GOK01BD,"X100TA0117",0,13
	BUTTON	BTN_PG_DN,139,513,143,517,,,,,"",0,34
	BUTTON	BTN_PG_UP,62,512,66,516,,,,,"",0,33
	IMAGE	IMG_RACE_1,548,387,593,460,,""
	IMAGE	IMG_RACE_2,606,387,651,460,,""
	IMAGE	IMG_RACE_3,666,387,711,460,,""
	IMAGE	IMG_RACE_4,723,387,768,460,,""
	TLBOX	TLBOX_GAME_SLOT,550,24,765,374,1,0,0,BTN_GAME_LIST_UP,BTN_GAME_LIST_DOWN,,,BTN_PG_UP,BTN_PG_DN,BTN_LOAD,"\b255;255;255;","",,,0,"",0
	TEXT	TXT_DESC,425,472,765,570,\c000;000;000;,"",""
	IMAGE	TXT_FIREFLY,73,321,260,433,_FIREFLY,""
	TEXT	TXT_INFO,425,346,535,456,\c000;000;000;\fSmall;\vC;\hC;,"",""
END
The red frames highlight the approximate location of the elements according to the file.

The red frames highlight the approximate location of the elements according to the file.

The background image of the scene is DLG_LOAD_BG1TRANScursor – _CUDEFAUL. The list of elements is also listed:

  1. BUTTON – button. They are assigned images for each of the states (normal, selected, pressed, blocked), a hint, a sign of automatic repeat pressing (repeat button), hot keys. In this scene there are only six buttons, two of which are invisible (but have a hot key, “page up” / “page down”).

  2. IMAGE – image or animation. There is one animation on the stage (TXT_FIREFLY with animation _FIREFLY) and 4 placeholders for the race icon (IMG_RACE_1/2/3/4).

  3. TLBOX – a table with text controls. Contains a lot of data: names of control buttons, text style. On the stage, the table contains one column with save names.

  4. TEXT – text element. Static text can be specified in resources, but in this case it is used as a placeholder to describe the save.

A more detailed description of the fields can be found see here or in code.

Thanks to these elements, any scene can be drawn as follows:

  1. Find its description in Interf.dlgadjust the size, background and cursor.

  2. For each element create GameObject and assign it an initial state (hide, block, set click handling, and so on).

  3. Additionally, manage elements depending on events on the scene (block and activate buttons, fill placeholders).

A complex scene (like a battle) may require fine control over the state of interface elements, but this approach still saves a lot of time.

Code links

  1. Interface File Unpacker.

  2. Creating objects on the scene.

Run on Linux

One of Avalonia's killer features is its cross-platform nature, so let's see it in action on Virtual Box 7.0 And Ubuntu 22.04. We roll a clean image, then install the necessary libraries: .NET 8.0 And vlc for video.

sudo apt-get install -y dotnet-runtime-8.0
sudo apt install vlc
sudo apt install libvlc-dev

An application for Linux can be easily built in Windows, for this we set up publishing in Visual Studio with the setting Target runtime linux-x64 (but you can choose any other bit depth if it is supported by native libraries).

Setting up publishing in Visual Studio

Setting up publishing in Visual Studio

Copy the resulting build to the virtual machine and run it:

What I encountered for Linux support:

  1. I had to look for cross-platform libraries, for example, a popular library for audioNAudio does not work on Linux.

  2. File paths: Windows understands as forward slash \and the opposite /Linux is just the opposite /. In order not to step on my rake, I advise you to use Path.Combine or backslashes for paths.

  3. Linux Window System – X11 – It has its own characteristics. To scale the game screen correctly, the window size is checked at startup. In the case of Linux, this did not work correctly, so I had to resort to small tricks.

As you can see, these problems are minor compared to the fact that the same code runs on two different OSes. On Linux, the application lags noticeably, but I attribute this to the virtual machine.

Conclusion

Thus, in practice it has been proven that you can develop a twenty-year-old game on Avalonia. WPF, alas, does not carry it out of the box (but you can always look for optimizations!). Nevertheless, continuing development in the same direction would be complete madness even for me. Theoretically, it would be possible to try to transfer current developments to the same Unity, but I am not sure whether I will get around to it.

I would like to separately note and express my deep gratitude to the modders and developers. modding toolset for Disciples 2. They helped me understand how to work with resources, the internal logic of the game, and much more. They also really pumped up the engine of the original game, adding a bunch of unique things (mods are even in development for new races!).

Thanks for reading to the end. I hope that someone will want to play Disciples II again 🙂

Finally, a few more links:

  1. Once again the link to the Git repository. In the README you can find links to the compiled game for Windows and for Linux.

  2. Disciples Fan Community on Discord.

  3. Disciples Fan Community on VKontakte.

  4. Popular mods for Disciples II.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *