From Test-Scratch-Wiki

A raycaster is a 专案 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

There are two types of raycaster that can be programmed: the sprite-based method, and the 清单-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 角色; each map/world is a 造型 As an Array (grid of numbers); each map is a separate array
Sprites Needed 1, but 3 is usually used (map, renderer, and ray) 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 uncomplicated, but slow. More advanced programmers and those who have finished making a sprite-based raycaster, can make an array based raycaster, which is faster but more complicated.

Sprite-Based Raycaster

In this raycaster, you will need three sprites:

  • A pixel, the "sensor"
    • This moves to the player's position, points in the direction the player is facing, then moves forwards until it touches the map
    • It then records the distance moved in a 清单.
    • It repeats this for many angles to the left and right of the direction.
  • A 480x360 rectangle with certain inner paths cleared out, the "map"
    • The rectangle left after clearing some paths is used to obstruct the sensor
  • Another pixel, the "renderer"
    • The renderer draws each wall based on the distance given by the ray

The tutorial below will teach you how to make a working sprite-based Raycaster. Here is a finished example

Creating the map

Make a map 角色 with walls, make sure it has a closed outside area. Then, add the following 程式 to it:

當 @greenflag 被點擊
重複無限次 
  筆跡清除
  定位到 x: (0) y: (0)
end

當 @greenflag 被點擊
定位到 x: (0) y: (0)
效果 [幻影 v] 設為 (100)

Movement

Make a movement sprite that has a one pixel by one pixel 造型. Then put it inside the walls of your map sprite. Then add this script to it:

當 @greenflag 被點擊
定位到 x: (-20) y: (0) // assuming that that is where you put your sprite
變數 [drawing v] 設為 [0]
變數 [touching wall v] 設為 [0]
效果 [幻影 v] 設為 (100)
重複無限次 
  如果 <<(drawing) = [1]> 不成立> 那麼 
    如果 <[向右 v] 鍵被按下?> 那麼 
      右轉 @turnright (2) 度 // this can be adjusted to rotate faster, good for fighting lag
      廣播訊息 [sense v]
    end
    如果 <[向左 v] 鍵被按下?> 那麼 
      右轉 @turnright (-2) 度 // this can be adjusted to rotate faster, good for fighting lag
      廣播訊息 [sense v]
    end
    如果 <<(touching wall) = [1]> 不成立> 那麼 
      如果 <[向上 v] 鍵被按下?> 那麼 
        移動 (speed) 點 // where speed is the desired amount of steps you want it to move
        廣播訊息 [sense v]
      end
    end
    如果 <<(touching wall) = [-1]> 不成立> 那麼 
      如果 <[向下 v] 鍵被按下?> 那麼 
        移動 <(speed) * (-1)> 點 // where speed is the desired amount of steps you want it to move
        廣播訊息 [sense v]
      end
    end
  end
end

Also, clicking the information button on this sprite and clicking "don't rotate" speeds it up a little and reduces lag.

Wall sensing

You now have your map and something that moves around in that map, but the only thing that will stop it moving is 变量 values. Add this wall sensing script to the wall sensing sprite:

當 @greenflag 被點擊
效果 [幻影 v] 設為 (100)
重複無限次 
  如果 <<(drawing) = [1]> 不成立> 那麼 
    定位到 [movement v] 位置 // where movement is the movement sprite
    面朝 <[direction v] 或 [movement v]> 度
    移動 (speed) 點
    如果 <碰到 [walls v] ?> 那麼 
      變數 [touching wall v] 設為 [1]
    
      變數 [touching wall v] 設為 [0]
      移動 <(speed) * (-2)> 點
      如果 <碰到 [walls v] ?> 那麼 
        變數 [touching walls v] 設為 [-1]
      
        變數 [touching walls v] 設為 [0]
      end
    end // the map sprite
  end
end

Now you can move around your map without going through walls.

Distance sensing

This is the sensing script if you only have one sensor. If you have multiple sprites for sensing, you should divide up the numbers and for each sprite make a new list.

當 @greenflag 被點擊
效果 [幻影 v] 設為 (0)
刪除第 (全部 v) 項 \( [distances v] \)
定位到 [movement v] 位置


當收到訊息 [sense v] // the broadcast from before
變數 [drawing v] 設為 [1]
刪除第 (全部 v) 項 \( [distances v] \)
變數 [distance v] 設為 [0]
變數 [angle offset v] 設為 [48]
重複 (96) 次 
  變數 [distance v] 設為 [60]
  定位到 [movement v] 位置
  面朝 <<[direction v] of [movement v]> + (angle offset)> 度
  重複直到 <<碰到 [walls v] ?> 或 <(distance) = [0]>> 
    移動 (1) 點
    變數 [distance v] 改變 (-1)
  end
  新增項目 (distance) \( [distances v] \)
  變數 [angle offset v] 改變 (-1)
end
等待直到 <(length of all lists) = [96]> // if you have other sensors
廣播訊息 [draw v]

Drawing

Divide up all the lists between all of the drawing sprites you have. Here is a script if you have only one drawer and one distance sensor

當收到訊息 [draw v]
定位到 x: (-237.5) y: (180)
筆跡寬度設為 (5) // because 96*5=480 which is the width of the screen
筆跡顏色設為 (Color you want) // Pick the color you want 
筆跡清除
停筆
變數 [help counting v] 設為 (清單 [distances v] 的項目數 :: list)
重複 (清單 [distances v] 的項目數 :: list) 次 
  筆跡亮度設為 ((item (help counting) of [distances v] :: list) * ((1) + ((2) / (3))))
  y 設為 ((item (help counting) of [distances v] :: list) * (4))
  下筆
  y 設為 (((item (help counting) of [distances v] :: list) * (4)) * (-1))
  停筆
  變數 [help counting v] 改變 (-1)
  筆跡亮度設為 (0)
  x 改變 (5)
end
變數 [drawing v] 設為 [0]

This project will be slow when you run it in Scratch, even in turbo speed. Upload and run in the Flash 播放器 in 加速模式, to run in at a decent speed. However, you can run this project at a decent speed without turbo mode by running this project without screen refresh by using the 更多积木. Here is an example.

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.

RunWithoutScreenRefreshPicForRaycaster.png


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

Setting the Variables

Here is the code used to setup the variables:

定義 Set up Variables
變數 [x 座标 v] 設為 [11]
變數 [y 座标 v] 設為 [7]
變數 [Direction X v] 設為 [-1]
變數 [Direction Y v] 設為 [0]
變數 [Plane X v] 設為 [0]
變數 [Plane Y v] 設為 [0.66]
變數 [Actual Resolution v] 設為 [1] //  If you aren't using resolution, you don't need this variable.
變數 [Height v] 設為 [300]
變數 [Resolution v] 設為 [12] //  Resolution is not needed, but it is recommended.

The Main Loop

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

當 @greenflag 被點擊
Set Up Variables //  You have already created this block!
重複無限次 
  停筆 //  DadOfMrLog did some tests and found that setting pen size to 1 and using the pen up block reduces lag!
  筆跡寬度設為 (1)
  隱藏
  變數 [Actual Resolution v] 設為 (((Resolution) - (16)) * (-1)) // If you want to have resolution, you need this script.
  Raycast //  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.

定義 Raycast
筆跡清除
變數 [x v] 設為 [-240] //  "x" is the increment variable here.
Read Keys //  This is the next script in the tutorial.
重複直到 <(x) > [280]> 
  停筆
  筆跡寬度設為 (1)
  筆跡亮度設為 (0)
  變數 [Camera X v] 設為 ((2) * ((x) / ((Actual Resolution) - (1))))
  變數 [Ray x 座标 v] 設為 (x 座標)
  變數 [Ray y 座标 v] 設為 (y 座標)
  變數 [Ray X Direction v] 設為 ((X Direction) + ((Plane X) * (Camera X)))
  變數 [Ray Y Direction v] 設為 ((Direction Y) + ((Plane Y) * (Camera X)))
  變數 [Map X v] 設為 ([floor v] 數值 (Ray x 座标))
  變數 [Map Y v] 設為 ([floor v] 數值 (Ray y 座标))
  Calculate Walls :: custom //  Explained later on.
  Draw Walls :: custom //  Explained later on.
  變數 [x v] 改變 (Actual Resolution)
end

Controls

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

定義 Read Keys
如果 <<(Touching Wall) = [1]> 不成立> 那麼 
  如果 <[向上 v] 鍵被按下?> 那麼 
    變數 [x 座标 v] 改變 ((X Direction) * (Move Speed))
    變數 [y 座标 v] 改變 ((Y Direction) * (Move Speed))
  end
end
如果 <<(Touching Wall) = [-1]> 不成立> 那麼 
  如果 <[向下 v] 鍵被按下?> 那麼 
    變數 [x 座标 v] 改變 ((X Direction) * (Move Speed))
    變數 [y 座标 v] 改變 ((Y Direction) * (Move Speed))
  end
end
如果 <[向右 v] 鍵被按下?> 那麼 
  變數 [Direction X Old v] 設為 (Direction X)
  變數 [Direction X v] 設為 (((Direction X) * ([cos v] 數值 ((Rotation Speed) * (-1)))) - ((Direction Y) * ([sin v] 數值 ((Rotation Speed) * (-1)))))
  變數 [Direction Y v] 設為 (((Direction X Old) * ([sin v] 數值 ((Rotation Speed) * (-1)))) + ((Direction Y) * ([cos v] 數值 ((Rotation Speed) * (-1)))))
  變數 [Plane X Old v] 設為 (Plane X)
  變數 [Plane X v] 設為 (((Plane X) * ([cos v] 數值 ((Rotation Speed) * (-1)))) - ((Plane Y) * ([sin v] 數值 ((Rotation Speed) * (-1)))))
  變數 [Plane Y v] 設為 (((Plane X Old) * ([sin v] 數值 ((Rotation Speed) * (-1)))) + ((Plane Y) * ([cos v] 數值 ((Rotation Speed) * (-1)))))
end
如果 <[向左 v] 鍵被按下?> 那麼 
  變數 [Direction X Old v] 設為 (Direction X)
  變數 [Direction X v] 設為 (((Direction X Old) * ([cos v] 數值 ((Rotation Speed) * (1)))) - ((Direction Y) * ([sin v] 數值 ((Rotation Speed) * (1)))))
  變數 [Direction Y v] 設為 (((Direction X Old) * ([sin v] 數值 ((Rotation Speed) * (1)))) + ((Direction Y) * ([cos v] 數值 ((Rotation Speed) * (1)))))
  變數 [Plane X Old v] 設為 (Plane X)
  變數 [Plane X v] 設為 (((Plane X) * ([cos v] 數值 ((Rotation Speed) * (1)))) - ((Plane Y) * ([sin v] 數值 ((Rotation Speed) * (1)))))
  變數 [Plane Y v] 設為 (((Plane X Old) * ([sin v] 數值 ((Rotation Speed) * (1)))) + ((Plane Y) * ([cos v] 數值 ((Rotation Speed) * (1)))))
end

Calculating Walls

Document.png Please expand this article or section. You can help by adding more information if you are an editor. More information might be found in a section of the talk page.


This section explains the block that calculates the walls. Unfortunately, wall touch detection is not included. If you know how to do this, please contact Furious-.

定義 Calculate Walls
變數 [Touching Wall v] 設為 [0]
變數 [Distance X Delta v] 設為 ([sqrt v] 數值 ((1) + (((Ray Y Direction) * (Ray Y Direction)) / ((Ray X Direction) * (Ray X Direction)))))
變數 [Distance Y Delta v] 設為 ([sqrt v] 數值 ((1) + (((Ray X Direction) * (Ray X Direction)) / ((Ray Y Direction) * (Ray Y Direction)))))
變數 [Wall Found v] 設為 [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.
如果 <(Ray X Direction) < [0]> 那麼 
  變數 [Step X v] 設為 [-1]
  變數 [Side X Distance v] 設為 (((Ray x 座标) - (Map X)) * (Distance X Delta))

  變數 [Step X v] 設為 [1]
  變數 [Side X Distance v] 設為 ((((Map X) + (1)) - (Ray x 座标)) * (Distance X Delta))
end
如果 <(Ray X Direction) < [0]> 那麼 
  變數 [Step Y v] 設為 [-1]
  變數 [Side Y Distance v] 設為 (((Ray y 座标) - (Map Y)) * (Distance Y Delta))

  變數 [Step Y v] 設為 [1]
  變數 [Side Y Distance v] 設為 ((((Map Y) + (1)) - (Ray y 座标)) * (Distance Y Delta))
end
重複直到 <(Wall Found) = [1]> 
  如果 <(Side X Distance) < (Side Y Distance)> 那麼 
    變數 [Side X Distance v] 改變 (Distance X Delta)
    變數 [Map X v] 改變 (Step X)
    變數 [side v] 設為 [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.
  
    變數 [Side Y Distance v] 改變 (Distance Y Delta)
    變數 [Map Y v] 改變 (Step Y)
    變數 [side v] 設為 [1] //  In this case, the ray has hit a Y wall.
  end
  變數 [Map XY v] 設為 (字串中第 (Map Y) 字\( (清單第 (Map X) 項項目\( [World Map v] \) :: list) \)) //  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).
  如果 <(Map XY) > [0]> 那麼 
    變數 [Wall Found v] 設為 [1]
  end //  If Map XY is more than zero, it has hit a wall.
end
如果 <(side) = [0]> 那麼 
  變數 [Perpendicular Wall Distance v] 設為 ([abs v] 數值 ((((Map X) - (Ray x 座标)) + (((1) - (Step X)) / (2))) / (Ray X Direction)))

  變數 [Perpendicular Wall Distance v] 設為 ([abs v] 數值 ((((Map Y) - (Ray y 座标)) + (((1) - (Step Y)) / (2))) / (Ray Y Direction)))
end
變數 [Line Height v] 設為 ([abs v] 數值 ((h) / (Perpendicular Wall Distance)))
變數 [Draw Start v] 設為 (((0) - (Line Height)) / (2))
變數 [Draw End v] 設為 ((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.

定義 Draw Walls
筆跡顏色設為 [#179fd7]
如果 <(side) = [1]> 那麼 
  變數 [Brightness v] 設為 (115) //  This script makes the Y walls darker than the X walls for a nice effect.

  變數 [Brightness v] 設為 (150)
end
定位到 x: (x 座標) y: (Draw Start)
筆跡亮度設為 ((Brightness) * (-0.2))
筆跡寬度設為 (Actual Resolution)
下筆
定位到 x: (x 座標) y: ((2) * (Draw End))
停筆
筆跡寬度設為 (1)
筆跡亮度設為 (0)

And that wraps up the list-based raycasting tutorial! Enjoy!

External Links

Cookies help us deliver our services. By using our services, you agree to our use of cookies.