A Cinematics Engine for a Video Game in JavaScript

The JavaScript code below implements a simple cinematics engine in JavaScript. Well, “cinematics” is probably the wrong word, because there’s not really anything very kinetic going on. Basically, two static characters at a time talk to each other with speech bubbles. I guess it’s more of a “talking head” engine.  Or, more accurately still, a “talking square” engine.

To see the code in action, copy it into an .html file. Then copy the .pngs included in this post to the same directory, and open the html file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/cinematics.html.

Rick

Jane

Spot

Suburbia

Cinematic

<html>
<body>
<script>

// main

function main()
{
	new ImageLoader().loadImages
	(
		[
			new Image("rick.png"),
			new Image("jane.png"),
			new Image("spot.png"),
			new Image("suburbia.png"),
		],
		main2
	);
}

function main2(imageLoader)
{
	var images = imageLoader.images;

	var scene = new Scene
	(
		"Scene0",
		new Background("Suburbia", images["suburbia.png"]),
		// actors
		[
			new Actor("Rick", images["rick.png"]),
			new Actor("Jane", images["jane.png"]),
			new Actor("Spot", images["spot.png"]),
		],
		// lines
		[
			new Line("Rick", "[enters left]"),
			new Line("Jane", "[enters right]"),
			new Line("Rick", "Jane, have you seen Spot?"),
			new Line("Jane", "His NAME is Mr. Beansprouts."),
			new Line("Rick", "For the hundredth time, no it's NOT."),
			new Line("Jane", "Yes, it is.  I'll prove it."),
			new Line("Jane", "Come here, Mr. Beansprouts!"),
			new Line("Rick", "..."),
			new Line("Jane", "Mr. Beeeeeeansprouts!"),
			new Line("Rick", "..."),
			new Line("Jane", "RIGHT NOW, MR. BEANSPROUTS!"),
			new Line("Rick", "..."),
			new Line("Jane", "Stupid dog.  Forget it."),
			new Line("Jane", "[exits]"),
			new Line("Rick", "..."),
			new Line("Spot", "[enters right]"),
			new Line("Spot", "Is she gone?"),
			new Line("Spot", "I've told her a million times..."),
			new Line("Spot", "...it's DOCTOR Beansprouts."),
			new Line("Spot", "Anyway, you wanted to see me?"),
			new Line("Rick", "Shut up and get in the crate, Spot."),
			new Line("Spot", "Yes, sir."),
		]
	);

	var viewSize = new Coords(200, 150);

	Globals.Instance.initialize
	(
		10, // fontHeight
		viewSize, 
		scene
	);
}

// classes

function Actor(name, image)
{
	this.name = name;
	this.image = image;
}

function ArrayExtensions()
{}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			var key = item[keyName];
			this[key] = item;
		}
	}
}

function Background(name, image)
{
	this.name = name;
	this.image = image;
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}

function DisplayHelper()
{}
{
	DisplayHelper.prototype.drawShowing = function(showing)
	{
		var scene = showing.scene;

		var backgroundImage = scene.background.image.systemImage;
		this.graphics.drawImage
		(
			backgroundImage,
			0, 0
		);

		var actorNames = showing.actorNamesLeftAndRight;

		var widthDivisor = 4;

		var actorPositions = 
		[
			actorPos = new Coords
			(
				this.viewSize.x / widthDivisor,
				this.viewSize.y / 2
			),

			actorPos = new Coords
			(
				(widthDivisor - 1) * this.viewSize.x / widthDivisor,
				this.viewSize.y / 2
			),
		];

		for (var i = 0; i < actorNames.length; i++)
		{
			var actorName = actorNames[i];
			if (actorName != null)
			{
				var actorPos = actorPositions[i];
				var actor = scene.actors[actorName];
				var actorImage = actor.image
	
				this.graphics.drawImage
				(
					actorImage.systemImage,
					actorPos.x - actorImage.size().x / 2, 
					actorPos.y - actorImage.size().y / 2
				);
			}
		}
		
		var line = showing.lineCurrent();
		var text = line.text;
		var isTextStageDirection = (text.indexOf("[") == 0);

		if (isTextStageDirection == false)
		{
			var textWidth = this.graphics.measureText(text).width;
			var textAsLines = [ text ];
			var numberOfTextLines = textAsLines.length;

			var speechBubbleMargin = 8;
			var textMargin = 8;
			var tailWidthHalf = speechBubbleMargin / 2;
			var tailLength = speechBubbleMargin;

			var actorIndex = actorNames.indexOf(line.actorName);
			var actorPos = actorPositions[actorIndex];

			var speechBubbleSize = new Coords
			(
				textWidth + textMargin * 2,
				numberOfTextLines * this.fontHeight + textMargin * 2
			);

			var speechBubblePosX = actorPos.x - textWidth / 2 - textMargin;

			if (actorIndex == 0)
			{
				if (speechBubblePosX < 0)
				{
					speechBubblePosX = speechBubbleMargin;
				}
			}
			else
			{
				if (speechBubblePosX + speechBubbleSize.x > this.viewSize.x)
				{
					speechBubblePosX = 
						this.viewSize.x 
						- speechBubbleMargin 
						- textMargin * 2 
						- textWidth;
				}
			}

			var speechBubblePos = new Coords(speechBubblePosX, this.fontHeight);
			var tailPosX = actorPos.x;
			var cornerRadius = textMargin;

			this.graphics.beginPath();
			this.graphics.moveTo(speechBubblePos.x + cornerRadius, speechBubblePos.y);
			this.graphics.arcTo
			(
				speechBubblePos.x + speechBubbleSize.x, speechBubblePos.y,
				speechBubblePos.x + speechBubbleSize.x, speechBubblePos.y + cornerRadius,
				cornerRadius
			);
			this.graphics.arcTo
			(
				speechBubblePos.x + speechBubbleSize.x, speechBubblePos.y + speechBubbleSize.y,
				speechBubblePos.x + speechBubbleSize.x - cornerRadius, speechBubblePos.y + speechBubbleSize.y,
				cornerRadius
			);

			this.graphics.lineTo(tailPosX + tailWidthHalf, speechBubblePos.y + speechBubbleSize.y);
			this.graphics.lineTo(tailPosX, speechBubblePos.y + speechBubbleSize.y + tailLength * 2);
			this.graphics.lineTo(tailPosX - tailWidthHalf, speechBubblePos.y + speechBubbleSize.y);

			this.graphics.arcTo
			(
				speechBubblePos.x, speechBubblePos.y + speechBubbleSize.y,
				speechBubblePos.x, speechBubblePos.y + speechBubbleSize.y - cornerRadius,
				cornerRadius
			);
			this.graphics.arcTo
			(
				speechBubblePos.x, speechBubblePos.y,
				speechBubblePos.x + cornerRadius, speechBubblePos.y,
				cornerRadius
			);
			this.graphics.fillStyle = "White";
			this.graphics.fill();

			this.graphics.fillStyle = "Gray";
			this.graphics.fillText
			(		
				text,
				speechBubblePos.x + textMargin, 
				speechBubblePos.y + textMargin + this.fontHeight * .8
			);

		}
	}

	DisplayHelper.prototype.initialize = function(fontHeight, viewSize)
	{
		this.fontHeight = fontHeight;
		this.viewSize = viewSize;

		var canvas = document.createElement("canvas");
		canvas.width = this.viewSize.x;
		canvas.height = this.viewSize.y;
		document.body.appendChild(canvas);
		this.graphics = canvas.getContext("2d");
		this.graphics.font = "" + fontHeight + "px sans-serif";

		this.fillStyle = "LightGray";
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(fontHeight, viewSize, scene)
	{
		this.displayHelper = new DisplayHelper();
		this.displayHelper.initialize(fontHeight, viewSize);

		this.inputHelper = new InputHelper();

		this.showing = new Showing(scene);
		this.showing.initialize();

		var millisecondsPerTimerTick = 100;
		setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();	
	}

	// events

	Globals.prototype.handleEventTimerTick = function()
	{
		this.showing.updateForTimerTick();
	}
}

function Image(source)
{
	this.source = source;
}
{
	Image.prototype.size = function()
	{
		return new Coords(this.systemImage.width, this.systemImage.height);
	}
}

function ImageLoader()
{
}
{
	ImageLoader.prototype.loadImages = function(images, callback)
	{
		this.images = images;
		this.images.addLookups("source");
		this.callback = callback;
		this.numberOfImagesLoaded = 0;

		for (var i = 0; i < this.images.length; i++)
		{
			var image = this.images[i];
			var systemImage = document.createElement("img");
			systemImage.src = image.source;
			systemImage.onload = this.loadImages_ImageLoaded.bind(this);
			image.systemImage = systemImage;
		}
	}

	ImageLoader.prototype.loadImages_ImageLoaded = function(event)
	{
		this.numberOfImagesLoaded++;
		if (this.numberOfImagesLoaded >= this.images.length)
		{
			this.callback(this);
		}
	}
}

function InputHelper()
{
	this.isMousePressed = false;
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onmousedown = this.handleEventMouseDown.bind(this);
	}

	InputHelper.prototype.handleEventMouseDown = function(event)
	{
		this.isMousePressed = true;
	}
}

function Line(actorName, text)
{
	this.actorName = actorName;
	this.text = text;
}

function Scene(name, background, actors, lines)
{
	this.name = name;
	this.background = background;
	this.actors = actors;
	this.lines = lines;

	this.actors.addLookups("name");
}

function Showing(scene)
{
	this.scene = scene;
	this.actorNamesLeftAndRight = [];
}
{
	Showing.prototype.draw = function()
	{
		Globals.Instance.displayHelper.drawShowing(this);
	}

	Showing.prototype.initialize = function()
	{
		this.isComplete = false;
		this.lineIndexCurrent = null;
		this.lineCurrentAdvance();
		this.draw();
	}

	Showing.prototype.lineCurrent = function()
	{
		return (this.scene.lines[this.lineIndexCurrent]);
	}

	Showing.prototype.lineCurrentAdvance = function()
	{
		if (this.isComplete == true)
		{
			return;
		}
		
		if (this.lineIndexCurrent == null)
		{
			this.lineIndexCurrent = -1;
		}
			
		while (true)
		{
			var lineIndexNext = this.lineIndexCurrent + 1;

			if (lineIndexNext >= this.scene.lines.length)
			{
				break;
			}

			this.lineIndexCurrent = lineIndexNext;
			var lineCurrent = this.lineCurrent();
			var lineText = lineCurrent.text;

			var isLineTextStageDirection = (lineText.indexOf("[") == 0);
			if (isLineTextStageDirection == false)
			{
				break;
			}
			else
			{
				var enters = "[enters ";
				var exits = "[exits ";
				var left = "left";
				var right = "right";	

				if (lineText.indexOf(enters) == 0)
				{
					var direction = lineText.substr(enters.length);
					if (direction.indexOf(left) == 0)
					{
						this.actorNamesLeftAndRight[0] = lineCurrent.actorName;
					}
					else
					{
						this.actorNamesLeftAndRight[1] = lineCurrent.actorName;
					}
				}
				else if (lineText.indexOf(exits) == 0)
				{
					var direction = lineText.substr(enters.length);
					if (direction.indexOf(left) == 0)
					{
						this.actorNamesLeftAndRight[0] = null;
					}
					else
					{
						this.actorNamesLeftAndRight[1] = null;
					}
				}
			}
		}
	}

	Showing.prototype.updateForTimerTick = function()
	{
		var inputHelper = Globals.Instance.inputHelper;

		if (inputHelper.isMousePressed == true)
		{
			inputHelper.isMousePressed = false;

			this.lineCurrentAdvance();

			this.draw();
		}
	}
}

// run

main();

</script>
</body>
</html>

This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

One Response to A Cinematics Engine for a Video Game in JavaScript

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s