Since the '90s, "patterns" have been stolen borrowed from Christopher Alexander's work on architecture. Alexander was curious about designing rooms, buildings, even entire cities, using a sort of "language" which addresses recurring problems like "What do we do with people waiting around?" In a house, this would be a drawing room; for a city, with a transit system, this would be benches (or whatever). Programmers realized they analogously face recurring problems which are solved with "very similar" solutions, and introduced design patterns to the world of software engineering.
For RPGs, we similarly divide it up into patterns: the players go on quests, which occur on levels, whose objectives are typically one of some half-dozen varieties ("fetch quests", "assemble parts for an artisan" quest, "kill quests", "defend quests", "follow quests", etc.). There are Non-Player Characters the user interacts with, Enemies to kill, etc. UC Santa Cruz has done some work on RPG patterns in this vein.
The goals of using patterns in programming include: speeding up development, decreasing the number of bugs, increasing stability, making it easier to extend the program's capabilities, aid testing, and so on.
Munificent Bob wrote a wonderful book, Game Programming Patterns. Design patterns address recurring problems which regularly crop up, and were the "hot new thing" in the '90s. What's an example of this in games? A recurring pattern in, e.g., Steam games are achievements, which are precisely the observer pattern.
Since design patterns have been well discussed by Munificent Bob, I'll focus on the less discussed architecture patterns for Rogue-likes. Specifically, MVC and its variations.
Architecture Patterns
"Architecture patterns" refers to particular ways to structure the code to cleanly separate concerns. For most rogue-likes, this is some variant of MVC: the rules to the game is coded up in the models, the UI rendering is handled by the views, and the translation of user input to game commands is handled in the controller.
If we wanted to have a "poor man's" Fallout-type Rogue-like, where there's a separate dialog interface showing possible responses to the NPC's statement, another interface for traversing the "world map", and a third for traversing the "local region" map, then we can handle this cleanly with an MVC-architecture. One controller for handling user inputs for the dialog, another for traversing the world, and a third controller for the local world map.
We can organize the code to reflect this architecture:
/game/ /game/models/actor.c /game/models/dialog.c /game/models/item.c /game/models/quest.c /game/controllers/dialog_controller.c /game/controllers/world_map_controller.c /game/controllers/combat_controller.c /game/views/dialog_view.c /game/views/world_map_view.c /game/views/combat_view.c
All the logic of dialog is handled in /game/models/dialog.c, which can trigger quests, complete quests, or otherwise update the game state to reflect the choices made. In an object-oriented language, the model is the subject of a view observer. Schematically, in C++, a model would look like:
// make this a template class (parametrized by Model)?
class Model {
public:
Model() : observers_() { };
~Model();
// business logic methods not shown
// subject methods, for observer pattern
void addObserver(const Observer &observer);
void removeObserver(const Observer &observer);
void notify() {
for (Observer o : observers_) {
o.update(*this);
}
}
private:
std::vector observers_;
};
The only place where UI occurs should be swept into the views. Ostensibly we could create subdirectories for the different libraries use (e.g., /game/views/ncurses/ for the code handling NCurses-rendering, /game/views/sdl/ for the analogous code using the SDL library, etc.). If we were implementing the game in an object-oriented language, the view would be an observer of the model, and be notified when the game state has changed. Schematically, in C++, a view class would look like:
// make this a template class (parametrized by Model)?
class Model;
class Observer {
public:
virtual ~Observer() { };
virtual update(const Model &model) = 0;
};
class View : public virtual Observer {
public:
View(...);
~View();
void render();
// observer pattern method
void update(const Model &model);
// action patterns to dispatch events to the controller
void addMouseClickListener(const MouseClickListener &listener);
// etc.
};
Controllers come in pairs with views. We attach the view to the model within controllers, and the action listeners are populated within the controller, too, for handling events (like mouse clicking or keyboard events, possibly inspired by DOM events). The generic controller resembles:
class Controller {
public:
Controller(View *view, Model *model) {
view_ = view;
model_ = model;
model->addObserver(view);
// attach action listeners here, too
};
~Controller();
// high level user interactions
private:
View* view_;
Model* model_;
};
A variation of this architecture, for the Fallout-type game with several interfaces (dialog, world map, local map) may take a hierarchical MVC: intuitively, a "maestro controller" which delegates the game-loop to the dialog controller from the start of dialog until dialog ends, whereupon the game-loop returns to the maestro controller.
Such an architecture permits unit-testing the models, which lets us catch bugs in the boring mechanics (e.g., "marking a quest complete works as expected, right?"), and make it easier to swap out the UI.
Following the Lisp philosophy, we want to create a DSL within which we implement the game. The models describe this DSL. We can store the game data in a separate directory (say, /game/data/) encoded in JSON or YAML or S-expressions. If we wanted to make sure there are multiple ways to solve a quest (through dialog, sneak, or violence as three alternatives, for example), we could setup a small helper program or routine to check each quest has this property...thereby checking a sneaky player could win the game, as could a violent player or a dialog-focused player.
This also permits several people to work on the game. If the DSL is minimally specified and worked on by everyone, there could even be a team working on the game script independent of the game engine. A small Lisp script could check that the game script is playable, an emacs mode could be written to make development faster (a simple C-k C-q to develop a new quest, or whatever, with a skeleton newly inserted at the cursor).
Thus is the goal for such an architecture: to make it easy to develop the game, separating the fun parts from the boring reptitive parts so that the boring repetitive parts are implemented only once. The fun parts could be extended by a team, independent of the boring parts.
References and Concluding Remarks
The Model-View-Controller architecture has evolved over time into something idiosynchratic to each language. Java's MVC in Swing is slightly different than the original MVC in Smalltalk (and when Ruby-on-rails hit the scene, MVC changed into a core component of web development). Admittedly, I am drawing upon the Java tradition (see also MVC Structure, Examples of Model-View-Controller Pattern, Java SE Application Design With MVC).
Java has the Controller class track View objects and Model objects; whereas a vanilla approach would have the View create and track an instance of a corresponding Controller class, and both the Controller and View would observe a Model object. Doubtless there are many other variations of this theme, I won't pretend to be comprehensive reviewing them here. And, in my experience, it's easy to transform an MVC architecture into a Presentation-Abstraction-Controller architecture (again, this is fine, but I just want to emphasize that following a plan should not deter us from doing what works).
- Model-View-Controller. In Pattern-Oriented Software Architecture, Volume 1 (Peter Sommerlad, Michael Stal, Frank Buschmann, Hans Rohnert, Regine Meunier), Wiley, 1996, pp. 125 et seq.
No comments:
Post a Comment