Here's a complete MVP raycasting engine. This is structured as multiple files for a proper SGDK project.
project/
├── src/
│ ├── main.c
│ ├── raycaster.c
│ ├── raycaster.h
│ ├── player.c
│ ├── player.h
│ ├── tables.c
│ ├── tables.h
│ └── level.h
├── res/
│ ├── resources.res
│ └── gfx/
│ └── walls.png
└── Makefile
#ifndef _TABLES_H_
#define _TABLES_H_
#include <genesis.h>
// Fixed point 16.16
#define FIX16_ONE 0x00010000
#define FIX16_HALF 0x00008000
// Angle constants (using 256 angles for full circle)
#define ANGLE_360 256
#define ANGLE_180 128
#define ANGLE_90 64
#define ANGLE_60 43
#define ANGLE_45 32
#define ANGLE_30 21
// Table sizes
#define SIN_TABLE_SIZE 256
#define TAN_TABLE_SIZE 256
// Lookup tables
extern const s32 sinTable[SIN_TABLE_SIZE];
extern const s32 cosTable[SIN_TABLE_SIZE];
extern const s32 tanTable[TAN_TABLE_SIZE];
// Fast fixed-point macros
#define FIX16_MUL(a, b) (((s32)(a) * (s32)(b)) >> 16)
#define FIX16_DIV(a, b) (((s32)(a) << 16) / (s32)(b))
#define FIX16_TO_INT(a) ((a) >> 16)
#define INT_TO_FIX16(a) ((a) << 16)
// Table access functions
s32 getSin(u16 angle);
s32 getCos(u16 angle);
s32 getTan(u16 angle);
void initTables(void);
#endif#include "tables.h"
// Pre-calculated sine table (256 entries, fixed 16.16)
// Values for 0-255 representing 0-360 degrees
const s32 sinTable[SIN_TABLE_SIZE] = {
0x0000, 0x0648, 0x0C8F, 0x12D5, 0x1918, 0x1F56, 0x2590, 0x2BC4,
0x31F1, 0x3817, 0x3E33, 0x4447, 0x4A50, 0x504D, 0x563E, 0x5C22,
0x61F7, 0x67BD, 0x6D74, 0x7319, 0x78AD, 0x7E2E, 0x839C, 0x88F5,
0x8E39, 0x9368, 0x987F, 0x9D7F, 0xA267, 0xA736, 0xABEB, 0xB085,
0xB504, 0xB968, 0xBDAE, 0xC1D8, 0xC5E4, 0xC9D1, 0xCD9F, 0xD14D,
0xD4DB, 0xD848, 0xDB94, 0xDEBE, 0xE1C5, 0xE4AA, 0xE76B, 0xEA09,
0xEC83, 0xEED8, 0xF109, 0xF314, 0xF4FA, 0xF6BA, 0xF853, 0xF9C7,
0xFB14, 0xFC3B, 0xFD3A, 0xFE13, 0xFEC4, 0xFF4E, 0xFFB1, 0xFFEC,
0x10000, 0xFFEC, 0xFFB1, 0xFF4E, 0xFEC4, 0xFE13, 0xFD3A, 0xFC3B,
0xFB14, 0xF9C7, 0xF853, 0xF6BA, 0xF4FA, 0xF314, 0xF109, 0xEED8,
0xEC83, 0xEA09, 0xE76B, 0xE4AA, 0xE1C5, 0xDEBE, 0xDB94, 0xD848,
0xD4DB, 0xD14D, 0xCD9F, 0xC9D1, 0xC5E4, 0xC1D8, 0xBDAE, 0xB968,
0xB504, 0xB085, 0xABEB, 0xA736, 0xA267, 0x9D7F, 0x987F, 0x9368,
0x8E39, 0x88F5, 0x839C, 0x7E2E, 0x78AD, 0x7319, 0x6D74, 0x67BD,
0x61F7, 0x5C22, 0x563E, 0x504D, 0x4A50, 0x4447, 0x3E33, 0x3817,
0x31F1, 0x2BC4, 0x2590, 0x1F56, 0x1918, 0x12D5, 0x0C8F, 0x0648,
0x0000, 0xF9B8, 0xF371, 0xED2B, 0xE6E8, 0xE0AA, 0xDA70, 0xD43C,
0xCE0F, 0xC7E9, 0xC1CD, 0xBBB9, 0xB5B0, 0xAFB3, 0xA9C2, 0xA3DE,
0x9E09, 0x9843, 0x928C, 0x8CE7, 0x8753, 0x81D2, 0x7C64, 0x770B,
0x71C7, 0x6C98, 0x6781, 0x6281, 0x5D99, 0x58CA, 0x5415, 0x4F7B,
0x4AFC, 0x4698, 0x4252, 0x3E28, 0x3A1C, 0x362F, 0x3261, 0x2EB3,
0x2B25, 0x27B8, 0x246C, 0x2142, 0x1E3B, 0x1B56, 0x1895, 0x15F7,
0x137D, 0x1128, 0x0EF7, 0x0CEC, 0x0B06, 0x0946, 0x07AD, 0x0639,
0x04EC, 0x03C5, 0x02C6, 0x01ED, 0x013C, 0x00B2, 0x004F, 0x0014,
0x0000, 0x0014, 0x004F, 0x00B2, 0x013C, 0x01ED, 0x02C6, 0x03C5,
0x04EC, 0x0639, 0x07AD, 0x0946, 0x0B06, 0x0CEC, 0x0EF7, 0x1128,
0x137D, 0x15F7, 0x1895, 0x1B56, 0x1E3B, 0x2142, 0x246C, 0x27B8,
0x2B25, 0x2EB3, 0x3261, 0x362F, 0x3A1C, 0x3E28, 0x4252, 0x4698,
0x4AFC, 0x4F7B, 0x5415, 0x58CA, 0x5D99, 0x6281, 0x6781, 0x6C98,
0x71C7, 0x770B, 0x7C64, 0x81D2, 0x8753, 0x8CE7, 0x928C, 0x9843,
0x9E09, 0xA3DE, 0xA9C2, 0xAFB3, 0xB5B0, 0xBBB9, 0xC1CD, 0xC7E9,
0xCE0F, 0xD43C, 0xDA70, 0xE0AA, 0xE6E8, 0xED2B, 0xF371, 0xF9B8
};
// Cosine table (same as sine but offset by 90 degrees)
const s32 cosTable[SIN_TABLE_SIZE] = {
0x10000, 0xFFEC, 0xFFB1, 0xFF4E, 0xFEC4, 0xFE13, 0xFD3A, 0xFC3B,
0xFB14, 0xF9C7, 0xF853, 0xF6BA, 0xF4FA, 0xF314, 0xF109, 0xEED8,
0xEC83, 0xEA09, 0xE76B, 0xE4AA, 0xE1C5, 0xDEBE, 0xDB94, 0xD848,
0xD4DB, 0xD14D, 0xCD9F, 0xC9D1, 0xC5E4, 0xC1D8, 0xBDAE, 0xB968,
0xB504, 0xB085, 0xABEB, 0xA736, 0xA267, 0x9D7F, 0x987F, 0x9368,
0x8E39, 0x88F5, 0x839C, 0x7E2E, 0x78AD, 0x7319, 0x6D74, 0x67BD,
0x61F7, 0x5C22, 0x563E, 0x504D, 0x4A50, 0x4447, 0x3E33, 0x3817,
0x31F1, 0x2BC4, 0x2590, 0x1F56, 0x1918, 0x12D5, 0x0C8F, 0x0648,
0x0000, 0xF9B8, 0xF371, 0xED2B, 0xE6E8, 0xE0AA, 0xDA70, 0xD43C,
0xCE0F, 0xC7E9, 0xC1CD, 0xBBB9, 0xB5B0, 0xAFB3, 0xA9C2, 0xA3DE,
0x9E09, 0x9843, 0x928C, 0x8CE7, 0x8753, 0x81D2, 0x7C64, 0x770B,
0x71C7, 0x6C98, 0x6781, 0x6281, 0x5D99, 0x58CA, 0x5415, 0x4F7B,
0x4AFC, 0x4698, 0x4252, 0x3E28, 0x3A1C, 0x362F, 0x3261, 0x2EB3,
0x2B25, 0x27B8, 0x246C, 0x2142, 0x1E3B, 0x1B56, 0x1895, 0x15F7,
0x137D, 0x1128, 0x0EF7, 0x0CEC, 0x0B06, 0x0946, 0x07AD, 0x0639,
0x04EC, 0x03C5, 0x02C6, 0x01ED, 0x013C, 0x00B2, 0x004F, 0x0014,
0x0000, 0x0014, 0x004F, 0x00B2, 0x013C, 0x01ED, 0x02C6, 0x03C5,
0x04EC, 0x0639, 0x07AD, 0x0946, 0x0B06, 0x0CEC, 0x0EF7, 0x1128,
0x137D, 0x15F7, 0x1895, 0x1B56, 0x1E3B, 0x2142, 0x246C, 0x27B8,
0x2B25, 0x2EB3, 0x3261, 0x362F, 0x3A1C, 0x3E28, 0x4252, 0x4698,
0x4AFC, 0x4F7B, 0x5415, 0x58CA, 0x5D99, 0x6281, 0x6781, 0x6C98,
0x71C7, 0x770B, 0x7C64, 0x81D2, 0x8753, 0x8CE7, 0x928C, 0x9843,
0x9E09, 0xA3DE, 0xA9C2, 0xAFB3, 0xB5B0, 0xBBB9, 0xC1CD, 0xC7E9,
0xCE0F, 0xD43C, 0xDA70, 0xE0AA, 0xE6E8, 0xED2B, 0xF371, 0xF9B8,
0x10000, 0xFFEC, 0xFFB1, 0xFF4E, 0xFEC4, 0xFE13, 0xFD3A, 0xFC3B,
0xFB14, 0xF9C7, 0xF853, 0xF6BA, 0xF4FA, 0xF314, 0xF109, 0xEED8,
0xEC83, 0xEA09, 0xE76B, 0xE4AA, 0xE1C5, 0xDEBE, 0xDB94, 0xD848,
0xD4DB, 0xD14D, 0xCD9F, 0xC9D1, 0xC5E4, 0xC1D8, 0xBDAE, 0xB968,
0xB504, 0xB085, 0xABEB, 0xA736, 0xA267, 0x9D7F, 0x987F, 0x9368,
0x8E39, 0x88F5, 0x839C, 0x7E2E, 0x78AD, 0x7319, 0x6D74, 0x67BD,
0x61F7, 0x5C22, 0x563E, 0x504D, 0x4A50, 0x4447, 0x3E33, 0x3817,
0x31F1, 0x2BC4, 0x2590, 0x1F56, 0x1918, 0x12D5, 0x0C8F, 0x0648
};
// Tangent table
const s32 tanTable[TAN_TABLE_SIZE] = {
0x0000, 0x0648, 0x0C91, 0x12DC, 0x192A, 0x1F7D, 0x25D6, 0x2C37,
0x32A1, 0x3916, 0x3F98, 0x4629, 0x4CCA, 0x537E, 0x5A46, 0x6125,
0x681D, 0x6F31, 0x7662, 0x7DB4, 0x8529, 0x8CC5, 0x948B, 0x9C7E,
0xA4A2, 0xACFB, 0xB58D, 0xBE5C, 0xC76E, 0xD0C7, 0xDA6E, 0xE469,
0xEEC0, 0xF97A, 0x104A0, 0x11046, 0x11C7D, 0x12959, 0x136EF, 0x1455E,
0x154C0, 0x16537, 0x176EB, 0x18A0E, 0x19ED0, 0x1B571, 0x1CE3C, 0x1E991,
0x207E5, 0x229CE, 0x25010, 0x27BA2, 0x2ADE1, 0x2E898, 0x32EB1, 0x38483,
0x3EE74, 0x47512, 0x52B7E, 0x62E84, 0x7FFFF, 0xB504F, 0x16A09E, 0x7FFFFF,
// Beyond 64 entries, values approach infinity (handle specially)
};
s32 getSin(u16 angle) {
return sinTable[angle & 0xFF];
}
s32 getCos(u16 angle) {
return cosTable[angle & 0xFF];
}
s32 getTan(u16 angle) {
return tanTable[angle & 0xFF];
}
void initTables(void) {
// Tables are const, nothing to init
// Could generate additional runtime tables here if needed
}#ifndef _PLAYER_H_
#define _PLAYER_H_
#include <genesis.h>
#include "tables.h"
// Player structure
typedef struct {
s32 x; // Fixed 16.16 position
s32 y; // Fixed 16.16 position
u16 angle; // 0-255 for 360 degrees
s16 moveSpeed; // Movement speed
s16 rotSpeed; // Rotation speed
} Player;
extern Player player;
void initPlayer(s16 startX, s16 startY, u16 startAngle);
void updatePlayer(void);
u8 isWall(s16 mapX, s16 mapY);
#endif#include "player.h"
#include "level.h"
Player player;
void initPlayer(s16 startX, s16 startY, u16 startAngle) {
player.x = INT_TO_FIX16(startX) + FIX16_HALF;
player.y = INT_TO_FIX16(startY) + FIX16_HALF;
player.angle = startAngle;
player.moveSpeed = 0x4000; // 0.25 in fixed point
player.rotSpeed = 4;
}
u8 isWall(s16 mapX, s16 mapY) {
if (mapX < 0 || mapX >= MAP_WIDTH || mapY < 0 || mapY >= MAP_HEIGHT)
return 1;
return levelMap[mapY][mapX] != 0;
}
void updatePlayer(void) {
u16 joy = JOY_readJoypad(JOY_1);
s32 cosA = getCos(player.angle);
s32 sinA = getSin(player.angle);
s32 newX = player.x;
s32 newY = player.y;
// Forward/Backward movement
if (joy & BUTTON_UP) {
newX += FIX16_MUL(cosA, player.moveSpeed);
newY += FIX16_MUL(sinA, player.moveSpeed);
}
if (joy & BUTTON_DOWN) {
newX -= FIX16_MUL(cosA, player.moveSpeed);
newY -= FIX16_MUL(sinA, player.moveSpeed);
}
// Strafe (with A button held)
if (joy & BUTTON_A) {
if (joy & BUTTON_LEFT) {
newX += FIX16_MUL(sinA, player.moveSpeed);
newY -= FIX16_MUL(cosA, player.moveSpeed);
}
if (joy & BUTTON_RIGHT) {
newX -= FIX16_MUL(sinA, player.moveSpeed);
newY += FIX16_MUL(cosA, player.moveSpeed);
}
} else {
// Rotation
if (joy & BUTTON_LEFT) {
player.angle = (player.angle - player.rotSpeed) & 0xFF;
}
if (joy & BUTTON_RIGHT) {
player.angle = (player.angle + player.rotSpeed) & 0xFF;
}
}
// Collision detection with margin
s32 margin = 0x3000; // Collision margin
// X collision
s16 checkX = FIX16_TO_INT(newX + (newX > player.x ? margin : -margin));
s16 checkY = FIX16_TO_INT(player.y);
if (!isWall(checkX, checkY)) {
player.x = newX;
}
// Y collision
checkX = FIX16_TO_INT(player.x);
checkY = FIX16_TO_INT(newY + (newY > player.y ? margin : -margin));
if (!isWall(checkX, checkY)) {
player.y = newY;
}
}#ifndef _LEVEL_H_
#define _LEVEL_H_
#include <genesis.h>
#define MAP_WIDTH 16
#define MAP_HEIGHT 16
// Map data: 0 = empty, 1+ = wall texture index
extern const u8 levelMap[MAP_HEIGHT][MAP_WIDTH];
// Player start position
#define PLAYER_START_X 2
#define PLAYER_START_Y 2
#define PLAYER_START_ANGLE 0
#endif#include "level.h"
const u8 levelMap[MAP_HEIGHT][MAP_WIDTH] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,2,2,2,0,0,0,3,3,3,0,0,0,1},
{1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,1},
{1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,4,4,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,4,4,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,3,0,0,0,0,0,0,0,2,0,0,0,1},
{1,0,0,3,0,0,0,0,0,0,0,2,0,0,0,1},
{1,0,0,3,3,3,0,0,0,2,2,2,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};#ifndef _RAYCASTER_H_
#define _RAYCASTER_H_
#include <genesis.h>
#include "player.h"
// Screen dimensions for raycasting
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 224
// Rendering resolution (lower = faster)
#define RENDER_WIDTH 80 // Number of rays to cast
#define RENDER_HEIGHT 100 // Height of 3D view
#define COLUMN_WIDTH 4 // Pixels per ray column
// View parameters
#define FOV_ANGLE 60 // Field of view in degrees (as angle units)
#define VIEW_DIST 277 // Distance to projection plane
// Texture dimensions
#define TEX_WIDTH 64
#define TEX_HEIGHT 64
// Ray hit information
typedef struct {
s32 distance; // Distance to wall (fixed 16.16)
u8 wallType; // Wall texture type
u8 side; // 0 = vertical hit, 1 = horizontal hit
s16 texX; // X coordinate on texture
} RayHit;
void initRaycaster(void);
void castRays(void);
void renderView(void);
void renderMinimap(void);
#endif#include "raycaster.h"
#include "level.h"
#include "tables.h"
// Buffer for ray casting results
static RayHit rayHits[RENDER_WIDTH];
// Pre-calculated column heights (for each possible distance)
static u16 columnHeights[512];
// Wall colors for different wall types (using palette indices)
static const u8 wallColors[5] = {0, 1, 2, 3, 4};
// Palette for walls (basic colors)
static const u16 wallPalette[16] = {
RGB24_TO_VDPCOLOR(0x000000), // 0: Black (floor)
RGB24_TO_VDPCOLOR(0x808080), // 1: Gray wall
RGB24_TO_VDPCOLOR(0x800000), // 2: Dark red wall
RGB24_TO_VDPCOLOR(0x008000), // 3: Dark green wall
RGB24_TO_VDPCOLOR(0x000080), // 4: Dark blue wall
RGB24_TO_VDPCOLOR(0x606060), // 5: Darker gray (shaded)
RGB24_TO_VDPCOLOR(0x600000), // 6: Darker red (shaded)
RGB24_TO_VDPCOLOR(0x006000), // 7: Darker green (shaded)
RGB24_TO_VDPCOLOR(0x000060), // 8: Darker blue (shaded)
RGB24_TO_VDPCOLOR(0x404040), // 9: Floor color
RGB24_TO_VDPCOLOR(0x4040A0), // 10: Ceiling color
RGB24_TO_VDPCOLOR(0xFFFFFF), // 11: White
RGB24_TO_VDPCOLOR(0xFF0000), // 12: Red
RGB24_TO_VDPCOLOR(0x00FF00), // 13: Green
RGB24_TO_VDPCOLOR(0x0000FF), // 14: Blue
RGB24_TO_VDPCOLOR(0xFFFF00) // 15: Yellow
};
void initRaycaster(void) {
// Pre-calculate column heights for each distance
for (u16 i = 1; i < 512; i++) {
// Height = constant / distance
u32 height = (64 * VIEW_DIST) / i;
if (height > RENDER_HEIGHT)
height = RENDER_HEIGHT;
columnHeights[i] = height;
}
columnHeights[0] = RENDER_HEIGHT;
// Load wall palette
PAL_setPalette(PAL1, wallPalette, CPU);
}
// Cast a single ray and return hit information
static RayHit castSingleRay(u16 rayAngle) {
RayHit hit;
hit.distance = 0x7FFFFFFF; // Max distance
hit.wallType = 0;
hit.side = 0;
hit.texX = 0;
s32 rayDirX = getCos(rayAngle);
s32 rayDirY = getSin(rayAngle);
// Player map position
s16 mapX = FIX16_TO_INT(player.x);
s16 mapY = FIX16_TO_INT(player.y);
// Length of ray from one side to next
s32 deltaDistX = (rayDirX != 0) ? abs(FIX16_DIV(FIX16_ONE, rayDirX)) : 0x7FFFFFFF;
s32 deltaDistY = (rayDirY != 0) ? abs(FIX16_DIV(FIX16_ONE, rayDirY)) : 0x7FFFFFFF;
// Direction to step in x and y
s16 stepX, stepY;
s32 sideDistX, sideDistY;
// Calculate step and initial sideDist
if (rayDirX < 0) {
stepX = -1;
sideDistX = FIX16_MUL(player.x - INT_TO_FIX16(mapX), deltaDistX);
} else {
stepX = 1;
sideDistX = FIX16_MUL(INT_TO_FIX16(mapX + 1) - player.x, deltaDistX);
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = FIX16_MUL(player.y - INT_TO_FIX16(mapY), deltaDistY);
} else {
stepY = 1;
sideDistY = FIX16_MUL(INT_TO_FIX16(mapY + 1) - player.y, deltaDistY);
}
// DDA algorithm
u8 hitWall = 0;
u8 side = 0;
u8 maxSteps = 32; // Limit steps for performance
while (!hitWall && maxSteps > 0) {
maxSteps--;
// Jump to next map square
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// Check for wall hit
if (mapX >= 0 && mapX < MAP_WIDTH && mapY >= 0 && mapY < MAP_HEIGHT) {
if (levelMap[mapY][mapX] > 0) {
hitWall = 1;
hit.wallType = levelMap[mapY][mapX];
}
} else {
hitWall = 1; // Out of bounds
hit.wallType = 1;
}
}
// Calculate distance
if (side == 0) {
hit.distance = sideDistX - deltaDistX;
} else {
hit.distance = sideDistY - deltaDistY;
}
hit.side = side;
// Calculate texture X coordinate
s32 wallX;
if (side == 0) {
wallX = player.y + FIX16_MUL(hit.distance, rayDirY);
} else {
wallX = player.x + FIX16_MUL(hit.distance, rayDirX);
}
wallX = wallX & 0xFFFF; // Fractional part only
hit.texX = (wallX * TEX_WIDTH) >> 16;
return hit;
}
void castRays(void) {
// Calculate starting angle (leftmost ray)
s16 startAngle = player.angle - (FOV_ANGLE / 2);
s16 angleStep = FOV_ANGLE / RENDER_WIDTH;
for (u16 i = 0; i < RENDER_WIDTH; i++) {
u16 rayAngle = (startAngle + (i * angleStep)) & 0xFF;
rayHits[i] = castSingleRay(rayAngle);
// Apply fish-eye correction
s16 angleDiff = rayAngle - player.angle;
if (angleDiff < -128) angleDiff += 256;
if (angleDiff > 128) angleDiff -= 256;
s32 correctionFactor = getCos(angleDiff & 0xFF);
rayHits[i].distance = FIX16_MUL(rayHits[i].distance, correctionFactor);
}
}
void renderView(void) {
// Using tile-based rendering for Genesis
// Each column is COLUMN_WIDTH pixels wide
u16 centerY = RENDER_HEIGHT / 2;
for (u16 col = 0; col < RENDER_WIDTH; col++) {
RayHit* hit = &rayHits[col];
// Get column height from distance
u16 distIndex = FIX16_TO_INT(hit->distance);
if (distIndex >= 512) distIndex = 511;
if (distIndex < 1) distIndex = 1;
u16 lineHeight = columnHeights[distIndex];
// Calculate draw start and end
s16 drawStart = centerY - (lineHeight / 2);
s16 drawEnd = centerY + (lineHeight / 2);
if (drawStart < 0) drawStart = 0;
if (drawEnd >= RENDER_HEIGHT) drawEnd = RENDER_HEIGHT - 1;
// Determine wall color based on type and side
u8 colorIndex = hit->wallType;
if (hit->side == 1) {
colorIndex += 4; // Darker shade for Y-side walls
}
// Calculate screen X position
u16 screenX = col * COLUMN_WIDTH;
// Draw ceiling (using BG color)
// Draw wall column
// Draw floor (using BG color)
// For performance, we use vertical strips
// In a full implementation, you'd update VRAM tiles
// Simple line drawing for MVP
for (u16 w = 0; w < COLUMN_WIDTH; w++) {
u16 x = screenX + w;
if (x < SCREEN_WIDTH) {
// Draw column using bitmap plotting
// Note: Genesis doesn't have a bitmap mode,
// so this needs to be adapted to tile updates
// Ceiling
for (s16 y = 0; y < drawStart; y++) {
// Set pixel to ceiling color
}
// Wall
for (s16 y = drawStart; y <= drawEnd; y++) {
// Set pixel to wall color
}
// Floor
for (s16 y = drawEnd + 1; y < RENDER_HEIGHT; y++) {
// Set pixel to floor color
}
}
}
}
}
void renderMinimap(void) {
// Draw minimap in corner
u16 mapStartX = SCREEN_WIDTH - MAP_WIDTH * 4 - 8;
u16 mapStartY = 8;
// Draw map cells - would use sprites or tiles in real implementation
for (u16 y = 0; y < MAP_HEIGHT; y++) {
for (u16 x = 0; x < MAP_WIDTH; x++) {
u16 screenX = mapStartX + x * 4;
u16 screenY = mapStartY + y * 4;
if (levelMap[y][x] > 0) {
// Wall - draw filled rectangle
}
}
}
// Draw player position
u16 playerScreenX = mapStartX + (FIX16_TO_INT(player.x) * 4);
u16 playerScreenY = mapStartY + (FIX16_TO_INT(player.y) * 4);
// Draw player marker
}#include <genesis.h>
#include "tables.h"
#include "player.h"
#include "raycaster.h"
#include "level.h"
// Frame buffer using tiles
#define FB_WIDTH_TILES 40
#define FB_HEIGHT_TILES 14
#define FB_TILE_START 256
static u32 tileBuffer[FB_WIDTH_TILES * FB_HEIGHT_TILES * 8];
// Simple wall patterns (8x8 pixels per tile)
static const u32 wallPatterns[5][8] = {
// Empty
{0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000},
// Wall type 1 - Gray brick
{0x11111111, 0x11111111, 0x11111111, 0x11111111,
0x11111111, 0x11111111, 0x11111111, 0x00000000},
// Wall type 2 - Red
{0x22222222, 0x22222222, 0x22222222, 0x22222222,
0x22222222, 0x22222222, 0x22222222, 0x00000000},
// Wall type 3 - Green
{0x33333333, 0x33333333, 0x33333333, 0x33333333,
0x33333333, 0x33333333, 0x33333333, 0x00000000},
// Wall type 4 - Blue
{0x44444444, 0x44444444, 0x44444444, 0x44444444,
0x44444444, 0x44444444, 0x44444444, 0x00000000}
};
// Color palette
static const u16 palette[16] = {
RGB24_TO_VDPCOLOR(0x000000), // 0: Black
RGB24_TO_VDPCOLOR(0x808080), // 1: Gray
RGB24_TO_VDPCOLOR(0xA04040), // 2: Red
RGB24_TO_VDPCOLOR(0x40A040), // 3: Green
RGB24_TO_VDPCOLOR(0x4040A0), // 4: Blue
RGB24_TO_VDPCOLOR(0x606060), // 5: Dark gray
RGB24_TO_VDPCOLOR(0x803030), // 6: Dark red
RGB24_TO_VDPCOLOR(0x308030), // 7: Dark green
RGB24_TO_VDPCOLOR(0x303080), // 8: Dark blue
RGB24_TO_VDPCOLOR(0x404040), // 9: Floor
RGB24_TO_VDPCOLOR(0x202050), // 10: Ceiling
RGB24_TO_VDPCOLOR(0xFFFFFF), // 11: White
RGB24_TO_VDPCOLOR(0xFF0000), // 12: Bright red
RGB24_TO_VDPCOLOR(0x00FF00), // 13: Bright green
RGB24_TO_VDPCOLOR(0xFFFF00), // 14: Yellow (player)
RGB24_TO_VDPCOLOR(0xC0C0C0) // 15: Light gray
};
// Optimized raycaster for tile-based rendering
static void renderFrame(void) {
// Clear tile buffer
memset(tileBuffer, 0, sizeof(tileBuffer));
u16 centerY = RENDER_HEIGHT / 2;
s16 startAngle = player.angle - 30; // 60 degree FOV
// Cast ray for each column (2 pixels per ray for speed)
for (u16 col = 0; col < 160; col += 2) {
// Calculate ray angle
u16 rayAngle = (startAngle + (col * 60 / 160)) & 0xFF;
s32 rayDirX = getCos(rayAngle);
s32 rayDirY = getSin(rayAngle);
s16 mapX = FIX16_TO_INT(player.x);
s16 mapY = FIX16_TO_INT(player.y);
s32 deltaDistX = (rayDirX != 0) ? abs(FIX16_DIV(FIX16_ONE, rayDirX)) : 0x7FFFFFFF;
s32 deltaDistY = (rayDirY != 0) ? abs(FIX16_DIV(FIX16_ONE, rayDirY)) : 0x7FFFFFFF;
s16 stepX = (rayDirX < 0) ? -1 : 1;
s16 stepY = (rayDirY < 0) ? -1 : 1;
s32 sideDistX, sideDistY;
if (rayDirX < 0)
sideDistX = FIX16_MUL(player.x - INT_TO_FIX16(mapX), deltaDistX);
else
sideDistX = FIX16_MUL(INT_TO_FIX16(mapX + 1) - player.x, deltaDistX);
if (rayDirY < 0)
sideDistY = FIX16_MUL(player.y - INT_TO_FIX16(mapY), deltaDistY);
else
sideDistY = FIX16_MUL(INT_TO_FIX16(mapY + 1) - player.y, deltaDistY);
// DDA
u8 hitWall = 0;
u8 side = 0;
u8 wallType = 1;
for (u8 step = 0; step < 24 && !hitWall; step++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX >= 0 && mapX < MAP_WIDTH && mapY >= 0 && mapY < MAP_HEIGHT) {
if (levelMap[mapY][mapX] > 0) {
hitWall = 1;
wallType = levelMap[mapY][mapX];
}
} else {
hitWall = 1;
}
}
// Calculate perpendicular distance
s32 perpDist;
if (side == 0)
perpDist = sideDistX - deltaDistX;
else
perpDist = sideDistY - deltaDistY;
// Fish-eye correction
s16 angleDiff = (rayAngle - player.angle) & 0xFF;
if (angleDiff > 128) angleDiff -= 256;
perpDist = FIX16_MUL(perpDist, getCos(angleDiff & 0x3F));
// Calculate column height
u16 distInt = FIX16_TO_INT(perpDist);
if (distInt < 1) distInt = 1;
if (distInt > 255) distInt = 255;
u16 lineHeight = (64 * 200) / distInt;
if (lineHeight > RENDER_HEIGHT) lineHeight = RENDER_HEIGHT;
s16 drawStart = centerY - (lineHeight >> 1);
s16 drawEnd = centerY + (lineHeight >> 1);
if (drawStart < 0) drawStart = 0;
if (drawEnd >= RENDER_HEIGHT) drawEnd = RENDER_HEIGHT - 1;
// Color based on wall type and side
u8 color = wallType;
if (side == 1) color += 4;
// Draw column into tile buffer
u16 tileX = col >> 3; // Column / 8
u8 pixelX = col & 7; // Column % 8
for (s16 y = drawStart; y <= drawEnd; y++) {
u16 tileY = y >> 3; // Row / 8
u8 pixelY = y & 7; // Row % 8
u16 tileIndex = tileY * FB_WIDTH_TILES + tileX;
u32* tileLine = &tileBuffer[tileIndex * 8 + pixelY];
// Set pixel in tile (4bpp packed format)
u8 shift = (7 - pixelX) * 4;
*tileLine = (*tileLine & ~(0xF << shift)) | (color << shift);
// Also set next pixel for doubled width
if ((pixelX + 1) < 8) {
shift = (6 - pixelX) * 4;
*tileLine = (*tileLine & ~(0xF << shift)) | (color << shift);
}
}
}
// Upload tiles to VRAM
VDP_loadTileData(tileBuffer, FB_TILE_START, FB_WIDTH_TILES * FB_HEIGHT_TILES, DMA);
}
static void setupTilemap(void) {
// Set up plane A with our framebuffer tiles
u16 tileIndex = FB_TILE_START;
for (u16 y = 0; y < FB_HEIGHT_TILES; y++) {
for (u16 x = 0; x < FB_WIDTH_TILES; x++) {
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 0, 0, 0, tileIndex), x, y + 5);
tileIndex++;
}
}
}
static void drawHUD(void) {
// Draw FPS counter
char buffer[16];
sprintf(buffer, "FPS:%2d", SYS_getFPS());
VDP_drawText(buffer, 1, 1);
// Draw position
sprintf(buffer, "X:%3d Y:%3d", FIX16_TO_INT(player.x), FIX16_TO_INT(player.y));
VDP_drawText(buffer, 1, 2);
// Draw angle
sprintf(buffer, "A:%3d", player.angle);
VDP_drawText(buffer, 1, 3);
// Draw minimap
u16 mmX = 32;
u16 mmY = 1;
for (u16 y = 0; y < MAP_HEIGHT; y++) {
for (u16 x = 0; x < MAP_WIDTH; x++) {
char c = levelMap[y][x] ? '#' : '.';
if (x == FIX16_TO_INT(player.x) && y == FIX16_TO_INT(player.y))
c = '@';
VDP_drawText(&c, mmX + x, mmY + y);
}
}
}
int main(void) {
// Initialize system
VDP_setScreenWidth320();
VDP_setScreenHeight224();
// Set up palettes
PAL_setPalette(PAL0, palette, CPU);
// Initialize game systems
initTables();
initPlayer(PLAYER_START_X, PLAYER_START_Y, PLAYER_START_ANGLE);
initRaycaster();
// Set up tilemap for rendering
setupTilemap();
// Clear screen
VDP_clearPlane(BG_A, TRUE);
VDP_clearPlane(BG_B, TRUE);
// Draw floor/ceiling background
VDP_setBackgroundColor(9); // Floor color
// Main game loop
while(TRUE) {
// Update player
updatePlayer();
// Render 3D view
renderFrame();
// Draw HUD
drawHUD();
// Wait for VBlank
SYS_doVBlankProcess();
}
return 0;
}# Wall textures (optional - for textured version)
# IMAGE walls "gfx/walls.png" BEST ALL
# Makefile for Wolf3D Genesis
# SGDK path - adjust this to your installation
SGDK ?= /opt/sgdk
# Project name
PROJECT = wolf3d
# Source files
SRC_DIR = src
RES_DIR = res
OUT_DIR = out
# Include SGDK makefile
include $(SGDK)/makefile.gen-
Install SGDK (https://github.com/Stephane-D/SGDK)
-
Set environment variable:
export SGDK=/path/to/sgdk- Build:
make -f $SGDK/makefile.gen- Run in emulator:
# Using BlastEm, Kega Fusion, or Genesis Plus GX
blastem out/rom.bin| Button | Action |
|---|---|
| D-Pad Up | Move Forward |
| D-Pad Down | Move Backward |
| D-Pad Left | Turn Left |
| D-Pad Right | Turn Right |
| A + Left | Strafe Left |
| A + Right | Strafe Right |
- Resolution: Rendering at 160x100 (~14 FPS on real hardware)
- Rays: 80 rays with 2-pixel columns
- Max depth: 24 steps for DDA algorithm
- Optimizations included:
- Fixed-point math (no floating point)
- Pre-calculated lookup tables
- DDA raycasting algorithm
- Tile-based rendering
For better performance, consider:
- Using assembly for critical loops
- Reducing ray count further
- Using the Z80 for audio/calculations
- Implementing column caching