We're on Discord! Please join our server now if you don't want to miss anything! (More info) | Join the UCC4 contest! (More info)

* WalrusIRC & Discord main room

If you have a forum account, have more than 4 posts and are not part of a restricted usergroup, then you can chat in our main Discord server room directly from here and continue using the forums at the same time. Or you can join our server directly and access many more discussion rooms!

Author Topic: How to make a simple raycaster in TI-BASIC.  (Read 771 times)

0 Members and 1 Guest are viewing this topic.

Offline _iPhoenix_

  • Custom title
  • Super User
  • Join Date: Mar 2017
  • Location: Location
  • Posts: 669
  • Post Rating Ratio: +16/-1
  • ███████ ▼ ♪ Best music ♫
    • @dj_iPhoenix
    • @UCytgMNPxAMDsxjimvxHf01w
    • Legend-of-iPhoenix
    • @the-legend-of-iphoenix
    • My website.
  • Gender: Male
How to make a simple raycaster in TI-BASIC.
« on: March 17, 2018, 06:54:14 pm »
Some of you may remember my old TI-BASIC raycaster. It was made back when I wasn't that good at the language, and I think I broke it with some long-forgotten edit. I was reminded of this by a recent post on Cemetech by someone who was trying to understand my old code to try and make it work.

They weren't very successful. It was a crappy, unreadable script that completely ignored the points that make a good TI-BASIC program. In short, it was a prototype.

A few months ago, I happened to come back to this concept and completely rewrote my code for it. Here's my complete new code (import this with SC3):
Code: [Select]
StoreGDB 0
ZStandard
ZSquare
GridOff
AxesOff
LabelOff
BorderColor 4
BackgroundOff
{Black,DarkGray,Gray,MedGray,LtGray->L1
For(A,1,10
   For(B,1,10
      If [A](A,B
      Then
         For(C,1,5
            For(D,1,5
               Pxl-On(115+5A-C,5B-D,Black
            End
         End
      End
   End
End
122->A
5->B
DelVar DRepeat K=45
   For(theta,~3,3,.1
      Line(12+theta,3,12+theta,10,0
   End
   Circle(12,6,1.8,Black
   Line(10,6,14,6,1,Black,1
   Line(12,4,12,8,1,Black,1
   For(theta,~40+D,40+D
      If not(remainder(abs(theta-D),10
      Line(12+2.5cos(theta-2D),6+2.5sin(theta-2D),12+2cos(theta-2D),6+2sin(theta-2D),1,Red+(theta=D),1
      sin(theta->U
      cos(theta->V
      DelVar NRepeat Ans or N=10
         N+1->N
         int(A+NU->I
         int(B+NV->J
         pxl-Test(I,Ans
      End
      (theta-D)/5
      Line(Ans,5,Ans,~5,0
      If N!=10
      Line(Ans,(10-N)/2,Ans,~(10-N)/2,1,L1(1+min(int(N/2),dim(L1
   End
   If not(pxl-Test(A,B
   Pxl-On(A,B,Red
   getKey
   Repeat Ans
      getKey
   End
   Ans->K
   Pxl-Off(A,B
   D-5(K=11->D
   D+5(K=15->D
   D-2(K=12->D
   D+2(K=14->D
   int(A+(K=13)2sin(D)+(K=23)5sin(D->A
   int(B+(K=13)2cos(D)+(K=23)5cos(D->B
End
RecallGDB 0

In concept, this is essentially the same program, but I like it much better because it is more concise and readable. It also works. That's somewhat important.

To turn, you use the top 5 buttons on the keypad, namely [y=], [window], [zoom], [trace], and [graph]. To move forward, press [zoom]. To move forward faster, press [del].

This code is not the most optimized I could make it, for the sake of making a understandable tutorial. This tutorial/explanation assumes you understand at least a little trig and TI-BASIC.

Let's break this program down into manageable chunks, which I will explain one-by-one.

Code: [Select]
StoreGDB 0
ZStandard
ZSquare
GridOff
AxesOff
LabelOff
BorderColor 4
BackgroundOff
{Black,DarkGray,Gray,MedGray,LtGray->L1

This section is pretty much my standard "initialize the graphscreen" set of commands. I store the user's settings to GDB0 to retrieve them when the program is over.

The line storing colors of increasing brightness to L1 is important. It will be explained thoroughly later in this post.

Code: [Select]
For(A,1,10
   For(B,1,10
      If [A](A,B
      Then
         For(C,1,5
            For(D,1,5
               Pxl-On(115+5A-C,5B-D,Black
            End
         End
      End
   End
End

This code loops through matrix A, drawing a 5x5 "pixel" on the minimap in the bottom left corner of the screen if the value is truthy (i.e. not 0). We use this minimap to do the raycasting.

Code: [Select]
122->A
5->B
DelVar D

The variables A and B store the row and column (because we are using pixels, they are not the X and Y coordinates) of the player. The D variable stores our rotation. Because DelVar effectively sets it to 0 and 0° is directly left, the player starts out facing left.

Code: [Select]
Repeat K=45
   For(theta,~3,3,.1
      Line(12+theta,3,12+theta,10,0
   End

Here, we start the main loop. We clear the area where the compass will be redrawn each frame.

Code: [Select]
   Circle(12,6,1.8,Black
   Line(10,6,14,6,1,Black,1
   Line(12,4,12,8,1,Black,1

This draws a nice little compass in the top right corner of the screen, so that the player knows which direction he or she is facing.

Code: [Select]
   For(theta,~40+D,40+D
      If not(remainder(abs(theta-D),10
      Line(12+2.5cos(theta-2D),6+2.5sin(theta-2D),12+2cos(theta-2D),6+2sin(theta-2D),1,Red+(theta=D),1
      sin(theta->U
      cos(theta->V

Here is the start of the meat of the program.

We sweep through an 80° arc, centered around the player's rotation value.

The [mono]If not(remainder(abs(theta-D),10[/mono] part and the line following it draw markers on the compass to show the progress. The If statement makes sure that we only draw lines at 10-degree intervals.

As an extra layer of optimization, we store the sine and cosine values of our angle to U and V, respectively, so that we don't have to recalculate them in the next step.

Code: [Select]
      DelVar NRepeat Ans or N=10
         N+1->N
         int(A+NU->I
         int(B+NV->J
         pxl-Test(I,Ans
      End

This is where the actual raycasting happens.

I reset the variable N to 0. It stores the length of the ray.
We basically keep increasing the radius of an imaginary circle (centered on our player) by 1, calculating the point on this circle at our angle, θ, and checking if there is a pixel drawn there. To save time, if the circle has a radius equal to 10 and we still haven't hit anything, we stop calculating the ray. The value of 10 is essentially our "maximum render distance".

A raycaster basically casts a ray from the player's position at each angle in the player's FOV, measuring the distance the ray travels until it hits a wall.

Code: [Select]
      (theta-D)/5
      Line(Ans,5,Ans,~5,0
      If N!=10
      Line(Ans,(10-N)/2,Ans,~(10-N)/2,1,L1(1+min(int(N/2),dim(L1
   End

This is most of our drawing code, and also the ending of our drawing loop, where we were iterating through each of the angles.

We clear out anything on our "canvas" where the line is going to go. If we didn't prevent the ray from continuing to propagate, we draw a line on our graph. The X coordinate is reliant on the angle, while the vertical length of the line and the color of the line is solely influenced by the length of the ray. If the ray took longer to hit a wall, the line should be shorter and lighter in color, because it is further away.

Code: [Select]
   If not(pxl-Test(A,B
   Pxl-On(A,B,Red

You might have noticed that we never actually drew the player in on the minimap yet. This is because some rays would hit the player and register as there being a wall right where the player is, even if there isn't one. If we didn't stop the ray from continuing to propagate, we can safely draw this in now. I first do a check to make sure that we are not inside of a wall. I should probably add player collision checking, but it's a trivial thing in this scenario.

Code: [Select]
   getKey
   Repeat Ans
      getKey
   End
   Ans->K

This section of code heads our user input.

The first getKey prevents any keys that were pressed during the drawing from being registered in our movement-handling code. I don't fully understand how or why this happens, but it does.

We then wait for the user to press a key, and store the key code they pressed to K.

Code: [Select]
   Pxl-Off(A,B
   D-5(K=11->D
   D+5(K=15->D
   D-2(K=12->D
   D+2(K=14->D
   int(A+(K=13)2sin(D)+(K=23)5sin(D->A
   int(B+(K=13)2cos(D)+(K=23)5cos(D->B

Next, we turn off the pixel on the minimap representing the player. I have explained why we need to do this earlier in the post. The next section, which I intentionally left unoptimized, adjusts the rotation of the player. If the user presses the [y=] button, we assume they want to rotate more than if they pressed the [window] button. After this section of code, we move the player.

If the player pressed the [zoom] button, we move them forward by (approx.) 2 pixels. Because the rendering process is so slow, though, we also give them the option to move forward by 5 pixels by pressing [del].

Code: [Select]
End
RecallGDB 0

This last little bit of code closes up our main loop. When the user presses [clear], the loop stops (because of the "K=45" at the beginning), and we recall the user's graph settings back.

If you enjoyed reading this, it helped you, or you need help understanding something, please do not hesitate to reply below. If you do not have an account yet, they are free and only require a few minutes of your time to create.


  • Calculators owned: Two TI-84+ CE's
Please spam here: https://legend-of-iphoenix.github.io/spam/

"walruses are better than tuxedo chickens, all hail the great :walrii:" ~ me
Evolution of my avatar:

 


You can also use the following HTML or bulletin board code to share it on your page or forum signature!


Also do not forget to check our affiliates below.
Planet Casio TI-Planet Calc.news BroniesQC BosaikNet Velocity Games