Build a Pseudo3D Game Engine with JavaScript Using Raycasting

Pseudo3D engine with JavaScript (from scratch)

In this article I’m going to show you how to build a simple Pseudo3D engine with JavaScript. No libraries, pure JavaScript.

But first things first, grab your coffee… it’s going to be an interesting learning journey.

What is Pseudo 3D?

In the early 90’s (and definitely before that) the computing power for computers weren’t that much to handle real 3D computations, in order to build real 3D experiences. So, they had to figure out a way to render things like 3D (or at least something close to that).

id Software was the first to adapt the Pseudo 3D rendering technique in their game: Wolfenstein 3D

. By . Originally released on May 5, 1992 for MS-DOS

So, how did they do it?

The solution was to use a technique called Ray-cast, and as the name implies, the idea is to cast/send rays on a 2D plane and once it hits an object in that plane (a cell of some value in the grid) it will send back some information about that object: distance, color, … etc. Then based on these info, the rendering engine is going to render a column, and the height of that column is going to be inversely proportional to its distance from the player, i.e the further it is the shorter the line will be, to simulate depth.

See the image below for better visualization

The orange cell (square) has been mapped to series of columns

Also, please note that what I’m going to teach you is an extremely simplified version of the approach used to render Wolfenstein 3D engine (also with less Math). I’m not going to care much about performance, like dividing the game map to zones in order to reduce the draw calls and other FPS (Frame Per Second) related improvements. Long story short, I’m going to make it as dumb as possible and try to remove most of the complexity required to build an efficient renderer, because all what we care now is to have an understanding on how to build such an engine in an abstract way.

Let’s do it…

Let’s create an empty folder and call it pseudo3D-renderer.

Then, in that folder we need to create 5 files:

  1. index.html
  2. styles.css
  3. index.js
  4. map.js
  5. util.js

index.html

<!DOCTYPE html>
<html>
<head>
<link rel=”stylesheet” type=”text/css” href=”style.css”> <title>Pseudo3D</title>
</head>
<body>
<p>Move mouse to move the camera. Press Right/Left arrows to rotate the camera
</p>
<canvas id=”scene”></canvas>
<canvas id=”canvas”></canvas>
<script src=”util.js”></script>
<script src=”map.js”></script>
<script src=”index.js”></script>
</body>
</html>

Nothing fancy, just a basic html file, but note that we have two canvases; one to render the 3D scene and one to render the mini map (grid view).

style.css

html, body {
background: #000000;
font-family: 'Courier New', Courier, monospace;
color: #FF4422;
font-size: 14px;
}
canvas {
display: block;
margin: 0 auto;
background: #222222;
margin-top: 10px;
}

I don’t think there is an urgent need to explain what is happening in this file, we’re just settings some colors, positions, margins, … etc.

util.js

const toRadian = d => d * Math.PI / 180; const circle = function(context, x, y, r) {  
context.beginPath();
context.fillStyle = "orange";
context.arc(x, y, r, 0, Math.PI * 2);
context.fill();
context.closePath();
}
const line = function(context, sx, sy, tx, ty) {
const w = 0;
context.beginPath();
context.globalAlpha = 0.8;
context.strokeStyle = "#FFFFFF";
context.moveTo(sx, sy);
context.lineTo(tx, ty);
context.stroke();
context.globalAlpha = 1;
context.closePath();
context.fillStyle = "white";
context.fillRect(tx - w / 2, ty - w / 2, w, w);
};

const rect = function(context, x, y, w, h, color, mode = "stroke", isCentered = false) {
context.beginPath();
if (isCentered) y = y - h / 2;
if (mode === "stroke") {
context.strokeStyle = color;
context.strokeRect(x, y, w, h);
context.stroke();
} else {
context.fillStyle = color;
context.fillRect(x, y, w, h);
}
context.closePath();
};

Don’t think about it too much, all what it has is 4 functions for converting degrees to radians, drawing circles, rectangles, and lines. That’s it.

map.js

const map = [    
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 1, 2, 0, 0, 0, 2, 2, 0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[3, 3, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3],
[3, 3, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4],
[0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4]];

This is a one way we can build a 2D array in JavaScript, where each index in this array is also an array. It’s constructed with 20 rows by 20 columns (if I didn’t miscalculated it) to build the grid and store each cell with its relevant value so we can render that cell on the grid.

index.js

This is where we’re going to write the logic for renderer engine.

From now on every piece of code will be added within the body of the window.onload d function.

window.onload responsibility is to make sure that the window was loaded successfully, so we can write our code safely.

window.onload = function() {
// the rest of the logic will be added here...
}

Next, we’ll setup the Canvases and get the Context object so we can draw things on both Canvases.

const canvas = document.getElementById("canvas");
const scene = document.getElementById("scene");
const context = canvas.getContext("2d");
const sceneContext = scene.getContext("2d");
const cellSize = 20;
const screenW = cellSize * map[0].length;
const screenH = cellSize * map.length;

Note: it’s better to call the canvas object above gridCanvas, also we can call the scene object a sceneCanvas, but let’s keep things as is for now.

So, what is going on in the above logic?

We are getting the canvas object which we’ll render the mimi map (the grid view) on it, and the scene Canvas to render 3D things on it.

Then, we are getting the Context object so we can use it to draw things.

Then we’re setting the cell size.

Then we’re setting the screen width/height multiplied by cellSize to find the maximum width/height required to render the grid, based on how many rows and columns we have in our map array.

Next…

const fov = 60;const camera = {    
x: parseInt(map[0].length / 2) * cellSize + cellSize / 2,
y: 14 * cellSize + cellSize / 2,
rotation: 0,
angle: toRadian(-90),
update: function() {
this.rotation *= 0.5;
this.angle += this.rotation;
}
};
const colorMap = {
1: "orange",
2: "green",
3: "red",
4: "yellow",
5: "#4488FF"
};

const sceneData = [];

fov (Field of View): how wide is the angle of the camera should see from the scene. Note that the wider the angle the more fish-eye effect you’ll get.

Then we’re setting the camera object, which is basically the player who will be moving in the grid.

colorMap is nothing more than a map object to map every cell’s value to a specific color.

sceneData will store some important values once the ray hits a cell on the grid.

Next…

canvas.width = screenW;  
canvas.height = screenH;
scene.width = 820; scene.height = 460;
scene.style.background = "#111111";
scene.style.border = "solid 2px white";

Here we are setting the width/height of the Canvas (grid view) based on the values we calculated previously.

Then we are setting some arbitrary values for width/height of the science.

Then we are setting some styles (you can do that in style.css, though)

Next, we are going to build a function to render the grid

const drawMap = function(matrix) {    
for (let y = 0; y < matrix.length; y ++) {
for (let x = 0; x < matrix[y].length; x ++) {
if (matrix[y][x] > 0 && matrix[y][x] < Object.keys(colorMap).length) {
const color = colorMap[matrix[y][x]];
rect(context, x * cellSize, y * cellSize, cellSize, cellSize, color, "stroke");
}
}
}
};

We are looping through each row/column of the grid to add a cell, then we are using the rect util function to draw a rectangle (square in our case).

Next is the most important function

const castRay = function(srcX, srcY, angle) {    
let rayX = srcX + Math.cos(angle);
let rayY = srcY + Math.sin(angle);
let dst = 0;
let isHit = false;
while (!isHit && dst < scene.width) {
dst += 0.1;
rayX = srcX + Math.cos(angle) * dst;
rayY = srcY + Math.sin(angle) * dst;
const row = parseInt(rayY / cellSize);
const col = parseInt(rayX / cellSize);
const a = camera.angle - angle;
const z = dst * Math.cos(a);
const h = scene.height / 2 * 64 / z;
if (rayX > screenW - 4 || rayX < 4 || rayY < 4 || rayY > screenH - 4) {
isHit = true;
sceneData.push({ h, val: 5}); // world boundaries
} else
if (map[row][col] > 0 && map[row][col] < Object.keys(colorMap).length) {
isHit = true;
sceneData.push({ h, val: map[row][col] });
}
}
line(context, srcX, srcY, rayX, rayY);
};

All what this function does is that it receives both positions on x and y axis from which the ray was sent (the circle in our case, or the player) with the angle we want the ray to be directed to, and keep marching until this ray hits a cell on a specific location, once it hits a cell on the grid (using row/col values) it will push some data to the sceneData array, note that what we care to send is the calculated height and the value of the cell in order to render the desired color for that cell.

Note: you’ll gain a huge performance boost if you don’t call the line function, or you can do this: if (angle === camera.angle) line(context, …) to only draw one line, which the the direction of the player in the grid.

Next, let’s send some rays

const castRays = function() {    
sceneData.length = 0;
for (let x = -fov / 2; x < fov / 2; x += 0.5) {
const rayAngle = camera.angle + toRadian(x);
castRay(camera.x, camera.y, rayAngle);
}
const w = parseInt(scene.width / sceneData.length);
for (let i = 0; i < sceneData.length; i ++) {
const h = sceneData[i].h;
const alpha = h / 200;
sceneContext.globalAlpha = alpha;
const x = i + (i * w);
const y = scene.height / 2;
rect(sceneContext, x, y, w, h, colorMap[sceneData[i].val], "fill", true);
sceneContext.globalAlpha = 1;
}
};

Now, castRays is going to call the castRay function many times and send a bunch of rays based on the fov (Field of View) to fill the sceneData array, then we can render columns based on what we have received from each index in that array.

Next, we update

const update = function() { camera.update(); castRays(); };

Next, we draw

const draw = function() {    
context.clearRect(0, 0, canvas.width, canvas.height);
sceneContext.clearRect(0, 0, scene.width, scene.height); drawMap(map);
circle(context, camera.x, camera.y, 8);
};

Next, our entry point is the frame function to call draw, update and requestAnimationFrame to keep calling itself.

const frame = function() {    
draw();
update();
requestAnimationFrame(frame);
};

Next, we handle keydown and mouse to move the Camera

document.body.onkeydown = function(e) {    
if (e.keyCode === 37)
camera.rotation -= 0.1
else if (e.keyCode === 39)
camera.rotation += 0.1
};
canvas.onmousemove = function(e) {
camera.x = e.offsetX;
camera.y = e.offsetY;
};

onkeydown is going to handle rotating the player (camera) to look around.

onmousemove is going to put the player (camera) relatively to the mouse x/y positions

And finally we call the frame function to start the engine

frame();

And that’s it, you have the basic engine where you can use as a template and start improving it to build your own classic Pseudo3D game with it.

Please note that I have skipped explaining some details to give you an abstract idea on how to build Pseudo3D renderer without adding noise to the basic knowledge needed to build this renderer.

Also, there are better ways to use more precise Math to get better results, but once you understand this you can make some research to refine the code and have an improved version of this renderer engine.

Hope you enjoyed reading this article :)

Github repo: https://github.com/fahadhaidari/pseudo3d.js

Connect with me on LinkedIn

Follow me on Twitter

Subscribe to my YouTube channel: Game Code Bites

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store