main.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright 2016 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. "fmt"
  18. "image"
  19. "image/color"
  20. _ "image/jpeg"
  21. "log"
  22. "math"
  23. "github.com/hajimehoshi/ebiten/v2"
  24. "github.com/hajimehoshi/ebiten/v2/ebitenutil"
  25. "github.com/hajimehoshi/ebiten/v2/examples/resources/images"
  26. )
  27. const (
  28. screenWidth = 640
  29. screenHeight = 480
  30. maxAngle = 256
  31. maxLean = 16
  32. )
  33. var (
  34. skyColor = color.RGBA{0x66, 0xcc, 0xff, 0xff}
  35. gophersImage *ebiten.Image
  36. repeatedGophersImage *ebiten.Image
  37. )
  38. func init() {
  39. // Decode an image from the image file's byte slice.
  40. img, _, err := image.Decode(bytes.NewReader(images.Gophers_jpg))
  41. if err != nil {
  42. log.Fatal(err)
  43. }
  44. gophersImage = ebiten.NewImageFromImage(img)
  45. const (
  46. xrepeat = 7
  47. yrepeat = 8
  48. )
  49. w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy()
  50. repeatedGophersImage = ebiten.NewImage(w*xrepeat, h*yrepeat)
  51. for j := 0; j < yrepeat; j++ {
  52. for i := 0; i < xrepeat; i++ {
  53. op := &ebiten.DrawImageOptions{}
  54. op.GeoM.Translate(float64(w*i), float64(h*j))
  55. repeatedGophersImage.DrawImage(gophersImage, op)
  56. }
  57. }
  58. }
  59. // player represents the current airship's position.
  60. type player struct {
  61. // x16, y16 represents the position in XY plane in fixed float format.
  62. // The fractional part has 16 bits of precision.
  63. x16 int
  64. y16 int
  65. // angle represents the player's angle in XY plane.
  66. // angle takes an integer value in [0, maxAngle).
  67. angle int
  68. // lean represents the player's leaning.
  69. // lean takes an integer value in [-maxLean, maxLean].
  70. lean int
  71. }
  72. func round(x float64) float64 {
  73. return math.Floor(x + 0.5)
  74. }
  75. // MoveForward moves the player p forward.
  76. func (p *player) MoveForward() {
  77. w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy()
  78. mx := w * 16
  79. my := h * 16
  80. s, c := math.Sincos(float64(p.angle) * 2 * math.Pi / maxAngle)
  81. p.x16 += int(round(16*c) * 2)
  82. p.y16 += int(round(16*s) * 2)
  83. for mx <= p.x16 {
  84. p.x16 -= mx
  85. }
  86. for my <= p.y16 {
  87. p.y16 -= my
  88. }
  89. for p.x16 < 0 {
  90. p.x16 += mx
  91. }
  92. for p.y16 < 0 {
  93. p.y16 += my
  94. }
  95. }
  96. // RotateRight rotates the player p in the right direction.
  97. func (p *player) RotateRight() {
  98. p.angle++
  99. if maxAngle <= p.angle {
  100. p.angle -= maxAngle
  101. }
  102. p.lean++
  103. if maxLean < p.lean {
  104. p.lean = maxLean
  105. }
  106. }
  107. // RotateLeft rotates the player p in the left direction.
  108. func (p *player) RotateLeft() {
  109. p.angle--
  110. if p.angle < 0 {
  111. p.angle += maxAngle
  112. }
  113. p.lean--
  114. if p.lean < -maxLean {
  115. p.lean = -maxLean
  116. }
  117. }
  118. // Stabilize tries to move the player in the stable position (lean).
  119. func (p *player) Stabilize() {
  120. if 0 < p.lean {
  121. p.lean--
  122. }
  123. if p.lean < 0 {
  124. p.lean++
  125. }
  126. }
  127. // Position returns the player p's position.
  128. func (p *player) Position() (int, int) {
  129. return p.x16, p.y16
  130. }
  131. // Angle returns the player p's angle.
  132. func (p *player) Angle() int {
  133. return p.angle
  134. }
  135. // updateGroundImage updates the ground image according to the current player's position.
  136. func (g *Game) updateGroundImage(ground *ebiten.Image) {
  137. ground.Clear()
  138. x16, y16 := g.player.Position()
  139. a := g.player.Angle()
  140. rw, rh := repeatedGophersImage.Bounds().Dx(), repeatedGophersImage.Bounds().Dy()
  141. gw, gh := ground.Bounds().Dx(), ground.Bounds().Dy()
  142. op := &ebiten.DrawImageOptions{}
  143. op.GeoM.Translate(float64(-x16)/16, float64(-y16)/16)
  144. op.GeoM.Translate(float64(-rw)/2, float64(-rh)/2)
  145. op.GeoM.Rotate(float64(-a)*2*math.Pi/maxAngle + math.Pi*3.0/2.0)
  146. op.GeoM.Translate(float64(gw)/2, float64(gh)/2)
  147. ground.DrawImage(repeatedGophersImage, op)
  148. }
  149. // drawGroundImage draws the ground image to the given screen image.
  150. func (g *Game) drawGroundImage(screen *ebiten.Image, ground *ebiten.Image) {
  151. g.perspectiveGroundImage.Clear()
  152. gw := ground.Bounds().Dx()
  153. pw, ph := g.perspectiveGroundImage.Bounds().Dx(), g.perspectiveGroundImage.Bounds().Dy()
  154. for j := 0; j < ph; j++ {
  155. // z is in [2, -1]
  156. rate := float64(j) / float64(ph)
  157. z := (1-rate)*2 + rate*-1
  158. // Avoid too small z, or the scale (1/z) can be too big.
  159. if z <= 0.1 {
  160. break
  161. }
  162. op := &ebiten.DrawImageOptions{}
  163. op.GeoM.Translate(-float64(pw)/2, 0)
  164. op.GeoM.Scale(1/z, 8) // 8 is an arbitrary number not to make empty lines.
  165. op.GeoM.Translate(float64(pw)/2, float64(j)/z)
  166. g.perspectiveGroundImage.DrawImage(ground.SubImage(image.Rect(0, j, gw, j+1)).(*ebiten.Image), op)
  167. }
  168. g.perspectiveGroundImage.DrawImage(g.fogImage, nil)
  169. op := &ebiten.DrawImageOptions{}
  170. op.GeoM.Translate(-float64(pw)/2, 0)
  171. op.GeoM.Rotate(-1 * float64(g.player.lean) / maxLean * math.Pi / 8)
  172. op.GeoM.Translate(float64(screenWidth)/2, screenHeight/3)
  173. screen.DrawImage(g.perspectiveGroundImage, op)
  174. }
  175. type Game struct {
  176. player *player
  177. groundImage *ebiten.Image
  178. perspectiveGroundImage *ebiten.Image
  179. fogImage *ebiten.Image
  180. }
  181. func NewGame() *Game {
  182. g := &Game{
  183. player: &player{
  184. x16: 16 * 100,
  185. y16: 16 * 200,
  186. angle: maxAngle * 3 / 4,
  187. },
  188. groundImage: ebiten.NewImage(screenWidth*3, screenHeight*2/3+200),
  189. perspectiveGroundImage: ebiten.NewImage(screenWidth*3, screenHeight),
  190. }
  191. const fogHeight = 16
  192. w := g.perspectiveGroundImage.Bounds().Dx()
  193. fogRGBA := image.NewRGBA(image.Rect(0, 0, w, fogHeight))
  194. for j := 0; j < fogHeight; j++ {
  195. a := uint32(float64(fogHeight-1-j) * 0xff / (fogHeight - 1))
  196. clr := skyColor
  197. r, g, b, oa := uint32(clr.R), uint32(clr.G), uint32(clr.B), uint32(clr.A)
  198. clr.R = uint8(r * a / oa)
  199. clr.G = uint8(g * a / oa)
  200. clr.B = uint8(b * a / oa)
  201. clr.A = uint8(a)
  202. for i := 0; i < w; i++ {
  203. fogRGBA.SetRGBA(i, j, clr)
  204. }
  205. }
  206. g.fogImage = ebiten.NewImageFromImage(fogRGBA)
  207. return g
  208. }
  209. func (g *Game) Update() error {
  210. // Manipulate the player by the input.
  211. if ebiten.IsKeyPressed(ebiten.KeySpace) {
  212. g.player.MoveForward()
  213. }
  214. rotated := false
  215. if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
  216. g.player.RotateRight()
  217. rotated = true
  218. }
  219. if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
  220. g.player.RotateLeft()
  221. rotated = true
  222. }
  223. if !rotated {
  224. g.player.Stabilize()
  225. }
  226. return nil
  227. }
  228. func (g *Game) Draw(screen *ebiten.Image) {
  229. // Draw the ground image.
  230. screen.Fill(skyColor)
  231. g.updateGroundImage(g.groundImage)
  232. g.drawGroundImage(screen, g.groundImage)
  233. // Draw the message.
  234. tutorial := "Space: Move forward\nLeft/Right: Rotate"
  235. msg := fmt.Sprintf("TPS: %0.2f\nFPS: %0.2f\n%s", ebiten.ActualTPS(), ebiten.ActualFPS(), tutorial)
  236. ebitenutil.DebugPrint(screen, msg)
  237. }
  238. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
  239. return screenWidth, screenHeight
  240. }
  241. func main() {
  242. ebiten.SetWindowSize(screenWidth, screenHeight)
  243. ebiten.SetWindowTitle("Air Ship (Ebitengine Demo)")
  244. if err := ebiten.RunGame(NewGame()); err != nil {
  245. log.Fatal(err)
  246. }
  247. }