Engine Development Tips & Tricks
The following is a list of suggestions, tips, and recommendations for people to keep in mind when developing new engines for ScummVM. These aren't in any particular order, and are suggestions or recommendations rather than rules, so it's up to the developer whether to follow them or not.
- Remember to put a g_system->delayMillis call into any loop that does event processing. Without it, your engine will automatically use up 100% of CPU time, and slow down any other programs running on your computer.
- ScummVM runs on different architectures, so take into account byte ordering when reading in data from game files:
- The absolute worst way to read data is read a block and try to cast it directly to a structure pointer. This is bad because different systems may put padding bytes between fields of a structure, and even individual fields that take several bytes to represent them may have the bytes ordered differently (this is called the "endianness" of values).
- A slightly better, but still bad way to read data would be to use the PACKED_STRUCT macros to create a packed structure that will not have any padding between fields, and is more likely to correctly map to data from a game file. However, even then, you'd really need to wrap access to individual fields of the structure using either the FROM_LE and FROM_BE, or READ_LE and READ_BE macros defined in common/endian.h to properly access the fields on all the systems ScummVM can run on.
- The best way would be to use the stream methods like readSint16LE, readUint16LE, readUint32LE, and so on, to read in values from a stream, specifying a specific size and byte ordering (LE is the most common byte ordering used by PC games, since standard PC systems are LE, or Little Endian).
A clean way to implement reading game data around this would be to create simple C++ classes to represent data structures from the game data files you're reading, and have a method on the class to read in the data for the structure. That way, an array of structures can be cleanly represented by simply defining an array of the objects and having a simple loop to iterate through calling the loading method of each object.
- ScummVM supports a variety of scalers, and some of these, when applied, take longer to generate the data for a given frame. Because of this, it's better if your game only updates modified parts of the screen each frame, rather than updating the entire screen surface in the g_system->copyRectToScreen calls.
The common terminology for this is maintaining "dirty rects", ie. rectangular areas of the screen that are "dirty" (modified). Every time anything is modified (things drawn, areas cleared, etc.), add the area covered to a list of dirty areas. Then when you do a frame update, you can use a simple loop to only copy the modified areas from whatever temporary surface you use to the screen, and then clear the list in preparation for the next frame. Any scaler currently active can then only bother applying their logic to these modified areas, rather than wasting time doing the entire frame.
It's important to try avoid a frame having too many dirty rects, as this can end up being slower than processing the entire frame. Because of this, most engines using dirty rects do some pre-processing to merge overlapping rectangles into a single bigger rectangle before using them.
- Avoid the use of global objects like the plague. For several reasons:
- Not all systems ScummVM can be compiled on can generate the necessary code to automatically create the global objects at startup.
- Users aren't necessarily going to play your game when they start up ScummVM, so it would be just wasting memory to have objects for your game being created.
- Remember that games can be restarted after exiting and returning to the GMM, so particularly if your objects have field values that need to be in a consistent state when the game states, it's better to have the game engine itself create objects each time the game starts, and free them when the game ends.
- On a similar note to the previous point, avoid using global variables even for simple things like integers, even more-so if they need to have an initial value. It's far better to have them as part of your engine class, and have them explicitly set when the engine is run.
- Also related to the previous points, Some of us feel that it's cleaner to pass pointers to the main engine to any sub-objects you create, rather than relying on global pointers to the engine. If you decide to do so, a common practice is for the sub-object classes to have a forward declaration of the engine class, and then a private engine '_vm' pointer property, which is set either via a parameter in the class constructor, or via a 'setVM' (or equivalent) instance method. It's up to you whether you want to do it this way, or use a global engine pointer.
- As a matter of style, it's a good idea to break your game engine up into different classes that represent different areas of the game, rather than putting everything into one big engine class. For example, you could have separate classes for screen/graphics, scripts, event handling, and so on. Have a look at the existing engines for inspiration. Even a cursory review should give you some ideas of how other developers have structured their code, and give you ideas for doing likewise.
- Try to avoid having separate code to load and save data from savegame files. ScummVM has a 'Common::Serializer' class that allows you to write common code to 'synchronize' individual fields. Having shared code like this will reduce the amount of code you require, and help avoid errors resulting from inconsistencies between separate reading and writing logic.
- Speaking of Common::Serializer and savegames, ScummVM doesn't generally strive to maintain compatibility with savegames created by the original executables. Particularly since ScummVM savegames can include extra data like a savegame name and thumbnail. As such, it's also a good idea to include a version number in your savegames. If later on you need to add extra information to saveagmes, having a version number will allow you to still handle old savegames correctly.
- Take your time to look through the common/ folder, and/or how existing engines use the code. There are a lot of good helper classes there to make your work easier. Likewise, ScummVM already supports various standard sound and music formats, so you may not need to manually implement all of a game's low level sound handling code at all.
- Any loop that waits for user input should include a check to break out if the shouldQuit() method of the engine returns true. This is because the user could close the ScummVM window, or some other system specific method to close the application, at any time, and the game shouldn't stall just because it's stuck in an arbitrary loop awaiting input. Likewise, any delay method should comprise of a loop doing smaller delay amounts, interspersed with pollEvents calls. This will allow the application to remain responsive, and exit quickly on a close without having to wait for the delay to end.
- Remember that specifying an object, such as a Common::String, as a method parameter will cause a copy of the object to be made every time the method is called. This can lead to lots of inefficiencies, and should be avoided where possible in favor of passing by reference. Additionally, it's good practice to use the 'const' prefix as well if the method isn't meant to change the passed object, since it can help prevent accidental changes. That's why in the engines, you'll see methods defined like:
- void ClassName::writeString(const Common::String &str);
- Avoid using large amounts of constant data in your engine. This is because it not only bloats the compiled ScummVM executable, it will also get loaded into memory when ScummVM starts, irrespective of whether your game is played or not. This is why many of the game engines come with a data file containing extra needed data, as well as a developer tool to create that data file, usually by extracting it from the original game's executable. There are no rules for how these data files need to be structured, so can come up with your own format, and then load it into memory when your game is started.