Skip to content

Architecture Overview

Austen McDonald edited this page Dec 26, 2016 · 10 revisions

Using an example game setup, let's discuss the primary pieces of Legend of the Green Dragon, Daenerys Edition's core code.

Key Concepts

First, let's cover some concepts you'll encounter as you read through this documentation.

Core, Crate and Modules

A full, playable installation of a Daenerys game has a number of parts. The core contains the main game loop, basic classes required for a functioning game, like Character and Scene, and module management code. Unlike the original LotGD codebase, it contains no module code and no "game story" code, like Villages or Forests, etc. It also has no UI, so no way to actually play the game; it is strictly an API for developers to build upon. The code in this repo is the core code, hence the name :)

In conjunction with the core, a Daenerys install has a crate, which wraps the core and is responsible for how the game is presented to the world: showing the game scenes to the user, collecting their navigation actions and passing that to the core's game loop. A simple crate might be a command line app that prints scene descriptions to the console and collects keyboard input, or a web app which renders HTML pages.

In practice, there might be additional layers. For example, the official Daenerys server wraps the core in a GraphQL API crate, but that is still not a playable UI. Our setup then relies on a client that reads/writes to the GraphQL interface. We have web and native mobile client implementations cooking.

We chose this separation of responsibilities to provide more flexibility in how LotGD apps can be built and to support many different UI paradigms, from command line to web to native mobile and beyond. This design also helps insulate the infrequently changing core code from rapidly evolving (and possibly diverging) UI code.

Finally, most of the actual "game" parts of the game, like storyline and interactions between characters, are provided via modules, which are like plugins. Modules can install new data into the game and can change game outcomes by responding to the events delivered by the crate, core or other modules (see Events and Hooks below).

The Game Object

Throughout the code, you'll frequently encounter the Game class, often with an instance called $g. Game provides access to the database, instances of controllers, and support for the game loop. Pretty much all operations go through the Game instance.

Events and Hooks

To facilitate communication between the crate, the core, and modules, the core contains a publish/subscribe communication channel. Any piece of code can subscribe to events via the $g->getEventManager()->subscribe() method call by providing an event name, which is a string. Whenever that event occurs, the registered code's handleEvent($event, $context) method will be called with any context array the calling code provides. The subscriber can then choose to take any action it likes, including modifying the context.

One common type of event is called a hook, which is just an event where the caller expects a response, usually provided via the context. For example, there is a hook called h/lotgd/core/default-scene that is triggered when the core wants to know where the starting scene for users is, as in, where a newly-created user should be placed once they sign up. It expects a Scene object to be placed into $context['scene'] as a response.

An Example Realm

For the purposes of this overview, let's talk about a realm (or world) centered around a simplified representation of the Lonely Mountain from JRR Tolkien's The Hobbit. See the diagram below representing the places, or scenes as we call them, where the user can go in this world:


                                    Lonely Mountain  ─────── Mirkwood
                                            │
                                  ┌─────────┴─────────┐
                                  │                   │
                                  │                   │
                            Throne Room            Forges
                                  │
                                  │
                            ┌─────┴──────┐
                            │            │
                            │            │
                        Traveling     Thorin
                          Elves

Some notes about gameplay in this realm:

  • In the "Forges", the user can buy weapons and armor for her battles in the forest.
  • The traveling elves are not always visiting the "Throne Room", and so may not be available every time the user visits.
  • In "Mirkwood", the user can find and fight beasts, for experience and gold.

Scenes and Navigation

Every location the user can visit in the game, and even every "screen" displayed, has a corresponding Scene model in the database. The basic data about a scene is stored in this model, like a description to display to the user and the connections to nearby scenes. We chose this data-driven approach to organizing scene data to allow game administrators to create, move and modify scenes in their realm without making code modifications. Imagine a module that provides a generic "Inn" scene. Once installed, the game admin can create any number of instances of this Inn scene by making copies and connecting copies to each one of their villages, making appropriate text changes along the way.

Note that when we say every "screen" needs a corresponding Scene model, we mean not only physical locations, like the "Forges", but also "logical" locations, like the buying or selling screens within the "Forges". Again, storing the text inside the database allows for easy customization of these scenes without code modifications.

Constructing a Viewpoint

To the user, a scene has a description and a navigation menu. For example, the Lonely Mountain scene might have the following:

  • Description: "A cavernous interior opens before your eyes, filled with massive pillars of stone. You hear distant hammering and dwarves scurry this way and that."
  • Navigation Menu:
    • (T)hrone Room
    • (F)orges
    • Exit to (M)irkwood

When a user visits a location in the game, the corresponding Scene object's description, list of children and parents, etc. are fetched from the database. The navigation menu is then generated from the list of children and parents. The resulting data is stored in the database as a Viewpoint, representing one user's visit to a scene. Module code can modify this description or navigation menu (or any other part of the Viewpoint) when the Scene is created (see how we implement the Visiting Elves below). However, once generated, the Viewpoint is fixed for that user's visit to the scene.

Get the current user's viewpoint via $g->getViewpoint().

Navigation

Items in the navigation menu are instances of the Action class and are grouped into instances of the ActionGroup class. Both Action and ActionGroup have unique identifiers to help in referencing them. Every Viewpoint has at least one ActionGroup with the ID ActionGroup::DefaultGroup. By default, navigation items targeting the user's previous scene and all children of the active scene will be placed in the DefaultGroup.

To register a navigation action, the crate should call $g->takeAction($id, $parameters) where $id is the action's identifier, reachable via the getId() method on Action. $parameters is an optional associative array of parameters available to modules responding to the navigation hooks.

Dynamic Game Elements and Modules

So far we've seen how static description and navigation items are created from the data stored in the database and stored in Viewpoints. What about dynamic content, like conditional navigation items or descriptions that change based on circumstances (think about the town clock, for example)? Let's talk about how we can implement these features using the navigation hooks.

When a Viewpoint is created, a hook is published called h/lotgd/core/navigate-to/[scene-template], where [scene-template] is $scene->getTemplate(). We haven't talked about templates yet, but each Scene object has a template property that represents the "type" of scene it is. For example, maybe there's another mountain in our game; let's call it "Mt. Doom." Both it and "The Lonely Mountain" might share the same template, because maybe they have a lot of the same elements or come from the same module. The template itself might be something like lotgd/wiki/mountain. When the user navigates to either "Mt. Doom" or "The Lonely Mountain", a hook called h/lotgd/core/navigate-to/lotgd/wiki/mountain would be published.

Subscribers to this navigate-to hook are passed a context like this:

[
    'referrer' => $referrer,
    'viewpoint' => $viewpoint,
    'scene' => $scene,
    'parameters' => $parameters,
    'redirect' => null
]

This context gives subscribers the opportunity to modify the $viewpoint based on data stored within the current $scene, any navigation $parameters passed through, and the previous scene, passed as $referrer. Subscribers can even redirect to another scene by setting $redirect to another scene, but this action is out of scope for this overview.

Let's suppose we want to display the "Visiting Elves" navigation option inside the "Throne Room" only on even days. The "Visiting Elves" module subscribes to h/lotgd/core/navigate-to/lotgd/wiki/throne-room, so it can modify the "Throne Room" menu. Inside its event handler, there would be code like this:

public static function handleEvent(Game $g, string $event, array &$context)
{
    // handleEvent() covers all subscribed events, so you need to check for
    // a specific one.
    switch ($event) {
        case 'h/lotgd/core/navigate-to/lotgd/wiki/throne-room':
            $viewpoint = $context['viewpoint'];

            if ($g->getTimeKeeper()->gameTime()->format('z') % 2 == 0) { // is this an even game day?
                $visitingElvesScene = self::findVisitingElvesScene(); // convenience function to find the elves scene, more on this later
                $viewpoint->removeActionsWithSceneId($visitingElvesScene->getId());
            }

            break;
    }
}

This code removes the "Visiting Elves" scene from the "Throne Room" menu on even days.

A note about self::findVisitingElvesScene(). To find the specific instance of Scene that represents the "Visiting Elves" location in the game, we could use a few mechanisms:

  1. We could search the scenes in the actions and look for one with the right template, though there may be more than one if the user duplicates the scene, etc.
  2. When the module is created, we can create a "Visiting Elves" Scene object and store its ID somewhere, then fetch it inside self::findVisitingElvesScene(). This our preferred method and module models have a set of methods called setProperty() and getProperty() to help with this. See the example in the onRegister() method inside the Weapons Shop module.

Attachments

We've seen how modules can modify the $viewpoint, including changing navigation items. But a description and navigation menu is simply too restrictive for the rich set of features we envision for the game, so each $viewpoint can have attachments as well, which are arbitrary associative arrays.

In our example realm, we will implement the "Forges" scene's ability to buy weapons and armor via this attachment scheme using the Forms module. Forms provides a simple, HTML-inspired form model complete with a list of elements, some text for a submit button, and an Action object for that submit button. Note that because only actions present in the navigation menu can be used to move the user from scene to scene, the Forms module adds its submit action to a special hidden ActionGroup with ID ActionGroup::HiddenGroup.

See the addForSaleForm() method in the Weapons Shop module for a full example of this kind of attachment.

Note that attachments are simply associative arrays, and so in true Daenerys fashion, it is up to the crate to determine how to display attachments and process user interactions with them. In our "Forges" case, presumably an HTML crate would render an HTML <form> tag.