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.