Skip to content

Instantly share code, notes, and snippets.

@trvswgnr
Created August 15, 2024 08:16
Show Gist options
  • Save trvswgnr/baadbbd901d19c334bff7046d5fd1d81 to your computer and use it in GitHub Desktop.
Save trvswgnr/baadbbd901d19c334bff7046d5fd1d81 to your computer and use it in GitHub Desktop.
raycasting multiple layers in go with ebitengine
package main
import (
"image/color"
"log"
"math"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/vector"
)
const (
screenWidth = 640
screenHeight = 480
mapWidth = 5
mapHeight = 5
numLayers = 3
)
var (
worldMap = [numLayers][mapHeight][mapWidth]int{
{
{1, 1, 1, 1, 1},
{1, 0, 0, 0, 1},
{1, 0, 2, 0, 1},
{1, 0, 0, 0, 1},
{1, 1, 1, 1, 1},
},
{
{1, 1, 1, 1, 1},
{1, 0, 0, 0, 1},
{1, 0, 3, 0, 1},
{1, 0, 0, 0, 1},
{1, 1, 1, 1, 1},
},
{
{1, 1, 1, 1, 1},
{1, 0, 0, 0, 1},
{1, 0, 4, 0, 1},
{1, 0, 0, 0, 1},
{1, 1, 1, 1, 1},
},
}
)
type Game struct {
posX, posY, dirX, dirY, planeX, planeY float64
}
func NewGame() *Game {
return &Game{
posX: 1.5,
posY: 1.5,
dirX: 1,
dirY: 0,
planeX: 0,
planeY: 0.66,
}
}
func (g *Game) Update() error {
if ebiten.IsKeyPressed(ebiten.KeyUp) {
g.movePlayer(g.dirX*0.1, g.dirY*0.1)
}
if ebiten.IsKeyPressed(ebiten.KeyDown) {
g.movePlayer(-g.dirX*0.1, -g.dirY*0.1)
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
g.rotatePlayer(0.05)
}
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
g.rotatePlayer(-0.05)
}
return nil
}
func (g *Game) movePlayer(dx, dy float64) {
newX, newY := g.posX+dx, g.posY+dy
if newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight {
if worldMap[0][int(newY)][int(newX)] == 0 {
g.posX, g.posY = newX, newY
}
}
}
func (g *Game) rotatePlayer(angle float64) {
oldDirX := g.dirX
g.dirX = g.dirX*math.Cos(angle) - g.dirY*math.Sin(angle)
g.dirY = oldDirX*math.Sin(angle) + g.dirY*math.Cos(angle)
oldPlaneX := g.planeX
g.planeX = g.planeX*math.Cos(angle) - g.planeY*math.Sin(angle)
g.planeY = oldPlaneX*math.Sin(angle) + g.planeY*math.Cos(angle)
}
func (g *Game) Draw(screen *ebiten.Image) {
floorColor := color.RGBA{R: 100, G: 100, B: 100, A: 255}
ceilingColor := color.RGBA{R: 50, G: 50, B: 150, A: 255}
for x := 0; x < screenWidth; x++ {
cameraX := 2*float64(x)/screenWidth - 1
rayDirX := g.dirX + g.planeX*cameraX
rayDirY := g.dirY + g.planeY*cameraX
mapX, mapY := int(g.posX), int(g.posY)
sideDistX, sideDistY := 0.0, 0.0
deltaDistX := math.Abs(1 / rayDirX)
deltaDistY := math.Abs(1 / rayDirY)
stepX, stepY := 0, 0
if rayDirX < 0 {
stepX = -1
sideDistX = (g.posX - float64(mapX)) * deltaDistX
} else {
stepX = 1
sideDistX = (float64(mapX) + 1.0 - g.posX) * deltaDistX
}
if rayDirY < 0 {
stepY = -1
sideDistY = (g.posY - float64(mapY)) * deltaDistY
} else {
stepY = 1
sideDistY = (float64(mapY) + 1.0 - g.posY) * deltaDistY
}
var perpWallDist float64
side := 0
hit := 0
for hit == 0 {
if sideDistX < sideDistY {
sideDistX += deltaDistX
mapX += stepX
side = 0
} else {
sideDistY += deltaDistY
mapY += stepY
side = 1
}
if mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight {
hit = 1
} else if worldMap[0][mapY][mapX] > 0 {
hit = 1
}
}
if side == 0 {
perpWallDist = (float64(mapX) - g.posX + (1-float64(stepX))/2) / rayDirX
} else {
perpWallDist = (float64(mapY) - g.posY + (1-float64(stepY))/2) / rayDirY
}
lineHeight := int(screenHeight / perpWallDist)
drawStart := -lineHeight/2 + screenHeight/2
if drawStart < 0 {
drawStart = 0
}
drawEnd := lineHeight/2 + screenHeight/2
if drawEnd >= screenHeight {
drawEnd = screenHeight - 1
}
// draw ceiling
vector.StrokeLine(screen, float32(x), 0, float32(x), float32(drawStart), 1, ceilingColor, false)
// draw floor
vector.StrokeLine(screen, float32(x), float32(drawEnd), float32(x), float32(screenHeight), 1, floorColor, false)
// draw walls for each layer
for layer := 0; layer < numLayers; layer++ {
if mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight {
continue
}
var wallColor color.Color
switch worldMap[layer][mapY][mapX] {
case 1:
wallColor = color.RGBA{R: 128, G: 128, B: 128, A: 255}
case 2:
wallColor = color.RGBA{R: 255, A: 255}
case 3:
wallColor = color.RGBA{G: 255, A: 255}
case 4:
wallColor = color.RGBA{B: 255, A: 255}
default:
continue
}
if side == 1 {
r, g, b, _ := wallColor.RGBA()
wallColor = color.RGBA{
R: uint8(float64(r) * 0.9),
G: uint8(float64(g) * 0.9),
B: uint8(float64(b) * 0.9),
A: 255,
}
}
layerStart := drawStart + (drawEnd-drawStart)*layer/numLayers
layerEnd := drawStart + (drawEnd-drawStart)*(layer+1)/numLayers
vector.StrokeLine(screen, float32(x), float32(layerStart), float32(x), float32(layerEnd), 1, wallColor, false)
}
}
ebitenutil.DebugPrint(screen, "use arrow keys to move")
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("raycast 3d multiple layers")
if err := ebiten.RunGame(NewGame()); err != nil {
log.Fatal(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment