- Joined
- Feb 13, 2024
- Messages
- 3
- Reaction score
- 0
Hey there! I'm currently trying to learn more about game development outside of typical engines like Unity which is what I'm most used to. I decided to follow 3DSage's raycaster engine in C and OpenGL. I'm finally beginning to really understand what's going on behind the scenes, but I'm getting this really weird behavior for some reason. In this specific area of the map, rays just seem to skip over a certain segment, which is really weird. As you can see from the following two images, this specific segment seems to be the issue.
Everywhere else it seems to work just fine, like here:
Here is my code. I know it's a lot, but I have poured over this and 3DSage's source code for days and after hours of testing, I just can't figure out what the issue is. Would really appreciate some help!
Everywhere else it seems to work just fine, like here:
Here is my code. I know it's a lot, but I have poured over this and 3DSage's source code for days and after hours of testing, I just can't figure out what the issue is. Would really appreciate some help!
C:
/*--HEADERS--*/
#include <stdio.h>
#include <stdlib.h>
#include <GL/glut.h>
#include <math.h>
/*--DEFINITIONS--*/
#define PI 3.141592653589793238
#define DR 0.0174533 //this is one degree in radians
/*--MAP--*/
int map_size_x = 8, map_size_y = 8, tile_size = 64; //defines the map as being 8 tiles horizontally and vertically, with each tile being 64 units
int map[] = //0's are empty, 1's are walls
{
1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1,
};
void DrawMap2D()
{
int x, y; //for loop iterators for each tile, x and y, on t he mpa
int x_offset, y_offset;
for(y = 0; y < map_size_y; y++) //for loop continues for each tile on the y axis
{
for(x = 0; x < map_size_x; x++) //for loops goes through each x axis tile in each y column
{
if(map[y * map_size_x + x] == 1) //a genius way of iterating through a 1 dimensional array as a 2 dimensional matrix. Hard to explain.
{
glColor3f(1, 1, 1); //if there's a wall, set the GL color to white
}
else
{
glColor3f(0, 0, 0); //if there's not a wall, set the GL color to black
}
/*
To draw a 64 unit block, multiply the current x and y coords for that tile by the tile size, 64.
This means that tile one will be drawn from units 0, 0; to units 64, 64;
Tile two will be drawn from units 64, 64 to units 128, 128; etc. forming the grid.
*/
x_offset = x * tile_size; y_offset = y * tile_size; //multiplies x and y by tile size to get the offset required
glBegin(GL_QUADS); //begins GL in quadrilateral mode
glVertex2i(x_offset+1, y_offset+1 ); //draws point one at the offset (so for tile zero it will be (0, 0))
glVertex2i(x_offset+1, y_offset + tile_size-1); //draws point two at the offset plus tile size for y (so for tile zero it will be (0, 64))
glVertex2i(x_offset-1 + tile_size, y_offset + tile_size-1); //draws point three at the offset plus tile size for both x and y (so for tile zero it will be (64, 64))
glVertex2i(x_offset-1 + tile_size, y_offset+1 ); //draws point four at the offset plus tile size for x (so for tile zero it will be (64, 0))
glEnd();
/*
This creates a quadrilateral with points (0, 0)----------(0, 64) for tile zero
| |
| |
| |
(64, 0)----------(64, 64)
This creates a quadrilateral with points (64, 64)----------(64, 128) for tile one
| |
| |
| |
(128, 64)----------(128, 128)
and so on;
These quads will be drawn regardless of if they are walls or not. If they are not walls, they will be
drawn as black instead of white, but they are still drawn;
The plus and minus one throughout is just to draw a border around the blocks for visibility
*/
}
}
}
/*--PLAYER--*/
float player_x, player_y; //player position
float player_delta_x, player_delta_y; //stores the amount of movement in the horizontal/vertical direction the player should move
float player_angle;
void DrawPlayer2D()
{
glColor3f(1, 1, 0); //sets the current gl color to yellow
glPointSize(8); //sets the point size to 8
glBegin(GL_POINTS); //starts GL in point drawing mode
glVertex2i(player_x, player_y); //uses the vertex integer draw function to draw a point at player's position
glEnd(); //ends the current GL process
glLineWidth(3);
glBegin(GL_LINES);
//draw a line from the players position to the players delta position, i.e. where they are facing
glVertex2i(player_x, player_y);
glVertex2i(player_x + player_delta_x * 5, player_y + player_delta_y * 5);
glEnd();
}
/*---RAYS---*/
float Dist(float ax, float ay, float bx, float by, float ang)
{
float horizontal_dist = bx - ax;
float vertical_dist = by - ay;
float horizontal_dist_sqr = horizontal_dist * horizontal_dist;
float vertical_dist_sqr = vertical_dist * vertical_dist;
float final_dist = sqrt(horizontal_dist_sqr + vertical_dist_sqr);
return final_dist;
}
void DrawRays2D()
{
int r, mx, my, mp, depth_of_field; //DoF == number of tiles to cast over, if more than DoF then just end the ray
float ray_x, ray_y, ray_angle, x_offset, y_offset;
float final_distance; //holds either horizontal or vertical distance, whicherver is shorter
float horizontal_distance = 100000;
float horizontal_x = player_x;
float horizontal_y = player_y;
ray_angle = player_angle - (DR * 30);
if(ray_angle < 0)
{
ray_angle += 2*PI;
}
if(ray_angle > 2*PI)
{
ray_angle -= 2*PI;
}
for(r = 0; r < 60; r++)
{
// Check Horizontal Lines
depth_of_field = 0;
float aTan = -1 / tan(ray_angle);
if(ray_angle > PI) //if the ray's angle is greater than Pi aka 180 degs, the player is looking up;
{
ray_y = ((((int)player_y) / tile_size) * tile_size) - 0.0001; //rounds the ray's y value to the nearest tile. Because each tile is 64 units, dividing and then multiplying by 64 rounds to the nearest multiple of 64, ensuring that the max y distance per depth_of_field iteration is a multiple of 64
//the -0.0001 is for accuracy. Floating point numbers are inherently a bit inaccurate, meaning sometimes the rays will overshoot their collision and keep firing forward to the next wall.
//Subtracting 0.0001 ensures that all rays undershoot by just a small amount, not enough to be perceptible but enough to prevent overshooting.
//Overshooting causes the ray to go to the next wall, which is 64 blocks away. Undershooting by 0.0001 is better than overshooting by 64.
/*---
-----
Tangent (Tan) is opposite over adjacent. We know the opposite line (the vertical one) is player_y - ray_y.
The Inverse Tangent (aTan) is adjacent over opposite. By multiplying opposite by aTan, we are multiplying
opp * adj/opp, the opps cancel out and give us adj. We then add the player's x position to adj to figure out
where on the map the ray's x vector is.
-----
---*/
ray_x = (player_y - ray_y) * aTan + player_x;
/*---
-----
the offsets calculate the next hit position. y is the same, just up by 64 (GL renders with 0, 0 being top left,
and moving down increases y position). x is the negative of the y_offset (because x is rendered normally,
left to right. If we shoot a ray and don't hit, we increment the ray's y value by subtracting 64, then use
the aTan trick to find the proper x offset.
-----
---*/
y_offset = -64;
x_offset = -y_offset * aTan;
}
if(ray_angle < PI) //if the ray's angle is less than Pi aka 180 degs, the player is looking down;
{
ray_y = ((((int)player_y) / tile_size) * tile_size) + tile_size;
//rounds the ray's y value to the nearest tile. Because each tile is 64 units, dividing and then multiplying
//by 64 rounds to the nearest multiple of 64, ensuring that the max y distance per depth_of_field iteration is a multiple
//of 64. By adding the tile_size (64), we ensure that the ray stops at the top of the tile rather than the bottom
ray_x = (player_y - ray_y) * aTan + player_x;
y_offset = 64;
x_offset = -y_offset * aTan;
}
if(ray_angle == 0 || ray_angle == PI) //player is looking straight left or right, the ray will never intersect
{
ray_x = player_x;
ray_y = player_y;
depth_of_field = 8; //Setting depth_of_field to 8 means that we don't need to continue casting rays
}
while(depth_of_field < 8) //if depth_of_field is less than 8, keep casting until it hits something or iterates 8 times
{
mx = (int)(ray_x) / tile_size; //the ray's x value divided by 64, effectively giving the tile position's x value
my = (int)(ray_y)>>6; //the ray's y value divided by 64, effectively giving the tile position's y value
mp = my * map_size_x + mx; //The ray's final point on the map
if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1) //if the ray's final point on the map is less than the size of the map (AKA not out of bounds) and is a 1, then the ray hit a wall. Set depth_of_field to 8 so that the while loop stops. The search is over.
{
horizontal_x = ray_x;
horizontal_y = ray_y;
horizontal_distance = Dist(player_x, player_y, horizontal_x, horizontal_y, ray_angle);
depth_of_field = 8;
}
else
{
ray_x += x_offset;
ray_y += y_offset;
depth_of_field += 1;
}
}
// Check Vertical Lines
depth_of_field = 0;
float nTan = -tan(ray_angle);
float vertical_distance = 100000;
float vertical_x = player_x;
float vertical_y = player_y;
if(ray_angle > (PI/2) && ray_angle < ((3*PI)/2)) //if the ray's angle is greater than PI/2 aka 90 degs, and less than 3PI/2 aka 270 degs, the player is looking left;
{
ray_x = ((((int)player_x) / tile_size) * tile_size) - 0.0001;
ray_y = (player_x - ray_x) * nTan + player_y;
x_offset = -64;
y_offset = -x_offset * nTan;
}
if(ray_angle < (PI/2) || ray_angle > ((3*PI)/2)) //if the ray's angle is less than PI/2 aka 90 or greater than 3PI/2 aka 270 degs, the player is looking right;
{
ray_x = ((((int)player_x) / tile_size) * tile_size) + tile_size;
ray_y = (player_x - ray_x) * nTan + player_y;
x_offset = 64;
y_offset = -x_offset * nTan;
}
if(ray_angle == 0 || ray_angle == PI) //player is looking straight up or down, the ray will never intersect
{
ray_x = player_x;
ray_y = player_y;
depth_of_field = 8;
}
while(depth_of_field < 8)
{
mx = (int)(ray_x) / tile_size;
my = (int)(ray_y)>>6;
mp = my * map_size_x + mx;
if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1)
{
vertical_x = ray_x;
vertical_y = ray_y;
vertical_distance = Dist(player_x, player_y, vertical_x, vertical_y, ray_angle);
depth_of_field = 8;
}
else
{
ray_x += x_offset;
ray_y += y_offset;
depth_of_field += 1;
}
}
if(horizontal_distance < vertical_distance)
{
ray_x = horizontal_x;
ray_y = horizontal_y;
final_distance = horizontal_distance;
}
if(horizontal_distance > vertical_distance)
{
ray_x = vertical_x;
ray_y = vertical_y;
final_distance = vertical_distance;
}
if(horizontal_distance == vertical_distance)
{
ray_x = horizontal_x;
ray_y = horizontal_y;
final_distance = horizontal_distance;
}
//printf("Horizontal X: %f\nHorizontal Y: %f\nHorizontal Dist: %f\n\nVertical X: %f\nVertical Y: %f\nVertical Dist: %f\n\nRay X: %f\n Ray Y: %f\nRayDist: &f\n\n", horizontal_x, horizontal_y, horizontal_distance, vertical_x, vertical_y, vertical_distance, ray_x, ray_y, (float)Dist(player_x, player_y, ray_x, ray_y, ray_angle));
glColor3f(1, 1, 0);
glLineWidth(3);
glBegin(GL_LINES);
glVertex2i(player_x, player_y);
glVertex2i(ray_x, ray_y);
glEnd();
/*------------*\
----------------
DRAWING 3D WALLS!
----------------
\*------------*/
float angle_offset = player_angle - ray_angle; //finds the distance between the player_angle (forward) and each ray
if(angle_offset < 0)
{
angle_offset += 2*PI;
}
if(angle_offset > 2*PI)
{
angle_offset -= 2*PI;
}
final_distance = final_distance * cos(angle_offset); //finds the correct ratio needed for level walls
float screen_height = 320; float screen_width = 530;
float line_height = (tile_size * screen_height) / final_distance; //By dividing by final distance, further away things will have a higher final distance and thus will divide more, creating a smaller object
if(line_height > 320)
{
line_height = 320; //ensures that we are not drawing lines offscreen;
}
float line_offset = screen_height - line_height / 2; //lowers the y value of each line so that it is centered on screen
/*---
-----
Draws a line every 8 pixels (vertex x value is r iteration times 8) with a width of 8. It is then shifted
530 pixels to the right so that it is not overlapping with the 2D scene;
All raycast 3D games have a fisheye warping effect. This is because lines further away from the center ray must
travel further than the center line, making their final_distance larger and thus the line smaller. To fix this, we
can calculate the distance between the center ray and each off-angle ray, then use trigonometry to find the correct
ratio to make all walls level;
-----
---*/
glLineWidth(8);
glBegin(GL_LINES);
glVertex2i(r*8+530, line_offset);
glVertex2i(r*8+530, line_height+line_offset);
glEnd();
ray_angle += DR;
if(ray_angle < 0)
{
ray_angle += 2*PI;
}
if(ray_angle > 2*PI)
{
ray_angle -= 2*PI;
}
}
}
void Display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //clears the color and depth buffer before drawing
DrawMap2D(); //map must be drawn before player is drawn to ensure that player is drawn afterwards and appears on top
DrawPlayer2D();
DrawRays2D();
glutSwapBuffers(); //swaps the buffers. One buffer draws, the other buffer displays. The draw buffer draws without showing the user, preventing artifacting and other weird junk
}
void Keystrokes(unsigned char key, int x, int y) //detects keystrokes
{
/*
---------
If A or D are pressed, rotates the character by a small amount. We are moving by radians, not degrees, so 1 degree movement is 0.02 rads. 0.1 rads is 5.7 degrees
Rotation works like this: You get the player's angle, then use cosine to determine x movement and sine to determine
y movement. These functions divide the amount of x and y movement by the hypotenuse of the right triangle made by
the angle of the player forwards. This is to normalize the value between 0 and 1. So if the player is facing all the
way to the right and moves forward, the player would move in the x direction by 1 and in the y direction by 0. If the
player is facing 45 degrees, then they would move 0.5 units up and 0.5 units right. This ensures proper movement
direction. These numbers (delta_x and delta_y) are then multiplied by 5, which is the 'speed' of the character;
If W or D are pressed, the delta_y and delta_x variables that we calculated are then applied, moving the character
in the motion we want;
---------
*/
if(key == 'w')
{
player_x += player_delta_x;
player_y += player_delta_y;
}
if(key == 'a')
{
player_angle -= 0.1;
if(player_angle < 0)
{
player_angle += 2*PI; //If player angle is negative, add 2*PI to ensure it is always between 0 and 360 degs
}
player_delta_x = cos(player_angle) * 5;
player_delta_y = sin(player_angle) * 5;
}
if(key == 's')
{
player_x -= player_delta_x;
player_y -= player_delta_y;
}
if(key == 'd')
{
player_angle += 0.1;
if(player_angle > (2*PI))
{
player_angle -= 2*PI; //If player angle is above 360 degrees, subtract 2*PI to ensure it is always between 0 and 360 degs
}
player_delta_x = cos(player_angle) * 5;
player_delta_y = sin(player_angle) * 5;
}
glutPostRedisplay(); //tells GL that the screen needs to be redrawn because the player moved
}
void Init()
{
glClearColor(0.3, 0.3, 0.3, 0);
gluOrtho2D(0, 1024, 512, 0);
player_x = 300; player_y = 300; //sets the player's position
player_delta_x = cos(player_angle) * 5; player_delta_y = sin(player_angle) * 5; //sets the delta_x and y to show the line when the program starts
}
int main(int argc, char* argv[]) //BS parameters that don't do anything
{
glutInit(&argc, argv); //BS parameters. Can allow passing commandline args, but I ain't doin' that shit brother
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); //sets display mode to double buffer (uses buffer swap) and RGBA
glutInitWindowSize(1024, 512);
glutCreateWindow("My Game");
Init();
glutDisplayFunc(Display); //the function that displays aka clears, draws, and swaps buffers
glutKeyboardFunc(Keystrokes); //this function is called whenever a key is pressed. It calls Keystrokes with the pressed key as a parameter
glutMainLoop();
return 0;
}