From 53ba73a804141e5d5a53fd05d46173cd22426b21 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst Date: Sun, 23 Jun 2019 12:09:44 +0100 Subject: [PATCH] Add ebiten example --- Makefile | 6 + README.md | 4 + ebiten/LICENSE | 13 ++ ebiten/index.html | 16 ++ ebiten/main.go | 389 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 + go.sum | 39 +++++ 7 files changed, 475 insertions(+) create mode 100644 ebiten/LICENSE create mode 100644 ebiten/index.html create mode 100644 ebiten/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/Makefile b/Makefile index 999cd41..ec57571 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,12 @@ tinygo-canvas: clean cp ./canvas/index.html ./html/index.html cp ./canvas/main.go ./html/main.go +.PHONY: ebiten +ebiten: clean + GO111MODULE=on GOOS=js GOARCH=wasm go build -o ./html/ebiten.wasm ./ebiten/main.go + cp ./ebiten/index.html ./html/index.html + cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js + test: clean GOOS=js GOARCH=wasm go test -c -o ./html/test.wasm ./test/ diff --git a/README.md b/README.md index e808502..200a82d 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,7 @@ send a HTTP request, parse the result and write it to the DOM. An updated version of the [repulsion](https://stdiopt.github.io/gowasm-experiments/repulsion) demo by [Luis Figuerido](https://github.com/stdiopt) usin Go 1.12. +### Ebiten + +A short demo of using the [Ebiten game engine](https://github.com/hajimehoshi/ebiten) +to create a WebGL based flappy bird clone. Copied from the Ebiten examples. diff --git a/ebiten/LICENSE b/ebiten/LICENSE new file mode 100644 index 0000000..8bae162 --- /dev/null +++ b/ebiten/LICENSE @@ -0,0 +1,13 @@ +// Copyright 2018 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. diff --git a/ebiten/index.html b/ebiten/index.html new file mode 100644 index 0000000..d7c4a4a --- /dev/null +++ b/ebiten/index.html @@ -0,0 +1,16 @@ + + + diff --git a/ebiten/main.go b/ebiten/main.go new file mode 100644 index 0000000..c402f1c --- /dev/null +++ b/ebiten/main.go @@ -0,0 +1,389 @@ +// +build js,wasm + +package main + +import ( + "bytes" + "fmt" + "image" + "image/color" + _ "image/png" + "log" + "math" + "math/rand" + "runtime" + "time" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/audio" + "github.com/hajimehoshi/ebiten/audio/vorbis" + "github.com/hajimehoshi/ebiten/audio/wav" + "github.com/hajimehoshi/ebiten/ebitenutil" + raudio "github.com/hajimehoshi/ebiten/examples/resources/audio" + "github.com/hajimehoshi/ebiten/examples/resources/fonts" + resources "github.com/hajimehoshi/ebiten/examples/resources/images/flappy" + "github.com/hajimehoshi/ebiten/inpututil" + "github.com/hajimehoshi/ebiten/text" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func floorDiv(x, y int) int { + d := x / y + if d*y == x || x >= 0 { + return d + } + return d - 1 +} + +func floorMod(x, y int) int { + return x - floorDiv(x, y)*y +} + +const ( + screenWidth = 640 + screenHeight = 480 + tileSize = 32 + fontSize = 32 + smallFontSize = fontSize / 2 + pipeWidth = tileSize * 2 + pipeStartOffsetX = 8 + pipeIntervalX = 8 + pipeGapY = 5 +) + +var ( + gopherImage *ebiten.Image + tilesImage *ebiten.Image + arcadeFont font.Face + smallArcadeFont font.Face +) + +func init() { + img, _, err := image.Decode(bytes.NewReader(resources.Gopher_png)) + if err != nil { + log.Fatal(err) + } + gopherImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault) + + img, _, err = image.Decode(bytes.NewReader(resources.Tiles_png)) + if err != nil { + log.Fatal(err) + } + tilesImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault) +} + +func init() { + tt, err := truetype.Parse(fonts.ArcadeN_ttf) + if err != nil { + log.Fatal(err) + } + const dpi = 72 + arcadeFont = truetype.NewFace(tt, &truetype.Options{ + Size: fontSize, + DPI: dpi, + Hinting: font.HintingFull, + }) + smallArcadeFont = truetype.NewFace(tt, &truetype.Options{ + Size: smallFontSize, + DPI: dpi, + Hinting: font.HintingFull, + }) +} + +var ( + audioContext *audio.Context + jumpPlayer *audio.Player + hitPlayer *audio.Player +) + +func init() { + audioContext, _ = audio.NewContext(44100) + + jumpD, err := vorbis.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Jump_ogg)) + if err != nil { + log.Fatal(err) + } + jumpPlayer, err = audio.NewPlayer(audioContext, jumpD) + if err != nil { + log.Fatal(err) + } + + jabD, err := wav.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Jab_wav)) + if err != nil { + log.Fatal(err) + } + hitPlayer, err = audio.NewPlayer(audioContext, jabD) + if err != nil { + log.Fatal(err) + } +} + +type Mode int + +const ( + ModeTitle Mode = iota + ModeGame + ModeGameOver +) + +type Game struct { + mode Mode + + // The gopher's position + x16 int + y16 int + vy16 int + + // Camera + cameraX int + cameraY int + + // Pipes + pipeTileYs []int + + gameoverCount int +} + +func NewGame() *Game { + g := &Game{} + g.init() + return g +} + +func (g *Game) init() { + g.x16 = 0 + g.y16 = 100 * 16 + g.cameraX = -240 + g.cameraY = 0 + g.pipeTileYs = make([]int, 256) + for i := range g.pipeTileYs { + g.pipeTileYs[i] = rand.Intn(6) + 2 + } +} + +func jump() bool { + if inpututil.IsKeyJustPressed(ebiten.KeySpace) { + return true + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + return true + } + if len(inpututil.JustPressedTouchIDs()) > 0 { + return true + } + return false +} + +func (g *Game) Update(screen *ebiten.Image) error { + switch g.mode { + case ModeTitle: + if jump() { + g.mode = ModeGame + } + case ModeGame: + g.x16 += 32 + g.cameraX += 2 + if jump() { + g.vy16 = -96 + jumpPlayer.Rewind() + jumpPlayer.Play() + } + g.y16 += g.vy16 + + // Gravity + g.vy16 += 4 + if g.vy16 > 96 { + g.vy16 = 96 + } + + if g.hit() { + hitPlayer.Rewind() + hitPlayer.Play() + g.mode = ModeGameOver + g.gameoverCount = 30 + } + case ModeGameOver: + if g.gameoverCount > 0 { + g.gameoverCount-- + } + if g.gameoverCount == 0 && jump() { + g.init() + g.mode = ModeTitle + } + } + + if ebiten.IsDrawingSkipped() { + return nil + } + + screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff}) + g.drawTiles(screen) + if g.mode != ModeTitle { + g.drawGopher(screen) + } + var texts []string + switch g.mode { + case ModeTitle: + texts = []string{"FLAPPY GOPHER", "", "", "", "", "PRESS SPACE KEY", "", "OR TOUCH SCREEN"} + case ModeGameOver: + texts = []string{"", "GAMEOVER!"} + } + for i, l := range texts { + x := (screenWidth - len(l)*fontSize) / 2 + text.Draw(screen, l, arcadeFont, x, (i+4)*fontSize, color.White) + } + + if g.mode == ModeTitle { + msg := []string{ + "Go Gopher by Renee French is", + "licenced under CC BY 3.0.", + } + for i, l := range msg { + x := (screenWidth - len(l)*smallFontSize) / 2 + text.Draw(screen, l, smallArcadeFont, x, screenHeight-4+(i-1)*smallFontSize, color.White) + } + } + + scoreStr := fmt.Sprintf("%04d", g.score()) + text.Draw(screen, scoreStr, arcadeFont, screenWidth-len(scoreStr)*fontSize, fontSize, color.White) + ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS())) + return nil +} + +func (g *Game) pipeAt(tileX int) (tileY int, ok bool) { + if (tileX - pipeStartOffsetX) <= 0 { + return 0, false + } + if floorMod(tileX-pipeStartOffsetX, pipeIntervalX) != 0 { + return 0, false + } + idx := floorDiv(tileX-pipeStartOffsetX, pipeIntervalX) + return g.pipeTileYs[idx%len(g.pipeTileYs)], true +} + +func (g *Game) score() int { + x := floorDiv(g.x16, 16) / tileSize + if (x - pipeStartOffsetX) <= 0 { + return 0 + } + return floorDiv(x-pipeStartOffsetX, pipeIntervalX) +} + +func (g *Game) hit() bool { + if g.mode != ModeGame { + return false + } + const ( + gopherWidth = 30 + gopherHeight = 60 + ) + w, h := gopherImage.Size() + x0 := floorDiv(g.x16, 16) + (w-gopherWidth)/2 + y0 := floorDiv(g.y16, 16) + (h-gopherHeight)/2 + x1 := x0 + gopherWidth + y1 := y0 + gopherHeight + if y0 < -tileSize*4 { + return true + } + if y1 >= screenHeight-tileSize { + return true + } + xMin := floorDiv(x0-pipeWidth, tileSize) + xMax := floorDiv(x0+gopherWidth, tileSize) + for x := xMin; x <= xMax; x++ { + y, ok := g.pipeAt(x) + if !ok { + continue + } + if x0 >= x*tileSize+pipeWidth { + continue + } + if x1 < x*tileSize { + continue + } + if y0 < y*tileSize { + return true + } + if y1 >= (y+pipeGapY)*tileSize { + return true + } + } + return false +} + +func (g *Game) drawTiles(screen *ebiten.Image) { + const ( + nx = screenWidth / tileSize + ny = screenHeight / tileSize + pipeTileSrcX = 128 + pipeTileSrcY = 192 + ) + + op := &ebiten.DrawImageOptions{} + for i := -2; i < nx+1; i++ { + // ground + op.GeoM.Reset() + op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), + float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize))) + screen.DrawImage(tilesImage.SubImage(image.Rect(0, 0, tileSize, tileSize)).(*ebiten.Image), op) + + // pipe + if tileY, ok := g.pipeAt(floorDiv(g.cameraX, tileSize) + i); ok { + for j := 0; j < tileY; j++ { + op.GeoM.Reset() + op.GeoM.Scale(1, -1) + op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), + float64(j*tileSize-floorMod(g.cameraY, tileSize))) + op.GeoM.Translate(0, tileSize) + var r image.Rectangle + if j == tileY-1 { + r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize) + } else { + r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2) + } + screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op) + } + for j := tileY + pipeGapY; j < screenHeight/tileSize-1; j++ { + op.GeoM.Reset() + op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)), + float64(j*tileSize-floorMod(g.cameraY, tileSize))) + var r image.Rectangle + if j == tileY+pipeGapY { + r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize) + } else { + r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize) + } + screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op) + } + } + } +} + +func (g *Game) drawGopher(screen *ebiten.Image) { + op := &ebiten.DrawImageOptions{} + w, h := gopherImage.Size() + op.GeoM.Translate(-float64(w)/2.0, -float64(h)/2.0) + op.GeoM.Rotate(float64(g.vy16) / 96.0 * math.Pi / 6) + op.GeoM.Translate(float64(w)/2.0, float64(h)/2.0) + op.GeoM.Translate(float64(g.x16/16.0)-float64(g.cameraX), float64(g.y16/16.0)-float64(g.cameraY)) + op.Filter = ebiten.FilterLinear + screen.DrawImage(gopherImage, op) +} + +func main() { + g := NewGame() + // On browsers, let's use fullscreen so that this is playable on any browsers. + // It is planned to ignore the given 'scale' apply fullscreen automatically on browsers (#571). + if runtime.GOARCH == "js" || runtime.GOOS == "js" { + ebiten.SetFullscreen(true) + } + if err := ebiten.Run(g.Update, screenWidth, screenHeight, 1, "Flappy Gopher (Ebiten Demo)"); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..983774b --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/johanbrandhorst/wasm-experiments + +go 1.12 + +require ( + github.com/hajimehoshi/ebiten v1.9.3 // indirect + golang.org/x/image v0.0.0-20190618124811-92942e4437e2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..14d88ae --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4= +github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= +github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ= +github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= +github.com/hajimehoshi/bitmapfont v1.1.1/go.mod h1:Hamfxgney7tDSmVOSDh2AWzoDH70OaC+P24zc02Gum4= +github.com/hajimehoshi/ebiten v1.9.3 h1:YijWGMBwH2XA1ZytUQFy33UDHeCSS6d4JZKH1wq38O0= +github.com/hajimehoshi/ebiten v1.9.3/go.mod h1:XxiJ4Eltvb1KmcD0i6F81eIB1asJhK47y5DC+FPkyso= +github.com/hajimehoshi/go-mp3 v0.2.0/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw= +github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04= +github.com/hajimehoshi/oto v0.3.3 h1:Wi7VVtxe9sF2rbDBIJtVXnpFWhRfK57hw0JY7tR2qXM= +github.com/hajimehoshi/oto v0.3.3/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM= +github.com/jakecoffman/cp v0.1.0/go.mod h1:a3xPx9N8RyFAACD644t2dj/nK4SuLg1v+jL61m2yVo4= +github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0= +github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= +github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U= +github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20180926015637-991ec62608f3/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190618124811-92942e4437e2 h1:fqF3kMQ0tlBEpnfxavzOrjqW5gokBwllwOABYxETOMA= +golang.org/x/image v0.0.0-20190618124811-92942e4437e2/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190127143845-a42111704963/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190202235157-7414d4c1f71c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=