A raycaster is a project that renders a 3D world based on a 2D map. This is a working example. A raycaster in Scratch is usually single frame and low resolution, to prevent lag.

Raycasting should not be mistaken with raytracing, which renders rays with more physical accuracy, catering to reflection and refraction of light rays, and traces rays in two dimensions rather than one dimension like in a raycaster.

Concept

A visual representation of the raycasting process

Raycasting works by casting "rays" to measure the distance to the nearest wall, hence the term "raycaster". The program send out rays starting from the player, moving forward until it hits a wall, at which point it takes the distance it has traveled and draws a column based on the distance. The closer the wall, the larger the column. The rays are sent in different directions, with the angle sent determining where the column will be drawn. A ray sent out to the right side of the player's viewpoint will draw a column at the right side of the screen and a ray sent to the left will draw a column at the left. After all rays have been sent, the complete picture will be seen.

Sprite Based vs List Based

This page explains two types of raycaster that can be programmed: the sprite-based method, and the list-based method. Each has their own advantages and disadvantages:

Sprite Based List Based
Degree of Difficulty Easy Hard
Framerate Low High
Knowledge Required Basic Scratch Programming Scratch Programming; Trigonometry; Array
How Map is Stored As a Sprite; each map/world is a Costume As an Array (grid of numbers); each map is a separate array
Sprites Needed 1 or more, depending on setup At least 1 (renderer)
Other Advantages Can be used for making worlds with curved walls Can easily generate random worlds

For beginners, it is recommended to start by making the sprite-based method, as it is easy and not complicated. For more advanced programmers and those who have made a sprite-based raycaster, it is recommended to make an array based raycaster, which is faster but more complicated.

Sprite-Based Raycaster

Document stub.png This article or section may not have content matching Scratch Wiki editing standards. Please improve it according to Scratch Wiki:Guidelines and Scratch Wiki:Editing Conventions. (September 2020)
Reason: 'We' used a lot

The necessary components of a raycaster are:

  • A "map"
    • The map shows the layout of the level.
  • A "sensor"
    • The sensor will compute distances from the player to each wall.
  • A "renderer"
    • The renderer draws each wall based on the distance given by the "sensor".

This tutorial will use four sprites to accomplish these three requirements. If someone is stuck at any time, here is a finished example.

Map

An example map.

Make a map sprite with walls, preferably 480x360. Floor space should be "clear" colored.

Then, add the following script to it:

when gf clicked
go to x: (0) y: (0)
set [ghost v] effect to (100)

Person

Next, a person sprite that walks around the map will be made.

Make a person sprite that has a one pixel by one pixel costume. The costume should be centered on the pixel. This is important.

Before diving into the sprite's scripts, note that all (speed) should be replaced with how fast the player needs to move.

Put these scripts in the "person" sprite:

when gf clicked
go to x: (0) y: (10) // Or wherever you want the sprite in the map.
set [ghost v] effect to (100)
broadcast [sense v] and wait
forever
  if <key [right arrow v] pressed?> then
    turn cw (3) degrees // This can be adjusted to rotate faster, to fight lag.
    broadcast [sense v] and wait
  else
    if <key [left arrow v] pressed?> then
      turn ccw (3) degrees // This can be adjusted to rotate faster, to fight lag.
      broadcast [sense v] and wait
    end
  end
  if <key [up arrow v] pressed?> then
    move (speed)::custom
    broadcast [sense v] and wait
  else
    if <key [down arrow v] pressed?> then
      move ((-1) * (speed))::custom
      broadcast [sense v] and wait
    end
  end
end
define move (speed)
move (speed) steps
if <touching [Map v]?> then // Wall sensing
  move ((-1) * (speed)) steps
end

A map has now been created that can be walked around in

Distance sensing

Archive.png
The subject of this article or section has changed dramatically and requires updating. Please keep in mind that some of the information or images may not be accurate or relevant to the current version of Scratch, the Scratch website, or the article subject. (May 2020)

This is where it starts to build up to 3D raycasting.

In the real world, the more distant an object is, the smaller it appears.

A gif showing off "distance sensing" in slow-motion action. The orange "blob" is the person sprite.

You will make a sensing script that measures the distance from the player to the walls around them. You need to do this for 96 different angles. All of this will happen when the "sense" broadcast is called.

Make a "distance sensing" sprite that has a one pixel by one pixel costume. The costume should be centered on the pixel.

Then, add this script:

when gf clicked
set [ghost v] effect to (100)

when I receive [sense v] // Used in "person"
sense
broadcast [draw v] and wait

define sense
. . . // Important: Make this custom block run without screen refresh.
delete (all v) of [distances v]
set [distance v] to [0]
set [angle offset v] to [-48]
repeat (96)
  set [distance v] to [0]
  go to [person v]
  point in direction (([direction v] of [person v]) + (angle offset))
  repeat until <<touching [map v]?> or <(distance::variables) = [80]>>
    move (1) steps
    change [distance v] by (1)
  end
  add (distance::variables) to [distances v] // We hit a wall. Let's keep track of the distance.
  change [angle offset v] by (1) // Let's test a new angle...
end

Drawing

Here is the information generated by "distance sensing" it taken and drawn. Remember, the more distant an object is, the smaller it is.

Make a "drawing" sprite with this script:

when I receive [draw v]
draw

define draw
. . . // Important: Make this custom block run without screen refresh.
go to x: (-237.5) y: (180)
set pen size to (5)
set pen color to [#7AF] // Pick the color that you want.
erase all // Prepare the screen for drawing!
pen up
set [column v] to (1)
repeat (length of [distances v])
  set pen [brightness v] to ((50) + ((item (column) of [distances v]) * ((50) / (80)))) // Fade to white
  set y to ((-1200) / (item (column) of [distances v])) // Adjust -1200 to make walls larger or smaller.
  pen down
  set y to ((1200) / (item (column) of [distances v])) // Adjust 1200 to make walls larger or smaller.
  pen up
  change x by (5)
  change [column v] by (1)
end

The tutorial is now finished. The project will be slow when it is run in Scratch, unfortunately.

Speed Optimization

There are several ways to eek out more speed:

  • Set the "person" sprite's rotation style to "no turning".
  • In the "distance sensing" sprite, check for walls every 2 steps instead of 1.
  • Instead of drawing 96 columns, draw fewer. 60 is a good amount, assuming that in "distance sensing", the sprite is rotated 2 degrees instead of 1.

The main speed bottleneck with a sprite-based raycaster is that the "distance sensing" sprite has to do lots of sensing.

List-Based Raycaster

A list-based raycaster relies on a map stored as a list and coordinates, such as that of the player and the ray, stored as variables. This method of raycasting is very virtual, all data is stored as numbers and there are no actual sprite costumes used. The only sprite required is a moving pen to draw the walls. Here is an example. If you want an example map, one is downloadable here.

A screenshot from a game that uses the list-based raycasting tutorial. Note that the frames per second counter is quite high for a scratch project.

This tutorial will teach you how to make a simple list-based raycaster. It uses custom blocks to make editing easier. Make sure to check off "run without screen refresh" box.

RunWithoutScreenRefreshBlock.png

Note Note: Only try this method after you have completely understood the Sprite-Based Raycaster or are already familiar with arrays and raycasting.

Variables Required

The following variables are required for the tutorial:

(Actual Resulution)
(Brightness)
(Camera X)
(Direction X)
(Direction X Old)
(Direction Y)
(Distance X Delta)
(Distance Y Delta)
(Draw End)
(Draw Start)
(Height)
(Line Height)
(Map X)
(Map XY)
(Map Y)
(Move Speed)
(Perpendicular Wall Distance)
(Plane X)
(Plane X Old)
(Plane Y)
(Ray X Direction)
(Ray X Position)
(Ray Y Direction)
(Ray Y Position)
(Resolution)
(Rotation Speed)
(Rotation Speed)
(Side X Distance)
(Side Y Distance)
(side)
(Step X)
(Step Y)
(Touching Wall)
(Wall Found)
(x)
(X Direction)
(X Position::variables)
(Y Direction)
(Y Position::variables)

Lists Required

The following lists are required for the tutorial:

(World Map::list)

Setting the Variables

The code used to setup the variables is:

define Set up Variables
set [X Position v] to [11]
set [Y Position v] to [7]
set [Direction X v] to [-1]
set [Direction Y v] to [0]
set [Plane X v] to [0]
set [Plane Y v] to [0.66]
set [Actual Resolution v] to [1] // If you aren't using resolution, you don't need this variable.
set [Height v] to [300]
set [Resolution v] to [12] // Resolution is not needed, but it is recommended.

Setting the Lists

The (World Map::list)list should contain n items of n numbers of either 0 or 1, where n is the distance of each side of the grid that the list represents. Squares that should be filled in should be represented by a 1, and squares that should be empty should be represented as a 0. Here is a example 10x10 world map.

when flag clicked
delete all of [world map v]
add [1111111111] to [world map v]
add [1000000001] to [world map v]
add [1000110001] to [world map v]
add [1000000001] to [world map v]
add [1001001001] to [world map v]
add [1001001001] to [world map v]
add [1000000001] to [world map v]
add [1000110001] to [world map v]
add [1000000001] to [world map v]
add [1111111111] to [world map v]

The Main Loop

This is the green flag script that starts all the other scripts.

when green flag clicked
Set Up Variables::custom
forever
pen up // DadOfMrLog did some tests and found that setting pen size to 1 and using the pen up block reduces lag!
set pen size to (1)
hide
set [Actual Resolution v] to (((Resolution) - (16)) * (-1)) // If you want to have resolution, you need this script.
Raycast::custom // This is the next script in the tutorial.
end

The Raycasting Script

Next is the main raycasting script. This block controls most of the custom blocks. Be sure to make it a run without screen refresh block.

define Raycast
erase all
set [x v] to [-240] // "x" is the increment variable here.
Read Keys::custom // This is the next script in the tutorial.
repeat until <(x) > [240]>
    pen up
    set pen size to (1)
    set pen shade to (0)
    set [Camera X v] to ((2) * ((x) / ((Actual Resolution) - (1)))
    set [Ray X Position v] to (X Position::variables)
    set [Ray Y Position v] to (Y Position::variables)
    set [Ray X Direction v] to ((Direction X) + ((Plane X) * (Camera X)))
    set [Ray Y Direction v] to ((Direction Y) + ((Plane Y) * (Camera X)))
    set [Map X v] to ([floor v] of (Ray X Position))
    set [Map Y v] to ([floor v] of (Ray Y Position))
    Calculate Walls :: custom // Explained later on.
    Draw Walls :: custom // Explained later on.
    change [x v] by (Actual Resolution)
end

Controls

One of the custom blocks in the "raycast" script was the custom block, "Read Keys." Here, we'll focus on that script.

define Read Keys
if <not <(Touching Wall) = [1]>> then
    if  <key [up arrow v] pressed?> then
        change [X Position v] by ((X Direction) * (Move Speed))
        change [Y Position v] by ((Y Direction) * (Move Speed))
    end
end
if <not <(Touching Wall) = [-1]>> then
    if <key [down arrow v] pressed?> then
        change [X Position v] by ((X Direction) * (Move Speed))
        change [Y Position v] by ((Y Direction) * (Move Speed))
    end
end
if <key [right arrow v] pressed?> then
    set [Direction X Old v] to (Direction X)
    set [Direction X v] to (((Direction X) * ([cos v] of ((Rotation Speed) * (-1)))) - ((Direction Y) * ([sin v] of ((Rotation Speed) * (-1)))))
    set [Direction Y v] to (((Direction X Old) * ([sin v] of ((Rotation Speed) * (-1)))) + ((Direction Y) * ([cos v] of ((Rotation Speed) * (-1)))))
    set [Plane X Old v] to (Plane X)
    set [Plane X v] to (((Plane X) * ([cos v] of ((Rotation Speed) * (-1)))) - ((Plane Y) * ([sin v] of ((Rotation Speed) * (-1)))))
    set [Plane Y v] to (((Plane X Old) * ([sin v] of ((Rotation Speed) * (-1)))) + ((Plane Y) * ([cos v] of ((Rotation Speed) * (-1)))))
end
if <key [left arrow v] pressed?> then
    set [Direction X Old v] to (Direction X)
    set [Direction X v] to (((Direction X Old) * ([cos v] of ((Rotation Speed) * (1)))) - ((Direction Y) * ([sin v] of ((Rotation Speed) * (1)))))
    set [Direction Y v] to (((Direction X Old) * ([sin v] of ((Rotation Speed) * (1)))) + ((Direction Y) * ([cos v] of ((Rotation Speed) * (1)))))
    set [Plane X Old v] to (Plane X)
    set [Plane X v] to (((Plane X) * ([cos v] of ((Rotation Speed) * (1)))) - ((Plane Y) * ([sin v] of ((Rotation Speed) * (1)))))
    set [Plane Y v] to (((Plane X Old) * ([sin v] of ((Rotation Speed) * (1)))) + ((Plane Y) * ([cos v] of ((Rotation Speed) * (1)))))
end

Calculating Walls

This section explains the block that calculates the walls. Unfortunately, wall touch detection is not included.

define Calculate Walls
set [Touching Wall v] to [0]
set [Distance X Delta v] to ([sqrt v] of ((1) + (((Ray Y Direction) * (Ray Y Direction)) / ((Ray X Direction) * (Ray X Direction)))))
set [Distance Y Delta v] to ([sqrt v] of ((1) + (((Ray X Direction) * (Ray X Direction)) / ((Ray Y Direction) * (Ray Y Direction)))))
set [Wall Found v] to [0] // Once the ray hits a wall, this variable is set to one, which is the same as the boolean value "true" in this case.
if <(Ray X Direction) < [0]> then
    set [Step X v] to [-1]
    set [Side X Distance v] to (((Ray X Position) - (Map X)) * (Distance X Delta))
else
    set [Step X v] to [1]
    set [Side X Distance v] to ((((Map X) + (1)) - (Ray X Position)) * (Distance X Delta))
end
if <(Ray Y Direction) < [0]> then
    set [Step Y v] to [-1]
    set [Side Y Distance v] to (((Ray Y Position) - (Map Y)) * (Distance Y Delta))
else
    set [Step Y v] to [1]
    set [Side Y Distance v] to ((((Map Y) + (1)) - (Ray Y Position)) * (Distance Y Delta))
end
repeat until <(Wall Found) = [1]>
    if <(Side X Distance) < (Side Y Distance)> then
        change [Side X Distance v] by (Distance X Delta)
        change [Map X v] by (Step X)
        set [side v] to [0] // The variable "side" is referring to which wall is being faced, a Y wall or an X wall. In this case, it is an X wall.
    else
        change [Side Y Distance v] by (Distance Y Delta)
        change [Map Y v] by (Step Y)
        set [side v] to [1] // In this case, the ray has hit a Y wall.
    end
    set [Map XY v] to (letter (Map Y) of (item (Map X) of [World Map v])) // Map XY is the item of the grid that the ray is in. Example: if X is eleven and Y is seven, then Map XY would be (letter 7 of (item 11 of World Map).
    if <(Map XY) > [0]> then // If Map XY is more than zero, it has hit a wall.
        set [Wall Found v] to [1]
    end
end
if <(side) = [0]> then
    set [Perpendicular Wall Distance v] to ([abs v] of ((((Map X) - (Ray X Position)) + (((1) - (Step X)) / (2))) / (Ray X Direction)))
else
    set [Perpendicular Wall Distance v] to ([abs v] of ((((Map Y) - (Ray Y Position)) + (((1) - (Step Y)) / (2))) / (Ray Y Direction)))
end
set [Line Height v] to ([abs v] of ([floor v] of ((360)/(Perpendicular Wall Distance))
set [Draw Start v] to (((0) - (Line Height)) / (2))
set [Draw End v] to ((Line Height) / (2))

Drawing the Walls

The last part in this tutorial is the pen script that draws the walls. It is a very simple script.

define Draw Walls
set pen color to [#179fd7]
if <(side) = [1]> then
    set [Brightness v] to (115) // This script makes the Y walls darker than the X walls for a nice effect.
else
    set [Brightness v] to (150)
end
go to x: (x) y: (Draw Start)
set pen shade to ((Brightness) * (-0.2))
set pen size to (Actual Resolution)
pen down
go to x: (x) y: ((2) * (Draw End))
pen up
set pen size to (1)
set pen shade to (0)

Your list-based raycaster is now ready!

External Links