Gamefic 4 and Upcoming Plans
by Gamefic on February 13, 2025
The latest major update to Gamefic introduces some important structural and architectural changes. Some of them break backwards compatibility, but I felt they were important for long-term maintainability, extensibility, and developer experience.
(Note: this post discusses a lot of technical details that explain the reasoning behind the design decisions, but hopefully they don't need much consideration when authoring games.)
One important goal was to clarify the difference between the script and the model. The script is code: scenes, responses and callbacks that handle game events. The model is data: game entities, counters, and other values that comprise the game world and indicate its current state. This distinction matters because scripts contains procs that can't be marshaled and therefore can't be included in snapshots.
Gamefic 3 took the first steps toward a distinct separation between scripts and models. This was crucial in ensuring that snapshots were capable of saving and restoring a complete game state, including runtime elements like dynamic entities and active subplots. Unfortunately, the separation still had some holes in it. Fixing those holes, along with major changes to custom scene management, comprise the majority of changes that are not backwards compatible.
Scripts and Seeds
Scripting methods such as `respond` used to be wrapped in `script` blocks.
# The old scripting method
class ExamplePlot < Gamefic::Plot
script do
introduction do |actor|
actor.tell 'Hello, world!'
end
respond :think do |actor|
actor.tell 'You ponder your predicament.'
end
end
end
Scripting methods are now class methods, so the `script` block is no longer necessary.
# The new scripting method
class ExamplePlot < Gamefic::Plot
introduction do |actor|
actor.tell 'Hello, world!'
end
respond :think do |actor|
actor.tell 'You ponder your predicament.'
end
end
Gamefic 3 started on the path to bypassing `script` blocks, but there were still certain cases where they were necessary. Gamefic 4 eliminates the need for those cases, but breaks backwards compatibility by eliminating the `script` method altogether. Among other things, `respond` is now purely a class method, so the way it used to interact with instance variables is incompatible. An instance variable in response arguments would now refer to a class instance variable, which is not the previously expected behavior. These changes made retaining the concept of `script` blocks untenable.
The `seed` method still exists, but other scripting features (especially `construct`) should reduce the need to define seed blocks.
The construct
Method
One long-standing issue with Gamefic has been entity referencing. Historically, the easiest way to manage static entities was with instance variables. This could be cumbersome if you need external access to plot entities, e.g., from chapters, subplots, or unit tests. Authors could solve this problem with instance attributes, but their declarations tended to be verbose.
class ExamplePlot < Gamefic::Plot attr_reader :gizmo seed do @gizmo = make Thing, name: 'a gizmo' end end plot = ExamplePlot.new plot.gizmo #=> the gizmo entity
The construct
method provides a shorthand way to seed entities with corresponding instance attributes. In technical terms, it memoizes an entity and ensures that the entity gets created when the narrative gets initialized.
class ExamplePlot < Gamefic::Plot construct :gizmo, Thing, name: 'a gizmo' end plot = ExamplePlot.new plot.gizmo #=> the gizmo entity
Entity Proxies
Another advantage of the construct
method is that it defines a corresponding proxy attribute in the class. This means you can reference constructed entities in class-level script arguments even though they won't exist until the plot gets instantiated.
class ExamplePlot < Gamefic::Plot construct :house, Room, name: 'a house' construct :gizmo, Thing, name: 'a gizmo', description: 'Some kind of doodad.', parent: house # `house` here is a proxy introduction do |actor| actor.parent = house # `house` here is a Room entity end respond :look, gizmo do |actor| # `gizmo` here is a proxy actor.tell gizmo.description # `gizmo` here is a Thing entity end end
Gamefic 4 also introduces pick
and pick!
class methods. At the class level, these methods return a proxy that will be converted to an entity through an instance-level pick(!)
call at runtime.
Command Hooks
The before_action
and after_action
hooks have been replaced with before_command
and after_command
.
class ExamplePlot < Gamefic::Plot before_command do |actor, command| actor.tell "You're performing a #{command.verb} command." if command.verb == :cheat actor.tell "But you're not allowed to cheat!" command.cancel end end end
Scene Naming
Authoring custom scenes has always been hairy. Gamefic 4 introduces a couple new features to help streamline it. Once again, mostly thanks to the clearer separation of scripts and models, the new patterns aren't fully backwards compatible, but hopefully they're easier to use.
First and foremost, version 4 allows authors to cue a scene subclass directly by referencing its class name.
class ExampleScene < Gamefic::Scene::Activity on_start do |actor, props| actor.tell "You're in an ExampleScene." props.prompt = 'Now what?' end on_finish do |actor, props| actor.tell "You're done with the ExampleScene. The command you entered is #{props.input}" end end class ExamplePlot < Gamefic::Plot respond :example do |actor| actor.tell 'Switching scenes...' actor.cue ExampleScene end end
ActiveChoice Scenes
Many parser games include multiple-choice components. One of the most common examples is conversation trees. In a MultipleChoice scene, the player must select one of the options in order to proceed. In Gamefic 4's ActiveChoice scene, player input that doesn't match a choice will be treated as a command in the same way as a default Activity scene.
class SaySomething < Gamefic::Scene::ActiveChoice on_start do |actor, props| actor.tell 'Select an option or enter a command.' props.options.push 'Hello, world!', 'Hocus pocus!' end on_finish do |actor, props| actor.tell "You say: #{props.selection}" end end class ExamplePlot < Gamefic::Plot respond :speak do |actor| actor.cue SaySomething end end
Example gameplay:
> speak Select an option or enter a command. 1. Hello, world! 2. Hocus pocus! > 1 You say: Hello, world! > speak Select an option or enter a command. 1. Hello, world! 2. Hocus pocus! > wait Time passes. >
Narrators
The new Narrator class is responsible for running plots. The Narrator is essentially a tiny engine that provides a convenient interface for clients, e.g., gamefic-tty and gamefic-driver. This helps ensure that clients are less tightly coupled to plot internals and plots have fewer methods with unexpected side effects.
Parent Relations
The gamefic-standard library provides a Supporter entity, which lets you place things on top of it instead of inserting things inside of it. The entities' parent-child association, however, does not differentiate between placement and insertion. A bowl on top of a table is the table's child in the same way that a bowl inside a cabinet is the cabinet's child.
Gamefic 4 allows you to specify an entity's relation
to its parent. The currently supported relationships are :in
and :on
, with :in
being the default.
class ExamplePlot < Gamefic::Plot construct :table, Supporter, name: 'table' construct :cabinet, Receptacle, name: 'cabinet' construct :bowl, Item, name: 'bowl' seed do bowl.put cabinet, :in bowl.parent #=> cabinet bowl.relation #=> :in bowl.put table, :on bow.parent #=> table bowl.relation #=> :on end end
Integer Queries
Response arguments can include queries for an integer
.
class ExamplePlot < Gamefic::Plot respond :jog, integer do |actor, number| actor.tell "You jog #{number} meters." end end
The original version of this query was implemented for the gamefic-commodities library. It seemed like a useful enough feature to add to the Gamefic core.
Autoloading in Projects
The Gamefic SDK has been updated to leverage the new features in the core library and add some new features of its own. One of the most notable is autoloading. The gamefic-autoload library leverages Zeitwerk to load classes and modules without explicit require
calls. The autoload feature is included by default in new game projects.
Web Builds
The SDK's default web build includes a new transcript feature and a few UX improvements. To see what's new, install the new SDK, create a new project, and generate a web build.
What's Next
Over the next few months, I expect to do a lot of minor and patch releases as I refine the new features. The online documentation is up to date, but I won't be surprised if I need to push an occasional correction. I also plan to release some more example games to demo various capabilities.
On a side note, I hope to do a post-comp release and postmortem of Focal Shift sometime in the next couple weeks.