main.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. // Copyright 2019 The Ebiten Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package main
  15. import (
  16. "bytes"
  17. "errors"
  18. "fmt"
  19. "image"
  20. "image/color"
  21. _ "image/png"
  22. "log"
  23. "math"
  24. "sort"
  25. "github.com/hajimehoshi/ebiten/v2"
  26. "github.com/hajimehoshi/ebiten/v2/ebitenutil"
  27. "github.com/hajimehoshi/ebiten/v2/examples/resources/images"
  28. "github.com/hajimehoshi/ebiten/v2/inpututil"
  29. "github.com/hajimehoshi/ebiten/v2/vector"
  30. )
  31. const (
  32. screenWidth = 240
  33. screenHeight = 240
  34. padding = 20
  35. )
  36. var (
  37. bgImage *ebiten.Image
  38. shadowImage = ebiten.NewImage(screenWidth, screenHeight)
  39. triangleImage = ebiten.NewImage(screenWidth, screenHeight)
  40. )
  41. func init() {
  42. // Decode an image from the image file's byte slice.
  43. img, _, err := image.Decode(bytes.NewReader(images.Tile_png))
  44. if err != nil {
  45. log.Fatal(err)
  46. }
  47. bgImage = ebiten.NewImageFromImage(img)
  48. triangleImage.Fill(color.White)
  49. }
  50. type line struct {
  51. X1, Y1, X2, Y2 float64
  52. }
  53. func (l *line) angle() float64 {
  54. return math.Atan2(l.Y2-l.Y1, l.X2-l.X1)
  55. }
  56. type object struct {
  57. walls []line
  58. }
  59. func (o object) points() [][2]float64 {
  60. // Get one of the endpoints for all segments,
  61. // + the startpoint of the first one, for non-closed paths
  62. var points [][2]float64
  63. for _, wall := range o.walls {
  64. points = append(points, [2]float64{wall.X2, wall.Y2})
  65. }
  66. p := [2]float64{o.walls[0].X1, o.walls[0].Y1}
  67. if p[0] != points[len(points)-1][0] && p[1] != points[len(points)-1][1] {
  68. points = append(points, [2]float64{o.walls[0].X1, o.walls[0].Y1})
  69. }
  70. return points
  71. }
  72. func newRay(x, y, length, angle float64) line {
  73. return line{
  74. X1: x,
  75. Y1: y,
  76. X2: x + length*math.Cos(angle),
  77. Y2: y + length*math.Sin(angle),
  78. }
  79. }
  80. // intersection calculates the intersection of given two lines.
  81. func intersection(l1, l2 line) (float64, float64, bool) {
  82. // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
  83. denom := (l1.X1-l1.X2)*(l2.Y1-l2.Y2) - (l1.Y1-l1.Y2)*(l2.X1-l2.X2)
  84. tNum := (l1.X1-l2.X1)*(l2.Y1-l2.Y2) - (l1.Y1-l2.Y1)*(l2.X1-l2.X2)
  85. uNum := -((l1.X1-l1.X2)*(l1.Y1-l2.Y1) - (l1.Y1-l1.Y2)*(l1.X1-l2.X1))
  86. if denom == 0 {
  87. return 0, 0, false
  88. }
  89. t := tNum / denom
  90. if t > 1 || t < 0 {
  91. return 0, 0, false
  92. }
  93. u := uNum / denom
  94. if u > 1 || u < 0 {
  95. return 0, 0, false
  96. }
  97. x := l1.X1 + t*(l1.X2-l1.X1)
  98. y := l1.Y1 + t*(l1.Y2-l1.Y1)
  99. return x, y, true
  100. }
  101. // rayCasting returns a slice of line originating from point cx, cy and intersecting with objects
  102. func rayCasting(cx, cy float64, objects []object) []line {
  103. const rayLength = 1000 // something large enough to reach all objects
  104. var rays []line
  105. for _, obj := range objects {
  106. // Cast two rays per point
  107. for _, p := range obj.points() {
  108. l := line{cx, cy, p[0], p[1]}
  109. angle := l.angle()
  110. for _, offset := range []float64{-0.005, 0.005} {
  111. points := [][2]float64{}
  112. ray := newRay(cx, cy, rayLength, angle+offset)
  113. // Unpack all objects
  114. for _, o := range objects {
  115. for _, wall := range o.walls {
  116. if px, py, ok := intersection(ray, wall); ok {
  117. points = append(points, [2]float64{px, py})
  118. }
  119. }
  120. }
  121. // Find the point closest to start of ray
  122. min := math.Inf(1)
  123. minI := -1
  124. for i, p := range points {
  125. d2 := (cx-p[0])*(cx-p[0]) + (cy-p[1])*(cy-p[1])
  126. if d2 < min {
  127. min = d2
  128. minI = i
  129. }
  130. }
  131. rays = append(rays, line{cx, cy, points[minI][0], points[minI][1]})
  132. }
  133. }
  134. }
  135. // Sort rays based on angle, otherwise light triangles will not come out right
  136. sort.Slice(rays, func(i int, j int) bool {
  137. return rays[i].angle() < rays[j].angle()
  138. })
  139. return rays
  140. }
  141. func (g *Game) handleMovement() {
  142. if ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
  143. g.px += 4
  144. }
  145. if ebiten.IsKeyPressed(ebiten.KeyS) || ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
  146. g.py += 4
  147. }
  148. if ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
  149. g.px -= 4
  150. }
  151. if ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
  152. g.py -= 4
  153. }
  154. // +1/-1 is to stop player before it reaches the border
  155. if g.px >= screenWidth-padding {
  156. g.px = screenWidth - padding - 1
  157. }
  158. if g.px <= padding {
  159. g.px = padding + 1
  160. }
  161. if g.py >= screenHeight-padding {
  162. g.py = screenHeight - padding - 1
  163. }
  164. if g.py <= padding {
  165. g.py = padding + 1
  166. }
  167. }
  168. func rayVertices(x1, y1, x2, y2, x3, y3 float64) []ebiten.Vertex {
  169. return []ebiten.Vertex{
  170. {DstX: float32(x1), DstY: float32(y1), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
  171. {DstX: float32(x2), DstY: float32(y2), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
  172. {DstX: float32(x3), DstY: float32(y3), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
  173. }
  174. }
  175. type Game struct {
  176. showRays bool
  177. px, py int
  178. objects []object
  179. }
  180. func (g *Game) Update() error {
  181. if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
  182. return errors.New("game ended by player")
  183. }
  184. if inpututil.IsKeyJustPressed(ebiten.KeyR) {
  185. g.showRays = !g.showRays
  186. }
  187. g.handleMovement()
  188. return nil
  189. }
  190. func (g *Game) Draw(screen *ebiten.Image) {
  191. // Reset the shadowImage
  192. shadowImage.Fill(color.Black)
  193. rays := rayCasting(float64(g.px), float64(g.py), g.objects)
  194. // Subtract ray triangles from shadow
  195. opt := &ebiten.DrawTrianglesOptions{}
  196. opt.Address = ebiten.AddressRepeat
  197. opt.Blend = ebiten.BlendSourceOut
  198. for i, line := range rays {
  199. nextLine := rays[(i+1)%len(rays)]
  200. // Draw triangle of area between rays
  201. v := rayVertices(float64(g.px), float64(g.py), nextLine.X2, nextLine.Y2, line.X2, line.Y2)
  202. shadowImage.DrawTriangles(v, []uint16{0, 1, 2}, triangleImage, opt)
  203. }
  204. // Draw background
  205. screen.DrawImage(bgImage, nil)
  206. if g.showRays {
  207. // Draw rays
  208. for _, r := range rays {
  209. vector.StrokeLine(screen, float32(r.X1), float32(r.Y1), float32(r.X2), float32(r.Y2), 1, color.RGBA{255, 255, 0, 150}, true)
  210. }
  211. }
  212. // Draw shadow
  213. op := &ebiten.DrawImageOptions{}
  214. op.ColorScale.ScaleAlpha(0.7)
  215. screen.DrawImage(shadowImage, op)
  216. // Draw walls
  217. for _, obj := range g.objects {
  218. for _, w := range obj.walls {
  219. vector.StrokeLine(screen, float32(w.X1), float32(w.Y1), float32(w.X2), float32(w.Y2), 1, color.RGBA{255, 0, 0, 255}, true)
  220. }
  221. }
  222. // Draw player as a rect
  223. vector.DrawFilledRect(screen, float32(g.px)-2, float32(g.py)-2, 4, 4, color.Black, true)
  224. vector.DrawFilledRect(screen, float32(g.px)-1, float32(g.py)-1, 2, 2, color.RGBA{255, 100, 100, 255}, true)
  225. if g.showRays {
  226. ebitenutil.DebugPrintAt(screen, "R: hide rays", padding, 0)
  227. } else {
  228. ebitenutil.DebugPrintAt(screen, "R: show rays", padding, 0)
  229. }
  230. ebitenutil.DebugPrintAt(screen, "WASD: move", 160, 0)
  231. ebitenutil.DebugPrintAt(screen, fmt.Sprintf("TPS: %0.2f", ebiten.ActualTPS()), 51, 51)
  232. ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Rays: 2*%d", len(rays)/2), padding, 222)
  233. }
  234. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
  235. return screenWidth, screenHeight
  236. }
  237. func rect(x, y, w, h float64) []line {
  238. return []line{
  239. {x, y, x, y + h},
  240. {x, y + h, x + w, y + h},
  241. {x + w, y + h, x + w, y},
  242. {x + w, y, x, y},
  243. }
  244. }
  245. func main() {
  246. g := &Game{
  247. px: screenWidth / 2,
  248. py: screenHeight / 2,
  249. }
  250. // Add outer walls
  251. g.objects = append(g.objects, object{rect(padding, padding, screenWidth-2*padding, screenHeight-2*padding)})
  252. // Angled wall
  253. g.objects = append(g.objects, object{[]line{{50, 110, 100, 150}}})
  254. // Rectangles
  255. g.objects = append(g.objects, object{rect(45, 50, 70, 20)})
  256. g.objects = append(g.objects, object{rect(150, 50, 30, 60)})
  257. ebiten.SetWindowSize(screenWidth*2, screenHeight*2)
  258. ebiten.SetWindowTitle("Ray casting and shadows (Ebitengine Demo)")
  259. if err := ebiten.RunGame(g); err != nil {
  260. log.Fatal(err)
  261. }
  262. }