mirror of
				https://github.com/hajimehoshi/ebiten.git
				synced 2025-10-31 19:52:47 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			449 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			449 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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.
 | |
| 
 | |
| //go:build example
 | |
| // +build example
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"image"
 | |
| 	"image/color"
 | |
| 	_ "image/png"
 | |
| 	"log"
 | |
| 	"math"
 | |
| 	"math/rand"
 | |
| 	"time"
 | |
| 
 | |
| 	"golang.org/x/image/font"
 | |
| 	"golang.org/x/image/font/opentype"
 | |
| 
 | |
| 	"github.com/hajimehoshi/ebiten/v2"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/audio"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/audio/vorbis"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/audio/wav"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
 | |
| 	raudio "github.com/hajimehoshi/ebiten/v2/examples/resources/audio"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
 | |
| 	resources "github.com/hajimehoshi/ebiten/v2/examples/resources/images/flappy"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/inpututil"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/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
 | |
| 	titleFontSize    = fontSize * 1.5
 | |
| 	fontSize         = 24
 | |
| 	smallFontSize    = fontSize / 2
 | |
| 	pipeWidth        = tileSize * 2
 | |
| 	pipeStartOffsetX = 8
 | |
| 	pipeIntervalX    = 8
 | |
| 	pipeGapY         = 5
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	gopherImage     *ebiten.Image
 | |
| 	tilesImage      *ebiten.Image
 | |
| 	titleArcadeFont font.Face
 | |
| 	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)
 | |
| 
 | |
| 	img, _, err = image.Decode(bytes.NewReader(resources.Tiles_png))
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	tilesImage = ebiten.NewImageFromImage(img)
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	tt, err := opentype.Parse(fonts.PressStart2P_ttf)
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	const dpi = 72
 | |
| 	titleArcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
 | |
| 		Size:    titleFontSize,
 | |
| 		DPI:     dpi,
 | |
| 		Hinting: font.HintingFull,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	arcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
 | |
| 		Size:    fontSize,
 | |
| 		DPI:     dpi,
 | |
| 		Hinting: font.HintingFull,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	smallArcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
 | |
| 		Size:    smallFontSize,
 | |
| 		DPI:     dpi,
 | |
| 		Hinting: font.HintingFull,
 | |
| 	})
 | |
| 	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
 | |
| 
 | |
| 	touchIDs   []ebiten.TouchID
 | |
| 	gamepadIDs []ebiten.GamepadID
 | |
| 
 | |
| 	audioContext *audio.Context
 | |
| 	jumpPlayer   *audio.Player
 | |
| 	hitPlayer    *audio.Player
 | |
| }
 | |
| 
 | |
| 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
 | |
| 	}
 | |
| 
 | |
| 	if g.audioContext == nil {
 | |
| 		g.audioContext = audio.NewContext(48000)
 | |
| 	}
 | |
| 
 | |
| 	jumpD, err := vorbis.DecodeWithoutResampling(bytes.NewReader(raudio.Jump_ogg))
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	g.jumpPlayer, err = g.audioContext.NewPlayer(jumpD)
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	jabD, err := wav.DecodeWithoutResampling(bytes.NewReader(raudio.Jab_wav))
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	g.hitPlayer, err = g.audioContext.NewPlayer(jabD)
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (g *Game) isKeyJustPressed() bool {
 | |
| 	if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
 | |
| 		return true
 | |
| 	}
 | |
| 	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
 | |
| 		return true
 | |
| 	}
 | |
| 	g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs[:0])
 | |
| 	if len(g.touchIDs) > 0 {
 | |
| 		return true
 | |
| 	}
 | |
| 	g.gamepadIDs = ebiten.AppendGamepadIDs(g.gamepadIDs[:0])
 | |
| 	for _, g := range g.gamepadIDs {
 | |
| 		if ebiten.IsStandardGamepadLayoutAvailable(g) {
 | |
| 			if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightBottom) {
 | |
| 				return true
 | |
| 			}
 | |
| 			if inpututil.IsStandardGamepadButtonJustPressed(g, ebiten.StandardGamepadButtonRightRight) {
 | |
| 				return true
 | |
| 			}
 | |
| 		} else {
 | |
| 			// The button 0/1 might not be A/B buttons.
 | |
| 			if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton0) {
 | |
| 				return true
 | |
| 			}
 | |
| 			if inpututil.IsGamepadButtonJustPressed(g, ebiten.GamepadButton1) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
 | |
| 	return screenWidth, screenHeight
 | |
| }
 | |
| 
 | |
| func (g *Game) Update() error {
 | |
| 	switch g.mode {
 | |
| 	case ModeTitle:
 | |
| 		if g.isKeyJustPressed() {
 | |
| 			g.mode = ModeGame
 | |
| 		}
 | |
| 	case ModeGame:
 | |
| 		g.x16 += 32
 | |
| 		g.cameraX += 2
 | |
| 		if g.isKeyJustPressed() {
 | |
| 			g.vy16 = -96
 | |
| 			if err := g.jumpPlayer.Rewind(); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			g.jumpPlayer.Play()
 | |
| 		}
 | |
| 		g.y16 += g.vy16
 | |
| 
 | |
| 		// Gravity
 | |
| 		g.vy16 += 4
 | |
| 		if g.vy16 > 96 {
 | |
| 			g.vy16 = 96
 | |
| 		}
 | |
| 
 | |
| 		if g.hit() {
 | |
| 			if err := g.hitPlayer.Rewind(); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			g.hitPlayer.Play()
 | |
| 			g.mode = ModeGameOver
 | |
| 			g.gameoverCount = 30
 | |
| 		}
 | |
| 	case ModeGameOver:
 | |
| 		if g.gameoverCount > 0 {
 | |
| 			g.gameoverCount--
 | |
| 		}
 | |
| 		if g.gameoverCount == 0 && g.isKeyJustPressed() {
 | |
| 			g.init()
 | |
| 			g.mode = ModeTitle
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (g *Game) Draw(screen *ebiten.Image) {
 | |
| 	screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff})
 | |
| 	g.drawTiles(screen)
 | |
| 	if g.mode != ModeTitle {
 | |
| 		g.drawGopher(screen)
 | |
| 	}
 | |
| 	var titleTexts []string
 | |
| 	var texts []string
 | |
| 	switch g.mode {
 | |
| 	case ModeTitle:
 | |
| 		titleTexts = []string{"FLAPPY GOPHER"}
 | |
| 		texts = []string{"", "", "", "", "", "", "", "PRESS SPACE KEY", "", "OR A/B BUTTON", "", "OR TOUCH SCREEN"}
 | |
| 	case ModeGameOver:
 | |
| 		texts = []string{"", "GAME OVER!"}
 | |
| 	}
 | |
| 	for i, l := range titleTexts {
 | |
| 		x := (screenWidth - len(l)*titleFontSize) / 2
 | |
| 		text.Draw(screen, l, titleArcadeFont, x, (i+4)*titleFontSize, color.White)
 | |
| 	}
 | |
| 	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.ActualTPS()))
 | |
| }
 | |
| 
 | |
| 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() {
 | |
| 	ebiten.SetWindowSize(screenWidth, screenHeight)
 | |
| 	ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)")
 | |
| 	if err := ebiten.RunGame(NewGame()); err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| }
 | 
