the tale of rpg-n

Two years ago I started writing an engine for a visual novel/rpg system. I totally forgot about it, and I’ve decided to start over. This post will dive into an analysis of what I got right and wrong the first time around and what learnings I have from an unfinished project.


Ah, the magic of visual novels. I can’t say I’m well versed in the genre, but I’ve been around the block a couple times. From flash games that parodied Phoenix Wright like Detective Grimoire (the og flash version, not the new one), or Socrates Jones: Pro Philosopher, to games developed with real budgets like Steins;gate and the quintessential Phoenix Wright, I’ve enjoyed the titles I’ve played. However, I found visual novels to be lacking a sense of interactivity that could make the story more engaging. The fun of a visual novel for me is in being able to self-insert and see myself as the protagonist. So I decided to spice things up by making visual novel that combined rpg elements. You could buy items from shops, add friends to your party, and combat enemies in a turn based combat system.

Of course, none of the existing off the shelf options for making visual novels appealed to me, or provided the flexibility of adding arbitrary game play mechanics like an RPG system. I needed to write my own engine. I decided to work in javascript in order to make it easy to distribute the resulting game. Additionally, by making the engine a javascript library, with careful design it should be flexible enough to add any new gameplay mechanic in the future (e.g. a visual novel that has an overworld you can navigate). I call this new engine rpg-n: the rpg novel.




The rpg-n logo. I worked very hard to make it.

So I built it - sort of. It “exists” here: aneeshdurg.me/rpg-n/. There’s a demo on the page, but it is very likely to be broken. The demos are also pretty strange, and if you find them unfunny I sincerely apologize. The strangeness is meant to be evocative of strange fanmade visual novel games. The last commit to to rpg-n repo was on July 31, 2019. Today being May 02, 2021, it’s almost been 2 years since the last commit. What happened?

Well, for starters, development on rpg-n was something I undertook as a summer project before I started my life as a professional developer. It got pretty far, but once summer ended and my employment began, I stopped thinking about it. Another big issue was that once I got the demo to work at all, I lost interest. It felt like the most interesting problems had been solved and the remaining problems felt incredibly difficult.

I recently had some ideas that I think would make good visual novel RPGs, so I think it’s time to clear the cobwebs and bring rpg-n back to life. I’ve been watching a lot of Adam Savage’s One day build series on his youtube channel Tested, and if there’s one thing I’ve learned, is that to make a project successful, you need to invest in setting up what you need and understanding your order of operations. Let’s take a closer look at what work has went into rpg-n and how I can improve on it to make this project something I might actually finish someday.

the developer interface

This was probably the part I spent the most time on and it shows. Let’s take a look at the source for the UI demo (cleaned up just a bit):

<html>
<head>
<title>Simple Story Demo</title>
<link rel="stylesheet" type="text/css" href="../src/css/animate.css"></link>
<link rel="stylesheet" type="text/css" href="../src/css/rpgn.css"></link>
<script type="module">
import {assets} from '../src/assets.js';
import {Game} from '../src/game.js';
import {Character, Player} from '../src/characters.js';
import {Left, Right, Center} from '../src/positions.js'

import {ui, Draw, Scene} from '../src/ui.js';
import * as Actions from '../src/actions.js';
import * as Text from '../src/text.js';

import Typed from '../src/typed/typed.js';

assets.loadImages({
  base_path: './assets',
  school: 'backgrounds/school.jpg',
  coin: 'coin.png',
});

assets.loadAudio({
  base_path: './assets/audio/',
  happy: '335361__cabled-mess__little-happy-tune-22-10.wav',
  coin: '135936__bradwesson__collectcoin.wav',
});

var Sonic = Character.from_obj({
  constructor_args: ['Sonic', 'blue'],
  assets: {
    images: {
      base_path: './assets/sonic',
      'default': 'sonic.png',
    },
  },
});
var s = Sonic.renderer;

var Me = new Player('Me', 'green');
var me = Me.renderer;

var game = new Game(Me);

var splashscreen = new Scene({
  name: 'splashscreen',
  cleanup: true,
  contents: async function(game) {
    return [
      "Welcome to the rpg-n demo!",
      ui.jump('intro'),
    ]
  },
});

var intro = new Scene({
  name: 'intro',
  cleanup: true,
  contents: async function(game) {
    return [
      ui.setBackground(assets.images.get('school')),
      ui.playAudio(
        assets.audio.get('happy'), {asynchronous: true, loop: true, fadeIn: true}),
      await ui.draw(
          Sonic.get_image('default'), Left, {"height": "100%"}, 'lightSpeedIn'),
      s("Hi! I'm Sonic!"),
      s("How are you?"),
      ui.choice(
        ["I'm good! hbu?", 'doing_good', {'backgroundColor': 'rgba(100, 200, 0, 1)'}],
        ["bad.", Actions.NO_ACTION, {'backgroundColor': 'rgba(200, 50, 0, 1)'}],
      ),
      me("Bad."),
      s("Oh. I'm sorry to hear that."),
      await ui.draw(assets.images.get('coin'), Center, {height: "10%"}),
      s("This is a coin, let's toss it and see where it lands!"),
      Draw.animate(assets.images.get('coin'), 'flipInX'),
      ui.exec((game) => {
        if (Math.random() < 0.5) {
          return ui.sequence(
            s("It landed on heads!"),
            ui.delay(500),
            s("You are a lucky person..."),
          );
        }
        return s("It landed on tails. I guess you have to die now.");
      }),
      ui.delay(500),
      s("Want to buy something?"),
      ui.menu(
        ["Booze"],
        ["Old GBA games"],
      ),
      ui.exec((game) => {
        return ui.sequence(
          s("Sorry, I can't sell you " + game.menu_selections.slice(-1)[0] + "."),
          s("I lost my permit to sell things..."),
        );
      }),
      ui.jump('credits'),
    ];
  },
});

var doing_good = new Scene({
  name: 'doing_good',
  contents: function (game) {
    return [
      me("I'm doing well! How about you?"),
      s("I'm swell!"),
      Draw.animate(Sonic.get_image('default'), 'bounce', {asynchronous: true, iterationCount: 'infinite', noCancel: true}),
      s("Thanks for asking!"),
      ui.clearScene(1000), // TODO make clearScene apply fadeOut to everything on the display
      ui.delay(500),
      ui.jump('credits'),
    ];
  },
});

var credits = new Scene({
  name: 'credits',
  cleanup: true,
  contents: async function (game) {
    return [
      // TODO create scrolling credits.
      await ui.draw(Sonic.get_image('default'), Center, {"height": "100%"}, 'flipInY', {'duration': '500ms'}),
      ui.sequence(
        s("This is the end of the game!"),
        ui.playAudio(assets.audio.get('coin'), {asynchronous: true}),
        Text.center_align(s("Thanks for playing!")),
      ),
      ui.delay(1000),
      Actions.HIDE_TEXTBOX,
    ];
  },
});


(async function() {
  await Sonic.wait_for_load();
  await assets.wait_for_load();

  console.log("Starting game");
  game.initialize(document.body, [splashscreen, intro, doing_good, credits]);
  game.run();
})();
</script>
</head>
</html>

Even without reading the rpg-n source (of course, there are no docs), it isn’t (in my opinion) too hard to follow what’s happening here. There’s various mechanisms for getting input from the user and manipulating control flow but it’s fairly intuitive to read. It’s also pretty clear how arbitrary javascript functions can be embedded into the game mechanics, like the random coin toss above (in the combat demos it’s used for determining what to do after combat completes) . The biggest evidence for my claims here is that I still understand how this code works, 2 years after writing it (the same can’t be said for the implementation of the library itself). It’s also amazing how this demo comes it at just around a 100 LoC.

The combat interface is also decent, you can view the source code for the combat demo here and here. The key thing with the combat implementation is that we also need to define all the player/enemy types and the movesets, so it’s a bit longer at around 300 LoC. Some parts of it are non-obvious to me now (like what is Combat.InteractiveCharacter? I vaguely remember what it does, but the naming and motivation for how to use it could have been a lot better. In short, it prevents the in-built “AI” from making decisions and instead converts the decision making process “interactive”) so I wouldn’t praise it for being as well designed as the visual novel portion of the engine.

Other parts of the interface are abysmal or non-existant. For example, the AI for the enemies can’t be replaced. It’s very unclear if the enemy can manage a party. Additionally there’s not much in the demos (mainly combat demo) about items, aside from the health potions, and what is present doesn’t really help illuminate how the item system works (it’s not just a fault of the demo, I know the item system is more implemented than what’s in the demo, but the inventory system is so confusing that I chose to just ignore it for the time being). Due to this code being pure javascript, there’s no type information, so it’s hard to tell what each function accepts as input. While it’s easy enough to understand what this already written code does, I’m not super confident I can make more complicated demos without re-reading the source code.

The goal of rpg-n is to make the engine so easy to use, that a first time programmer might be able to use it without having to learn more than just some basic javascript. To that end, I think this interface is on the right track, but it needs some TLC ([T]ypes, [L]onger and better names, and [C]ool documentation /j).

overcomplicating, overengineering and overthinking

rpg-n was an exciting project for me, and when I embarked on it, I remember thinking - there’s no need to design this out, a good design will emerge organically if I just start working. So I went into it with no design and no plan. Instead I just kept a text file with TODOs and kept appending to it as I found new features and bugs. While this kept me productive (66 commits in 15 days!), it led to lots of spaghetti (yum). The project is a mess, and what’s kept me from coming back to it for so long is knowing the last thing I worked on (implementing saving and loading) had hit a wall that is very hard to overcome with the current design. There’s horrible design patterns, uses of objects and inheritance I’m not proud of, and no clear seperation of responsibilities in some areas of the code.

A large part of this mess comes from the fact that I didn’t really work on any one feature until it was in a good state. I just bounced around from one interesting subproblem to the next, often with no continuity. There’s also features which are probably more “nice to have”s then actual requirements (for example, the fact that I found a CSS library to make combat buttons look nice and retro before getting the rest of the engine complete. And on that note, why is the engine even responsible for styling the buttons?! That should be defined by the game developer!!!). To be fair I wasn’t as good at frontend development when I took on this project as I am now (not that I’m a great frontend dev now either, I’m just better). In my arrogance at the time, I made a lot of bad decisions, and didn’t look hard enough for good solutions, choosing instead to roll my own.

Case in point, the asset loading system. I don’t even know how necessary it is but I implemented it, and it causes the demos to crash or fail to load on most browsers. What I should have done is looked online to see how other projects handle this. I’m not the first person in the world to build a game engine for the browser, and my requirements for game engine performance are definitely lower than a traditional engine. I can definitely use pre-existing solutions. This touches on a greater paradigm shift I’ve experienced over the last few years which is recognizing that software development isn’t a contest of who can write more different types of code, but instead an exercise in clearly defining and solving problems efficiently. If the problem is defined as “I want to write an asset management system because I want to learn how to build one”, that requires a very different approach from “I need a way to ensure audio and image files are loaded when they are used in game”. Mixing the two problems without considering why is a recipe for disaster.

redesigning and rebuilding

It’s clear to me that the current version of rpg-n isn’t long for this world. It’s got some good ideas, but I just don’t see myself getting re-acquainted with the code and then still being motivated to complete the project. It’s probably easier to start fresh and borrow on work and ideas from the current version instead.

I titled this section redesigning, but as I’ve already touched on, this is really designing for the first time. There’s a lot I’ve learned in two years. I’m aware of more technologies that I know I can rely on (e.g. instead of rolling my own saving and loading system, why not using something like localforage to store all state in persistant local storage to begin with?). This time around I intend to be more focused in my approach to implementation and I’ll choose explicit goals to focus on (e.g. the engine will work, but the UI will be very plain and boring beyond basic UI features that are necessary for playability).

Starting fresh also gives me a chance to make lower level design decisions. For example, switching over to typescript. The first time around, I’d never setup a typescript environment before and it seemed like a high barrier to entry to just get something up and running. This time, I think it’s critical to pay the cost of setting up a solid environment before I start again. Another decision is to revisit the idea of defining some kind of markup language or DSL and building a sort of static site generator that builds the visual novel from some simpler interface. In a related vein, it’s worth revisiting whether the current “single page app” architecture for the game is helping or hurting.

some thoughts on motivating myself to start projects

A key part of starting a new project (which is basically what I’m planning on doing here), is to embrace the startup cost. It takes time to get some inertia in the system, and especially when there are problems you want to solve it can feel frustrating to get caught up on less rewarding parts of the project. It’s invaluable to find a mindset where you enjoy the initial aspects of the project and really invest your energy in setting up for success. I think about this kind of thing a lot while biking up hills. It really sucks when you focus on getting to the top, but it’s surprisingly fun if you just focus on the feeling of the hill itself.

I’m not really sure when I’ll resume work on rpg-n since I already have other in-progress projects that are already starved for attention, but at least this post documents my thoughts on the subject whenever I do.

Written on May 1, 2021