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.