Golang raycaster engine using the Ebitengine 2D Game Library
Golang raycasting engine using the Ebitengine 2D Game Library to render a 3-Dimensional perspective in a 2-Dimensional view. It was originally converted from the C# based OwlRaycastEngine, which in turn was created based on a raycasting tutorial.
To see it in action visit the YouTube playlist.
The raycaster-go-demo project is available as an example of how to use the raycaster-go engine as a module.
To get started with your own project using the raycaster-go engine as a Go module:
go mod init github.com/yourname/yourgame
go get github.com/harbdog/raycaster-go
NOTE: Depending on the OS, the Ebitengine game library may have additional dependencies to install.
You will first want to become familiar with how to use the Ebitengine 2D Game Library. It has all of the APIs needed to render images on a 2D canvas, handle inputs from the player, and even play sounds. The raycaster-go-demo is available for reference.
Just like any other Ebitengine game, there are a few game interface functions required from your game implementation:
Update
Draw
Layout
Refer to the Ebitengine tour pages for detailed explanation about the usage of these interface functions needed for basic game flow.
There are additional raycaster-go specific interfaces that will be required to render the map levels, wall textures, and sprites.
Interface functions required to provide layout of wall positions on the 2-dimensional array representing the game map.
Level(levelNum int) [][]int
Needs to return the 2-dimensional array map of a given level index.
The first order array is used as the X-axis, and the second as the Y-axis.
The int
value at each X/Y array index pair represents whether a wall is present at that map coordinate.
> 0
: indicates presence of a wall at that map coordinate.
<= 0
: indicates absence of walls at that map coordinate.
Length of X and Y arrays do not need to match within a level, can be square or rectangle map layout.
Each level of the map must have arrays of the same size.
NumLevels() int
Interface functions required for rendering texture images for the walls and floor.
TextureAt(x, y, levelNum, side int) *ebiten.Image
side
is currently provided as either 1
or 0
indicating the texture viewed from the X or Y direction,
respectively. This value can be used, if desired, to have a different texture image representing
alternate sides of the wall.texSize
) provided
to the NewCamera
function. For example texSize: 256
requires all wall textures to be 256x256
pixels in size.FloorTextureAt(x, y int) *image.RGBA
nil
to only render the non-repeating floor texture provided to
the camera.SetFloorTexture
function.Interface functions required to determine sprite images and positions to render in game.
Pos() *geom.Vector2
PosZ() float64
0.0
represents the very bottom of the floor on the first elevation level.VerticalAnchor()
value will be used to determine rendered sprite orientation about the Z-position.VerticalAnchor() raycaster.SpriteAnchor
raycaster.AnchorBottom
: makes an image shift to the bottom of its Z-position.raycaster.AnchorCenter
: keeps an image centered to its Z-position.raycaster.AnchorTop
: makes an image shift to the top of its Z-position.Scale() float64
1.0
indicates no scaling.1.0
will likely require VerticalAnchor()
to return desired position
since the centered scaling point is at the vertical center of the screen.Texture() *ebiten.Image
camera.Update
function call.TextureRect() image.Rectangle
Needs to return an image.Rectangle representing the texture sheet position
and area to render for the image currently returned by Texture()
.
If the image source only contains a single image, just set to origin position along with the width and height of the image:
return image.Rect(0, 0, imageWidth, imageHeight)
SetScreenRect(rect *image.Rectangle)
nil
.Illumination() float64
0.0
is used for normal sprite illumination.5000.0
, can be used to have a sprite illuminate itself even in dark environments.IsFocusable() bool
true
only if the Sprite object needs to be converged upon by the center point
(used with camera.GetConvergenceDistance()
and camera.GetConvergencePoint()
).After implementing all required interface functions, the last step is to initialize an instance of raycaster.Camera
and make the function calls needed to update and draw during your game loop.
func NewCamera(width int, height int, texSize int, mapObj Map, tex TextureHandler) *Camera
width
, height
: the window/viewport size.texSize
: the pixel width and height of all textures.mapObj
: struct implementing all required Map interfaces.tex
: struct implementing all required TextureHandler interfaces.camera.SetPosition(pos *geom.Vector2)
camera.SetPositionZ
0.5
represents the middle of the first elevation level).camera.SetHeadingAngle
0.0
is in the positive X-axis with no Y-axis direction).camera.SetPitchAngle
0.0
is looking straight ahead).camera.SetFloorTexture(floor *ebiten.Image)
TextureHandler.FloorTexture()
interface returns nil
, and for areas outside of map bounds.camera.SetSkyTexture(sky *ebiten.Image)
camera.Update(sprites []Sprite)
sprites
: an array of structs implementing all required Sprite interfaces.Draw(screen *ebiten.Image)
to perform raycasting updates.camera.Draw
.camera.Draw(screen *ebiten.Image)
Draw(screen *ebiten.Image)
to render the raycasted levels and sprites.camera.Update
.camera.SetRenderDistance(distance float64)
-1
camera.SetLightFalloff(falloff float64)
-100
camera.SetGlobalIllumination(illumination float64)
300
camera.SetLightRGB(min, max color.NRGBA)
camera.GetConvergencePoint() *geom3d.Vector3
camera.GetConvergenceDistance() float64
camera.GetConvergenceSprite() Sprite
nil
if the point of convergence is not a Sprite but wall, floor, or ceiling.camera.SetAlwaysSetSpriteScreenRect(b bool)
> 0.0 && <= 1.0
).