Scrolling a Simple Top-Down Map in JavaScript

The JavaScript code listed below implements a simple, scrollable, top-down map made of colored tiles.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.  Or, for an online version, visit http://thiscouldbebetter.neocities.org/scrollablemap.html.

Use the W, A, S and D keys to move the reticle around the map.

ScrollablleMapMadeOfTiles

<html>
<body>
<script type='text/javascript'>

// main

function main()
{
	var world = new World
	(
		"World 0",
		new Camera
		(
			new Coords(0, 0) // pos
		),
		new Map
		(
			new Coords(16, 16), // cellSizeInPixels
			MapTerrain.Instances._All,
			[
				"~~~~~~~~~~~~~~~~",
				"~~...........~~~",
				"~..............~",
				"~...^^^........~",
				"~~....^^^.....~~",
				"~.......^^.....~",
				"~~~~.........~.~",
				"~~~~~~~~~~~~~~~~",
			]
		)
	);

	var viewSizeInPixels = new Coords(100, 100);

	Globals.Instance.initialize
	(
		new DisplayHelper(viewSizeInPixels),
		world
	);
}

// classes

function ArrayHelper()
{}
{
	ArrayHelper.addLookupsToArray = function
	(
		arrayToAddLookupsTo, 
		propertyNameForKey
	)
	{
		for (var i = 0; i < arrayToAddLookupsTo.length; i++)
		{
			var item = arrayToAddLookupsTo[i];
			var keyValue = item[propertyNameForKey];
			arrayToAddLookupsTo[keyValue] = item;
		}
	}
}

function Camera(pos)
{
	this.pos = pos;
}

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

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0, 0);
	}

	// instance methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;

		return this;
	}
	
	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;

		return this;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;

		return this;
	}

	Coords.prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);

		return this;
	}

	Coords.prototype.magnitude = function(other)
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;

		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;

		return this;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;

		return this;
	}

	Coords.prototype.trimToMagnitude = function(magnitudeToTrimTo)
	{
		var magnitude = this.magnitude();
		if (magnitude > magnitudeToTrimTo)
		{
			this.divideScalar(magnitude);
			this.multiplyScalar(magnitudeToTrimTo);
		}
	}

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		if (this.x < min.x)
		{
			this.x = min.x;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}

		if (this.y < min.y)
		{
			this.y = min.y;
		}
		else if (this.y > max.y)
		{
			this.y = max.y;
		}

		return this;
	}
}

function DisplayHelper(viewSize)
{
	this.viewSize = viewSize;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	DisplayHelper.prototype.drawMapForCamera = function(map, camera)
	{
		this.graphics.fillStyle = "Black";
		this.graphics.fillRect
		(
			0, 0, 
			this.viewSize.x, this.viewSize.y
		);

		var viewSizeHalf = this.viewSizeHalf;
		var cameraPos = camera.pos;
		var cellSizeInPixels = map.cellSizeInPixels;
		var mapSizeInCellsMinusOnes = map.sizeInCellsMinusOnes;

		this.startPosInCells.overwriteWith
		(
			cameraPos
		).subtract
		(
			viewSizeHalf
		).divide
		(
			cellSizeInPixels
		).trimToRangeMinMax
		(
			Coords.Instances.Zeroes,
			mapSizeInCellsMinusOnes
		).floor();


		this.endPosInCells.overwriteWith
		(
			cameraPos
		).add
		(
			viewSizeHalf
		).divide
		(
			cellSizeInPixels
		).trimToRangeMinMax
		(
			Coords.Instances.Zeroes,
			mapSizeInCellsMinusOnes
		).floor();

		this.cellPos.clear();

		for (var y = this.startPosInCells.y; y <= this.endPosInCells.y; y++)
		{
			this.cellPos.y = y;		

			for (var x = this.startPosInCells.x; x <= this.endPosInCells.x; x++)
			{
				this.cellPos.x = x;

				var terrainAtCellPos = map.terrainAtCellPos
				(
					this.cellPos
				);

				this.graphics.fillStyle = terrainAtCellPos.systemColor;

				this.drawPos.overwriteWith
				(
					this.cellPos
				).multiply
				(
					cellSizeInPixels
				).subtract
				(
					cameraPos
				).add
				(
					viewSizeHalf
				);
	
				this.graphics.fillRect
				(
					this.drawPos.x,
					this.drawPos.y,
					cellSizeInPixels.x, 
					cellSizeInPixels.y
				)
			}
		}

		this.graphics.strokeStyle = "Cyan";
		this.graphics.strokeRect
		(
			viewSizeHalf.x - this.reticleSizeHalf.x, 
			viewSizeHalf.y - this.reticleSizeHalf.y, 
			this.reticleSize.x, 
			this.reticleSize.y
		);
	}

	DisplayHelper.prototype.drawWorld = function(world)
	{
		this.drawMapForCamera(world.map, world.camera);
	}

	DisplayHelper.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.viewSize.x;
		this.canvas.height = this.viewSize.y;
		this.graphics = this.canvas.getContext("2d");

		this.cellPos = new Coords(0, 0);
		this.drawPos = new Coords(0, 0);
		this.endPosInCells = new Coords(0, 0);
		this.reticleSize = new Coords(10, 10);
		this.reticleSizeHalf = this.reticleSize.clone().divideScalar(2);
		this.startPosInCells = new Coords(0, 0);

		document.body.appendChild(this.canvas);
	}
}

function Globals()
{}
{
	// instance

	Globals.Instance = new Globals();

	// instance methods

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

	Globals.prototype.initialize = function(displayHelper, world)
	{
		this.displayHelper = displayHelper;
		this.world = world;

		this.inputHelper = new InputHelper();

		this.displayHelper.initialize();
		this.inputHelper.initialize();

		var millisecondsPerTimerTick = 100;

		setInterval
		(
			"Globals.Instance.handleEventTimerTick();",
			millisecondsPerTimerTick
		);
	}
}

function InputHelper()
{}
{
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = event.keyCode;
		var camera = Globals.Instance.world.camera;
		var cameraPos = camera.pos;
		var cameraSpeed = 1;

		if (keyCode == "65") // a
		{
			cameraPos.x -= cameraSpeed;
		}
		else if (keyCode == "68") // d
		{
			cameraPos.x += cameraSpeed;
		}
		else if (keyCode == "83") // s
		{
			cameraPos.y += cameraSpeed;
		}
		else if (keyCode == "87") // w
		{
			cameraPos.y -= cameraSpeed;
		}
		
		
	}

	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}
}

function Map(cellSizeInPixels, terrains, cellsAsStrings)
{
	this.cellSizeInPixels = cellSizeInPixels;
	this.terrains = terrains;
	this.cellsAsStrings = cellsAsStrings;

	this.sizeInCells = new Coords
	(
		this.cellsAsStrings[0].length,
		this.cellsAsStrings.length
	);

	this.sizeInCellsMinusOnes = new Coords
	(
		this.sizeInCells.x - 1,
		this.sizeInCells.y - 1
	);

	this.sizeInPixels = this.cellSizeInPixels.clone().multiply
	(
		this.sizeInCells	
	);

	ArrayHelper.addLookupsToArray(this.terrains, "codeChar");
}
{
	// instance methods

	Map.prototype.terrainAtCellPos = function(cellPos)
	{
		var codeChar = this.cellsAsStrings[cellPos.y][cellPos.x];
		var terrainAtCell = this.terrains[codeChar];

		return terrainAtCell;
	}
}

function MapTerrain(name, codeChar, systemColor)
{
	this.name = name;
	this.codeChar = codeChar;
	this.systemColor = systemColor;
}
{
	// instances

	MapTerrain.Instances = new MapTerrain_Instances();

	function MapTerrain_Instances()
	{
		this.Grass = new MapTerrain("Grass", ".", "Green"),
		this.Rock = new MapTerrain("Rock", "^", "Gray"),
		this.Water = new MapTerrain("Water", "~", "Blue"),

		this._All = 
		[
			this.Grass,
			this.Rock,
			this.Water,
		];
	}
}

function World(name, camera, map)
{
	this.name = name;
	this.camera = camera;
	this.map = map;
}
{
	World.prototype.updateForTimerTick = function()
	{
		var displayHelper = Globals.Instance.displayHelper;

		this.camera.pos.trimToRangeMinMax
		(
			Coords.Instances.Zeroes,
			this.map.sizeInPixels
		);

		displayHelper.drawWorld(this);
	}
}

// run

main();

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

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

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