In (hopefully) the last post about the transition to fully scriptable game logic, I’ll try to quickly summarize the loose ends I’ve been tying up over the past few weeks to round off the integration of the scripting layer. Most of the time went into relatively boring cleanup and refactoring work, all in order to fix the last two things that got lost in the transition to scriptable game logic: freezing/unfreezing game state, and action replays.
Refactored entity snapshots and added support for capturing custom properties defined on the Lua wrapper class for an entity
Instead of subclassing
K14EntitySnapshot, creating a snapshot class
K14ScriptableEntitySnapshotspecific to scriptable entities, support for custom entity properties is now baked directly into the interface of the
K14Entityclass, which now advertises a
customPropertiesmethod that should be implemented by subclasses (such as
K14ScriptableEntity). This method returns a dictionary of properties, encoded as name-value pairs, which is retrieved by
K14EntitySnapshotwhen taking a snapshot of an entity.
Lua classes for entities now export a table mapping property names to data types, which is used by the
K14ScriptableEntitysubclass to to implement the
K14Entity customPropertiesmethod. When retrieving the custom properties of a scriptable entity, this method will retrieve the advertised wrapper class properties from the Lua VM, converting them to Objective-C types (
NSString, etc) that can be added to the custom property dictionary so they can be included in the snapshot.
Added support for restoring custom entity properties
Now that all entity properties not related to core engine state are encoded as part of the Lua wrapper class for each entity, restoring entities from a snapshot also requires copying these wrapper properties to the entity snapshots and back into the wrapper class when the snapshot is restored.
To make a long story short: restoring the properties is basically the inverse of capturing them, as described in the previous item. The
K14ScriptableEntityclass overrides the
K14Entity initWithPlanet:snapshot:initializer to push the advertised custom properties to the Lua stack and call the
Entity:restore(snapshot)method of the Lua base class for entity wrappers, which will assign them to the wrapper instance.
Introduced a two-level hierarchy to identify game, planet and entity types
Instead of a single class name such as ‘Ship’ or ‘Planet0’, the
K14Entityclasses now have both a class name, and a subclass name. The class name identifies the Objective-C class that implements the game, planet or entity, the subclass name identifies its actual type. For example, the ship entity is now identified using the class name ‘K14ScriptableGame’ and the subclass name ‘Ship’.
The reason for this change is because I want to keep the option open to re-introduce support for native entity classes at some point in the future, alongside scriptable entities. I can imagine using entities for particle effects or very large numbers of projectiles, which would really slow down the game if they have to go through the Lua VM every time they have to be updated and sent to the renderer (remember that the renderer also uses the snapshot classes to ‘double-buffer’ game state by storing a read-only representation of the game before rendering each frame). When creating or restoring a game, planet or entity, the implementing Objective-C class can be created dynamically from its class name using
NSClassFromString, passing the subclass name to the initializer to create the right entity subclass, e.g. using it to determine the name of the Lua wrapper class that implements the entity.
Re-enabled action replay functionality
Now that game state can again be fully saved and restored to and from snapshots it was possible to re-enable action replay support. The only changes I made to the actual replay recording code was that action replays are now saved using a filename that uses the planet subclass name instead of its class name (which is now always
K14ScriptablePlanetso replays for different planets don’t overwrite each other. I also appended a timestamp to the file name to allow multiple replays for the same planet. Otherwise all the existing replay (recording, playback) functions still worked just fine, thanks to the fact they basically just capture and simulate user inputs, and don’t depend on any other game code.
Cleaned up initializer hierarchies
While trying to restore game state from a snapshot, I found out that my initializer hierarchies had been messed up all along. Up to now I was using an ‘anything goes’ approach to the initializers of the game, planet and entity classes. Subclasses would have multiple convenience initializers, some of which called each other, others which called the superclass initializer.
This initializer model falls apart when you have different ways to initialize the same things, for example when creating an entity from scratch, or when creating it from a snapshot. Unless you are careful and explicit about the the sequence in which base classes and subclasses are initialized, there is a high risk of creating initializer hierarchies that don’t follow a nice up-down path that first initializes the topmost class in the hierarchy, then unwinds back down into the subclass at the bottom of the hierarchy. Especially when overriding base class initializers in subclasses, the initializer hierarchy can jump back and forth between the base class and the subclass, potentially introducing unwanted inversion of control, or violating assumptions about the base class state in subclass initializers.
Instead of trying to come up with an example that illustrates the potential problems and solutions of careless initializer hierarchies myself, I’ll refer to the following informative article about Objective-C initializer design patterns, which does a great job of explaining them. I’ll quickly summarize the generally accepted ‘correct’ way to implement Objective-C initializers.
Objective-C has the ‘designated initializer’ idiom to help you avoid problems caused by careless initializer hierarchies. A designated initializer of a class is required to call a superclass initializer, it cannot omit this call, and it cannot call any convenience initializers, not any of its own, not any of its superclass. Conversely, convenience initializers can not call any superclass initializer. they can call each other, and they can call a designated initializer of their own class, but nothing else. Following this idiom, it is impossible to create initializer hierarchies that result in incomplete superclass initialization: the chain of initializer calls will always first go all the way up to the lowest-level initializer, and then ‘unwind’ into progressively higher-level initializers. When overriding designated initializers of a base class, you can always assume the base class will be properly initialized, and loss of sanity is prevented.
Unfortunately, Objective-C does not enforce the use of designated initializers, like many other things in the language you just have to be diligent and apply the idiom by convention. Luckily, XCode has a handy macro
DESIGNATED_INITIALIZERthat can be added after the declaration of your initializers, which will cause the compiler to emit warnings if you violate the designated initializer idiom. I’ve added the macro to most classes, and will start to do this by default for new classes from now on.
As a side-note, I’ll mention here that these kinds of problems are largely absent in less dynamic language with stricter initializer/constructor semantics. Many languages don’t allow virtual constructors, or constructors calling other constructors. Because Objective-C uses full run-time dynamic binding by means of message passing, any object can choose to override any selector of its base class, including its initializers. Additionally, initializers are allowed to call whatever selector of whatever object they want, and can even reassign the
thisin other languages) to an object of a different class than themselves. While these are powerful and flexible tools, they are also easily abused.
Pushed out all Objective-C++ dependencies into a handful of implementation files, so all other files could be converted to Objective-C
Because some classes interface with Box2D, which is C++, they have to be compiled as Objective-C++ code. The mistake I made early on in the development process was to leave Box2D includes and data types in the public interface of these classes, which meant that any source file including their header file also had to be compiled as Objective-C++. This way I ended up with almost all source files compiled as Objective-C++, because they would almost all include (directly or indirectly) the
K14Entity.hheader file, which (amongst other things) had a
b2Body *property to access its Box2D body.
The annoying thing about Objective-C++ source files is that the XCode refactoring tools don’t work on them, even if they don’t contain a single line of Objective-C++ or C++ code. If a file depends on some header file that has anything in it that only compiles as Objective-C++ (e.g. a forward
classdeclaration), XCode will refuse to do any refactoring on it, such as renaming variables or properties.
To fix this problem I wrote simple Objective-C wrapper classes around anything that interfaces with Box2D, such as the Box2D world (
K14World), entity bodies (
K14Body), etc. None of these classes depend on C++ or Objective-C++ header files, which allowed changing all files back to Objective-C except a handful of implementation files that call Box2D functions.
Split the level select screen into two separate list views, for level select, and for replay select
I’m planning to make more use of the action replay code, which will require a somewhat more flexible way to organize replays. Before, action replay were always saved out using the planet classname, overwriting any replay with the same name, and effectively limiting us to one replay per planet. Replays are now saved using a name that includes a time-stamp, and the new ‘replay select screen’ works like a simple file manager, listing all replays found on the device so they can be started or deleted.
Removed lots of deprecated code
Basically everything that was previously implemented using Objective-C classes, but now in Lua, has been removed. This includes all
K14ScriptableEntity, all gameplay event handling code, etc. All in all the number of lines of native code was reduced by almost 1000.
All this work and we’re basically back to square one ;-). At this point all of the features that were implemented before the scripting transition are working again, but with lots of changes behind the scene. The core engine code has been separated from the gameplay logic, which provides a great checkpoint from which we can really start to build gameplay. As an added benefit the engine code has been cleaned up in many places, providing a much better separation of concerns between the various components of the engine code, and how they interact.
I’ve made another video, which from a feature/gameplay point of view does not show anything new at all, except the new level select and replay select screens. You just have to keep in mind that behind the scenes things are being driven in a completely different way compared to the last video, and everything that happens on-screen that’s related to what the planet looks like, what entities are on it, and how they behave, etc. is now running inside a Lua VM.
I think I’m mostly done with the scripting infrastructure, so now is a good time to start making use of it. I have some ideas, but I’ll save them for a later post ;-)
Besides new features I’ll also have to round up some minor issues with the snapshot save/restore functionality that have been neglected until now. For example, joints between entities are not re-created when restoring from a snapshot. There’s probably more of these minor deficiencies that I’ll fix along the way.
All of the above took about 10 hours to implement, bringing the total development time to ~230 hours. SLOC count actually went down significantly, because of all the deprecated code that has been removed and is now implemented in Lua scripts. The native code SLOC count went down from 6546 to 5713 (a decrease of 833). Lua SLOC count went up from 476 to 641 (an increase of 65).