diff --git a/_docs/gen.go b/_docs/gen.go index 8d36e6ed5..8e1e6c2bb 100644 --- a/_docs/gen.go +++ b/_docs/gen.go @@ -176,29 +176,37 @@ func versions() string { return fmt.Sprintf("v%s (dev: v%s)", stableVersion, devVersion) } -var examples = []example{ - {"alphablending", 320, 240}, - {"audio", 320, 240}, - {"font", 320, 240}, - {"highdpi", 320, 240}, - {"hsv", 320, 240}, - {"hue", 320, 240}, - {"gamepad", 320, 240}, - {"infinitescroll", 320, 240}, - {"keyboard", 320, 240}, - {"life", 320, 240}, - {"masking", 320, 240}, - {"mosaic", 320, 240}, - {"noise", 320, 240}, - {"paint", 320, 240}, - {"perspective", 320, 240}, - {"piano", 320, 240}, - {"rotate", 320, 240}, - {"sprites", 320, 240}, - {"typewriter", 320, 240}, - {"2048", 210, 300}, - {"blocks", 256, 240}, -} +var ( + graphicsExamples = []example{ + {"alphablending", 320, 240}, + {"font", 320, 240}, + {"highdpi", 320, 240}, + {"hsv", 320, 240}, + {"hue", 320, 240}, + {"infinitescroll", 320, 240}, + {"life", 320, 240}, + {"masking", 320, 240}, + {"mosaic", 320, 240}, + {"noise", 320, 240}, + {"paint", 320, 240}, + {"perspective", 320, 240}, + {"rotate", 320, 240}, + {"sprites", 320, 240}, + } + inputExamples = []example{ + {"gamepad", 320, 240}, + {"keyboard", 320, 240}, + {"typewriter", 320, 240}, + } + audioExamples = []example{ + {"audio", 320, 240}, + {"piano", 320, 240}, + } + gameExamples = []example{ + {"2048", 210, 300}, + {"blocks", 256, 240}, + } +) func clear() error { if err := filepath.Walk("public", func(path string, info os.FileInfo, err error) error { @@ -253,11 +261,14 @@ func outputMain() error { } data := map[string]interface{}{ - "URL": url, - "Copyright": copyright, - "StableVersion": stableVersion, - "DevVersion": devVersion, - "Examples": examples, + "URL": url, + "Copyright": copyright, + "StableVersion": stableVersion, + "DevVersion": devVersion, + "GraphicsExamples": graphicsExamples, + "InputExamples": inputExamples, + "AudioExamples": audioExamples, + "GameExamples": gameExamples, } return t.Funcs(funcs).Execute(f, data) } @@ -355,6 +366,12 @@ func main() { if err := outputExampleResources(); err != nil { log.Fatal(err) } + + examples := []example{} + examples = append(examples, graphicsExamples...) + examples = append(examples, inputExamples...) + examples = append(examples, audioExamples...) + examples = append(examples, gameExamples...) for _, e := range examples { if err := outputExampleContent(&e); err != nil { log.Fatal(err) diff --git a/_docs/index.tmpl.html b/_docs/index.tmpl.html index fa38848a5..72ab4a728 100644 --- a/_docs/index.tmpl.html +++ b/_docs/index.tmpl.html @@ -60,13 +60,39 @@

Examples

+

Graphics

- {{range .Examples -}} + {{range .GraphicsExamples -}}
Ebiten example: {{.Name}}
{{- end}}
+

Input

+
+ {{range .InputExamples -}} +
+ Ebiten example: {{.Name}} +
+ {{- end}} +
+

Audio

+
+ {{range .AudioExamples -}} +
+ Ebiten example: {{.Name}} +
+ {{- end}} +
+

Game

+
+ {{range .GameExamples -}} +
+ Ebiten example: {{.Name}} +
+ {{- end}} +
+

The Gopher photographs by Chris Nokleberg are licensed under the Creative Commons 3.0 Attributions License.

Execute the examples

diff --git a/docs/examples/noise.html b/docs/examples/noise.html index cbf397e65..cb6a780aa 100644 --- a/docs/examples/noise.html +++ b/docs/examples/noise.html @@ -62,20 +62,23 @@ func (r *rand) next() uint32 { return r.w } -var randInstance = &rand{12345678, 4185243, 776511, 45411} +var theRand = &rand{12345678, 4185243, 776511, 45411} func update(screen *ebiten.Image) error { + // Generate the noise with random RGB values. const l = screenWidth * screenHeight for i := 0; i < l; i++ { - x := randInstance.next() + x := theRand.next() noiseImage.Pix[4*i] = uint8(x >> 24) noiseImage.Pix[4*i+1] = uint8(x >> 16) noiseImage.Pix[4*i+2] = uint8(x >> 8) noiseImage.Pix[4*i+3] = 0xff } + if ebiten.IsRunningSlowly() { return nil } + screen.ReplacePixels(noiseImage.Pix) ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %f", ebiten.CurrentFPS())) return nil diff --git a/docs/examples/paint.html b/docs/examples/paint.html index 78a2d4664..8980b1c49 100644 --- a/docs/examples/paint.html +++ b/docs/examples/paint.html @@ -53,46 +53,12 @@ var ( canvasImage *ebiten.Image ) -func paint(screen *ebiten.Image, x, y int) { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(x), float64(y)) - op.ColorM.Scale(1.0, 0.50, 0.125, 1.0) - theta := 2.0 * math.Pi * float64(count%ebiten.FPS) / ebiten.FPS - op.ColorM.RotateHue(theta) - canvasImage.DrawImage(brushImage, op) -} - -func update(screen *ebiten.Image) error { - drawn := false - mx, my := ebiten.CursorPosition() - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { - paint(screen, mx, my) - drawn = true - } - for _, t := range ebiten.Touches() { - x, y := t.Position() - paint(screen, x, y) - drawn = true - } - if drawn { - count++ - } - if ebiten.IsRunningSlowly() { - return nil - } - screen.DrawImage(canvasImage, nil) - - msg := fmt.Sprintf("(%d, %d)", mx, my) - for _, t := range ebiten.Touches() { - x, y := t.Position() - msg += fmt.Sprintf("\n(%d, %d) touch %d", x, y, t.ID()) - } - ebitenutil.DebugPrint(screen, msg) - return nil -} - -func main() { - const a0, a1, a2 = 0x40, 0xc0, 0xff +func init() { + const ( + a0 = 0x40 + a1 = 0xc0 + a2 = 0xff + ) pixels := []uint8{ a0, a1, a1, a0, a1, a2, a2, a1, @@ -107,7 +73,55 @@ func main() { canvasImage, _ = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest) canvasImage.Fill(color.White) +} +// paint draws the brush on the given canvas image at the position (x, y). +func paint(canvas *ebiten.Image, x, y int) { + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(x), float64(y)) + // Scale the color and rotate the hue so that colors vary on each frame. + op.ColorM.Scale(1.0, 0.50, 0.125, 1.0) + theta := 2.0 * math.Pi * float64(count%ebiten.FPS) / ebiten.FPS + op.ColorM.RotateHue(theta) + canvas.DrawImage(brushImage, op) +} + +func update(screen *ebiten.Image) error { + drawn := false + + // Paint the brush by mouse dragging + mx, my := ebiten.CursorPosition() + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + paint(canvasImage, mx, my) + drawn = true + } + + // Paint the brush by touches + for _, t := range ebiten.Touches() { + x, y := t.Position() + paint(canvasImage, x, y) + drawn = true + } + if drawn { + count++ + } + + if ebiten.IsRunningSlowly() { + return nil + } + + screen.DrawImage(canvasImage, nil) + + msg := fmt.Sprintf("(%d, %d)", mx, my) + for _, t := range ebiten.Touches() { + x, y := t.Position() + msg += fmt.Sprintf("\n(%d, %d) touch %d", x, y, t.ID()) + } + ebitenutil.DebugPrint(screen, msg) + return nil +} + +func main() { if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Paint (Ebiten Demo)"); err != nil { log.Fatal(err) } diff --git a/docs/examples/perspective.html b/docs/examples/perspective.html index 2eb9c6130..5c666c102 100644 --- a/docs/examples/perspective.html +++ b/docs/examples/perspective.html @@ -53,17 +53,25 @@ func update(screen *ebiten.Image) error { if ebiten.IsRunningSlowly() { return nil } + + // Split the image into horizontal lines and draw them with different scales. op := &ebiten.DrawImageOptions{} w, h := gophersImage.Size() for i := 0; i < h; i++ { op.GeoM.Reset() - width := w + i*3/4 - x := ((h - i) * 3 / 4) / 2 - op.GeoM.Scale(float64(width)/float64(w), 1) - op.GeoM.Translate(float64(x), float64(i)) - maxWidth := float64(w) + float64(h)*3/4 - op.GeoM.Translate(-maxWidth/2, -float64(h)/2) + + // Move the image's center to the upper-left corner. + op.GeoM.Translate(-float64(w)/2, -float64(h)/2) + + // Scale each lines and adjust the position. + lineW := w + i*3/4 + x := -float64(lineW) / float64(w) / 2 + op.GeoM.Scale(float64(lineW)/float64(w), 1) + op.GeoM.Translate(x, float64(i)) + + // Move the image's center to the screen's center. op.GeoM.Translate(screenWidth/2, screenHeight/2) + r := image.Rect(0, i, w, i+1) op.SourceRect = &r screen.DrawImage(gophersImage, op) diff --git a/docs/examples/piano.html b/docs/examples/piano.html index 61d82b386..83674efe6 100644 --- a/docs/examples/piano.html +++ b/docs/examples/piano.html @@ -47,10 +47,6 @@ import ( "github.com/hajimehoshi/ebiten/text" ) -const ( - arcadeFontSize = 8 -) - var ( arcadeFont font.Face ) @@ -72,7 +68,10 @@ func init() { log.Fatal(err) } - const dpi = 72 + const ( + arcadeFontSize = 8 + dpi = 72 + ) arcadeFont = truetype.NewFace(tt, &truetype.Options{ Size: arcadeFontSize, DPI: dpi, @@ -84,6 +83,7 @@ const ( screenWidth = 320 screenHeight = 240 sampleRate = 44100 + baseFreq = 220 ) var audioContext *audio.Context @@ -96,27 +96,21 @@ func init() { } } -var pcm = make([]float64, 4*sampleRate) - -const baseFreq = 220 - -func init() { +// pianoAt returns an i-th sample of piano with the given frequency. +func pianoAt(i int, freq float64) float64 { + // Create piano-like waves with multiple sin waves. amp := []float64{1.0, 0.8, 0.6, 0.4, 0.2} x := []float64{4.0, 2.0, 1.0, 0.5, 0.25} - for i := 0; i < len(pcm); i++ { - v := 0.0 - for j := 0; j < len(amp); j++ { - a := amp[j] * math.Exp(-5*float64(i)/(x[j]*sampleRate)) - v += a * math.Sin(2.0*math.Pi*float64(i)*baseFreq*float64(j+1)/sampleRate) - } - pcm[i] = v / 5.0 + v := 0.0 + for j := 0; j < len(amp); j++ { + // Decay + a := amp[j] * math.Exp(-5*float64(i)*freq/baseFreq/(x[j]*sampleRate)) + v += a * math.Sin(2.0*math.Pi*float64(i)*freq*float64(j+1)/sampleRate) } + return v / 5.0 } -var ( - noteCache = map[int][]byte{} -) - +// toBytes returns the 2ch little endian 16bit byte sequence with the given left/right sequence. func toBytes(l, r []int16) []byte { if len(l) != len(r) { panic("len(l) must equal to len(r)") @@ -131,62 +125,109 @@ func toBytes(l, r []int16) []byte { return b } -func addNote(freq float64, vol float64) { - // TODO: Call Close method of *audio.Player. - // However, this works without Close because Close is automatically called when GC - // collects a *audio.Player object. - f := int(freq) - if n, ok := noteCache[f]; ok { - p, _ := audio.NewPlayerFromBytes(audioContext, n) - p.Play() - return - } - length := len(pcm) * baseFreq / f - l := make([]int16, length) - r := make([]int16, length) - j := 0 - jj := 0 - for i := 0; i < len(l); i++ { - p := pcm[j] - l[i] = int16(p * vol * math.MaxInt16) - r[i] = l[i] - jj += f - j = jj / baseFreq - } - n := toBytes(l, r) - noteCache[f] = n - p, _ := audio.NewPlayerFromBytes(audioContext, n) - p.Play() - return -} - -var keys = []ebiten.Key{ - ebiten.KeyQ, - ebiten.KeyA, - ebiten.KeyW, - ebiten.KeyS, - ebiten.KeyD, - ebiten.KeyR, - ebiten.KeyF, - ebiten.KeyT, - ebiten.KeyG, - ebiten.KeyH, - ebiten.KeyU, - ebiten.KeyJ, - ebiten.KeyI, - ebiten.KeyK, - ebiten.KeyO, - ebiten.KeyL, -} - -var keyStates = map[ebiten.Key]int{} +var ( + pianoNoteSamples = map[int][]byte{} + pianoNoteSamplesInited = false + pianoNoteSamplesInitCh = make(chan struct{}) +) func init() { - for _, key := range keys { - keyStates[key] = 0 + // Initialize piano data. + // This takes a little long time (especially on browsers), + // so run this asynchronously and notice the progress. + go func() { + // Create a reference data and use this for other frequency. + const refFreq = 110 + length := 4 * sampleRate * baseFreq / refFreq + refData := make([]int16, length) + for i := 0; i < length; i++ { + refData[i] = int16(pianoAt(i, refFreq) * math.MaxInt16) + } + + for i := range keys { + freq := baseFreq * math.Exp2(float64(i-1)/12.0) + + // Clculate the wave data for the freq. + length := 4 * sampleRate * baseFreq / int(freq) + l := make([]int16, length) + r := make([]int16, length) + for i := 0; i < length; i++ { + idx := int(float64(i) * freq / refFreq) + if len(refData) <= idx { + break + } + l[i] = refData[idx] + } + copy(r, l) + n := toBytes(l, r) + pianoNoteSamples[int(freq)] = n + } + close(pianoNoteSamplesInitCh) + }() +} + +// playNote plays piano sound with the given frequency. +func playNote(freq float64) { + f := int(freq) + p, _ := audio.NewPlayerFromBytes(audioContext, pianoNoteSamples[f]) + p.Play() +} + +var ( + pianoImage *ebiten.Image +) + +func init() { + pianoImage, _ = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest) + + const ( + keyWidth = 24 + y = 48 + ) + + whiteKeys := []string{"A", "S", "D", "F", "G", "H", "J", "K", "L"} + for i, k := range whiteKeys { + x := i*keyWidth + 36 + height := 112 + ebitenutil.DrawRect(pianoImage, float64(x), float64(y), float64(keyWidth-1), float64(height), color.White) + text.Draw(pianoImage, k, arcadeFont, x+8, y+height-8, color.Black) + } + + blackKeys := []string{"Q", "W", "", "R", "T", "", "U", "I", "O"} + for i, k := range blackKeys { + if k == "" { + continue + } + x := i*keyWidth + 24 + height := 64 + ebitenutil.DrawRect(pianoImage, float64(x), float64(y), float64(keyWidth-1), float64(height), color.Black) + text.Draw(pianoImage, k, arcadeFont, x+8, y+height-8, color.White) } } +var ( + keys = []ebiten.Key{ + ebiten.KeyQ, + ebiten.KeyA, + ebiten.KeyW, + ebiten.KeyS, + ebiten.KeyD, + ebiten.KeyR, + ebiten.KeyF, + ebiten.KeyT, + ebiten.KeyG, + ebiten.KeyH, + ebiten.KeyU, + ebiten.KeyJ, + ebiten.KeyI, + ebiten.KeyK, + ebiten.KeyO, + ebiten.KeyL, + } + keyStates = map[ebiten.Key]int{} +) + +// updateInput updates the input state. func updateInput() { for _, key := range keys { if !ebiten.IsKeyPressed(key) { @@ -197,47 +238,33 @@ func updateInput() { } } -var ( - imagePiano *ebiten.Image -) - -func init() { - imagePiano, _ = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest) - whiteKeys := []string{"A", "S", "D", "F", "G", "H", "J", "K", "L"} - width := 24 - y := 48 - for i, k := range whiteKeys { - x := i*width + 36 - height := 112 - ebitenutil.DrawRect(imagePiano, float64(x), float64(y), float64(width-1), float64(height), color.White) - text.Draw(imagePiano, k, arcadeFont, x+8, y+height-8, color.Black) - } - - blackKeys := []string{"Q", "W", "", "R", "T", "", "U", "I", "O"} - for i, k := range blackKeys { - if k == "" { - continue - } - x := i*width + 24 - height := 64 - ebitenutil.DrawRect(imagePiano, float64(x), float64(y), float64(width-1), float64(height), color.Black) - text.Draw(imagePiano, k, arcadeFont, x+8, y+height-8, color.White) - } -} - func update(screen *ebiten.Image) error { - updateInput() - for i, key := range keys { - if keyStates[key] != 1 { - continue + // The piano data is still being initialized. + // Get the progress if available. + if !pianoNoteSamplesInited { + select { + case <-pianoNoteSamplesInitCh: + pianoNoteSamplesInited = true + default: } - addNote(220*math.Exp2(float64(i-1)/12.0), 1.0) } + + if pianoNoteSamplesInited { + updateInput() + for i, key := range keys { + if keyStates[key] != 1 { + continue + } + playNote(baseFreq * math.Exp2(float64(i-1)/12.0)) + } + } + if ebiten.IsRunningSlowly() { return nil } + screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff}) - screen.DrawImage(imagePiano, nil) + screen.DrawImage(pianoImage, nil) ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f", ebiten.CurrentFPS())) return nil diff --git a/docs/examples/rotate.html b/docs/examples/rotate.html index 0cda9113f..7293a9246 100644 --- a/docs/examples/rotate.html +++ b/docs/examples/rotate.html @@ -46,7 +46,7 @@ const ( ) var ( - count int + count = 0 gophersImage *ebiten.Image ) @@ -57,8 +57,17 @@ func update(screen *ebiten.Image) error { } w, h := gophersImage.Size() op := &ebiten.DrawImageOptions{} + + // Move the image's center to the screen's upper-left corner. + // This is a prepartion for rotating. When geometry matrices are applied, + // the origin point is the upper-left corner. op.GeoM.Translate(-float64(w)/2, -float64(h)/2) + + // Rotate the image. As a result, the anchor point of this rotate is + // the center of the image. op.GeoM.Rotate(float64(count%360) * 2 * math.Pi / 360) + + // Move the image to the screen's center. op.GeoM.Translate(screenWidth/2, screenHeight/2) screen.DrawImage(gophersImage, op) return nil diff --git a/docs/examples/sprites.html b/docs/examples/sprites.html index 19280d029..a528e1fb7 100644 --- a/docs/examples/sprites.html +++ b/docs/examples/sprites.html @@ -104,43 +104,7 @@ var ( op = &ebiten.DrawImageOptions{} ) -func update(screen *ebiten.Image) error { - if ebiten.IsKeyPressed(ebiten.KeyLeft) { - sprites.num -= 20 - if sprites.num < MinSprites { - sprites.num = MinSprites - } - } - if ebiten.IsKeyPressed(ebiten.KeyRight) { - sprites.num += 20 - if MaxSprites < sprites.num { - sprites.num = MaxSprites - } - } - sprites.Update() - - if ebiten.IsRunningSlowly() { - return nil - } - w, h := ebitenImage.Size() - for i := 0; i < sprites.num; i++ { - s := sprites.sprites[i] - op.GeoM.Reset() - op.GeoM.Translate(-float64(w)/2, -float64(h)/2) - op.GeoM.Rotate(2 * math.Pi * float64(s.angle) / maxAngle) - op.GeoM.Translate(float64(w)/2, float64(h)/2) - op.GeoM.Translate(float64(s.x), float64(s.y)) - screen.DrawImage(ebitenImage, op) - } - msg := fmt.Sprintf(`FPS: %0.2f -Num of sprites: %d -Press <- or -> to change the number of sprites`, ebiten.CurrentFPS(), sprites.num) - ebitenutil.DebugPrint(screen, msg) - return nil -} - -func main() { - var err error +func init() { img, _, err := ebitenutil.NewImageFromFile("_resources/images/ebiten.png", ebiten.FilterNearest) if err != nil { log.Fatal(err) @@ -165,6 +129,55 @@ func main() { angle: a, } } +} + +func update(screen *ebiten.Image) error { + // Decrease the nubmer of the sprites. + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + sprites.num -= 20 + if sprites.num < MinSprites { + sprites.num = MinSprites + } + } + + // Increase the nubmer of the sprites. + if ebiten.IsKeyPressed(ebiten.KeyRight) { + sprites.num += 20 + if MaxSprites < sprites.num { + sprites.num = MaxSprites + } + } + + sprites.Update() + + if ebiten.IsRunningSlowly() { + return nil + } + + // Draw each sprite. + // DrawImage can be called many many times, but in the implementation, + // the actual draw call to GPU is very few since these calls satisfy + // some conditions e.g. all the rendering sources and targets are same. + // For more detail, see: + // https://godoc.org/github.com/hajimehoshi/ebiten#Image.DrawImage + w, h := ebitenImage.Size() + for i := 0; i < sprites.num; i++ { + s := sprites.sprites[i] + op.GeoM.Reset() + op.GeoM.Translate(-float64(w)/2, -float64(h)/2) + op.GeoM.Rotate(2 * math.Pi * float64(s.angle) / maxAngle) + op.GeoM.Translate(float64(w)/2, float64(h)/2) + op.GeoM.Translate(float64(s.x), float64(s.y)) + screen.DrawImage(ebitenImage, op) + } + msg := fmt.Sprintf(`FPS: %0.2f +Num of sprites: %d +Press <- or -> to change the number of sprites`, ebiten.CurrentFPS(), sprites.num) + ebitenutil.DebugPrint(screen, msg) + return nil +} + +func main() { if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Sprites (Ebiten Demo)"); err != nil { log.Fatal(err) } diff --git a/docs/examples/typewriter.html b/docs/examples/typewriter.html index c8e1ac91e..c8741af21 100644 --- a/docs/examples/typewriter.html +++ b/docs/examples/typewriter.html @@ -46,14 +46,23 @@ var ( ) func update(screen *ebiten.Image) error { + // Add a string from InputChars, that returns string input by users. + // Note that InputChars result changes every frame, so you need to call this + // every frame. text += string(ebiten.InputChars()) + + // Adjust the string to be at most 10 lines. ss := strings.Split(text, "\n") if len(ss) > 10 { text = strings.Join(ss[len(ss)-10:], "\n") } + + // If the enter key is pressed, add a line break. if ebiten.IsKeyPressed(ebiten.KeyEnter) && !strings.HasSuffix(text, "\n") { text += "\n" } + + // If the backspace key is pressed, remove one character. bsPressed := ebiten.IsKeyPressed(ebiten.KeyBackspace) if !bsPrevPressed && bsPressed { if len(text) >= 1 { @@ -68,6 +77,7 @@ func update(screen *ebiten.Image) error { return nil } + // Blink the cursor. t := text if counter%60 < 30 { t += "_" diff --git a/docs/index.html b/docs/index.html index f4e6d59e9..ef53dd9ef 100644 --- a/docs/index.html +++ b/docs/index.html @@ -60,11 +60,10 @@

Examples

+

Graphics

Ebiten example: alphablending -
- Ebiten example: audio
Ebiten example: font
@@ -73,12 +72,8 @@ Ebiten example: hsv
Ebiten example: hue -
- Ebiten example: gamepad
Ebiten example: infinitescroll -
- Ebiten example: keyboard
Ebiten example: life
@@ -91,20 +86,39 @@ Ebiten example: paint
Ebiten example: perspective -
- Ebiten example: piano
Ebiten example: rotate
Ebiten example: sprites +
+
+

Input

+
+
+ Ebiten example: gamepad +
+ Ebiten example: keyboard
Ebiten example: typewriter +
+
+

Audio

+
+
+ Ebiten example: audio
+ Ebiten example: piano +
+
+

Game

+
+
Ebiten example: 2048
Ebiten example: blocks
+

The Gopher photographs by Chris Nokleberg are licensed under the Creative Commons 3.0 Attributions License.

Execute the examples