Created
August 15, 2024 08:16
-
-
Save trvswgnr/baadbbd901d19c334bff7046d5fd1d81 to your computer and use it in GitHub Desktop.
raycasting multiple layers in go with ebitengine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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