mirror of
				https://github.com/hajimehoshi/ebiten.git
				synced 2025-10-31 03:36:21 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			316 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2019 The Ebiten Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"image"
 | |
| 	"image/color"
 | |
| 	_ "image/png"
 | |
| 	"log"
 | |
| 	"math"
 | |
| 	"sort"
 | |
| 
 | |
| 	"github.com/hajimehoshi/ebiten/v2"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/inpututil"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/vector"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	screenWidth  = 240
 | |
| 	screenHeight = 240
 | |
| 	padding      = 20
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	bgImage       *ebiten.Image
 | |
| 	shadowImage   = ebiten.NewImage(screenWidth, screenHeight)
 | |
| 	triangleImage = ebiten.NewImage(screenWidth, screenHeight)
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	// Decode an image from the image file's byte slice.
 | |
| 	img, _, err := image.Decode(bytes.NewReader(images.Tile_png))
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	bgImage = ebiten.NewImageFromImage(img)
 | |
| 	triangleImage.Fill(color.White)
 | |
| }
 | |
| 
 | |
| type line struct {
 | |
| 	X1, Y1, X2, Y2 float64
 | |
| }
 | |
| 
 | |
| func (l *line) angle() float64 {
 | |
| 	return math.Atan2(l.Y2-l.Y1, l.X2-l.X1)
 | |
| }
 | |
| 
 | |
| type object struct {
 | |
| 	walls []line
 | |
| }
 | |
| 
 | |
| func (o object) points() [][2]float64 {
 | |
| 	// Get one of the endpoints for all segments,
 | |
| 	// + the startpoint of the first one, for non-closed paths
 | |
| 	var points [][2]float64
 | |
| 	for _, wall := range o.walls {
 | |
| 		points = append(points, [2]float64{wall.X2, wall.Y2})
 | |
| 	}
 | |
| 	p := [2]float64{o.walls[0].X1, o.walls[0].Y1}
 | |
| 	if p[0] != points[len(points)-1][0] && p[1] != points[len(points)-1][1] {
 | |
| 		points = append(points, [2]float64{o.walls[0].X1, o.walls[0].Y1})
 | |
| 	}
 | |
| 	return points
 | |
| }
 | |
| 
 | |
| func newRay(x, y, length, angle float64) line {
 | |
| 	return line{
 | |
| 		X1: x,
 | |
| 		Y1: y,
 | |
| 		X2: x + length*math.Cos(angle),
 | |
| 		Y2: y + length*math.Sin(angle),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // intersection calculates the intersection of given two lines.
 | |
| func intersection(l1, l2 line) (float64, float64, bool) {
 | |
| 	// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
 | |
| 	denom := (l1.X1-l1.X2)*(l2.Y1-l2.Y2) - (l1.Y1-l1.Y2)*(l2.X1-l2.X2)
 | |
| 	tNum := (l1.X1-l2.X1)*(l2.Y1-l2.Y2) - (l1.Y1-l2.Y1)*(l2.X1-l2.X2)
 | |
| 	uNum := -((l1.X1-l1.X2)*(l1.Y1-l2.Y1) - (l1.Y1-l1.Y2)*(l1.X1-l2.X1))
 | |
| 
 | |
| 	if denom == 0 {
 | |
| 		return 0, 0, false
 | |
| 	}
 | |
| 
 | |
| 	t := tNum / denom
 | |
| 	if t > 1 || t < 0 {
 | |
| 		return 0, 0, false
 | |
| 	}
 | |
| 
 | |
| 	u := uNum / denom
 | |
| 	if u > 1 || u < 0 {
 | |
| 		return 0, 0, false
 | |
| 	}
 | |
| 
 | |
| 	x := l1.X1 + t*(l1.X2-l1.X1)
 | |
| 	y := l1.Y1 + t*(l1.Y2-l1.Y1)
 | |
| 	return x, y, true
 | |
| }
 | |
| 
 | |
| // rayCasting returns a slice of line originating from point cx, cy and intersecting with objects
 | |
| func rayCasting(cx, cy float64, objects []object) []line {
 | |
| 	const rayLength = 1000 // something large enough to reach all objects
 | |
| 
 | |
| 	var rays []line
 | |
| 	for _, obj := range objects {
 | |
| 		// Cast two rays per point
 | |
| 		for _, p := range obj.points() {
 | |
| 			l := line{cx, cy, p[0], p[1]}
 | |
| 			angle := l.angle()
 | |
| 
 | |
| 			for _, offset := range []float64{-0.005, 0.005} {
 | |
| 				points := [][2]float64{}
 | |
| 				ray := newRay(cx, cy, rayLength, angle+offset)
 | |
| 
 | |
| 				// Unpack all objects
 | |
| 				for _, o := range objects {
 | |
| 					for _, wall := range o.walls {
 | |
| 						if px, py, ok := intersection(ray, wall); ok {
 | |
| 							points = append(points, [2]float64{px, py})
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				// Find the point closest to start of ray
 | |
| 				min := math.Inf(1)
 | |
| 				minI := -1
 | |
| 				for i, p := range points {
 | |
| 					d2 := (cx-p[0])*(cx-p[0]) + (cy-p[1])*(cy-p[1])
 | |
| 					if d2 < min {
 | |
| 						min = d2
 | |
| 						minI = i
 | |
| 					}
 | |
| 				}
 | |
| 				rays = append(rays, line{cx, cy, points[minI][0], points[minI][1]})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Sort rays based on angle, otherwise light triangles will not come out right
 | |
| 	sort.Slice(rays, func(i int, j int) bool {
 | |
| 		return rays[i].angle() < rays[j].angle()
 | |
| 	})
 | |
| 	return rays
 | |
| }
 | |
| 
 | |
| func (g *Game) handleMovement() {
 | |
| 	if ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
 | |
| 		g.px += 4
 | |
| 	}
 | |
| 
 | |
| 	if ebiten.IsKeyPressed(ebiten.KeyS) || ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
 | |
| 		g.py += 4
 | |
| 	}
 | |
| 
 | |
| 	if ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
 | |
| 		g.px -= 4
 | |
| 	}
 | |
| 
 | |
| 	if ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
 | |
| 		g.py -= 4
 | |
| 	}
 | |
| 
 | |
| 	// +1/-1 is to stop player before it reaches the border
 | |
| 	if g.px >= screenWidth-padding {
 | |
| 		g.px = screenWidth - padding - 1
 | |
| 	}
 | |
| 
 | |
| 	if g.px <= padding {
 | |
| 		g.px = padding + 1
 | |
| 	}
 | |
| 
 | |
| 	if g.py >= screenHeight-padding {
 | |
| 		g.py = screenHeight - padding - 1
 | |
| 	}
 | |
| 
 | |
| 	if g.py <= padding {
 | |
| 		g.py = padding + 1
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func rayVertices(x1, y1, x2, y2, x3, y3 float64) []ebiten.Vertex {
 | |
| 	return []ebiten.Vertex{
 | |
| 		{DstX: float32(x1), DstY: float32(y1), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
 | |
| 		{DstX: float32(x2), DstY: float32(y2), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
 | |
| 		{DstX: float32(x3), DstY: float32(y3), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type Game struct {
 | |
| 	showRays bool
 | |
| 	px, py   int
 | |
| 	objects  []object
 | |
| }
 | |
| 
 | |
| func (g *Game) Update() error {
 | |
| 	if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
 | |
| 		return errors.New("game ended by player")
 | |
| 	}
 | |
| 
 | |
| 	if inpututil.IsKeyJustPressed(ebiten.KeyR) {
 | |
| 		g.showRays = !g.showRays
 | |
| 	}
 | |
| 
 | |
| 	g.handleMovement()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (g *Game) Draw(screen *ebiten.Image) {
 | |
| 	// Reset the shadowImage
 | |
| 	shadowImage.Fill(color.Black)
 | |
| 	rays := rayCasting(float64(g.px), float64(g.py), g.objects)
 | |
| 
 | |
| 	// Subtract ray triangles from shadow
 | |
| 	opt := &ebiten.DrawTrianglesOptions{}
 | |
| 	opt.Address = ebiten.AddressRepeat
 | |
| 	opt.Blend = ebiten.BlendSourceOut
 | |
| 	for i, line := range rays {
 | |
| 		nextLine := rays[(i+1)%len(rays)]
 | |
| 
 | |
| 		// Draw triangle of area between rays
 | |
| 		v := rayVertices(float64(g.px), float64(g.py), nextLine.X2, nextLine.Y2, line.X2, line.Y2)
 | |
| 		shadowImage.DrawTriangles(v, []uint16{0, 1, 2}, triangleImage, opt)
 | |
| 	}
 | |
| 
 | |
| 	// Draw background
 | |
| 	screen.DrawImage(bgImage, nil)
 | |
| 
 | |
| 	if g.showRays {
 | |
| 		// Draw rays
 | |
| 		for _, r := range rays {
 | |
| 			vector.StrokeLine(screen, float32(r.X1), float32(r.Y1), float32(r.X2), float32(r.Y2), 1, color.RGBA{255, 255, 0, 150}, true)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Draw shadow
 | |
| 	op := &ebiten.DrawImageOptions{}
 | |
| 	op.ColorScale.ScaleAlpha(0.7)
 | |
| 	screen.DrawImage(shadowImage, op)
 | |
| 
 | |
| 	// Draw walls
 | |
| 	for _, obj := range g.objects {
 | |
| 		for _, w := range obj.walls {
 | |
| 			vector.StrokeLine(screen, float32(w.X1), float32(w.Y1), float32(w.X2), float32(w.Y2), 1, color.RGBA{255, 0, 0, 255}, true)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Draw player as a rect
 | |
| 	vector.DrawFilledRect(screen, float32(g.px)-2, float32(g.py)-2, 4, 4, color.Black, true)
 | |
| 	vector.DrawFilledRect(screen, float32(g.px)-1, float32(g.py)-1, 2, 2, color.RGBA{255, 100, 100, 255}, true)
 | |
| 
 | |
| 	if g.showRays {
 | |
| 		ebitenutil.DebugPrintAt(screen, "R: hide rays", padding, 0)
 | |
| 	} else {
 | |
| 		ebitenutil.DebugPrintAt(screen, "R: show rays", padding, 0)
 | |
| 	}
 | |
| 
 | |
| 	ebitenutil.DebugPrintAt(screen, "WASD: move", 160, 0)
 | |
| 	ebitenutil.DebugPrintAt(screen, fmt.Sprintf("TPS: %0.2f", ebiten.ActualTPS()), 51, 51)
 | |
| 	ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Rays: 2*%d", len(rays)/2), padding, 222)
 | |
| }
 | |
| 
 | |
| func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
 | |
| 	return screenWidth, screenHeight
 | |
| }
 | |
| 
 | |
| func rect(x, y, w, h float64) []line {
 | |
| 	return []line{
 | |
| 		{x, y, x, y + h},
 | |
| 		{x, y + h, x + w, y + h},
 | |
| 		{x + w, y + h, x + w, y},
 | |
| 		{x + w, y, x, y},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	g := &Game{
 | |
| 		px: screenWidth / 2,
 | |
| 		py: screenHeight / 2,
 | |
| 	}
 | |
| 
 | |
| 	// Add outer walls
 | |
| 	g.objects = append(g.objects, object{rect(padding, padding, screenWidth-2*padding, screenHeight-2*padding)})
 | |
| 
 | |
| 	// Angled wall
 | |
| 	g.objects = append(g.objects, object{[]line{{50, 110, 100, 150}}})
 | |
| 
 | |
| 	// Rectangles
 | |
| 	g.objects = append(g.objects, object{rect(45, 50, 70, 20)})
 | |
| 	g.objects = append(g.objects, object{rect(150, 50, 30, 60)})
 | |
| 
 | |
| 	ebiten.SetWindowSize(screenWidth*2, screenHeight*2)
 | |
| 	ebiten.SetWindowTitle("Ray casting and shadows (Ebitengine Demo)")
 | |
| 	if err := ebiten.RunGame(g); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| }
 | 
