Skip to content

Instantly share code, notes, and snippets.

@celsowm
Created January 8, 2026 12:57
Show Gist options
  • Select an option

  • Save celsowm/08604b432a5c3b8acd0f1923f5fd780a to your computer and use it in GitHub Desktop.

Select an option

Save celsowm/08604b432a5c3b8acd0f1923f5fd780a to your computer and use it in GitHub Desktop.
raycast sgdk claude opus

Wolf3D-Style Raycasting Engine for Sega Genesis (SGDK)

Here's a complete MVP raycasting engine. This is structured as multiple files for a proper SGDK project.

Project Structure

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

src/tables.h

#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

src/tables.c

#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
}

src/player.h

#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

src/player.c

#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;
    }
}

src/level.h

#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

src/level.c

#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}
};

src/raycaster.h

#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

src/raycaster.c

#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
}

src/main.c

#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;
}

res/resources.res

# Wall textures (optional - for textured version)
# IMAGE walls "gfx/walls.png" BEST ALL

Makefile

# 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

Building and Running

  1. Install SGDK (https://github.com/Stephane-D/SGDK)

  2. Set environment variable:

export SGDK=/path/to/sgdk
  1. Build:
make -f $SGDK/makefile.gen
  1. Run in emulator:
# Using BlastEm, Kega Fusion, or Genesis Plus GX
blastem out/rom.bin

Controls

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

Performance Notes

  1. Resolution: Rendering at 160x100 (~14 FPS on real hardware)
  2. Rays: 80 rays with 2-pixel columns
  3. Max depth: 24 steps for DDA algorithm
  4. 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment