In March of 2010, I wrote a visual novel (VN) to explain hypothesis testing to my Psychology Research Methods class. I used the Ren'py visual novel engine, and it worked great. Grant Paton-Simpson, the author of SOFA - Statistics Open For All liked the content, but said he would prefer if it were web-based instead of having to download an executable file.
After some experimentation, I have come up with a visual novel engine written in JavaScript. It is not meant as a replacement for Ren'py, nor is it intended to do everything that Ren'py does. I have tried to make it general enough to do a variety of simple VNs, but my main purpose was to adapt a single visual novel to the web. If it serves your purposes too, that’s great.
Although the engine was inspired by Ren'py, it is not a copy. Just as Ren'py is written in Python and “plays well” with that language, this engine was written to work in an HTML/CSS/JavaScript environment, and its notation is closely linked to that environment.
From a conceptual level, a visual novel takes place in two screen areas: the tableau, where all the images appear, and the dialog area, where the characters’ speech appears. The novel is populated by actors, which can either be characters (people, animals, magical beings, etc.), text blocks (used to display text that isn’t someone’s speech in the tableau), or menu items, which let you take different paths through the novel.
The novel has a script that lets you specify which characters
appear at what times, what they say, and how the menu items let the reader
take a path through the novel. This is not the same as your
<script>
element that lets you put JavaScript into
your HTML file. Speaking of that, though, let’s look at:
This tutorial will create a sample novel in file exampleNovel.html. It is not a particularly interesting visual novel (in fact, its plot line is non-existent); rather, it’s designed to be an example of, well, an example.
In the <head>
of your document, you must include
the visual novel JavaScript file, with markup like this. In this example,
the JavaScript is in the same directory as the HTML file. For this tutorial,
all the images the novel uses will be placed in a subdirectory named
images
, and audio will be placed in a subdirectory named
audio
. There’s no law that says you must do this, but
it’s a good idea to keep from having one directory with an
unmanageable number of files in it.
<script type="text/javascript" src="visual_novel.js"></script>
You will also include a <script>
element that
refers to the JavaScript that describes your novel.
For this example, the file will be named
exampleNovel.js, and it will reside in the same directory as
the HTML file.
<script type="text/javascript" src="exampleNovel.js"></script>
<body>
In the <body>
of the document, you set up the
tableau and dialog areas, as well as an area for the background image
of the tableau. Here is a sample setup. The line numbers at the left are
for the explanation that follows; don’t enter them into your markup.
1 <body onload="initNovel(800, 600);"> 2 <div id="novelDiv"> 3 <div style="position: absolute; 4 left:0; top: 0; z-index:-1"> 5 <img id="background0" src="images/empty.png"> 6 </div> 7 <div style="position: absolute; 8 left:0; top: 0; z-index:-1"> 9 <img id="background1" src="images/empty.png"> 10 </div> 11 <div id="dialogDiv" style="z-index:100"> 12 </div> 13 </div> 14 <div style="text-align:center; margin-top: 0.5em;"> 15 <input type="button" value="Back to Beginning" 16 onclick="initNovel(800, 450)"/> 17 </div>
id
must be
"novelDiv".<div>
The position
,
left
, and top
make the background image appear
at the upper left of the tableau; the z-index: -1
ensures that the background image appears behind the characters.id="background0"
.
In this case, the initial background is a totally transparent image.
id
must be
"dialogDiv".
If your <div id="dialogDiv">
is inside the
<div id="novelDiv">
(as it is in the
sample novel, “The Question”), you must add
style="z-index:100"
to the dialogDiv
to
ensure that the dialog always appears above the characters.
onclick
re-initializes the
novel, just as you did in line 1.
At this point, you can use CSS to attach styles to the tableau,
dialog area, and menu items. Here is a sample. First, the tableau.
The width and height must match the numbers you specified in the
call to initNovel()
, and the position: relative
is required in order to place the image properly within in the tableau.
The text-align: center
on the body
is
necessary in order for the tableau to be centered horizontally on the
screen when using Internet Explorer.
body { text-align: center; } #novelDiv { width: 800px; height: 600px; border: 1px solid black; margin-left: auto; margin-right: auto; position: relative; text-align: center; }
The dialog, in this case, is within the tableau rather than below it. The padding plus the width add up to 800 pixels so that the borders coincide.
#dialogDiv { font-family: "Deja Vu Sans", Helvetica, Arial, sans-serif; font-size: 20px; position: absolute; width: 790px; height: 140px; top: 450px; left: 0; padding: 5px; background-color: #3d372c; color: white; text-align: left; filter: alpha(opacity=75); opacity: 0.75; }
Finally, add styling for menu items. Of course, these sizes and colors are just an example; you may change them to reflect the look you want for your novel.
.menuItem { font-family: "Deja Vu Sans", Helvetica, Arial, sans-serif; font-size: 18px; background-color: #0050a0; color: white; text-align: center; width: 75%; margin: 0.5em auto; padding: 0.5em 3em; -moz-border-radius: 15px; border-radius: 15px; } .menuItem:hover { background-color: #ff8c00; }
Now, turn your attention to the exampleNovel.js
file. This is
where you will describe the novel’s actors and write the script
that puts them through their paces.
If you have a lot of fairly large images, you want them to be available
as soon as your novel needs them. You can start a preloading process to
have the browser retrieve the images while the rest of the page loads.
Create an array with the names of the files you want to preload.
Here is an example. Notice that the images/
directory
isn’t in the file names; that saves some typing.
(See the pictures that this example preloads.)
var preload = [ "hills1.jpg", "hills2.jpg", "amazed1.png", "amazed2.png", "cool1.png", "cool2.png", "hand_waving1.png", "hand_waving2.png", "laughing1.png", "laughing2.png", "loving1.png", "loving2.png", "simple1.png", "simple2.png", "smiling1.png", "smiling2.png", "sad1.png", "sad2.png", "silence1.png", "silence2.png", "angry1.png" ];
Now add this code to do the actual preloading; here is where you
will use the name of the path to the images
directory.
var preloadObj = new Array(preload.length); for (var i = 0; i < preload.length; i++) { preloadObj[i] = new Image(); preloadObj[i].src = "images/" + preload[i]; }
The next part of the JavaScript files declares all the variables you
will need for the script itself, your characters, textblocks, and
(optionally) positions that your script will use. The next line is
absolutely essential; it is the variable that will hold your script,
and it must be named script
.
var script;
Now, reserve variables for your main characters. In this tutorial, there are two main characters named Tyler (a tourist) and Gavin (a guide), along with a narrator. There will also be two pseudo-characters: one used to display photos or diagrams, and a text block used to display text in the tableau.
The following example also declares variables used to specify the position of characters. Tyler and Gavin will appear at the lower left and lower right side of the window, and the diagram will appear in the upper center. Although you can declare multiple variables on one line in JavaScript, code is easier to read and maintain if you put one variable per line. You can use any variable names you want for your characters. It’s best if they resemble the characters’ actual names, but you can keep them short to save yourself some typing.
var tyler; var gavin; var n; // short for "narrator" var photo; var textBlock; var leftSide; var rightSide; var upperCenter;
Actual initialization of the variables takes place in a function which
must be named prepareNovel
. The first two lines
will give the path to the sub-folders where your images and audio live.
In this case, there’s no audio, so the audioPath
is set to
""
. If you aren’t using a sub-folder for your images
or audio, you should also set the path to ""
.
function prepareNovel() { novel.imagePath = "images/"; // path to your image directory novel.audioPath = ""; // path to your audio directory
Continuing the function, initialize the characters. A character is initialized by giving the name and the color in which his name appears when he speaks in the dialog area. The name must be in quote marks and can include blanks. The color may be in any format that would be acceptable in CSS. Because the narrator doesn’t have a name, no name will appear when he “speaks,” so you don’t have to specify a color.
gavin = new Character("Gavin", {color: "rgb(64, 204, 64)"}); tyler = new Character("Tyler", {color: "#ffff00"}); n = new Character("");
To specify a position on the screen, you give four numbers:
You may omit the last two numbers, in which case they will be presumed to be zero. Warning: this may change to a default of 0.5 in later versions, depending upon feedback from authors who use the novel engine.
An anchor tells how far to offset the top or left point of an image. Consider these three positions:
place1 = new Position(300, 400, 0, 0); place2 = new Position(300, 400, 0.5, 0.5); place3 = new Position(300, 400, 1, 0);
Let’s say one of your characters has an image that is 200 pixels wide
and 150 pixels tall. If you display the character at place1
, the
upper left corner of the image will be at coordinate (300, 400) in the
tableau, because the anchors are zero; there is no offset.
If you display the image at place2
, the left coordinate will
be shifted left by one-half the image width, and the top coordinate will
be shifted up by one-half the image height. That will make the
upper left corner of the image appear at
(300 - (200 * 0.5), 400 - (150 * 0.5)),
or
(200, 325). In effect, this will put the
center of the picture at (300, 400).
If you display the image at place3
, the left coordinate will
be shifted left the entire image width, and the top coordinate will be
not change. That will make the upper left
coordinate of the image appear at (100, 400),
effectively putting the right edge of the picture at x-coordinate
300.
Here are the four positions that the example novel uses.
leftSide = new Position(0, .75, 0, 1); rightSide = new Position(800, 450, 1, 1); upperCenter = new Position(0.5, 0.3, 0.5, 0.5); rightTop = new Position(0.9, 0.1, 1, 0);
The leftSide
position will place images so that their
left side is at the left of the screen and their bottom side is
three-fourths of the way down the tableau.
The rightSide
position will place images so that
their right and bottom side are at location (800, 450), which
is all the way at the right and three-fourths of the way
down the tableau.
The upperCenter
position will place images so that their
horizontal centers are at the center of the tableau, and their vertical
centers about three-tenths down the screen.
See if you can figure out where an image at rightTop
would
appear.
Warning: because JavaScript does not distinguish between floating point and integers, a position of (1, 1, 0, 0) will be taken to mean 100% of the width and 100% of the height of the tableau, not an absolute location one pixel to the right and one pixel below the upper left of the tableau. That means you will not be able to place an image at absolute position (1, 1).
Now that you have specified a position, you can use it to create the
photo
character and set its initial position. The photo
doesn’t speak, so it doesn't need a name or a color.
photo = new Character("", {position: upperCenter});
The last item to define in this example is a text block. It doesn’t
really need a name either, but I put one in just as an example. If you
don’t want to name your text blocks, just put in the null
string ""
.
textArea = new TextBlock("myText");
At last! You can finally start entering the script that tells the characters what to do. The following script, which consists of pairs of items in a JavaScript array, establishes a scene and has the narrator say something. In this script, the little arrow (→) means that the line is continued. The line numbers at the left are for reference.
1 script = [ 2 label, "start", 3 scene, "hills1.jpg", 4 n, "It’s a beautiful day at Montgomery Hill Park,→ where we meet Tyler the tourist, who is looking for Gavin the Guide." 5 ];
"start"
indicates the place where the
novel begins.scene
command clears out any characters that
might be on screen and sets the background to the filename
given as the second item ("hills1.jpg" in this case).
’
gives a curly right single quote (’),
which looks better at large sizes than a straight-up-and-down quote (').
(See an example)
Now add the following lines to make the characters appear and speak (they are in bold). Note that I had to add a comma at the end of line 4 in order to avoid a JavaScript error. This is the most common cause of errors when you write your script. Always remember to add a comma to the last line in the script before adding new lines!
1script = [ 2 label, "start", 3 scene, "hills1.jpg", 4 n, "It’s a beautiful day at Montgomery Hill Park,→ where we meet Tyler the tourist, who is looking for Gavin the Guide.", 5 tyler, {image: "worried1.png", position: leftSide}, 6 tyler, "Hey, Gavin, where are you?" 7 gavin, {position: rightTop, image: "simple2.png"}, 8 tyler, {image: "simple1.png"}, 9 gavin, "Up here in the hills, Tyler!", 10 tyler, "Please come down here. It’s easier to talk with you.", 11 gavin, {position: rightSide}, 12 gavin, "OK. That better?", 13 tyler, "Much better, thanks!" 14];
tyler
appear
with the given image
at the position
you
specify.gavin
appear at the
specified position
with the given image
.
You can specify image and position in any order you want. Ordinarily,
you would pick one order and stay with it throughout your script.gavin
character’s position has changed,
but since no image has been specified, it stays the same as it was.
At this point, the story has a branch; Tyler can choose to either see dangerous or not-so-dangerous animals. Whenever you have a choice point in the story, you add a menu, as shown in the following part of the script.
1 tyler, "Much better, thanks!", 2 3 label, "menu1", 4 menu, [ 5 "So, what would you like to see?", 6 "Dangerous animals", [jump, "wild"], 7 "Not-so-dangerous animals", [jump, "tame"], 8 ], 9 10 label, "wild", 11 scene, "hills2.jpg", 12 /* script continues... */
menu
is contained in a JavaScript array (which
is enclosed in square brackets).label
of "wild".
Similarly, line 7 makes a button with the text
“Not-so-dangerous animals” appear. If the reader clicks it,
she will go to the section of the novel that has a label
of "tame".
In this example, there’s only one thing to do as the result of a click—a simple jump. It is possible to do more than one thing as the result of a click on a menu item. This is covered in the advanced features section of the tutorial.
It is also possible to display a character’s image and have the
character speak with one command.
Just add a say
property in addition to specifying the image.
The following two sequences do the same thing:
// sequence 1: tyler, {image: "loving1.png"}, tyler, "Aww, that’s really nice.", // sequence 2: tyler, {image: "loving1.png", say: "Aww, that’s really nice."},
With these simple commands, you can put together a reasonable visual novel. The full example is here. The only feature that it uses that has not been explained here is the TextBlock, which is discussed in the advanced features.