The challenge of video game development isn’t just making functional, usable software. Games need an additional special ingredient: fun! Not only is fun subjective, it often takes exploration, feedback, and iteration to find. gr4Xity itself emerged from several failed prototypes and is still deep in discovery.
My favorite example: the addictive one more turn mechanic that made Civilization a hit emerged only late in development. Sid Meier realized players of the real-time version were at first content to watch the AI play by itself… until they got bored. Literally making history wait on the player changed how players engaged and thus the focus of development, birthing the modern 4X genre in the process.
Recently, the independent developer of the fantastically well-received Balatro caught flak on the internet (oh no!) when it was revealed that gameplay logic is dictated by a single massive lua
file. Lua has long been a popular scripting solution for game teams at all scales because of its familiar C-ish syntax and C API for embedding. But C programmers certainly wouldn’t consider long chains of IF
statements a “best practice”.
Load-bearing Tomato’s Christina Pollock provides a reasoned defense of Balatro’s lone developer, with a thoughtful discussion on the tradeoffs around rapid prototyping. This means quickly developing and testing ideas to iteratively build in whatever direction your exploration leads.
How can we lean into rapid prototyping? By giving designers power and flexibility to experiment while still allowing them to think like designers. Even in solo development it’s helpful to separate game rules and logic from operational processing.
The tradeoff Christina identifies is that a system designed for optimization can limit exploratory development. Coming from a data-driven background makes the road easier, but the tradeoff is real. The key insight is that different tools impose different tradeoffs.
A simplified visual model helps illustrate.
Adapting the economic concept of a production possibility frontier, game teams with a given technology can produce inside or on the line in the short run, but not outside. For example, a novice team might operate close to the origin–neither very explorative nor very optimal–while a group of experts can push the edge of the envelope.
Those working on performance-critical components tend to prefer relatively low-level languages like C, which give developers more direct control over system resources and how they’re used. Optimization is easier at the cost of less flexibility and a steeper learning curve, especially for designers without a computer science background.
Lua is often a win-win because not only does it provide an easier and more forgiving experience for designers, but also it allows easy embedding in C applications. Diverse game teams can thus gain from the comparative advantage of specializing in each language. Critical engine systems can be optimized in C by programmers, while designers can iterate on game rules and logic in Lua. Together, such specialized game teams face a more favorable set of tradeoffs than with C or Lua alone.
Christina asserts that the pattern in the Balatro example “was efficient from a design perspective because it allowed for rapid prototyping of cards before the design was locked in.” But “allowed” is one thing. Does the absence of a system design necessarily mean optimal prototyping? Can we do better on the vertical axis of our tradeoff graph?
From Chaoskampf‘s data-driven perspective, there’s massive inefficiency in having to repeat the same imperative instructions {if this then return that end
} over, and over, and over… Not only is this boilerplate repetitive, the actual game rules are buried in all this cruft, making it more difficult for designers to iterate than necessary.
Why not simply store sets of this
and that
as data? Then we could just iterate over this data, matching patterns against the this
es and evaluating the that
s accordingly. Improvements and optimizations to that process would be isolated from how design builds game rules. The design team would also be empowered to better manage their rules as data, for example with controlled testing of variations of rules in combination.
The answer is that imperative languages like Lua just don’t work that way. This is however straightforward in homoiconic functional languages like Lisp and Scheme, where code itself is a data structure that can be directly manipulated like any other data, in the same syntax. This kind of metaprogramming opens radical new doors for prototyping, with more dynamic code coming at the cost of more limited opportunities for optimization.
Winestock‘s “Curse of Lisp” is a reflection of our fundamental tradeoff: because anyone can prototype a solution to their specific X in a day or a week, no group spends a month or a year collaborating on a robust and optimized library for related Xs in general.
Given the tradeoffs in the diagram alone:
- An action game that lives or dies based on performance should start with C and embed something like C + Lua only to the extent needed by the design team. This combo is still a clear win-win on the right side of the chart.
- Games that don’t require much if any optimization can lean into rapid prototyping with interpreted homoiconic functional languages like Lisp. This narrow, specialized sliver on the left side of the chart is focused on innovative but relatively light game mechanics, perhaps an ideal platform for iterative puzzle game development.
- Games in between, with some systems requiring low-level optimization in a C-like language, might be better off embedding a Lisp-like scripting language rather than Lua for more exploratory flexibility.
Of course, developer and designer experience and preferences play a strong role in these choices, as do technical decisions on game engines and external libraries as well as business decisions like target platform.
As quoted by Wikipedia from “The evolution of Lua“, the language’s authors “did not consider LISP or Scheme because of their unfriendly syntax.” Syntax is definitely an important consideration for an embedded scripting language. If design and engine teams live in totally different worlds, experienced programmers can’t provide feedback for designers’ data-driven code. So a team embedding Lisp in C might have less capability for exploration and optimization than either language alone, while still offering potential for gains in the middle.
“Unfriendly syntax” in this context starts with (parentheses)
, which Lisp and related languages use to delimit every expression. This has been mocked as “parenthesis hell“, and as always there’s a relevant xkcd. C-style languages offer explicit control flow with {curly brackets}
, saving parentheses to separate function names from lists of their arguments.
Lua’s “sole data-structuring mechanism” is called “tables”, a kind of associative array specified as a comma-delimited lists in curly brackets. These one-dimensional arrays can be nested into tree structures, but they’re not what a data analyst thinks of as “tables” in a database. Still, they provide much higher-level structure than forcing designers to manually manage data with pointers in C, which is largely responsible for the different shape of Lua’s tradeoffs on the explore-exploit spectrum.
Syntax aside, Lua’s designers missed something else from Lisp. There’s no provision to operate over Lua tables and execute their data as code within the state of your program. Lua can load “chunks” of data from a string or file and compile them into anonymous functions, enabling Lua to call itself an interpreted language, but this has severe limitations.
These Lua chunk functions have a limited scope and no side effects, and performance doesn’t scale as each call results in a new load and compilation. In contrast, with a homoiconic functional language, the data is loaded once, ready in the same kind of syntax tree as code… just waiting to be evaluated at the right time in the right context.
More modern Lispy languages give developers more power over evaluation and context (scoping), such as John Schutt’s beautiful Kernel. Even though R is not homoiconic, R’s Tidyverse is also built on evaluating code in a data context. This is what made things like the declarative Grammar of Graphics easily doable in R but not fully possible* in more C-like imperative Python.
* While Plotnine‘s aping of most of ggplot2 in Python is a commendable achievement, it’ll never be as dynamic as ggplot2 in R. As Christoph Scheuch recently reminds us: “Column references are implemented via strings in Python, while you can use unquoted column names in R due to its support of non-standard evaluation.”
Achieving Escape Velocity
What does this all mean for a project like gr4Xity? Traditional 4X games develop a sense of inevitability that trains players to quit as soon as things start to get boring. Event-driven scripted solutions to shake things up tend to arbitrarily punish players for doing well in the early game, reinforcing quitting and driving churn.
Building a 4X strategy game on top of an immersive simulation targets two sources of fun:
- Emergence: Gameplay arises dynamically from interactions between rule-based systems. The whole is greater than the sum of its parts. This not only allows unique situations and solutions to develop, this process is interactive. Players have more agency in creating their own narratives.
- Verisimilitude: A sense of depth behind the curtain. Providing enough of a framework for players to hang up their sense of disbelief lets imagination take over to fill in the gaps. This gives players a deeper sense of ownership over their actions in-game, as well as more willingness to give meaning to random results and see things through that don’t radically affect outcomes.
These two factors working hand-in-hand can create unforgettable moments. But they can also work against each other. Feeling like a game recognizes and responds to you can be engaging to an extent, but overplayed this can make players feel steered into pre-canned paths [e.g. Deus Ex: Mankind Divided is more choose-your-own-adventure book than immersive sim].
This approach comes with costs and risks. Emergent gameplay is notoriously difficult to debug let alone balance. Managing complexity can be overwhelming. But unintended consequences are the point! The freedom of a sandbox means most players won’t even experience most content or utilize all game systems, especially on the first playthrough. Do you need a lot more content to make a good initial impression? How do you maximize the fun for such individual experiences?
While immersive sims have been traditionally recognized in action genres, strategy and management games like Dwarf Fortress and Rimworld are also rooted in immersive simulation. So-called Grand Strategy games have also made numerous advances in emergent character-driven narrative. There’s a lot to learn from and experiment with! gr4Xity‘s strategic focus means we can start with rapid prototyping and defer optimization as necessary. But we don’t want to preclude future optimization.
The Red toolchain was chosen for development of gr4Xity and its Graphecs engine because it promises both Lisp-like flexibility for exploration and potential for C-like low-level optimization. The goal is an even more favorable tradeoff.
Red achieves its “full stack” nature by pairing a high-level dynamically-typed interpreted language with a low-level statically-typed compiled language with pointer control over memory.
The key: these closely-related languages share the same user-friendly syntax, avoiding the headache of mashing Lisp and C together for a more developer-friendly approach, like Lua embedded in C but better. The (parentheses)
and {curly brackets}
gangs can argue all they want. Inspired by the kid-friendly Lisp called Logo that I was lucky to be taught in elementary school, Red’s primary data structure uses [square brackets]
that don’t require shifting to type on a modern US keyboards.
Red also embeds nicely in C and other languages if need be, though the contrast in syntaxes can pose challenges as mentioned. The example that struck me was embedding in VBA to play Pong between Red and Excel.
Thanks to Red, the Graphecs engine is entirely data-driven. You supply Graphecs with some lists of rules–data that mixes declarative and imperative code–and Graphecs does the rest. The game emerges from the data.
The order you supply these lists to Graphecs doesn’t matter (neither does from where–loaded from files or a database or whatever), but there is a potentially helpful mnemonic:
my-game-config: make graphecs/config [
Groups: [...] ; defines dynamic sets of entities
Relations: [...] ; defines dynamic edges between entities
Activators: [...] ; defines systems that run on start-up
Procedures: [...] ; code that runs on relations in graphs
Handlers: [...] ; code for intersections between graphs
Entities: [...] ; defines vertices composed of components
Components: [...] ; defines data containers
Systems: [...] ; defines code that runs on entities
]
That’s it. Activators
include code to initialize entities as well as define and start the main game loop.
In the next post, we’ll take a detailed look at the Graphecs test app and how this declarative rule-driven game engine enabled iterating away from Pong towards something more dynamic and maybe even more fun…
Leave a Reply