Pevolutionary Clicker

Introduction

Revolutionary Clicker is a project I started with my friends with the intention of telling an exciting story. It combines both gameplay elements from 'Clicker games'(hence the name) and a very popular story telling medium called 'Visual Novels', and runs on the browser platform. In this project, other than designing the game mechanics, concept and story, I was also tasked to create a Visual Novel API by my scene director. This allowed him to write the scenes using solely JavaScript and avoid having to use HTML, CSS AND JavaScript which would needlessly complicate matters.

In this page, I broadly explain the level of abstraction the API provides, explaining the scripting process in the context of Visual Novel scenes, and also how I designed the pseudo-synchronous pipelining for concurrent animations on the screen!

Here are some links related to this API

Visual Novels

Properties of a Visual Novel
>Clicking (anywhere)for story advancement
> Story is told primarily through character conversations
> Characters interact via the textbox on screen
> Movement of characters and background
> "Slide-based" story telling

Design

Why a API?

This Application Programming Interface is in JavaScript, it provides meaning to the script (level of abstraction) so that the writer can make sense of what he is writing at a glance, instead of having to create DOM elements of images, styling them in CSS, then animating them in Javascript. The latter process is repetitive and difficult to debug, and this frequent repetition of similar actions lends well to simplification through the design of an API.

Analogy

The terminology used in the API are analogues of terms from screenplay- Stages and Characters.

The scene starts when a Stage(backdrop) is displayed. Characters can then be added to this Stage to act.

Since Stages comprise a set of backdrops and major changes in location require a change of Stage, a full set of actions on a Stage is called a Scene! Borrowing terminology from animation, the fundamental building blocks of Scenes are Frames. Each frame is the smallest unit within a scene and contains dialogue and actions performed upon the click of a mouse, with clicks advancing the Scene one frame at a time.

Literal Explanation

In a literal sense, a Frame is a sequence of (both synchronous and otherwise) actions that both the Stage and its characters perform, and the Scene is a sequence of Frames. When a scene starts, it automatically plays the first frame. When the frame ends, it awaits a click from the player to advance to the next frame. Attempting to click in the middle of a frame will initiate a check on whether the animations are skippable, resulting in either a skip to the end of the frame or no effect.

Program Logic

The API uses the class-based Object Oriented paradigm instead of the default prototype-based paradigm in JavaScript. It assume each of following - Characters, Stages, Frames and Scenes - are objects, setting their default values during object construction. Thereafter, the objects are manipulated to perform actions ranging from movement and scaling to sprite changes.

Showcase

Without going into too much technical detail (The adventurous might want to take a look at the documentation page), I will demonstrate how this level of abstraction is provided.

Firstly, we will construct a stage! Let's call it brostika and give it the backdrop image "brostika.png".

                        //creating a stage called brostika (a village name!)
var brostika = new Stage("brostika", "brostika.png")
		.complete(); //marks completion of stage

                    

Next, we will create our main character, Sophie! The following details are defined in her constructor:

  • ID: sophie
  • Displayed Name: Sophie
  • Default Image: "sophie/def.png"
  • Width %: 76.3% of screen width
  • Height %: 106.85% of screen width(the API assigns the screen height as a function of screen width for easier scaling)
  • X Position(on screen): 35 left of the origin
  • Y Position(on screen): 52.26 below origin
  • X Reference: X coordinate references the centre of character
  • Y Reference: Y coordinate references the centre of character
Other than the above, we use a chain constructor to give her another sprite(image): Shy Image: "sophie/shy.png"

//create sophie, check documentation for parameters 
                         //   id       name      default image  width  height   x   y     origin    
var sophie = new Character("sophie", "Sophie", "sophie/def.png", 76.3, 106.85, -35, 53.26, 1, 1) 
        .addSprite("shy", "sophie/shy.png") //add a sprite  shy
        .setDefaultAnimateInterpolation("swing") //make all animation swing instead of linearly animate
        .complete(); //complete the construction of the character
                    

Now, we shall create our first frame. The first frame is used to initialise the stage and the character! Within a frame, each line of code is "synchronous". This means they will all execute at the same time. To define an order of events(an action is only taken after the previous animation ended), We can use a promise based system to pipeline the synchronicity, which will be explained further down the page.

                        new Frame(function () {
    //set sophie's position before
    sophie.setXOffSet(0);
    sophie.setYOffSet(53.26);
    //bring sophie to the stage called brostika
    brostika.bringCharacter(sophie);
    //fades in the background over a time span of 500ms
    brostika.display(1,1,500, function () { //<-- this is the promise
        //after the above animation is done, display text
        //parameters are: text, time, promise, name (if there is a speaker), centered, fontsize, fontcolor, bold, italic, skip
        //unfortunately, if your ide can't recognize classes and stuff, use the documentation to check how to fill in these parameters
        brostika.displayText("--Kirinnee VN Engine V0.27--", 400, null, null, true, "2.5vw", null, false, false, false); 
    });
})                               

                    

The code above is, as you can see, rather self explanatory. It sets Sophie's initial position, and loads her onto the 'brostika' stage. After that, the stage fades in, subsequently displaying the title text"--Kirinnee VN Engine V0.27--". Speaking of which, I also go by the handle "kirinnee" on the Internet! c:

In the next frame, we change her sprite to default, after which, we fade her in, making her speak simultaneously.

 new Frame(function () {
        //changes the sprite of sophie to default
        sophie.changeSprite("def", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie fade in AND speak at the same time
                sophie.appear(500);
                sophie.speak("Hello there! I'm Sophie!");
        });
})               
                    

For our last frame, let's make her move to the right by 5% of the screen width within 1 second, and maker her say "Hmmm" when she is done moving.

 new Frame(function () {
        //changes the sprite of sophie to shy
        sophie.changeSprite("shy", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie move the x direction by 5 unit within 1000milisecond 
                sophie.move(5,0,1000,function(){
                        //say hmm after moving!
                        sophie.speak("Hmmm");
                });
        });
})

                        
                    

Finally, we will string together all the frames above to form a frame array.

var frames = [
        //the first frame, frame 0
        new Frame(function () {
            //set sophie's position before
            sophie.setXOffSet(0);
            sophie.setYOffSet(53.26);
            //bring sophie to the stage called brostika
            brostika.bringCharacter(sophie);
            //fades in the background over a time span of 500ms
            brostika.display(1,1,500, function () {
                //after the above animation is done, display text
                //parameters are: text, time, promise, name (if there is a speaker), centered, fontsize, fontcolor, bold, italic, skip
                //unfortunately, if your ide can't recognize classes and stuff, use the documentation to check how to fill in these parameters
                brostika.displayText("--Kirinnee VN Engine V0.27--", 400, null, null, true, "2.5vw", null, false, false, false); 

            });

        })
        , // marks the next part of the array, the comma i mean
        //second frame, frame 2
        new Frame(function () {
            //changes the sprite of sophie to default
            sophie.changeSprite("def", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie fade in AND speak at the same time
                sophie.appear(500);
                sophie.speak("Hello there! I'm Sophie!");
            });
        })
	,
        //frame 3
         new Frame(function () {
        //changes the sprite of sophie to shy
        sophie.changeSprite("shy", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie move the x direction by 5 unit within 1000milisecond 
                sophie.move(5,0,1000,function(){
                        //say hmm after moving!
                        sophie.speak("Hmmm");
                });
            });
        })
];


                    

Using this array, we can define a scene called, "sophieDemo", which will send an alert saying "End of the demostration" once it ends!

                        //creating the scene using the frame array
//parameters: scene id, frames, callback after scene completes
var Demo1 = new Scene("sophieDemo", frames, function () {
    alert("End of the demostration");
});                         

                    

Putting it all together:

                        //creating a stage called brostika (a village name!)
var brostika = new Stage("brostika", "brostika.png")
		.complete(); //marks completion of stage

//create sophie, check documentation for parameters 
                         //   id       name      default image  width  height   x   y     origin    
var sophie = new Character("sophie", "Sophie", "sophie/def.png", 76.3, 106.85, -35, 53.26, 1, 1) 
        .addSprite("shy", "sophie/shy.png") //add a sprite  shy
        .setDefaultAnimateInterpolation("swing") //make all animation swing instead of linearly animate
        .complete(); //complete the construction of the character
var frames = [
        //the first frame, frame 0
        new Frame(function () {
            //set sophie's position before
            sophie.setXOffSet(0);
            sophie.setYOffSet(53.26);
            //bring sophie to the stage called brostika
            brostika.bringCharacter(sophie);
            //fades in the background over a time span of 500ms
            brostika.display(1,1,500, function () {
                //after the above animation is done, display text
                //parameters are: text, time, promise, name (if there is a speaker), centered, fontsize, fontcolor, bold, italic, skip
                //unfortunately, if your ide can't recognize classes and stuff, use the documentation to check how to fill in these parameters
                brostika.displayText("--Kirinnee VN Engine V0.27--", 400, null, null, true, "2.5vw", null, false, false, false); 

            });

        })
        , // marks the next part of the array, the comma i mean
        //second frame, frame 2
        new Frame(function () {
            //changes the sprite of sophie to default
            sophie.changeSprite("def", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie fade in AND speak at the same time
                sophie.appear(500);
                sophie.speak("Hello there! I'm Sophie!");
            });
        })
	,
        //frame 3
         new Frame(function () {
        //changes the sprite of sophie to shy
        sophie.changeSprite("shy", function(){
                //execute this after the previous has been compelted/skipped
                // make sophie move the x direction by 5 unit within 1000milisecond 
                sophie.move(5,0,1000,function(){
                        //say hmm after moving!
                        sophie.speak("Hmmm");
                });
            });
        })
];

//creating the scene using the frame array
//parameters: scene id, frames, callback after scene completes
var Demo1 = new Scene("sophieDemo", frames, function () {
    alert("End of the demostration");
});                         

                    

Result: scene. Click to see how that scene executes!

Promise Pipelining

What?

Generally, promise-based design is uses proxy objects for synchronous code execution.

Why?

My promise system is not actually real synchronous code execution. It functions like one, under the following assumptions:

1)The time difference between each Javascript code line execution is 0 or very close to 0
2)The time the Javascript take to modify DOM element is also 0 or very close to 0

Under the above assumptions, code execution might as well be synchronous as sequential differences would be imperceptible to the human eye.
As some animations may need to be executed sequentially, we need a system that allows animation execution to be dependent on whether other "thread(s)" (characters or the stage) have completed their animation, which is usually an unknown.

How?

This system stores 'the animation to be delayed 'as a variable called the 'promise object' in the preceding action. When the preceding action ends, it will immediately call on the 'promise object' to "fulfill the promise". This executes the delayed animation instead of from cycling to the next loop before perform the action complete check, hence "pipelining".

Consider the 2 animations, 'x' and 'y', performed by 2 different entities, 'a' and 'b', respectively:

                    //a performing x
a.x();
//b performing y
b.y();

                    

and b must perform y after a performs finish performing x, where how long it takes for a to performs x is unknown. Our promise system would store b.y(); as an code within the promise object, and when a finishes x, it invokes the promise to call on the code.

This should be distinguished from parallel asynchronous message passing or animations. In a system that supports concurrent code execution, in the first cycle, it would compute that 'a' has completed 'x' and only on the second cycle, will 'b' perform 'y'. But under a promise system, within the very same cycle, 'b' would immediately perform 'y'. This advantage in latency may appear small in this isolated scenario, but becomes significant during prolonged and complex animation sequences.