123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- // 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.
- package main
- import (
- "bytes"
- _ "embed"
- "flag"
- "fmt"
- "image"
- "image/color"
- _ "image/png"
- "log"
- "math"
- "math/rand/v2"
- "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/v2"
- )
- var flagCRT = flag.Bool("crt", false, "enable the CRT effect")
- //go:embed crt.go
- var crtGo []byte
- 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
- arcadeFaceSource *text.GoTextFaceSource
- )
- 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() {
- s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.PressStart2P_ttf))
- if err != nil {
- log.Fatal(err)
- }
- arcadeFaceSource = s
- }
- 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(crt bool) ebiten.Game {
- g := &Game{}
- g.init()
- if crt {
- return &GameWithCRTEffect{Game: g}
- }
- 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.DecodeF32(bytes.NewReader(raudio.Jump_ogg))
- if err != nil {
- log.Fatal(err)
- }
- g.jumpPlayer, err = g.audioContext.NewPlayerF32(jumpD)
- if err != nil {
- log.Fatal(err)
- }
- jabD, err := wav.DecodeF32(bytes.NewReader(raudio.Jab_wav))
- if err != nil {
- log.Fatal(err)
- }
- g.hitPlayer, err = g.audioContext.NewPlayerF32(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 = "FLAPPY GOPHER"
- texts = "\n\n\n\n\n\nPRESS SPACE KEY\n\nOR A/B BUTTON\n\nOR TOUCH SCREEN"
- case ModeGameOver:
- texts = "\nGAME OVER!"
- }
- op := &text.DrawOptions{}
- op.GeoM.Translate(screenWidth/2, 3*titleFontSize)
- op.ColorScale.ScaleWithColor(color.White)
- op.LineSpacing = titleFontSize
- op.PrimaryAlign = text.AlignCenter
- text.Draw(screen, titleTexts, &text.GoTextFace{
- Source: arcadeFaceSource,
- Size: titleFontSize,
- }, op)
- op = &text.DrawOptions{}
- op.GeoM.Translate(screenWidth/2, 3*titleFontSize)
- op.ColorScale.ScaleWithColor(color.White)
- op.LineSpacing = fontSize
- op.PrimaryAlign = text.AlignCenter
- text.Draw(screen, texts, &text.GoTextFace{
- Source: arcadeFaceSource,
- Size: fontSize,
- }, op)
- if g.mode == ModeTitle {
- const msg = "Go Gopher by Renee French is\nlicenced under CC BY 3.0."
- op := &text.DrawOptions{}
- op.GeoM.Translate(screenWidth/2, screenHeight-smallFontSize/2)
- op.ColorScale.ScaleWithColor(color.White)
- op.LineSpacing = smallFontSize
- op.PrimaryAlign = text.AlignCenter
- op.SecondaryAlign = text.AlignEnd
- text.Draw(screen, msg, &text.GoTextFace{
- Source: arcadeFaceSource,
- Size: smallFontSize,
- }, op)
- }
- op = &text.DrawOptions{}
- op.GeoM.Translate(screenWidth, 0)
- op.ColorScale.ScaleWithColor(color.White)
- op.LineSpacing = fontSize
- op.PrimaryAlign = text.AlignEnd
- text.Draw(screen, fmt.Sprintf("%04d", g.score()), &text.GoTextFace{
- Source: arcadeFaceSource,
- Size: fontSize,
- }, op)
- 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.Bounds().Dx(), gopherImage.Bounds().Dy()
- 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.Bounds().Dx(), gopherImage.Bounds().Dy()
- 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)
- }
- type GameWithCRTEffect struct {
- ebiten.Game
- crtShader *ebiten.Shader
- }
- func (g *GameWithCRTEffect) DrawFinalScreen(screen ebiten.FinalScreen, offscreen *ebiten.Image, geoM ebiten.GeoM) {
- if g.crtShader == nil {
- s, err := ebiten.NewShader(crtGo)
- if err != nil {
- panic(fmt.Sprintf("flappy: failed to compiled the CRT shader: %v", err))
- }
- g.crtShader = s
- }
- os := offscreen.Bounds().Size()
- op := &ebiten.DrawRectShaderOptions{}
- op.Images[0] = offscreen
- op.GeoM = geoM
- screen.DrawRectShader(os.X, os.Y, g.crtShader, op)
- }
- func main() {
- flag.Parse()
- ebiten.SetWindowSize(screenWidth, screenHeight)
- ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)")
- if err := ebiten.RunGame(NewGame(*flagCRT)); err != nil {
- panic(err)
- }
- }
|