C Raycaster Breakdown3
In today’s article, we’re going to see drawing the sprites on the screen.
Disclaimer
This code is written only in C. I used OpenGL to implement the 3D drawing in C. Shout out to ‘3D Sage’ who gave me an inspiration and way to make a game.
Code Breakdown
main.c
void drawPlayerSprite() {
int spriteSize = 32; // the sprite is 32x32 pixels
int pixelSize = 15; // size of each 'pixel' of the sprite on the screen
int screenPosX = 480 - (spriteSize * pixelSize); // center of the sprite on the screen width
int screenPosY = 640 - (spriteSize * pixelSize);
int(*sprite)[32][32] = (currentFrame == 0) ? &playerSprite1 : &playerSprite2;
for (int y = 0; y < spriteSize; y++) {
for (int x = 0; x < spriteSize; x++) {
if ((*sprite)[y][x] == 1) { // If the value is 1, color white
glColor3f(1.0, 1.0, 1.0);
}
else if ((*sprite)[y][x] == 2) { // If the value is 2, color black
glColor3f(0.0, 0.0, 0.0);
}
else {
continue;
}
glBegin(GL_QUADS);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize + pixelSize);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize + pixelSize);
glEnd();
}
}
}
To draw in OpenGL, you need a function named display(){...}
. C is procedual program so, OpenGL draws in order of funtions called.
void drawPlayerSprite()
is the main function for drawing the player. I’ve used TPS(Third Person Shoot), Over the Shoulder View like “Resident Evil 4”
int spriteSize = 32; int pixelSize = 15; int screenPosX = 480 - (spriteSize * pixelSize); int screenPosY = 640 - (spriteSize * pixelSize);
I’ve draw sprite in $32*32$. int pixelSize=15
can make a change of sprite size drawing in front of the screen. int screenPosX
and int screenPosY
is showing where the sprite is drawn.
int(*sprite)[32][32] = (currentFrame == 0) ? &playerSprite1 : &playerSprite2;
I’ve used pointer to point 2 different sprites. Two sprites are for walking animation. Sprite can be found in “sprites.h” and “sprites.c”.
The following Nested For loop is for drawing sprites in different color. Player sprite is drawn in 2 colors. And each array is defined as 0, 1, 2. If array has a value of 1 pixel is colored as white. If array is 2, pixel is colored with black.
glBegin(GL_QUADS)
to glEND()
is for drawing in OpenGL. It’s drawn by GL_QUADS
not GL_POINTS
or GL_LINES
. Following has the same mechanism with drawPlayerSprite()
void drawZoomSprite() {
int spriteSize = 32;
int pixelSize = 15;
int screenPosX = 480 - (spriteSize * pixelSize);
int screenPosY = 640 - (spriteSize * pixelSize);
int(*zSprite)[32][32] = (currentFrame == 0) ? &zoomSprite1 : &zoomSprite2;
for (int y = 0; y < spriteSize; y++) {
for (int x = 0; x < spriteSize; x++) {
if ((*zSprite)[y][x] == 1) { // If the value is 1, color white
glColor3f(1.0, 1.0, 1.0);
}
else if ((*zSprite)[y][x] == 2) { // If the value is 2, color black
glColor3f(0.0, 0.0, 0.0);
}
else {
continue;
}
glBegin(GL_QUADS);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize + pixelSize);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize + pixelSize);
glEnd();
}
}
}
void drawFireSprite() {
int spriteSize = 32;
int pixelSize = 15;
int screenPosX = 480 - (spriteSize * pixelSize);
int screenPosY = 640 - (spriteSize * pixelSize);
for (int y = 0; y < spriteSize; y++) {
for (int x = 0; x < spriteSize; x++) {
if ((fireSprite)[y][x] == 1) {glColor3f(1.0, 1.0, 1.0);} // If the value is 1, color white
else if ((fireSprite)[y][x] == 2) { glColor3f(0.0, 0.0, 0.0);}// If the value is 2, color black
else if ((fireSprite)[y][x] == 3) {glColor3f(1.0, 0.0, 0.0);} // If the value is 2, color black
else if ((fireSprite)[y][x] == 4) {glColor3f(1.0, 1.0, 0.0);} // If the value is 2, color black
else {continue;}
glBegin(GL_QUADS);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize);
glVertex2i(screenPosX + x * pixelSize + pixelSize, screenPosY + y * pixelSize + pixelSize);
glVertex2i(screenPosX + x * pixelSize, screenPosY + y * pixelSize + pixelSize);
glEnd();
}
}
}
Drawing zoomSprite and fireSprite has the same mechanism with drawPlayerSprite()
. I made zoomSprite and fireSprite additionally to make a shooting mechanism and accurate shooting.
Drawing MonsterSprite
Drawing MonsterSprite is a little bit different. For the PlayerSprite, it’s drawn over the screen. So, you don’t need to modify the location or pixels, making it smaller when it goes far away. But, Drawing Monster Sprite is different. Monster keeps moving so, we need to trace the location of Monster.
typedef struct
{
int type; //static, key, enemy
int state; //on, off
int map; //texture to show
double x, y, z; //position
}sprite; sprite sp[4];
int depth[120];
void drawMonsterSprite()
{
int x, y, s;
int spx = (int)sp[0].x >> 6, spy = (int)sp[0].y >> 6;
int spx_add = ((int)sp[0].x + 15) >> 6, spy_add = ((int)sp[0].y + 15) >> 6;
int spx_sub = ((int)sp[0].x - 15) >> 6, spy_sub = ((int)sp[0].y - 15) >> 6;
if (sp[0].x > px && mapW[spy * 8 + spx_sub] == 0) { sp[0].x -= 0.03 * fps; }
if (sp[0].x < px && mapW[spy * 8 + spx_add] == 0) { sp[0].x += 0.03 * fps; }
if (sp[0].y > py && mapW[spy_sub * 8 + spx] == 0) { sp[0].y -= 0.03 * fps; }
if (sp[0].y < py && mapW[spy_add * 8 + spx] == 0) { sp[0].y += 0.03 * fps; }
for (s = 0; s < 2; s++) {
float sx = sp[s].x - px;
float sy = sp[s].y - py;
float sz = sp[s].z;
float CS = cos(degToRad(pa)), SN = sin(degToRad(pa)); // rotate based on player side
float a = sy * CS + sx * SN;
float b = sx * CS - sy * SN;
sx = a; sy = b;
sx = (sx * 108.0 / sy) + (120 / 2); //convert to screen
sy = (sz * 108.0 / sy) + (80 / 2);
int scale = 45 * 80 / b;
if (scale < 0) { scale = 0; }
if (scale > 120) { scale = 120; }
//texture
float t_x = 0, t_y = 31, t_x_step = 32.0 / (float) scale, t_y_step = 32.0/(float) scale;
for (x = sx - scale / 2; x < sx + scale / 2; x++)
{
t_y = 31;
for (y = 0; y < scale; y++)
{
if (x > 0 && x < 120 && b < depth[x]) //check the depth and see draw if it's on screen.
{
int pixel = ((int)t_y * 32 + (int)t_x) * 3;
int red = monster_walkSprite1[pixel + 0];
int green = monster_walkSprite1[pixel + 1];
int blue = monster_walkSprite1[pixel + 2];
if (red != 255, green != 255, blue != 255) {
glPointSize(8);
glColor3ub(red, green, blue);
glBegin(GL_POINTS);
glVertex2i(x * 8, sy * 8 - y * 8);
glEnd();
}
t_y -= t_y_step; if (t_y < 0) { t_y = 0; }
}
}
t_x += t_x_step;
}
}
}
struct
on the top of the code is for Monsters. Type is defined to differentiate keys, monsters, or others. This is to reuse the funtion. state
is for decide to draw or not.
int depth[120]
is counting how may rays to draw. In this case, we are drawing 120 rays.