mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
75721e6fc1
This change forces Ebiten apps on browsers 'fullscreen' mode. After this change, 'scale' value is no longer used on browsers. Note that this breaks backward compatibility, but as long as the game works in an iframe, this should not be problematic. Fixes #960
398 lines
8.9 KiB
Go
398 lines
8.9 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.
|
|
|
|
// +build example jsgo
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
_ "image/png"
|
|
"log"
|
|
"math"
|
|
"math/rand"
|
|
"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()
|
|
if err := ebiten.Run(g.Update, screenWidth, screenHeight, 1, "Flappy Gopher (Ebiten Demo)"); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|